claw run_turn
run_turn 函数解析
run_turn 有两层实现:
| 层次 | 函数 | 位置 | 职责 |
|---|---|---|---|
| CLI 编排层 | LiveCli::run_turn |
main.rs:5315 | spinner 动画、自动压缩重试、持久化、打印结果 |
| 核心循环层 | Runtime::run_turn |
conversation.rs:318 | LLM API 流式调用、tool_use 循环、工具执行、权限控制、hooks |
run_repl 中普通文本走的是第 5 条路径 cli.run_turn(&trimmed),调用的是 LiveCli::run_turn,它再委托给 Runtime::run_turn。
LiveCli::run_turn(CLI 编排层)
位置: rust/crates/rusty-claude-cli/src/main.rs:5315-5488
函数签名
fn run_turn(&mut self, input: &str) -> Result<(), Box<dyn std::error::Error>>
完整流程
┌─ LiveCli::run_turn(input)
│
│ [准备工作]
│ ├── prepare_turn_runtime(true)
│ │ ├── HookAbortSignal::new() ← 创建 Ctrl+C 中止信号
│ │ ├── build_runtime(session clone, ...) ← 构建 ConversationRuntime
│ │ │ └── 携带: session、model、system_prompt、allowed_tools、permission_mode
│ │ └── HookAbortMonitor::spawn(signal) ← 启动后台线程监听 Ctrl+C
│ │
│ ├── Spinner::new()
│ │
│ ├── spinner.tick("🦀 Thinking...", ...) ← 显示旋转动画
│ │
│ ├── CliPermissionPrompter::new(self.permission_mode) ← 终端用户授权器
│ │
│ ├── runtime.run_turn(input, prompter) ← ★ 委托给核心循环(见下节)
│ │
│ └── hook_abort_monitor.stop() ← 停止 Ctrl+C 监控线程
│
│ [成功分支] ─── Ok(summary)
│ ├── self.replace_runtime(runtime) ← 新 runtime 写回 self
│ │ ├── runtime.shutdown_plugins() ← 关闭旧插件
│ │ └── self.runtime = runtime ← 替换
│ │
│ ├── spinner.finish("✨ Done", ...) ← 停止动画
│ ├── final_assistant_text(&summary) ← 提取最后一条 text block
│ ├── if 非空 → println(final_text) ← 打印助手回复
│ ├── if summary.auto_compaction 有值
│ │ └── format_auto_compaction_notice() ← 打印 "[auto-compacted: removed N messages]"
│ │
│ └── self.persist_session() ← 持久化 session 到磁盘
│
│ [失败分支] ─── Err(error)
│ ├── runtime.shutdown_plugins() ← 关闭插件
│ ├── spinner.fail("❌ Request failed", ...) ← 显示失败动画
│ │
│ └── ★ 自动压缩重试(context window 恢复)
│ │
│ ├── 检测条件:
│ │ error_str.contains("context_window")
│ │ || error_str.contains("Context window")
│ │ || error_str.contains("no parseable body") ← 某些 provider 的隐式超限
│ │
│ └── 渐进式重试循环(最多 4 轮)
│ preserve_schedule = [4, 2, 1, 0]
│ │
│ ├── 打印 "Auto-compacting session (round N/4, preserving M recent)..."
│ │
│ ├── trident_compact_session(runtime.session(), config{preserve})
│ │ └── Trident 压缩: 摘要旧消息,保留最近 N 条
│ │
│ ├── self.runtime.session_mut() = compacted_session ← 关键:替换 session
│ │
│ ├── prepare_turn_runtime(true) ← 重新构建 runtime
│ ├── new_runtime.run_turn(input, ...) ← 重试请求
│ │
│ ├── 重试成功 → persist_session() 后 return Ok(())
│ └── 重试仍失败:
│ ├── 仍是 context_window 且还有轮次 → continue(更激进压缩)
│ └── 不是 context_window 或轮次耗尽 → return Err(最终错误)
│
└──────────────────────────────────────────────────
关键设计点
prepare_turn_runtime(main.rs:5287):
每次 run_turn 都基于当前 LiveCli 状态(session、model、system_prompt、allowed_tools、permission_mode)克隆 session 并重新构建一个全新的 ConversationRuntime。这使得重试时可以替换为压缩后的 session 而不影响原始状态。
replace_runtime(main.rs:5309):
成功后将新 runtime 替换回 self.runtime,同时关闭旧 runtime 的插件。确保状态一致。
自动压缩重试(main.rs:5360-5481):
当 API 返回 context window 溢出时,不直接抛给用户,而是自动渐进式压缩:
| 轮次 | 保留消息数 | 说明 |
|---|---|---|
| 第 1 轮 | 4 条 | 轻度压缩,保留最近的 4 轮对话 |
| 第 2 轮 | 2 条 | 中度压缩 |
| 第 3 轮 | 1 条 | 激进压缩 |
| 第 4 轮 | 0 条 | 极限压缩(仅保留摘要) |
每轮压缩后重新 run_turn,成功则返回。如果某一轮 removed == 0(没有消息可压缩了)则提前退出,返回原始错误。
HookAbortMonitor(main.rs:5137):
后台线程,监听 Ctrl+C 信号。当用户按 Ctrl+C 时调用 abort_signal.abort(),通知 runtime 中断当前请求。
final_assistant_text(main.rs:9786):
从 TurnSummary 的 assistant_messages 中取最后一条消息,提取其中的所有 Text block,拼接后返回。
Runtime::run_turn(核心循环)
位置: rust/crates/runtime/src/conversation.rs:318-524
函数签名
pub fn run_turn(
&mut self,
user_input: impl Into<String>,
mut prompter: Option<&mut dyn PermissionPrompter>,
) -> Result<TurnSummary, RuntimeError>
核心数据结构
pub struct TurnSummary {
pub assistant_messages: Vec<ConversationMessage>, // 本轮所有 LLM 回复
pub tool_results: Vec<ConversationMessage>, // 本轮所有工具执行结果
pub prompt_cache_events: Vec<PromptCacheEvent>, // prompt cache 事件
pub iterations: usize, // 循环迭代次数
pub usage: TokenUsage, // token 用量
pub auto_compaction: Option<AutoCompactionEvent>, // 自动压缩事件
}
完整流程
┌─ Runtime::run_turn(user_input, prompter)
│
│ [阶段 1: 会话健康检查]
│ ├── 如果 session.compaction.is_some()(被压缩过)
│ │ └── run_session_health_probe()
│ │ ├── 空 session + 有 compaction 记录 → Ok(正常情况)
│ │ └── 否则执行 glob_search 探针
│ │ ├── 成功 → Ok
│ │ └── 失败 → Err(会话不一致,提示 /session new)
│ │
│ [阶段 2: 记录输入]
│ ├── record_turn_started(user_input) ← 记录指标
│ ├── session.push_user_text(user_input) ← 用户消息入 session
│ │
│ [阶段 3: 主循环]
│ ├── loop (最多 max_iterations 次)
│ │ │
│ │ ├── 3.1 构建 API 请求
│ │ │ let request = ApiRequest {
│ │ │ system_prompt: self.system_prompt,
│ │ │ messages: self.session.messages, ← 整个消息历史
│ │ │ };
│ │ │
│ │ ├── 3.2 流式调用 LLM API
│ │ │ match self.api_client.stream(request) {
│ │ │ Ok(events) → events, ← AssistantEvent 流
│ │ │ Err(error) → record_failed + return Err
│ │ │ }
│ │ │
│ │ ├── 3.3 构建助手回复
│ │ │ build_assistant_message(events)
│ │ │ │
│ │ │ │ 遍历 AssistantEvent 流:
│ │ │ │ ├── Thinking{thinking, signature} → push 到 blocks
│ │ │ │ ├── TextDelta(delta) → 追加到 text buffer
│ │ │ │ ├── ToolUse{id, name, input} → flush text + push block
│ │ │ │ ├── Usage(value) → 保存用量
│ │ │ │ ├── PromptCache(event) → 记录 cache 事件
│ │ │ │ └── MessageStop → 标记完成
│ │ │ │
│ │ │ │ 检查:
│ │ │ │ ├── 必须有 MessageStop → 否则 Err
│ │ │ │ └── blocks 非空 → 否则 Err
│ │ │ │
│ │ │ └── return (ConversationMessage, TokenUsage, PromptCacheEvents)
│ │ │
│ │ ├── 3.4 记录用量
│ │ │ if let Some(usage) = usage {
│ │ │ self.usage_tracker.record(usage);
│ │ │ }
│ │ │
│ │ ├── 3.5 提取 tool_use
│ │ │ pending_tool_uses = assistant_message.blocks
│ │ │ .filter_map(ContentBlock::ToolUse)
│ │ │ .collect()
│ │ │
│ │ ├── 3.6 记录助手回复到 session
│ │ │ self.record_assistant_iteration(...)
│ │ │ self.session.push_message(assistant_message)
│ │ │
│ │ ├── 3.7 自动压缩检查(ROADMAP #3106)
│ │ │ if self.maybe_auto_compact() {
│ │ │ // cumulative input_tokens > threshold
│ │ │ // → compact_session() 压缩旧消息
│ │ │ // → self.session = compacted_session
│ │ │ // → 记录 AutoCompactionEvent
│ │ │ }
│ │ │
│ │ ├── 3.8 判断是否继续
│ │ │ if pending_tool_uses.is_empty() {
│ │ │ break; ← 没有 tool_use,本轮结束
│ │ │ }
│ │ │
│ │ │ [阶段 4: 工具执行]
│ │ │ └── for (tool_use_id, tool_name, input) in pending_tool_uses {
│ │ │ │
│ │ │ ├── 4.1 PreToolUse Hook
│ │ │ │ let pre_hook_result = self.run_pre_tool_use_hook(&tool_name, &input);
│ │ │ │ let effective_input = pre_hook_result.updated_input()
│ │ │ │ .map_or(input, identity);
│ │ │ │
│ │ │ ├── 4.2 权限检查
│ │ │ │ permission_outcome =
│ │ │ │ if pre_hook_result.is_cancelled()
│ │ │ │ → Deny("hook cancelled tool")
│ │ │ │ else if pre_hook_result.is_failed()
│ │ │ │ → Deny("hook failed for tool")
│ │ │ │ else if pre_hook_result.is_denied()
│ │ │ │ → Deny("hook denied tool")
│ │ │ │ else if let Some(prompt) = prompter
│ │ │ │ → policy.authorize_with_context(..., prompter) ← 终端交互授权
│ │ │ │ else
│ │ │ │ → policy.authorize_with_context(..., None) ← 策略自动决策
│ │ │ │
│ │ │ ├── 4.3 执行工具 或 拒绝
│ │ │ │ match permission_outcome {
│ │ │ │ Allow → {
│ │ │ │ record_tool_started(tool_name);
│ │ │ │ match self.tool_executor.execute(&tool_name, &effective_input) {
│ │ │ │ Ok(output) → (output, false)
│ │ │ │ Err(error) → (error.to_string(), true)
│ │ │ │ }
│ │ │ │ output = merge_hook_feedback(pre_hook_result.messages(), output, false);
│ │ │ │ │
│ │ │ │ ├── PostToolUse Hook
│ │ │ │ │ if is_error → run_post_tool_use_failure_hook()
│ │ │ │ │ else → run_post_tool_use_hook()
│ │ │ │ │ │
│ │ │ │ │ ├── hook denied/failed/cancelled → is_error = true
│ │ │ │ │ └── output = merge_hook_feedback(post_hook_result, output, is_error)
│ │ │ │ │
│ │ │ │ └── → ConversationMessage::tool_result(...)
│ │ │ │ }
│ │ │ │ Deny{reason} → {
│ │ │ │ ConversationMessage::tool_result(
│ │ │ │ tool_use_id, tool_name,
│ │ │ │ merge_hook_feedback(pre_hook_result, reason, true),
│ │ │ │ true ← is_error
│ │ │ │ )
│ │ │ │ }
│ │ │ │ }
│ │ │ │
│ │ │ ├── session.push_message(tool_result) ← 结果写回 session
│ │ │ └── record_tool_finished(...)
│ │ │ }
│ │ │
│ │ └── ──→ 回到 loop 开头,继续下一轮迭代 ──→
│ │
│ [阶段 5: 构建结果]
│ ├── summary = TurnSummary {
│ │ assistant_messages, // 本轮所有助手消息
│ │ tool_results, // 本轮所有工具结果
│ │ prompt_cache_events,
│ │ iterations,
│ │ usage, // 累积用量
│ │ auto_compaction, // 自动压缩事件(如有)
│ │ }
│ │
│ ├── record_turn_completed(&summary) ← 记录指标
│ └── return Ok(summary)
│
└──────────────────────────────────────────────────
主循环关键数据结构
AssistantEvent — LLM API 流式事件枚举:
| 事件 | 含义 |
|---|---|
TextDelta(String) |
文本增量 |
Thinking { thinking, signature } |
思考过程 |
ToolUse { id, name, input } |
工具调用请求 |
Usage(TokenUsage) |
token 用量 |
PromptCache(PromptCacheEvent) |
prompt cache 事件 |
MessageStop |
消息终止信号 |
PermissionOutcome — 工具权限决策结果:
| 变体 | 含义 |
|---|---|
Allow |
允许执行 |
Deny { reason } |
拒绝,附带理由(作为错误结果返回给 LLM) |
关键设计点
会话健康探针(conversation.rs:301):
ROADMAP #38 特性。如果 session 被 compact 过,在下一次 run_turn 时先发送一个无害的 glob_search 探针(匹配不可能存在的文件 *.health-check-probe-*),验证 tool executor 在压缩后仍然正常工作。如果探针失败,返回错误提示用户 /session new。
自动压缩(conversation.rs:564):
每次工具迭代后检查 cumulative_usage().input_tokens 是否超过阈值。如果超过,自动调用 compact_session() 压缩旧消息,防止 session 无限增长导致 context window 溢出。这是运行时主动压缩,区别于上面 LiveCli 层的失败后重试压缩。
工具执行链路:
PreToolUse hook → 权限检查 → ToolExecutor.execute → PostToolUse hook
- PreToolUse hook: 可以在工具执行前修改输入参数、拒绝执行或取消执行
- 权限检查: 4 级决策(hook 取消 > hook 失败 > hook 拒绝 > 用户提示/策略决策)
- PostToolUse hook: 可以在工具执行后修改输出结果、标记为错误
循环终止条件:
- 正常终止:
pending_tool_uses.is_empty()→ break - 超限保护:
iterations > max_iterations→ 报错退出 - API 错误:
api_client.stream()失败 → 报错退出 - 消息构建失败:
build_assistant_message()失败 → 报错退出
一次典型对话的完整调用轨迹
用户输入 "帮我看看main.rs的run_turn函数"
│
├── run_repl 循环捕获输入
│ └── cli.run_turn("帮我看看main.rs的run_turn函数")
│
├── LiveCli::run_turn
│ ├── prepare_turn_runtime(true) ← 构建 Runtime
│ ├── spinner.tick("🦀 Thinking...")
│ └── runtime.run_turn(input, prompter)
│
├── Runtime::run_turn【第一轮迭代】
│ ├── stream(request) → API 流式响应
│ ├── build_assistant_message
│ │ └── 事件流: TextDelta("我来查看...") → ToolUse(read_file) → MessageStop
│ └── pending_tool_uses = [ToolUse(read_file)]
│ ├── PreToolUse hook → 通过
│ ├── 权限检查 → Allow(读文件通常自动允许)
│ ├── tool_executor.execute("read_file", {path: "main.rs"})
│ │ └── Ok("main.rs 内容...")
│ ├── PostToolUse hook → 通过
│ └── tool_result 写回 session
│
├── Runtime::run_turn【第二轮迭代】
│ ├── stream(request) → API 响应(含完整文件内容)
│ ├── build_assistant_message
│ │ └── 事件流: TextDelta("我来解释run_turn函数...") → MessageStop
│ └── pending_tool_uses = [] → break
│
├── Runtime::run_turn 返回 Ok(summary)
│ └── summary = { iterations: 2, assistant_messages: [...], tool_results: [...] }
│
├── LiveCli: replace_runtime(runtime)
├── LiveCli: spinner.finish("✨ Done")
├── LiveCli: println("我来解释run_turn函数...") ← 最终回复
└── LiveCli: persist_session()
其他调用点
Runtime::run_turn 不仅被 LiveCli::run_turn 调用,还被以下函数直接使用:
| 调用者 | 位置 | 用途 |
|---|---|---|
run_prompt_compact |
main.rs:5504 | 非交互式单轮对话(简洁模式) |
run_prompt_compact_json |
main.rs:5517 | 非交互式单轮对话(JSON 输出) |
run_prompt_json |
main.rs:5542 | 非交互式单轮对话(JSON 输出) |
run_internal_prompt_text_with_progress |
main.rs:6444 | 内部 prompt(如 bughunter、commit 等) |
compacts_session_after_turns |
conversation.rs:1398 | 测试辅助 |
状态流转图
┌──────────────┐ run_turn(input) ┌──────────────────┐
│ LiveCli │ ───────────────────────→ │ Runtime │
│ (self) │ │ (conversation) │
│ │ │ │
│ prepare │ build_runtime │ loop: │
│ spinner │ ←────────────────────── │ ├── stream API │
│ persist │ │ ├── parse events │
│ auto-retry │ │ ├── exec tools │
│ │ │ └── check break │
│ │ Ok(TurnSummary) │ │
│ │ ←────────────────────── │ │
│ │ └──────────────────┘
│ replace_rt │
│ persist() │
│ return Ok() │
└──────────────┘
总结
- LiveCli::run_turn 是一个 UI 编排包装层,负责 spinner、持久化、特别是关键的 自动压缩重试 机制(最多 4 轮渐进式压缩后重试 context window 溢出错误)
- Runtime::run_turn 是真正的 LLM 对话引擎,以
请求 → 解析 → 工具执行 → 再请求的循环实现多轮 tool_use 交互,每次迭代都将新消息推入 session,直到 LLM 不再调用工具为止 - 两者通过
TurnSummary结构体解耦:Runtime 产生原始结果,LiveCli 消费并呈现给用户

浙公网安备 33010602011771号