第五部分 · 进程架构与界面
第 15 章:AgentConnection——可重连的执行边界
generation、快照与游标:把断线重连做成一次无感的状态对齐
边界两侧
第 14 章讲的是执行一侧的进程架构。这一章看边界本身:AgentConnection——client 与执行之间的唯一接口。官方文档给它立的规矩是:
UI 不得依赖
AgentSessionRuntime、AgentSession、SessionManager、socket 细节或可执行回调。
这条边界的存在让「终端关掉、执行继续」成为可能:只要界面只消费可序列化的状态与事件,换一条连接、换一个终端、甚至换一个进程,画面都能原样长回来。本章拆三样东西:接口形状、generation 与游标、detach/reattach 的完整闭环。
接口形状:约 90 个方法,一条事件流
types.ts 里的 AgentConnection 接口约 90 个方法,覆盖提示/转向/中止、模型与思考级别、压缩/重试/refine、会话导航(new/switch/fork/navigate/import/export)、队列操作、cron/heartbeat、agent 消息、extension UI、RLM 深度……事件面则收敛为十种 AgentConnectionEvent:session_event(包装第 11 章的 AgentSessionEvent)、session_replaced、session_resynced、session_status、extension_ui_request、connection_status、closed 等。
接口有两个实现,对 InteractiveMode 完全同形:
| 实现 | 传输 | 用途 |
|---|---|---|
DaemonAgentConnection(约 2,100 行) | daemon socket(JSONL) | 交互式默认路径:一切经 supervisor |
InProcessAgentConnection(约 650 行) | 无——直接包装 AgentSessionRuntime | SDK 直调与显式 fallback:嵌入方可以传不可序列化的 extension factory |
同形不是偶然:快照构造函数 createAgentConnectionSnapshot()(snapshot.ts)从 runtime 直接投影,两条路径共用。in-process 路径没有 wire、没有游标、没有重连——它是「执行恰好也在本进程」的特例,而不是另一套语义。
generation:一条会话的「辈子」
daemon 事件都带游标 {generation, sequence}。generation 的生成时机是理解一切的关键:每个 ActiveSessionState 创建时随机生成自己的 eventGeneration——于是同一个 activeSessionId 下,runtime 被替换(new/switch/fork/import)或 worker 崩溃重启,generation 就翻新,旧 sequence 全部作废。
客户端侧的处理浓缩在一个方法里:
private observeEventCursor(cursor: DaemonEventCursor): void {
const current = this.lastEventCursor;
if (current && current.generation !== cursor.generation) {
this.retiredEventGenerations.add(current.generation);
}
if (!current || current.generation !== cursor.generation || cursor.sequence > current.sequence) {
this.lastEventCursor = cursor;
}
}
generation 变了,旧 generation 全体退休(retiredEventGenerations),迟到事件直接丢弃;同 generation 内 sequence 不递增的视为重复丢弃。断线重连、worker 重启、会话替换——三种完全不同的灾难,在客户端收敛成同一条规则:只信当前 generation 的单调递增事件,其余都是噪声。
replay:存在但不承诺
协议里有完整的 replay 形状:attach 携带 resumeCursor,服务端回 createDaemonReplayInfo 的判定。但读一下判定逻辑会发现一个诚实的事实:generation 不同 → unavailable(event_generation_changed);resume 超前 → unavailable;区间明明在范围内 → 同样 unavailable(event_replay_not_available)。增量重放当前并未启用——快照是唯一的恢复基线。
这不是缺陷,是刻意的债务划分:游标机制负责精确判定重复与陈旧(这已经让客户端逻辑完备),历史重放作为可选优化留白。服务端因此不必维护任何事件存档,客户端逻辑也不分支:拿不到 replay,就用快照。
detach / reattach 的完整闭环
关闭终端
TUI 退出 → dispose() 发 detach → supervisor 把 client 移出 attached 集合、取消它的在途 prompt admission、对 client-owned worker 安排 30 秒清理宽限。worker 不受任何影响——会话、scheduler、kernel 全部继续。唯一的隐形生命周期:从未有过消息的空草稿会话,在最后一个 client detach 时被丢弃。
重新接上
prime-agent attach <selector>(选择器支持 activeSessionId / sessionId / 名称 / 无歧义后缀):
- client 连 daemon,
list找到活动会话 summary。 attach命令带 clientId + capabilities + resumeCursor。- supervisor 命中快照缓存则直接复用;否则向 worker 拉取——worker 序列化一次(state + messages + context + tree + RLM children + streamingMessage + lastEventCursor),supervisor 缓存进
SnapshotTranscriptCache。 - chunked 客户端收 begin/chunk/end 流,
DaemonAgentConnection组装 snapshot 交给 InteractiveMode——画面完整恢复,包括进行中的 assistant 消息:streamingMessage 单独投影,supervisor 用它 seed 流重建器,后续增量事件无缝接上。 - 断线期间的事件:resumeCursor 与新 generation 比对——generation 变了(worker 重启过)→ 快照即基线;同 generation 也不重放(同上);客户端靠 stale 过滤吞掉一切重复与退休事件。
值得澄清一个容易混淆的角色:compact-session-stream(第 14 章的增量 delta)不为 reattach 服务——它是直播路径的流量优化;snapshot-transcript-cache 才是画面恢复的主力。两条机制服务两种时间:实时的流畅,与离开后的重逢。
断线重连:60 秒的自救
socket 意外断开(且不是 shutdown/update 关闭)时,DaemonAgentConnection 进入重连循环:
emit connection_status: reconnecting → options.recoverDaemon() // ensureInteractiveDaemonRunning——必要时 spawn 新 supervisor → client.connect(1000ms) → waitForHello(3000ms) → attach()(带 resumeCursor) → getInitialSnapshot() → emit session_resynced + connected
退避 100×2ⁿ 封顶 2s,总时限默认 60 秒;recoverDaemon 可能一路拉起一个全新的 supervisor(第 14 章的收养机制在另一头接住)。update 关闭走独立路径:等新 daemon 起来再重新 attach。
owned 会话的 dispose 有个体贴的细节:断线重连中 dispose 会至多等 10 秒重连,只为把 complete_owned_session 的完成状态送达——一次性任务的善后也要有始有终。
边界上的纪律
extension UI 是这条边界最严格的试金石:扩展可以发起的界面请求只有可序列化的几种(select / confirm / input / editor / notification / status / widget / title / editor-text)。tool execute、renderer、completion 函数永不过界。第 10 章说过扩展系统还在——但在 daemon 化的世界里,「能过边界的东西」被重新划定了:数据可以,行为不可以;请求可以,回调不行。
实践应用
- 用 generation 把「一辈子」显式化。凡是「逻辑上同一个东西、物理上可能重启/替换」的实体,都值得一代一号。客户端的重复判定、陈旧过滤、恢复基线选择,全部收敛到「当前 generation 单调递增」一条规则。
- 恢复基线选快照,重放当优化。快照恢复逻辑简单、服务端零存档义务;游标照样把重复与陈旧判得干干净净。机制的形状可以先于性能存在。
- 接口同形,实现分形。daemon 与 in-process 两条路径共用快照投影与事件形状,UI 层完全无感——「执行在哪」成了纯粹的部署问题。
- 边界只传数据,不传行为。可序列化性不是实现细节,是架构约束:它决定了哪些扩展能力可以远程化,哪些永远留在本地。
总结
AgentConnection 是 Prime 执行边界的客户端半圆:约 90 个方法、十种事件、两个同形实现。generation 把会话的每次重生标记成新一代,游标让客户端以一条规则过滤一切重复与陈旧;快照缓存承担画面恢复,compact delta 承担直播流量;断线时 60 秒自救循环可以从零拉起 supervisor 再 attach 回来。关闭终端只是 detach,画面、执行、时间线,全都还在原处。
下一章看边界的最后一侧:终端里那个把 IPython cell、子代理树、多会话目录渲染出来的 TUI。