Ironclaw 03 - IronClaw Agent Loop 停止条件拆解
两层循环:先分清「在哪一层停」
IronClaw 的「Agent」至少涉及两层结构:
- 外层事件循环(
agent_loop.rs里Agent::run):从各 Channel 拉消息、处理、回写;与「一次对话里 LLM↔工具」无关。 - 共享 Agentic 内循环(
agentic_loop.rs的run_agentic_loop):单次用户请求(或后台任务)内部的「LLM → 文本或工具 → 再 LLM」迭代。
下面分别说明它们的停止条件。
一、外层 Agent::run:进程级何时结束
在 agent_loop.rs 的主 loop 中,会在以下情况 break,结束整个 Agent 进程级循环:
| 条件 | 行为 |
|---|---|
收到 Ctrl+C(tokio::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_iterations 为 50,但 Chat 路径会覆盖(见下一节)。
三、对话路径 ChatDelegate(dispatcher.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_response以Response结束。- 若仍无法在兜底轮内以「文本返回」结束,则会撞到
MaxIterations(错误里会带Exceeded maximum tool iterations一类说明)。
5. 费用与上下文
call_llm前cost_guard.check_allowed失败 → 错误,不是LoopOutcome。- 上下文超长时会在
ChatDelegate::call_llm内尝试压缩重试;重试仍失败则 Err。
四、同引擎的其它实现(便于对照,不在 dispatcher 文件内)
Scheduler 后台 JobDelegate(src/worker/job.rs)与容器 ContainerDelegate(src/worker/container.rs)也调用 run_agentic_loop,但停止条件不同,例如:
- Job:
check_signals里WorkerMessage::Stop、或 Job 上下文已进入 Cancelled / Failed / Completed / Submitted / Accepted 等终态 →Stopped;文本分支里若llm_signals_completion(text)则mark_completed并以Response结束;否则把 assistant 消息推进上下文Continue(可多轮对话式推进)。 - Container:
check_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.rs 中 impl LoopDelegate for ChatDelegate 四处:check_signals、call_llm、handle_text_response、execute_tool_calls。