06 · 一次 Turn 的完整生命周期
从用户按下发送,到 Provider 流式回调、checkpoint 与多端 UI 收敛的端到端追踪。
这一章是全书主线。我们追踪一个具体场景:用户在手机打开已有 thread,发送“修复登录竞态并运行测试”, Agent 请求执行命令,用户批准,最终修改文件并完成。过程中手机短暂断网,桌面仍保持在线。
阶段 0:连接与快照基线
客户端先通过 EnvironmentRegistry 获得执行环境租约,由 connection supervisor 建立一个 RPC session。
连接成功不等于 UI 已准备好;runtime 还要拉取环境配置、project/thread shell 与所需 thread detail 快照,记录当前 sequence。
resolve environment → acquire lease → open WS → probe → load snapshots → subscribe from sequence如果在“快照读完”和“订阅建立”之间产生事件,Server 用 cursor 补 gap;gap 大于上限则客户端丢弃这次基线并重取。
阶段 1:UI 形成稳定意图
Composer 会将文本、附件、模型选择、交互/权限模式、可选 proposed plan 等整理成 RPC payload。 关键是生成一个在网络重试中保持不变的 command ID。UI 可以立即显示 pending/optimistic 状态,但权威消息序号来自 Server。
Web 中负责交互的大组件非常庞大,例如 ChatView.tsx 汇聚虚拟化、composer、活动、审批和滚动锚点;
这不改变协议原则:组件只发一个领域意图,不直接创建 Provider session。
阶段 2:RPC 鉴权并 dispatch
ws.ts 从连接 principal 检查发送消息所需 scope,Effect Schema 校验 payload,再调用 orchestration engine dispatch。
Transport 层不决定“thread 当前能否开始 turn”;这个 invariant 由 decider 根据权威 read model 判断。
decider 通常产生至少两类事实:用户消息已经进入线程,turn start 已被请求;还可能重置上一轮生命周期状态。 事务把事件、关系投影和幂等回执一起落盘。此时 RPC 可以返回“命令已接受”,但 Agent 尚未一定启动。
阶段 3:ProviderCommandReactor 执行意图
ProviderCommandReactor.ts
订阅 turn-start-requested。它解析 thread 绑定的 provider instance、工作目录、runtime/interaction mode 和模型选择,然后:
- 查询当前 thread 是否已有可恢复 Provider session;
- 没有则通过 instance registry 取得 adapter 并
startSession; - 有则验证 session 仍存在,必要时恢复或重建;
- 更新 session binding/lifecycle;
- 调用
adapter.sendTurn发送规范化输入。
首次 turn 还可能触发线程标题生成和 worktree branch 命名。它们是附属工作,失败不应抹掉主消息;实现通过独立 drainable worker 和安全包装把故障降级为活动/错误。
为什么要“确保 session”而不是在创建 thread 时就启动?因为很多 thread 可能永远不发送消息,Provider CLI 也可能当时未安装、 账号配置后来变化。按 turn 延迟建立把昂贵、易失败的资源放到真正需要的时刻。
阶段 4:Provider 返回规范事件
每个 adapter 暴露统一 streamEvents。原始协议可能给出 JSON-RPC notification、SDK async iterator 或 ACP update,
adapter 先映射成 canonical provider events。ProviderRuntimeIngestion 再把它们翻译成 orchestration commands。
常见序列如下:
session started
turn started
assistant segment started
text delta × N
tool activity started
approval requested
approval resolved
tool activity updated/completed
assistant segment completed
turn completed
session idle并非所有 Provider 都严格发齐这些事件。ingestion 因此维护 session/turn/segment 缓存和生命周期 guard,补足关联信息并拒绝非法跃迁。
阶段 5:流式文本被合并,而非 token 级持久化
如果每个 token 都成为一条 SQLite 事件、一个 RPC frame 和一次 React 更新,吞吐与渲染会被放大。ingestion 使用 assistant segment 缓存和 buffer,把碎片聚合后发出规范化 message delta;单次缓冲有约 24,000 字符上限,防止异常 Provider 无限积累。
projector 收到 delta 后追加到已有 assistant message segment。客户端看到的是渐进文本,但领域模型仍保持“消息由 segment 构成”, 而不是几万个 token 对象。
阶段 6:审批是挂起的领域状态
当 Agent 要执行需要确认的命令,Provider 发 approval request。ingestion 不在内存里等待某个 callback,而是 dispatch 命令,
投影形成 pending approval。于是审批可以:
- 在桌面显示,也在手机显示;
- 经 Server 重启后仍有可读状态;
- 带明确 request/session/turn 关联;
- 由有权限的任一客户端响应。
用户点击允许后,又是一个 RPC → command → event。ProviderCommandReactor 看到 approval response intent,调用
adapter.respondToApproval。Provider 后续回调确认执行继续,ingestion 再结算 pending 状态。
这里有两次确认:产品记录“用户做出了决定”,Provider 协议确认“决定被消费”。两者之间可能失败,UI 需要展示而非假装原子。
阶段 7:checkpoint 与代码变化
CheckpointReactor 在 turn 边界协调 Git:
- turn 开始前捕获 baseline checkpoint;
- turn 完成后捕获 completed checkpoint;
- 计算二者 diff,保存/投影可查看的变更;
- 用户选择 revert 时恢复目标 checkpoint 并记录结果。
checkpoint 使用隐藏 refs 和临时 Git index,不会把 Agent 的工作树快照塞进用户当前 staged index。详见Git 检查点。
阶段 8:完成、失败与结算
正常完成时,runtime events 最终产生 turn/session lifecycle 事件,projector 把最新 turn 结算为 completed/idle。 若 Provider 调用失败,reactor 将错误变成 activity 与 session error;若 Provider 突然离开 running,projector 也会结算最新 turn, 避免客户端永久显示 spinner。
取消路径同样是双阶段:用户 interrupt 意图先落盘,reactor 调用 adapter;真实 session 状态由后续 provider event 回流。
阶段 9:断线客户端如何追上
假设手机在文本流中断开,桌面继续收到事件,Server 与 Agent 不受影响。手机 supervisor 观察网络状态:离线期间不增加重试 attempt; 恢复在线后重新建立 RPC session。若 sequence gap 小于 1,000,订阅补齐;否则重新装载 thread 快照。
客户端绝不能把断线前的局部 spinner 当真相。它用新快照替换 environment-owned cache,再恢复局部视图状态(滚动位置、当前 tab 等)。
全链路时序表
| 顺序 | 所有者 | 输入 | 持久事实/输出 |
|---|---|---|---|
| 1 | Client | composer payload + command ID | RPC request |
| 2 | RPC/Auth | principal + method | authorized call 或 typed error |
| 3 | Decider | state + start command | user message、turn requested events |
| 4 | Engine TX | events + projection + receipt | committed sequence |
| 5 | Command Reactor | turn request event | provider startSession/sendTurn |
| 6 | Adapter | provider-native stream | canonical runtime events |
| 7 | Ingestion | canonical event + caches | orchestration commands |
| 8 | Engine/Projector | commands | message/activity/session/turn projections |
| 9 | Checkpoint Reactor | turn lifecycle | hidden Git refs + diff |
| 10 | RPC stream | snapshot sequence + events | client cache advances |
最容易写错的五个地方
- RPC 成功就把 turn 标 completed。 RPC 只表示意图接受,Provider 结果必须回流。
- 用 Provider session 当 Thread。 重建 session 会丢产品历史和 checkpoint 关联。
- 审批只保存在 adapter callback。 断线或重启后无法恢复,也无法跨端接力。
- 逐 token 进入领域日志。 放大存储、网络和渲染成本;应按 segment/buffer 合并。
- 客户端自行猜状态。 多端会分叉;权威 lifecycle 来自投影与 sequence。
源码锚点
可迁移经验
端到端正确性来自明确的所有权交接,而不是单个“万能会话对象”。为每一阶段回答:输入是谁的事实、成功意味着什么、 失败由谁记录、重启后从哪里恢复。能回答这四个问题,流式 Agent 产品才不会在 happy path 之外失去可信状态。