🎒 预备知识:完成上一课「L15」(零基础入口:第 0 课)
while (true) { // 外层:run 的生命周期
let hasMoreToolCalls = true;
while (hasMoreToolCalls || pendingMessages.length > 0) { // 内层:agentic 主循环
if (await stopIfAborted()) { // ← 铁律③的 abort 检查,每轮第一件事
return newMessages;
}
if (!firstTurn) {
await emit({ type: "turn_start" }); // 事件:一轮开始(外部可观测)
turnOpen = true;
} else {
firstTurn = false;
}
if (pendingMessages.length > 0) {
const injectedMessage = await commitPendingMessages(); // steering/排队消息入 transcript
if (!injectedMessage && !hasMoreToolCalls) {
// 整批 drained 消息在落盘前被取消的边界情况…
读法提示:先认内层条件再读体。内层继续的条件是 "还有工具要消化 或 还有排队消息"——注意 `pendingMessages` 也在条件里: 用户在 agent 干活时发的新消息(steering),不是丢掉而是排进下一轮。这就是 A3 说的 "可中断"的温和版本:用户的话永远有位置,但不一定打断当前步。
hasMoreToolCalls =
streamed.continuationRequired || // ①
(message.stopReason === "stop" &&
message.endTurn === false &&
!executedToolBatch?.terminate) || // ②
(executedToolBatch !== undefined && !executedToolBatch?.terminate); // ③
pendingMessages = executedToolBatch?.steeringMessages ?? [];
...
for (const result of terminalToolBatch?.messages ?? []) {
state.context.messages.push(result); // ← 铁律②:tool results 追加进上下文
newMessages.push(result);
}
await emit({ type: "turn_end", message, toolResults });
① continuationRequired:流式层判定"模型还有没说完的工具调用"——
流可能分多段吐 tool calls,这段没完就得继续。
② stop 且 endTurn === false:模型说"stop"但显式标记"话没说完"
(Responses API 的续跑语义)——即使看起来停了也不终止。
③ 批次未终止:刚执行的这批工具没有被钩子/准入标记 terminate——
只要没被叫停且确实有批次在跑,就再给模型一轮。
三项任一为真就继续内层循环;全假才出循环走 followUp 队列 → agent_end。
打开 ~/Code/Projects/openclaw-src/packages/agent-core/src/agent-loop.ts
□ :218 stopIfAborted —— 谁设置这个信号?(往上层找 abort controller)
□ :252 streamAgentResponse —— 回调参数里发生了什么(下一课 L17 主角)
□ :281 providerFailed = stopReason error|aborted —— 终止路径之一
□ :317 三段式(本课已拆,去源码核对每个字段)
□ :327 回填 push(本课已见,找它在流式路径的孪生兄弟)
□ :332 turn_end 事件
□ :373 prepareNextTurn 钩子 —— 谁在这里换模型/思维档
□ :428-439 排空 followUp → agent_end
① abort 检查为什么在循环顶部而不是流式回调里?(提示:回调里中断要处理半成品消息的归属)。
② steering(:414 getSteeringAtCheckpoint)与 followUp 的区别——一个打断当前工具批次,一个排队等下轮;
去源码找出它们分别从哪个队列来。
③ 外层的重试预算(helpers.ts:51-54,24 基数+8/档案,min 32 max 160)在哪个层生效?
为什么内层 agentic 循环反而没有硬步数限制?(提示:内层有 tool-loop 检测兜底。)
坑 1:把外层当内层读。外层管"这次 run 活着"(重试/compaction 后续跑), 内层管"这一步做什么"——在源码里两层都是 while,靠变量名区分,别迷路。
坑 2:忽略 taint。:316 有一行 `turnTainted ||= toolResults.some(toolResultTaintsTurn)`—— 被污染的结果会标记整轮,影响后续重放。现在只需知道存在,L17 讲钩子时回收。
坑 3:跳过事件 emit。emit 是外部世界(Easel 的流式!)看懂 loop 的唯一窗口; 只读控制流不读事件,等于看了引擎没看仪表盘。
合上一切资料,手写 ≤10 行伪代码覆盖双层循环的完整控制流, 必须含至少三个终止路径(abort / providerFailed / 三段式全假)。 写完发我,我逐行对照源码批改。
一手源:agent-loop.ts:214-436 本体;卡住先回 速查表 看调用链图。