Stop Conditions
src/query.ts 中的 while(true) 循环没有自然的终止条件,而是依赖散落在循环体各处的显式 return 语句。理解这些 stop condition 对于理解 Claude Code 的行为至关重要。
Stop Condition 分类
Section titled “Stop Condition 分类”graph TD
A[Loop Iteration] --> B{Stop Conditions}
B --> C[completed - model finished]
B --> D[max_turns - turn limit hit]
B --> E[max_budget_usd - cost limit hit]
B --> F[aborted_streaming - user interrupt during stream]
B --> G[aborted_tools - user interrupt during tools]
B --> H[blocking_limit - context window full]
B --> I[stop_hook_prevented - hook stopped execution]
B --> J[hook_stopped - PostToolUse hook stopped]
B --> K[prompt_too_long - irrecoverable 413]
B --> L[model_error - API/runtime error]
B --> M[image_error - media size error]
1. 完成(end_turn)
Section titled “1. 完成(end_turn)”最常见的 stop condition。模型的响应不包含 tool_use 块,说明其已完成工作。
if (!needsFollowUp) { // ... handle recoverable errors, stop hooks ... return { reason: 'completed' };}在外层 QueryEngine 中,这会产生一个成功结果:
yield { type: 'result', subtype: 'success', is_error: false, duration_ms: Date.now() - startTime, result: textResult, stop_reason: lastStopReason, total_cost_usd: getTotalCost(), usage: this.totalUsage,};stop_reason 来自 API 的 message_delta 事件,模型自然完成时通常为 "end_turn"。
2. 最大 Turn 数
Section titled “2. 最大 Turn 数”通过 maxTurns 参数(SDK)或 --max-turns CLI flag 配置。在每次 tool 结果收集后检查:
const nextTurnCount = turnCount + 1;if (maxTurns && nextTurnCount > maxTurns) { yield createAttachmentMessage({ type: 'max_turns_reached', maxTurns, turnCount: nextTurnCount, }); return { reason: 'max_turns', turnCount: nextTurnCount };}attachment 消息由 QueryEngine 接收并转换为 SDK 错误结果:
case 'attachment': if (message.attachment.type === 'max_turns_reached') { yield { type: 'result', subtype: 'error_max_turns', is_error: true, errors: [`Reached maximum number of turns (${message.attachment.maxTurns})`], }; return; }3. 费用预算(max_budget_usd)
Section titled “3. 费用预算(max_budget_usd)”在外层 QueryEngine 中执行,而非在循环内部:
// src/QueryEngine.ts — after each yielded messageif (maxBudgetUsd !== undefined && getTotalCost() >= maxBudgetUsd) { yield { type: 'result', subtype: 'error_max_budget_usd', is_error: true, errors: [`Reached maximum budget ($${maxBudgetUsd})`], total_cost_usd: getTotalCost(), usage: this.totalUsage, }; return;}此检查在内层循环 yield 的每条消息后运行——包括 stream 事件、助手消息和 tool 结果。这意味着预算可能在被捕获之前就被一次 API 调用的费用超出。
4. 用户中断(中止)
Section titled “4. 用户中断(中止)”用户中断(Ctrl+C、ESC 或 submit-interrupt)会触发 abortController.abort() 信号。循环在两个点检查此信号:
streaming 期间
Section titled “streaming 期间”// src/query.ts — after the stream loopif (toolUseContext.abortController.signal.aborted) { if (streamingToolExecutor) { for await (const update of streamingToolExecutor.getRemainingResults()) { if (update.message) yield update.message; } } else { yield* yieldMissingToolResultBlocks(assistantMessages, 'Interrupted by user'); }
if (toolUseContext.abortController.signal.reason !== 'interrupt') { yield createUserInterruptionMessage({ toolUse: false }); } return { reason: 'aborted_streaming' };}tool 执行期间
Section titled “tool 执行期间”// src/query.ts — after tool results collectedif (toolUseContext.abortController.signal.aborted) { if (toolUseContext.abortController.signal.reason !== 'interrupt') { yield createUserInterruptionMessage({ toolUse: true }); } return { reason: 'aborted_tools' };}'interrupt' 原因是特殊的——它表示用户在 tool 运行时提交了新消息。此时跳过中断消息,因为新的用户消息提供了足够的 context。
5. 阻断 token 限制
Section titled “5. 阻断 token 限制”当 auto-compact 被禁用且 context window 即将填满时,会生成一个合成错误:
// src/query.ts — before the API callif ( !compactionResult && querySource !== 'compact' && !(reactiveCompact?.isReactiveCompactEnabled() && isAutoCompactEnabled())) { const { isAtBlockingLimit } = calculateTokenWarningState( tokenCountWithEstimation(messagesForQuery) - snipTokensFreed, toolUseContext.options.mainLoopModel, ); if (isAtBlockingLimit) { yield createAssistantAPIErrorMessage({ content: PROMPT_TOO_LONG_ERROR_MESSAGE, }); return { reason: 'blocking_limit' }; }}这是一个预防性检查——在 API 调用之前触发,节省了一次必然返回 413 错误的往返费用。仅在以下情况适用:
- Auto-compact 被关闭(用户明确禁用)
- Reactive compact 不可用作回退
- Context collapse 不可用作回退
6. Stop Hook 阻止
Section titled “6. Stop Hook 阻止”stop hook 在模型产生无 tool 调用的响应时运行。它们可以:
- 阻断响应(注入错误,强制重试)
- 阻止继续(完全停止当前 turn)
const stopHookResult = yield* handleStopHooks( messagesForQuery, assistantMessages, systemPrompt, userContext, systemContext, toolUseContext, querySource, stopHookActive,);
if (stopHookResult.preventContinuation) { return { reason: 'stop_hook_prevented' };}
if (stopHookResult.blockingErrors.length > 0) { state = { messages: [...messagesForQuery, ...assistantMessages, ...stopHookResult.blockingErrors], stopHookActive: true, transition: { reason: 'stop_hook_blocking' }, // ... }; continue;}stopHookActive 标志防止无限循环:一旦 stop hook 进行了阻断,它会被记录,后续迭代便知道正处于 hook 重试过程中。
7. PostToolUse Hook 停止
Section titled “7. PostToolUse Hook 停止”PostToolUse hook 可以发出 preventContinuation 信号,在 tool 执行后停止循环:
// src/query.ts — inside tool result collectionif ( update.message.type === 'attachment' && update.message.attachment.type === 'hook_stopped_continuation') { shouldPreventContinuation = true;}
// After all tools completeif (shouldPreventContinuation) { return { reason: 'hook_stopped' };}8. 不可恢复错误
Section titled “8. 不可恢复错误”若恢复尝试失败,若干错误条件会终止循环:
Prompt 过长(413)
Section titled “Prompt 过长(413)”// After collapse drain and reactive compact both failyield lastMessage; // surface the withheld errorvoid executeStopFailureHooks(lastMessage, toolUseContext);return { reason: 'prompt_too_long' };} catch (error) { yield* yieldMissingToolResultBlocks(assistantMessages, errorMessage); yield createAssistantAPIErrorMessage({ content: errorMessage }); return { reason: 'model_error', error };}图片/媒体错误
Section titled “图片/媒体错误”if (error instanceof ImageSizeError || error instanceof ImageResizeError) { yield createAssistantAPIErrorMessage({ content: error.message }); return { reason: 'image_error' };}优先级与恢复顺序
Section titled “优先级与恢复顺序”当多个条件可能同时适用时,检查顺序决定哪个先触发:
1. 中止信号(最高优先级——最先检查)2. streaming 错误(catch 块)3. 图片错误(catch 块)4. Prompt-too-long 恢复(collapse → reactive compact → 暴露错误)5. Max output tokens 恢复(提升 → 注入 resume → 暴露错误)6. API 错误消息(跳过 stop hook)7. Stop hook(阻断 → 阻止 → 通过)8. Token budget 续行9. Max turns(在继续点检查)10. Max budget USD(在 QueryEngine 每次 yield 后检查)11. 完成(最低优先级——自然结束)优雅终止 vs 立即终止
Section titled “优雅终止 vs 立即终止”| 条件 | 类型 | Tool 结果 | 中断消息 |
|---|---|---|---|
completed | 优雅 | 全部收集 | 无 |
max_turns | 优雅 | 全部收集 | Attachment 消息 |
max_budget_usd | 立即 | 可能不完整 | SDK result 消息 |
aborted_streaming | 立即 | 合成错误 | 若非 submit-interrupt 则有 |
aborted_tools | 立即 | 部分 + 合成 | 若非 submit-interrupt 则有 |
blocking_limit | 预防性 | 无(API 调用前) | 错误消息 |
model_error | 立即 | 合成错误 | 错误消息 |
context window 填满时会发生什么
Section titled “context window 填满时会发生什么”当 context 接近模型的 window 上限时,一系列机制会依次激活:
graph TD
A[Context growing] --> B{Auto-compact enabled?}
B -- yes --> C[Auto-compact fires]
C --> D[Summarize older messages]
D --> E[Continue with smaller context]
B -- no --> F{At blocking limit?}
F -- yes --> G[Preemptive PROMPT_TOO_LONG_ERROR]
F -- no --> H[API call proceeds]
H --> I{API returns 413?}
I -- yes --> J{Context collapse available?}
J -- yes --> K[Drain staged collapses]
K --> L{Still 413?}
L -- yes --> M{Reactive compact available?}
L -- no --> N[Continue with collapsed context]
M -- yes --> O[Full compaction]
O --> P{Still 413?}
P -- yes --> Q[Surface error, stop]
P -- no --> R[Continue with compacted context]
M -- no --> Q
J -- no --> M
I -- no --> S[Normal processing]
关键洞察:系统针对 context 溢出有四层防御,每层都比上一层更激进:
- Auto-compact(主动式,在可配置阈值触发)
- Context collapse drain(响应式,保留细粒度 context)
- Reactive compact(响应式,完整摘要)
- Blocking limit(预防式,面向用户的错误)