OpenClaw源码解析(七):Agent Runner 全执行链 | 上集
核心文件:
– src/agents/pi-embedded-runner/run.ts
– src/agents/pi-embedded-runner/run/attempt.ts
– src/agents/pi-embedded-subscribe.ts
读完这篇你会了解以下问题
-
OpenClaw 到底有几层 loop,它们分别负责什么。 -
run.ts 的 while (true) 和 agent 真正执行任务的 loop 是不是一回事。 -
一个复杂任务正常执行时,代码主要停留在哪一层。 -
如果任务在“第 3 轮工具调用之后”出错,外层 retry 是从第 3 轮继续,还是重新来一遍。 -
attempt.ts 返回以后,run.ts 到底是在做兜底,还是在继续任务。
如果你只记一句话,先记这个:
run.ts 管的是“这次请求是否需要重新发起一个新 attempt”,attempt.ts 管的是“一个 attempt 内部如何让 agent 持续推进任务直到停下”。
先给总图
用户发送一条消息-> runEmbeddedPiAgent(...) in run.ts-> 解析 provider / model / auth profile / context window / context engine-> 进入 run.ts 外层 while(true)-> 发起一次 runEmbeddedAttempt(...)-> 打开 session / tools / memory / prompt / hooks / context-> 调 activeSession.prompt(...)-> 底层 agent 在这一次 prompt 里进入内部模型-工具-模型循环-> attempt 收集 streaming 事件、tool result、compaction 事件-> attempt 等待 compaction 收敛-> attempt 返回结构化诊断结果-> run.ts 判断:-> 成功:直接收束并返回-> 可恢复失败:修改状态后 continue,发起一个“新的 attempt”-> 不可恢复失败:构造最终错误并返回
理解这张图以后,很多细节都会自动清楚。
第 1 层:一次用户请求不是一次模型调用,而是一次“run”
入口在 src/agents/pi-embedded-runner/run.ts 的 runEmbeddedPiAgent(…)。
这里的基本运行单位不是“prompt”,而是“run”。
一个 run 的职责是:
-
为这次用户请求选择模型、provider、账号、上下文窗口。 -
建立这次请求允许使用的恢复策略。 -
在失败时判断能不能修复,然后决定是否再发起一个新 attempt。 -
最终把结果收束成 payload + meta 返回给上层。
一条消息= 一次 run= 1 到多次 attempt 的调度过程
第 2 层:run.ts 的外层 while (true) 是恢复循环,不是 agent 思考循环
最关键的代码结构在 src/agents/pi-embedded-runner/run.ts:792:
while (true) {...const attempt = await runEmbeddedAttempt(...)...}
2.1 这个 loop 的真实职责
这个 loop 不负责让模型一轮轮思考。
它负责的是:
-
retry limit 控制 -
auth profile 轮换 -
failover 到其他 provider/model -
thinking level 降级 -
context overflow 后的 compaction / truncation -
timeout / overload / billing / auth 错误恢复
换句话说,这层 loop 的语义不是:
第 1 轮推理 -> 第 2 轮推理 -> 第 3 轮推理
而是:
第 1 次完整执行尝试失败-> 调整运行条件-> 第 2 次完整执行尝试
2.2 这一层为什么必须存在
因为 agent 运行时并不总是“逻辑错了”,更多时候是“环境失败了”:
-
账号 token 过期 -
当前 provider 暂时超载 -
当前模型不支持某个 thinking level -
本轮 transcript 过大 -
某个 tool result 把上下文顶爆了
这些失败都不是 agent 内部“下一轮思考”能解决的,而必须由外层运行器介入。
所以:
run.ts 的 loop 是“运行环境恢复状态机”,不是“任务求解循环”。
第 3 层:attempt.ts 是单次执行容器,不是简单的 API 封装
run.ts 每次循环都会调用一次 runEmbeddedAttempt(…),调用点在 src/agents/pi-embedded-runner/run.ts:834。
一个 attempt 不是“发一次 HTTP 请求”那么简单,它是一个完整的执行容器。
它在启动时要做这些事:
-
打开并修复 session。 -
创建 SessionManager。 -
解析 workspace、sandbox、skills、bootstrap files。 -
构建 tools。 -
构建 system prompt。 -
清洗、校验、限制、修复历史 transcript。 -
让 contextEngine.assemble(…) 介入。 -
运行 hooks,改写 prompt / system prompt。 -
加载 prompt-local 图片。 -
订阅流式事件。 -
真正调用 activeSession.prompt(…)。 -
等待 compaction 和后处理收束。 -
调用 afterTurn(…) / ingest(…)。 -
返回结构化结果给 run.ts。
因此你更应该把 attempt 理解成:
一次“从当前会话状态出发,让 agent 尽可能完整地执行任务”的容器。
第 4 层:agent 真正推进复杂任务的 loop,不在 run.ts 明面上,而在 activeSession.prompt(…) 背后
在 src/agents/pi-embedded-runner/run/attempt.ts:1775,真正触发 agent 执行的是:
await activeSession.prompt(effectivePrompt, ...)
这里很多人会误会,看源码时你可能会问:
“我在 attempt.ts 里没看到一个显式 while,那多轮llm调用到底在哪里?”
答案是:
真正的模型 -> 工具 -> tool result -> 模型 的内部执行循环,在底层 session / agent runtime 里,由 activeSession.prompt(…) 驱动。
所以:
-
attempt.ts 是容器和编排层 -
底层 session runtime 才是内部 ReAct-like loop 的执行层
第 5 层:正常情况下,一个复杂任务几乎都“待在同一个 attempt 里”
例如一个代码修复任务:
用户提问-> 搜索文件-> 读代码-> 运行命令-> 修改文件-> 跑测试-> 根据测试结果再修-> 输出最终结论
如果整个过程没有发生需要外层恢复的错误,那么:
-
run.ts 只调用一次 runEmbeddedAttempt(…) -
这整个长链都会在这一 个 attempt 内完成 -
run.ts 直到 attempt 返回之前,基本不参与任务推进
所以可以非常明确地说:
run.ts 不是正常任务推进的主战场;正常推进主要发生在一次 attempt 背后的内部 agent loop 中。
第 6 层:如果 attempt 内部“第 3 轮”出错,外层不会从第 3 轮续跑
6.1 结论
如果一个任务在某次 attempt 内部已经走到了“第 3 轮工具调用之后”,然后异常冒泡到 run.ts,那么:
-
run.ts 不会恢复到“第 3 轮内部断点” -
它也不会把底层模型 loop 从中间接上 -
它会根据 当前已经持久化/保留下来的最新 session 状态,重新发起一次新的 runEmbeddedAttempt(…)
也就是说:
attempt 级重试,而不是 internal round 级续跑。
6.2 为什么不是“从第 3 轮继续”
因为 OpenClaw 当前没有实现“agent 内部 loop 的断点恢复器”。
外层 run.ts 能掌握的是:
-
本次 attempt 结束时的 messagesSnapshot -
最新 session 文件 -
最后一次 assistant / tool / usage / error 信息 -
当前 provider/model/profile/thinking 状态
它掌握不到的,是一个能安全恢复的“内部调用栈断点”。
因此外层能做的最稳妥动作只有:
-
修复上下文或运行条件。 -
从最新可用 transcript 出发。 -
发起一个新的 attempt。
夜雨聆风