Ironclaw 03 - IronClaw Agent Loop 停止条件拆解

两层循环:先分清「在哪一层停」

IronClaw 的「Agent」至少涉及两层结构:

  1. 外层事件循环agent_loop.rsAgent::run):从各 Channel 拉消息、处理、回写;与「一次对话里 LLM↔工具」无关。
  2. 共享 Agentic 内循环agentic_loop.rsrun_agentic_loop):单次用户请求(或后台任务)内部的「LLM → 文本或工具 → 再 LLM」迭代。

下面分别说明它们的停止条件


一、外层 Agent::run:进程级何时结束

agent_loop.rs 的主 loop 中,会在以下情况 break,结束整个 Agent 进程级循环:

条件 行为
收到 Ctrl+Ctokio::signal::ctrl_c 记录日志后 break
消息流结束message_stream.next()None 所有 channel 流结束后退出
handle_message 返回 Ok(None) 例如用户提交 /quit/exit/shutdown(见 submission.rs / thread_ops 一侧约定),视为关机信号

其它情况(正常回复、Ok(Some(empty))、错误)不会因此退出外层循环。


二、共享内循环 run_agentic_loop:通用终止语义

agentic_loop.rs 定义了统一的 LoopOutcome 与信号 LoopSignal。一次内循环会在下列任一情况结束(Ok(LoopOutcome::...)Err):

1. 每轮开头的信号 check_signals()

  • LoopSignal::Stop → 立即返回 LoopOutcome::Stopped(优雅停止)。
  • LoopSignal::InjectMessage(msg) → 把用户消息推进上下文,不退出,继续下一轮。
  • LoopSignal::Continue → 正常进入本轮后续步骤。

2. before_llm_call 提前返回

若 delegate 返回 Some(LoopOutcome),内循环立刻结束,不再调用本轮 LLM。典型用途:各路径自定义的「预算/状态机」提前收尾(Chat 路径里当前实现始终返回 None,不在这里结束)。

3. LLM 调用失败

delegate.call_llm(...) 返回 Err → 整个 run_agentic_loop 向上返回错误(例如费用护栏 cost_guard 拒绝、其它 LLM 错误)。这不是 LoopOutcome,而是错误路径终止。

4. 纯文本分支 RespondResult::Text

  • 若启用 tool intent nudge(且未超 max_tool_intent_nudges、未 force_text、有可用工具),且文本被判定为「说了要用工具却没调工具」→ 注入 nudge、continue不结束
  • 否则进入 handle_text_response
    • TextAction::Return(outcome) → 立即 Ok(outcome),循环结束。
    • TextAction::Continue → 不结束,进入 after_iteration 后继续下一轮。

5. 工具调用分支 RespondResult::ToolCalls

执行 execute_tool_calls

  • 若返回 Some(LoopOutcome) → 立即以该 outcome 结束(例如对话路径的 待审批)。
  • None → 工具结果已写入上下文,通常继续下一轮。

6. 迭代次数上限

for iteration in 1..=config.max_iterations 全部跑完仍无任何上述「提前返回」→ 返回 LoopOutcome::MaxIterations

默认 AgenticLoopConfig::default().max_iterations50,但 Chat 路径会覆盖(见下一节)。


三、对话路径 ChatDelegatedispatcher.rs):实际停在哪

对话使用同一套 run_agentic_loop,但 delegate 行为决定了「什么叫一轮结束」。

1. 中断:ThreadState::Interrupted

check_signals 中若当前线程状态为 Interrupted(例如用户 /interrupt/stop 等把线程标成中断)→ LoopSignal::Stop → 最终 Stopped;上层会把其映射为「Interrupted」类错误信息。

2. 绝大多数情况:第一次非 nudge 的纯文本即结束

handle_text_response 对任意文本(经 strip_internal_tool_call_text 清理后)一律:

TextAction::Return(LoopOutcome::Response(sanitized))

因此:只要模型在某轮给出最终走文本分支(且未被 tool-intent nudge 拐去继续),内循环就以该字符串为结果结束。这与「多轮工具」兼容:多轮里只要最后还是 ToolCalls,就不会走这条;一旦某轮变成纯文本,就结束。

3. 工具路径上的「结束但非普通回复」

execute_tool_calls 里:

  • deferred_auth(工具返回需要用户去扩展里鉴权)→ 返回 Some(LoopOutcome::Response(instructions)),用说明文案当作本轮最终结果,结束内循环
  • NeedApproval(需审批且未自动批准等)→ Some(LoopOutcome::NeedApproval(pending))暂停在审批流;恢复后在别处继续,不是 run_agentic_loop 同一次调用的自然延续。

其它拒绝(hook、非 DM relay 渠道自动拒绝等)会写入 tool 错误消息,通常 None继续迭代。

4. 轮数上限与「强制纯文本」

  • max_tool_iterations 来自 Agent 配置(默认 10,见 config/agent.rs)。
  • loop_config.max_iterations = max_tool_iterations + 1(硬上限多一轮兜底)。
  • nudge_at = max_tool_iterations - 1:接近上限时注入 system 提示要求下一轮给最终答案且不要再调工具。
  • force_text_at = max_tool_iterations:从该轮起 force_text,系统提示切换为无工具版本,迫使模型输出文本;随后仍由 handle_text_responseResponse 结束
  • 若仍无法在兜底轮内以「文本返回」结束,则会撞到 MaxIterations(错误里会带 Exceeded maximum tool iterations 一类说明)。

5. 费用与上下文

  • call_llm cost_guard.check_allowed 失败 → 错误,不是 LoopOutcome
  • 上下文超长时会在 ChatDelegate::call_llm 内尝试压缩重试;重试仍失败则 Err

四、同引擎的其它实现(便于对照,不在 dispatcher 文件内)

Scheduler 后台 JobDelegatesrc/worker/job.rs)与容器 ContainerDelegatesrc/worker/container.rs)也调用 run_agentic_loop,但停止条件不同,例如:

  • Jobcheck_signalsWorkerMessage::Stop、或 Job 上下文已进入 Cancelled / Failed / Completed / Submitted / Accepted 等终态 → Stopped;文本分支里若 llm_signals_completion(text)mark_completed 并以 Response 结束;否则把 assistant 消息推进上下文 Continue(可多轮对话式推进)。
  • Containercheck_signals 恒为 Continue;文本在 llm_signals_completion 时用 last_output 或当前文本 作为 Response 结束;并有 整体 timeout 包在循环外。

这些说明:同一 run_agentic_loop 骨架,停止条件主要由各 LoopDelegate 决定


五、routine_engine.rs:轻量 Routine 的另一套小循环

execute_lightweight_with_tools 没有使用 run_agentic_loop,而是自建 loop

  • max_iterations = min(max_tool_rounds, lightweight_max_iterations, 5)
  • iteration >= max_iterations 时最后一轮 强制纯文本(不调工具),然后根据 handle_text_response 判定 ROUTINE_OK / 空内容 / 需关注内容Routine 专用 状态后 return
  • 在未达上限时:若 LLM 无 tool_calls,直接按文本 return(结束);若有工具则执行完工具后 continue

这是 Routine 专用语义,不要与主对话的 ChatDelegate 混为一谈。


小结表:LoopOutcome 何时出现

结果 常见触发(内循环层面)
Response(String) Chat:首次有效文本回复;或鉴权说明文本。Job:完成信号 + mark_completed。Container:完成信号 + 输出汇总。
Stopped LoopSignal::Stop(Chat:线程 Interrupted;Job:Stop 或终态)。
MaxIterations 迭代用尽未提前 Return
NeedApproval Chat:requires_approval 且进入审批队列(仅对话路径实现)。

若要把行为与代码对齐,优先阅读 agentic_loop.rs 主循环dispatcher.rsimpl LoopDelegate for ChatDelegate 四处:check_signalscall_llmhandle_text_responseexecute_tool_calls