<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Agent流式协议 on 扎塔-Zata</title><link>https://www.zata.cc/tags/agent%E6%B5%81%E5%BC%8F%E5%8D%8F%E8%AE%AE/</link><description>Recent content in Agent流式协议 on 扎塔-Zata</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><copyright>Example Person</copyright><lastBuildDate>Thu, 10 Sep 2026 14:54:48 +0800</lastBuildDate><atom:link href="https://www.zata.cc/tags/agent%E6%B5%81%E5%BC%8F%E5%8D%8F%E8%AE%AE/index.xml" rel="self" type="application/rss+xml"/><item><title>AG-UI：当 Agent 学会了和前端说话</title><link>https://www.zata.cc/p/ag-ui%E5%BD%93agent%E5%AD%A6%E4%BC%9A%E4%BA%86%E5%92%8C%E5%89%8D%E7%AB%AF%E8%AF%B4%E8%AF%9D/index.md/</link><pubDate>Thu, 10 Sep 2026 14:30:00 +0800</pubDate><guid>https://www.zata.cc/p/ag-ui%E5%BD%93agent%E5%AD%A6%E4%BC%9A%E4%BA%86%E5%92%8C%E5%89%8D%E7%AB%AF%E8%AF%B4%E8%AF%9D/index.md/</guid><description>&lt;img src="https://www.zata.cc/p/ag-ui%E5%BD%93agent%E5%AD%A6%E4%BC%9A%E4%BA%86%E5%92%8C%E5%89%8D%E7%AB%AF%E8%AF%B4%E8%AF%9D/index.md/images/index/index.svg" alt="Featured image of post AG-UI：当 Agent 学会了和前端说话" />&lt;p>最近在给自己的 Agent Runtime 设计输入输出契约时，我在 README 里画了一张三方对照表：我定义的事件类型一列、Runtime 原生事件一列，第三列空着——那是留给 &lt;a class="link" href="https://github.com/ag-ui-protocol/ag-ui" target="_blank" rel="noopener"
>AG-UI&lt;/a> 的。填完这张表之后我发现，AG-UI 值得不只是一列，它值得被完整讲一遍。为了写这篇文章，我把官方仓库、Python SDK 源码和 PyPI/npm 的发布记录都翻了一遍，下面带着版本号讲。&lt;/p>
&lt;h2 id="先看仓库现状2026-09-10">先看仓库现状（2026-09-10）
&lt;/h2>&lt;p>先把事实钉住，本文所有描述都对应这个版本：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>项&lt;/th>
&lt;th>值&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>仓库&lt;/td>
&lt;td>&lt;a class="link" href="https://github.com/ag-ui-protocol/ag-ui" target="_blank" rel="noopener"
>&lt;code>github.com/ag-ui-protocol/ag-ui&lt;/code>&lt;/a>，创建于 2025-05-07&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>文档&lt;/td>
&lt;td>&lt;a class="link" href="https://docs.ag-ui.com/" target="_blank" rel="noopener"
>docs.ag-ui.com&lt;/a>（draft spec + SDK 参考）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>热度&lt;/td>
&lt;td>&lt;strong>15.8k stars / 1.4k forks&lt;/strong>，MIT License&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>本文对应 commit&lt;/td>
&lt;td>&lt;code>main@5f32a64ee999&lt;/code>（2026-09-09）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Python SDK&lt;/td>
&lt;td>&lt;a class="link" href="https://pypi.org/project/ag-ui-protocol/" target="_blank" rel="noopener"
>&lt;code>ag-ui-protocol&lt;/code>&lt;/a> &lt;strong>0.1.22&lt;/strong>（PyPI，2026-08-31 发布，首版 0.1.4 于 2025-04-30）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>TypeScript SDK&lt;/td>
&lt;td>&lt;a class="link" href="https://www.npmjs.com/package/@ag-ui/core" target="_blank" rel="noopener"
>&lt;code>@ag-ui/core&lt;/code>&lt;/a> 及 client/encoder/proto 系列 &lt;strong>0.0.59&lt;/strong>（npm latest，另有 canary/alpha 通道）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>发布节奏&lt;/td>
&lt;td>几乎&lt;strong>每日&lt;/strong>切一次 release（2026-08-20 到 09-09 切了 5 个），协议仍处 draft 阶段&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Python 包只依赖 &lt;code>pydantic&amp;gt;=2.11.2&lt;/code>，一个依赖，克制得不像一个协议实现。&lt;/p>
&lt;h2 id="三层协议的最后一块">三层协议的最后一块
&lt;/h2>&lt;p>把 2024-2026 这两年 agent 协议的版图摊开，会发现一个很规整的分层：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>MCP&lt;/strong>（Anthropic）：agent ↔ 工具。让模型能标准地&amp;quot;伸手拿东西&amp;quot;。&lt;/li>
&lt;li>&lt;strong>A2A&lt;/strong>（Google）：agent ↔ agent。让多个 agent 标准地互相委派任务。&lt;/li>
&lt;li>&lt;strong>AG-UI&lt;/strong>（CopilotKit）：agent ↔ 前端。让 agent 的运行过程标准地&amp;quot;演给用户看&amp;quot;。&lt;/li>
&lt;/ul>
&lt;p>前两层这几年讨论得足够多了，第三层却长期处于&amp;quot;每家自己造&amp;quot;的状态：LangGraph 自己定义 stream mode，OpenAI Assistants 自己定义 run steps，各家 ChatUI 各自解析各家的事件格式。你写一个 agent 后端，想换个前端，事件层的适配就得重写一遍；你写一个聊天前端，想接不同的 agent，每种 agent 的 SSE 格式都得单独处理。&lt;/p>
&lt;p>AG-UI 的野心就是把这层标准化掉。它不是委员会里设计出来的协议——它是从 CopilotKit 这个产品里长出来的：CopilotKit 做了几年&amp;quot;把 agent 嵌进 React 应用&amp;quot;这件事，攒够了事件类型的实战样本，回头把内部格式提炼成了开放协议，初始合作伙伴是 LangChain（LangGraph）和 CrewAI。这一点从仓库结构里看得见：&lt;code>integrations/&lt;/code> 目录下有 21 个条目——18 个框架适配层、2 个 server starter 模板加一个 community 目录；&lt;code>apps/&lt;/code> 里挂着 dojo（官方演示站）和一个 CLI 示例应用。&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/ag-ui%E5%BD%93agent%E5%AD%A6%E4%BC%9A%E4%BA%86%E5%92%8C%E5%89%8D%E7%AB%AF%E8%AF%B4%E8%AF%9D/index.md/images/three-protocol-layers.svg"
loading="lazy"
alt="三层 Agent 协议:各管一个边界"
>&lt;/p>
&lt;h2 id="仓库解剖一个协议-monorepo-长什么样">仓库解剖：一个协议 monorepo 长什么样
&lt;/h2>&lt;p>官方仓库是一个 pnpm + nx 的 monorepo，顶层结构本身就是协议生态的切片：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">ag-ui/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── sdks/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── python/ # ag-ui-protocol 0.1.22 (core + encoder)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── typescript/ # @ag-ui/core|client|encoder|proto 0.0.59 + cli
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── dotnet/ # 官方 .NET SDK
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── community/ # dart / rust / ruby / c++ / kotlin / go / java ...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── integrations/ # 20 个框架适配层(见后文生态一节)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── middlewares/ # a2a / a2ui / mcp / mcp-apps / event-throttle
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── apps/ # dojo(官方演示) + client-cli-example
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└── docs/ # 文档源
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>两个细节值得注意：&lt;strong>middlewares 是独立一层&lt;/strong>——A2A 桥接、MCP 桥接、MCP Apps、A2UI、事件节流各占一个包，说明协议把&amp;quot;和别的协议对接&amp;quot;也当成了标准件而不是用户自理；&lt;strong>protocol 有 TypeScript 和 Python 两套官方实现&lt;/strong>，且明确以 TypeScript 为参考实现（源码注释里直接写着 TS 的行为是其他 SDK 对齐的基准）。&lt;/p>
&lt;h2 id="事件流是唯一事实源">事件流是唯一事实源
&lt;/h2>&lt;p>AG-UI 的核心设计只有一句话：&lt;strong>agent 的一次运行 = 一条有序事件流，前端状态完全由事件序列归约得到&lt;/strong>。&lt;/p>
&lt;p>没有独立的 REST 查询接口，没有&amp;quot;拉取当前状态&amp;quot;的端点。前端想知道 agent 在干什么？订阅事件流，逐条归约。想在断线后恢复？重新拿一遍事件（或快照），重放。这个思想和事件溯源（Event Sourcing）一脉相承，也和我自己的 Canonical Run 契约不谋而合——事件即事实，投影即状态。&lt;/p>
&lt;p>协议在 0.1.22 的定义里共有 &lt;strong>36 个事件枚举成员&lt;/strong>——31 个在用，加上 5 个已废弃的 &lt;code>THINKING_*&lt;/code>（1.0.0 移除）。全部继承自一个四字段信封：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;TEXT_MESSAGE_CONTENT&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;timestamp&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1730000000&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;rawEvent&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">null&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;metadata&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>按功能分组看一眼全集，就能感受到协议想覆盖的交互光谱：&lt;/p>
&lt;p>&lt;strong>生命周期（5 种）&lt;/strong>——&lt;code>RUN_STARTED&lt;/code>、&lt;code>RUN_FINISHED&lt;/code>、&lt;code>RUN_ERROR&lt;/code> 管起止，&lt;code>STEP_STARTED&lt;/code>/&lt;code>STEP_FINISHED&lt;/code> 标记过程里的小步。注意 run 的两个标识 &lt;code>threadId&lt;/code> 和 &lt;code>runId&lt;/code> 都是客户端生成的，服务端在 &lt;code>RUN_STARTED&lt;/code> 里回显——这和 OpenAI 式&amp;quot;服务端生成 ID 返回给你&amp;quot;正好相反。&lt;/p>
&lt;p>&lt;strong>文本消息（4 种）&lt;/strong>——经典三段式 &lt;code>TEXT_MESSAGE_START&lt;/code> → &lt;code>TEXT_MESSAGE_CONTENT&lt;/code>（带 &lt;code>delta&lt;/code>）→ &lt;code>TEXT_MESSAGE_END&lt;/code>，外加一个 &lt;code>TEXT_MESSAGE_CHUNK&lt;/code> 快捷形式：省掉配对开销，客户端自己展开。消息靠 &lt;code>messageId&lt;/code> 关联。&lt;/p>
&lt;p>&lt;strong>工具调用（5 种）&lt;/strong>——&lt;code>TOOL_CALL_START&lt;/code>（带 &lt;code>toolCallId&lt;/code> 和 &lt;code>toolCallName&lt;/code>）→ &lt;code>TOOL_CALL_ARGS&lt;/code>（参数 JSON 增量下发，支持流式渲染参数）→ &lt;code>TOOL_CALL_END&lt;/code>，加上 &lt;code>TOOL_CALL_RESULT&lt;/code> 和 &lt;code>TOOL_CALL_CHUNK&lt;/code>。&lt;/p>
&lt;p>&lt;strong>共享状态（3 种）&lt;/strong>——这是 AG-UI 最有辨识度的部分。&lt;code>STATE_SNAPSHOT&lt;/code> 全量下发一份 typed state，之后 &lt;code>STATE_DELTA&lt;/code> 用 &lt;strong>RFC 6902 JSON Patch&lt;/strong> 增量同步。这意味着前端不只是&amp;quot;看&amp;quot;agent 生成文本，而是和 agent 共享一份可编辑的应用状态——agent 改了表单、改了画布、改了文档大纲，前端实时跟着变。CopilotKit 的生成式 UI 就建立在这上面。&lt;/p>
&lt;p>&lt;strong>活动消息（2 种）&lt;/strong>——&lt;code>ACTIVITY_SNAPSHOT&lt;/code>/&lt;code>ACTIVITY_DELTA&lt;/code>，聊天消息之间的结构化进度材料，为 generative UI 准备的通道。&lt;/p>
&lt;p>&lt;strong>推理（7 种）&lt;/strong>——&lt;code>REASONING_*&lt;/code> 一族把思维链流式透出（&lt;code>REASONING_MESSAGE_CONTENT&lt;/code> 的 &lt;code>delta&lt;/code>），甚至有 &lt;code>REASONING_ENCRYPTED_VALUE&lt;/code> 用于加密透传私有推理。另有 5 个旧的 &lt;code>THINKING_*&lt;/code> 事件已废弃、1.0.0 移除。&lt;/p>
&lt;p>&lt;strong>子代理（3 种）&lt;/strong>——&lt;code>SUBAGENT_STARTED&lt;/code>/&lt;code>FINISHED&lt;/code>/&lt;code>ERROR&lt;/code>，用 &lt;code>subagentRunId&lt;/code> + &lt;code>parentSubagentRunId&lt;/code> 构成树。有个字段很说明问题：&lt;code>SUBAGENT_STARTED&lt;/code> 带可选的 &lt;code>parentToolCallId&lt;/code>，直接支持&amp;quot;agent-as-tool&amp;quot;模式——子代理是父代理调用的一个工具（deep agents 的 &lt;code>task&lt;/code> 就是这个模式），事件树和工具调用树是同一棵。&lt;/p>
&lt;p>&lt;strong>特殊（2 种）&lt;/strong>——&lt;code>RAW&lt;/code>（底层框架事件透传）和 &lt;code>CUSTOM&lt;/code>（自定义事件通道）。&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/ag-ui%E5%BD%93agent%E5%AD%A6%E4%BC%9A%E4%BA%86%E5%92%8C%E5%89%8D%E7%AB%AF%E8%AF%B4%E8%AF%9D/index.md/images/event-stream-model.svg"
loading="lazy"
alt="事件流是唯一事实源:BaseEvent 信封与 36 个枚举的九个功能组"
>&lt;/p>
&lt;h2 id="能力声明协议里藏着一份agent-名片">能力声明：协议里藏着一份&amp;quot;agent 名片&amp;quot;
&lt;/h2>&lt;p>读 Python SDK 源码时的一个意外发现：除了事件和输入类型，&lt;code>ag_ui/core/&lt;/code> 下还有一个 &lt;code>capabilities.py&lt;/code>（415 行），定义了一整套 &lt;strong>&lt;code>AgentCapabilities&lt;/code> 能力声明体系&lt;/strong>，十个分类：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>分类&lt;/th>
&lt;th>回答的问题&lt;/th>
&lt;th>典型字段&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>identity&lt;/code>&lt;/td>
&lt;td>你是谁&lt;/td>
&lt;td>name / type(框架标识) / version / provider / documentation_url&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>transport&lt;/code>&lt;/td>
&lt;td>怎么连你&lt;/td>
&lt;td>streaming / websocket / http_binary / push_notifications / &lt;strong>resumable&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>tools&lt;/code>&lt;/td>
&lt;td>你能调什么&lt;/td>
&lt;td>supported / 自带工具清单(完整 JSON Schema) / parallel_calls / &lt;strong>client_provided&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>output&lt;/code>&lt;/td>
&lt;td>你产出什么&lt;/td>
&lt;td>structured_output / supported_mime_types&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>state&lt;/code>&lt;/td>
&lt;td>你的状态怎么管&lt;/td>
&lt;td>snapshots / deltas / &lt;strong>memory&lt;/strong>(跨会话记忆) / persistent_state&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>multi_agent&lt;/code>&lt;/td>
&lt;td>你和谁协作&lt;/td>
&lt;td>delegation / handoffs / sub_agents 清单&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>reasoning&lt;/code>&lt;/td>
&lt;td>你的思考可见吗&lt;/td>
&lt;td>supported / streaming / &lt;strong>encrypted&lt;/strong>(零数据保留模式)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>multimodal&lt;/code>&lt;/td>
&lt;td>什么模态进、什么出&lt;/td>
&lt;td>input: image/audio/video/pdf/file; output: image/audio&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>execution&lt;/code>&lt;/td>
&lt;td>你的执行边界&lt;/td>
&lt;td>code_execution / sandboxed / max_iterations / max_execution_time&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>human_in_the_loop&lt;/code>&lt;/td>
&lt;td>人在哪介入&lt;/td>
&lt;td>approvals / interventions / feedback / &lt;strong>interrupts&lt;/strong> / approve_with_edits&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>关键语义写在 docstring 里：&lt;strong>&amp;ldquo;所有字段可选，缺省意味着未声明（unknown），不等于不支持&amp;rdquo;&lt;/strong>——典型的开放协议渐进声明风格；外加 &lt;code>custom&lt;/code> 逃生舱给集成特有能力。这份能力体系和我之前给自己协议设计 discovery 契约（&lt;code>RuntimeDescriptor&lt;/code>）时想解决的问题一模一样：让调用方在运行前就知道对端有什么。AG-UI 的答案粒度更细——它同时服务于&amp;quot;agent 市场、发现 UI、调试&amp;quot;三类消费场景，甚至 &lt;code>sub_agents&lt;/code> 清单的注释都写着&amp;quot;帮助客户端构建 agent 选择界面&amp;quot;。&lt;/p>
&lt;h2 id="输入全量历史的无状态哲学">输入：全量历史的无状态哲学
&lt;/h2>&lt;p>事件流往回走，输入往前送。AG-UI 的请求体 &lt;code>RunAgentInput&lt;/code> 长这样：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">class&lt;/span> &lt;span class="nc">RunAgentInput&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">BaseModel&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">thread_id&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span> &lt;span class="c1"># 会话标识&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">run_id&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span> &lt;span class="c1"># 本次运行,客户端生成&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">parent_run_id&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span> &lt;span class="o">|&lt;/span> &lt;span class="kc">None&lt;/span> &lt;span class="c1"># agent-started-agent 场景的父 run&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">state&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Any&lt;/span> &lt;span class="c1"># 起始共享状态&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">messages&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">list&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">Message&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="c1"># 全量对话历史,按序&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">tools&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">list&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">Tool&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="c1"># 前端工具定义&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">context&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">list&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">Context&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="c1"># 注入上下文&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">forwarded_props&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Any&lt;/span> &lt;span class="c1"># 透传段,中间层不得改动&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">resume&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">list&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">ResumeEntry&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">|&lt;/span> &lt;span class="kc">None&lt;/span> &lt;span class="c1"># 中断应答&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>最有态度的是 &lt;code>messages&lt;/code>：&lt;strong>全量历史回传，按序，无旁路通道&lt;/strong>。服务器不存对话记忆，每一轮前端把完整历史发过来。这是刻意的——无状态服务器好扩展、好重放、好调试，代价是每轮请求变大、长对话依赖客户端裁剪。&lt;/p>
&lt;p>消息定义了七种角色：&lt;code>developer&lt;/code>、&lt;code>system&lt;/code>、&lt;code>assistant&lt;/code>（可带 OpenAI 风格的 &lt;code>tool_calls&lt;/code>）、&lt;code>user&lt;/code>（支持多模态：text/image/audio/video/document，URL 或 base64 inline）、&lt;code>tool&lt;/code>（工具结果）、&lt;code>activity&lt;/code>（渲染材料，发送前必须剥离，不作为恢复历史）、&lt;code>reasoning&lt;/code>。&lt;/p>
&lt;p>人在回路是一个完整闭环：&lt;code>RUN_FINISHED&lt;/code> 的 &lt;code>outcome&lt;/code> 可以是 &lt;code>{type: &amp;quot;interrupt&amp;quot;, interrupts: [...]}&lt;/code>——agent 停下来等输入，且源码里有校验：&lt;strong>interrupt outcome 至少携带一个 interrupt&lt;/strong>，空的直接构造失败。每个 interrupt 带 &lt;code>id&lt;/code>、&lt;code>reason&lt;/code>、可选的 &lt;code>response_schema&lt;/code> 和 &lt;code>expires_at&lt;/code>；客户端下一轮在 &lt;code>resume&lt;/code> 数组里逐条应答（&lt;code>resolved&lt;/code> 或 &lt;code>cancelled&lt;/code>，可带 &lt;code>payload&lt;/code>）——审批、确认、澄清问题，都是这个机制的产品化。能力声明里的 &lt;code>approve_with_edits&lt;/code> 说明连&amp;quot;批准前修改参数&amp;quot;都在协议考虑内。&lt;/p>
&lt;h2 id="从-sdk-源码里读出的工程细节">从 SDK 源码里读出的工程细节
&lt;/h2>&lt;p>我读了 Python SDK（&lt;code>ag-ui-protocol&lt;/code> 0.1.22）的几个核心模块，这几处注释值得单独说：&lt;/p>
&lt;p>&lt;strong>序列化的血泪史&lt;/strong>。基类 &lt;code>ConfiguredBaseModel&lt;/code> 自定义了 serializer：可选字段为 None 时，整个键从线上 JSON 省略，而不是写 &lt;code>null&lt;/code>。注释里明说了缘由——Python SDK 曾是唯一把 null 写上线的 producer，为此协议吃了三个兼容补丁（&lt;code>TOOL_CALL_START.parentMessageId&lt;/code>、&lt;code>TOOL_CALL_CHUNK.parentMessageId&lt;/code>、&lt;code>RUN_FINISHED.outcome&lt;/code>）。类型注释里那句&amp;quot;在基类上统一省略，才能保证每一条序列化路径都生效&amp;quot;是一位工程师被 null 坑过之后的防御性姿势。字段 camelCase 上线（&lt;code>alias_generator=to_camel&lt;/code>），&lt;code>extra=&amp;quot;allow&amp;quot;&lt;/code> 放行未知字段——协议演进不锁死旧 producer。&lt;/p>
&lt;p>&lt;strong>token 计数的跨语言红线&lt;/strong>。&lt;code>token_usage.py&lt;/code> 里有一个细节：所有计数上限被钳在 &lt;strong>2⁵³−1&lt;/strong>——不是 int64 的上限，而是因为&amp;quot;TypeScript 的 protobuf 解码器停在 &lt;code>Number.MAX_SAFE_INTEGER&lt;/code>，这才是各语言绑定之间的真实天花板&amp;quot;。而且注释明确写了这个结构会喂给匿名遥测，&lt;strong>不允许携带任何内容字段&lt;/strong>（无 prompt、无补全、无 thread/run/user ID），只有 provider/model 标签和纯数字。跨语言兼容和隐私边界都焊死在类型定义里。&lt;/p>
&lt;p>&lt;strong>metadata 的克制&lt;/strong>。每个事件都带可选 &lt;code>metadata&lt;/code>，开放键值空间，但保留 &lt;code>&amp;quot;ag-ui&amp;quot;&lt;/code> 键给协议自己用——而且是纯约定，不做运行时强制。注释的原话：验证它的 shape 会&amp;quot;contradict that&amp;quot;（与开放性矛盾）。&lt;/p>
&lt;p>&lt;strong>双编码传输&lt;/strong>。&lt;code>encoder.py&lt;/code> 不只是 SSE——协议定义了二进制编码，媒体类型 &lt;code>application/vnd.ag-ui.event+proto&lt;/code>（protobuf over HTTP），TypeScript 侧有独立的 &lt;code>@ag-ui/proto&lt;/code> 包维护 &lt;code>.proto&lt;/code> 定义。文本 SSE 用于开发调试，二进制用于生产，同一套事件类型两种编码。&lt;/p>
&lt;h2 id="运行时管线一次-run-的完整解剖">运行时管线：一次 run 的完整解剖
&lt;/h2>&lt;p>前面讲的是协议的&amp;quot;静态&amp;quot;部分（类型和字段）。协议真正的工程含量在客户端 SDK 的&lt;strong>运行时管线&lt;/strong>里——一次 &lt;code>runAgent()&lt;/code> 调用背后是一条精心编排的 RxJS 管道。以 TypeScript &lt;code>@ag-ui/client&lt;/code> 0.0.59 的 &lt;code>AbstractAgent.runAgent()&lt;/code>（&lt;code>src/agent/agent.ts&lt;/code>）为例，事件从网络到你的回调要穿过这条流水线：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">run()（子类实现,发 HTTP/SSE 请求）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ↓ pipe 依次串接:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">① middleware 链 ← reduceRight 组装成洋葱模型
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">② transformChunks ← CHUNK 快捷事件展开成三段式
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">③ verifyEvents ← 事件文法状态机校验
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">④ takeUntil(detach$) ← 单次运行的中止信号
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">⑤ apply ← 把事件归约进 messages/state
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">⑥ processApplyEvents ← 分发到 subscriber 回调
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">⑦ catchError / finalize ← 错误与收尾
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>每一层都可以单独讲。这正是&amp;quot;看哪些文件&amp;quot;的答案：读懂这七个环节，你就读懂了 AG-UI 客户端的全部。&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/ag-ui%E5%BD%93agent%E5%AD%A6%E4%BC%9A%E4%BA%86%E5%92%8C%E5%89%8D%E7%AB%AF%E8%AF%B4%E8%AF%9D/index.md/images/runtime-pipeline.svg"
loading="lazy"
alt="一次 run 的运行管线:事件从网络到回调穿过七个环节"
>&lt;/p>
&lt;h3 id="-middleware洋葱模型的拦截器">① Middleware：洋葱模型的拦截器
&lt;/h3>&lt;p>中间件机制定义在 &lt;code>src/middleware/middleware.ts&lt;/code>。核心接口只有一个方法：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl">&lt;span class="kr">abstract&lt;/span> &lt;span class="kr">class&lt;/span> &lt;span class="nx">Middleware&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">abstract&lt;/span> &lt;span class="nx">run&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">input&lt;/span>: &lt;span class="kt">RunAgentInput&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">next&lt;/span>: &lt;span class="kt">AbstractAgent&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">Observable&lt;/span>&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">BaseEvent&lt;/span>&lt;span class="p">&amp;gt;;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 调下一个 agent,附带 chunk 展开
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kr">protected&lt;/span> &lt;span class="nx">runNext&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">input&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">next&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">next&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">run&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">input&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">pipe&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">transformChunks&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="p">));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 调下一个,且把每一步之后的 messages/state 一起带出来
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kr">protected&lt;/span> &lt;span class="nx">runNextWithState&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">input&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">next&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">Observable&lt;/span>&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">EventWithState&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>组装方式是 &lt;code>agent.ts&lt;/code> 里的 &lt;code>reduceRight&lt;/code>——数组里&lt;strong>后注册的中间件先执行&lt;/strong>，&lt;code>next&lt;/code> 指向链上更内层的 agent（最内层是真正 agent 的 &lt;code>run()&lt;/code>）。这就是经典的洋葱模型，和 Koa/Express 中间件一个思想。&lt;/p>
&lt;p>有意思的是 &lt;code>runNextWithState&lt;/code> 的实现：它用一个 &lt;code>ReplaySubject&lt;/code> 把事件喂给 &lt;code>defaultApplyEvents&lt;/code>（事件归约器），让中间件在&lt;strong>每个事件后&lt;/strong>都能拿到&amp;quot;应用了这个事件之后&amp;quot;的 messages 和 state——中间件因此能基于语义状态做决策，而不只是看原始事件。实现里那句 &lt;code>await new Promise(resolve =&amp;gt; setTimeout(resolve, 0))&lt;/code> 是给归约器留一个微任务窗口同步状态，朴素但有效。&lt;/p>
&lt;p>官方仓库 &lt;code>middlewares/&lt;/code> 目录下有 6 个可参考的成品：&lt;code>event-throttle&lt;/code>（按帧率节流 + 合并 delta）、&lt;code>mcp&lt;/code>（把 MCP 工具桥接进事件流）、&lt;code>a2a&lt;/code>（A2A 协议互转）、&lt;code>a2ui&lt;/code>、&lt;code>mcp-apps&lt;/code>，外加一个 &lt;code>middleware-starter&lt;/code> 脚手架。&lt;/p>
&lt;h3 id="-三个值得抄的中间件设计">② 三个值得抄的中间件设计
&lt;/h3>&lt;p>&lt;strong>EventThrottleMiddleware&lt;/strong>（&lt;code>middlewares/event-throttle-middleware/src/index.ts&lt;/code>）——把高频 delta 按时间窗（默认 16ms ≈ 60fps）和最小字符数节流并合并，防止前端被打爆。它的三个设计决策都写满了&amp;quot;为什么&amp;quot;：&lt;/p>
&lt;p>其一，&lt;strong>只对白名单事件做缓冲&lt;/strong>。&lt;code>BUFFERABLE_EVENT_TYPES&lt;/code> 是显式白名单（各种 &lt;code>*_CHUNK&lt;/code>、&lt;code>*_CONTENT&lt;/code>、&lt;code>STATE_*&lt;/code>、&lt;code>ACTIVITY_*&lt;/code>），&lt;strong>不在表内的事件一律立即透传&lt;/strong>。注释原话：&amp;ldquo;这是白名单而非黑名单，这样协议将来新增的事件类型默认走立即透传——对生命周期/边界事件来说，这是更安全的失败模式。&amp;ldquo;新事件宁可多一次渲染，也不能被错误地延迟。&lt;/p>
&lt;p>其二，&lt;strong>合并只在同一 subagent lane 内进行&lt;/strong>。chunk 的合并键是 &lt;code>JSON.stringify([kind, owner, entityId])&lt;/code>——用 JSON 编码而不是分隔符拼接，因为 owner/id 是任意字符串，&amp;ldquo;任何分隔符都可能出现在某个分量里，把两个不同的 (owner, id) 别名到同一个键上&amp;rdquo;。这个注释值得所有写缓存键的人读一遍。&lt;/p>
&lt;p>其三，&lt;strong>metadata 反向合并&lt;/strong>。合并两个 chunk 时，&lt;code>role&lt;/code>/&lt;code>name&lt;/code> 取&lt;strong>第一个&lt;/strong>（它们只出现在首 chunk），但 &lt;code>metadata&lt;/code> 取&lt;strong>最后一个&lt;/strong>并按 key 逐项合并——因为 metadata 是设计成&amp;quot;最后到达&amp;quot;的，携带 usage 和 finish reason。&amp;ldquo;在这里丢掉它会吞掉一个纯 usage 的尾 chunk。&amp;rdquo;&lt;/p>
&lt;p>&lt;strong>FilterToolCallsMiddleware&lt;/strong>（&lt;code>src/middleware/filter-tool-calls.ts&lt;/code>）——按工具名拦截工具调用事件。看起来平平无奇，但注释记了一个真实并发 bug：中间件实例是跨 run 复用的，如果被拦截的 toolCallId 集合放在&lt;strong>实例&lt;/strong>上，&amp;ldquo;一个卡住的 run 的订阅还开着时，下一个 run 启动会抹掉那些还在过滤这个卡住的 run 的 id，于是它被禁的工具的 &lt;code>TOOL_CALL_ARGS&lt;/code>/&lt;code>END&lt;/code>/&lt;code>RESULT&lt;/code> 就开始漏出来了&amp;rdquo;。解法是用 &lt;code>defer&lt;/code> 给&lt;strong>每个订阅&lt;/strong>一份独立集合，并在 &lt;code>RUN_STARTED&lt;/code> 时重置。并发场景下&amp;quot;状态放实例还是放订阅&amp;quot;这个坑，这是教科书级的案例。&lt;/p>
&lt;p>&lt;strong>四个 BackwardCompatibility 中间件&lt;/strong>（&lt;code>src/middleware/backward-compatibility-0-0-*.ts&lt;/code>）——这是我见过最优雅的协议演进手法。agent 构造时读对端声明的 &lt;code>maxVersion&lt;/code>，按版本号&lt;strong>自动插到中间件链最前面&lt;/strong>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">compareVersions&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">maxVersion&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;0.0.39&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">&amp;lt;=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">middlewares&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">unshift&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">new&lt;/span> &lt;span class="nx">BackwardCompatibility_0_0_39&lt;/span>&lt;span class="p">());&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">compareVersions&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">maxVersion&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;0.0.57&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">&amp;lt;=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 0.0.57 之前的 agent 不认识 subagent:剥掉 subagentRunId、丢弃 SUBAGENT_* 事件
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">middlewares&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">unshift&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">new&lt;/span> &lt;span class="nx">BackwardCompatibility_0_0_57&lt;/span>&lt;span class="p">());&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>0.0.45&lt;/code> 中间件把废弃的 &lt;code>THINKING_*&lt;/code> 事件翻译成新的 &lt;code>REASONING_*&lt;/code>，&lt;code>0.0.47&lt;/code> 把老的 &lt;code>BinaryInputContent&lt;/code> 映射成新的分模态输入类型，&lt;code>0.0.57&lt;/code> 给不认识 subagent 的老客户端剥掉归属字段。&lt;strong>每个协议破坏性变更都固化为一个可测试、可移除的适配中间件&lt;/strong>，而不是散落在业务代码里的 if-else。协议处于日更的 draft 阶段还能保持向后兼容，靠的就是这套机制。&lt;/p>
&lt;h3 id="-verifyevents1052-行的事件文法状态机">③ verifyEvents：1052 行的事件文法状态机
&lt;/h3>&lt;p>&lt;code>src/verify/verify.ts&lt;/code> 是整个客户端最硬核的文件——它把&amp;quot;合法的 AG-UI 事件流&amp;quot;编码成了一个状态机，逐事件校验。维护四组&amp;quot;哪些实体还开着&amp;quot;的集合（文本消息、工具调用、推理、活动），外加每个实体类型的&lt;strong>归属映射&lt;/strong>（哪个 subagent 开的）。&lt;/p>
&lt;p>这个文件的注释本身就是一部协议演进史，每条规则背后都是真实 bug：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>为什么 owners 按实体类别分桶&lt;/strong>：一个 message 和一个 tool call 可以都叫 &amp;ldquo;x&amp;rdquo; 而不冲突。早期单个桶时，&amp;ldquo;tool call 的写入会覆盖 message 的 owner，导致后续一个加密值拿了错误的 owner 做校验而被接受&amp;rdquo;。&lt;/li>
&lt;li>&lt;strong>为什么 owners 关闭后不清除&lt;/strong>：&lt;code>REASONING_ENCRYPTED_VALUE(subtype=&amp;quot;tool-call&amp;quot;)&lt;/code> 合法地要在 &lt;code>TOOL_CALL_END&lt;/code> &lt;strong>之后&lt;/strong>到达，&amp;ldquo;清掉 owner 会让这个不匹配变得无法匹配从而接受一个错误的&amp;rdquo;。&lt;/li>
&lt;li>&lt;strong>为什么 step 要按 (owner, name) 嵌套 Map 而不是拼字符串&lt;/strong>：曾经用分隔符拼接 owner 和 name，结果&amp;quot;没有 owner 的父级会和 subagent id 是空字符串的实体撞键&amp;rdquo;——而空字符串是合法的 opaque id。注释直言：&amp;ldquo;有个设计伙伴从真实的 deepagents run 里报回了这个 bug：合法的嵌套 step 被拒，而非法的跨 owner 关闭却被接受。&amp;rdquo;&lt;/li>
&lt;li>&lt;strong>为什么 &lt;code>STEP_STARTED&lt;/code> 判重按 owner 分&lt;/strong>：父 agent 和子 agent 常常跑同一个图，两边&lt;strong>同时&lt;/strong>有一个叫 &amp;ldquo;tools&amp;rdquo; 的 step 是合法的（父的包着委派，子的是自己的内部工作）——按名字单独判重会把合法嵌套当成错误拒绝。&lt;/li>
&lt;/ul>
&lt;p>这些不是设计文档里能写出来的东西，全是生产流量打磨出来的。想理解 AG-UI 的边界条件，&lt;code>verify.ts&lt;/code> 比 spec 更有信息量。&lt;/p>
&lt;h3 id="-transformchunks快捷事件的双向门">④ transformChunks：快捷事件的双向门
&lt;/h3>&lt;p>&lt;code>src/chunks/transform.ts&lt;/code>（564 行）负责 &lt;code>*_CHUNK&lt;/code> 快捷事件和 &lt;code>*_START/CONTENT/END&lt;/code> 三段式之间的转换。核心概念是 &lt;strong>lane（通道）&lt;/strong>：每个 subagent 一条，&amp;ldquo;一条 lane 上最多有一路正在组装的流，因为 chunk 的简写只靠&amp;rsquo;和之前一样&amp;rsquo;来标识延续&amp;rdquo;。这里也有个精妙的元数据规则——chunk 的 metadata 会扩散到由它合成的每个事件上，&lt;strong>但绝不施加于关闭前一条消息的合成 &lt;code>*_END&lt;/code>&lt;/strong>，&amp;ldquo;这就是防止某个 chunk 的 metadata 泄漏到它正在关闭的那条消息上的原因&amp;rdquo;。&lt;/p>
&lt;h3 id="-apply-与-subscribers归约器和观察者">⑤ apply 与 subscribers：归约器和观察者
&lt;/h3>&lt;p>&lt;code>src/apply/default.ts&lt;/code>（约 1500 行）是事件归约器——一个巨型 &lt;code>switch(event.type)&lt;/code>，把每一种事件映射成 &lt;code>messages&lt;/code>/&lt;code>state&lt;/code> 的变更（&lt;code>AgentStateMutation&lt;/code>）。这是&amp;quot;事件即事实、状态即投影&amp;quot;落地的地方。它对 &lt;code>*_CHUNK&lt;/code> 事件直接抛错（&lt;code>TEXT_MESSAGE_CHUNK must be transformed before being applied&lt;/code>）——这是管线的纪律：chunk 必须先过 &lt;code>transformChunks&lt;/code> 这道门，归约器只认展开后的三段式。&lt;/p>
&lt;p>&lt;code>src/agent/subscriber.ts&lt;/code>（408 行）则是应用层的观察者接口，钩子分三类：生命周期（&lt;code>onRunInitialized&lt;/code>/&lt;code>onRunFailed&lt;/code>/&lt;code>onRunFinalized&lt;/code>）、按事件类型（&lt;code>onTextMessageContentEvent&lt;/code> 连 &lt;code>textMessageBuffer&lt;/code> 都给你拼好了、&lt;code>onToolCallArgsEvent&lt;/code> 带 &lt;code>toolCallBuffer&lt;/code> 和 &lt;code>toolCallName&lt;/code>）、变更通知（&lt;code>onMessagesChanged&lt;/code>/&lt;code>onStateChanged&lt;/code>）。&lt;/p>
&lt;p>这里有个订阅者协议的关键设计：&lt;strong>订阅者不直接改状态，而是返回一个 &lt;code>AgentStateMutation&lt;/code>&lt;/strong>，由 &lt;code>runSubscribersWithMutation&lt;/code> 统一应用。多个订阅者的 mutation 可以 &lt;code>stopPropagation&lt;/code> 拦截后续订阅者——订阅者之间也是中间件式的。实现里还有一段性能注释：dev 环境会对输入做 &lt;code>structuredClone&lt;/code> + &lt;code>deepFreeze&lt;/code> 来抓&amp;quot;原地修改&amp;quot;的 bug，但这个守卫是&amp;quot;每个事件最大的一笔分配&amp;rdquo;，流式大参数时会把 V8 堆打爆，所以它在生产环境关闭、dev 环境下 payload 过大时也跳过——&amp;ldquo;常见的不改状态的事件cost 零次克隆&amp;rdquo;。&lt;/p>
&lt;h2 id="怎么读-ag-ui-的源码一份导航">怎么读 AG-UI 的源码：一份导航
&lt;/h2>&lt;p>如果你要接 AG-UI 或者只是想学它的设计，按这个顺序读最省时间：&lt;/p>
&lt;p>&lt;strong>第一梯队——协议本身（先读，两小时）&lt;/strong>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>文件&lt;/th>
&lt;th>读什么&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>sdks/python/ag_ui/core/events.py&lt;/code>&lt;/td>
&lt;td>全部事件类型（555 行，每个类型的字段和约束都在）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>sdks/python/ag_ui/core/types.py&lt;/code>&lt;/td>
&lt;td>&lt;code>RunAgentInput&lt;/code>、七种消息、多模态、中断类型（416 行）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>sdks/python/ag_ui/core/capabilities.py&lt;/code>&lt;/td>
&lt;td>能力声明体系（415 行，看注释里的&amp;quot;为什么这样设计&amp;quot;）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>sdks/python/ag_ui/encoder/encoder.py&lt;/code>&lt;/td>
&lt;td>SSE 与二进制编码（很短，看序列化约定）&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>第二梯队——客户端运行时（TypeScript &lt;code>packages/client/src/&lt;/code>，深读）&lt;/strong>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>文件&lt;/th>
&lt;th>读什么&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>agent/agent.ts&lt;/code>&lt;/td>
&lt;td>运行管线七环节 + middleware 链组装 + 生命周期（看 &lt;code>runAgent&lt;/code>）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>middleware/middleware.ts&lt;/code>&lt;/td>
&lt;td>洋葱模型基类 + &lt;code>runNextWithState&lt;/code> 的状态追踪&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>middleware/backward-compatibility-0-0-*.ts&lt;/code>&lt;/td>
&lt;td>协议演进如何固化成中间件（四份文件一起看）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>verify/verify.ts&lt;/code>&lt;/td>
&lt;td>事件文法状态机（注释里的 bug 案例最值钱）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>apply/default.ts&lt;/code>&lt;/td>
&lt;td>事件归约器（看状态怎么从事件投影出来）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>chunks/transform.ts&lt;/code> + &lt;code>agent/subscriber.ts&lt;/code>&lt;/td>
&lt;td>chunk 双向门 + 订阅者协议&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>第三梯队——生态接线&lt;/strong>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>目录&lt;/th>
&lt;th>读什么&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>integrations/langgraph/{python,typescript}/&lt;/code>&lt;/td>
&lt;td>一个真实框架适配层的全貌（含中断处理、SSE 断线恢复测试）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>middlewares/event-throttle-middleware/&lt;/code>&lt;/td>
&lt;td>生产级中间件的最完整示例（含性能和不变量注释）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>apps/dojo/&lt;/code>&lt;/td>
&lt;td>官方演示站，交互式体验所有事件类型&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>一个实用建议：读 TS 客户端时&lt;strong>先读注释再读代码&lt;/strong>——这个仓库的注释密度和坦诚度罕见，&amp;ldquo;我们当初这么写、后来发现 X 有问题、现在的写法是 Y、原因是 Z&amp;quot;的叙事随处可见。很多边界条件 spec 里没有，但注释里有。&lt;/p>
&lt;h2 id="生态谁在用它">生态：谁在用它
&lt;/h2>&lt;p>AG-UI 的知名产品就是创造它的 &lt;a class="link" href="https://www.copilotkit.ai/" target="_blank" rel="noopener"
>CopilotKit&lt;/a>（React 前端 agent 框架，&lt;code>useCopilotAction&lt;/code> 的 human-in-the-loop 模式就是协议里前端工具机制的产品化），加上官方 demo 站 &lt;strong>Dojo&lt;/strong>（&lt;code>apps/dojo&lt;/code>，仓库内）。&lt;/p>
&lt;p>但真正的覆盖面在 &lt;code>integrations/&lt;/code> 目录里，18 个框架适配层的原文清单：LangGraph、LangChain、CrewAI、Microsoft Agent Framework、Google ADK、AWS Strands、Agno、Mastra、Pydantic AI、LlamaIndex、AG2、Vercel AI SDK、watsonx、Langroid、&lt;strong>Claude Agent SDK&lt;/strong>、&lt;strong>Claude Managed Agents&lt;/strong>、A2A、Agent Spec，另有两个 server starter 模板。社区 SDK 覆盖 Dart、Rust、Ruby、C++、Kotlin、Go、Java。客户端不止浏览器——终端、React Native、Slack、Teams 都能当 AG-UI client。&lt;/p>
&lt;p>不过要校准一下预期：AG-UI 的&amp;quot;知名&amp;quot;集中在&lt;strong>框架生态&lt;/strong>，不是终端产品。和 MCP 一样，它是管道协议，终端用户永远看不到它——人们用的是接了它的 CopilotKit 应用和 LangGraph agent。&lt;/p>
&lt;h2 id="和有状态-runtime-的张力">和有状态 Runtime 的张力
&lt;/h2>&lt;p>最后说一个我实际设计契约时撞上的问题。AG-UI 的无状态模型很优雅，但我的 Runtime 是&lt;strong>有状态&lt;/strong>的：对话记忆按 &lt;code>session_id&lt;/code> 存在服务端（SQLite checkpointer），每次 Run 只需要送最新的问题。&lt;/p>
&lt;p>两套范式接在一起时的正确姿势是：AG-UI 前端发全量 &lt;code>messages&lt;/code>，adapter 只取&lt;strong>最后一条 user 消息&lt;/strong>作为 Runtime 的输入，历史由服务端记忆提供；事件流方向则反过来，把 Runtime 的事件投影成 AG-UI 事件喂给前端。适配层的本质是两种会话范式之间的翻译。&lt;/p>
&lt;p>还有几块 AG-UI 有、多数自研契约没有的东西，值得列为后续演进的清单：interrupt/resume 的人在回路（自研契约通常只有&amp;quot;取消&amp;rdquo;，没有&amp;quot;挂起等输入&amp;quot;）、共享状态的 StateSnapshot/StateDelta、子代理事件树、前端工具的反向执行回路，以及那份克制的能力声明体系。&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/ag-ui%E5%BD%93agent%E5%AD%A6%E4%BC%9A%E4%BA%86%E5%92%8C%E5%89%8D%E7%AB%AF%E8%AF%B4%E8%AF%9D/index.md/images/stateful-vs-stateless.svg"
loading="lazy"
alt="两种会话范式对比:AG-UI 的无状态服务器 vs Canonical Runtime 的服务端记忆"
>&lt;/p>
&lt;p>三层协议凑齐之后的图景其实很清晰：MCP 让 agent 拿到工具，A2A 让 agent 找到同伴，AG-UI 让用户看见过程。你的 agent 用哪套实现无所谓——只要它在这三个边界上说标准语言，就同时获得了被所有前端渲染、被所有 agent 编排、被所有工具增强的资格。&lt;/p>
&lt;hr>
&lt;p>&lt;em>本文基于 &lt;code>ag-ui-protocol/ag-ui&lt;/code> main@5f32a64ee999（2026-09-09）、Python SDK ag-ui-protocol 0.1.22、TypeScript SDK @ag-ui/client 0.0.59 写成，源码研读覆盖 &lt;code>sdks/python/ag_ui/core/&lt;/code>（events / types / capabilities / token_usage / encoder）与 &lt;code>sdks/typescript/packages/client/src/&lt;/code>（agent / middleware / verify / apply / chunks / interrupts）、&lt;code>middlewares/event-throttle-middleware/&lt;/code>。协议处于 draft 阶段、日更演进，引用前请核对最新版本。&lt;/em>&lt;/p></description></item></channel></rss>