本文基于项目实际源码,深入分析 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
onSubmit 是 PromptInput 组件的回调,是所有用户输入的第一个处理点。它接收四个参数:
// 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);
判定为即时命令需要满足两个条件:
queryGuard.isActive— 当前有查询正在执行- 命令标记了
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)。
2.3 委托 handlePromptSubmit
通过前述检查后,onSubmit 将控制权委托给 handlePromptSubmit:
onSubmit
├─ 即时命令 → executeImmediateCommand() → return
├─ 远程模式空输入 → return
├─ 空闲回归 → dialog/hint/pass
└─ 正常路径 → handlePromptSubmit(...)
三、handlePromptSubmit — 五路分发
源码位置:
src/utils/handlePromptSubmit.ts:120
handlePromptSubmit 是输入处理的核心分发器,有五条互斥路径:
handlePromptSubmit(params)
│
├─① queuedCommands?.length → executeUserInput() [直接执行预验证命令]
│
├─② ['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: true 的 local-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 是连接用户输入处理和查询系统的桥梁。它的核心职责:
- 创建
AbortController - 预留
queryGuard(reserve()将状态从 idle 转为 dispatching) - 循环处理所有
queuedCommands - 调用
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 清理与循环完成
onQuery 的 finally 块通过 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:1311feature 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' }
}
两个中止路径的区别在于 createUserInterruptionMessage 的 toolUse 参数,标识中断发生在工具执行期间。
十三、工具执行与循环续接
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 回到循环顶部
注意每次正常循环续接时,maxOutputTokensRecoveryCount 和 hasAttemptedReactiveCompact 被重置,这意味着恢复保护是每轮而非全局的。
总结
Claude Code 的核心对话循环是一个精心设计的多层架构:
-
输入层(onSubmit → handlePromptSubmit → executeUserInput):负责输入分类、即时命令拦截、队列管理和并发保护。五条互斥路径确保每种输入场景都有明确的处理策略。
-
查询控制层(onQuery → onQueryImpl):QueryGuard 状态机提供严格的并发控制,并行上下文加载最小化延迟,
for await模式优雅地消费异步事件流。 -
核心引擎层(query → queryLoop → while(true)):可变 State 对象在迭代间传递状态,五阶段预查询管线渐进式压缩上下文,六种恢复转换通过
state = next; continue模式实现优雅的错误恢复。 -
执行层(StreamingToolExecutor +循环续接管线):流内工具并发执行与模型生成重叠,异步工具摘要生成和记忆/技能预取充分利用等待时间,工具刷新和队列压缩确保动态资源及时可用。
整个系统的核心设计哲学是弹性和自修复:通过错误扣留机制推迟决策,通过分级恢复策略(压缩 → 反应式压缩 → 输出限制升级 → 多轮续写)逐步降级,通过严格的单次触发保护和 generation 编号防止无限循环。这使得 Claude Code 能够在面对各种运行时错误(上下文溢出、输出截断、模型高负载、用户中断)时自动恢复,极大提升了交互式代码助手的可靠性。