SSE 事件协议
对话页与编码会话都靠同一条 SSE 流把「模型在做什么」实时推给前端。协议本身不含业务语义:后端只负责把结构化事件按帧写出,前端按 type 分派。
帧的三种形态
| 形态 | 识别方式 | 前端处理 |
|---|---|---|
| 结构化事件 | 合法 JSON,且含 type | 映射成强类型对象后分派 |
| 纯文本片段 | 不是 JSON | 作为 plainText 直接追加到正文(不丢内容) |
| 错误帧 | 以 [ERROR] 前缀开头 | 渲染为错误状态 |
未知
type 不会被丢弃,而是归到 unknown 并保留原始 JSON——这样后端先上新事件、前端后适配也不会丢帧。事件清单
| 事件 | 关键字段 | 用途 |
|---|---|---|
content | text | 正文增量 |
thinking | text | 可折叠的思考轨迹 |
tool_call | id / name / arguments / hidden | 开始调用工具 |
tool_result | id / name / content / isError / collapsed / hidden | 工具输出(可折叠、可隐藏) |
approval_request | id / name / arguments | 工具待批准(交互卡片) |
question | toolCallId / data | 向用户追问信息 |
todo / plan | data / toolCallId / data | 计划与待办清单 |
usage | promptTokens / completionTokens / cachedTokens / totalTokens / latencyMs | 用量徽标 |
file_change | path / action | 本会话文件改动 |
checkpoint | id / label / fileCount | 检查点记录 |
subagent | id / description / stage / tool / steps / message | 子代理进度(编码会话会额外落库以便回放) |
search_results / knowledge_results / memory_results | results[] | 联网 / 知识库 / 记忆命中列表 |
notice | kind / text | 系统提示(如上下文已压缩、工作树创建失败) |
steering | text | 运行中引导被接受 |
debug | text | 调试输出 |
done | — | 本轮流结束(由调用方补齐) |
工作流的独立事件
工作流运行走自己的事件名(同一套 SSE 传输,但语义不同):run_started / node_started / node_completed / node_failed / human_approval_required / agent_tool_call / run_completed / run_failed。
两条流的差异
| 维度 | 对话页 | 编码会话 |
|---|---|---|
| 错误帧 | 直接渲染 | 重新拼回 [ERROR] 前缀后交给统一解析器 |
subagent | 不特别处理 | 额外落库,用于历史回放 |
| 检索类结果 | 渲染命中列表 | 当前未在时间线展示(解析器里是 no-op) |
done | 由调用方补齐 | 循环结束后显式补一帧 |
实现注意
- 字段名大小写必须稳定:前端映射依赖后端原样输出的 JSON 键。
approval_request与question都有 id,但语义不同(前者批工具,后者问人),不能混用。usage缺少字段时前端按 0 兜底;编码会话的latencyMs由前端自行计算/补零。plainText兜底是刻意设计:即使后端只写裸文本(例如某些代理/中间层),正文也不会丢。- 新增事件类型的做法:后端加
type与字段 → 前端解析器加映射 → 各自 reducer 决定渲染,未适配期间事件不会丢。