数据截至 (上游 commit e42bdbedc27c)
01 · 事件协议本体(类型与生命周期)
这一章只讲「协议长什么样」:有哪些事件、各带什么字段、什么顺序才算合法。读完你能看懂任意一条 AG-UI 流。
1.1 协议的骨架:一个枚举 + 一组 schema
AG-UI 的协议本体非常小,核心就是 EventType 这个枚举,定义在 packages/core/src/events.ts:13-62。所有事件都共享一个基类:
// packages/core/src/events.ts:64 BaseEventSchema
export const BaseEventSchema = z
.object({
type: z.nativeEnum(EventType), // 必有:事件类型
timestamp: z.number().optional(),
rawEvent: z.any().optional(), // 可选:夹带原始后端事件
})
.passthrough(); // 允许额外字段透传
.passthrough() 是个关键设计:未知字段不会被拒绝,而是原样透传。这就是 README 说的「loose event format matching」——协议留了向前兼容的口子。
每一种具体事件用 BaseEventSchema.extend({...}) 扩展。最后所有事件被收进一个 discriminated union(按 type 字段区分的联合类型):
// packages/core/src/events.ts:361 EventSchemas —— 用 type 当判别键
export const EventSchemas = z.discriminatedUnion("type", [
TextMessageStartEventSchema,
TextMessageContentEventSchema,
/* …… 共 30+ 个 schema(含已弃用的 THINKING_* 与 *_CHUNK)…… */
]);
这是 wire 解码的总入口:transform/http.ts:55 直接 EventSchemas.parse(json),一行就把任意 JSON 校验+判别成强类型事件。
1.2 按用途分组的事件(这是协议的「词汇表」)
虽然枚举里有 30 多个成员,但去掉「弃用别名」和「chunk 便捷形式」后,真正的语义事件约 16 类(README 也说「~16 standard event types」)。按用途分组:
| 组 | 事件 | 作用 |
|---|---|---|
| 运行生命周期 | RUN_STARTED / RUN_FINISHED / RUN_ERROR | 一轮 run 的开始 / 结束 / 出错 |
| 步骤 | STEP_STARTED / STEP_FINISHED | 标记 run 内部的命名步骤 |
| 文本消息 | TEXT_MESSAGE_START / _CONTENT / _END | 一条助手消息的开始 / 增量文本 / 结束 |
| 工具调用 | TOOL_CALL_START / _ARGS / _END / _RESULT | 工具名 / 参数流 / 结束 / 结果回填 |
| 状态 | STATE_SNAPSHOT / STATE_DELTA | 共享状态的全量替换 / 增量(JSON Patch) |
| 消息快照 | MESSAGES_SNAPSHOT | 一次性给出完整消息列表 |
| 活动(自定义 UI) | ACTIVITY_SNAPSHOT / ACTIVITY_DELTA | 客户端专属的结构化「活动」消息 |
| 推理 | REASONING_START / REASONING_MESSAGE_* / REASONING_END / REASONING_ENCRYPTED_VALUE | 思考过程的流式展示与加密 |
| 透传/扩展 | RAW / CUSTOM | 原样转发后端事件 / 自定义事件 |
| 便捷(chunk) | TEXT_MESSAGE_CHUNK / TOOL_CALL_CHUNK / REASONING_MESSAGE_CHUNK | 「start+content 合一」的简写,见 ch04 |
弃用提醒:
THINKING_*系列已被REASONING_*取代,标注@deprecated … Will be removed in 1.0.0(events.ts:23-42)。新代码别用。
1.3 三组事件细看字段
文本消息——一条助手回复的「打字过程」拆成三段。注意 role 默认 assistant,且不允许 tool(TextMessageRoleSchema,events.ts:6-11):
// packages/core/src/events.ts:76-91(节选)
TextMessageStartEventSchema // { messageId, role="assistant", name? }
TextMessageContentEventSchema // { messageId, delta } ← 增量文本
TextMessageEndEventSchema // { messageId }
工具调用——比文本多一个 _RESULT,而且 TOOL_CALL_START 带一个 parentMessageId(把工具调用挂到哪条助手消息上):
// packages/core/src/events.ts:126-155(节选)
ToolCallStartEventSchema // { toolCallId, toolCallName, parentMessageId? }
ToolCallArgsEventSchema // { toolCallId, delta } ← 参数 JSON 的增量片段
ToolCallEndEventSchema // { toolCallId }
ToolCallResultEventSchema // { messageId, toolCallId, content, role:"tool"? }
参数是以字符串增量流过来的(delta),前端拼起来再 JSON.parse——这意味着流到一半的参数是不完整 JSON,客户端用 untruncate-json 容错解析(见 ch03)。
状态同步——两种风格,全量 vs 增量:
// packages/core/src/events.ts:188-196
StateSnapshotEventSchema // { snapshot } ← 整个 state 直接替换
StateDeltaEventSchema // { delta: JSON Patch[] } ← RFC 6902 增量
STATE_DELTA 的 delta 是 JSON Patch(RFC 6902,一种用 op/path/value 描述「怎么改一个 JSON」的标准) 数组,前端用 fast-json-patch 套用。这就是「双向状态同步」的底层机制。
1.4 生命周期:什么顺序才合法
协议不只规定事件长什么样,还规定它们的合法顺序。这套规则没写成独立文档,而是直接编码在 verifyEvents 状态机里(packages/client/src/verify/verify.ts,详见 ch03)。核心规则:
- 一条流必须以
RUN_STARTED开头(否则报First event must be 'RUN_STARTED',verify.ts:72)。例外:RUN_ERROR可以单独出现。 RUN_FINISHED之后不能再发普通事件;只能用新的RUN_STARTED开下一轮(verify.ts:55-66)。RUN_ERROR终止本轮:一旦发出,后续事件都报错——唯一例外是RUN_STARTED(一条流可合法带多个 run,允许另起新轮)(verify.ts:45-49)。- 配对必须闭合:
TEXT_MESSAGE_START必须有对应的_END才能RUN_FINISHED;工具调用、步骤同理(verify.ts:236-266)。 - 同一个
messageId/toolCallId不能重复 START(verify.ts:99、verify.ts:151)。
用 ASCII 画一轮典型 run 的合法骨架(嵌套表示「必须闭合」):
RUN_STARTED
├─ STEP_STARTED "plan"
│ ├─ TEXT_MESSAGE_START(m1) … CONTENT* … TEXT_MESSAGE_END(m1)
│ ├─ TOOL_CALL_START(c1) … ARGS* … TOOL_CALL_END(c1)
│ └─ TOOL_CALL_RESULT(c1)
│ STEP_FINISHED "plan" ← step 必须先于 RUN_FINISHED 闭合
└─ RUN_FINISHED ← 此时不能有未闭合的 message/tool/step
1.5 RUN_FINISHED 的两种结局:成功 vs 中断
RUN_FINISHED 不只是「跑完了」,它带一个可选的 outcome,这是 human-in-the-loop 的协议入口:
// packages/core/src/events.ts:251-254(节选)
RunFinishedOutcomeSchema = discriminatedUnion("type", [
{ type: "success" },
{ type: "interrupt", interrupts: Interrupt[] }, // ← 暂停,等人响应
]);
当 outcome.type === "interrupt",这一轮其实是停下来等人:每个 Interrupt(types.ts:206)带 id/reason/可选的 responseSchema/expiresAt。人响应后,下一轮 runAgent 通过 RunAgentInput.resume(types.ts:216-239 的 ResumeEntry[],每项 resolved/cancelled + payload)把答复带回去恢复执行。
兼容细节:
outcome接受null并当作「省略」处理(events.ts:285的.nullable().optional().transform(...))——这是为了兼容 Python SDK 用model_dump()序列化时会输出"outcome": null的老行为。这种「为另一语言的序列化习惯让路」的小补丁,正是跨语言协议的真实负担。
1.6 代码地图
| 主题 | 文件 | 符号名 |
|---|---|---|
| 事件类型枚举 | packages/core/src/events.ts | EventType |
| 事件基类 schema | packages/core/src/events.ts | BaseEventSchema |
| 全部事件的判别联合(解码入口) | packages/core/src/events.ts | EventSchemas |
| RUN_FINISHED 的成功/中断 outcome | packages/core/src/events.ts | RunFinishedOutcomeSchema |
| 消息/工具/运行输入模型 | packages/core/src/types.ts | MessageSchema、ToolSchema、RunAgentInputSchema |
| 中断与恢复 | packages/core/src/types.ts | InterruptSchema、ResumeEntrySchema |
| 合法顺序(生命周期)的真实实现 | packages/client/src/verify/verify.ts | verifyEvents |