Agent 工程对照
← 返回框架对照

循环 · 第 2 章

中断与恢复

被打断之后,从哪里、凭什么接着跑?

分歧在恢复的最小粒度:粒度越粗越容易保证一致,越细越省重复工作。

恢复的最小粒度

  1. 粗:迭代边界
  2. openJiuwen
  3. Pi
  4. CodexDeepSeek
  5. 细:日志重放

定性示意:只表示先后,间距没有数值含义。

场景建议

编辑判断

按你的需求,倾向哪种做法

借鉴的是那一家的设计,不等于选用那个框架。

场景

工具调用有副作用(写库、发消息、付款)

它区分「已记录开始但结果未落盘」与「尚未开始」,前者明确告诉模型结果未知、只有幂等操作才可重试。

场景

会话很长,恢复不能从头重放

倒序扫描到最近的压缩检查点,只正向重放检查点之后的尾巴。

场景

恢复逻辑越简单越好

openJiuwen 只记一个迭代号;Pi 的恢复就是不追加新消息、从当前上下文继续。

容易踩的坑

  • 恢复点一旦落在工具调用中间,就绕不开幂等性:先分清哪些工具可以安全重试,再设计恢复。

四家怎么做

差异与源码

openJiuwen

中断状态记住迭代号,从下一轮继续

恢复的粒度是 ReAct 迭代。中断状态里存着被打断时的 iteration,恢复时先给挂起的工作流补记反馈,等所有工作流都跑完,再把 resume_iteration + 1 写进 ctx.extra 的 RESUME_START_ITERATION_KEY——主循环的 for range 正是从这个值起步(见「主循环形态」一行)。也就是说中断点被对齐到迭代边界,不会从半轮中间接续。

适合
人机协作审批频繁、中断点天然落在迭代之间的流程。
代价
只能从迭代边界恢复,被打断的那一轮要整轮重来。

此后的变化

  • 2026-W35 →

    swarmflow 新增工作流级 pause/resume/stop,此前该能力只在子代理层(W34 的 spawn/wait/resume)。

Codex

倒序扫描 rollout,找到最近的压缩检查点,只正向重放它之后的尾巴

恢复就是从 JSONL rollout 重建历史,但不是从头重放。它从最新一条往回扫:遇到带 replacement_history 的压缩条目,就把它当作完整的历史基座,更早的条目不再影响结果(rollout_suffix 从这里截断);遇到 ThreadRolledBack,就记下「跳过接下来 N 个用户回合」——回滚在倒序里变成了跳过。扫到基座和恢复所需的元数据后,再把尾巴正向重放一遍,保证语义与在线时一致。注释写明这是在为将来的「惰性倒序加载」预先对齐形状。

适合
超长会话、需要回滚若干回合的交互式产品。
代价
恢复逻辑要同时处理压缩检查点、回滚与窗口编号,是四家里最复杂的回放器。

此后的变化

  • 2026-W41 →

    W39 新增的 instant_interrupt 改为默认开启;code_mode_interrupt 标为稳定并默认开启,打断回合会终止活动的 code-mode cell 与嵌套工具调用,会话中已存的值保留。

  • 2026-W39 →

    新增默认关闭的 instant_interrupt:用户输入到达时中断请求准备、流式响应与重试退避,code-mode 的 exec/wait 先交回 cell 编号而不停止执行;被抢占后重置 WebSocket 续接,下一次请求发全量历史。原格描述的是跨进程恢复,这是回合内抢占的补充。

  • 2026-W38 →

    Guardian 审查证据(用户限制、已验证答案)现在独立于压缩/重启存在:恢复与远端压缩时解析所选 reviewer 的兼容性哈希,不兼容或未知时保留旧 transcript,兼容压缩后才失效绑定旧证据策略的待处理审查。本格此前描述的「倒序找压缩检查点、只重放尾巴」的恢复机制,现在多了一条并行规则——审查证据的保留窗口可以比常规历史重放窗口更长。

DeepSeek Harness

崩溃的回合不截断,而是补写合成的收尾事件

会话日志是唯一事实来源,恢复就是重放;这一格要补的是「日志停在半截」的情况。interruptedTurnClosers 扫描加载进来的日志,给每个没有结果的工具调用补一条错误结果,再补 step/end 和 reason 为 interrupted 的 turn/end,让重放出来的历史仍是厂商能接受的合法转录。最值得看的是那两句写给模型的话:调用若已记录开始、但结果没落盘,就是「结果未知」——只读或幂等的操作才可以重试,有副作用的要先核对外部状态或问用户;还没记录开始的,才可以放心重试。持久化层的 README 把这条列为所有后端必须遵守的不变量:崩溃的回合是被关闭,不是被截断。

适合
工具有副作用、进程可能随时崩溃的长任务。
代价
每个后端都必须实现「关闭而非截断」这套不变量,持久化层更重。
Pi

从当前上下文续跑,用两条断言守住前置条件

恢复不是特殊模式,而是一个不追加新消息、直接从现有上下文继续的入口,主要用于重试。它只守两条前置条件:上下文不能为空,最后一条消息不能是 assistant——因为下一次请求要求末尾能转成 user 或 toolResult,否则厂商会直接拒。注释诚实地写明这一点无法在此处校验,因为 convertToLlm 每轮只调用一次。会话本身以 append-only 的 JSONL 落盘,恢复时从最近一次压缩条目开始重建上下文。

适合
恢复主要用于重试、会话不长的场景。
代价
只守两条前置条件,更复杂的一致性要调用方自己保证。

此后的变化

  • 2026-W42 →

    durable 运行时新增崩溃循环保护:任务被崩溃中断的次数累计到 restartLimit(默认 5)即挂起为 blocked,需 retryTask 重新放行;任务提交新状态时计数清零。另用持久化的 sleeping 状态替代 runtime.sleep,休眠截止时间可跨重启。

  • 2026-W41 →

    durable 包新增可重放的 code mode:脚本在虚拟时钟与固定随机种子下运行,每个工具结果先写日志再交付,重启后按日志重放到断点再继续。原格描述的是 coding-agent 从当前上下文续跑,这是 durable 运行时里另一条恢复路径。

横向读这一行

分歧在「恢复的最小粒度是什么」:openJiuwen 对齐到 ReAct 迭代边界,恢复即从记下的迭代号加一继续;Pi 对齐到消息角色,只要末尾能转成 user 或 toolResult 就能续跑;Codex 和 DeepSeek 都靠重放日志恢复,差别在细节——Codex 倒序找到最近的压缩检查点、只重放它之后的尾巴,DeepSeek 则给崩在半截的回合补写合成的收尾事件,并明确告诉模型哪些工具调用「结果未知、别盲目重试」。粒度越粗越容易保证一致性,越细越省重复工作;而一旦恢复点落在工具调用中间,就绕不开幂等性问题。

设计空间分析

恢复的难点不在「从哪里接着跑」,而在「中断那一刻正在做的事算不算数」——四家对这个问题的回答,决定了各自恢复逻辑的复杂度。

openJiuwen 和 Pi 选择了让问题不出现:openJiuwen 把中断点对齐到 ReAct 迭代边界,恢复只需记住一个迭代号、从下一轮继续,被打断的那一轮整轮作废;Pi 的恢复甚至不是一种模式,只是一个不追加新消息、从当前上下文继续的入口,前置条件只有两条断言——上下文非空、末尾不是 assistant 消息。

Codex 和 DeepSeek 都以日志为正本,恢复等于重放,但各自攻克了不同的难处。Codex 的难处是长:它从 rollout 末尾倒着扫,找到最近一个带完整替换历史的压缩检查点,更早的条目一概不读,只把检查点之后的尾巴正向重放,回滚在倒序扫描里变成「跳过接下来 N 个用户回合」。DeepSeek 的难处是半截:进程崩在回合中间时,它不截断日志,而是补写合成的工具结果、step/end 和 interrupted 的 turn/end,让转录重新合法。

DeepSeek 那两句写给模型的话,是整行最有工程含量的细节:已经记录开始、但结果没落盘的工具调用,被明确标为「结果未知」,只允许只读或幂等的操作重试,有副作用的要先核对外部状态或问用户;尚未记录开始的,才可以放心重试。其余三家的格子里都没有写到这一层区分。

编辑判断

恢复粒度与工具幂等性是同一个问题的两面:粒度越粗(迭代、整轮),越不容易撞上半截的副作用,代价是重复工作;粒度越细,越需要像 DeepSeek 那样把「这次调用到底发生了没有」显式交给模型或用户判断。自建 Harness 时,先给工具分出可重试与不可重试两类,再决定恢复粒度,顺序不宜颠倒。

还没有定论:Codex 与 Pi 在崩溃发生于工具调用中间时如何处理未闭合的调用,本行格子读到的代码没有覆盖,需要后续走读确认。

这个问题为什么存在

第 2 章

Agent 循环

收集上下文 → 行动 → 检查 → 重复,直到满足停止条件