OpenClaw源码解析(七) | 下集:Agent Runner 全执行链
书接上回
OpenClaw源码解析(七):Agent Runner 全执行链 | 上集
第 7 层:为什么“attempt 级重试”是合理的
7.1 优点
-
恢复边界清晰
外层只需要信任 session transcript 和 attempt 结果,不需要理解底层 agent 内部状态机。
-
容错简单
一旦 provider、auth、上下文窗口、tool result 格式变化,直接新起 attempt 更安全。
-
便于跨 provider failover
底层内部 loop 的中间态可能带有强 provider 依赖;attempt 级重放更容易切模型。
-
和 compaction / truncation 兼容
如果外层刚做了上下文压缩,再回到旧内部断点反而会失真。
7.2 代价
-
某些中间推理会重复发生。 -
某些工具调用可能重新规划。 -
如果 transcript 保留不够好,重试质量会下降。
这也正是之后的非常重要的篇章:上下文工程和记忆系统。
因为一旦系统采用“attempt 级重放”,那它对“最新上下文是否足够好”就极为敏感。
第 8 层:attempt 启动前,真正重要的不是 prompt,而是“准备可执行会话”
attempt.ts 前半段做了大量准备工作,这些不是附属动作,而是 agent 能否稳定运行的前提。
包括:
-
创建 agent session -
注入 built-in tools / custom tools / client tools -
设置 streamFn 兼容层 -
安装 tool result context guard -
构建 system prompt -
将 system prompt 写入 session
第 9 层:进入模型前,历史 transcript 会经过一条严格预处理链
在 src/agents/pi-embedded-runner/run/attempt.ts:1384 history 先经过:
sanitizeSessionHistory-> validateGeminiTurns / validateAnthropicTurns-> limitHistoryTurns-> sanitizeToolUseResultPairing-> contextEngine.assemble
-
进入模型前历史消息会先被“清洗、校验、结构裁剪、配对修复”。 -
contextEngine 在默认实现里不是唯一主角,它插在这条流水线的后段。
这对理解“两层 run loop”很重要,因为外层重试时,重新发起的新 attempt 并不是拿原始脏历史重新跑,而是再次执行这条预处理链路。
第 10 层:system prompt 并不是一次写死,而是 attempt 内部逐步改写
在一个 attempt 里,system prompt 大致经历:
-
初始构建。 -
applySystemPromptOverrideToSession(…) 写入 session。 -
contextEngine.assemble(…) 追加 systemPromptAddition。 -
hooks 可能 override / prepend / append。
这意味着:
一个新的 attempt 虽然基于旧 transcript 继续,但它的系统控制面会重新构建,不是沿用上一轮 attempt。
所以外层重试不是“冻结现场再接着跑”,而是“用最新上下文重建再次执行”。
第 11 层:subscribeEmbeddedPiSession(…) 说明 attempt 不是只等模型返回结果
attempt.ts 在 src/agents/pi-embedded-runner/run/attempt.ts:1505 会创建订阅器:
const subscription = subscribeEmbeddedPiSession(...)
它跟踪的不只是文本流,还有:
-
assistant 文本块 -
tool meta -
usage totals -
compaction count -
tool error -
messaging tool 输出
这说明 attempt 的后半段实际上在做:
“把底层 runtime 的流式事件重构成一个可供外层判断的结构化状态聚焦。”
所以 attempt 返回的不是一个“字符串”,而是一个诊断对象。
第 12 层:为什么 activeSession.prompt(…) 返回后,attempt 还不能立刻结束
在 src/agents/pi-embedded-runner/run/attempt.ts:1807,attempt 还会:
await waitForCompactionRetry();
这意味着:
-
模型主调用结束,不代表这轮上下文已经稳定。 -
可能还有 auto-compaction 或 compaction retry 在进行。 -
只有等 compaction 相关流程结束,当前 attempt 才算真正结束。
这对两层 loop 的边界非常关键:
attempt 内部不仅包含“模型-工具循环”,还包含“本轮执行后的上下文稳定化阶段”。
第 13 层:attempt 结束后,外层 run.ts 怎么决定是成功还是继续
run.ts 拿到 attempt 结果后,做的不是简单 if error then retry。
它会区分很多分支:
-
上下文溢出 -
compaction failure -
oversized tool result -
prompt submission error -
role ordering error -
image size error -
auth failure -
rate limit / billing / overload -
thinking level 不兼容 -
timeout
其中最重要的几类恢复分支都在 src/agents/pi-embedded-runner/run.ts:
-
context overflow 后 continue:run.ts:994, run.ts:1048, run.ts:1091 -
auth/profile/thinking/failover 后 continue:run.ts:1147, run.ts:1222, run.ts:1233, run.ts:1260, run.ts:1281, run.ts:1330
这些 continue 的意义不是“继续 attempt 内的某一轮”,而是:
回到外层 run loop 顶部,重新发起一个新的 attempt。
第 14 层:把整个运行过程压成一条最终流程图
用户发消息-> run.ts 解析 provider/model/profile/context window/context engine-> run.ts 外层恢复 loop 开始-> attempt.ts 创建 session + tools + prompt + context-> activeSession.prompt(...) 进入底层 agent 内部循环-> 模型判断-> 调工具-> tool result 写回 transcript-> 模型继续判断-> 重复直到停止-> attempt.ts 等待 compaction / streaming / afterTurn 收束-> attempt.ts 返回结构化结果-> run.ts 判断-> 成功:返回最终 payload-> 可恢复失败:更新条件并重新发起一个新 attempt-> 不可恢复失败:返回最终错误
这一篇的最终结论
-
OpenClaw 至少有两层 loop,但它们语义完全不同。
run.ts 是恢复 loop,activeSession.prompt(…) 背后才是 agent 真正的任务推进 loop。
-
正常复杂任务通常只需要一次 attempt。
只要没有逃逸到外层恢复条件,任务的大部分执行都待在 attempt.ts 这一个容器里。
-
外层 retry 不是从内部 round 断点继续。
它是基于最新稳定 transcript 发起一个全新的 attempt。
-
因为重试是 attempt 级,所以上下文工程和记忆系统变得极其重要。
它们决定“新的 attempt 能不能快速接上之前的任务状态”。
夜雨聆风