从 Token Streaming 到 Agent Event Stream
Claude Code、Codex、Cursor、Grok Build 与 Google Antigravity 的实时流式架构演进
副标题:为什么现代 Agent 不只是“逐字输出”,而是在流式传输整个执行过程
资料截止日期:2026-07-13
本文讨论的重点不是编程语言、前端框架或某一种具体网络协议的优劣,而是现代 Agent 产品如何把一个长耗时、强不确定、包含模型推理、工具调用、代码执行、人类审批与多 Agent 协作的任务,组织成一个可观察、可交互、可恢复、可持续推进的实时执行系统。
摘要
过去两年,生成式 AI 产品的“流式输出”正在发生一次容易被低估的架构升级。
第一代大模型应用的流式输出,本质上是 Token Streaming:服务端不等待完整文本生成完毕,而是把 text_delta 持续推送给客户端。它显著降低了“等待完整答案”的感知延迟,但它只能回答一个问题:模型正在说什么?
当产品从 Chatbot 演化为 Agent,这个问题已经不够。一个 Coding Agent 可能要读取数百个文件、搜索代码、生成工具参数、运行 Bash、修改文件、等待权限、启动子 Agent、执行测试,并在失败后继续修复。此时,用户需要知道的不是单一文本序列,而是:
- 任务是否已经开始;
- Agent 当前处于哪个执行阶段;
- 哪个工具正在运行;
- 命令是否有输出;
- 文件是否发生修改;
- 是否正在等待人类批准;
- 子 Agent 是否已经启动;
- 网络断开后任务是否仍继续;
- 重新连接后能否恢复到正确状态;
- 哪些实时内容只是临时预览,哪些才是最终权威记录。
因此,现代 Agent 的流式架构正在经历如下演进:
Token Streaming ↓Typed Model Event Streaming ↓Agent Runtime Event Streaming ↓Durable Agent Event Streaming ↓Multi-Agent Execution / Progress / Artifact Stream本文将论证:现代 Agent 的核心实时抽象正在从“文本流”转向“执行事件流”。
OpenAI 的 Responses API 已明确使用 typed semantic events;Codex App Server 进一步把 Thread → Turn → Item 暴露为可流式观察的 Agent Runtime 协议;Anthropic 的 Managed Agents 明确区分 best-effort preview delta 与 authoritative buffered event;Cursor 把 Agent 执行抽象为可持续、可断线重连的 Run;Grok Build 提供 newline-delimited streaming JSON 与 ACP session/update;Google Antigravity 则把用户可见的进度表达进一步提升为 Task、Plan、Artifact 与 Verification。[1][2][3][4][5][6]
本文的核心判断是:
LLM Streaming 描述“模型正在说什么”;Agent Event Streaming 描述“系统正在发生什么”。
进一步说:
优秀 Agent 产品真正优化的,不只是 Time To First Token,而是 Time To First Event、Time To First Action,以及从任务开始到任务结束之间整段过程的可观察性、并行度、恢复能力与信息密度。
第一部分:问题提出——Agent 的“快”到底是什么
1. 为什么 Coding Agent 的流式输出值得单独研究
传统 Chatbot 的一次请求可以被简化为:
User ↓Model Inference ↓TokenTokenToken ↓Final Answer即使模型内部推理复杂,从产品架构看,它仍然接近一个“输入 → 生成 → 输出”的单阶段过程。Token Streaming 的价值非常直接:模型每产生一部分内容,客户端立即显示,不必等待整个响应结束。
Coding Agent 完全不同。
一次真实任务更接近:
User Task ↓Understand Goal ↓Inspect Repository ↓Search Code ↓Read Files ↓Plan ↓Call Tools ↓Execute Commands ↓Modify Files ↓Run Tests ↓Observe Failure ↓Repair ↓Verify ↓Complete这里至少存在五类不同时间尺度:
- 模型生成时间:几十毫秒到数十秒;
- 本地工具时间:文件读取可能毫秒级,测试可能几十秒;
- 远程工具时间:搜索、MCP、API 调用可能受网络影响;
- 人类等待时间:审批、补充信息可能持续数秒乃至数小时;
- 长任务执行时间:后台 Agent 可能持续数分钟、数小时甚至跨设备会话。
如果仍然把整个系统抽象成:
Request → 等待 → Response那么即使底层 Agent 正在高速工作,用户看到的仍然是一个黑盒。
所以现代 Agent 产品面临的第一性问题不是“如何把文字吐得更快”,而是:
如何把一个长时间、非线性、多参与者的执行过程,持续映射为用户能够理解的状态变化。
这正是 Event Streaming 出现的根本原因。
2. Agent 的“快”不是单纯模型生成快
谈 Agent 性能时,如果只讨论 tokens per second,往往会误判真实体验。
至少应该区分三个首要指标。
2.1 TTFT:Time To First Token
Request ↓Model Starts Generating ↓First Text TokenTTFT 衡量模型开始产生可显示文本的速度。
它对聊天产品非常重要,但对 Agent 不够。
2.2 TTFE:Time To First Event
Agent 可以在模型首个文本 Token 之前就告诉客户端:
run.startedturn.startedrequest.acceptedagent.status = working因此:
TTFE = Time To First Meaningful Execution Event用户不一定需要立即看到一句自然语言。很多时候,一个可信的:
正在分析仓库或者:
任务已启动就足以证明系统已经开始工作。
2.3 TTA:Time To First Action
对 Agent 更关键的是:
多久真正开始读取、搜索、调用工具或执行命令?可以定义:
TTA = Time To First Action假设两个 Agent 的首 Token 都是 1 秒:
- Agent A 在 1 秒后说“我先分析一下”,10 秒后才开始搜索;
- Agent B 在 1.2 秒后已经启动代码检索,同时持续回传搜索进度。
从用户与系统吞吐的角度,Agent B 往往更快。
因此,本文后续使用一个更完整的感知速度模型:
这不是一个可直接求数值的物理公式,而是一个架构分析框架:Agent 的“快”由首个可感知信号、首个真实动作、持续反馈、重叠执行、并行执行与恢复能力共同决定。
3. Agent Streaming 不等于 LLM Streaming
3.1 LLM Streaming:流的是生成内容
典型形式:
text_deltatext_deltatext_delta它回答:
模型正在生成什么?
3.2 Agent Streaming:流的是执行状态变化
现代 Agent 的流可能包含:
run.startedturn.startedmessage.deltatool.call.startedtool.input.deltatool.output.deltacommand.startedcommand.stdout.deltafile.change.startedapproval.requiredsubagent.startedartifact.createdrun.completed它回答:
系统正在发生什么?
两者最大的差异不是数据格式,而是抽象边界。
在 Token Streaming 中,模型响应是系统的最高层对象:
Model Response └── Text Delta在 Agent Event Streaming 中,模型响应只是一个更大执行过程中的子过程:
Agent Run ├── Model Request │ └── Message Delta ├── Tool Call ├── Command Execution ├── File Change ├── Approval └── Subagent因此可以得到全文最重要的第一个结论:
Token Streaming 是 Agent Streaming 的子集,而不是同义词。
第二部分:流式输出的技术基础
本部分只建立理解后文所需的传输基础。真正决定 Agent 架构水平的,不是单独使用 SSE、WebSocket 还是 JSONL,而是其上承载的事件模型、运行时语义与恢复协议。传输层首先要回答的是:字节如何分帧、数据何时真正离开缓冲区、连接如何存活与退出、慢消费者如何处理;至于 Run 是否持久、事件能否重放,则必须由更高层协议回答。
4. Agent Streaming 的底层传输技术
分析任何一种 Agent Streaming 方案,至少要拆开四个边界:
Runtime write ↓Transport bytes / frames ↓Protocol record ↓Agent event一次 write() 不一定对应一次网络发送;一个 TCP segment、HTTP DATA frame 或 WebSocket frame 也不一定对应一个业务事件。只有协议明确规定了 delimiter、length prefix、SSE 空行或 WebSocket message 之类的 framing,接收方才能把连续字节稳定地恢复成消息;只有事件协议再为消息定义类型、身份与生命周期,它才成为 Agent Event。
4.1 普通 HTTP 请求为什么不适合长时 Agent
这里所谓“普通 HTTP 请求”,更准确地说,是 服务端完成全部工作后才返回一个最终对象 的应用契约,而不是说 HTTP 协议本身不能流式传输。
Client │ Request ▼Server │ │ 运行 5 分钟,并把结果留在应用缓冲区 │ ▼Client ← Final Response这种模型对 CRUD 很自然,但与长时 Agent 的执行过程不匹配:
- 用户无法区分“系统仍在工作”与“系统已经卡死”;
- 工具调用、执行进度与中间结果不可见,TTFE 与 TTA 被最终响应掩盖;
- 网关、负载均衡、反向代理、Serverless 平台和客户端往往各有请求时长或 idle timeout,整条链路取最短者;
- 客户端断线后,服务端容易把“HTTP response 无人接收”错误解释为“Run 应该终止”;
- 人类审批、补充输入、steer 与 cancel 无法通过同一个单向 response 自然表达;
- 最终响应越大,服务端与中间层越可能先完整缓冲,内存占用和失败重试成本也越高。
长任务还有一个更隐蔽的问题:连接失败与执行失败是两种不同的事实。浏览器收到 502,只能证明网关没有成功交付响应,不能证明后端工具没有执行。若客户端直接重试原始 POST,就可能重复创建 Run、重复写文件或重复调用有副作用的工具。
因此,一个更稳健的接口通常先建立耐久执行身份,再订阅其输出。例如:
- 客户端以带幂等键的命令创建或继续
run_id; - 服务端确认命令已接受,执行生命周期不再依赖该请求是否仍连接;
- 客户端通过 SSE、WebSocket 或轮询订阅
run_id的事件; - cancel、approval 等控制命令显式引用同一个
run_id。
这不是要求每个 Agent 都必须使用“两次 HTTP 请求”,而是要求 API 在语义上区分:
Command acceptance ≠Execution completion ≠Event delivery普通的 request-final-response 模式真正缺少的,不只是“边生成边显示”,而是对这三个生命周期的独立建模。
4.2 HTTP Streaming:先解决“什么时候到”
HTTP Streaming 的基本做法,是响应头先返回,响应 body 在执行期间持续增加。HTTP/1.1 在未知最终长度时常使用 chunked transfer coding。[20] HTTP/2 与 HTTP/3 则在各自的 DATA frame / stream 上承载 body,并不使用 HTTP/1.1 的 Transfer-Encoding: chunked。HTTP/3 规范进一步明确禁止使用 Transfer-Encoding 字段。[21][22] 这些都是 HTTP 的传输细节,不是业务事件格式。
HTTP Response Body ├── bytes written now ├── bytes written later ├── ... └── end-of-body必须避免把以下边界等同:
| 边界 | 它能说明什么 | 不能假设什么 |
|---|---|---|
应用 write() | 数据进入框架或运行时 | 已经发到网卡 |
| HTTP/1.1 chunk 或 HTTP/2 DATA frame | 一段传输载荷 | 恰好是一个 JSON 或 Event |
| TCP read 返回的一段 bytes | 当前可读的数据 | 与发送方 write 一一对应 |
| 协议 record | 接收方可完整解析的一条消息 | 一定是耐久业务事件 |
| Agent Event | 一次领域状态变化 | 一定等于一个 UI 更新 |
TCP 可以拆分一次 write,也可以合并多次 write;HTTP 实现还可能把多个小 write 合并为一个 DATA frame。因而裸 JSON 对象连续拼接是不可解析的:
{"type":"a"}{"type":"b"}接收方不知道第一个对象何时结束。HTTP body 之上仍需一种记录分帧规则,例如:
- newline-delimited JSON:一行一个完整 JSON;
- SSE:一个空行结束一个事件记录;
- length prefix:先发送长度,再发送指定字节数;
- multipart:用 MIME boundary 分隔不同 part;
- 流式 JSON parser:解析一个有明确顶层结构的增量 JSON 文档,但实现复杂度通常更高。
Flush 不是一次函数调用
Agent 的首事件延迟是整条链路缓冲的总和:
| 位置 | 常见缓冲行为 | 需要检查的控制点 |
|---|---|---|
| Agent / SDK | 等 token、等完整 JSON、批量合并 delta | batch 大小与最大等待时间 |
| Web 框架 | response writer、异步队列未 flush | streaming API 是否真的逐步 yield |
| 语言运行时 | 用户态 buffer、小写入合并 | flush、high-water mark、await drain |
| 压缩器 | 等待更多输入以提高压缩率 | 禁用流压缩或使用同步 flush 策略 |
| 反向代理 / CDN | response buffering、最小块大小、缓存 | 明确关闭相关 buffering,确认平台支持 |
| 浏览器 / SDK | 解码器、文本 parser、渲染批处理 | 增量读取与 UI 合并刷新 |
这解释了一个常见现象:服务端日志显示每 20 ms 产生一次 delta,浏览器却每 1 秒成批出现。问题可能不在模型,而在压缩器或代理等到足够多字节后才转发。
Content-Type、Cache-Control: no-cache 等响应头很重要,但不能被当作“所有代理都不会缓冲”的通用开关。某些反向代理有自己的 response buffering 配置;例如 Nginx 官方文档说明可以通过 proxy_buffering 或 X-Accel-Buffering 控制其代理响应缓冲,但这不是跨基础设施标准。[23] 正确做法是在真实生产链路上测量每一跳的到达时间,而不是只测应用进程里的 write 时间。
压缩也不是无条件收益。Token delta 往往很小,压缩器为了压缩比而聚合数据,可能让节省的带宽换来更差的 TTFE。对于低带宽、高频事件,可以按短时间窗口 coalesce 后再压缩;对于首屏和控制事件,则应优先及时 flush。
连接存活与半开连接
只要中间层在其 idle timeout 内没有看到任何字节,即使 Agent 正在本地执行工具,连接也可能被关闭。因此流式协议通常需要 heartbeat。heartbeat 的间隔应短于链路中最小的 idle timeout,并加入少量 jitter,避免大量连接同一时刻唤醒;它只证明传输通道仍可写,不能证明 Agent 有业务进展。
另一侧也存在“半开”问题:Wi-Fi 切换、NAT 状态丢失或进程崩溃后,服务端不一定立即收到一个干净的 close。很多实现直到下一次 write 失败,或 heartbeat 超时,才知道对端已经消失。因此:
- 不要把 socket close 当作 cancel 的唯一来源;
- 需要释放的连接级资源可以在断线后回收;
- 是否继续 Run,应由 Run 策略或显式 cancel 决定;
- 对昂贵的临时订阅,要设置无订阅者超时,但不能因此删除耐久状态。
传输流控不是业务流控
TCP、HTTP/2 和 QUIC 都有各自的流量控制;HTTP/2 明确区分 stream 与 connection flow-control window,HTTP/3 则依赖 QUIC 的 stream 与 connection flow control。[21][22] 当客户端读得慢时,底层发送最终会阻塞或返回 backpressure 信号。但在信号传回应用之前,数据可能已经堆积在框架、代理和内核缓冲区中。若应用继续把每个 token delta 放入无界队列,“网络有 TCP flow control”仍然挡不住进程 OOM。
因此 HTTP Streaming 至少还需要:
- 每连接有界发送队列;
- 写入时真正 await drain,而不是无限 enqueue;
- 对可重建的 preview delta 做合并或丢弃;
- 对 approval、error、completed 等权威事件保留空间;
- 慢消费者超过阈值时主动结束连接,并返回可恢复游标。
HTTP Streaming 解决了“数据可以什么时候到”,但没有自动解决“这一段数据是什么”“断线后从哪里继续”以及“客户端是否已经处理”。SSE 正是在 HTTP body 之上增加了一套轻量、标准化的记录语法与浏览器重连行为。
4.3 SSE:适合 Server → Client 的持续事件流
Server-Sent Events 由 HTML Living Standard 定义。浏览器 EventSource 建立 HTTP 长连接,服务端以 UTF-8 的 text/event-stream 发送文本记录。[7]
一条 SSE 记录以空行结束;data: 可以出现多次,浏览器会用换行符连接这些值;event: 指定事件类型;id: 更新连接的 last event ID;retry: 给出客户端后续重连等待时间。以冒号开头的行是 comment,常用于 heartbeat:
: heartbeat
event: tool_completedid: 1042data: {"run_id":"run_7","tool":"grep","matches":18}最后的空行不是排版,而是 dispatch boundary。若服务端只写了 data: {...}\n 而没有再写一个换行,客户端可以一直不触发事件。JSON 中的真实换行必须被 JSON 字符串转义,或者拆成多个 data: 行,不能让任意日志文本直接破坏 SSE 字段语法。
SSE 的优势在于:
- 基于 HTTP,通常比自定义长连接更容易接入现有网关、鉴权与观测体系;
- Server → Client 的方向与“订阅 Run 事件”高度匹配;
- record framing、事件类型和事件 ID 已有标准语法;
- 浏览器原生
EventSource支持断线自动重连和Last-Event-ID; - comment heartbeat 不会变成业务
message事件; - 文本 delta、工具状态、进度和最终状态都能用同一条有序响应流交付。
但这些便利有清晰边界。
EventSource 的 API 边界
浏览器原生 EventSource 面向 GET 型订阅,不能像 fetch 那样自由设置 method、request body 和任意请求头。Cookie 鉴权可以工作,但跨域凭证、CORS 与 CSRF 策略必须一起设计;把长效 bearer token 放进 query string,则可能泄漏到访问日志、浏览器历史或监控系统。
如果产品要求 POST body、动态 Authorization header、精细错误处理或自定义二进制格式,可以使用 fetch() 读取 ReadableStream 并自行解析 SSE。代价是自动重连、retry、Last-Event-ID 和终止状态处理也要由客户端实现。此时 wire format 仍可以是 SSE,但已经不再享受完整的原生 EventSource 生命周期。
自动重连不等于事件恢复
SSE 标准定义的是 连接重建:浏览器记住最近处理过的 SSE id,重连时通过 Last-Event-ID 告诉服务端。[7] 它没有要求服务端保存历史,更没有保证该 ID 对应耐久提交。
一个可恢复的 Agent SSE 端点至少要定义:
id是仅用于日志关联,还是可用于重放的 resume cursor;- cursor 的作用域是全局、用户、Run,还是某个投影流;
- 服务端保留多少历史,压缩后如何返回 snapshot;
- 重放是 at-least-once 还是可能存在 gap;
- 客户端如何按
event_id/sequence去重; - Run 已完成、无权限、游标过旧时分别返回什么状态。
若事件 ID 只是随机 UUID,服务端通常无法高效回答“它之后有哪些事件”。工程上常将 SSE id 映射到 Run 内单调递增的 sequence 或不透明但可比较的日志 offset;如果内部事件经过 projection 或 coalescing,则外部 cursor 还必须对应 外部可重放流,不能直接暴露一个随后会消失的内部队列下标。
重连最容易出错的是“先查历史,再订阅实时”的窗口:
查询历史结束 ── gap 中产生 evt_1043 ── 实时订阅建立此时 evt_1043 既不在历史结果中,也没有被实时订阅捕获。可靠实现通常使用以下两类方法之一:
- 直接从同一个 append-only log 按 cursor tail,历史与实时没有两个数据源;
- 先建立实时订阅并暂存新事件,再读取一个 high-water mark 以内的历史,最后按 sequence 去重、排序并切换到 live。
如果历史已被 compact,服务端不应悄悄从“现在”继续,让 UI 误以为状态完整;它应明确返回 cursor expired,并提供当前 snapshot 与新的基线 sequence。更完整的恢复语义将在后文 Event Store 与 Durable Execution 中展开。
Heartbeat 与 timeout
SSE heartbeat 通常使用 : ping\n\n 这样的 comment。它有三个作用:
- 让代理看到响应仍有字节流动,避免 idle timeout;
- 让服务端较早通过 write error 发现断开的客户端;
- 为端到端延迟和连接健康提供观测点。
它没有三个能力:
- 不代表 Run 有进展;
- 不推进 durable sequence,也不应伪装成权威业务事件;
- 不替代客户端的“多久没有业务进展就显示 stalled”判断。
heartbeat 周期必须依据实际代理链路配置,而不是固定照抄某个秒数。太慢会被网关先关闭,太快则在数万连接下造成无意义流量与唤醒。服务端还应区分正常 EOF、客户端 cancel、代理 reset 与自身超时,避免所有断线都记录成 Agent failure。
SSE 的 flow control 边界
SSE 没有应用层 ACK。浏览器收到事件只表示字节已到达并被解析,不表示 React 已渲染、状态已持久化或用户已看到。慢客户端最终会通过 HTTP/TCP backpressure 影响 writer,但服务器仍需限制自己的每连接队列。
比较实用的优先级策略是:
- token、日志 tail、光标位置等 preview event 可以 coalesce;
tool.started与连续的 progress 可以只保留最新投影;- approval、权限变化、error、completed 等状态转换不能丢;
- 超出队列上限时,关闭慢连接并让客户端从最后确认应用的 cursor 恢复。
注意“最后收到”与“最后应用”也可能不同。若 UI reducer 在主线程拥塞时已经积压了数千事件,浏览器维护的 last event ID 可能领先于 UI state。严格场景需要客户端把已应用 sequence 单独持久化,并在自管重连时使用它,而不是盲信底层接收位置。
常见失败模式
| 现象 | 根因 | 正确处理 |
|---|---|---|
| 事件每隔数秒成批出现 | 框架、压缩或代理缓冲 | 逐层测量 flush,关闭或调整 buffering |
| 连接稳定在固定时长断开 | 网关 request / idle timeout | 调整链路配置并发送 comment heartbeat |
| 重连后少一段事件 | replay 与 live subscribe 有竞态 | 使用单一日志 tail 或 high-water mark 协议 |
| 重连后事件重复 | at-least-once replay 未去重 | 以 event ID / sequence 幂等 reduce |
| 浏览器不断重连已完成 Run | 终止语义不明确 | 客户端在 terminal event 后 close;重连端点可按规范以 HTTP 204 阻止继续重连 |
| 401 后无限 error loop | 原生 EventSource 无法刷新自定义 header | 先刷新凭证再重建,或使用自管 fetch stream |
| 其他用户可猜 cursor 读取事件 | 只校验 cursor,未重新校验 Run 权限 | 每次订阅与重放都做对象级授权 |
因此:
SSE reconnect ≠Replay ≠Durable executionSSE 最适合“控制命令走普通 HTTP,服务器持续向客户端投递事件”的结构。当客户端必须在同一连接上高频反向发送控制、输入或媒体数据时,WebSocket 的全双工语义更自然。
4.4 WebSocket:适合真正的持续双向控制
WebSocket 通过 HTTP 握手建立持久的全双工连接,其核心协议由 RFC 6455 定义。[8] 建连后,双方都可以主动发送消息,因而适合:
- 用户中途 steer;
- cancel / pause / resume;
- approval request / response;
- 终端 stdin 与 stdout;
- 高频协同编辑或 TUI 控制;
- 实时语音、音频与二进制增量;
- 一个长生命周期客户端管理多个活跃 Run。
Frame 不是 Event
RFC 6455 定义 text、binary、continuation 与 control frame。一个 WebSocket message 可以由多个 frame 分片;一个 frame 在 TCP 上又可能被拆成多次 read。大多数高级 WebSocket API 会替应用重组完整 message,但服务端库的最大 frame、最大 message 与内存策略可能不同。[8]
因此协议必须明确采用哪一种 record boundary,例如:
- 一个 WebSocket text message 恰好承载一个 JSON-RPC message;
- 一个 binary message 承载一个 protobuf record;
- 一个 message 内允许 batch,但 batch 自身有明确数组结构。
不要读取底层 socket 后假设“一次 read 就是一个 JSON”,也不要把 WebSocket fragmentation 当作 Agent 的 delta 语义。fragmentation 是传输实现选择;tool.output.delta 是业务协议。
Ping/Pong 与应用心跳
WebSocket 有协议级 Ping/Pong control frame,可用于检测连接活性。许多服务端框架会自动回复 Pong;浏览器 WebSocket API 则不直接暴露发送协议 Ping 的接口,因此浏览器产品常再定义应用级 ping / pong 或周期性 server heartbeat。
两层心跳回答的问题不同:
- 协议 Ping/Pong:对端 WebSocket 栈是否仍可达;
- 应用 heartbeat:事件循环、会话处理器或 Run 订阅是否仍健康;
- progress event:Agent 是否产生了新的有意义状态。
把三者合并成一个“alive”布尔值,会让故障定位变得困难。
连接生命周期必须由应用协议补全
WebSocket 没有像浏览器 EventSource 那样的自动重连与 Last-Event-ID。一个可恢复会话通常需要显式握手:
- 建立 WSS / 本地 socket 连接并完成认证;
initialize/hello交换协议版本、能力、最大消息大小和 heartbeat 参数;- 客户端以
run_id + last_applied_sequence订阅; - 服务端返回 snapshot 或 replay,再进入 live;
- 双方发送 command、event、request、response 与 ACK;
- 断线后使用 backoff 和 jitter 重连,并重新认证与订阅。
握手成功只说明“通道建立”,不说明先前 Run 仍存在。恢复结果要显式区分:
resumed:从 cursor 连续恢复;snapshot_required:历史已压缩,需要重建状态;run_terminal:Run 已结束,只需获取最终状态;run_not_found或forbidden:不能把它们伪装成空事件流。
代理链路并不天然更简单
WebSocket 常通过 HTTP/1.1 Upgrade 穿过反向代理;其他 HTTP 版本有各自的承载机制,但网关、WAF、企业代理和 Serverless 平台的端到端支持并不一致。生产部署需要确认:
- Upgrade / Connection 相关头是否被正确转发;
- 最大连接时长、idle timeout 与 draining 行为;
- 负载均衡是否要求 sticky session,还是任意节点都能从共享状态恢复;
- 发布或扩缩容时,旧连接如何收到 reconnect hint;
- 中间层最大 frame / message 和速率限制;
- WebSocket 是否进入与普通 HTTP 同等的鉴权、审计和 DDoS 防护体系。
如果 WebSocket session state 只存在某一进程内存中,sticky session 只能降低问题概率,不能提供 durability。该进程重启时,连接和状态仍会一起消失。
send() 成功不等于消费成功
WebSocket 基于可靠、有序的字节流,但“可靠”只覆盖当前连接内的数据交付。一次 send() 返回,通常只表示消息被应用或系统发送缓冲区接受,不表示对端业务逻辑已经处理,更不表示写入 Event Store。
慢消费者会在三个位置暴露:
- 浏览器的
bufferedAmount持续增长; - 服务端每连接 send queue 达到 high-water mark;
- 应用 ACK 的
last_applied_sequence长时间不前进。
高可靠场景可以增加应用层 credit / ACK:
- 服务端只允许最多 N 个未确认 event;
- 客户端 reducer 应用后累计 ACK 到 sequence S;
- 服务端据此释放重放窗口或发送下一批;
- 重连从 S 继续,因此重复仍可能发生,客户端必须幂等。
ACK 的语义必须写清:是“socket 收到”“parser 解析”“state 已应用”还是“本地持久化”。定义不清的 ACK 只会制造虚假的 exactly-once。
全双工也不自动带来优先级。同一 TCP 连接严格有序,大体积 tool output 已经进入发送队列后,后来的 cancel 不能跨过这些字节。常见对策包括限制单 message 大小、将大 artifact 改为对象引用、为 control event 预留有界高优先级队列,必要时将控制面与大数据面拆成不同连接。
安全边界
远程 WebSocket 应使用 wss://。浏览器可能在跨站 WebSocket 握手中携带 Cookie,因此服务端不能只看“有登录 Cookie”,还应校验 Origin、对象级权限与允许的子协议。非浏览器客户端没有可信 Origin,仍需 token、mTLS 或等效认证。
RFC 6455 要求客户端到服务端的 frame 使用不可预测的 masking key。[8] Masking 是 wire-level 规则,主要防止恶意脚本构造看似其他协议的字节流去污染中间代理;它不是加密、身份认证或消息完整性授权,不能替代 WSS 和应用鉴权。
长连接还会遇到普通请求较少暴露的问题:
- token 在连接存续期间过期,协议需要 re-auth 或受控重连;
- 单个超大 message 可造成内存放大,必须限制 message、解压后大小与嵌套深度;
- JSON schema 不应因为“连接已经认证”而跳过验证;
permessage-deflate等压缩会增加 CPU、内存和敏感上下文压缩侧信道风险,应按威胁模型配置;- server-initiated request 也必须带 scope,客户端不能把任何
approval消息都当作可信 UI 指令。
常见失败模式
| 现象 | 根因 | 正确处理 |
|---|---|---|
| 页面显示 connected,但事件不再前进 | 半开连接或应用 handler 卡住 | 协议心跳与 progress watchdog 分离 |
| reconnect 后重复执行命令 | 命令无幂等键,误把重发当恢复 | command ID 去重,Run 与 socket 解耦 |
| cancel 很久才生效 | 大消息占满同一有序发送队列 | 限制 message,控制面优先或分离 |
| 服务端内存随慢客户端增长 | 无界 send queue | 有界队列、ACK/credit、断开并恢复 |
| 小消息正常,大 artifact 断线 | 代理或库最大 message 不一致 | 协商上限,artifact 外置或分块 |
| 浏览器跨站建立了已登录连接 | 只依赖 Cookie,未校验 Origin | 校验 Origin、CSRF 风险与对象权限 |
因此:
WebSocket ≠Session durability ≠Event replay ≠Exactly-once commandWebSocket 的价值是给应用协议提供持续双向通道。协议仍需自行定义身份、顺序、背压、恢复与权限。
4.5 stdio + JSONL:Coding Agent 的重要本地形态
Agent 不一定运行在浏览器或远程服务器。CLI、IDE Plugin、Desktop App 常把 Agent Runtime 作为子进程:
IDE / Host Process │ stdin : command / response │ stdout : event / request ▼Local Agent Runtime这里的 stdin / stdout 是 字节流,同样没有天然消息边界。JSONL / NDJSON 约定每个物理行是一个完整 JSON value,换行符结束 record:
{"type":"tool.started","tool":"grep"}{"type":"tool.output.delta","delta":"src/auth.ts\nsrc/user.ts"}{"type":"tool.completed","status":"ok"}第二条中的换行是 JSON 字符串内的 \n 转义,而不是协议换行。接收方必须能处理:
- 一次 read 只得到半行;
- 一次 read 同时得到多行;
- 不同平台的 LF / CRLF;
- UTF-8 字符跨 read 边界;
- 最后一行尚未换行时进程异常退出;
- 超长行和恶意嵌套 JSON。
一个正确 parser 会累积 bytes,找到 delimiter 后再按 UTF-8 解码并解析完整 JSON,同时设置最大 record 长度。它不会对每次 read() 直接调用 JSON.parse()。
stdout 必须是协议专用通道
stdio JSONL 最常见的故障不是网络,而是某个依赖库打印了一行 banner:
Debugger listening on port 9229对人类只是日志,对协议 parser 却是非法 JSON。因而:
- stdout 只写协议 record;
- 日志、诊断、stack trace 写 stderr;
- host 必须持续 drain stderr,否则 stderr pipe 填满也会阻塞子进程;
- 多个 producer 不能无锁并发写 stdout,应由单一 serializer 排队;
- 每条 record 写完要 flush,不能依赖连接到 TTY 时的行缓冲行为。
不少语言运行时在 stdout 指向终端时采用 line buffering,指向 pipe 时却改用更大的 block buffer,所以“手动运行很流畅,IDE 中几秒一批”往往是 flush 问题,而不是 IPC 天生慢。
Pipe backpressure 会造成真实死锁
操作系统 pipe 有有限缓冲区。当 host 停止读取 stdout,Agent 的 write 最终会阻塞。若双方都采取“先写完再读”的同步流程,就可能出现:
Host 等待 responseAgent 等待 stdout 可写Host 又没有并发 drain stdout因此 host 与 child 都需要独立的 read loop、write queue 和 dispatcher:
- read loop 持续解析 record,并按 request ID / event type 路由;
- write loop 串行化输出,达到 high-water mark 时让 producer await;
- tool output 等高频 preview 可以合并,但 control response 不应饿死;
- 大文件、截图或二进制 artifact 不宜长期 base64 塞进单行 JSON,应传路径、句柄或内容地址,并单独校验权限。
不要假设“小于某个大小的一次 write 在所有平台和所有多写者场景下一定原子”。让一个 serializer 成为 stdout 的唯一写者,才是跨平台、可维护的边界。
Process 生命周期就是一种连接生命周期
stdio 通常把 transport 生命周期绑定到子进程:
- host spawn child,设置 cwd、环境变量和 pipe;
- 双方 initialize,交换协议版本与 capability;
- 多个 request、notification 和 server-initiated request 交错;
- host 发送 shutdown / cancel,停止接受新任务;
- 等待有界 grace period 后关闭 stdin,必要时再终止进程;
- child exit 后,host 消费完剩余 stdout / stderr,并将未完成 request 标记为 transport failure。
EOF 只表示对端不会再写字节,不自动说明某个 Run 成功、取消还是崩溃。进程 exit code、最后一个权威 Run event 与 stderr 诊断要分别记录。
stdio 也不天然 durable。若 Agent Runtime 就是该子进程,IDE 崩溃通常会让 pipe 关闭,子进程可能退出,也可能变成 orphan。要实现“IDE 重启后 Run 继续”,Runtime 必须升级为独立 daemon / service,并把 stdio 只作为一个客户端 adapter,或把执行状态持久化后由新进程恢复。
本地不等于可信
本地 Agent 往往继承 host 的环境变量、工作目录和文件权限,安全影响可能高于一个受限远程连接。协议实现至少需要:
- 对 method、params、路径和 artifact handle 做 schema 与授权校验;
- 不让日志或模型文本注入协议字段;
- 限制单行长度、队列长度、请求并发和 tool output;
- 明确哪些环境变量可传给 child,避免无关密钥泄漏;
- 在多工作区 IDE 中把 session 与 workspace root 绑定;
- 对 approval response 校验对应的 pending request,而不是只匹配一个字符串 ID。
Codex App Server 的公开文档说明,其默认 transport 是基于 stdio 的 newline-delimited JSON;同时提供 WebSocket 与 Unix Socket 形态,其中当前 TCP WebSocket transport 被标记为 experimental / unsupported。[2] Grok Build 的 streaming-json 是 newline-delimited incremental events,其 ACP 模式通过 stdin/stdout 上的 JSON-RPC 工作。[5] 这些公开信息说明的是可观察协议边界,不应进一步推断产品未公开的内部进程拓扑。
stdio + JSONL 的关键价值不是“比网络更快”,而是部署简单、调试直接、天然适合本地 host-child 模型。它依旧需要严格 framing、flush、backpressure、版本协商与生命周期管理。
4.6 JSON-RPC、ACP、IPC 与 Unix Socket
这一节容易把四个不同层级混在一起:
- JSON-RPC 是消息模型;
- ACP 是在消息模型之上定义 Agent 会话方法与更新语义的协议;
- IPC 是进程间通信这一大类机制;
- Unix domain socket 是一种本地 IPC transport。
它们不是互斥选项。完全可能同时采用“ACP-style methods → JSON-RPC messages → WebSocket messages → Unix domain socket bytes”。
JSON-RPC 解决的是关联,不是流式可靠性
JSON-RPC 2.0 定义 Request、Response、Notification 与 Error 的结构。[9] Request 带 id,Response 用同一个 id 关联;Notification 没有 id,接收方不得为其返回 Response。只要底层连接可双向发送,任意一侧都可以在不同时间扮演 requester 与 responder。
下面只是协议形态示意,不代表某一产品的精确 method 名:
{"jsonrpc":"2.0","id":"client:42","method":"turn/start","params":{"run_id":"run_7"}}{"jsonrpc":"2.0","id":"client:42","result":{"accepted":true}}{"jsonrpc":"2.0","method":"event","params":{"sequence":1042,"type":"tool.started"}}{"jsonrpc":"2.0","id":"server:9","method":"approval/request","params":{"tool":"shell"}}这里有三条不同的逻辑流:
turn/start的 Response 只确认命令是否被接受;eventNotification 持续报告执行状态;approval/request是服务端发起、等待客户端 Response 的反向请求。
JSON-RPC 本身没有定义:
- 消息如何在 TCP / stdio 字节流上分帧;
- notification 是否有顺序、是否能重放;
- cancellation、progress、subscription 或 heartbeat 的标准 method;
- 身份认证、授权和 capability negotiation;
- 断线后 request 是否仍执行;
- delivery 是 at-most-once、at-least-once 还是其他语义。
因此“使用 JSON-RPC”不能推出“支持 durable streaming”。应用协议仍需定义 run_id、event_id、sequence、resume cursor 和幂等命令 ID。
双向 JSON-RPC 还要处理 ID 空间。若两端都从数字 1 开始发 request,而实现既不区分消息形态、又把两个方向的 request 状态塞进同一个 map,就可能错误关联。正确实现应先区分 Request 与 Response,再为每一侧发出的 pending request 独立关联;也可以使用带角色前缀的 ID 简化诊断。无论采用哪种,同一 requester 的 ID 在其尚未完成请求集合中都必须唯一。连接断开时,不能把所有 pending request 都简单重发:只有具有幂等键、且协议允许重试的 method 才能安全重发。
ACP 把 Agent Session 语义放到 JSON-RPC 之上
ACP 官方协议概览把通信模型定义为 JSON-RPC methods 与 notifications,并在典型 Prompt Turn 中使用 session/prompt、session/update 与 session/cancel;公开的 Grok Build 文档则展示了这套模式在 stdin/stdout 上的实现,其中 session/prompt 返回 completion metadata,assistant 文本以 session/update chunk 到达。[5][28] 这类设计的重要点不在 method 的字符串名称,而在于:
- prompt 是一个有明确 session 身份的命令;
- update 是会话执行期间的增量通知;
- host 与 Agent 可以在同一双向通道中完成请求、更新和交互;
- session 语义独立于 stdio,理论上可映射到其他 transport。
本文只依据公开接口讨论这种协议形态,不把未公开的内部队列、持久化或调度实现写成产品事实。尤其不能因为出现 session 一词,就自动推断它支持跨进程重启、任意时长 replay 或 exactly-once。
IPC / Unix Socket 只改变可达范围
Unix domain socket 避开 TCP 端口,通过文件系统命名空间或平台相关地址连接本机进程。对 stream socket 而言,它仍然只是有序字节流,仍需 newline、length prefix、WebSocket message 等 framing。recv() 返回一次的 bytes 不等于一条 JSON-RPC message。
Codex App Server 的公开文档显示,其 Unix Socket transport 在 socket 上承载 WebSocket 连接,而 TCP WebSocket 则是另一种监听形态。[2] 这很好地说明了分层:Unix Socket 规定“进程如何在本机相遇”,WebSocket 规定 message transport,JSON-RPC 规定 request / response / notification。
本地 socket 的工程问题主要包括:
- socket path 的目录权限与所有者;
- stale socket file 在进程崩溃后如何安全清理;
- 是否允许多个客户端,以及每个客户端的订阅与配额;
- 服务重启后旧客户端如何重新发现 endpoint;
- 平台支持的 peer credential 是否用于加强身份校验;
- Windows 上是否需要 Named Pipe 或 loopback TCP adapter;
- 大量本地事件下的 send queue、最大 message 与慢消费者处理。
“只监听 localhost”也不是完整认证。共享机器上的其他用户、低权限进程、浏览器到 loopback 的请求以及被劫持的插件,都可能访问本地 endpoint。应优先使用权限受控的 socket directory / Named Pipe ACL;使用 loopback TCP 时还应增加随机 bearer token 或等效握手,并防止非预期 Origin。
协议错误与传输错误要分开
一个成熟客户端至少要区分:
| 类型 | 例子 | 是否应自动重试 |
|---|---|---|
| Parse / framing error | 半行、非法 UTF-8、stdout 混入日志 | 通常终止连接并记录原始诊断 |
| JSON-RPC protocol error | 缺少 jsonrpc、重复 ID、非法 Response | 取决于是否仍能可靠确定消息边界 |
| Application error | tool denied、invalid params、Run 不存在 | 按 method 语义处理,不能盲目重试 |
| Transport error | pipe EOF、socket reset、heartbeat timeout | 重连后按幂等键与 cursor 恢复 |
| Runtime failure | Agent process crash、Event Store 不可用 | 查询耐久状态,不能仅靠 socket 状态判断 |
把所有异常都包装成一个 connection_lost,会让客户端既无法安全恢复,也无法向用户解释任务到底发生了什么。
4.7 传输层不是核心差异
至此可以得到一个更严格的分层:
┌────────────────────────────────────────────┐│ Agent Semantics ││ Run / Turn / Tool / Approval / Artifact │├────────────────────────────────────────────┤│ Event / Control Protocol ││ event envelope / JSON-RPC / ACP / custom │├────────────────────────────────────────────┤│ Record Framing ││ SSE record / JSONL / WS message / length │├────────────────────────────────────────────┤│ Transport ││ HTTP body / WebSocket / pipe / Unix Socket │└────────────────────────────────────────────┘不同 transport 的真实边界可以概括为:
| 方案 | Record boundary | 方向 | 内建重连 | 应用层仍必须提供 |
|---|---|---|---|---|
| 裸 HTTP Streaming | 自定义 delimiter / parser | 主要是 S→C | 无 | event framing、cursor、重放、队列 |
| SSE | 空行结束 record | S→C | 浏览器可自动重连 | 耐久历史、去重、授权、慢消费者策略 |
| WebSocket | text / binary message | 双向 | 无 | handshake、ACK、resume、幂等、优先级 |
| stdio + JSONL | newline | 双向约定 | 无 | flush、dispatcher、进程恢复、限流 |
| Unix stream socket | 取决于上层协议 | 双向 | 无 | framing、认证、发现、session 恢复 |
JSON-RPC 与 ACP 没有放在第一列,因为它们不是 transport。它们可以在 stdio、WebSocket 或 Unix Socket 上承载;同一套 Agent event 也可以分别投影成 SSE record、WebSocket message 与 JSONL line。
因此,一个成熟 Runtime 往往把 transport adapter 限制在少数职责:
- 建立、认证和关闭连接;
- 将 bytes / frames 解码为 protocol message;
- 将 protocol message 编码并安全 flush;
- 暴露连接级 backpressure、heartbeat 与错误;
- 在重连时携带订阅参数,但不自行决定 Run 是否存在。
而以下不变量应位于 adapter 之上:
- 同一个
run_id不因切换 transport 而改变; - 权威事件的
event_id、sequence与因果关系不变; - disconnect 不自动等于 cancel;
- replay cursor 指向耐久事件或明确定义的 projection;
- command 使用幂等身份,event reducer 能处理重复;
- preview event 可降级,approval 与 terminal event 不可静默丢失;
- 每次重新订阅都重新执行认证与对象级授权。
所以,分析 Agent 产品时,不应该停留在:
“它用了 SSE,所以快。”“它用了 WebSocket,所以实时。”“它用了 JSON-RPC,所以可靠。”更准确的问题应该是:
它在这条连接上传输什么语义?记录如何分帧并及时 flush?连接断开时 Run 是否继续?客户端从哪个 cursor 恢复?慢消费者会丢什么、保什么?控制命令如何幂等?每次重连如何重新授权?
传输层决定事件能否低延迟、可控地移动;事件协议决定移动的内容能否被正确理解;Runtime 与 Event Store 则决定连接消失后,执行事实是否仍然存在。下一部分将沿着这一分层,讨论 Agent 如何从 Token Streaming 演进为完整的 Execution Stream。
第三部分:Agent 流式架构的演进
本部分建立的是能力演进框架,不是按年份划分的产品代际,也不意味着后一阶段会替代前一阶段。一个现代 Agent 往往同时使用 Token Stream、Typed Model Event、Runtime Event 与 Durable Run;真正发生变化的,是系统把什么视为有身份、可确认、可恢复的权威执行对象。
以下厂商事件名与产品能力均来自公开资料;状态机、耐久分层、重试边界和因果模型如未明确归属于某一产品,均属于本文的架构综合,不代表任何厂商完整内部实现。下一部分将在此基础上展开统一参考架构。
5. 从 Token Streaming 到 Agent Execution Stream
五个阶段可以压缩为一条主线:
传输生成结果 ↓传输有类型的模型结果 ↓传输 Agent 执行状态变化 ↓让执行状态脱离连接并可恢复 ↓汇聚多个执行主体的因果进度与工作产物每次跃迁都不是因为“换了一种更快的网络协议”,而是上一阶段无法再回答新的运行时问题。
5.1 第一阶段:Token Streaming
第一阶段的核心对象是 Text Delta,目标是降低 TTFT:模型生成一部分,客户端就显示一部分,不再等待完整答案。
最小数据流是:
Request → Model Decode → Text Delta * → Finish / Error │ └──→ Client Accumulator → UI客户端通常维护一个临时 accumulator,将按协议解析出的 delta 依次追加并渲染。这里必须区分三个经常被混为一谈的对象:
| 对象 | 所在层 | 是否天然具备语义边界 |
|---|---|---|
| 模型内部 Token | 推理层 | 由 tokenizer 决定 |
| Text Delta | 应用协议层 | 由 API 事件 schema 决定 |
| Network Chunk | 传输实现层 | 只表示一次底层读取 |
三者不必一一对应。一次 delta 可以包含多个 Token,一个协议帧也可能被底层网络拆分。客户端应使用协议解析器恢复完整事件,不能把每次 socket read 当成一个完整 Token 或 JSON 对象。
架构综合:最小状态机
| 当前状态 | 输入 | 下一状态 | 客户端可做什么 |
|---|---|---|---|
IDLE | request accepted | GENERATING | 建立预览缓冲区 |
GENERATING | text delta | GENERATING | 追加并渲染,不宣告完成 |
GENERATING | finish signal | COMPLETED | 固化完整消息 |
GENERATING | model error | FAILED | 保留或标记不完整预览 |
GENERATING | accepted cancellation | CANCELLED | 停止接收并显示取消状态 |
Connection Closed 不在这张业务状态表中。它只是客户端观察到的传输事实,不能单独证明生成已经 COMPLETED、FAILED 或 CANCELLED。连接可能在终止事件到达前断开,也可能在服务端完成后、终止帧送达前断开。
在基线实现中,delta 只存在于当前连接和客户端内存,完整消息才可能在完成后写入数据库。其耐久边界是“预览缓冲区通常瞬时,最终消息才可能持久”。这直接决定失败语义:
| 失败场景 | 能否安全续接 | 正确处理 | 主要风险 |
|---|---|---|---|
| 已收到明确 finish,随后断线 | 通常可以结束本地归约 | 以终止事件为准 | 终态持久化仍取决于服务端 |
| 收到部分 delta 后断线 | 通常不能从任意字节位置续接 | 标记 incomplete,查询最终结果或重新请求 | 把截断内容误当完整答案 |
| 用户重新提交同一 prompt | 这是新一次生成 | 创建新 response identity | 两次输出被错误拼接 |
| 传输层重复交付片段 | 取决于协议是否有事件身份 | 按事件身份去重,否则重建完整结果 | 预览重复字符 |
该阶段的控制平面通常只有 start 与 cancel,数据平面几乎只有文本。因而至少有三个不变量:只有协议定义的终止信号才能完成消息;同一逻辑响应的 delta 必须按协议顺序归约;任何自然语言片段都不能被当作外部工具已经执行的证明。
进入下一阶段的触发条件是输出出现多种机器语义。当模型同时产生文本、推理摘要、工具参数、引用或代码执行状态时,把所有内容塞进文本通道会迫使客户端从自然语言中猜结构。系统需要的不再是更多分隔符,而是稳定的事件类型。
5.2 第二阶段:Typed Model Event Streaming
第二阶段把“所有内容都是 text”升级为 text.delta、thinking.delta、function_call_arguments.delta、code_interpreter.in_progress、response.completed 等类型化事件。
OpenAI Responses API 官方明确使用“semantic events”描述其流式机制:每个事件拥有预定义 schema,客户端可以只监听关心的事件类型。官方列出的事件覆盖 response lifecycle、output item、text delta、function call arguments、file search、code interpreter 等。[1]
Anthropic Messages API 同样以结构化事件表达 message、content block、text delta、input JSON delta 与 thinking delta。[3]
核心变化是:
客户端从“拼接一个字符串”升级为“按事件类型和对象身份归约多个状态”。
Model Output ↓Semantic Event Encoder ↓Typed Event Stream ↓Client Dispatcher ├── text reducer ├── reasoning reducer ├── tool-argument reducer └── lifecycle reducer一个可交错输出的客户端不能只维护全局 buffer。它至少需要按 response_id、item_id 或 content_block_index、事件类型维护独立 accumulator。事件类型回答“这是什么”,对象身份回答“它属于谁”,生命周期字段回答“它是否已经完成”。缺少任何一个维度,都可能把一个 tool call 的参数追加到另一段文本或另一个 call。
架构综合:Response 与 Output Item 是两层状态机
| 实体 | 起始状态 | 可重复的中间事件 | 权威完成边界 | 异常终态 |
|---|---|---|---|---|
| Response | CREATED / IN_PROGRESS | 多个 Item 生命周期事件 | response.completed 一类终止事件 | failed / cancelled / incomplete |
| Output Item | ADDED / STARTED | 对应类型的 delta | item / content block done | invalid / aborted |
Response 完成与单个 Item 完成不是同一个边界。客户端只有看到对应 Item 的终止事件,才能把该 Item 从“预览”提升为“可消费结果”;而所有必需 Item 终止后,Response 才能进入整体完成状态。
工具参数最能说明 typed stream 的工程价值。参数 delta 可能在字符串、转义字符、Unicode 序列或嵌套对象中间切开,例如:
{"path":"src/auth.ts","mode":"read"}正确路径是按 call identity 聚合,等待参数完成边界,再解析完整 JSON、执行 schema 校验并交给 Tool Runtime。除非操作明确可回滚且采用专门的推测执行机制,否则不应因为缓冲区“看起来已经是完整 JSON”就提前触发有副作用的工具;后续 delta 仍可能改变参数,网络截断也可能留下语法完整但语义未完成的片段。
Typed Event 还要求客户端定义明确的协议失败处理:
| 情况 | 不安全做法 | 更稳健的语义 |
|---|---|---|
| 未知事件类型 | 让整个 stream 崩溃 | 忽略或记录未知类型,保留前向兼容性 |
| delta 后没有 done | 把当前 buffer 当最终值 | 将该 Item 标为 incomplete |
| 单个 Item JSON 解析失败 | 宣告整个 Response 成功 | 将错误绑定到该 Item,并等待整体终态 |
| 连接异常但没有业务终态 | 根据 EOF 猜 completed | 查询状态或标记结果未知 |
| 同一 Item 出现交错 delta | 按到达位置追加到全局字符串 | 按 Item 身份和该流的顺序规则归约 |
但“有类型”不自动等于“可恢复”:事件仍可能只在一条连接上短暂存在,也未必有稳定 event_id、可重放游标或服务端历史。因此 typed ≠ durable ≠ replayable。
这一阶段最高层抽象仍然主要是 Model Response。即使协议给出了 function_call_arguments.done,它也只能证明模型形成了一个工具请求,不能证明工具已经获得批准、真正启动、产生输出、修改文件或完成重试。
进入下一阶段的触发条件是模型开始驱动外部世界。此时事件源必须从 Model Adapter 扩展到 Scheduler、Policy Engine、Tool Runtime 与文件系统,系统也需要表达比 Response 更长的执行生命周期。
5.3 第三阶段:Agent Runtime Event Streaming
第三阶段的最高层对象从 Response 升级为 Agent Run / Thread / Turn / Item。事件开始描述 Tool、Command、File Change、Approval、Plan、Message 与 Context Compaction;Token 只是 Agent Runtime Event 的一个子类型。
Codex App Server 是当前公开资料中非常清晰的工业例子。它把一次交互组织为 thread、turn 与 item,并在 turn 运行期间流式发送 item/started、item/completed、item/agentMessage/delta、命令输出、工具进度和审批请求。[2]
真正的跃迁是事件生产者和执行权威发生了变化:
Model Adapter ─┐Tool Runtime ─┤Policy Engine ─┼──→ Agent Runtime State Machine ──→ Runtime Event StreamScheduler ─┤File System ─┘一次工具调用不再是一段函数参数,而是一个跨组件执行链:模型提出 call;策略引擎决定是否允许或需要审批;调度器分配执行;工具产生进度与输出;Runtime 记录终态并把结果重新送回模型;模型随后继续生成、调用其他工具或结束 Turn。任何一个环节都可能等待、失败或取消。
数据平面与控制平面
| 平面 | 典型消息 | 语义要求 |
|---|---|---|
| Control Plane | start、steer、approve/deny、cancel、pause/resume | 必须绑定目标实体、请求身份和预期状态;允许双向请求/响应 |
| Data Plane | message delta、tool output、progress、file change、item completed | 面向观察和投影;需区分预览与权威终态 |
两者可以复用一条物理连接,但不能共享含糊语义。审批响应必须关联到具体 approval request;取消请求必须说明取消的是 Item、Turn 还是整个 Run;重复控制请求必须可去重,或由状态版本拒绝迟到写入。
这一阶段需要稳定的实体层级:
| 实体 | 回答的问题 | 典型生命周期边界 |
|---|---|---|
| Thread / Session | 长期上下文属于谁 | 创建、恢复、归档 |
| Run / Turn | 本次目标驱动的执行是否结束 | queued、running、waiting、terminal |
| Item | 当前可观察工作单元是什么 | started、delta、completed/failed |
| Tool Execution | 哪个有副作用的操作被请求 | proposed、approved、running、terminal |
| Attempt | 逻辑操作经历了第几次执行 | scheduled、started、lost/finished |
架构综合:运行时状态机
| 实体 | 允许的关键跃迁 | 禁止或需拒绝的跃迁 |
|---|---|---|
| Turn | QUEUED → RUNNING ↔ WAITING_APPROVAL/WAITING_TOOL/PAUSED → terminal | 仍有必需 Item 运行时直接 completed |
| Tool Execution | PROPOSED → POLICY_CHECKED → QUEUED → RUNNING → terminal | 未过策略检查直接 running;denied 后重新 running |
| Approval | REQUESTED → APPROVED/DENIED/EXPIRED | 对同一版本产生两个相反终态 |
| Item | STARTED → STREAMING → COMPLETED/FAILED/CANCELLED | completed 后被迟到 timeout 改回 failed |
状态名不必按此原样公开,但 Runtime 必须维护这些不变量:
- 一个实体只能提交一个权威终态;
- 父级完成必须建立在所有必需子 Item 已终止之上;
- 工具输出 delta 只证明“观察到输出”,不证明工具成功;
- 工具进程退出码、Tool Runtime 错误和流传输错误是三类不同失败;
- 审批必须绑定不可变动作,而不是只绑定一段可变化的自然语言描述。
审批绑定可以包含 approval_id、tool_execution_id、规范化参数哈希、工作区 revision 与过期时间。这是本文的架构建议,用于避免 Agent 在等待期间修改了命令参数,却继续消费旧批准。
运行时事件还迫使系统正视重试不是一个布尔值。建议以 tool_execution_id 表示逻辑操作,以 attempt_id 表示某次执行尝试。这样才能区分“当前 Attempt 失败但准备重试”“逻辑 Tool Execution 已经终止失败”与“外部副作用可能成功,但结果未知”。
| 操作类型 | 超时后的主要风险 | 推荐恢复语义 |
|---|---|---|
| 纯读取、确定性查询 | 重复成本 | 可按退避策略创建新 Attempt |
| 支持幂等键的外部写入 | 响应丢失但写入已成功 | 使用同一幂等键重试并查询结果 |
| 不支持幂等的外部写入 | 重复创建、重复部署 | 进入 outcome_unknown,先 reconcile,不盲目重试 |
| 基于旧 revision 的文件 patch | 覆盖并发修改 | 校验 base hash;冲突时重新生成或显式合并 |
| 长时间命令 | Runtime 超时但进程仍存活 | 先确认进程身份和状态,再决定 attach、cancel 或 retry |
这张表揭示一个关键边界:事件去重不等于副作用去重。客户端不重复渲染 tool.completed,并不能阻止底层命令执行两次。AWS Builders’ Library 对幂等 API 的建议进一步说明:应由调用方提供唯一 request identifier 来表达“这是同一逻辑请求的重试”,而不能仅根据参数相同推断重复,因为两次参数完全相同的调用也可能代表两个真实意图。[19] 对 Agent Runtime 而言,tool_execution_id 应承担这种意图身份,所有 Attempt 重用它,新的逻辑操作则必须使用新身份。
在事件权威性上,Codex App Server 的公开生命周期是 item/started → delta → item/completed,并建议将 item/completed 视为最终权威状态。[2] 因此 message delta 或 stdout delta 可以先服务低延迟 UI,最终 Item 负责校正完整结果。
但 Runtime Event Streaming 仍可能只是进程内或连接期的可观察性。如果事件没有持久化,客户端断线后仍无法可靠回答:错过了哪些事件、Runtime 是否继续执行、当前权威状态是什么、应从哪个位置恢复。
进入下一阶段的触发条件是任务寿命开始超过连接和 Worker 寿命:设备休眠、IDE 重启、网络切换、Worker 崩溃或用户隔天回来,都不应自动抹掉一次昂贵执行。
5.4 第四阶段:Durable Agent Event Streaming
第四阶段的核心变化是 Connection ≠ Execution。客户端可以连接、接收事件、断开;Agent 在后台继续;客户端之后再连接并恢复到同一个持久 Run。
Cursor 官方说明,Cloud Agent 在笔记本休眠或网络中断后仍可继续运行,客户端可以持续 stream,并在之后 reconnect;Cursor 与 Notion 的集成案例进一步明确,每个 follow-up 启动新的 run,通过 SSE 流式观察,并可在连接中断后从 last event 恢复。[4]
Anthropic Managed Agents 的公开事件模型提供了另一种耐久边界:event_start / event_delta 是低延迟、best-effort 的 preview,buffered agent.message 才是 authoritative record。官方进一步明确:服务端在负载下可以只发送连续前缀后丢弃剩余 delta;preview 永不持久化,断线后无法补取;客户端应重新打开 stream、读取事件历史,并用其中完整的 buffered event 替换按 (event_id, index) 积累的 scratch buffer。[12]
这些公开事实共同表明,核心对象不再是临时连接,而是 Persistent Run。
架构综合:Run、Attempt 与 Subscription 必须分离
| 对象 | 生命周期 | 故障含义 |
|---|---|---|
| Run | 从用户目标创建到业务终态 | 用户要完成的持久任务 |
| Attempt | 某个 Worker 在一段租约内推进 Run | Worker 可失败并由新 Attempt 接手 |
| Stream Subscription | 客户端观察 Run 的一次连接 | 断开只影响实时观察,不自动改变 Run |
所以 Subscription 断开 ≠ Attempt 失败,Attempt 失败 ≠ Run 失败,Run 完成 ≠ 客户端当时在线。
一个长任务 Run 可以拥有如下状态:
| 状态 | 进入条件 | 离开条件 | 是否终态 |
|---|---|---|---|
QUEUED | Run 已持久化但未获得 Worker | 调度成功或取消 | 否 |
RUNNING | 有有效 lease 的 Attempt 正在推进 | 等待、重试、取消或完成 | 否 |
WAITING_INPUT/APPROVAL | 缺少人类控制输入 | 收到匹配版本的响应或超时 | 否 |
RETRY_SCHEDULED | 可恢复失败且满足策略 | backoff 到期后重新排队 | 否 |
CANCELLING | 持久取消意图已接受 | 活动 Attempt 停止或被 fencing | 否 |
COMPLETED/FAILED/CANCELLED | 权威终止条件满足 | 不再离开 | 是 |
Worker 崩溃时,逻辑 Run 可以保持非终态。租约到期后旧 Attempt 被标记 lost,调度器从最近权威状态创建新 Attempt。为了阻止旧 Worker 恢复后继续提交,需要 lease epoch 或 fencing token;存储层只接受当前 epoch 的状态写入。
Durable 的关键不是“把每个字都写进数据库”,而是定义耐久等级:
| 等级 | 示例 | 丢失容忍度 | 恢复用途 |
|---|---|---|---|
| Transient Preview | token、stdout 高频 delta、动画进度 | 可合并、可降级、断线可丢 | 只优化低延迟体验 |
| Authoritative Event | 状态跃迁、审批决定、工具终态 | 不应静默丢失;需稳定身份和游标 | 去重、重放、审计 |
| Snapshot | 某一 cursor 上的完整 Run 投影 | 可重建但应版本化 | 降低长日志恢复成本 |
| Artifact | Diff、测试报告、截图、产出文件 | 按业务要求持久和校验 | 验证、交付、下游消费 |
低延迟 UI 可以先消费 preview,再由 authoritative event 校正;snapshot 用于缩短恢复时间,但不能制造日志中不存在的状态;Artifact 则应有独立版本和内容完整性校验。
Microsoft Azure Architecture Center 对 Event Sourcing 的公开说明给出了这一恢复模型的通用依据:事件以 immutable、append-only 方式保存,当前状态或 materialized projection 可以通过 replay 重建;长 event stream 可以使用 snapshot 缩短重建时间。该文档同时提醒,event store 与 message broker 职责不同,后者不能自动替代按实体查询、乐观并发与 snapshot 等事件存储能力。[16]
架构综合:权威状态与事件应原子提交。 一种常见方式是事务性 append 或 transactional outbox:
BEGIN compare-and-set run.state_version = N update run state to version N+1 append authoritative event(sequence = S+1) append outbox recordCOMMIT ↓Publisher sends event to live subscribers具体存储技术可以不同,但必须避免“状态已改变、事件未记录”和“事件已发布、状态提交失败”两类裂缝。AWS 对 Transactional Outbox 的定义正是把数据库更新与 outbox record 放进同一事务,以消除 database write + event notification 的 dual-write 不一致;其文档也明确提醒 Publisher 可能产生重复消息,消费者必须幂等并保持通知顺序。[17]
因此 Outbox Publisher 的投递通常按 at-least-once 设计,客户端 reducer 应按稳定 event_id 去重,而不是轻易假设端到端 exactly-once。Apache Kafka 对 delivery semantics 的定义也区分 at-most-once、at-least-once 与 exactly-once,并强调 exactly-once 声明必须核对生产者、消费者、故障与外部写入的具体边界。[18] Agent 写文件、调用 SaaS 或启动部署跨出了事件日志事务边界,仍需独立的幂等、reconciliation 或补偿语义。
架构综合:可靠重连协议至少包含以下步骤:
- 客户端保存最后一个已归约的 durable cursor,而不是最后一个 preview delta;
- 重新认证并订阅同一个
run_id,携带 cursor; - 服务端返回 cursor 之后的 authoritative events,或先返回带高水位的 snapshot;
- 客户端按
event_id去重,按协议定义的 sequence 归约; - 丢弃无法重放的旧 preview,用权威 Item 或 snapshot 校正 UI;
- replay 追平高水位后切换到 live stream。
实现还必须消除“先读快照、后订阅 live”之间的竞态。可以让快照返回高水位游标并从其后订阅;也可以先建立带缓冲的订阅,再读取快照并合并。否则恰好发生在两步之间的事件会永久丢失。
耐久化后的失败矩阵也更精确:
| 故障 | Run 应否继续 | 恢复动作 | 必要不变量 |
|---|---|---|---|
| 客户端断线 | 是 | 保持执行,之后 replay/reconcile | Subscription 不拥有 Run 生命周期 |
| Publisher 在 ACK 前崩溃 | 是 | 重发 outbox event | consumer 按 event identity 幂等 |
| Worker lease 丢失 | 通常是 | fencing 旧 Attempt,新建 Attempt | 只有当前 epoch 可提交 |
| 工具副作用后响应丢失 | 未知 | 查询外部状态或执行补偿 | 不把 transport error 直接记为 tool failed |
| 状态存储提交失败 | 不应发布该跃迁 | 回滚并重试事务 | event 与 state version 一致 |
| cancel 与 complete 竞争 | 由提交顺序和策略决定 | compare-and-set 单一终态 | 终态不可反转 |
取消在这一阶段也从一个瞬时按钮升级为持久控制意图。cancel.requested 只表示请求已接受,Run 随后进入 CANCELLING;只有活动 Attempt 停止、被 fencing 或进入受控遗留状态后,才能发出权威 CANCELLED。因此“点击取消成功”和“所有副作用已经停止”不能共用一个含糊事件。
进入下一阶段的触发条件是系统不再只有一个 Run、一个 Agent 和一条自然时间线。当多个 Agent 并行研究、编码和测试时,即使每条子流都可恢复,用户仍无法从大量交错事件中判断整体是否推进、谁阻塞了谁,以及成果由何而来。
5.5 第五阶段:Multi-Agent Execution / Progress / Artifact Stream
第五阶段把单 Agent 的事件时间线升级为多 Agent 的任务图、因果图和成果图:
Coordinator │ ┌────────────┼────────────┐ ▼ ▼ ▼ Research Agent Code Agent Test Agent │ │ │ └────── Typed Event Streams ──────┘ │ ▼ Causal Aggregation / Projection │ ▼ Progress / Artifact / VerificationxAI 的 Agent Dashboard 已经展示了在单一界面并行运行多个 Session、按状态汇总、将等待输入项置顶,并把 Subagent 折叠到发起它的 Session 之下的产品形态。[10]
Google Antigravity 2.0 官方 Codelab 将其描述为用于启动、监控和编排多个本地 Agent 的“central command center”。Codelab 将 Task List、Implementation Plan、Walkthrough 与 Screenshot 列为主要 Artifact,并把 Artifact 描述为沟通工作、获取反馈和缩短信任距离的载体;Code Diff 同样可审阅和评论,但官方特意说明它“technically not an artifact”,因此不应混入 Artifact 类型清单。[6]
这里必须保持证据边界:这些公开资料可以确认多 Agent 管理与 Artifact 产品模型,但不能据此反推出其完整内部 Wire Protocol。下面的调度、因果与一致性设计属于本文架构综合。
从线性 Run 到任务 DAG
Root Task ├── Research Task ─────────────┐ ├── API Implementation Task ───┼──→ Integration Task → Verification └── UI Implementation Task ────┘每个 Task 都需要独立身份、依赖和终态,而不能只用 Agent 名称区分日志:
| 字段 | 作用 |
|---|---|
task_id / parent_task_id | 表达任务分解与归属 |
agent_id / attempt_id | 表达执行者与重试身份 |
depends_on[] | 建立调度前置条件 |
caused_by_event_id | 解释任务为何被创建、解锁或取消 |
produced_artifact_ids[] | 关联执行与成果 |
local_sequence | 保证单 Task 流内的确定顺序 |
Task 状态机可定义为:依赖未满足时 BLOCKED;满足后进入 READY;获得 lease 后进入 RUNNING;执行中可以等待输入、安排重试,最终进入 completed、failed 或 cancelled。Coordinator 只有在依赖的权威终态满足条件后,才能解锁下游 Task,不能因为上游 Agent 发出“快完成了”的文本或 preview delta 就提前消费其产物。
多 Agent 顺序本质上是偏序,而不是全局总序。 更合理的模型是每个 Agent 或 Task 内维护 monotonic sequence,跨 Task 使用 dependency 与 causation edge,时间戳只辅助展示。比如:
plan.item.completed (A) ├──causes──> research.task.created (B) └──causes──> implementation.task.created (C)
spec.artifact.completed (D, produced by B) └──unblocks─> implementation.started (E, belongs to C)B 与 C 谁先显示并不重要;系统必须能够证明 A → B、A → C、D → E。不要依赖多机时间戳推断这些关系:时钟会漂移,并发事件本来就没有唯一自然顺序。因果字段让重放、调试和 UI 都能回答“为什么发生”,而不仅是“何时看见”。
多 Agent 因果模型至少应维持四个不变量:父任务不能早于其创建事件存在;依赖未满足的 Task 不能提交 started;旧 Attempt 不能覆盖新 Attempt 已提交的 Artifact;根 Run 的完成必须由任务图和验收状态推导,不能由某个 Coordinator 的总结文本决定。
架构综合:聚合层不是日志拼接器
| 职责 | 输入 | 输出 |
|---|---|---|
| Normalize | 各 Agent 私有事件 | 共同 Task/Attempt/Artifact 语义 |
| Correlate | run、task、agent、artifact identity | 层级与因果边 |
| Reduce | authoritative events | 可重建的任务图状态 |
| Project | 任务图、优先级、用户角色 | 阶段、阻塞项、成果、待处理操作 |
整体进度不应按 Token 数、事件数或“完成 Agent 数”简单平均。三个研究 Task 完成,并不等于集成代码已经可交付;一个只占节点数 5% 的关键验证 Task 可能决定整条关键路径。更可靠的进度应来自显式依赖、Task 权重或验收条件,并同时展示阻塞原因与可操作下一步。
多 Agent 故障也必须在图级定义策略:
| 场景 | 图级风险 | 推荐语义 |
|---|---|---|
| Worker lease 过期 | 旧、新 Attempt 同时写入 | fencing 旧 Attempt;新建 Attempt 接手 |
| 同一 Task 收到重复 completed | 父任务被重复解锁 | 按 Task/Attempt/Event 身份幂等归约 |
| 一个非关键子任务失败 | 整个 Run 被无条件终止 | 由显式 fail-fast、best-effort 或降级策略决定 |
| Root cancel | 子任务继续产生副作用 | 传播取消;等待确认或标记受控遗留状态 |
| 竞争性多 Attempt | 多个版本同时宣称成功 | 只允许一个 winner 提交权威产物,其余隔离或取消 |
| Coordinator 重启 | 丢失内存中的调度决定 | 从 durable task graph 和 lease 状态重建 |
Coding Agent 还有一个特殊并发边界:多个 Agent 可能同时修改同一工作区。独立 worktree、分支、沙箱或带版本条件的 patch 都是可选实现;无论使用哪一种,“合并”都应成为显式 Task,产生 merge.completed 或 merge.conflict.detected 一类权威结果。两个 Agent 都报告成功,不等于最终代码已经成功集成。
Artifact 在这一阶段成为一等对象,因为它既是成果,也是依赖和证据:
| Artifact 元数据 | 回答的问题 |
|---|---|
artifact_id / version | 下游消费的是哪个不可变版本 |
producer_task_id / producer_attempt_id | 谁产生了它 |
source_revision | 它基于哪个代码状态 |
caused_by_event_id | 哪个决定触发了它 |
content_hash | 内容是否完整、是否被替换 |
verification_status | 哪些检查已经验证该版本 |
Plan、Diff、测试报告和截图不再只是聊天附件,而是任务依赖、审核与完成判定的一部分。下游 Agent 消费哪个版本、测试验证哪个 commit、用户批准哪个 Diff,都应能沿因果边追溯。
面对多 Agent 事件放大,用户可见流还必须按语义分级:
| 优先级 | 事件 | 投影策略 |
|---|---|---|
| 高 | approval、blocked、failed、terminal、cancel conflict | 立即展示,不得被普通 delta 淹没 |
| 中 | Task transition、Artifact、verification | 合并成稳定进度时间线 |
| 低 | token delta、verbose stdout、heartbeat | 批量、采样、折叠或负载下降级 |
用户首先需要的是可操作的整体状态,同时能够按需下钻到某个 Task、Attempt 或原始事件。否则并行 Agent 只会把一个黑盒变成多个交错黑盒。
根 Run 的完成条件也必须高于“所有 Agent 都不再输出”。本文建议至少同时满足:所有 required Task 已进入允许的终态;预期 Artifact 已提交到确定版本;必须的 verification 已通过;不存在未决 approval 或结果未知的关键副作用。任何自然语言总结都只能解释这一结论,不能替代它。
因此用户可见流正在从 Streaming Text 升级为:
Streaming Work Progress +Streaming Work Products +Streaming Verification Evidence五个阶段的差异最终可以归结为:
| 阶段 | 最小核心对象 | 主要权威边界 | 断线后的典型能力 | 尚未解决的问题 |
|---|---|---|---|---|
| Token Streaming | Text Delta | 完整消息 | 通常只能重新请求 | 多语义输出无法可靠区分 |
| Typed Model Event | Response / Output Item | Item / Response done | 类型化本身不保证重放 | 不知道工具和运行时发生了什么 |
| Agent Runtime Event | Run / Turn / Item / Tool | Runtime terminal event | 可观察但未必可恢复 | 执行仍可能依附连接或进程 |
| Durable Agent Event | Persistent Run / Attempt | Durable event + snapshot | 按 cursor 重放并校正 | 多个 Run 的整体因果与进度难理解 |
| Multi-Agent Progress / Artifact Stream | Task Graph / Artifact | 图级终态 + 验证结果 | 恢复任务图并继续编排 | 转向更高层的自治治理与验证问题 |
所以这条演进路线不是 text → more text → faster text,而是:
connection-scoped bytes ↓typed semantic output ↓observable runtime state ↓durable recoverable execution ↓causal, verifiable multi-agent work第四部分:现代 Agent Event Streaming 的核心架构
这是全文的核心。
为避免把公开协议、工程推断和设计建议混为一谈,本部分使用三种标记:
- [公开事实]:可由官方文档或已注明性质的社区研究直接确认;
- [统一抽象]:从多套公开系统提炼出的解释模型,不代表任何一家厂商的完整内部实现;
- [设计建议]:面向生产系统的规范性建议,描述“应该怎样设计”,而不是声称现有产品都已如此实现。
6. 参考架构:从 Agent Runtime 到用户投影
一个现代 Agent 实时系统,不再是“模型连接到前端”的单通道,而是一个同时承载执行、记录、恢复、控制和展示的分层系统。可以抽象为:
Client / UI CLI / IDE / Web / SDK │ ▲ Control Command│ │Projected Stream ▼ │ ┌─────────────────────────┐ │ Stream / Control Gateway│ │ Auth · Resume · QoS │ └────────────┬────────────┘ │ ┌───────────┴───────────┐ ▼ ▼ ┌────────────────┐ ┌──────────────────┐ │ Run Supervisor │ │ Projection Layer │ │ State Machine │ │ Reducer / View │ │ Lease / Retry │ │ Aggregation │ └───────┬────────┘ └─────────▲────────┘ │ │ Model · Tool · File · Agent │ │ │ ▼ │ ┌──────────────────────────────────────┐ │ Internal Event Backbone │ │ Preview Lane │ Authoritative Lane │ └───────┬───────┴──────────┬───────────┘ │ │ ▼ ▼ Best-effort Fan-out Durable Event Log Snapshot / Outbox Audit / Recovery[统一抽象] 图中的组件未必对应独立服务。单机 CLI 可以用 AsyncIterator + SQLite 承担这些职责,云端系统可能使用 actor、队列、数据库和消息代理。真正重要的是语义边界,而不是组件名称。
| 层 | 负责什么 | 不应该负责什么 |
|---|---|---|
| Run Supervisor | 执行状态机、租约、重试、暂停与取消 | UI 文案和渲染 |
| Event Backbone | 规范化、路由、隔离生产者和消费者 | 默认充当永久真相源 |
| Durable Store | 权威事件、快照、恢复游标、审计记录 | 保存每个动画帧式 delta |
| Projection | 把语义事件折叠为查询状态和产品视图 | 触发不可逆工具副作用 |
| Gateway | 鉴权、订阅、控制命令、流控、重连 | 决定业务状态转换是否合法 |
一个生产级系统至少应守住以下不变量:
run_id的生命周期独立于任意 SSE、WebSocket 或进程间连接;- Preview 丢失只能降低实时体验,不能改变权威执行结果;
- 权威事件一旦对外可见,就必须能够被再次读取,或由等价快照覆盖;
- 执行状态转换必须由服务端状态机验证,不能由客户端“看起来已经完成”来决定;
- 同一权威事件重复投递,不得导致 reducer 重复追加内容或工具副作用重复执行;
- 恢复必须存在明确的同步边界,不能在 replay 与 live stream 之间留下事件窗口;
- Approval、Cancel、Resume 等控制命令必须拥有独立身份、前置版本和可确认结果。
因此,现代 Agent Streaming 的技术内核不是“更快地吐字”,而是:
把一个长时、并发、可中断的执行过程,编码成低延迟可观察、最终可校正、断线可恢复、控制可验证的协议。
7. 事件契约:Envelope、身份、顺序与版本
传统聊天系统以 Message 为中心;Agent Runtime 需要以 Event 为中心。原因并不只是事件类型更多,而是 Agent 中存在大量不能被可靠压缩成聊天文本的事实:命令启动、工具输入验证、权限等待、文件提交、子 Agent 汇合、运行租约转移、Artifact 验证等。
事件不是任意 JSON 日志。它必须同时回答四组问题:
Identity 它是谁?属于哪个执行实体?Semantics 发生了什么?负载版本是什么?Ordering 在哪个局部序列中?由什么导致?Policy 是否耐久?优先级和数据级别是什么?7.1 一个参考 Event Envelope
以下是**[设计建议]**,不是任何厂商的统一标准。CNCF CloudEvents 1.0.2 已把 id、source、specversion、type 等上下文字段与 event data 分开,并提供 subject、time 等可选属性;下面的设计借鉴了这种“公共外壳与领域负载分离”的思路,但额外加入了 Agent 所需的执行层级、局部顺序、因果、耐久度与安全字段,不能被称为 CloudEvents 的直接实现。[24]
{ "event_id": "evt_01J2Y8M7H6A4", "type": "tool.execution.completed", "envelope_version": 1, "schema_version": 3,
"occurred_at": "2026-07-13T07:21:43.118Z", "recorded_at": "2026-07-13T07:21:43.123Z",
"scope": { "thread_id": "thread_12", "run_id": "run_123", "turn_id": "turn_7", "task_id": "task_45", "agent_id": "coder_agent" }, "subject": { "kind": "tool_call", "id": "call_88", "revision": 4 }, "stream": { "partition_key": "run_123", "sequence": 1024, "cursor": "cur_eyJwIjoi..." }, "causality": { "correlation_id": "corr_turn_7", "causation_ids": ["evt_01J2Y8KX2P9"], "trace_id": "trace_abc", "span_id": "span_def" }, "delivery": { "durability": "authoritative", "priority": "P1" }, "security": { "classification": "workspace_sensitive", "redaction_profile": "remote-ui-v2" }, "payload": { "attempt": 2, "outcome": "succeeded", "result_ref": "blob_sha256:9f...", "duration_ms": 1842 }}几个字段看似相似,语义却不能混用:
| 字段 | 语义 | 常见误用 |
|---|---|---|
event_id | 一个不可变事实的身份 | 用它表达业务顺序 |
subject.id | 被改变的实体身份 | 把每次重试都当成新实体 |
subject.revision | 该实体的并发控制版本 | 当作全局序号 |
stream.sequence | 某个分区内的提交顺序 | 推断跨分区因果 |
occurred_at | 生产者观察到事实的时间 | 作为可靠排序依据 |
recorded_at | 权威存储接受事实的时间 | 当作真实发生时间 |
cursor | 客户端恢复位置的协议令牌 | 解析其内部结构并长期依赖 |
Preview 事件可以使用同一语义外壳,但通常没有权威 recorded_at 和 durable cursor,而使用 (preview_id, preview_seq) 表达临时片段顺序。不要把两类序号塞进同一字段后再让客户端猜测。
7.2 event_id:事件身份
event_id 的核心用途是 deduplication、审计定位、因果引用和跨投影关联。它必须满足:
- 同一个事实重试发布时复用同一个 ID;
- 同一业务动作的不同事实使用不同 ID,例如
tool.execution.started与tool.execution.completed; - ID 生成不能依赖单机自增主键,除非其唯一性作用域被显式编码;
- 事件内容提交后不可原地改写。纠错应产生补偿或修订事件。
客户端去重的基本规则是:
if event_id already applied: advance transport acknowledgement if needed do not apply reducer again但永久保存所有 event_id 会无限增长。对严格有序的 durable stream,更实际的做法是持久化已应用 cursor,再配合一个小型近期 ID 缓存处理重投和交错;对于跨分区合并流,则需要按分区分别保存 cursor。
7.3 sequence:顺序不是时间戳的替代品
时间戳不能提供可靠顺序:多机时钟会漂移,同一精度内可能出现多个事件,事件还可能在网络中延迟或倒序到达。
[设计建议] 至少区分三种顺序:
subject.revision 同一聚合实体的状态转换顺序stream.sequence 同一 durable partition 的提交/播放顺序preview_seq 同一 preview buffer 的片段顺序权威 stream.sequence 应在 durable append 成功时分配,而不是由生产者提前猜测。它通常只保证单调,不保证客户端过滤后的序列无空洞:某些事件可能因权限、类型过滤或压缩而不可见。因此客户端不应仅凭 1024 → 1026 就断言丢包,应由协议明确返回 gap、reset_required 或可比较 cursor。
如果多个 worker 并发修改同一 Run,必须再使用 expected revision、单写者 lease 或 compare-and-swap。给并发写入强行编号,只能得到存储顺序,不能自动消除业务竞争。
7.4 run_id:执行生命周期的锚点
run_id 表示一次可独立追踪、暂停、恢复和终结的执行。它不等于:
thread_id:可包含多次 Run 的长期对话或工作空间;turn_id:一次用户意图驱动的执行单元;task_id:可被调度、依赖或分派的工作节点;item_id:面向协议或 UI 的工作项;attempt_id:失败重试中的单次尝试。
[公开事实] Cursor 将 Run 暴露为 SDK 对象;Codex App Server 以 Thread、Turn、Item 组织执行生命周期。[2][4] 名词并不完全相同,但共同说明执行实体需要稳定身份,不能依附于临时连接。
一个工具重试的建模示例是:
tool_call_id = call_88 # 业务意图不变attempt_id = attempt_1 # 超时attempt_id = attempt_2 # 成功这样可以同时回答“这个工具最终怎样”和“第一次尝试为什么失败”。
7.5 parent_event_id / causation:表达“为什么发生”
时间上先发生,不等于因果上触发。推荐区分:
correlation_id:同一业务链的检索标签;causation_ids:直接促成当前事实的一个或多个事件;parent_event_id:在确实存在单父层级时的便利字段;trace_id/span_id:调用链观测上下文,不代替业务因果;task.parent_id/depends_on:长期任务图关系,不应伪装成事件父子关系。
多 Agent 汇合往往有多个直接原因,例如测试任务只有在代码 Agent 和环境 Agent 都完成后才启动,此时单个 causation_id 不够。causation_ids 应形成无环图;如果收到指向未来事件或形成环的关系,写入端应拒绝或标记为不可验证,而不是让 UI 猜测。
7.6 Schema Versioning:事件协议必须允许新旧客户端共存
事件一旦进入历史记录,就会比某一版客户端活得更久。[设计建议] 将外壳版本与负载版本分开:
envelope_version = 身份、顺序、因果等公共字段的版本schema_version = 当前 event type 的 payload 版本兼容性规则应写进协议,而不是只靠团队默契:
- 新增可选字段通常是向后兼容变更;旧 reader 必须忽略未知字段;
- 删除字段、改变单位、改变枚举含义属于破坏性变更,应提升版本或创建新事件类型;
- 存储中的旧事件保持原样,通过 upcaster 在读取边界转换,不要批量静默改写历史;
- 未知的非关键事件可被跳过,但未知的状态转换事件不能悄悄忽略,应触发客户端升级或 snapshot resync;
- 金额、字节、时间等字段必须固定单位;字符串
status的语义不能跨版本重定义; - 每种事件应有 fixture 和兼容性测试,至少覆盖“旧 writer → 新 reader”与“新 writer → 旧 reader”。
事件命名也应表达已经发生的事实,例如 approval.required,而不是模糊命令 requireApproval。Command 使用祈使语义,Event 使用过去事实语义,这是后续状态机可验证的前提。
8. 事件域:从 Run 生命周期到 Artifact
事件 taxonomy 的目标不是列出尽可能多的名称,而是让每种事实拥有稳定的聚合实体、合法前态和完成边界。
| 事件域 | 主要实体 | 它回答的问题 |
|---|---|---|
| Run / Turn | Run、Turn | 整体执行处于什么生命周期 |
| Model | Request、Message、Content Block | 模型请求和输出是否完整 |
| Tool / Command / File | Call、Process、Change Set | 外部副作用进行到哪一步 |
| Human Interaction | Approval、Input Request | 为什么等待,谁作出了决定 |
| Agent / Task | Agent、Task、Handoff | 谁在工作,依赖与汇合是什么 |
| Artifact | Artifact Version | 工作产品是什么,是否验证 |
8.1 Run Lifecycle Events
建议的 Run 主状态机为:
created → queued → running ───────────────┬→ completed │ ├→ failed ├→ waiting_input ────┤ ├→ paused ───────────┤ └→ recovering ───────┴→ cancelled相应事实可以是:
run.createdrun.queuedrun.startedrun.waitingrun.pausedrun.resumedrun.recovery.startedrun.completedrun.failedrun.cancelled状态机需要明确以下不变量:
completed、failed、cancelled是终态;同一 Run 只能提交一个终态;cancel.requested是控制意图或中间事实,不等于run.cancelled;只有执行已停止并完成清理后才能发出后者;run.waiting必须带reason和关联对象,例如approval_id,否则无法可靠恢复;- 重复
run.started不得被解释为第二次执行;worker 恢复应使用 attempt/lease 信息; message.completed、tool.execution.completed都不蕴含run.completed。
状态转换表应成为可执行协议测试,而不是只画在文档里:
| Event | 允许前态 | 新状态 | 必须同时满足的条件 |
|---|---|---|---|
run.queued | created | queued | 调度请求已耐久接受 |
run.started | created/queued/recovering | running | 当前 worker 持有有效 lease epoch |
run.waiting | running | waiting_input | waiting_on 对象已在同一提交边界持久化 |
run.paused | running/waiting_input | paused | 已到达声明的安全暂停点 |
run.resumed | paused/waiting_input/recovering | running | 等待条件已满足且 lease 有效 |
run.completed | running | completed | 必需 Task 均终结;无未决 Approval 或 outcome-unknown 副作用 |
run.failed | 任一非终态 | failed | error class、retryability 与失败主体已记录 |
run.cancelled | 任一非终态 | cancelled | active attempt 已停止,或残留资源已明确列入清理状态 |
cancel_requested、waiting_reason、recovery_attempt 等往往更适合作为与主 status 正交的字段,否则把所有组合都塞进单一枚举会造成状态爆炸。但无论采用单枚举还是正交字段,允许组合和终态条件都必须被验证。
状态转换验证可以写成:
append(event, expected_revision): state = load(event.subject.id) assert state.revision == expected_revision assert transition_table.allows(state.status, event.type) persist(new_state, event, outbox) atomically这段逻辑必须位于权威 Runtime,而不是 UI reducer。
8.2 Turn Events
turn.startedturn.steeredturn.interruptedturn.completedturn.failedTurn 是“当前用户意图驱动的一段可持续执行”,不必等同于一问一答。turn.steered 应记录输入身份、接收时的 turn revision 和实际生效边界;如果 steering 到达时 Turn 已终结,服务端应明确拒绝或创建新 Turn,不能悄悄把输入附加到错误执行。
[公开事实] Codex App Server 支持 turn/start,并可通过 turn/steer 向 in-flight turn 追加用户输入。[2] 这说明控制输入与普通聊天消息已经具有不同协议语义。
8.3 Model Events
模型域至少应区分请求、消息和内容块:
model.request.startedmessage.startedmessage.delta # previewreasoning.summary.delta # previewmessage.completed # authoritative content boundarymodel.usage.recordedmodel.request.completedmodel.request.failedmessage.completed 表示一条消息内容完整;model.request.completed 表示一次模型调用结束;两者都不表示 Run 已完成。重试时还应保留 request_id 和 attempt,否则超时后第二次请求的 delta 可能与第一次混合。
[公开事实] OpenAI Responses API 的 semantic events 与 Anthropic Messages API 的 content_block_delta 都把 token 增量放进更大的结构化事件体系。[1][3] 因此 Token Streaming 应被视为 Model Event 的高频子类型,而不是整个 Agent 协议。
8.4 Tool Events
工具调用不是一个瞬时动作,至少跨越以下边界:
tool.call.proposed ↓tool.input.delta # 可选 preview,可能不是合法 JSON ↓tool.input.completed # 完整字节边界 ↓tool.input.validated / rejected # 解析、schema 与策略验证 ↓approval.required / resolved # 按策略可选 ↓tool.execution.started ↓tool.output.delta # 可选 preview ↓tool.execution.completed / failed / outcome_unknown需要特别区分三种 streaming:Tool Intent、Tool Argument、Tool Execution Output。它们分别缩短“知道模型想调用什么”“看到大参数到达”“看到工具执行进度”的等待时间,可靠性边界并不相同。
[公开事实] Anthropic fine-grained tool streaming 允许工具参数在服务端尚未完整缓冲和 JSON 校验时逐片到达;客户端必须能够处理 partial 或暂时 invalid JSON。[11] 因此 tool.input.delta 只能进入 scratch buffer,不能直接触发工具。
[设计建议] 只有 tool.input.validated 之后才能进入审批或执行。每次执行还应携带 attempt_id 和 idempotency_key。如果进程在外部工具已执行、结果尚未持久化时崩溃,而外部系统不支持幂等查询,状态应是 outcome_unknown,不能武断重试并声称 exactly-once。
8.5 Command Execution Events
Coding Agent 常见:
command.startedcommand.stdout.deltacommand.stderr.deltacommand.output.truncatedcommand.completedcommand.failed生产级输出 delta 至少需要:
{ "process_id": "proc_7", "stream": "stdout", "byte_offset": 65536, "encoding": "utf-8", "data": "..."}byte_offset 使重复块和缺口可检测;command.completed 应包含 exit_code、signal、duration_ms、output_truncated 和可选最终输出引用。stdout 与 stderr 是两个独立流,除非 Runtime 使用同一 PTY 或在采集点分配统一序号,否则不能仅凭客户端到达时间声称精确还原两者交错顺序。
如果消费者过慢,Runtime 仍必须持续排空子进程管道,否则子进程可能因 pipe buffer 填满而阻塞。正确退化通常是“本地 bounded spool + 合并/截断 preview + 最终摘要”,而不是停止读取 stdout。
[公开事实] Codex App Server 公开 item/commandExecution/outputDelta,其 process control API 使用 process/outputDelta 与 process/exited 表达进程输出和终止。[2]
8.6 File Events
文件事件需要区分“建议修改”和“实际落盘”:
file.change.proposedfile.diff.delta # previewfile.change.approval_requiredfile.change.commit_startedfile.change.committedfile.change.failed权威提交事件建议携带:
change_set_idpath / repository-relative identitybase_content_hashnew_content_hashworkspace_revisionapplied_diff_refbase_content_hash 可以检测 Agent 读取文件后用户又修改文件的 lost update。多文件 patch 应有 change_set_id 与逐文件结果;如果底层文件系统无法提供真正原子事务,就不能把“逻辑 change set”描述为物理原子提交,应显式记录 partial failure 和补偿结果。
[公开事实] Codex App Server 的 fileChange Item 与审批流程体现了文件变更作为一等执行对象的设计。[2]
8.7 Human Interaction Events
approval.requiredapproval.resolvedapproval.expireduser.input.requireduser.input.receivedexecution.pausedexecution.resumedapproval.required 必须是 durable state transition,至少绑定:
approval_idrun_id / item_idaction_digest # 命令、参数、目标资源的不可变摘要requested_scopeexpires_atsubject_revision客户端发送的是 approval.resolve Command,而服务端验证后产生 approval.resolved Event。决策应单次使用且幂等:相同 command_id 重试返回原结果;不同命令试图覆盖已决议 Approval 时返回 conflict。批准 A 命令不能被复用于参数已变化的 B 命令。
[公开事实] Codex App Server 公开的审批采用 server-initiated request / client response 模式。[2] 这说明 Human-in-the-loop 是 Runtime Protocol 的一等状态转换,而非前端弹窗细节。
8.8 Agent / Subagent Events
agent.spawnedagent.startedagent.status.changedagent.waitingagent.completedagent.failedhandoff.startedhandoff.completedtask.dependency.satisfied事件需要区分 agent_id、task_id 与 attempt_id。同一 Task 可以因 worker 故障被另一个 Agent attempt 接管;若只记录“Agent B started”,恢复时无法判断它是在重复工作还是接管任务。
agent.waiting 必须指向等待对象或依赖集合。汇合事件应列出全部 satisfied dependency,而不是只挂在最后到达的父事件下。对于共享工作区,并发 Agent 的事件顺序也不能解决文件写冲突;仍需要 workspace revision、隔离分支、lease 或 merge policy。
8.9 Artifact Events
artifact.createdartifact.version.createdartifact.verification.startedartifact.verifiedartifact.rejectedartifact.completedArtifact 是有稳定身份和版本的工作产品,不只是聊天附件。在本文的统一架构中,Plan、Task List、Code Diff、截图、测试证据和部署结果都可以被建模为 Artifact。推荐让每个版本不可变,并记录 content_ref、媒体类型、内容哈希、producer、source event IDs 与 verification result。artifact.updated 若覆盖原内容,会破坏审计与因果定位,宜展开为新版本。
[公开事实] Google Antigravity 将 Implementation Plan、Task Lists、Walkthrough 与截图列为主要 Artifact,并把 Code Diff 描述为同样可审阅和评论、但“technically not an artifact”的独立对象。[6] 因此,把 Diff 统一纳入 Artifact 是本文的架构扩展,不是对 Google 产品术语的照抄。
长时 Agent 不应只流“它说了什么”,还要流“它做出了什么、依据是什么、是否经过验证”。
9. 实时正确性与持久执行
实时 Agent 的正确性不是单点能力,而是一条连续链路:前端先消费可丢失的 Preview,Runtime 再提交权威事实,内部 Backbone 负责隔离生产者与消费者,Event Store 最终承担重放、恢复和压缩。把这四步拆成互不关联的模块,会在 final 交付、断线恢复和故障转移之间留下状态裂缝。
9.1 Preview 与 Authoritative:实时性和正确性的分层
这是 Agent Streaming 最关键的协议边界。低延迟要求片段尽早可见;正确性要求完整边界、验证和持久化。两者若使用同一种承诺,就会同时拖慢 UI 并污染事实记录。
9.1.1 实时系统的天然矛盾
Preview Plane Authoritative Plane------------- -------------------低延迟 完整且已验证可能缺片、重复、乱序 有稳定身份和 durable cursor允许合并或丢弃 不允许静默丢失只影响临时展示 驱动恢复、审计和最终状态Preview 不是“低质量 Event Store”,而是明确的 scratch layer。推荐使用 preview_id + subject_id + preview_seq + base_revision,让客户端知道片段属于哪个临时对象,以及最终事件应替换哪一层 overlay。
9.1.2 Anthropic 的公开范例:Preview 是 scratch buffer,Buffered Event 才是 record
[公开事实] Anthropic Managed Agents 文档说明:event_start 与 event_delta 用于低延迟预览;preview 是 best-effort display aid;buffered agent.message 才是 authoritative record;负载下 delta 可以被丢弃,且 preview 不持久化、断线后不可补取,但完整 buffered event history 可以读取。[12]
其公开语义可以抽象为:
Agent Output / \ preview delta buffered final │ │ scratch overlay durable record └──── reconcile ─────┘这里的关键不是具体事件名,而是协议明确承认:用户可能没看到全部动画片段,但最终记录仍然完整。
9.1.3 Codex 的公开范例:item/started、delta 与 item/completed
[公开事实] Codex App Server 在工作单元开始时发送 item/started,执行中发送各类 delta,结束时以 item/completed 给出最终 Item;官方建议把 completed Item 作为 authoritative state。[2]
item/started ↓item/.../delta # 临时进度 ↓item/completed # 最终对象这与 Preview / Authoritative 分层具有相同核心:delta 优化等待体验,completed object 负责校正最终事实。
9.1.4 为什么不能持久化所有 Token Delta
把每个 token、terminal chunk 和 diff fragment 都作为同等级永久记录,会造成写放大、索引膨胀、replay 变慢、查询噪音以及 reducer 重建成本上升。更合理的耐久度层级是:
| 层级 | 例子 | 恢复承诺 | 典型保留策略 |
|---|---|---|---|
| Transient Preview | token、stdout 小片段、动画进度 | 不补发 | 内存短缓存,拥塞可丢 |
| Durable Progress | 阶段变化、周期性 checkpoint | 可恢复当前阶段 | 限时保留或可压缩 |
| Authoritative Fact | 完整消息、工具结果、审批、终态 | 可重放、可审计 | 按产品与合规策略保留 |
不是所有高频数据都必须丢弃。若 terminal 原始输出本身具有审计价值,可以把它分块写入 blob/object storage,并在权威完成事件中保存内容引用;Event Log 只保留索引和边界,而不是数万个字符级行项目。
9.1.5 临时展示必须有明确提交边界
实时性不要求每一个实时片段都成为系统事实;最终一致性要求每个临时对象都有明确的提交、替换或废弃边界。
仅说“最终会一致”还不够,协议必须定义如何一致。
9.1.6 Preview Commit Protocol:从临时片段到最终事实
以下是**[设计建议]**的最小提交协议:
1. begin preview(subject_id, preview_id, base_revision)2. emit preview.delta(preview_id, preview_seq=N) # best effort3. Runtime 形成完整结果并完成验证4. 在一个 durable commit 中: compare subject revision update authoritative state append final event write outbox record5. commit 成功后发布 final event6. Client 按 subject_id / preview_id 删除 overlay,以 final payload 替换服务端应使用两种不同 API,避免开发者误把 preview 当权威事实:
publishPreview(delta) # 可以失败或被丢弃appendAuthoritative(expectedRevision, fact) # 成功意味着 durable commit客户端 reducer 可以近似写成:
onPreview(p): if p.preview_seq <= overlay[p.preview_id].last_seq: return if sequence has a gap: overlay[p.preview_id].incomplete = true append_to_overlay(p)
onAuthoritative(e): if durable_cursor_already_applied(e.cursor): return state = reduce(state, e) delete overlay linked to e.subject.id / e.payload.preview_id persist_applied_cursor(e.cursor)注意:final payload 不必等于所有 delta 的字符串拼接。服务端可能修复 partial JSON、过滤不安全内容、压缩推理摘要或因取消而提交部分结果。客户端必须替换,不能只在 preview 尾部继续追加。
| 故障场景 | 正确结果 |
|---|---|
| Preview 全部丢失 | final 仍可独立渲染 |
| final 重复投递 | event/cursor 去重,不重复追加 |
| Preview 与 final 内容不同 | final 覆盖 overlay |
| 断线发生在 final 之前 | overlay 可消失;重连后等待或读取最终事实 |
| durable commit 成功、实时 publish 失败 | outbox 或重放路径再次交付 final |
| Runtime 在外部副作用后、结果提交前崩溃 | 进入可查询恢复或 outcome_unknown,不能盲目重做 |
9.2 Internal Event Backbone:执行模块如何解耦
现代 Agent 同时存在 Model Runtime、Tool Runtime、Command Runner、Sandbox、File System、Scheduler、Subagent Runtime、Approval Manager 和 Context Manager。如果每个模块直接操作 UI、数据库和 telemetry,任意新产品面都会要求修改执行核心。
[统一抽象] Event Backbone 的职责是把多生产者产生的语义事实标准化,并按不同可靠性需求交给多消费者:
Producers Backbone Consumers--------- -------- ---------Model ───────┐ validate / stamp / route ┌── UI ProjectorTool ────────┤ ├── Durable StoreCommand ─────┼──────→ preview lane / fact lane ────┼── Trace AdapterFile ────────┤ ├── MetricsSubagent ────┘ └── Audit Export它可以由进程内 typed channel、actor mailbox、AsyncIterator、队列、broker 或持久化 log 实现。单机系统不应仅为“架构看起来先进”而引入分布式 broker;当跨进程扇出、独立扩缩容、持久订阅或故障隔离成为真实需求时,再提高实现复杂度。
9.2.1 多生产者:统一事实写入边界
生产者只表达领域事实,但不能任意 emit({type: string})。推荐由 typed producer API 负责:
- schema validation 与版本标记;
- 补齐 scope、subject、trace 和 security context;
- 检查当前状态是否允许该事实;
- 为权威事件执行 append,而不是 fire-and-forget;
- 为 preview 实施大小、频率和优先级限制。
最危险的错误是双写:
update run_state succeedspublish run.completed fails如果权威状态与 Event Log 位于同一数据库,可在同一事务中更新 state、append event 并写 outbox;提交后的 publisher 再将 outbox 投递到实时总线。AWS 对 Transactional Outbox 的公开说明正是用同一事务消除 database update 与 event notification 的 dual-write 裂缝,并明确提醒 Publisher 可能重发,消费者仍需幂等。[17] 如果 Event Log 本身就是状态源,则 append 成功后由投影更新 read model。两种方式都比“先改状态,再尽力发通知”更可恢复。
外部工具副作用无法与本地数据库形成普通 ACID 事务。此时要持久化 operation intent/idempotency key,调用外部系统,再记录 result;崩溃后的 recovery worker 查询外部结果或进入 outcome unknown,而不是假装本地事务覆盖了网络另一端。
9.2.2 多消费者:隔离可靠性与消费速度
同一事件可被 UI Projector、Event Store、Trace Collector、Metrics Aggregator、Audit Logger 和 Debugger 消费,但它们不能共享一个会相互阻塞的无界回调链。
[设计建议] 为不同消费者建立独立 bounded subscription:
durable writer slow → 对权威 append 施加背压或拒绝新工作metrics exporter slow → 批量、采样或短时丢弃 telemetryone UI slow → 断开该订阅,保留其 durable cursoraudit sink slow → 使用独立可靠 spool,不允许静默跳过安全事实“事件化解耦”不意味着没有契约。每个 consumer 仍需声明可接受版本、delivery semantics、最大 lag、失败重试和 poison event 处理。否则只是把同步耦合变成不可见的队列堆积。
9.2.3 Claude Code 社区分析中的异步生成器链
[社区研究,非官方事实] how-claude-code-works 明确声明其为独立研究。其对约 2026 年 3 月底源码快照的分析认为,Claude Code 核心 query() 使用 async function*,持续 yield assistant、progress、stream event 和 tool result;用户体验分析描述了从 API SSE、callModel()、query()、QueryEngine、REPL 到终端渲染的生成器链路。[13][14]
这不能证明 Claude Code 当前或完整内部架构,但很好地说明了本地数据面的典型模式:
Push Source → Async Decode → Runtime State Machine → Yield Semantic Event → UI ProjectionAsync generator 提供自然的顺序和取消传播,但多消费者扇出、慢消费者隔离与 durable replay 仍需要额外机制;不能因为使用 yield 就认为已经拥有 Event Store。
9.3 Event Store 与 Durable Run:连接、恢复与压缩
现代长时 Agent 的基本等式是:
Connection ≠ Subscription ≠ Worker Lease ≠ Run- Connection 是临时传输通道;
- Subscription 是“从哪个 cursor 看哪些事件”的读取关系;
- Worker Lease 是某个执行器当前推进 Run 的资格;
- Run 是可持久跟踪和终结的业务实体。
9.3.1 为什么连接不能成为任务生命线
如果连接关闭就杀死 Agent,浏览器刷新、IDE 重启、设备休眠、手机切网或代理超时都会摧毁长时任务。正确模型是:连接只是观察和控制 Persistent Run 的窗口;是否因无观察者而暂停,应是显式产品策略,而不是 socket 生命周期的偶然副作用。
[公开事实] Cursor 说明 Cloud Agent 在设备休眠或网络掉线后仍继续运行,并支持之后 reconnect。[4] xAI Agent Dashboard 的公开页面足以证明多 Session 并行、状态排序与 Subagent 汇总,但本文不据此推断其断线持续执行或恢复协议。[10]
9.3.2 Durable Run 的最小组成
一个可恢复 Run 至少需要:
{ "run_id": "run_123", "status": "waiting_input", "revision": 47, "durable_cursor": "cur_1024", "checkpoint_ref": "snapshot_900", "waiting_on": {"kind": "approval", "id": "apr_7"}, "worker_lease": {"epoch": 12, "owner": "worker_9", "expires_at": "..."}, "updated_at": "..."}此外还需要任务图、已提交工具 operation、取消状态、审批状态、外部资源引用和足以继续运行的 context/checkpoint。仅保存聊天 transcript,通常不足以恢复正在等待的工具或子 Agent。
Worker crash recovery 还必须防止“双执行者”:
1. 新 worker 获取 lease,lease epoch 从 12 增为 132. 所有权威提交都携带 epoch=133. 旧 worker 即使恢复网络,其 epoch=12 的提交也被拒绝这种 fencing token 比单纯 heartbeat 更关键。Heartbeat 能发现可能失效的 worker,epoch 才能阻止旧 owner 在网络分区后继续写入。
9.3.3 Disconnect 不等于 Stop
Client Disconnect ↓Transport Lost ───────────────┐ │ no implicit transitionRun Supervisor: running ─────┘cancel、pause 与 detach 应是三个不同命令:
detach:关闭观察连接,Run 不变;pause:在安全边界停止推进,保留可恢复状态;cancel:请求终止并执行必要清理,最终产生 terminal event。
如果系统支持“最后一个客户端离开后自动取消”,也应由显式 retention/ownership policy 触发并记录原因,而不是把 TCP FIN 当作业务命令。
9.3.4 Reconnect:不能只“重新订阅现在”
重连协议必须避免 replay/live race。一个可行的**[设计建议]**流程是:
Client → resume(run_id, last_cursor, supported_schema_versions)
Server: 1. 重新鉴权,并验证 cursor 属于该 tenant/run/projection 2. 读取 latest compatible snapshot(如需要) 3. 从 durable log 确定 high-watermark H 4. 发送 snapshot + (snapshot.cursor, H] 的权威事件 5. 从 durable log 订阅 H 之后的事件 6. 发送 sync.completed(H),再进入 live 模式关键是第 3 至 5 步必须无缝。如果底层 log 支持 subscribe(after=H),即使订阅建立稍晚也可补齐;如果只有内存 fan-out,则必须先注册 live buffer,再读取 high-watermark/replay,最后去重合并。简单地“先查数据库,再连 WebSocket”会在两个动作之间丢事件。
Cursor 应被视为 opaque token。服务端可以在其中编码 partition、projection version 或签名;客户端只负责原样保存和回传。若 cursor 已超出 retention 或 projection schema 不兼容,应返回明确的 resume.reset_required,附带新 snapshot,而不是从当前 live point 悄悄继续。
[公开事实] Cursor/Notion 的案例提到 SSE Run 可在连接中断后从 last event resume。[4] Anthropic Managed Agents 的 preview 不重放,重连后通过 event history 获取断线期间的 buffered authoritative events。[12] 两者共同体现:恢复的是权威状态,不是每个临时动画帧。
9.3.5 Replay 与 Debugging
“Replay”至少有三种完全不同的含义:
| 模式 | 做什么 | 是否再次调用模型/工具 |
|---|---|---|
| Playback | 按历史事实还原时间线或 UI | 否 |
| State rebuild | 从 snapshot + authoritative events 重建状态 | 否 |
| Re-execution | 用相同输入重新执行流程 | 是 |
前两者可以相对确定;Re-execution 通常不确定,因为模型输出、网络 API、文件系统和时间都可能变化,而且重复执行工具可能造成副作用。调试界面必须清楚标注是在“播放记录”还是“重新执行”,不能把两者都叫 Replay。
同样需要严谨区分 Event Streaming 与 Event Sourcing。严格 Event Sourcing 要求业务状态由权威事件重建;许多 Agent 系统实际依赖:
Event History + State Snapshot + External Tool State + Content Blobs因此,**[统一抽象]**可以说现代 Durable Agent 越来越采用事件溯源思想,但不能仅因存在事件流就断言某产品实现了完整 Event Sourcing。Microsoft 对 Event Sourcing 的公开说明也把 immutable append-only event、replay、materialized view 与 snapshot 作为一组相关但职责不同的机制,并提醒 event store 不等同于 message broker。[16]
9.3.6 Snapshot、Compaction 与 Tombstone
长 Run 不应每次从 evt_1 重放到 evt_100000:
Current State = Snapshot(through_cursor=C90000) + Authoritative Events(C90000, current]可靠 snapshot 至少包含:
run_idthrough_cursor / through_sequenceaggregate_revisionreducer_versionminimum_event_schema_versionstate_checksumcreated_atSnapshot 必须对应一个已提交的权威边界。先读取 state、稍后再猜一个 cursor,可能把某事件既包含在 snapshot 中又重放一次,或两边都漏掉。解决方法是同事务生成,或使用 MVCC/read barrier 保证 state 与 high-watermark 一致。
Compaction 也不是简单删除旧事件。推荐区分:
- 可折叠:字符 delta、周期性 progress、被最终对象完整覆盖的临时块;
- 需保留或转入审计层:审批决定、外部副作用、权限变更、终态、失败原因;
- 可外置:大段 terminal output、截图、模型完整内容,Event Log 保存 hash 与
content_ref; - 有引用约束:仍被后续
causation_ids、Artifact provenance 或审计记录引用的事件不能无痕消失。
压缩或删除 durable range 时,应写入可验证的 tombstone/compaction manifest,而不是制造无法解释的 cursor 空洞。Manifest 至少记录被替换的 cursor range、替代 snapshot 或 summary 的引用与 hash、压缩原因、策略版本和执行时间。它带来三条规则:旧 cursor 若落入已压缩区间,服务端返回 reset_required 并指向兼容 snapshot;后续事件若仍引用被压缩 event,应保留最小 identity/causality tombstone;“因保留策略删除 payload”和“业务实体被删除”是两种不同事实,不能共用一个含义模糊的 tombstone。
对于合规删除,可以清除或加密销毁敏感 payload,同时保留不含敏感内容的序列边界、类型和删除证明;是否允许这样做取决于具体法规和产品政策。架构上必须承认:审计不可变性与数据删除权可能冲突,需要显式 retention class,而不是承诺所有事件永久不可删除。
当 reducer 或 schema 升级时,可以重建新 snapshot;如果旧事件已被压缩到不足以用新 reducer 重建,就必须把 snapshot 本身视为长期兼容资产,并维护 snapshot upcaster。这是 compaction 会引入的真实技术债务。
10. 投影、聚合与背压
权威事件只有经过 Projection 才成为客户端状态,经过 Aggregation 才成为人类可理解的进度;当生产速度超过消费速度时,Backpressure 与 QoS 又决定哪些数据等待、合并、降级或转入重放。三者共同构成 Runtime 到用户界面的数据处理链,不能分别只当作 UI、性能和网络问题。
10.1 Event Projection:同一 Runtime 如何服务不同客户端
Runtime 应发布 tool.execution.started、file.change.committed、approval.required 这类语义事实,而不是“显示绿色按钮”或“在终端打印一行”。Projection 的职责是把稳定事实转换为特定消费者需要的 read model:CLI 时间线、IDE 工具卡片、Web Dashboard、移动端审批列表或观测控制台。
Authoritative Event Log │ ├── Run State Projection ├── Timeline Projection ├── Pending Approval Projection ├── Artifact Index Projection └── Metrics / Trace AdapterProjection 与 Event Store 的关系类似 CQRS 中的写模型和读模型,但不要求系统完整采用 CQRS。关键点是:一个读模型损坏可以重建,而执行事实不应由某个 UI 缓存反向推断。
10.1.1 同一事件,不同投影
同一个 tool.execution.started 可以被投影为:
| 消费者 | 表现 |
|---|---|
| CLI | 一行可更新的 Running grep 状态 |
| IDE | 与代码位置关联的 Tool Card 和取消控件 |
| Web | Timeline Item、耗时和 Agent 归属 |
| Observability | Tool span 的开始边界 |
| Approval Center | 若需要审批,则进入 pending action 列表 |
服务端和客户端都可以有 projection。服务端适合维护跨设备共享的 Run summary、待审批索引和分页时间线;客户端适合维护 viewport、展开状态和 preview overlay。二者不应争夺同一权威字段:服务端 run.status 是事实投影,客户端 isExpanded 是纯展示状态。
一个权威 reducer 应尽量满足纯函数性质:
reduce(state, event) -> new_state
约束:- 不读取当前时间、随机数或网络- 不调用工具或再次产生外部副作用- 相同 snapshot + 相同有序事件得到相同结果- 重复 event_id 不改变结果- 只接受兼容 schema,未知关键事实显式失败伪代码如下:
project(snapshot, durable_events): assert snapshot.reducer_version is supported state = upcast(snapshot.state) cursor = snapshot.through_cursor
for event in durable_events: if event.cursor <= cursor: continue event = upcast(event) state = handler[event.type](state, event) cursor = event.cursor
return {state, cursor, reducer_version}这里的 <= 是协议层可比较关系的简写;若 cursor 是完全 opaque,比较和去重由 stream client 库完成,业务 reducer 不应自行解析。
Preview 不进入上述权威 fold,而作为 overlay:
Rendered View = Projected Authoritative State + Ephemeral Preview Overlayfinal event 到达时先推进权威状态,再删除对应 overlay。这样即使 preview 缺片、乱序或在刷新时消失,read model 仍可由 durable history 重建。
10.1.2 为什么这是产品扩展性的关键
稳定的 semantic protocol 允许同一 Runtime 支持 CLI、Desktop、IDE Extension、Web Dashboard、远程控制和调试器,而无需让执行核心知道每个界面的组件结构。
Projection 自身也需要版本治理:
- 保存
projection_name、projection_version和through_cursor; - 部署新 reducer 时从兼容 snapshot 或 Event Log 建立 shadow projection;
- 校验关键计数、终态和 checksum 后再切换读流量;
- projector 崩溃后从自身 cursor 重放,而不是从“现在”继续;
- 对 poison event 隔离并告警,不能跳过关键状态转换后继续给出看似正常的 UI。
若客户端收到比自身更新的非关键事件,可以保留 cursor 并以 generic timeline item 降级;若未知事件可能改变权限、终态或 Approval,则必须要求升级或重新获取服务器生成的 snapshot,不能 silent ignore。
[公开事实] Codex App Server 允许远程 TUI 通过 WebSocket 连接同一 App Server,这是 Runtime 与 Presentation 分离的公开例子。[2]
10.2 Event Aggregation:从 Raw Event 到用户进度
一个 Run 可能包含上万 token delta、上千 terminal chunk 和数百工具事实。把它们逐条显示既不是透明,也不是可观察。Aggregation 的目标是提高信息密度,但必须区分三层,否则性能优化会不小心改变业务语义。
| 层 | 典型操作 | 是否生成新业务事实 |
|---|---|---|
| Transport aggregation | 把多个 frame 批量发送、压缩 | 否 |
| Stream coalescing | 合并同一 stdout/preview 的连续片段 | 否 |
| Semantic projection | 从任务与工具事实推导“正在运行测试” | 生成派生 read model,不改原事实 |
| Presentation grouping | 把 20 次 Read 折叠为可展开的一组 | 否 |
聚合主要通过以下三种方式降低传输与认知成本,同时保留回到原始事实的路径。
10.2.1 Coalescing:合并连续片段
对相同 (run_id, item_id, stream) 的连续 delta,在一个大小或时间窗口内合并为一次网络/渲染更新。窗口必须在以下边界强制 flush:对象终结、流类型改变、高优先级事件到达、权限级别改变、buffer 达到上限。
Coalescing 不应跨 subject 合并,也不应丢失可检测性。合并块可以记录 first_offset、last_offset、source_count 和 truncated,这样排障时能区分“生产者只产生一次输出”和“中间层合并了一百次输出”。
10.2.2 Grouping:折叠同类工作
连续读取 A、B、C 三个文件可以展示为“已读取 3 个文件”,但原始子项仍应可展开,失败项不能被成功计数掩盖。Grouping key 通常包含 event type、task/agent、phase 和 security class;跨 Agent 或跨权限边界分组会造成归属和授权混乱。
[社区研究,非官方事实] Claude Code 社区分析描述了同类型工具调用的分组渲染,用于减少视觉噪音。[14]
10.2.3 Semantic Progress Projection:生成可行动进度
底层 grep → read → read 可以投影成“正在分析认证模块”,但这一层最容易产生虚假确定性。[设计建议] 优先由显式 Task、Plan step、Tool category 和 Artifact 状态确定性推导,并保留 provenance:
{ "progress_key": "task_auth_analysis", "label": "正在分析认证模块", "phase": "running", "derived_from": ["evt_301", "evt_302", "evt_303"], "projection_version": 4}如果进度文案由模型概括,应标记为 heuristic summary,不能用它驱动权限、重试或完成判定。task.completed 只能来自权威状态机,不能因为摘要写了“完成”就被推断出来。
10.2.4 为什么“更多事件”不等于“更透明”
真正的透明度要求信息 Relevant、Timely、Actionable、Understandable。产品层至少应同时保留三种视图:
- 当前阶段:用户第一眼知道 Run 在执行、等待、失败还是终结;
- 可行动项:Approval、Input Request、冲突和失败置顶,不被 token 洪流淹没;
- 可展开证据:需要时可以查看命令、Diff、来源事件和完整 Artifact。
Aggregation 必须遵守“不得掩盖终态、失败、安全决策和数据缺失”的不变量。例如 terminal output 被截断时,UI 必须显示 truncated;十个并行任务九成成功时,不能聚合成“全部完成”。
Agent UI 的竞争力不在于展示多少 Raw Event,而在于能否把 Execution Graph 压缩成可验证、可行动的 Progress Model。
10.3 Backpressure 与 Event QoS
Backpressure 是端到端问题。可能拥塞的并不只是服务端队列,而是完整链路中的任一点:模型解码、Runtime 规范化、durable append、broker fan-out、网络、客户端解析、state update 或 renderer。Reactive Streams 将 non-blocking back pressure 视为异步流处理的核心,并明确指出其目标之一是让线程间队列保持 bounded;Agent 系统还需在此基础上加入事件优先级、耐久游标与业务级降级策略。[27]
10.3.1 事件生产者比 UI 快
假设 test runner 每秒产生数千个小 stdout chunk。若每个 chunk 都触发独立 frame、JSON parse、store update 和 React render,系统会在业务工作尚未饱和前先被调度、GC 和重绘开销拖垮。
生产系统应为每一跳定义:
queue capacityhigh / low watermarkmaximum event agebatch size / flush boundaryoverflow actionupstream feedback mechanism高低两个 watermark 用于 hysteresis:达到 high 后进入降级,只有降到 low 才恢复,避免负载在阈值附近频繁切换策略。
10.3.2 不同事件需要不同流控手段
不同技术解决不同问题:Buffer 吸收短峰值;Batch 降低每条固定开销;Throttle 限制更新频率;Coalesce 保留最新或连续内容;Sample 用于 telemetry;Drop 只适用于允许降级的数据;Admission Control 则在系统没有执行容量时拒绝新 Run。
[设计建议] 按事件类别定义 QoS,而不是给整个连接一个统一策略:
| 类别 | 示例 | 队列满时策略 | 恢复方式 |
|---|---|---|---|
| P0 Safety / Control | Approval、Cancel、权限撤销 | 预留容量;无法接受时显式拒绝,不静默丢 | request id + 幂等重试 |
| P1 Authoritative | 生命周期、final Item、工具结果 | durable append;慢客户端断开或转 replay | cursor 重放 |
| P2 Progress | 阶段、任务计数、工具进度 | 按 subject 保留最新值或合并 | snapshot / 后续状态覆盖 |
| P3 Preview | token、stdout、diff delta | batch、coalesce、drop-oldest、标记 gap | final 权威对象校正 |
| P4 Cosmetic | 光标动画、typing signal | 直接丢弃 | 无需恢复 |
P0/P1 的“不能丢”不意味着必须无限堆在每个 WebSocket 的内存队列里。正确方式是先确保 durable source,再让过慢订阅者携 cursor 断开重连。无界 per-client queue 会把一个慢浏览器变成服务器 OOM。
[公开事实] Anthropic Managed Agents 把 preview delta 定义为 best-effort,并允许负载下 shedding delta,而完整 agent.message 仍作为权威记录。[12] Codex App Server 的 WebSocket 模式公开使用 bounded queue;request ingress 满时拒绝新请求并返回 overload,客户端需以指数退避和 jitter 重试。[2] 前者体现数据面降级,后者体现控制面/admission 限流。
10.3.3 Backpressure 不只是“队列变长”
每种生产者的阻塞能力不同:
- Model API 可以通过暂停读取施加有限 TCP backpressure,但过久可能触发上游超时;
- 本地命令的 stdout/stderr 必须持续 drain,否则可能阻塞被执行进程;
- durable writer 变慢时继续无限接收权威事实会耗尽内存,应降低并发或停止接纳新 Run;
- SSE 没有应用层双向 credit,可通过批量、心跳、断开与 cursor 恢复控制;
- WebSocket 可增加 credit/window update,但协议必须防止客户端虚报容量;
- Renderer 应以固定帧率或调度批次消费 preview,而不是让网络事件直接触发重绘。
系统还需要 noisy-neighbor 隔离。队列和 token budget 至少按 tenant、Run 或 connection 设配额,并使用公平调度;否则一个输出海量日志的 Run 会推迟另一个 Run 的 Approval。
10.3.4 事件优先级必须对应资源策略
优先级必须落实到资源预留和 overflow action,而不仅是 envelope 中的标签。例如可以为 P0/P1 保留独立队列容量,在 P3 拥塞时采用以下次序:
- 丢弃 P4;
- 延长 P3 coalescing 窗口,并优先丢最旧 preview;
- 将 P2 变为 latest-value 更新;
- 断开持续落后的订阅者,返回最后 durable cursor;
- durable writer 或 Run supervisor 饱和时,拒绝新 Run,而不是牺牲已接受 Run 的终态记录。
任何丢弃都应可观察:preview_dropped_total、每连接 queue depth、oldest event age、projection lag、durable append latency 和 forced resync 次数都应进入 metrics。客户端若发现 preview gap,应显示“实时输出已省略”或等待 final,不能悄悄拼接出看似连续的内容。
11. 顺序、状态与控制语义
并发 Agent 会同时产生事件、修改状态和接收控制命令。系统必须分别回答四个问题:事实按什么范围排序、当前状态如何构建、控制命令何时真正生效,以及重试后是否会重复执行副作用。把这四类语义都压进一个全局 sequence 或一个模糊的 ok 响应,无法支撑恢复与并发控制。
11.1 Ordering 与 Causality:事件流何时从序列变成图
并行 Agent 的全局到达顺序只是某个观察点看到的 interleaving:
Agent A: A1 ───── A2 ───────── A3Agent B: B1 ───────── B2Agent C: C1 ───────── C2
Observed: A1, B1, A2, C1, B2, A3, C2这个列表可以用于播放,却不能证明 A2 导致 C1。Multi-Agent 协议需要同时表达局部顺序和 happens-before 关系。
11.1.1 不要强求不存在的全局业务顺序
建议将顺序需求分层:
| 需求 | 机制 | 保证范围 |
|---|---|---|
| 同一实体状态转换 | subject.revision + CAS | 单 Run/Task/Item aggregate |
| 同一 stream 播放 | partition sequence / cursor | 单 durable partition |
| 同一 preview 拼接 | preview_seq / byte offset | 单 preview buffer |
| 跨 Agent 依赖 | causation_ids / task dependencies | 因果 DAG |
| 调试调用链 | trace/span | 被采样的观测链路 |
数据库可能为了存储方便给所有事件分配全局 offset,但该 offset 仍只是提交顺序。把它当成业务因果会导致错误,例如独立 Research Agent 的结果只是更早写入数据库,并不意味着 Code Agent 读取过它。
只有确实需要检测并发更新时,才考虑 Lamport clock、vector clock 或版本向量;它们会增加 envelope、合并和持久化成本。大多数 Agent UI 使用“单聚合 revision + durable playback cursor + 显式 cause edges”已经足够。
11.1.2 局部顺序与因果关系需要哪些字段
最小字段集合通常包括 run_id、agent_id、task_id、subject.id/revision、partition cursor、correlation_id、causation_ids 和 trace context。还应保留 occurred_at 与 recorded_at,但只用于人类时间线和延迟分析,不用于状态转换合法性。
因果关系应满足:
- cause 已存在或可由同批事务验证;
- 同一因果图不能形成环;
- join event 可以有多个 causes;
- retry attempt 关联原 operation,而不是伪装成原事件的重复投递;
- 补偿事件指向被补偿事实,并保留原事实,不做历史覆盖。
11.1.3 因果关系示例与共享资源冲突
{ "event_id": "evt_test_started", "type": "task.started", "scope": {"run_id": "run_1", "task_id": "integration_test"}, "causality": { "correlation_id": "corr_run_1", "causation_ids": [ "evt_code_artifact_verified", "evt_environment_ready" ] }}UI 可以按时间、Agent、Task 或因果树展示同一组事实。Scheduler 则应根据 task dependency state 决定是否启动测试,而不是扫描 UI 时间线。
还要注意:事件顺序不能自动解决共享资源冲突。两个 Agent 都基于 file revision 7 生成 patch,即使事件被排成先后,第二个 patch 仍可能覆盖第一个。文件提交必须检查 base hash/revision,并选择拒绝、rebase、merge 或人工决策。Ordering 记录冲突发生的顺序,Concurrency Control 才决定冲突如何处理。
11.2 Event、Command 与 State:事实和当前状态不是一回事
建议明确区分五个对象:
| 对象 | 含义 | 例子 |
|---|---|---|
| Command | 希望系统做什么 | run.cancel |
| Event | 系统确认发生了什么 | run.cancelled |
| Aggregate State | 权威执行当前状态 | run.status=cancelled |
| Projection | 面向查询的派生状态 | “过去 24h 失败 Run 列表” |
| Preview Overlay | 临时展示 | 尚未完成的 message delta |
状态可由事件折叠表示:
但这只在事件集合足够、reducer 版本兼容且外部状态已被引用或快照化时成立。文件系统、浏览器页面和第三方 API 的当前状态不会因为 Event Log 存在就自动可重建。
11.2.1 Client Reducer:从权威事实构建视图状态
客户端通常维护两层:
{ "authoritative": { "cursor": "cur_1024", "items": { "item_1": {"revision": 4, "status": "completed", "output": "..."} } }, "preview": { "preview_item_2": {"last_seq": 19, "incomplete": false, "text": "..."} }}Reducer 需要处理重复、旧 revision、final reconciliation 与 resync:
applyAuthoritative(event): reject wrong run/tenant if cursor already applied: return if event.subject.revision is stale: quarantine or ignore by contract if required base revision is missing: request snapshot/resync authoritative = pureReduce(authoritative, event) remove superseded preview persist cursor after state commit“persist cursor after state commit”很重要。若先保存 cursor 再更新本地 state,客户端崩溃后会跳过尚未应用的事件;本地数据库可将 state 与 cursor 放在同一事务中。纯内存 UI 则在重启后从服务器 snapshot 重新开始。
11.2.2 为什么 Delta 不能直接等同 State
Delta 必须声明操作语义。append bytes、replace range、JSON Patch 与“最新 progress value”不能共用模糊的 data 字段。建议为可应用 delta 指定:
operation: append | replace | patch | latest_valuebase_revision / byte_offsetpreview_seqcontent_encoding若 base revision 或 byte offset 不匹配,客户端不能继续盲拼,应把 overlay 标记 incomplete、请求局部 snapshot 或等待 authoritative final。Finalization 规则还要说明:完成事件是包含完整对象,还是仅声明某个 blob 已封存。二者都会影响重连时所需数据。
服务端的 aggregate state 负责授权和执行;客户端 reducer 只负责可视化。即使恶意客户端把本地 approval.status 改成 approved,也不能让 Runtime 越过等待状态。
11.3 Control Plane 与 Data Plane:一条连接不等于一种可靠性
Control Plane 与 Data Plane 是逻辑分层,不一定要求两条物理连接。它们可以复用 WebSocket,但需要不同消息类型、队列预算和确认语义。
Data Plane
Data Plane 承载 message delta、stdout、tool output、progress 和 authoritative notifications。其主要目标是吞吐与可恢复性:preview 允许降级,权威事实依靠 durable cursor 补发。
Control Plane
Control Plane 承载 turn/start、turn/steer、cancel、pause、resume、approval response 和 permission grant。它低频但会改变执行状态,因此需要身份、鉴权、幂等、并发前置条件和明确响应。
一个控制命令可采用:
{ "request_id": "req_77", "command": "approval.resolve", "target": {"run_id": "run_1", "approval_id": "apr_7"}, "expected_revision": 12, "idempotency_key": "user42-apr7-v12", "actor": {"type": "user", "id": "user_42"}, "deadline": "2026-07-13T08:00:00Z", "payload": {"decision": "approve", "action_digest": "sha256:..."}}响应层级必须区分:
| 响应 | 表示什么 | 不表示什么 |
|---|---|---|
received | Gateway 收到字节 | 命令已验证 |
accepted | 幂等与前置条件通过,已进入 durable control path | 目标动作已完成 |
rejected | 权限、版本、状态或容量不允许 | 可盲目重试 |
| 后续 authoritative event | 状态转换实际发生 | 所有外部清理一定成功,除非 payload 明示 |
例如 cancel.accepted 之后,Runtime 可能仍需终止进程、回收 sandbox 和等待子 Agent;只有 run.cancelled 才是终态。若 cancel 与 normal completion 并发到达,状态机通过 expected revision 决定哪一个先提交,失败的一方返回 conflict,而不是产生两个终态。
P0 control queue 应保留容量,避免 stdout 洪流造成 head-of-line blocking。即便复用单 WebSocket,也可以使用独立 logical channel 和调度权重;如果底层 transport 无法保证公平性,应考虑物理分离。
[公开事实] Codex App Server 的 JSON-RPC 结构使用 Request/Response 承担控制、Notification 承担持续事件,审批还可由服务端主动发起 Request。[2] 这是控制与数据语义分开的公开例子,并不意味着所有系统都必须采用 JSON-RPC。
11.4 Delivery Semantics:Exactly-Once 不是首要目标
“Exactly-once”必须说明发生在哪个边界。Apache Kafka 的 delivery semantics 文档区分 at-most-once、at-least-once 与 exactly-once,并要求结合 producer、consumer、故障和外部写入边界理解承诺。[18] 单个数据库事务可以原子地写 state 和 outbox,但跨 Runtime、broker、客户端以及第三方工具的端到端 exactly-once 通常无法仅靠消息协议保证。
[设计建议] 按 hop 定义语义:
| 路径 | 推荐语义 | 实现重点 |
|---|---|---|
| Runtime → local state/event store | 原子 append 或 state + outbox 原子提交 | transaction / expected revision |
| Event store → projector/client | at-least-once | cursor、event ID、幂等 reducer |
| Preview → client | best-effort / 可合并 | preview sequence、final reconciliation |
| Client command → Runtime | at-least-once request | idempotency key、durable command result |
| Runtime → 外部非幂等工具 | 取决于工具能力 | provider key、operation ledger、reconciliation |
Transactional outbox 解决的是“本地 commit 成功但 publish 丢失”:[17]
DB transaction: update aggregate where revision = expected insert authoritative_event insert outbox(event_id, unpublished)commit
publisher: publish event_id mark outbox publishedPublisher 在 publish 成功、标记失败时会再次发布,所以消费者仍必须幂等。若 broker 支持事务,也只是改变这个边界,不能自动让浏览器渲染和外部支付/部署 API exactly-once。
外部副作用应使用 operation ledger:相同 idempotency_key 首先查询已有 result;只有 owner attempt 可以调用;完成后写 result。AWS Builders’ Library 对幂等 API 的公开建议强调由 caller 提供唯一 request identifier 表达同一逻辑请求的重试,不能只根据参数相同推断重复。[19] 但仍存在最困难的崩溃窗口:外部调用已成功,本地 result 未写入。如果 provider 支持同一 idempotency key 查询,可安全恢复;如果不支持,只能通过外部资源查询、补偿或人工确认,不能自动重试。
客户端也要明确 ACK 的含义。WebSocket frame 已送达、事件已写入本地内存、reducer 已提交和 cursor 已持久化是四个不同边界。协议若只返回模糊 ok,故障恢复时无法知道该从哪里继续。
| 故障 | 重试后可能发生什么 | 防护 |
|---|---|---|
| Response 丢失,Command 实际已接受 | 同一命令再次到达 | idempotency_key 返回原 accepted/result |
| final event 发布两次 | UI 重复追加 | event ID/cursor + 幂等 reducer |
| projector 应用后、保存 cursor 前崩溃 | 事件再次应用 | state update 与 cursor 原子提交 |
| poison event 一直失败 | 分区停滞或被跳过 | quarantine + 告警 + 修复后重放;关键事实不可静默跳过 |
| 非幂等工具成功后 Runtime 崩溃 | 重试造成重复副作用 | provider idempotency、查询/补偿、outcome_unknown |
Dedup retention 必须覆盖最大合法重试/恢复窗口。服务端若只保存 5 分钟 command key,却允许客户端 24 小时后重试 Approval,就无法承诺幂等。相反,严格有序事件流可以通过 durable cursor 和 snapshot watermark 限制 event ID 去重集合的大小。
这是一种推荐工程策略,不代表所有厂商公开采用相同 delivery semantics。
12. 安全、可观测性与审计边界
Event 同时穿过执行协议、用户界面、历史记录和观测系统,因此它既是业务事实,也是敏感数据的复制载体。安全策略需要约束事件能否产生、谁能读取和回放;可观测性则用于解释实现是否健康。两者共享关联字段,但不能共享所有 payload、保留策略或可靠性承诺。
12.1 Event Security:事件本身也是敏感数据
Agent Event 可能包含 shell command、环境路径、工具参数、文件 Diff、API 响应、MCP Result、用户秘密和审批理由。Event Backbone 不是天然可信的内部日志;它扩大了敏感数据的复制面、保留期和订阅者数量。
12.1.1 Event Redaction:在进入通用 Backbone 前分级
最安全的秘密是从未进入通用 event payload 的秘密。OWASP Logging Cheat Sheet 明确要求访问令牌、认证密码、数据库连接串和加密密钥等不应直接记录,并要求对事件数据做格式校验、编码与清洗以防 log injection。[26] [设计建议] 使用分层处理:
- 生产者按结构化 schema 标记
secret、PII、credential 和 workspace-sensitive 字段; - 在进入通用 backbone 前将秘密替换为 opaque reference 或固定 redaction token;
- 若业务必须保留原值,写入独立加密 blob store,Event 只保存受 ACL 控制的
content_ref; - 针对 local UI、remote UI、telemetry、audit export 生成不同 redacted view;
- 对非结构化 stdout 再使用 pattern scanner 作为第二道防线,但不能把 regex 当成唯一秘密识别机制。
建议维护数据级别并使其随派生链传播:
| 级别 | 例子 | 典型策略 |
|---|---|---|
| Public | 通用状态名 | 可进入普通 telemetry |
| Workspace Sensitive | 路径、代码、Diff | 仅 Run 授权主体可读 |
| Secret | token、credential | 不进普通 Event;引用或掩码 |
| Restricted | 高风险审批、受监管数据 | 强审计、短保留、专用访问策略 |
Projection 和 aggregation 不能降低 classification,除非经过明确 redaction transform。否则原事件虽然被遮蔽,派生的 progress 文案或 error message 仍可能重新泄漏路径和 token。
12.1.2 权限事件必须对应服务端状态转换
approval.required 必须对应服务端 Runtime 的 waiting state。Approval response 至少校验用户权限、Run/tenant 归属、approval ID、subject revision、action digest、decision scope、expiry 和单次使用状态。
高风险操作可把 Approval 视为一个受限 capability:只允许在指定 Run、指定 action digest、指定资源和有效期内使用。命令参数在审批后改变时,旧 capability 立即失效。权限被管理员撤销后,已有长连接也必须在控制命令和敏感事件读取时重新授权,而不是只在 WebSocket 建立时检查一次。
12.1.3 Replay、Snapshot 与 Artifact 的读取权限
能读取 live stream 不应自动意味着能下载完整历史;能看摘要也不一定能看原始命令输出。订阅、catch-up replay、snapshot、Artifact blob 和 audit export 都需要资源级授权,并在每次恢复时重新验证。
还需要处理以下边界:
- tenant/run ID 不能只由客户端传入后直接拼查询条件,应由授权上下文绑定;
- cursor 应签名或在服务端查表,防止被篡改为其他 Run 的位置;
- 撤销访问后,历史缓存、预签名 blob URL 和本地离线副本要有明确失效策略;
- retention/deletion 必须覆盖 Event Log、snapshot、projection、outbox、trace、backup 和外置 blob;
- 审计记录本身包含 actor、命令和资源,也属于敏感数据。
最后,事件 payload 是不可信输入。工具输出可能包含 ANSI 控制序列、恶意 Markdown/HTML、伪造链接或超长单行;UI 应进行 escaping、长度限制和安全 link handling。显示 command.stdout 绝不等于允许它调用终端控制接口。
12.2 Event、Trace、Metric 与 Log:关联但不混用
Agent 可观测性并不等于把 Event Log 接到日志平台。OpenTelemetry 将 traces、metrics、logs 等作为不同 observability signals;W3C Trace Context 则只标准化跨服务传播 trace context 的 HTTP headers 与格式。[25] 因此 trace_id 能关联调用链,却不能天然充当业务事件身份或恢复游标。不同数据回答不同问题,也有不同基数、采样和保留策略。
| 数据 | 回答的问题 | 结构与保留 | 能否作为业务恢复依据 |
|---|---|---|---|
| Metric | 总量、分位数、错误率、趋势如何 | 聚合时间序列;低标签基数 | 否 |
| Log | 某组件当时记录了什么诊断信息 | 半结构化;可采样、可轮转 | 通常否 |
| Event | 哪个领域事实发生了 | typed、可关联;按耐久度分层 | 仅 authoritative event 可以 |
| Trace | 一次调用链经过哪里、为何慢 | span tree/DAG;通常采样 | 否 |
| Transcript / Timeline | 用户可恢复地看到哪些会话与工作记录 | 经过筛选的 durable narrative | 部分 |
| Audit Record | 谁在何时以何权限执行/批准了什么 | 防篡改、严格访问和保留 | 用于问责,不直接驱动执行 |
Metric
Metric 适合 token count、run failure rate、tool latency、queue saturation 和 dropped preview。不要把 run_id、event_id、文件路径等高基数字段作为普通 label;需要定位具体样本时使用 trace exemplar 或日志链接。
Event
Event 回答 tool.execution.completed、approval.denied 等离散事实。业务权威事件的保留策略由恢复和审计要求决定;observability-only event 可以采样。不能因为 telemetry pipeline 丢包就让 Run 状态缺少终态。
Trace
Trace 表达 Run 下的 Model、Tool、Approval Wait 等 span 关系。Span 可以因采样而不存在,业务事件却仍必须成立;trace_id 是关联键,不是 Event Store cursor。跨 subagent、queue 和 tool boundary 应显式传播 trace context,同时用 span link 表达 fan-out/fan-in,而不是把所有工作强塞进单父树。
Transcript / Durable Timeline
Transcript 是为人类阅读和恢复筛选后的记录,不等于所有 raw event,也不等于调试 log。它可能包含完整 assistant message、工具摘要、Artifact、审批和终态,却省略 token delta、heartbeat 与内部重试噪音。
[社区研究,非官方事实] Claude Code 社区仓库的可观测性分析提出 metrics、events、traces 与 transcript 分层,并强调离散事件用 ID 关联、因果层用 span、聚合层避免高基数 ID。[15] 这对统一参考架构有启发,但不应被描述为 Anthropic 官方内部规范。
生产系统应定义端到端 SLI,而不只测模型 TTFT:
| SLI | 起点与终点 | 诊断价值 |
|---|---|---|
| Preview latency | producer 产生 delta → client reducer 接收 | 实时体感 |
| Commit latency | 结果形成 → authoritative append 成功 | 正确性路径压力 |
| Projection lag | durable recorded_at → view cursor applied | UI/索引新鲜度 |
| Resume recovery time | resume accepted → sync.completed | 断线恢复能力 |
| Control acknowledgement | command sent → accepted/rejected | 可干预性 |
| Approval wait | required → resolved/expired | 人类瓶颈,不应计入模型 latency |
| Terminal completeness | started Run 中具有唯一终态的比例 | 状态机健康度 |
Event、Metric、Trace、Log 的时间点定义必须一致,否则“工具延迟”可能有人从 proposal 算起,有人从 execution started 算起,聚合结果无法比较。
Event Stream 是执行协议的一部分;Observability 是观察该协议和实现是否健康的另一套系统。两者关联,但不能互相替代。
13. 生产级 Agent Event Streaming 的设计原则
前面的机制可以收束为十二条工程不变量。它们不是相互独立的口号,而是从实体建模、事实提交、状态恢复到用户控制逐层建立可靠性。
| 原则 | 工程含义 |
|---|---|
| 1. 先定义执行实体,再定义事件 | 明确 Thread、Run、Turn、Task、Item、Agent、Attempt、Approval 与 Artifact 的身份、所有权和终态;没有稳定实体,事件只会退化为带 JSON 的日志。 |
| 2. Token 是 Event 的子类型 | Token delta 只属于 Model 数据面的 preview;工具、文件、审批、任务和 Artifact 必须有自己的结构化生命周期。 |
| 3. Connection 与 Execution 分离 | 连接、订阅、worker lease 和 Run 是不同对象;断线默认只失去观察窗口,pause/cancel 由显式命令和策略决定。 |
| 4. Preview 与 Authoritative 分离 | Preview 可丢、可合并;Authoritative 必须有原子提交边界、稳定 cursor 和重放路径,Final 能独立替换 preview。 |
| 5. 事件协议与 Transport 分离 | 先定义 event/command 语义、版本、顺序、可靠性和安全级别,再选择 SSE、WebSocket、JSONL 或 IPC。 |
| 6. Raw Event 与 User Progress 分离 | Transport batching、stream coalescing、semantic progress 和 presentation grouping 分层实现,聚合不能掩盖失败、安全决策或数据截断。 |
| 7. 状态通过 Reducer / Projection 构建 | Reducer 应确定、幂等、可版本化;权威 state 与 cursor 原子推进,preview 只是 overlay,执行副作用不能藏在 UI reducer 中。 |
| 8. 高频流必须有 Backpressure 策略 | 每一跳使用 bounded queue、watermark 和明确 overflow action;按 P0-P4 定义 QoS,慢客户端通过 durable cursor 恢复。 |
| 9. Multi-Agent 需要因果关系 | 局部 revision 负责状态顺序,cursor 负责播放,causation_ids 与 task dependencies 负责因果;全局 offset 不能解决共享资源冲突。 |
| 10. 恢复依赖 Durable State | Resume 必须处理 snapshot、high-watermark、catch-up 与 live handoff;worker lease 使用 epoch/fencing 阻止旧执行者重复提交。 |
| 11. 控制面和数据面采用不同可靠性 | Approval、Cancel 和权限变更需要身份、鉴权、expected revision、幂等结果与预留容量;token delta 可以在拥塞时退化。 |
| 12. 目标是 Observable Autonomy | Streaming 的终点不是动画,而是让 Agent 在获得更大自主权时仍然可观察、可暂停、可恢复、可授权、可审计和可验证。 |
最后可以用六个故障演练检验设计是否成立:客户端在 final 前断线、worker 在外部副作用后崩溃、同一 Approval 重试两次、慢客户端耗尽队列、旧客户端遇到新关键事件、两个 Agent 并发修改同一文件。如果系统对这些场景只有“通常不会发生”的回答,那么它仍然只是带事件外观的聊天流,而不是生产级 Agent Event Streaming。
第五部分:五家主流 Agent 产品的技术实现
本部分不比较语言栈,而比较它们如何把“模型输出”升级为“Agent 执行可观察性”。
需要特别强调:不同产品公开程度差异很大。Codex App Server、Anthropic Managed Agents、Grok Build CLI/ACP 的协议细节公开较多;Cursor 公开了 Run、stream、reconnect 等产品语义,但未公开全部内部事件 taxonomy;Antigravity 公开了多 Agent 与 Artifact 产品模型,但其完整底层传输协议并未充分公开;Claude Code 的部分细节来自官方 Claude API,部分实现观察来自明确标注为非官方的社区研究仓库。
14. Claude Code:从 Model Stream 到 Agent Loop 的连续执行体验
14.1 官方可确认的底层:Claude Messages 的结构化 SSE
Anthropic Messages API 的流式输出不是单一字符串,而是结构化 SSE 事件:
message_startcontent_block_startcontent_block_deltacontent_block_stopmessage_deltamessage_stopcontent_block_delta 又可以携带不同 delta 类型:
text_deltainput_json_deltathinking_deltasignature_delta因此在 Claude API 层,已经完成从:
Bare Token Stream到:
Typed Content Event Stream的升级。[3]
工具调用尤其值得关注。官方示例中,一个 tool_use block 可以先通过 content_block_start 建立,然后由多个 input_json_delta 持续补全参数,最后 content_block_stop 结束。[3]
这意味着客户端可以明确知道:
现在不是普通文本而是在生成工具输入14.2 Fine-Grained Tool Streaming:优化的不只是首 Token,而是首个工具参数片段
Anthropic 官方的 fine-grained tool streaming 允许工具输入在生成过程中直接流向客户端,不经过服务端完整 JSON buffering 与 validation。[11]
传统方式:
Model Generates Large Tool Input ↓Server Buffers Entire JSON ↓Validate ↓Client Receives细粒度方式:
Model Generates ├── fragment 1 ──→ Client ├── fragment 2 ──→ Client └── fragment N ──→ Client优化的是:
Time To First Tool Input Fragment而不仅是:
Time To First Text Token代价同样明确:客户端可能收到 partial / invalid JSON,必须使用累积器、容错解析和最终边界确认。
这是一条重要规律:
更低延迟往往意味着把完整性检查从“发送前”推迟到“消费与完成边界”。
14.3 Claude Code 的 Agent Loop:流式输出不是只为显示
how-claude-code-works 仓库对约 2026 年 3 月底源码快照的独立分析认为,Claude Code 的一次交互大致经历:
用户输入 ↓上下文组装 ↓API 流式调用 ↓解析 assistant / tool_use ↓工具执行 ↓结果注入 ↓继续下一轮或终止其 query() 被描述为异步生成器,向上层持续 yield assistant、progress、stream event、tool result 等不同消息类型;上层再持续处理与渲染。[13]
这里最重要的不是 TypeScript 语法,而是一个架构事实:
Agent Loop 本身可以成为一个事件生产器。
Agent Loop │ ├── Model Event ├── Progress Event ├── Tool Result ├── Recovery Signal └── Final ResultUI 不必等 Agent Loop 返回最终值,才能知道发生了什么。
14.4 StreamingToolExecutor:把模型生成窗口变成工具执行窗口
该社区仓库最值得关注的观察之一,是其所称的 StreamingToolExecutor。
仓库分析称:当流式响应中一个完整 tool_use block 已经解析完成时,工具可以立即进入执行队列,而不必等待整个模型响应完全结束;被标记为 concurrency-safe 的工具可以并行,非并发安全工具则独占执行。[13]
时间线可以抽象为:
串行:[------ Model Response ------][Tool A][Tool B]
重叠:[------ Model Response ------] [Tool A] [Tool B]假设:
模型流式响应:12 秒文件读取:0.2 秒代码搜索:0.8 秒如果工具可以在模型仍输出后续内容时启动,那么工具的部分延迟被隐藏在模型生成窗口内。
这可以定义为:
因为执行发生了 overlap。
但需要严格区分两件事:
- Anthropic 官方 fine-grained tool streaming:工具参数片段更早到达客户端;
- 社区仓库描述的 Claude Code StreamingToolExecutor:在完整 tool_use block 已解析后,工具尽早调度,并与后续模型输出重叠。
二者都在减少等待,但优化层次不同。
14.5 从 SSE 到终端渲染:全链路低延迟需要每一层都增量化
该社区仓库的用户体验章节将 Claude Code 的流式链路描述为:
API SSE ↓callModel() ↓query() ↓QueryEngine ↓REPL ↓Terminal Renderer并指出每层通过异步迭代 / yield 把增量继续向下传递。[14]
这揭示一个非常重要的工程原则:
上游支持 Streaming,不代表用户就一定能得到低延迟 Streaming。
如果任一中间层做:
collect all ↓return final整个链路就重新变成批处理。
真正的全链路低延迟要求:
Model API incrementalRuntime incrementalTool progress incrementalState update incrementalRenderer incremental14.6 Streaming Markdown 与增量渲染:网络快,不代表 UI 快
社区仓库还分析了 StreamingMarkdown:稳定前缀不反复重新解析,只重新处理仍在增长的不稳定后缀;终端渲染器通过 diff 更新变化区域,避免整屏重绘。[14]
这说明 Agent Streaming 的最后一公里同样重要:
Network Latency+Parse Latency+State Update Latency+Render Latency如果每个 Token 都触发全量 Markdown Parse 与全屏重绘,那么服务端再快,客户端仍然卡顿。
因此“流畅”来自:
Incremental Transport+Incremental Parsing+Incremental State+Incremental Rendering14.7 Claude Code 的核心优势:Observable Autonomy
社区分析用“可观察的自主性”概括 Claude Code 的 UX:Agent 可以自主行动,但工具调用、进度、结果与权限状态持续可见。[14]
从架构角度看,这比“逐字输出”更本质。
Claude Code 的流式体验可被概括为:
Typed Model Stream ↓Agent Loop Stream ↓Tool Execution Progress ↓Incremental Terminal Projection其优势不是某一个协议,而是:
模型、工具与 UI 之间没有被一个“等全部完成”的同步边界切断。
15. OpenAI Codex:Agent Runtime Event Architecture 的公开范例
如果要选择一个公开资料最适合研究“Agent Event Protocol”的产品,Codex App Server 是当前最典型的案例之一。
15.1 Runtime 不是模型代理,而是持续运行的 Agent Server
Codex App Server 支持:
stdio + JSONLWebSocketUnix Socket并使用类似 JSON-RPC 2.0 的双向消息模型;需要注意,官方当前把 WebSocket transport 标记为 experimental / unsupported,因此本文将其作为公开架构能力分析,而不把它描述为已经稳定承诺的生产接口。[2]
这意味着客户端面对的不是:
POST /model而是一个:
Stateful Agent Runtime Protocol15.2 Thread → Turn → Item:把执行过程结构化
Codex 的重要抽象是:
Thread └── Turn └── ItemsItem 可以对应不同工作单元。
公开事件包括:
turn/starteditem/starteditem/agentMessage/deltaitem/plan/deltaitem/reasoning/summaryTextDeltaitem/commandExecution/outputDeltaitem/completed这使执行不再是一条不可解释的文本流,而是一组有身份、有生命周期的工作单元。[2]
15.3 item/started → delta → item/completed
这是非常值得学习的统一生命周期:
Item Starts ↓Incremental Updates ↓Item Completes例如命令:
item/started(commandExecution) ↓item/commandExecution/outputDelta ↓item/completed(commandExecution)客户端不需要猜:
这段 stdout 属于哪个命令?因为 delta 关联具体 Item ID。
15.4 item/completed 作为权威状态
Codex 官方明确说明:item/completed 发送最终 Item,应视为 authoritative state。[2]
这非常成熟。
因为流式 UI 最常见的错误是:
把 delta 累积结果当作最终真相但:
- delta 可能经过不同格式转换;
- plan final 甚至可能不完全等于简单拼接的 deltas;
- 中途可能取消或失败。
因此:
Delta = display progressCompleted Item = final state15.5 Approval 是协议,不是前端特效
Codex 的 command execution 与 file change 可以触发 server-initiated approval request,客户端返回 accept / decline 等决策,Runtime 再继续或结束 Item。[2]
这条链路意味着:
Approval本质上是:
Distributed State Transition而不是:
一个 Modal这对企业 Agent 极其重要。
15.6 turn/steer:流式系统也必须支持中途修正
Codex App Server 支持向当前 in-flight turn 追加输入。[2]
这表明现代 Agent 交互正在从:
等待 Agent 完成然后再发下一条消息升级为:
Agent 正在执行用户中途 steerRuntime 调整后续执行当任务持续几十分钟时,这种能力的重要性会快速上升。
15.7 Bounded Queue:公开展示 Backpressure 意识
Codex App Server 的 WebSocket 模式明确使用 bounded queues;当 ingress 满时拒绝新请求,要求客户端指数退避并加入 jitter。[2]
这说明:
真正的生产级 Streaming 必须定义拥塞时怎么办,而不是假设消费者永远跟得上。
15.8 Codex 的核心架构思想
可以概括为:
UI ↓Agent Runtime Protocol ↓Thread / Turn / Item ↓Model / Tool / Command / File / Approval最重要的不是 WebSocket 或 JSONL,而是:
客户端订阅的是 Runtime 的工作单元生命周期,而不是直接订阅模型。
16. Cursor:从 Response Stream 升级为 Durable Agent Run
16.1 Run 是一等对象
Cursor SDK 当前公开:
const run = await agent.send(...)
for await (const event of run.stream()) { // consume events}这段 API 的意义不只是语法优雅。
它表明抽象已经从:
send → response升级为:
send → run → event stream[4]
16.2 Durable State 与 Session Management 是 Runtime 的核心能力
Cursor 官方在介绍 SDK 时直接把:
secure sandboxingdurable statesession managementenvironment setupcontext management列为构建生产级 Coding Agent 所需的重要基础设施。[4]
这说明“流式输出”不能脱离 Runtime 生命周期单独理解。
16.3 网络掉线,Agent 继续
Cursor 官方明确说明:
laptop sleepsor network drops ↓agent keeps going ↓client reconnects later[4]
这正是:
Connection ≠ Execution的直接工业实现。
16.4 Notion 集成:SSE + last event resume
Cursor 与 Notion 的官方案例进一步披露:每个 follow-up 开启新的 run,通过 SSE 流式展示执行过程;连接中断时可以从 last event 恢复。[4]
这使 Cursor 非常适合代表:
Durable Agent Event Streaming而不只是:
SSE Token Streaming16.5 Cursor 的核心架构思想
Agent ↓Persistent Run ↓Event Stream ↓Reconnect / Resume其关键价值是:
把长时执行从前端连接生命周期中解放出来。
17. Grok Build:Protocol-Based Agent Streaming
17.1 Headless streaming-json
Grok Build 官方支持:
plainjsonstreaming-json其中 streaming-json 是 newline-delimited JSON events,事件到达时增量输出。[5]
这对脚本、CI 与 Agent-to-Agent 集成非常重要,因为机器不需要从人类文本里猜状态。
17.2 ACP:session/prompt 与 session/update
Grok Build 的 ACP 模式通过:
JSON-RPC over stdin/stdout运行。
调用:
session/prompt返回完成元数据,而 assistant text 通过:
session/updatechunks 持续到达。[5]
这再次体现:
Request Completion Metadata ≠Live Incremental Updates17.3 Session 是持久对象
Grok Build Headless 模式支持:
--session-id--resume--continue并把 session 存储到本地。[5]
这说明它的执行模型已经具有:
Session Identity+Resume+Streaming Updates17.4 Agent Dashboard:从单流走向多 Session 聚合
xAI 2026 年 6 月发布的 Agent Dashboard 可以:
- 同屏管理多个 Coding Session;
- 并行运行;
- 按 working / waiting / idle 状态组织;
- 把等待用户输入的 Session 提到前面;
- 汇总 Subagent。[10]
这说明多 Agent 产品的 UI 核心已经不是:
一条聊天流而是:
多个执行流 ↓状态聚合 ↓注意力调度17.5 Grok Build 的核心架构思想
Session ↓Streaming JSON / ACP Update ↓Machine-Readable Integration ↓Multi-Session Dashboard它强调的是:
Agent 不只是给人看,也要被 IDE、脚本与其他系统消费。
18. Google Antigravity:从 Event Stream 走向 Progress / Artifact Stream
18.1 公开资料能确认什么
Google 官方将 Antigravity 描述为 Agentic Development Platform。2026 年的 Antigravity 2.0 Codelab 进一步将其描述为用于启动、监控和编排多个本地 Agent 的 central command center。[6]
需要严谨说明:
当前公开资料足以确认它的 Agent、Project、Artifact、多 Agent 管理等产品模型,但不足以可靠断言其完整内部 Wire Protocol 究竟统一采用 SSE、WebSocket、私有 RPC 还是多种传输组合。
因此,不应从 UI 表象反推内部协议。
18.2 Artifact:把“工作过程”转成“可验证工作产物”
Antigravity 官方 Codelab 明确列出:
Implementation PlanTask ListsWalkthroughScreenshots并把 Artifact 定位为 Agent 在规划和执行过程中沟通工作、获取反馈和建立信任的机制。Code Diff 同样支持审阅和评论,但 Codelab 明确说明它“technically not an artifact”。[6]
这代表一种比 Token Streaming 更高层的 UX:
Token tells you what the model says.Artifact shows you what the agent has produced.18.3 Progress Stream 的信息密度
长任务中:
Thinking...Thinking...Thinking...的信息密度非常低。
而:
Implementation Plan Ready3 / 7 Tasks CompletedUI Screenshot CapturedTests PassedWalkthrough Ready信息密度高得多。
因此,未来的高级 Agent UI 很可能从:
Token-centric转向:
Task-centricArtifact-centricVerification-centric18.4 Antigravity 的核心价值
它代表了 Agent Streaming 的上层演进:
Raw Event ↓Progress Model ↓Artifact ↓Verification也就是说,真正成熟的产品不要求用户理解底层每一个 Event,而是把复杂执行投影为:
可理解的进度+可检查的产物+可验证的结果第六部分:五家产品的横向比较
19. 横向比较:系统表达了什么,而不是只看使用什么协议
| 维度 | Claude Code | Codex | Cursor | Grok Build | Antigravity |
|---|---|---|---|---|---|
| Typed Model Stream | 官方 Claude API 明确 | Responses API 明确 | 底层细节未完整公开 | xAI API/CLI 有结构化流 | 底层协议未充分公开 |
| Tool Streaming / Tool Events | 官方支持 tool input delta;社区研究观察工具执行流 | item/*、tool progress、approval 明确 | Run events 公开,taxonomy 未完全公开 | streaming JSON / ACP update | 产品层可观察工具与任务执行 |
| Runtime Execution Object | Claude Code Session / Agent Loop | Thread / Turn / Item | Agent / Run | Session | Project / Agent / Task / Artifact |
| Durable Run | 新版后台能力存在社区研究;需区分非官方分析 | Runtime/Thread 持续存在 | 官方明确 reconnect | Session resume;Dashboard 提供多 Session 聚合 | 官方明确多 Agent 管理与长期任务能力 |
| Reconnect / Resume | 具体协议公开有限 | 取决于 client/runtime transport | 官方明确 last event resume | --resume / --continue | 产品能力存在,底层流恢复协议未充分公开 |
| Multi-Agent Visibility | 社区研究记录 subagent / teams / background fleet | Subagents 与相关 runtime 能力持续演进 | Cloud Agents / SDK | Agent Dashboard | 官方核心定位之一 |
| Artifact / High-Level Progress | Tool UI、Diff、结果 | Item / Plan / File Change | PR、Demo、Screenshot | Session state / output | 官方核心能力 |
| 公开协议透明度 | API 高、Claude Code 内部低 | 很高 | 中等 | 较高 | 产品高、内部协议低 |
这张表最重要的不是打勾数量,而是看共同方向:
Text ↓Typed Event ↓Execution Object ↓Durable Run ↓Aggregated Progress ↓Artifact / Verification20. 五家产品的共同收敛方向
横向比较的价值不在于重复六个独立结论,而在于观察这些产品如何沿同一条能力链收敛:从结构化执行对象,到脱离连接的身份,再到面向用户的高层进度与控制。
| 共同方向 | 公开表现 | 架构含义 |
|---|---|---|
| 文本不再是唯一输出 | 工具、命令、文件、审批和任务成为一等对象 | Token 只是 Event 的一种高频子类型 |
| Runtime 与 UI 解耦 | Codex App Server、Cursor SDK、Grok ACP 可服务 CLI、IDE、Web 或外部产品 | Agent Core 发布语义事件,客户端负责 Projection |
| 长任务拥有 Durable Identity | Thread、Run、Session、Project 名称不同但都提供稳定身份 | 任务生命周期不能依附单条连接 |
| Progress 高于裸 Token | 用户首先关心开始、阶段、阻塞、输入请求和终态 | Raw Event 需要被聚合为可行动进度 |
| 人类干预进入协议 | Approval、Steer、Input Required、Pause / Resume | Human-in-the-loop 是状态转换,不是后补 UI 按钮 |
| 前端成为事件解释器 | 同一事实可投影为 CLI 时间线、IDE 卡片或 Web Dashboard | UI 消费状态模型,而不是直接打印无限事件流 |
第七部分:为什么这些 Agent 看起来这么快
21. 感知速度的三条优化链
21.1 首信号:TTFT、TTFE 与 TTA
TTFT:尽早显示模型内容
这是基础,但不是全部。
TTFE:尽早证明“任务已经活了”
即使模型还没有可展示文本:
run.started也能立即建立反馈。
因此:
TTFE may be earlier than TTFTTTA:尽早开始真实动作
真正有价值的是:
Search StartedRead StartedTool StartedCommand Started而不是更早输出一句:
“我现在开始分析。”优秀 Agent 会尽量缩短:
Request → First Useful Action21.2 执行重叠:Progressive Visibility、Overlap 与 Parallelism
Progressive Visibility:减少黑盒等待
心理等待时间与物理耗时并不完全相同。
45 秒无反馈和:
0s 任务启动2s 已扫描仓库8s 正在修改认证模块18s 开始运行测试32s 发现失败并修复45s 完成物理耗时相同,体验完全不同。
但必须指出:
Progressive Visibility 不能变成虚假进度条。
最可信的进度来自真实 Runtime Event,而不是前端随机动画。
Execution Overlap:把等待隐藏进等待
总延迟不一定等于各阶段简单相加。
串行:
重叠后:
Claude Code 社区研究中描述的流式工具调度就是这种思路的一个实现观察。[13]
Parallelism:多个独立动作同时推进
例如:
Read ARead BSearch C如果都是只读且相互独立,可以:
parallel但并行必须建立在:
Dependency AnalysisSide-Effect ClassificationConcurrency Safety之上。
盲目并行可能导致:
- 文件冲突;
- 隐式依赖失败;
- 输出乱序;
- 更高成本。
21.3 执行位置与恢复:Local、Durable 与 Incremental Rendering
Local Execution:缩短远程往返
Coding Agent 的很多能力:
readgrepgitbashfile diff可以在本地 Runtime 执行。
这减少:
Remote Round Trip但本地执行并不自动等于低延迟。仍需要:
- 工具调度;
- 权限检查;
- 沙箱;
- 输出 streaming;
- 渲染优化。
Durable Execution:避免因断线重复工作
如果网络断开后:
任务全部重跑那么恢复成本巨大。
Durable Run 可以:
continue execution+reconnect+reconcile state它优化的是:
Failure Recovery Latency而不只是正常路径延迟。
Incremental Rendering:最后一公里同样决定“快不快”
服务端每 20ms 发一个 delta,如果客户端每次都:
Parse Entire MarkdownRebuild Entire DOMRepaint Entire View用户仍会觉得卡。
所以:
Fast Model×Slow Renderer=Slow Product流式系统需要端到端优化。
22. 一个更完整的 Agent Latency 模型
可以把端到端延迟近似拆成:
其中最影响主观体验的往往不是总时长本身,而是:
最长无反馈间隔因此可以增加指标:
Maximum Silent Interval即:
用户最长多久看不到任何有意义的新进展?
对于长时 Agent,这可能比平均 Token 速度更有产品价值。
推荐关注:
TTFE 首个有意义事件TTFT 首个文本TTA 首个真实动作Event Freshness 事件发生到用户可见的延迟Max Silent Interval 最长静默区间Resume Time 断线恢复时间Completion Latency 最终完成时间第八部分:技术选型——SSE 还是 WebSocket
23. Transport Selection:从交互模型和恢复语义出发
正确顺序是:
1. 先定义交互模型2. 再定义事件协议3. 最后选择 Transport23.1 SSE:以服务端事件订阅为主
适合:
Server → Client 为主例如:
- 文本流;
- Agent Progress;
- 工具结果;
- 后台任务通知;
- Web 产品中的 Run Stream。
优势:
- HTTP 语义简单;
- 浏览器支持;
- 自动重连;
- Event ID 机制自然。
不足:
- 原生方向是单向;
- 客户端控制通常需要额外 HTTP API;
- 高频真正双向交互不如 WebSocket 自然。
一个非常实用的组合是:
Command API +SSE Event Stream例如:
POST /runsPOST /runs/{id}/cancelPOST /runs/{id}/approveGET /runs/{id}/events (SSE)23.2 WebSocket:需要持续双向控制
适合:
持续双向、高频控制例如:
- Remote TUI;
- Terminal stdin;
- 实时 steer;
- Voice Agent;
- 高频协同。
但必须自行设计:
AuthReconnectResumeSequenceBackpressureHeartbeatReplayWebSocket 不会自动替你解决这些问题。
23.3 stdio / JSONL / IPC:本地 Runtime 集成
适合:
Local CLIIDE PluginDesktop HostAgent Sidecar优势:
- 本地通信简单;
- 易调试;
- 易与进程生命周期结合;
- 不必开放网络端口。
Codex 与 Grok Build 都公开提供这类形态。[2][5]
23.4 Transport Adapter:让协议语义独立于通道
Agent Event Protocol │ ├── SSE Adapter ├── WebSocket Adapter ├── JSONL Adapter ├── IPC Adapter └── Unix Socket Adapter核心原则:
先设计 Event Protocol,再选择 Transport。
如果业务语义直接绑定某一种传输:
“只有 WebSocket message 才代表 tool started”那么未来从 IDE 扩展到 Web、Remote TUI、SDK 时会非常痛苦。
第九部分:结论——Agent 的未来不是 Token Stream,而是 Execution Stream
24. 流式架构真正发生了什么变化
第一代:
Token Streaming解决:
不要等完整答案第二代:
Typed Model Event Streaming解决:
不同模型输出拥有机器可理解类型第三代:
Agent Runtime Event Streaming解决:
工具、命令、文件、审批都进入统一执行语义第四代:
Durable Agent Event Streaming解决:
连接断开不等于任务死亡第五代:
Multi-Agent Execution / Progress / Artifact Stream解决:
复杂并行执行如何被人理解、验证和管理最终演进图:
Token Streaming ↓Typed Model Event Streaming ↓Agent Runtime Event Streaming ↓Durable Agent Event Streaming ↓Multi-Agent Execution Stream ↓Progress / Artifact / Verification Stream25. 最值得记住的三个结论
结论一:Token 不再是最高层抽象
在现代 Agent 中:
Token只是:
Execution Event 的一个子类型结论二:真正先进的流式架构是“可观察的执行系统”
优秀 Agent 的实时体验来自:
Model Streaming+Tool Streaming+Runtime Events+Durable State+Progress Projection+Human Control而不是某一个 SSE 或 WebSocket 开关。
结论三:下一代竞争点是 Observable Autonomy
Agent 越自主,执行越长,越不可能让用户盯着每一个 Token。
真正重要的是:
它是否开始了?它现在在做什么?为什么做这件事?哪里阻塞了?什么时候需要我?断线后是否继续?它最终产出了什么?我如何验证它真的完成了?因此,Agent 产品的未来很可能不是:
谁吐 Token 更快而是:
谁能更早开始真实行动谁能更连续地证明任务正在推进谁能把复杂执行压缩成可信的 Progress谁能在网络与进程故障后继续恢复谁能让多 Agent 的并发工作仍然可理解谁能用 Artifact 与 Verification 缩短信任距离最终可以把全文压缩成一句话:
Token Streaming 解决的是“不要让用户等待完整答案”;Agent Event Streaming 解决的是“不要让用户面对一个不可观察、不可恢复、不可干预的执行黑盒”。
而现代 Agent 架构真正追求的,是:
让自主执行变得可观察,让实时反馈不牺牲最终正确性,让长时任务脱离连接生命周期,让复杂多 Agent 工作最终被投影为人类可以理解和验证的进度与成果。
附录 A:本文提出的统一分析模型
A. 统一分析模型速查
本附录把正文压缩为四个互补视图:四层模型回答职责如何分层,耐久度模型回答哪些事件可以丢失,逻辑平面回答数据与控制如何交互,时间指标则回答实时体验如何测量。
A.1 四层模型
┌──────────────────────────────────────┐│ Presentation / Projection ││ Timeline / Progress / Artifact / UI │├──────────────────────────────────────┤│ Agent Semantic Event Protocol ││ Run / Turn / Item / Tool / Approval │├──────────────────────────────────────┤│ Agent Runtime ││ State / Scheduler / Tool / Subagent │├──────────────────────────────────────┤│ Transport ││ SSE / WS / JSONL / IPC / Socket │└──────────────────────────────────────┘A.2 三类事件耐久度
Transient Preview ↓Durable Progress ↓Authoritative State EventA.3 两个逻辑平面
Control Plane - start - steer - cancel - approve - resume
Data Plane - delta - progress - stdout - tool outputA.4 三类核心时间指标
TTFE = Time To First EventTTFT = Time To First TokenTTA = Time To First Action再加:
Max Silent IntervalResume TimeEvent FreshnessCompletion Latency附录 B:证据等级说明
B. 证据等级与使用边界
本文严格区分三类材料。证据等级不是来源质量排行榜,而是限制论证范围:官方材料用于确认公开能力,社区研究只用于解释可观察实现,统一架构则明确属于本文综合,不能反推为任何厂商的内部事实。
B.1 官方公开事实
来自:
- OpenAI / Codex 官方文档;
- Anthropic / Claude Platform 官方文档;
- Cursor 官方博客与 SDK 资料;
- xAI 官方文档与产品公告;
- Google 官方博客与 Codelab;
- WHATWG、IETF/RFC、JSON-RPC、CloudEvents、ACP 与 W3C 规范;
- Microsoft、AWS、Apache Kafka、OpenTelemetry、Reactive Streams 与 OWASP 的公开工程资料。
B.2 社区实现观察
Windy3f3f3f3f/how-claude-code-works 仓库明确声明其内容是独立教育性架构分析,不代表 Anthropic 官方设计,也不保证与 Claude Code 最新内部实现完全一致。本文仅将其用于补充 Claude Code 的 Agent Loop、StreamingToolExecutor、终端渲染和可观测性等实现观察,并在相关段落明确标注“社区仓库分析”。
B.3 本文统一架构抽象
包括:
Event BackboneEvent EnvelopeDurability TiersControl Plane / Data PlaneEvent QoSCausality ModelReference Event Taxonomy这些是基于多个公开工业系统与通用分布式系统资料提炼出的架构模型,不应被误解为某一家厂商完整内部实现。通用资料能够证明某种机制的工程语义与已知边界,但不能证明五家产品内部一定采用相同组件或实现。
参考资料
以下资料均为本文写作时实际参考,资料核验截止 2026-07-13。
[1] OpenAI, Streaming API responses / Responses API semantic events
https://developers.openai.com/api/docs/guides/streaming-responses
[2] OpenAI, Codex App Server
https://developers.openai.com/codex/app-server
[3] Anthropic, Streaming messages — Claude Platform Docs
https://platform.claude.com/docs/en/build-with-claude/streaming
[4] Cursor, Build programmatic agents with the Cursor SDK;How Notion used the Cursor SDK to embed coding agents
https://cursor.com/blog/typescript-sdk
https://cursor.com/blog/notion
[5] xAI, Grok Build — Headless & Scripting
https://docs.x.ai/build/cli/headless-scripting
[6] Google, Getting Started with Google Antigravity;Gemini 3 for developers: Agentic coding / Antigravity
https://codelabs.developers.google.com/getting-started-google-antigravity
https://blog.google/innovation-and-ai/technology/developers-tools/gemini-3-developers/
[7] WHATWG, HTML Living Standard — Server-sent events
https://html.spec.whatwg.org/multipage/server-sent-events.html
[8] IETF / RFC Editor, RFC 6455: The WebSocket Protocol
https://www.rfc-editor.org/rfc/rfc6455
[9] JSON-RPC Working Group, JSON-RPC 2.0 Specification
https://www.jsonrpc.org/specification
[10] xAI, Agent Dashboard in Grok Build, 2026-06-15
https://x.ai/news/agent-dashboard
[11] Anthropic, Fine-grained tool streaming
https://platform.claude.com/docs/en/agents-and-tools/tool-use/fine-grained-tool-streaming
[12] Anthropic, Managed Agents — Session event stream
https://platform.claude.com/docs/en/managed-agents/events-and-streaming
[13] Windy3f3f3f3f3f, How Claude Code Works — 系统主循环 / StreamingToolExecutor(社区独立研究,非 Anthropic 官方)
https://github.com/Windy3f3f3f3f/how-claude-code-works/blob/main/docs/02-agent-loop.md
[14] Windy3f3f3f3f, How Claude Code Works — 用户体验设计 / 流式输出(社区独立研究,非 Anthropic 官方)
https://github.com/Windy3f3f3f3f/how-claude-code-works/blob/main/docs/12-user-experience.md
[15] Windy3f3f3f3f, How Claude Code Works — 可观测性:Metrics 与 Trace(社区独立研究,非 Anthropic 官方)
https://github.com/Windy3f3f3f3f/how-claude-code-works/blob/main/docs/16-observability.md
[16] Microsoft, Event Sourcing Pattern — Azure Architecture Center
https://learn.microsoft.com/en-us/azure/architecture/patterns/event-sourcing
[17] Amazon Web Services, Transactional outbox pattern — AWS Prescriptive Guidance
https://docs.aws.amazon.com/prescriptive-guidance/latest/cloud-design-patterns/transactional-outbox.html
[18] Apache Kafka, Design — Message Delivery Semantics (Kafka 4.3)
https://kafka.apache.org/43/design/design/#message-delivery-semantics
[19] Amazon Web Services, Making retries safe with idempotent APIs — Amazon Builders’ Library
https://aws.amazon.com/builders-library/making-retries-safe-with-idempotent-APIs/
[20] IETF / RFC Editor, RFC 9112: HTTP/1.1
https://www.rfc-editor.org/rfc/rfc9112
[21] IETF / RFC Editor, RFC 9113: HTTP/2
https://www.rfc-editor.org/rfc/rfc9113
[22] IETF / RFC Editor, RFC 9114: HTTP/3
https://www.rfc-editor.org/rfc/rfc9114
[23] NGINX, ngx_http_proxy_module — proxy_buffering
https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_buffering
[24] Cloud Native Computing Foundation, CloudEvents Specification 1.0.2
https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md
[25] OpenTelemetry, Signals;W3C, Trace Context
https://opentelemetry.io/docs/concepts/signals/
https://www.w3.org/TR/trace-context/
[26] OWASP, Logging Cheat Sheet
https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html
[27] Reactive Streams, Reactive Streams Specification
https://www.reactive-streams.org/
[28] Agent Client Protocol, Protocol Overview
https://agentclientprotocol.com/protocol/overview
完