本文档描述外部程序集成 cyber 时使用的两组 API。
| 功能组 | 传输 | 语义 |
|---|---|---|
| Application WebSocket | 双向、长连接、二进制 protobuf | Session/Turn 生命周期和实时事件流 |
| ConnectRPC | unary 请求/响应 | 会话历史、扫描、配置、Agent、系统状态和原始 Artifact 归档同步 |
第三方语言的 protobuf 生成和接入流程见 integration.md。Go 可运行示例见 examples/acp/README.md。字段级参考以 proto 源码为准,需要时按 api/README.md 的步骤生成文档。
实时对话必须使用 Application WebSocket:
- 创建或打开 session
- 发送自然语言输入
- 接收
message_delta、工具事件和turn_ended - 取消正在运行的 Turn
- 持续订阅及断线续传
管理查询使用 ConnectRPC:
- 查询 session 列表和持久化历史
- 重置或删除 session
- 提交、查询和取消扫描
- 查询或更新配置
- 查询 Agent、系统状态和同步原始 Artifact 归档;规范化资产视图在浏览器生成
SessionService/ListEvents 只返回已持久化历史,不替代 WebSocket WatchEvents。
GET /api/aop/application/ws HTTP/1.1
Authorization: Bearer <access-key>
Upgrade: websocket| 服务地址 | WebSocket 地址 |
|---|---|
http://host:8080 |
ws://host:8080/api/aop/application/ws |
https://host |
wss://host/api/aop/application/ws |
- 外部 client 使用 Bearer token。
- 浏览器登录后也可以使用
cyber_sessioncookie。 - 鉴权失败时 upgrade 返回 HTTP 401。
aiscan web未指定--token时会自动生成 access key,而不是关闭鉴权。- Application Endpoint 不需要握手消息。首个 envelope 如果包含
AgentHello,会返回WRONG_ENDPOINT。
每个 WebSocket message 必须是 BinaryMessage,内容为一个序列化的 aop.Envelope。文本 JSON frame 不属于 cyber Application WebSocket wire format。
message Envelope {
string id = 1;
string reply_to = 2;
string delivery_cursor = 3;
google.protobuf.Any payload = 4;
}| 字段 | 发送请求 | 接收响应/事件 |
|---|---|---|
id |
client 生成的唯一请求 ID | server 生成的消息 ID |
reply_to |
通常为空 | 原请求 envelope 的 id |
delivery_cursor |
空 | 持久化事件的恢复 cursor;瞬时 delta 为空 |
payload |
Any<aop.ProtocolMessage> |
对应 namespace 的 protobuf 消息 |
Go 中使用:
envelope, err := aop.Wrap(id, "", message)
message, err := aop.Unwrap(envelope)其他语言使用 protobuf Any.pack / unpack。不要自行添加 namespace 字段;消息类型由 Any.type_url 决定。
client 至少维护两张表:
pending[requestEnvelopeID] -> 单次响应等待者
subscriptions[watchEnvelopeID] -> 长期事件消费者
接收 envelope 时:
- 使用
reply_to查找订阅。 - 如果 payload 是
ProtocolMessage.event,交给订阅消费者。 - 否则使用
reply_to完成对应 pending request。
事件和 RunTurnResponse 可能并发到达,不能假设回执一定先于事件。
真实实现可参考 examples/acp/client/client.go:
Dial:转换 ws/wss URL,设置 Bearer header,启动readLoopreadLoop:解码 Envelope,通过reply_to分发响应和事件call:创建 envelope ID,发送请求并等待单次响应Watch:以 watch envelope ID 建立长期事件 channel
| 字段 | 必填 | 说明 |
|---|---|---|
session_id |
否 | client 指定的 session ID;空时由 server 生成 |
node_id |
是 | 在线且能够处理 chat 的 agent node ID |
title |
否 | session 标题 |
parent_session_id |
否 | 子会话的父 session |
parent_tool_call_id |
否 | 创建子会话的 tool call |
extensions |
否 | namespace 自有的 protobuf Any 扩展 |
最小请求:
OpenSessionRequest{node_id: "local"}
oneof outcome:
accepted: Session{id,state,node_id,title}
rejected: Rejection{code,message,retryable}
常见 rejection:
| code | 原因 |
|---|---|
INVALID_ARGUMENT |
缺少 node_id |
UNAVAILABLE |
node 不在线或无法打开 chat session |
ALREADY_EXISTS |
指定的 session ID 已绑定到其他 node,或 envelope ID 冲突 |
NOT_FOUND |
扩展引用的资源不存在 |
成功后必须使用 accepted.id,不能假设它等于请求中的 session_id。
真实示例:Client.OpenSession(ctx, nodeID, title)。
| 字段 | 必填 | 说明 |
|---|---|---|
session_id |
是 | 要订阅的 session |
after_cursor |
否 | exclusive cursor;空表示从当前可用历史开始重放 |
WatchEvents 没有单独的 response message。服务端持续发送:
Envelope{
reply_to: "<watch-envelope-id>"
delivery_cursor: "<cursor-or-empty>"
payload: Any<ProtocolMessage{event: Event{...}}>
}
推荐顺序:OpenSession 成功后先 WatchEvents,再 RunTurn。这样可以收到用户消息和最早的瞬时 delta。
真实示例:Client.Watch(sessionID, afterCursor)。
当前示例 client 演示最小实时订阅;生产 client 还应保留收到的非空 delivery_cursor,并在重连后重新建立 WatchEvents。
| 字段 | 必填 | 说明 |
|---|---|---|
session_id |
是 | OpenSession 返回的 session ID |
turn_id |
否 | client 指定的 Turn ID;空时由 server 生成 |
input |
通常是 | 用户 Message;continue_session=true 时允许没有内容 |
continue_session |
否 | 继续已有 agent 上下文,不发布新的用户消息 |
max_turns |
否 | 本次执行允许的最大内部 Turn 数 |
extensions |
否 | Cyber 或其他 namespace 的请求扩展 |
普通自然语言输入:
RunTurnRequest{
session_id: "session-1"
input: Message{
role: "user"
content: [Content{text: TextContent{text: "列出当前目录"}}]
}
}
oneof outcome:
accepted: TurnReceipt{session_id,turn_id,state:"running"}
rejected: Rejection{code,message,retryable}
该响应只表示 server 已接受并拥有本次操作。回答、工具调用、usage 和最终状态全部通过 WatchEvents 发送。
常见 rejection:
| code | 原因 |
|---|---|
INVALID_ARGUMENT |
缺少 session 或输入内容 |
NOT_FOUND |
session 不存在 |
UNAVAILABLE |
session 绑定的 node 已离线 |
ALREADY_EXISTS |
envelope ID 与另一请求冲突 |
真实示例:Client.RunTurn(ctx, sessionID, text)。
取消正在运行的 Turn:
CancelTurnRequest{
session_id: "session-1"
turn_id: "turn-1"
reason: "user_requested"
}
响应为 CancelTurnResponse,accepted 中仍使用 TurnReceipt。被接受的 Turn 最终仍应收敛到 turn_ended,通常带有 canceled stop reason。
关闭 session:
CloseSessionRequest{
session_id: "session-1"
reason: "completed"
}
响应为 CloseSessionResponse,accepted 中返回最终 Session。
CancelOperation 取消 envelope ID 标识的长操作。终止 WatchEvents:
CancelOperation{
target_id: "<watch-envelope-id>"
reason: "client_closed"
}
断开 watcher 不会自动取消其观察的 session 或 Turn。
Event 公共字段:
| 字段 | 说明 |
|---|---|
id |
Event ID |
emitted_at |
产生时间 |
session_id |
所属 session |
turn_id |
所属 Turn;会话级事件可以为空 |
emitter |
事件来源 |
seq |
session 内严格递增的事件序号 |
extensions |
附加元数据 |
Event.seq 是业务事件顺序,delivery_cursor 是持久化位置;二者不能互换。
| payload | 关键字段 | 说明 |
|---|---|---|
session_started |
model, parent IDs |
session 已启动 |
session_ended |
reason |
session 已结束 |
turn_started |
— | Turn 已启动 |
turn_ended |
stop_reason, error, usage, context_tokens |
Turn 的唯一终止事件 |
message |
Message |
完整权威消息 |
message_delta |
message_id, content_index, operation, value |
实时消息增量 |
tool_call |
id, name, arguments |
完整工具调用 |
tool_call_delta |
call_id, index, name, arguments |
工具参数增量 |
tool_result |
call_id, output, is_error, duration_ms |
工具最终结果 |
usage |
input/output/total tokens | 用量更新 |
error |
code, message, retryable |
非终止或附加业务错误 |
status |
state |
运行状态 |
provider_frame |
provider 原始 frame | 仅在启用相关策略时出现 |
extension |
Any |
应用自定义主 payload |
operation 使用 START、APPEND、REPLACE、END。value 可以是:
textreasoningrefusaldatatool_arguments- 完整
Content
常见文本流使用 APPEND + text。client 不应假设每个 provider 都只产生 text。
Message.role 常见值:system、user、assistant、tool。
Content oneof:
| 类型 | 用途 |
|---|---|
text |
普通文本及 annotations |
reasoning |
reasoning 文本 |
refusal |
模型拒绝信息 |
media |
图片、音频等资源 |
tool_call |
内嵌工具调用 |
tool_result |
内嵌工具结果 |
完整 message 是最终权威内容。delta 只用于实时投影。
message_delta和tool_call_delta不写入 SQLite,delivery_cursor为空。- 其他完整 Event 会进入 session 历史并获得 cursor。
WatchEvents.after_cursor是 exclusive cursor。- 重连时使用最近收到的非空
delivery_cursor,而不是Event.seq。 - 迟到或重连 watcher 会重放完整消息和其他持久化事件,但不会重放 delta。
标准恢复流程:
disconnect
-> reconnect websocket
-> WatchEvents{session_id, after_cursor:lastCursor}
-> replay events after lastCursor
-> continue live events
两层业务结果:
| 类型 | 何时使用 |
|---|---|
ProtocolMessage.protocol_error |
envelope 解码、namespace、路由或无法形成正常 response 的执行错误 |
*Response.rejected |
请求已被解析,但参数、状态或策略不允许执行 |
client 收到 ProtocolError 时仍通过 envelope reply_to 查找原请求或订阅。
Envelope id 同时是幂等 ID:
- 相同
id、相同请求体重发:返回首次响应,不创建第二个逻辑操作。 - 相同
id、不同请求体:返回冲突。 - 网络超时后重试原请求:复用原
id。 - 新的用户操作:生成新的
id。
examples/acp/client/main.go 的核心调用顺序:
client, err := Dial(ctx, serverURL, "", token)
session, err := client.OpenSession(ctx, nodeID, title)
events, err := client.Watch(session.GetId(), "")
receipt, err := client.RunTurn(ctx, session.GetId(), prompt)
for event := range events {
if printEvent(event) {
break
}
}其中:
Dial默认路径为/api/aop/application/wsOpenSession检查 accepted/rejected outcomeWatch使用独立 envelope ID 注册订阅 channelRunTurn返回TurnReceipt,但不返回回答printEvent在turn_ended或session_ended时结束
运行:
go run ./examples/acp/client --server http://127.0.0.1:8080 --token demo --node local -p "列出当前目录"本节的 ConnectRPC 指 cyber 的 unary 管理服务。它与 Application WebSocket 使用相同的 server base URL 和 access key,但解决不同的问题。
AOPService.Connect是 Application 协议的双向流投影,不属于 unary 管理功能组。普通 Web/ACP client 应优先使用/api/aop/application/ws;本节重点描述管理 RPC。
生成的 Go client:
client := rpc.NewSessionServiceClient(http.DefaultClient, "http://127.0.0.1:8080")
request := connect.NewRequest(&types.ListSessionsRequest{Limit: 100})
request.Header().Set("Authorization", "Bearer demo")
response, err := client.ListSessions(ctx, request)- 请求使用 Connect 协议,默认 binary protobuf。
- server 同时兼容 Connect、gRPC 和 gRPC-Web handler。
- Bearer token 通过 Connect interceptor 校验。
- 鉴权失败返回 Connect code
Unauthenticated。 - 业务参数和状态错误使用标准 Connect code,例如
InvalidArgument、NotFound、Unavailable。
| Method | Request | Response | 用途 |
|---|---|---|---|
ListSessions |
ListSessionsRequest |
ListSessionsResponse |
分页查询 session |
GetSession |
GetSessionRequest |
GetSessionResponse |
查询单个 session |
ResetSession |
ResetSessionRequest |
ResetSessionResponse |
关闭旧 session 并创建新 session |
DeleteSession |
DeleteSessionRequest |
DeleteSessionResponse |
删除 session |
ListCommands |
ListCommandsRequest |
ListCommandsResponse |
查询 session 可用命令 |
ListEvents |
aop.ListEventsRequest |
aop.ListEventsResponse |
查询持久化事件 |
HTTP procedure 示例:
/cyber.rpc.chat.SessionService/ListSessions
/cyber.rpc.chat.SessionService/ListEvents
| Method | 用途 |
|---|---|
SubmitScan |
提交扫描 |
GetScan |
查询扫描 |
ListScans |
查询扫描列表 |
CancelScan |
取消扫描 |
| Method | 用途 |
|---|---|
GetConfig |
查询当前配置视图 |
UpdateConfig |
更新配置 |
ActivateProfile |
激活 LLM profile |
TestLLM |
测试 LLM 调用 |
ListModels |
查询 provider models |
TestConnection |
测试外部依赖连接 |
| Method | 用途 |
|---|---|
ListAgents |
查询在线 Agent、capabilities、状态和统计 |
| Method | 用途 |
|---|---|
GetStatus |
查询系统状态 |
| Method | 用途 |
|---|---|
SyncArtifacts |
追加浏览器导入的原始 Artifact events,并按 cursor 读取归档事件 |
SyncArtifactsRequest 只有 after_cursor 和可选的 artifacts;响应是
aop.EventDelivery 列表。服务端固定按 100 条分页,不接收或返回 CSTX node、
处理完成状态或额外 Artifact DTO。
完整字段见 proto/rpc/*.proto。
| 字段 | 说明 |
|---|---|
after_cursor |
分页 cursor;当前实现为非负 offset 字符串 |
limit |
页大小;0 使用服务端默认值 |
include_closed |
是否包含关闭的 session |
ListSessionsResponse.next_cursor 非空表示还有下一页。
| 字段 | 说明 |
|---|---|
session_id |
必填 |
after_cursor |
exclusive 持久化 cursor |
limit |
最大事件数量 |
ListEventsResponse.events 是 EventDelivery{cursor,event} 列表,next_cursor 是本次返回的最后 cursor。
它不会包含 message_delta 或 tool_call_delta,因为这两类事件不持久化。
修改类管理 RPC 使用独立 request_id 做幂等标识:
ResetSessionRequest:request_id、session_id必填,new_session_id和title可选。DeleteSessionRequest:request_id、session_id必填。- response 使用 accepted/rejected outcome,而 transport 失败使用 Connect error。
| Connect code | 常见原因 |
|---|---|
Unauthenticated |
token 缺失或错误 |
InvalidArgument |
缺少必填字段或 cursor 非法 |
NotFound |
session、scan 或 node 不存在 |
AlreadyExists |
request/session ID 冲突 |
FailedPrecondition |
对应 service 或 runtime 不可用 |
ResourceExhausted |
达到并发或容量限制 |
Unavailable |
Agent 或外部依赖不可用 |
Internal |
未映射的服务端错误 |
examples/acp/connectrpc/main.go 使用真实生成 client:
client := rpc.NewSessionServiceClient(http.DefaultClient, serverURL)
request := connect.NewRequest(&types.ListSessionsRequest{
Limit: 100,
IncludeClosed: true,
})
request.Header().Set("Authorization", "Bearer "+token)
response, err := client.ListSessions(ctx, request)查询 session 列表:
go run ./examples/acp/connectrpc --server http://127.0.0.1:8080 --token demo查询指定 session 的持久化事件:
go run ./examples/acp/connectrpc --server http://127.0.0.1:8080 --token demo --session <session-id>示例以标准 protobuf JSON 输出 response,方便直接检查字段。
Application/AOP schema:
web/frontend/cyber-ui/packages/aop/proto/aop/*.proto
ConnectRPC service 和 Cyber 类型:
proto/rpc/*.proto
proto/types/*.proto
字段参考来源:
- Application WebSocket:
web/frontend/cyber-ui/packages/aop/proto/aop/** - 管理平面:
proto/rpc/*.proto、proto/types/*.proto
Application WebSocket 不需要生成 service client,只需要 protobuf messages:
protoc \
-I web/frontend/cyber-ui/packages/aop/proto \
--java_out=lite:<out-dir> \
web/frontend/cyber-ui/packages/aop/proto/aop/*.protoAndroid Gradle 示例:
plugins { id("com.google.protobuf") version "0.9.4" }
protobuf {
protoc { artifact = "com.google.protobuf:protoc:4.31.0" }
generateProtoTasks {
all().forEach { task ->
task.builtins { create("java") { option("lite") } }
}
}
}
dependencies {
implementation("com.google.protobuf:protobuf-javalite:4.31.0")
}proto 当前没有设置 java_package 和 java_multiple_files。直接生成时 Java 类默认按 proto 文件嵌套;vendor 到自己的 SDK 时可以添加符合项目规范的 Java options。
ConnectRPC 除 protobuf message generator 外,还需要对应语言的 Connect client generator。生成时同时提供两个 include root:
-I web/frontend/cyber-ui/packages/aop/proto
-I proto
需要编译的入口是 proto/rpc/*.proto,其 imports 会引用 proto/types 和 AOP schema。
仓库内 Go 代码统一通过:
go run ./cmd/gen生成的 Go clients 位于 pkg/rpc/*connect.go。
- WebSocket 使用 binary protobuf Envelope。
- ConnectRPC 默认使用 binary protobuf,也可以协商标准 protobuf JSON。
- protobuf JSON 中
bytes是 base64 字符串。 - enum 使用符号名称。
- oneof 使用生成的 JSON 字段名。
- 未知
Any.type_url应保留,不应按 JSON shape 猜测类型。
go test ./examples/acp/client ./examples/acp/connectrpc