SDK Protocol(WebSocket 双向流)¶
客户端 / CLI 与 Agent 运行时之间的双向流协议:一条 WebSocket 上传 JSON 文本帧,承载数据平面(对话消息)、控制平面(权限询问 / 中断 / 配置)、生命周期(心跳 / 断线恢复)三组帧。协议的权威定义是仓库内的 JSON Schema(schema/sdk/v1/)与 Go 实现(packages/go-sdk/biu/sdkproto/v1/),本文全部内容以这两处代码为准。
协议有两个接入面:
| 接入面 | 服务端 | 适用场景 |
|---|---|---|
| 云端 Agent Plane | Brain 服务(/v1/agent/sessions/...) |
Flutter 客户端、任意第三方 UI 远程驱动 BiuMind 托管的会话 |
| 本地 biu daemon bridge | biu serve / biu bridge 进程(loopback HTTP + WS) |
IDE / 本地工具驱动用户自己机器上的 biu 引擎 |
两个接入面使用完全相同的帧格式(同一个 sdkproto.Frame 集合),差别只在会话建立方式与断线重连机制(见传输层)。
1. 总览:四组帧平面¶
每条 WS 消息就是一个 JSON 对象,一级判别字段是 type:
| 平面 | type 取值 |
方向 | 定义 |
|---|---|---|---|
| 数据平面 | user / assistant / stream_event / result / system / auth_status / rate_limit_event / prompt_suggestion / tool_progress / tool_use_summary / streamlined_text / streamlined_tool_use_summary |
双向(多数为服务端 → 客户端) | schema/sdk/v1/data/ |
| 控制平面 | control_request / control_response / control_cancel_request |
双向 | schema/sdk/v1/control/ |
| 生命周期 | keep_alive / update_environment_variables / biumind.*(共 8 种) |
双向(有方向限制) | schema/sdk/v1/lifecycle.json |
| 编码模块 | code_request / code_response / code_pty_* / code_session_event(共 7 种) |
双向 | packages/go-sdk/biu/sdkproto/v1/code.go,仅本地 bridge 的 /v1/code/ws 通道使用 |
通用解析入口是 UnmarshalFrame:先按 type 分到平面,再走该平面的二级判别(见下)。协议是封闭集合——Frame 接口的 isFrame() 方法未导出,外部包不能新增 wire 类型(service.go 注释明确此约束)。
2. 传输层¶
2.1 云端 Agent Plane¶
端点寻址¶
客户端只配一个「服务器地址」。官方部署里所有路径经站点 nginx 反代(web/site/nginx.conf 的 location /v1/agent/,上游 http://brain:7003)。该 location 针对 WS 做了必要配置:proxy_http_version 1.1 + Upgrade 头透传、proxy_read_timeout 600s、proxy_buffering off(不关 buffering 的话服务端帧会被 nginx 攒着不吐)。
会话建立是「先 HTTP、后 WS」两步:
第一步:创建会话(POST /v1/agent/sessions,鉴权 Authorization: Bearer <JWT 或 PAT>)
请求体核心字段(完整定义见 services/brain/internal/agentplane/router.go 的 CreateSessionAPIReq):
{
"mode": "chat",
"thread_id": "可选,关联对话线程",
"model": "可选,留空由服务端按默认链解析",
"prompt": "本轮用户输入(chat / agent / task 模式的首条消息)",
"environment_id": "agent 模式必填;chat / task 留空",
"workdir": "agent / task 模式的工作目录",
"images": [{ "mime_type": "image/png", "data": "<base64,不带 data: 前缀>" }]
}
mode 三选一(biumind_ext.json 的 Mode enum):
chat:Brain 进程内直接驱动模型,不绑执行环境;agent:投给你自己的设备(biu_daemon/biu_cli注册的 environment),environment_id必填;task:从在线 runtime 池自动挑一个执行环境。
成功响应 201(writeSessionCreated,router.go):
{
"session_id": "b0b6a3e2-…",
"session_token": "<短效 JWT,30 分钟>",
"expires_at": 1735689600000,
"mode": "chat",
"state": "active",
"jetstream_subject_in": "biu.session.<sid>.in",
"jetstream_subject_out": "biu.session.<sid>.out",
"created_at": 1735687800000
}
[!NOTE]
state为pending表示目标设备当前离线、任务已排队(设备上线后自动派发)——此时不要连 WS,等设备上线后再恢复。绑定了执行环境的会话(agent/task)额外返回environment_id;创建时传了thread_id的会话额外返回thread_id。
第二步:连接 WS 流
- 鉴权用
session_token:优先 query 参数(浏览器 WS 客户端不方便设 header),也接受Authorization: Bearer头。token 的 scope 必须匹配 URL 里的 session_id(拿 A 会话的 token 连 B 会话会被拒)。 - token 过期前调
POST /v1/agent/sessions/{id}/refresh-token(带长效凭证)换新 token。token 在连接建立时校验,新 token 于下次(重)连生效——Flutter 客户端在到期前 5 分钟定时刷新(_scheduleTokenRefresh)。 - 不协商 WS 子协议(服务端 gorilla
Upgrader未配置Subprotocols,客户端无需发送Sec-WebSocket-Protocol);帧一律是 WS text frame,一条消息一个 JSON 对象,无外层信封。 - 会话已进入终态(
completed/failed/cancelled)时连接返回409 session_finalized,响应里会指路GET /v1/agent/sessions/{id}/result取最终结果。
实现:services/brain/internal/agentplane/ingress.go。
心跳与帧大小限制¶
- WS 协议层:服务端每 30 秒发一个 ping,60 秒内收不到 pong 即断开(
ingressPingPeriod/ingressPongWait)。客户端只要响应标准 pong 即可,不需要自己实现应用层心跳。 - 帧大小:单条入站帧上限 32 KB(
ingressMaxFrameSize);LLM 流式单块通常远小于此。 - 云端路径不会周期性发送
keep_alive应用帧(brain 侧无任何 KeepAlive 发布代码);keep_alive帧目前只在本地 bridge 出现(见 2.2)。
断线重连:?since_seq=N¶
服务端把每个会话的下行帧逐条写入 NATS JetStream(stream BIU_SESSIONS,subject biu.session.<sid>.out,保留期 1 小时)。重连时带 ?since_seq=<已见最大序号>:
since_seq缺省或为 0:实时拉,只看新帧;since_seq > 0:服务端用 OrderedConsumer 从since_seq + 1开始重放历史帧,重放完无缝接实时流;- 若你要的历史已被流保留策略清理(
since_seq + 1 < 流当前最早序号),服务端推一帧biumind.session_desynced后正常关闭连接——客户端按帧内final_result_url去取会话最终结果兜底。
{
"type": "biumind.session_desynced",
"session_id": "b0b6a3e2-…",
"final_result_url": "/v1/agent/sessions/b0b6a3e2-…/result",
"since_seq": 42,
"reason": "requested seq 42 before stream first seq 100"
}
[!WARNING]
since_seq是「客户端已经看过的最大序号」,不是「想从哪条开始看」。客户端要自己持久化已见序号(Flutter 端BiuSessionConnection每处理 10 帧落一次库)。会话流只在 JetStream 保留 1 小时,更早的帧无法重放。另注意:v1 的下行帧不携带服务端序号字段,Flutter 客户端用本地收帧计数近似(biu_client.dart注释明确把「帧内嵌 seq」列为后续版本工作)——重放起点可能因流内混入其他消息而略有偏移,集成方如需精确重放应自行评估(本地 bridge 的last_event_id无此问题,缓冲只存下行帧)。
客户端重连的参考实现:apps/client/lib/data/api/biu_client.dart——指数退避 1 秒到 30 秒、最多 8 次;连续失败超过 2 次触发 token 刷新回调。
2.2 本地 biu daemon bridge¶
biu serve(桌面端嵌入守护进程)或 biu bridge 在本机起一个 HTTP + WS 服务(apps/cli/biu/internal/bridge/)。鉴权可选:Options.AuthToken 非空时所有请求要求 Authorization: Bearer <token>,空则不鉴权(仅限 loopback / 开发环境)。
路由(server.go 顶部注释):
| 路由 | 作用 |
|---|---|
POST /v1/code/sessions |
创建会话,返回 { "id": "…" } |
POST /v1/code/sessions/{id}/messages |
提交一轮任务,body {"prompt":"…"};语义是「中止当前轮 + 重开一轮」 |
GET /v1/code/sessions/{id}/ws |
WS 流(帧格式与云端完全一致) |
GET /v1/code/ws |
编码模块专用通道(code_* 帧,PTY / Git / 文件系统) |
GET /v1/code/sessions/{id}/cost |
费用快照 |
POST /v1/code/sessions/{id}/compact |
手动压缩上下文 |
POST /v1/code/sessions/{id}/attachments |
上传附件 |
DELETE /v1/code/sessions/{id} |
关闭会话 |
断线重连:?last_event_id=N(环形缓冲)¶
与云端的 JetStream 方案不同,本地 bridge 在每个会话内维护一个 256 帧的内存环形缓冲(eventBufferCap,server.go):每帧分配单调递增 id,重连时带 ?last_event_id=N 重放 id 大于 N 的帧,然后接实时推流。
- 没有在跑的任务、又没带 resume 游标时,连接返回
409 no turn in progress; - 任务已跑完(缓冲还在)时,重放完后推一帧
keep_alive作为「流正常结束」哨兵,然后以CloseNormalClosure("done")关闭连接。
心跳同样是 WS 协议层 ping(30 秒一次,60 秒无 pong 断开),与云端参数一致(wsPingPeriod / wsPongWait,ws.go)。
[!NOTE] 两种接入面的恢复游标不通用:云端用
since_seq(JetStream 消息序号),本地用last_event_id(进程内环形缓冲序号)。两者都只在各自进程 / 流的生命周期内有意义。
2.3 帧在服务端的流转(云端)¶
理解一条帧的路径有助于排障。云端一帧的旅程:
客户端 ──WS──▶ brain ingress ──publish──▶ biu.session.<sid>.in(JetStream)
brain ingress ◀─subscribe── biu.session.<sid>.out ◀─publish── 执行方(chat: brain 进程内;agent/task: worker)
入站帧里有两类会旁路 .in 主题、直接走控制队列投给执行方(ingress.go):
control_cancel_request—— 触发会话中断;control_response—— 权限答复 / 提问表单作答(按request_id分流)。
其余入站帧原样发布到 .in 主题存档。当前执行方不消费 .in 主题上的数据帧(多轮 user 消息直发 WS 尚未接通),因此云端一轮回来的标准做法是:prompt 走创建会话的 HTTP 请求,WS 只收帧 + 发控制帧。
3. 消息模型¶
3.1 判别规则(union discriminator)¶
帧的判别是「先 type、后二级字段」两步(unmarshal.go 与 wrappers.go):
type字段决定平面(数据 / 控制 / 生命周期 / 编码);- 数据平面内部:
type: "system"的帧再用subtype区分 16 个子类型;type: "result"用is_error区分成功 / 失败(不是subtype——两种 result 共享大部分字段,subtype分别为"success"/"error_*");- 其余
type一对一对应一个结构; - 控制平面内部:
control_request的request对象用subtype区分 21 种请求。
各平面的多数结构带这些公共字段(具体见各结构定义):
uuid:本帧唯一标识(帧级别,非会话级别);session_id:所属会话;parent_tool_use_id:子代理嵌套场景下,本消息归属的上层工具调用 id(user/assistant/stream_event/tool_progress上可出现);type: "system"系帧额外带subtype。
Schema 层面所有对象 open by default(additionalProperties: true,见 _common.json 描述):服务端可能附加未来字段,客户端解析必须容忍未知字段。
[!NOTE] 个别代码注释里仍写着「24 个 / 29 个 variant」的历史数字,与实现不符。以 [
unmarshal.go的isSDKMessage()清单为准:数据平面共 28 个 variant(下表逐个列出,data/index.json的 union 也是 28 个引用)。
3.2 数据平面:28 个 variant¶
type |
(subtype) |
Go 结构 | 含义 |
|---|---|---|---|
user |
— | SDKUserMessage |
用户消息(含工具结果回传:tool_use_result 字段)。isReplay=true 的重放形态在 schema 里单列为 SDKUserMessageReplay,Go 侧同一结构 |
assistant |
— | SDKAssistantMessage |
一轮完整助手消息,message.content 是 content block 数组(text / tool_use 等) |
stream_event |
— | SDKPartialAssistantMessage |
上游模型流的原始增量事件(event 为透传 JSON) |
result |
success |
SDKResultSuccess |
一轮结束(成功)。含 duration_ms / num_turns / total_cost_usd / usage / modelUsage / stop_reason |
result |
error_*(is_error=true) |
SDKResultError |
一轮结束(失败)。同上 + errors 数组 |
system |
init |
SDKSystemInit |
会话初始化快照(工具清单 / 模型 / 权限模式等) |
system |
status |
SDKSystemStatus |
引擎状态 |
system |
compact_boundary |
SDKCompactBoundary |
上下文压缩完成边界 |
system |
api_retry |
SDKAPIRetry |
上游模型调用重试通知 |
system |
local_command_output |
SDKLocalCommandOutput |
本地命令输出 |
system |
hook_started |
SDKHookStarted |
hook 开始执行 |
system |
hook_progress |
SDKHookProgress |
hook 执行中间输出 |
system |
hook_response |
SDKHookResponse |
hook 执行结果 |
system |
files_persisted |
SDKFilesPersisted |
文件持久化完成 |
system |
task_notification |
SDKTaskNotification |
后台任务通知 |
system |
task_started |
SDKTaskStarted |
后台任务开始 |
system |
task_progress |
SDKTaskProgress |
后台任务进度 |
system |
session_state_changed |
SDKSessionStateChanged |
会话状态切换(idle / running / requires_action) |
system |
elicitation_complete |
SDKElicitationComplete |
MCP 服务器发起的提问流程结束 |
system |
form_answer |
SDKFormAnswer |
提问表单的终态(action: accept / decline / cancel / timeout) |
system |
post_turn_summary |
SDKPostTurnSummary |
一轮结束后的结构化摘要 |
auth_status |
— | SDKAuthStatus |
认证状态变化 |
rate_limit_event |
— | SDKRateLimitEvent |
上游速率限制事件 |
prompt_suggestion |
— | SDKPromptSuggestion |
输入建议 |
tool_progress |
— | SDKToolProgress |
工具调用进行中(含 tool_use_id / tool_name / 耗时) |
tool_use_summary |
— | SDKToolUseSummary |
工具调用结果摘要(preceding_tool_use_ids 关联到 tool_use_id) |
streamlined_text |
— | SDKStreamlinedText |
推荐的流式文本增量——轻量帧,客户端直接拼接渲染 |
streamlined_tool_use_summary |
— | SDKStreamlinedToolUseSummary |
轻量工具结果摘要 |
(表中「(subtype)」列的 success / error_* 是 result 帧的两种形态;system 的 16 个子类型各计一个 variant。)
字段级定义逐类见 schema/sdk/v1/data/(user.json / assistant.json / result.json / system.json / tool.json / post_turn.json / streamlined.json)与 data.go。
[!NOTE] 同一份流式文本可能以两种形式到达:
streamlined_text增量帧,以及assistant帧message.content里的text块(权威完整副本)。Flutter 客户端的策略是:已有流式文本块时跳过assistant帧里的text项,避免重复渲染(biu_session_connection.dart的_onAssistantMessage)。
3.3 控制平面¶
三种顶层帧:
control_request({type, request_id, request:{subtype, …}}),request.subtype 共 21 种(control.go):
subtype |
用途 |
|---|---|
initialize |
会话初始化配置(hooks / MCP 服务器 / 系统提示词 / agents 等) |
interrupt |
中断当前轮(本地 bridge 路径) |
can_use_tool |
服务端 → 客户端的权限询问(tool_name / input / tool_use_id 等) |
set_model |
切换模型 |
set_permission_mode |
切换权限模式(default / acceptEdits / bypassPermissions / plan / dontAsk) |
set_max_thinking_tokens |
调整思考预算 |
mcp_status / mcp_message / mcp_set_servers / mcp_reconnect / mcp_toggle |
MCP 服务器管理 |
get_context_usage |
查询上下文占用 |
hook_callback |
回应服务端挂起的 hook 回调 |
rewind_files |
回滚文件到某条用户消息时点 |
cancel_async_message |
取消某条异步消息 |
seed_read_state |
预置文件读取状态 |
reload_plugins |
重载插件 |
stop_task |
停止后台任务 |
apply_flag_settings / get_settings |
设置读写 |
elicitation |
服务端 → 客户端的提问表单(message / mode: form\|url / requested_schema) |
control_response({type, kind?, response:{subtype, request_id, response?, error?}})——对某个 request_id 的应答,subtype 为 success / error。kind 字段(elicitation_response / permission_response)显式声明回包种类,服务端兼容不带的旧客户端(按回包体形状判别)。两种常见回包体:
- 权限答复:
{"behavior": "allow" | "deny", "updatedInput": …, "updatedPermissions": …}(完整定义见permissions.json的PermissionResult); - 表单作答:
{"action": "accept" | "decline" | "cancel", "content": {…}}。
control_cancel_request({type, request_id})——取消整个会话的当前执行,是独立顶层 type,与 cancel_async_message(subtype)不同。
3.4 生命周期:8 种 BiuMind 自有帧¶
lifecycle.json / lifecycle.go:
type |
方向 | 含义 |
|---|---|---|
keep_alive |
双向 | 心跳 / 流结束哨兵(ts 毫秒时间戳) |
update_environment_variables |
客户端 → 服务端 | 更新会话环境变量 |
biumind.session_desynced |
服务端 → 客户端 | 请求的历史序号已被清理,按 final_result_url 取最终结果 |
biumind.session_paused |
服务端 → 客户端 | 会话暂停(如执行方在等表单作答时掉线) |
biumind.session_resumed |
服务端 → 客户端 | 暂停会话已恢复,流继续 |
biumind.session_primary_promoted |
服务端 → 客户端 | 多副本场景下的主副本切换通知 |
biumind.compact_started |
服务端 → 客户端 | 上下文压缩开始(reason / tokens_before) |
biumind.compact_finished |
服务端 → 客户端 | 压缩完成(tokens_before / tokens_after / tokens_saved) |
3.5 编码模块:7 种 code_* 帧¶
仅本地 bridge 的 GET /v1/code/ws 通道(code.go):code_request / code_response 是通用 RPC 信封(method 分发 git.status / fs.read / pty.open 等);code_pty_chunk / code_pty_input / code_pty_resize / code_pty_exit 是 PTY 字节流(字节经 JSON 自动 base64,无损传输);code_session_event 是结构化会话事件(外部编码代理会话的 JSONL 事件解析结果)。
3.6 方向规则¶
同一套帧双向共用,service.json 的两个 union 定义了名义方向:
- StdinMessage(客户端 → 服务端):全部
SDKMessage+control_request+control_cancel_request+update_environment_variables; - StdoutMessage(服务端 → 客户端):全部
SDKMessage+control_request(反向询问)+control_response+ 除update_environment_variables外的 7 种生命周期帧。
[!NOTE]
control_response实际上双向都会出现:服务端用它应答客户端的control_request(本地 bridge 的中断请求),客户端用它应答服务端反向发起的权限 / 表单询问(两个接入面的服务端都显式处理这一方向,见ws.go的 read pump 与ingress.go的maybeRoutePermissionResponse)。Go 侧IsStdinMessage助手把control_response判为非上行方向,与实际收发路径不一致,仅用于单测断言——集成时以上述实际路径为准。
3.7 JSON Schema 与校验¶
- Schema 根入口:
schema/sdk/v1/index.json(StdinMessage/StdoutMessageunion)、data/index.json(SDKMessageunion); - 实例样本(每种帧一份可校验样例):
schema/sdk/v1/fixtures/; - 校验工具:
make schema-validate(仓库根;工具实现tools/schema-validate/main.go,schema draft 2020-12)。
[!WARNING] fixture 的
$schema字段必须指向具体$defs/<Type>(schema 文件顶层只有$defs、无约束,指向文件根会让校验空转通过)。
4. 会话生命周期与事件流¶
4.1 一轮的标准帧序列(云端 chat 模式)¶
首条用户消息随创建会话的 HTTP 请求传入(prompt 字段),随后 WS 上按序到达(帧生成逻辑:brain 进程内 FrameEmitter,daemon / runtime worker 路径同构):
服务端 → 客户端 说明
streamlined_text ×N 流式文本增量(每帧一小段,客户端拼接)
tool_progress 某工具开始执行
streamlined_tool_use_summary (或 tool_use_summary)工具结果摘要
streamlined_text ×N 工具结果后的继续输出
assistant 本轮完整消息快照(content block 数组)
result (subtype=success) 本轮结束:耗时 / 轮数 / 用量 / 费用 / stop_reason
result 帧是一轮的终止信号:客户端收到后即可收尾渲染。stop_reason 为 interrupted 时表示用户取消走的干净停止路径,应按「已取消」而非「失败」渲染。
出错时以 result(is_error: true,subtype: error_*,带 errors 数组)终止。会话进入终态后,result 帧决定了会话状态(completed / failed / cancelled)。
4.2 权限询问(服务端 → 客户端 → 服务端)¶
执行方要跑一个需要授权的工具时,反向发一条 control_request:
{
"type": "control_request",
"request_id": "9f2c…",
"request": {
"subtype": "can_use_tool",
"tool_name": "Bash",
"input": { "command": "rm -rf build" },
"tool_use_id": "toolu_01…",
"decision_reason": "destructive command"
}
}
客户端弹卡,用户决策后回:
{
"type": "control_response",
"kind": "permission_response",
"response": {
"subtype": "success",
"request_id": "9f2c…",
"response": { "behavior": "deny", "message": "user rejected" }
}
}
[!WARNING] 权限询问有 30 秒超时,超时或回包解析失败一律按
deny处理(「默认拒绝」是安全侧,见 bridgeaskPermission与 worker 同名实现)。客户端断线时正在挂起的询问也会落到 deny。
4.3 提问表单(elicitation)¶
执行方需要结构化用户输入时(选项 / 表单),发 control_request(request.subtype: "elicitation",mode: "form",requested_schema 是 JSON Schema 风格描述)。客户端渲染表单,作答回 control_response(kind: "elicitation_response",response.response 为 {"action":"accept","content":{"answer":"…"}})。服务端随后推 system + subtype: form_answer 终态帧沉淀问答结果。不应答时服务端约 5 分钟超时兜底,会话不死。
4.4 中断¶
- 云端:客户端发
control_cancel_request({"type":"control_cancel_request","request_id":"cancel-…"})。执行方走干净停止路径:补发result(stop_reason: "interrupted")+ 为未完成的工具调用合成结果; - 本地 bridge:WS 上发
control_request(request.subtype: "interrupt"),服务端回control_response(成功或"no turn in progress"错误)。
[!NOTE] 发出取消后不要立刻关连接——干净停止需要时间投递最终
result帧。Flutter 客户端设 3 秒兜底窗口,窗口内等到stop_reason: "interrupted"算干净取消,否则按已取消强制收尾(_cancelGraceWindow)。
4.5 暂停与恢复(durable resume)¶
执行方在等表单作答时掉线,brain 把会话置 paused 并推 biumind.session_paused。客户端仍可对挂起表单作答(迟到作答会落库),随后调 POST /v1/agent/sessions/{id}/resume 重跑,服务端推 biumind.session_resumed 后流式帧继续。挂起中的表单可用 GET /v1/agent/sessions/{id}/elicitations 拉回(路由见 resume.go)。
4.6 会话收尾¶
- 云端:
result帧后连接不会被服务端自动关闭(可继续等下一轮 / 重新订阅),客户端确认收到后主动关;会话状态落终态后再连/stream会得到409 session_finalized; - 本地 bridge:当前轮跑完,服务端推一帧
keep_alive哨兵后以CloseNormalClosure("done")主动关闭连接。
5. 三端实现指针¶
| 语言 | 位置 | 说明 |
|---|---|---|
| Go | packages/go-sdk/biu/sdkproto/v1/ |
权威实现。UnmarshalFrame(任意帧)/ UnmarshalSDKMessage / UnmarshalControlRequestInner / UnmarshalLifecycle;105 个单测覆盖往返保真 |
| Dart | apps/client/lib/data/api/sdkproto/v1/ |
sealed class + ServiceFrame.fromJson 工厂(sdk_message.dart 等数据类,json_serializable 生成 .g.dart);上层封装 BiuClient(连接 / 重连 / 离线队列)与 BiuSessionConnection(帧 → 消息块渲染) |
| TypeScript | apps/miniapp/src/lib/sdkproto/v1/index.ts |
占位类型别名(计划 vendor Zod schema),当前仅 Mode 常量可用 |
自研客户端最省事的路径:直接引用 schema/sdk/v1/ 的 JSON Schema 生成解析代码(Go 端即由此手写对齐),解析入口按 3.1 的两步判别实现。
6. 最小集成示例¶
以下序列来自代码与 schema fixture(fixtures/system_init.json、fixtures/user__basic.json、fixtures/result_success.json、fixtures/streamlined_text.json 及 BiuClient.sendUserText / bridge askPermission 的序列化字段),可直接当作集成对照样本。
步骤 1 —— 创建会话:
curl -X POST https://your-biumind.example.com/v1/agent/sessions \
-H "Authorization: Bearer <你的PAT或JWT>" \
-H "Content-Type: application/json" \
-d '{"mode":"chat","prompt":"帮我看看这个仓库的结构"}'
步骤 2 —— 连接 WS(任一语言的标准 WebSocket 客户端):
步骤 3 —— 收帧并处理。 浏览器 JavaScript 骨架:
const ws = new WebSocket(
`wss://your-biumind.example.com/v1/agent/sessions/${sessionId}/stream` +
`?session_token=${sessionToken}`
);
let text = "";
ws.onmessage = (e) => {
const frame = JSON.parse(e.data); // 一条消息 = 一个 JSON 帧
switch (frame.type) {
case "streamlined_text": // 流式文本:拼接渲染
text += frame.text;
break;
case "assistant": // 本轮完整快照(含 tool_use 块)
break;
case "result": // 一轮终止信号
if (frame.is_error) { /* 展示 frame.errors */ }
else { /* 展示 frame.result / frame.usage / frame.total_cost_usd */ }
ws.close(); // 客户端主动收尾
break;
case "control_request": // 反向询问:权限 / 表单
if (frame.request.subtype === "can_use_tool") {
ws.send(JSON.stringify({
type: "control_response",
kind: "permission_response",
response: {
subtype: "success",
request_id: frame.request_id,
response: { behavior: "allow" } // 或 { behavior: "deny", message: "…" }
}
}));
}
break;
case "biumind.session_desynced": // 重连游标过期:去取最终结果
fetch(frame.final_result_url, { headers: { Authorization: "Bearer <长效凭证>" } });
break;
default:
break; // 未知 type / 字段:容忍,不报错
}
};
对照:一条 user 数据帧的 wire 形态(多轮场景客户端上行,或回放历史时服务端下行):
{
"type": "user",
"message": { "role": "user", "content": [{ "type": "text", "text": "hi" }] },
"uuid": "u1",
"session_id": "s1"
}
对照:一条成功的 result 帧(fixture result_success.json):
{
"type": "result",
"subtype": "success",
"duration_ms": 1000,
"duration_api_ms": 800,
"is_error": false,
"num_turns": 3,
"result": "done",
"stop_reason": "end_turn",
"total_cost_usd": 0.01,
"usage": {},
"modelUsage": {
"claude-3-7": {
"inputTokens": 10,
"outputTokens": 20,
"cacheReadInputTokens": 0,
"cacheCreationInputTokens": 0,
"webSearchRequests": 0,
"costUSD": 0.01,
"contextWindow": 200000,
"maxOutputTokens": 4096
}
},
"permission_denials": [],
"uuid": "r1",
"session_id": "s1"
}
断线重连:记录已处理的帧数作为游标,重连时带上(云端 since_seq / 本地 last_event_id),服务端会先补发错过的帧、再无缝接实时流。