第五部分 · 进程架构与界面

第 15 章:AgentConnection——可重连的执行边界

generation、快照与游标:把断线重连做成一次无感的状态对齐

边界两侧

第 14 章讲的是执行一侧的进程架构。这一章看边界本身:AgentConnection——client 与执行之间的唯一接口。官方文档给它立的规矩是:

UI 不得依赖 AgentSessionRuntimeAgentSessionSessionManager、socket 细节或可执行回调。

这条边界的存在让「终端关掉、执行继续」成为可能:只要界面只消费可序列化的状态与事件,换一条连接、换一个终端、甚至换一个进程,画面都能原样长回来。本章拆三样东西:接口形状generation 与游标detach/reattach 的完整闭环

接口形状:约 90 个方法,一条事件流

types.ts 里的 AgentConnection 接口约 90 个方法,覆盖提示/转向/中止、模型与思考级别、压缩/重试/refine、会话导航(new/switch/fork/navigate/import/export)、队列操作、cron/heartbeat、agent 消息、extension UI、RLM 深度……事件面则收敛为十种 AgentConnectionEventsession_event(包装第 11 章的 AgentSessionEvent)、session_replacedsession_resyncedsession_statusextension_ui_requestconnection_statusclosed 等。

接口有两个实现,对 InteractiveMode 完全同形

实现传输用途
DaemonAgentConnection(约 2,100 行)daemon socket(JSONL)交互式默认路径:一切经 supervisor
InProcessAgentConnection(约 650 行)无——直接包装 AgentSessionRuntimeSDK 直调与显式 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 的完整闭环

图 1:从关闭终端到画面完整恢复。worker 全程不知道终端存在过;画面恢复的主力是 supervisor 的快照缓存,直播增量走 compact delta 旁路。

关闭终端

TUI 退出 → dispose()detach → supervisor 把 client 移出 attached 集合、取消它的在途 prompt admission、对 client-owned worker 安排 30 秒清理宽限。worker 不受任何影响——会话、scheduler、kernel 全部继续。唯一的隐形生命周期:从未有过消息的空草稿会话,在最后一个 client detach 时被丢弃。

重新接上

prime-agent attach <selector>(选择器支持 activeSessionId / sessionId / 名称 / 无歧义后缀):

  1. client 连 daemon,list 找到活动会话 summary。
  2. attach 命令带 clientId + capabilities + resumeCursor。
  3. supervisor 命中快照缓存则直接复用;否则向 worker 拉取——worker 序列化一次(state + messages + context + tree + RLM children + streamingMessage + lastEventCursor),supervisor 缓存进 SnapshotTranscriptCache
  4. chunked 客户端收 begin/chunk/end 流,DaemonAgentConnection 组装 snapshot 交给 InteractiveMode——画面完整恢复,包括进行中的 assistant 消息:streamingMessage 单独投影,supervisor 用它 seed 流重建器,后续增量事件无缝接上。
  5. 断线期间的事件: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 化的世界里,「能过边界的东西」被重新划定了:数据可以,行为不可以;请求可以,回调不行。

实践应用

  1. 用 generation 把「一辈子」显式化。凡是「逻辑上同一个东西、物理上可能重启/替换」的实体,都值得一代一号。客户端的重复判定、陈旧过滤、恢复基线选择,全部收敛到「当前 generation 单调递增」一条规则。
  2. 恢复基线选快照,重放当优化。快照恢复逻辑简单、服务端零存档义务;游标照样把重复与陈旧判得干干净净。机制的形状可以先于性能存在。
  3. 接口同形,实现分形。daemon 与 in-process 两条路径共用快照投影与事件形状,UI 层完全无感——「执行在哪」成了纯粹的部署问题。
  4. 边界只传数据,不传行为。可序列化性不是实现细节,是架构约束:它决定了哪些扩展能力可以远程化,哪些永远留在本地。

总结

AgentConnection 是 Prime 执行边界的客户端半圆:约 90 个方法、十种事件、两个同形实现。generation 把会话的每次重生标记成新一代,游标让客户端以一条规则过滤一切重复与陈旧;快照缓存承担画面恢复,compact delta 承担直播流量;断线时 60 秒自救循环可以从零拉起 supervisor 再 attach 回来。关闭终端只是 detach,画面、执行、时间线,全都还在原处。

下一章看边界的最后一侧:终端里那个把 IPython cell、子代理树、多会话目录渲染出来的 TUI。