写 Agent runtime 时,我经常会看到一个很顺手的设计:拿到一次 tool call,在同一个 handler 里读会话状态、检查权限、执行 shell,再把结果推给 UI。demo 阶段它很省事,到了重试、超时和恢复场景,问题就开始变得具体:到底谁拥有这次调用的状态?谁能证明副作用已经发生?失败后下一轮应该相信哪一份记录?
把 recovery-boundaries 章节和相邻的工具、patch、审批章节放在一起看,会得到一个很实用的拆法:把责任分成 owner、executor、governor、observer。它们可以由同一个类型中的不同方法承担,但不应该被揉成一份"工具结果"。
先找 owner:谁能把状态带过下一次 await
owner 持有的是跨 await、跨调用仍然有效的状态。它通常知道当前 session、turn、调用身份,以及下一步继续执行所需的上下文。这个角色决定哪份状态可以被更新、持久化或恢复。
这里有一个很容易被 UI 掩盖的错误:界面上出现了"工具已完成",不代表 owner 已经把完成事实写进自己的状态。一个事件可能已经发到前端,session 的 durable 记录却还没有落下;重新打开会话时,UI 看到过的那件事就没有地方可以重放。
所以我现在看 Agent 的状态问题,第一问不是"有没有 event",而是"这个 event 对应的权威 owner 是谁"。如果答案只能指向一段渲染代码,恢复边界通常还没有被定义。
executor 只负责碰世界
executor 是真正调用文件系统、shell、PTY、pipe 或远端 backend 的那一层。它拿到的是已经通过调度的 invocation,然后产生实际副作用或明确的错误。
这层不该顺便承担"用户是否批准""UI 要显示什么""下一轮上下文放什么"。把这些逻辑塞进执行函数,会让一次失败变得很难解释:进程可能已经启动,模型却只收到一个 Err;目录可能已经改变,TurnDiff 却没有对应的 patch;重试时又不知道上一次到底走到了哪一步。
源码里有一条很重要的边界:tool future 返回 Err 时,drain 分支调用的是 error_or_panic,不会自动补出一条 durable output。也就是说,失败调用可能没有可回放的工具产物。模型输出、机器状态、后续 prompt 需要分别核对,不能压成一个"调用结果"。
governor 决定这次能不能做
governor 处理审批、permission 和 sandbox。它回答的是"这次请求允许执行到哪里",不是"执行完以后发生了什么"。
如果权限决定隐藏在 executor 内部,审查日志里就只剩一句"handler 返回成功/失败",很难知道拒绝是因为用户没有批准、沙箱不允许,还是执行本身出错。更糟的是,同一个工具在不同 session 或不同模式下可能拿到不同权限,调用者却只看到了同一个工具名。
把 governor 单独拿出来,至少能给每次 invocation 留下三个可核对的事实:请求了什么权限,采用了什么 sandbox,最终是批准、拒绝还是绕过。它们不需要进入模型的自然语言回复,但应该存在于 runtime 能回查的记录里。
observer 看到的是投影,不是机器本身
observer 负责生成 item、diff、event 和 transcript。这些东西对 UI、日志和下一轮提示都很重要,但它们仍然是观察结果。
例如 TurnDiff 不是完整审计日志。普通 shell、外部进程、目录或权限变化,可能没有对应的 AppliedPatchDelta;patch move 和失败前缀也没有事务回滚。observer 没看到变化,只能说明这条投影没有记录,不能拿来证明机器没有改变。
这也是为什么"UI 显示完成"不能替代执行收据。一个可靠的收据至少要能指向执行者、权限决定和持久化 owner;observer 只负责把这些事实投影出来,不负责创造事实。
四种责任合在一起,故障会变成一句模糊的话
可以用一次 apply_patch 失败来检查边界:
text
owner 保存 session/turn 与调用身份
governor 决定 permission、sandbox 与审批结果
executor 对文件执行 patch,返回成功或错误
observer 生成 item、diff、transcript,供界面和日志读取
如果 patch 已经改动了部分文件,executor 需要提供可核对的副作用收据;如果 governor 拒绝了请求,observer 应该能显示"拒绝"而不是笼统的失败;如果进程在返回前崩溃,owner 还要知道这次 invocation 是否有 durable 结果。四层各自回答一个问题,重试策略才有依据。
我给 Agent 平台留了一份很小的检查表:
- 每个 action 的状态 owner 是谁,跨 await 后仍由谁持有;
- executor 产生的副作用是否有独立收据,失败是否也会落记录;
- governor 的 permission、sandbox 和审批结果能否按 invocation 回查;
- observer 的 diff/event/transcript 哪些是投影,哪些字段指向权威状态;
- 工具 future 失败、进程半途退出、网络超时后,下一轮从哪份事实继续。
这几问没有漂亮的统一答案。它们的价值在于把"工具调用成功"拆成可以逐项验证的责任。模型能看到的 ToolSpec、runtime 能执行的 handler、界面能展示的 event,分别属于不同边界;把它们混成一个对象,代码会更短,恢复时却只能靠猜。
完整的责任交接可以继续看 codex-internals 第 13 章。
源码依据:codex-rs/core/src/session/turn.rs L419-L459、L1112-L1206、L1892-L1916;codex-rs/core/src/tools/router.rs L28-L78、L221-L243;对应章节为 13-recovery-boundaries、14-tool-spec-registry、15-apply-patch-turn-diff 与 17-approval-permissions-sandbox。