跳到主要内容

01 · 事件协议本体(类型与生命周期)

这一章只讲「协议长什么样」:有哪些事件、各带什么字段、什么顺序才算合法。读完你能看懂任意一条 AG-UI 流。

1.1 协议的骨架:一个枚举 + 一组 schema

AG-UI 的协议本体非常小,核心就是 EventType 这个枚举,定义在 packages/core/src/events.ts:12-61。所有事件都共享一个基类:

// packages/core/src/events.ts:63 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:311 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:22-41)。新代码别用。

1.3 三组事件细看字段

文本消息——一条助手回复的「打字过程」拆成三段。注意 role 默认 assistant,且不允许 tool(TextMessageRoleSchema,events.ts:5-10):

// packages/core/src/events.ts:71-87(节选)
TextMessageStartEventSchema // { messageId, role="assistant", name? }
TextMessageContentEventSchema // { messageId, delta } ← 增量文本
TextMessageEndEventSchema // { messageId }

工具调用——比文本多一个 _RESULT,而且 TOOL_CALL_START 带一个 parentMessageId(把工具调用挂到哪条助手消息上):

// packages/core/src/events.ts:121-145(节选)
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:170-178
StateSnapshotEventSchema // { snapshot } ← 整个 state 直接替换
StateDeltaEventSchema // { delta: JSON Patch[] } ← RFC 6902 增量

STATE_DELTAdeltaJSON 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:70)。例外:RUN_ERROR 可以单独出现。
  • RUN_FINISHED 之后不能再发普通事件;只能用新的 RUN_STARTED 开下一轮(verify.ts:53-64)。
  • RUN_ERROR 是终止态:一旦发出,后面任何事件都报错(verify.ts:43-50)。
  • 配对必须闭合:TEXT_MESSAGE_START 必须有对应的 _END 才能 RUN_FINISHED;工具调用、步骤同理(verify.ts:233-263)。
  • 同一个 messageId/toolCallId 不能重复 START(verify.ts:96verify.ts:148)。

用 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:233-247(节选)
RunFinishedOutcomeSchema = discriminatedUnion("type", [
{ type: "success" },
{ type: "interrupt", interrupts: Interrupt[] }, // ← 暂停,等人响应
]);

outcome.type === "interrupt",这一轮其实是停下来等人:每个 Interrupt(types.ts:193)带 id/reason/可选的 responseSchema/expiresAt。人响应后,下一轮 runAgent 通过 RunAgentInput.resume(types.ts:203-218ResumeEntry[],每项 resolved/cancelled + payload)把答复带回去恢复执行。

兼容细节:outcome 接受 null 并当作「省略」处理(events.ts:246.nullable().optional().transform(...))——这是为了兼容 Python SDK 用 model_dump() 序列化时会输出 "outcome": null 的老行为。这种「为另一语言的序列化习惯让路」的小补丁,正是跨语言协议的真实负担。

1.6 代码地图

主题文件符号名
事件类型枚举packages/core/src/events.tsEventType
事件基类 schemapackages/core/src/events.tsBaseEventSchema
全部事件的判别联合(解码入口)packages/core/src/events.tsEventSchemas
RUN_FINISHED 的成功/中断 outcomepackages/core/src/events.tsRunFinishedOutcomeSchema
消息/工具/运行输入模型packages/core/src/types.tsMessageSchemaToolSchemaRunAgentInputSchema
中断与恢复packages/core/src/types.tsInterruptSchemaResumeEntrySchema
合法顺序(生命周期)的真实实现packages/client/src/verify/verify.tsverifyEvents