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_runtimemain.rs:5287):
每次 run_turn 都基于当前 LiveCli 状态(session、model、system_prompt、allowed_tools、permission_mode)克隆 session 并重新构建一个全新的 ConversationRuntime。这使得重试时可以替换为压缩后的 session 而不影响原始状态。

replace_runtimemain.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(没有消息可压缩了)则提前退出,返回原始错误。

HookAbortMonitormain.rs:5137):
后台线程,监听 Ctrl+C 信号。当用户按 Ctrl+C 时调用 abort_signal.abort(),通知 runtime 中断当前请求。

final_assistant_textmain.rs:9786):
TurnSummaryassistant_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 消费并呈现给用户
posted @ 2026-05-28 16:56  青山見我  阅读(34)  评论(0)    收藏  举报