T3 Code 源码拆解
核心运行时

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 判断。

用户输入的提交路径
Composer payload Typed RPC Method scope check dispatch envelope Single command queue decider thread.message-sent thread.turn-start-requested Atomic transaction Commit then publish

decider 通常产生至少两类事实:用户消息已经进入线程,turn start 已被请求;还可能重置上一轮生命周期状态。 事务把事件、关系投影和幂等回执一起落盘。此时 RPC 可以返回“命令已接受”,但 Agent 尚未一定启动。

阶段 3:ProviderCommandReactor 执行意图

ProviderCommandReactor.ts 订阅 turn-start-requested。它解析 thread 绑定的 provider instance、工作目录、runtime/interaction mode 和模型选择,然后:

  1. 查询当前 thread 是否已有可恢复 Provider session;
  2. 没有则通过 instance registry 取得 adapter 并 startSession;
  3. 有则验证 session 仍存在,必要时恢复或重建;
  4. 更新 session binding/lifecycle;
  5. 调用 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 对象。

Provider 流的两级归一化
SDK / JSON-RPC / ACP update Provider Adapter Canonical Provider Event Runtime Ingestion lifecycle guard Segment buffer / coalescing Orchestration command Engine Projected message/activity RPC subscription Incremental UI

阶段 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:

  1. turn 开始前捕获 baseline checkpoint;
  2. turn 完成后捕获 completed checkpoint;
  3. 计算二者 diff,保存/投影可查看的变更;
  4. 用户选择 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 等)。

全链路时序表

顺序所有者输入持久事实/输出
1Clientcomposer payload + command IDRPC request
2RPC/Authprincipal + methodauthorized call 或 typed error
3Deciderstate + start commanduser message、turn requested events
4Engine TXevents + projection + receiptcommitted sequence
5Command Reactorturn request eventprovider startSession/sendTurn
6Adapterprovider-native streamcanonical runtime events
7Ingestioncanonical event + cachesorchestration commands
8Engine/Projectorcommandsmessage/activity/session/turn projections
9Checkpoint Reactorturn lifecyclehidden Git refs + diff
10RPC streamsnapshot sequence + eventsclient cache advances

最容易写错的五个地方

  1. RPC 成功就把 turn 标 completed。 RPC 只表示意图接受,Provider 结果必须回流。
  2. 用 Provider session 当 Thread。 重建 session 会丢产品历史和 checkpoint 关联。
  3. 审批只保存在 adapter callback。 断线或重启后无法恢复,也无法跨端接力。
  4. 逐 token 进入领域日志。 放大存储、网络和渲染成本;应按 segment/buffer 合并。
  5. 客户端自行猜状态。 多端会分叉;权威 lifecycle 来自投影与 sequence。

源码锚点

可迁移经验

端到端正确性来自明确的所有权交接,而不是单个“万能会话对象”。为每一阶段回答:输入是谁的事实、成功意味着什么、 失败由谁记录、重启后从哪里恢复。能回答这四个问题,流式 Agent 产品才不会在 happy path 之外失去可信状态。

On this page