Claude Code 源码分析 — 核心对话循环

0 阅读18分钟

本文基于项目实际源码,深入分析 Claude Code CLI 从用户提交输入到一轮完整对话结束的核心对话循环。涵盖输入分发、并发控制、上下文加载、API 流式调用、工具执行、错误恢复、中止处理与循环续接的完整链路。

一、总体流程概览

Claude Code 的核心对话循环从用户在 REPL 提交输入开始,经过多层处理和分发,最终进入一个 while(true) 驱动的 API 调用-工具执行-续行循环。完整路径为:

flowchart TD
    A[用户输入] --> B["onSubmit (REPL.tsx)"]
    B -->|即时命令拦截 / 空闲检测| C["handlePromptSubmit (handlePromptSubmit.ts)"]
    C -->|五路分发| D[executeUserInput]
    D -->|"processUserInput 循环 + queryGuard.reserve()"| E["onQuery (REPL.tsx)"]
    E -->|"queryGuard.tryStart() 并发控制"| F["onQueryImpl (REPL.tsx)"]
    F -->|"Promise.all 加载上下文"| G["for await (query(...))"]
    G --> H["query() → queryLoop() (query.ts)"]

    H --> I["while(true)"]
    I --> J["预查询管线 (5 阶段压缩)"]
    J --> K["API 流式调用 (deps.callModel)"]
    K --> L{"错误恢复 / 中止?"}
    L -->|"6 种恢复转换"| I
    L -->|"2 条中止路径"| R
    L -->|正常| M["工具执行 (StreamingToolExecutor)"]
    M --> N["循环续接管线<br/>附件/记忆/技能/工具刷新"]
    N --> O{maxTurns 检查}
    O -->|"未达上限: state = next; continue"| I
    O -->|达到上限| R

    subgraph 循环退出
        R["return { reason }"]
    end

    style I fill:#f9f,stroke:#333,stroke-width:2px
    style R fill:#bbf,stroke:#333,stroke-width:2px

以下逐层拆解每个阶段的源码实现。

二、REPL.tsx onSubmit — 用户输入入口

源码位置:src/screens/REPL.tsx:3723

onSubmitPromptInput 组件的回调,是所有用户输入的第一个处理点。它接收四个参数:

// src/screens/REPL.tsx:3723
const onSubmit = useCallback(
  async (
    input: string,
    helpers: PromptInputHelpers,
    speculationAccept?: {
      state: ActiveSpeculationState;
      speculationSessionTimeSavedMs: number;
      setAppState: SetAppState;
    },
    options?: { fromKeybinding?: boolean },
  ) => {

进入后首先执行两个固定操作:

// src/screens/REPL.tsx:3736
repinScroll(); // 重新锁定滚动到底部

if (feature('PROACTIVE') || feature('KAIROS')) {
  proactiveModule?.resumeProactive(); // 恢复主动模式
}

2.1 即时命令拦截

当输入以 / 开头且不是推测接受 (speculationAccept) 时,onSubmit 尝试匹配即时命令:

// src/screens/REPL.tsx:3746
if (!speculationAccept && input.trim().startsWith('/')) {
  const trimmedInput = expandPastedTextRefs(input, pastedContents).trim();
  const spaceIndex = trimmedInput.indexOf(' ');
  const commandName = spaceIndex === -1 ? trimmedInput.slice(1) : trimmedInput.slice(1, spaceIndex);

  const matchingCommand = commands.find(
    cmd =>
      isCommandEnabled(cmd) &&
      (cmd.name === commandName || cmd.aliases?.includes(commandName) || getCommandName(cmd) === commandName),
  );

  const shouldTreatAsImmediate = queryGuard.isActive && (matchingCommand?.immediate || options?.fromKeybinding);

判定为即时命令需要满足两个条件:

  1. queryGuard.isActive — 当前有查询正在执行
  2. 命令标记了 immediate: true,或由快捷键触发 (fromKeybinding)

一次用户与Claude Code的对话,在Claude Code内部是用的Query,本文翻译成了查询,虽然我感觉查询也不太准确,但没有找到合适的词,所以保留查询的翻译.

匹配到即时命令后,如果类型是 local-jsx,则直接加载并执行,不进入查询队列

// src/screens/REPL.tsx:3776
if (matchingCommand && shouldTreatAsImmediate && matchingCommand.type === 'local-jsx') {
  // 清理输入框 ...
  const executeImmediateCommand = async (): Promise<void> => {
    const context = getToolUseContext(messagesRef.current, [], createAbortController(), mainLoopModel);
    const mod = await matchingCommand.load();
    const jsx = await mod.call(onDone, context, commandArgs);
    if (jsx && !doneWasCalled) {
      setToolJSX({ jsx, shouldHidePromptInput: false, isLocalJSXCommand: true });
    }
  };
  void executeImmediateCommand();
  return; // 始终提前返回
}

2.2 远程模式与空闲检测

即时命令检查之后,onSubmit 处理两个特殊场景:

远程模式空输入过滤

// src/screens/REPL.tsx:3884
if (activeRemote.isRemoteMode && !input.trim()) {
  return;
}

空闲回归检测 — 当用户长时间离开后返回,根据 willowMode 配置决定处理方式:

  • 'dialog':显示阻塞式对话框
  • 'hint':显示提示通知
  • 'off':无操作

判定条件包括空闲时间超过阈值(默认 75 分钟)且累计 token 数超过阈值(默认 100k)。

claude-code源码分析-核心对话循环_2026-04-11-16-48-46.png

2.3 委托 handlePromptSubmit

通过前述检查后,onSubmit 将控制权委托给 handlePromptSubmit

onSubmit
  ├─ 即时命令 → executeImmediateCommand() → return
  ├─ 远程模式空输入 → return
  ├─ 空闲回归 → dialog/hint/pass
  └─ 正常路径 → handlePromptSubmit(...)

三、handlePromptSubmit — 五路分发

源码位置:src/utils/handlePromptSubmit.ts:120

handlePromptSubmit 是输入处理的核心分发器,有五条互斥路径:

handlePromptSubmit(params)
  │
  ├─① queuedCommands?.lengthexecuteUserInput() [直接执行预验证命令]
  │
  ├─② ['exit','quit',':q',...]handlePromptSubmit({input:'/exit'}) [递归转换]
  │
  ├─③ queryGuard.isActive + immediate command → load() + call() [即时命令]
  │
  ├─④ queryGuard.isActive || isExternalLoading → enqueue() [入队等待]
  │
  └─⑤ 默认路径 → executeUserInput([cmd]) [直接执行]

3.1 队列处理器快速路径

queuedCommands 已经存在时(来自队列处理器 useQueueProcessor),命令已经过预验证,直接进入执行:

// src/utils/handlePromptSubmit.ts:150
if (queuedCommands?.length) {
  startQueryProfile()
  await executeUserInput({
    queuedCommands,
    messages,
    mainLoopModel,
    // ... 其他参数
  })
  return
}

这一步一般发生在对话已经进行时输入一些命令时,Claude Code不会直接执行,而是被enqueue()进队列(参考3.4),等上一轮对话完成后才会从队列移出并执行executeQueuedInput。因为在进队列前已经做了必要的判断,所以立即执行,避免重复判断。

3.2 退出命令检测

识别多种退出命令格式,统一转换为 /exit 斜杠命令处理:

// src/utils/handlePromptSubmit.ts:194
if (
  !skipSlashCommands &&
  ['exit', 'quit', ':q', ':q!', ':wq', ':wq!'].includes(input.trim())
) {
  const exitCommand = commands.find(cmd => cmd.name === 'exit')
  if (exitCommand) {
    void handlePromptSubmit({ ...params, input: '/exit' }) // 递归调用
  } else {
    exit() // 降级直接退出
  }
  return
}

3.3 忙碌时的即时命令

当系统正在处理查询(queryGuard.isActive || isExternalLoading)且匹配到 immediate: truelocal-jsx 命令时,直接执行而不入队:

// src/utils/handlePromptSubmit.ts:248
if (
  immediateCommand &&
  immediateCommand.type === 'local-jsx' &&
  (queryGuard.isActive || isExternalLoading)
) {
  const impl = await immediateCommand.load()
  const jsx = await impl.call(onDone, context, commandArgs)
  if (jsx && !doneWasCalled) {
    setToolJSX({ jsx, shouldHidePromptInput: false, isLocalJSXCommand: true, isImmediate: true })
  }
  return
}

3.4 忙碌时入队

当查询正在运行且输入不是即时命令时,将输入入队等待:

// src/utils/handlePromptSubmit.ts:313
if (queryGuard.isActive || isExternalLoading) {
  if (mode !== 'prompt' && mode !== 'bash') { return }

  // 如果当前工具支持中断,发送中止信号
  if (params.hasInterruptibleToolInProgress) {
    params.abortController?.abort('interrupt')
  }

  enqueue({
    value: finalInput.trim(),
    preExpansionValue: input.trim(),
    mode,
    pastedContents: hasImages ? pastedContents : undefined,
    skipSlashCommands,
    uuid,
  })
  // 清理输入框 ...
  return
}

3.5 直接执行

空闲状态下的默认路径,将输入包装为 QueuedCommand 后调用 executeUserInput

// src/utils/handlePromptSubmit.ts:353
startQueryProfile()
const cmd: QueuedCommand = {
  value: finalInput,
  preExpansionValue: input,
  mode,
  pastedContents: hasImages ? pastedContents : undefined,
  skipSlashCommands,
  uuid,
}
await executeUserInput({
  queuedCommands: [cmd],
  messages,
  mainLoopModel,
  // ... 其他参数
})

四、executeUserInput — 输入处理与查询触发

源码位置:src/utils/handlePromptSubmit.ts:396

executeUserInput 是连接用户输入处理和查询系统的桥梁。它的核心职责:

  1. 创建 AbortController
  2. 预留 queryGuardreserve() 将状态从 idle 转为 dispatching)
  3. 循环处理所有 queuedCommands
  4. 调用 onQuery 触发查询
// src/utils/handlePromptSubmit.ts:419
const abortController = createAbortController()
setAbortController(abortController)

try {
  queryGuard.reserve()  // idle → dispatching
  
  const newMessages: Message[] = []
  let shouldQuery = false
  // ...

  await runWithWorkload(turnWorkload, async () => {
    for (let i = 0; i < commands.length; i++) {
      const cmd = commands[i]!
      const isFirst = i === 0
      const result = await processUserInput({
        input: cmd.value,
        // 第一条命令获完整处理(附件、IDE选区、粘贴内容)
        // 后续命令跳过附件避免重复
        skipAttachments: !isFirst,
        // ...
      })
      newMessages.push(...result.messages)
      if (isFirst) {
        shouldQuery = result.shouldQuery
        allowedTools = result.allowedTools
        model = result.model
        effort = result.effort
      }
    }

    // 文件历史快照
    if (fileHistoryEnabled()) {
      newMessages.filter(selectableUserMessagesFilter).forEach(message => {
        void fileHistoryMakeSnapshot(/* ... */)
      })
    }

    if (newMessages.length) {
      await onQuery(
        newMessages,
        abortController,
        shouldQuery,
        allowedTools ?? [],
        model ? resolveSkillModelOverride(model, mainLoopModel) : mainLoopModel,
        shouldCallBeforeQuery ? onBeforeQuery : undefined,
        primaryInput,
        effort,
      )
    }
  })
} finally {
  queryGuard.cancelReservation()  // 安全网:释放 dispatching 状态
  setUserInputOnProcessing(undefined)
}

processUserInput 的作用是把原始用户输入转换为可以发送给 API 的结构化消息数组,同时决定是否需要发起 API 调用。

命令处理的关键设计:第一条命令得到完整处理(包括附件、IDE 选区、粘贴内容),后续命令跳过附件以避免重复注入上下文。

五、onQuery — 并发控制与状态初始化

源码位置:src/screens/REPL.tsx:3382

5.1 QueryGuard 状态机

onQuery 的第一件事是通过 queryGuard.tryStart() 进行并发控制。QueryGuard 维护一个简单的状态机:

idle → dispatching → running   →   idle
         ↑ reserve()  ↑ tryStart()  ↑ end()

tryStart() 原子地检查并转换 dispatching → running,返回一个单调递增的 generation 号。如果已经处于 running 状态,返回 null

5.2 并发冲突处理

tryStart() 返回 null 时,说明已有查询在运行。此时将用户消息入队而非丢弃:

// src/screens/REPL.tsx:3406
const thisGeneration = queryGuard.tryStart();
if (thisGeneration === null) {
  logEvent('tengu_concurrent_onquery_detected', {});
  // 提取并入队用户消息文本,跳过 meta 消息
  newMessages
    .filter((m): m is UserMessage => m.type === 'user' && !m.isMeta)
    .map(_ => getContentText(_.message.content))
    .filter(_ => _ !== null)
    .forEach((msg, i) => {
      enqueue({ value: msg, mode: 'prompt' });
    });
  return;
}

成功获取 guard 后,初始化各类状态:

// src/screens/REPL.tsx:3429
try {
  resetTimingRefs();
  setMessages(oldMessages => [...oldMessages, ...newMessages]);
  responseLengthRef.current = 0;
  if (feature('TOKEN_BUDGET')) {
    const parsedBudget = input ? parseTokenBudget(input) : null;
    snapshotOutputTokensForTurn(parsedBudget ?? getCurrentTurnTokenBudget());
  }
  apiMetricsRef.current = [];
  setStreamingToolUses([]);
  setStreamingText(null);
  const latestMessages = messagesRef.current;
  // ...
  await onQueryImpl(latestMessages, newMessages, abortController, shouldQuery, ...);

5.3 清理与循环完成

onQueryfinally 块通过 queryGuard.end(thisGeneration) 原子地检查 generation 并转换 running → idle。如果 generation 不匹配(取消+重提交竞态),返回 false 跳过清理:

// src/screens/REPL.tsx:3468
finally {
  if (queryGuard.end(thisGeneration)) {
    setLastQueryCompletionTime(Date.now());
    resetLoadingState();
    await mrOnTurnComplete(messagesRef.current, abortController.signal.aborted);
    sendBridgeResultRef.current(); // 通知 bridge 客户端

    // Token budget 捕获
    if (feature('TOKEN_BUDGET')) { /* ... */ }

    // 超过 30s 的轮次显示耗时消息
    const turnDurationMs = Date.now() - loadingStartTimeRef.current - totalPausedMsRef.current;
    if ((turnDurationMs > 30000 || budgetInfo !== undefined) && !abortController.signal.aborted) {
      setMessages(prev => [...prev, createTurnDurationMessage(turnDurationMs, budgetInfo, ...)]);
    }

    setAbortController(null); // 清理 controller
  }
}

六、onQueryImpl — 上下文加载与查询发起

源码位置:src/screens/REPL.tsx:3120

onQueryImpl 是实际执行查询的核心函数。

6.1 并行上下文加载

通过 Promise.all 并行加载多个上下文源,最小化延迟:

// src/screens/REPL.tsx:3253
const [, , defaultSystemPrompt, baseUserContext, systemContext] = await Promise.all([
  checkAndDisableBypassPermissionsIfNeeded(toolPermissionContext, setAppState),
  feature('TRANSCRIPT_CLASSIFIER')
    ? checkAndDisableAutoModeIfNeeded(...)
    : undefined,
  getSystemPrompt(freshTools, mainLoopModelParam, ..., freshMcpClients),   // 系统提示词
  getUserContext(),       // 用户上下文 (CLAUDE.md, 内存文件等)
  getSystemContext(),     // 系统上下文 (git status, 日期等)
]);

加载完成后,构建最终生效的系统提示词:

// src/screens/REPL.tsx:3282
const systemPrompt = buildEffectiveSystemPrompt({
  mainThreadAgentDefinition,
  toolUseContext,
  customSystemPrompt,
  defaultSystemPrompt,
  appendSystemPrompt,
});

6.2 查询执行与事件消费

核心查询通过 for await 消费 query() 生成器产生的事件流:

// src/screens/REPL.tsx:3296
for await (const event of query({
  messages: messagesIncludingNewMessages,
  systemPrompt,
  userContext,
  systemContext,
  canUseTool,
  toolUseContext,
  querySource: getQuerySourceForREPL(),
})) {
  onQueryEvent(event);  // 分发给 UI 层处理
}

onQueryEvent 根据事件类型分发到不同的 UI 更新路径:消息追加、流式文本更新、工具调用状态等。

查询完成后执行收尾工作:

// src/screens/REPL.tsx:3308
// Companion 观察者(BUDDY 功能)
if (feature('BUDDY') && typeof fireCompanionObserver === 'function') {
  void fireCompanionObserver(messagesRef.current, ...);
}

// API 指标捕获(ant-only)
if (process.env.USER_TYPE === 'ant' && apiMetricsRef.current.length > 0) {
  // 计算 TTFT、OTPS 指标 ...
  setMessages(prev => [...prev, createApiMetricsMessage(...)]);
}

resetLoadingState();
await onTurnComplete?.(messagesRef.current);

七、query() → queryLoop() — 核心对话引擎

源码位置:src/query.ts:219

7.1 State 类型与可变状态

queryLoop 的核心是一个可变的 State 对象,在循环迭代间传递状态:

// src/query.ts:204
type State = {
  messages: Message[]                           // 消息历史
  toolUseContext: ToolUseContext                 // 工具执行上下文
  autoCompactTracking: AutoCompactTrackingState | undefined  // 自动压缩跟踪
  maxOutputTokensRecoveryCount: number          // 输出限制恢复计数
  hasAttemptedReactiveCompact: boolean          // 是否已尝试反应式压缩
  maxOutputTokensOverride: number | undefined   // 输出限制覆盖值
  pendingToolUseSummary: Promise<ToolUseSummaryMessage | null> | undefined  // 待处理工具摘要
  stopHookActive: boolean | undefined           // 停止钩子状态
  turnCount: number                             // 当前循环计数
  transition: Continue | undefined              // 上次迭代的转换原因
}

初始化时设定默认值:

// src/query.ts:268
let state: State = {
  messages: params.messages,
  toolUseContext: params.toolUseContext,
  maxOutputTokensOverride: params.maxOutputTokensOverride,
  autoCompactTracking: undefined,
  stopHookActive: undefined,
  maxOutputTokensRecoveryCount: 0,
  hasAttemptedReactiveCompact: false,
  turnCount: 1,
  pendingToolUseSummary: undefined,
  transition: undefined,
}

同时创建两个循环级别的资源:

// src/query.ts:280
const budgetTracker = feature('TOKEN_BUDGET') ? createBudgetTracker() : null

// using 关键字确保在生成器所有退出路径上自动 dispose
using pendingMemoryPrefetch = startRelevantMemoryPrefetch(
  state.messages,
  state.toolUseContext,
)

7.2 while(true) 主循环结构

queryLoop 的主体是一个 while(true) 无限循环,每次迭代构成一个完整的"查询-执行-决策"周期:

flowchart TD
    A["1. 解构 state,初始化迭代变量"] --> B["2. 预查询管线(5 阶段压缩)"]
    B --> C["3. API 流式调用 + 流内工具并发"]
    C --> C1{中止?}
    C1 -->|是| R1["return 'aborted_streaming'"]
    C1 -->|否| C2{错误?}
    C2 -->|是| R2["return 'model_error'"]
    C2 -->|否| D{needsFollowUp?}
    D -->|false| E{恢复检查}
    E -->|"6 种恢复转换"| A
    E -->|无需恢复| R3["return 'completed'"]
    D -->|true| F["5. 工具执行"]
    F --> F1{中止?}
    F1 -->|是| R4["return 'aborted_tools'"]
    F1 -->|否| F2{hook 阻止?}
    F2 -->|是| R5["return 'hook_stopped'"]
    F2 -->|否| G["6. 下轮循环续接管线<br/>附件/记忆/技能/工具刷新"]
    G --> H{maxTurns 检查}
    H -->|达到上限| R6["return 'max_turns'"]
    H -->|未达上限| A

八、预查询管线 — 五阶段上下文压缩

在每次 API 调用之前,消息历史经过五个阶段的处理以控制上下文窗口大小。

8.1 applyToolResultBudget — 工具结果预算

源码位置:src/query.ts:379

对聚合工具结果的总大小施加预算限制,替换超出预算的内容:

messagesForQuery = await applyToolResultBudget(
  messagesForQuery,
  toolUseContext.contentReplacementState,
  persistReplacements ? records => void recordContentReplacement(...) : undefined,
  new Set(toolUseContext.options.tools.filter(t => !Number.isFinite(t.maxResultSizeChars)).map(t => t.name)),
)

运行在 microcompact 之前,因为缓存的 MC 通过 tool_use_id 操作而不检查内容,两者可以干净地组合。

8.2 snipCompactIfNeeded — 片段式压缩

feature flag: HISTORY_SNIP

基于片段的压缩策略,保留消息结构但截断过长的内容。

8.3 microcompact — 微压缩

源码位置:src/query.ts:414

细粒度压缩,支持缓存编辑以避免缓存失效:

const mcResult = await deps.microcompact(
  messagesForQuery,
  toolUseContext,
  querySource,
)
messagesForQuery = mcResult.messages

8.4 contextCollapse — 上下文折叠

feature flag: CONTEXT_COLLAPSE

读取时投影:不修改原始消息,而是在查询时动态折叠上下文:

messagesForQuery = contextCollapse.applyCollapsesIfNeeded(
  messagesForQuery,
  querySource,
)

8.5 autocompact — 自动压缩

源码位置:src/query.ts:454

当上下文接近限制时触发的全量压缩,使用系统提示词和用户上下文生成摘要:

const compactResult = await deps.autocompact(
  messagesForQuery,
  systemPrompt,
  userContext,
  toolUseContext,
  tracking,
  // ...
)

五个阶段按严格顺序执行,每个阶段的输出作为下一阶段的输入。

九、API 流式调用与工具并发执行

9.1 deps.callModel() 流式调用

源码位置:src/query.ts:659

API 调用通过 deps.callModel() 发起,返回一个 AsyncGenerator 产生 BetaRawMessageStreamEvent 事件流:

// src/query.ts:659
for await (const message of deps.callModel({
  messages: prependUserContext(messagesForQuery, userContext),
  systemPrompt: fullSystemPrompt,
  thinkingConfig: toolUseContext.options.thinkingConfig,
  tools: toolUseContext.options.tools,
  signal: toolUseContext.abortController.signal,
  options: {
    model: currentModel,
    fallbackModel,
    maxOutputTokensOverride,
    taskBudget: params.taskBudget && {
      total: params.taskBudget.total,
      ...(taskBudgetRemaining !== undefined && { remaining: taskBudgetRemaining }),
    },
    // ... 更多选项
  },
})) {

9.2 StreamingToolExecutor 并发执行

StreamingToolExecutor 实现了流内工具并发执行——在模型仍在生成后续内容时就开始执行已完成的工具调用:

// 模型流产生 tool_use block 时立即喂入执行器
if (streamingToolExecutor && !toolUseContext.abortController.signal.aborted) {
  for (const toolBlock of msgToolUseBlocks) {
    streamingToolExecutor.addTool(toolBlock, assistantMessage); // 开始执行
  }
}

// 收集已完成的结果(非阻塞)
if (streamingToolExecutor && !toolUseContext.abortController.signal.aborted) {
  for (const result of streamingToolExecutor.getCompletedResults()) {
    if (result.message) {
      yield result.message;
      toolResults.push(...normalizeMessagesForAPI([result.message], ...));
    }
  }
}

这种设计使得工具执行可以与模型的流式输出重叠,显著减少总循环时间。

9.3 流式回退与错误扣留

流式回退(Streaming Fallback):当模型在流式传输中降级到备用模型时,需要清理已产生的孤立消息:

// src/query.ts:712
if (streamingFallbackOccured) {
  // 为孤立消息生成墓碑消息(从 UI 和记录中移除)
  for (const msg of assistantMessages) {
    yield { type: 'tombstone' as const, message: msg }
  }
  assistantMessages.length = 0
  toolResults.length = 0
  toolUseBlocks.length = 0
  needsFollowUp = false

  // 丢弃旧执行器的待处理结果,创建新的
  if (streamingToolExecutor) {
    streamingToolExecutor.discard()
    streamingToolExecutor = new StreamingToolExecutor(...)
  }
}

错误扣留(Withheld Errors):某些可恢复的错误(prompt-too-long、media-size、max-output-tokens)在流式传输期间被扣留而不是立即 yield,等待后续恢复逻辑决定是否可以自动修复:

// src/query.ts:801
let withheld = false
if (feature('CONTEXT_COLLAPSE')) {
  if (contextCollapse?.isWithheldPromptTooLong(message as Message, isPromptTooLongMessage, querySource)) {
    withheld = true
  }
}
if (reactiveCompact?.isWithheldPromptTooLong(message as Message)) {
  withheld = true
}
if (isWithheldMaxOutputTokens(message)) {
  withheld = true
}
if (!withheld) {
  yield yieldMessage  // 只有非扣留消息才 yield 给上层
}

被扣留的错误仍然被 push 到 assistantMessages 数组中,供后续恢复检查使用。

十、错误处理与模型回退

10.1 FallbackTriggeredError — 模型降级

源码位置:src/query.ts:897

当主模型因高负载不可用时,抛出 FallbackTriggeredError。处理逻辑:

目前从源码中没找到哪里可以设置fallbackModel,官方文档也没找到设置的方式,可能是个还没对外的feature

// src/query.ts:897
catch (innerError) {
  if (innerError instanceof FallbackTriggeredError && fallbackModel) {
    currentModel = fallbackModel  // 切换到降级模型
    attemptWithFallback = true    // 标记重试

    // 清理孤立消息
    yield* yieldMissingToolResultBlocks(assistantMessages, 'Model fallback triggered')
    assistantMessages.length = 0
    toolResults.length = 0

    // 丢弃旧执行器
    if (streamingToolExecutor) {
      streamingToolExecutor.discard()
      streamingToolExecutor = new StreamingToolExecutor(...)
    }

    // 更新上下文中的模型
    toolUseContext.options.mainLoopModel = fallbackModel

    // 剥离签名块(thinking 签名与模型绑定)
    if (process.env.USER_TYPE === 'ant') {
      messagesForQuery = stripSignatureBlocks(messagesForQuery)
    }

    // 向用户显示降级通知
    yield createSystemMessage(
      `Switched to ${renderModelName(innerError.fallbackModel)} due to high demand for ${renderModelName(innerError.originalModel)}`,
      'warning',
    )
    continue  // 重试 API 调用
  }
  throw innerError
}

10.2 通用错误处理

外层 catch 处理所有未被内层捕获的错误:

// src/query.ts:958
catch (error) {
  logError(error)

  // 图片大小/调整错误的特殊处理
  if (error instanceof ImageSizeError || error instanceof ImageResizeError) {
    yield createAssistantAPIErrorMessage({ content: error.message })
    return { reason: 'image_error' }
  }

  // 为孤立的 tool_use 块补充合成 tool_result
  yield* yieldMissingToolResultBlocks(assistantMessages, errorMessage)

  // 显示真实错误而非误导性的"用户中断"消息
  yield createAssistantAPIErrorMessage({ content: errorMessage })
  return { reason: 'model_error', error }
}

yieldMissingToolResultBlocks 确保每个 tool_use 块都有对应的 tool_result,防止后续 API 调用因消息格式不完整而报错。

十一、六种恢复转换

当模型响应不需要后续工具调用(!needsFollowUp)时,进入恢复检查逻辑。六种恢复转换形成一个优先级递减的级联结构:

11.1 collapse_drain_retry — 上下文折叠压缩

源码位置:src/query.ts:1092 触发条件:prompt-too-long 错误 + CONTEXT_COLLAPSE 功能启用 + 上次转换不是此类型

当出现模型API返回413表面超出最大token时,将所有已暂存的上下文折叠,这是成本最低的恢复手段——保留细粒度上下文:

if (feature('CONTEXT_COLLAPSE') && contextCollapse && state.transition?.reason !== 'collapse_drain_retry') {
  const drained = contextCollapse.recoverFromOverflow(messagesForQuery, querySource)
  if (drained.committed > 0) {
    state = { ...state, messages: drained.messages, transition: { reason: 'collapse_drain_retry', committed: drained.committed } }
    continue
  }
}

单次触发保护——如果压缩后重试仍然 413,将 fall through 到反应式压缩。

11.2 reactive_compact_retry — 反应式压缩

源码位置:src/query.ts:1122 触发条件:prompt-too-long 或 media-size 错误 + reactiveCompact 可用 + 未尝试过

全量反应式压缩,生成消息历史的完整摘要:

if ((isWithheld413 || isWithheldMedia) && reactiveCompact) {
  const compacted = await reactiveCompact.tryReactiveCompact({
    hasAttempted: hasAttemptedReactiveCompact,
    messages: messagesForQuery,
    // ...
  })
  if (compacted) {
    state = { ...state, messages: buildPostCompactMessages(compacted), hasAttemptedReactiveCompact: true, transition: { reason: 'reactive_compact_retry' } }
    continue
  }
  // 恢复失败 → 显示扣留的错误并退出
  yield lastMessage
  return { reason: isWithheldMedia ? 'image_error' : 'prompt_too_long' }
}

11.3 max_output_tokens_escalate — 输出限制升级

源码位置:src/query.ts:1191 触发条件:max-output-tokens 错误 + maxOutputTokensOverride === undefined

当模型使用默认的 8k 输出限制触顶时,升级到 64k (ESCALATED_MAX_TOKENS) 重试同一请求——无需注入 meta 消息:

if (capEnabled && maxOutputTokensOverride === undefined && !process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS) {
  state = { ...state, maxOutputTokensOverride: ESCALATED_MAX_TOKENS, transition: { reason: 'max_output_tokens_escalate' } }
  continue
}

单次触发——如果 64k 也触顶,fall through 到多轮恢复。

11.4 max_output_tokens_recovery — 输出限制恢复

源码位置:src/query.ts:1226 触发条件:max-output-tokens 错误 + 恢复次数 < MAX_OUTPUT_TOKENS_RECOVERY_LIMIT (3)

注入一条 meta 用户消息要求模型从断点续写:

if (maxOutputTokensRecoveryCount < MAX_OUTPUT_TOKENS_RECOVERY_LIMIT) {
  const recoveryMessage = createUserMessage({
    content: `Output token limit hit. Resume directly — no apology, no recap of what you were doing. ` +
      `Pick up mid-thought if that is where the cut happened. Break remaining work into smaller pieces.`,
    isMeta: true,
  })
  state = {
    ...state,
    messages: [...messagesForQuery, ...assistantMessages, recoveryMessage],
    maxOutputTokensRecoveryCount: maxOutputTokensRecoveryCount + 1,
    transition: { reason: 'max_output_tokens_recovery', attempt: maxOutputTokensRecoveryCount + 1 },
  }
  continue
}

最多重试 3 次,超出后显示扣留的错误消息。

11.5 stop_hook_blocking — 停止钩子阻塞

源码位置:src/query.ts:1270 触发条件:handleStopHooks() 返回阻塞错误

停止钩子(用户配置的 shell 命令)在模型响应完成后执行。如果钩子返回阻塞错误,将错误消息注入对话并重试:

const stopHookResult = yield* handleStopHooks(
  messagesForQuery, assistantMessages, systemPrompt, userContext, systemContext, toolUseContext, querySource, stopHookActive,
)
if (stopHookResult.blockingErrors.length > 0) {
  state = {
    ...state,
    messages: [...messagesForQuery, ...assistantMessages, ...stopHookResult.blockingErrors],
    stopHookActive: true,
    hasAttemptedReactiveCompact,  // 保留压缩保护
    transition: { reason: 'stop_hook_blocking' },
  }
  continue
}

注意 hasAttemptedReactiveCompact 被保留而非重置——防止"压缩 → 仍然太长 → 停止钩子阻塞 → 压缩 → ..."的无限循环。

11.6 token_budget_continuation — 令牌预算续行

源码位置:src/query.ts:1311 feature flag: TOKEN_BUDGET 触发条件:checkTokenBudget() 返回 action === 'continue'

当启用令牌预算且模型尚未用完预算时,注入 nudge 消息继续执行:

if (feature('TOKEN_BUDGET')) {
  const decision = checkTokenBudget(budgetTracker!, toolUseContext.agentId, getCurrentTurnTokenBudget(), getTurnOutputTokens())
  if (decision.action === 'continue') {
    incrementBudgetContinuationCount()
    state = {
      ...state,
      messages: [...messagesForQuery, ...assistantMessages, createUserMessage({ content: decision.nudgeMessage, isMeta: true })],
      transition: { reason: 'token_budget_continuation' },
    }
    continue
  }
}

如果没有任何恢复条件命中,返回正常完成:

return { reason: 'completed' }

十二、中止处理 — 两条中断路径

用户可以在两个阶段中止对话:流式传输阶段和工具执行阶段。

12.1 流式阶段中止

源码位置:src/query.ts:1018

流式 API 调用完成后立即检查中止信号:

// src/query.ts:1018
if (toolUseContext.abortController.signal.aborted) {
  if (streamingToolExecutor) {
    // 消费剩余结果 — 执行器为已排队/进行中的工具生成合成 tool_result
    for await (const update of streamingToolExecutor.getRemainingResults()) {
      if (update.message) { yield update.message }
    }
  } else {
    yield* yieldMissingToolResultBlocks(assistantMessages, 'Interrupted by user')
  }

  // Computer Use 清理(CHICAGO_MCP 功能)
  if (feature('CHICAGO_MCP') && !toolUseContext.agentId) {
    const { cleanupComputerUseAfterTurn } = await import('./utils/computerUse/cleanup.js')
    await cleanupComputerUseAfterTurn(toolUseContext)
  }

  // 对于 submit-interrupt(输入中断),跳过中断消息
  if (toolUseContext.abortController.signal.reason !== 'interrupt') {
    yield createUserInterruptionMessage({ toolUse: false })
  }
  return { reason: 'aborted_streaming' }
}

关键细节:当中止原因是 'interrupt'(用户提交了新输入触发的中断)时,跳过中断消息——因为紧随其后的用户消息已经提供了足够的上下文。

12.2 工具执行阶段中止

源码位置:src/query.ts:1488

工具执行完成后检查中止信号:

// src/query.ts:1488
if (toolUseContext.abortController.signal.aborted) {
  // Computer Use 清理
  if (feature('CHICAGO_MCP') && !toolUseContext.agentId) {
    const { cleanupComputerUseAfterTurn } = await import('./utils/computerUse/cleanup.js')
    await cleanupComputerUseAfterTurn(toolUseContext)
  }
  if (toolUseContext.abortController.signal.reason !== 'interrupt') {
    yield createUserInterruptionMessage({ toolUse: true }) // 注意 toolUse: true
  }
  // 中止时也检查 maxTurns
  const nextTurnCountOnAbort = turnCount + 1
  if (maxTurns && nextTurnCountOnAbort > maxTurns) {
    yield createAttachmentMessage({ type: 'max_turns_reached', maxTurns, turnCount: nextTurnCountOnAbort })
  }
  return { reason: 'aborted_tools' }
}

两个中止路径的区别在于 createUserInterruptionMessagetoolUse 参数,标识中断发生在工具执行期间。

十三、工具执行与循环续接

13.1 工具执行

源码位置:src/query.ts:1369

当模型响应包含 tool_use 块时(needsFollowUp === true),进入工具执行阶段:

// src/query.ts:1383
const toolUpdates = streamingToolExecutor
  ? streamingToolExecutor.getRemainingResults()   // 流式执行器:收集剩余结果
  : runTools(toolUseBlocks, assistantMessages, canUseTool, toolUseContext) // 回退:顺序执行

for await (const update of toolUpdates) {
  if (update.message) {
    yield update.message

    // 检查钩子是否阻止续行
    if (update.message.type === 'attachment' && update.message.attachment.type === 'hook_stopped_continuation') {
      shouldPreventContinuation = true
    }

    toolResults.push(...normalizeMessagesForAPI([update.message], ...).filter(_ => _.type === 'user'))
  }
  if (update.newContext) {
    updatedToolUseContext = { ...update.newContext, queryTracking }
  }
}

工具执行的同时,异步生成工具使用摘要(用于移动端 UI):

// src/query.ts:1472
nextPendingToolUseSummary = generateToolUseSummary({
  tools: toolInfoForSummary,
  signal: toolUseContext.abortController.signal,
  isNonInteractiveSession: toolUseContext.options.isNonInteractiveSession,
  lastAssistantText,
})
  .then(summary => summary ? createToolUseSummaryMessage(summary, toolUseIds) : null)
  .catch(() => null)

这个摘要生成是非阻塞的——异步调用 Haiku 模型(~1s),在下一次 API 调用期间(5-30s)完成,在下一轮迭代开始时 yield。

13.2 下轮循环续接管线

工具执行完成后,进入续行管线为下一轮 API 调用准备额外上下文:

1. 队列命令

源码位置:src/query.ts:1573

从全局队列中获取待处理的命令,按优先级和范围过滤:

const queuedCommandsSnapshot = getCommandsByMaxPriority(sleepRan ? 'later' : 'next')
  .filter(cmd => {
    if (isSlashCommand(cmd)) return false              // 排除斜杠命令
    if (isMainThread) return cmd.agentId === undefined  // 主线程只取无 agentId 的
    return cmd.mode === 'task-notification' && cmd.agentId === currentAgentId  // 子代理取自己的
  })

2. 获取附件消息

源码位置:src/query.ts:1583

for await (const attachment of getAttachmentMessages(null, updatedToolUseContext, null, queuedCommandsSnapshot, [...messagesForQuery, ...assistantMessages, ...toolResults], querySource)) {
  yield attachment
  toolResults.push(attachment)
}

3. 消费记忆预取

源码位置:src/query.ts:1602

零等待消费——如果预取已完成则使用,否则跳过等下一轮重试:

if (pendingMemoryPrefetch && pendingMemoryPrefetch.settledAt !== null && pendingMemoryPrefetch.consumedOnIteration === -1) {
  const memoryAttachments = filterDuplicateMemoryAttachments(await pendingMemoryPrefetch.promise, toolUseContext.readFileState)
  for (const memAttachment of memoryAttachments) {
    const msg = createAttachmentMessage(memAttachment)
    yield msg
    toolResults.push(msg)
  }
  pendingMemoryPrefetch.consumedOnIteration = turnCount - 1
}

4. 注入技能发现预取

源码位置:src/query.ts:1623-1631

if (skillPrefetch && pendingSkillPrefetch) {
  const skillAttachments = await skillPrefetch.collectSkillDiscoveryPrefetch(pendingSkillPrefetch)
  for (const att of skillAttachments) {
    const msg = createAttachmentMessage(att)
    yield msg
    toolResults.push(msg)
  }
}

5. 刷新工具列表

源码位置:src/query.ts:1663-1674

在循环间刷新工具,使新连接的 MCP 服务器可用:

if (updatedToolUseContext.options.refreshTools) {
  const refreshedTools = updatedToolUseContext.options.refreshTools()
  if (refreshedTools !== updatedToolUseContext.options.tools) {
    updatedToolUseContext = { ...updatedToolUseContext, options: { ...updatedToolUseContext.options, tools: refreshedTools } }
  }
}

13.3 maxTurns 检查与状态转换

循环续接管线完成后,检查是否达到最大循环限制:

// src/query.ts:1708
const nextTurnCount = turnCount + 1
if (maxTurns && nextTurnCount > maxTurns) {
  yield createAttachmentMessage({ type: 'max_turns_reached', maxTurns, turnCount: nextTurnCount })
  return { reason: 'max_turns', turnCount: nextTurnCount }
}

未达到限制时,构建下一轮状态并 continue

// src/query.ts:1718
const next: State = {
  messages: [...messagesForQuery, ...assistantMessages, ...toolResults],
  toolUseContext: toolUseContextWithQueryTracking,
  autoCompactTracking: tracking,
  turnCount: nextTurnCount,
  maxOutputTokensRecoveryCount: 0,        // 重置恢复计数
  hasAttemptedReactiveCompact: false,       // 重置压缩标记
  pendingToolUseSummary: nextPendingToolUseSummary,
  maxOutputTokensOverride: undefined,       // 重置输出限制覆盖
  stopHookActive,
  transition: { reason: 'next_turn' },
}
state = next
// } // while (true) → continue 回到循环顶部

注意每次正常循环续接时,maxOutputTokensRecoveryCounthasAttemptedReactiveCompact重置,这意味着恢复保护是每轮而非全局的。

总结

Claude Code 的核心对话循环是一个精心设计的多层架构:

  1. 输入层(onSubmit → handlePromptSubmit → executeUserInput):负责输入分类、即时命令拦截、队列管理和并发保护。五条互斥路径确保每种输入场景都有明确的处理策略。

  2. 查询控制层(onQuery → onQueryImpl):QueryGuard 状态机提供严格的并发控制,并行上下文加载最小化延迟,for await 模式优雅地消费异步事件流。

  3. 核心引擎层(query → queryLoop → while(true)):可变 State 对象在迭代间传递状态,五阶段预查询管线渐进式压缩上下文,六种恢复转换通过 state = next; continue 模式实现优雅的错误恢复。

  4. 执行层(StreamingToolExecutor +循环续接管线):流内工具并发执行与模型生成重叠,异步工具摘要生成和记忆/技能预取充分利用等待时间,工具刷新和队列压缩确保动态资源及时可用。

整个系统的核心设计哲学是弹性和自修复:通过错误扣留机制推迟决策,通过分级恢复策略(压缩 → 反应式压缩 → 输出限制升级 → 多轮续写)逐步降级,通过严格的单次触发保护和 generation 编号防止无限循环。这使得 Claude Code 能够在面对各种运行时错误(上下文溢出、输出截断、模型高负载、用户中断)时自动恢复,极大提升了交互式代码助手的可靠性。