跳转到内容

Stop Conditions

src/query.ts 中的 while(true) 循环没有自然的终止条件,而是依赖散落在循环体各处的显式 return 语句。理解这些 stop condition 对于理解 Claude Code 的行为至关重要。

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]

最常见的 stop condition。模型的响应不包含 tool_use 块,说明其已完成工作。

src/query.ts
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"

通过 maxTurns 参数(SDK)或 --max-turns CLI flag 配置。在每次 tool 结果收集后检查:

src/query.ts
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 错误结果:

src/QueryEngine.ts
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;
}

在外层 QueryEngine 中执行,而非在循环内部:

// src/QueryEngine.ts — after each yielded message
if (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 调用的费用超出。

用户中断(Ctrl+C、ESC 或 submit-interrupt)会触发 abortController.abort() 信号。循环在两个点检查此信号:

// src/query.ts — after the stream loop
if (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' };
}
// src/query.ts — after tool results collected
if (toolUseContext.abortController.signal.aborted) {
if (toolUseContext.abortController.signal.reason !== 'interrupt') {
yield createUserInterruptionMessage({ toolUse: true });
}
return { reason: 'aborted_tools' };
}

'interrupt' 原因是特殊的——它表示用户在 tool 运行时提交了新消息。此时跳过中断消息,因为新的用户消息提供了足够的 context。

当 auto-compact 被禁用且 context window 即将填满时,会生成一个合成错误:

// src/query.ts — before the API call
if (
!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 不可用作回退

stop hook 在模型产生无 tool 调用的响应时运行。它们可以:

  • 阻断响应(注入错误,强制重试)
  • 阻止继续(完全停止当前 turn)
src/query.ts
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 重试过程中。

PostToolUse hook 可以发出 preventContinuation 信号,在 tool 执行后停止循环:

// src/query.ts — inside tool result collection
if (
update.message.type === 'attachment' &&
update.message.attachment.type === 'hook_stopped_continuation'
) {
shouldPreventContinuation = true;
}
// After all tools complete
if (shouldPreventContinuation) {
return { reason: 'hook_stopped' };
}

若恢复尝试失败,若干错误条件会终止循环:

// After collapse drain and reactive compact both fail
yield lastMessage; // surface the withheld error
void executeStopFailureHooks(lastMessage, toolUseContext);
return { reason: 'prompt_too_long' };
} catch (error) {
yield* yieldMissingToolResultBlocks(assistantMessages, errorMessage);
yield createAssistantAPIErrorMessage({ content: errorMessage });
return { reason: 'model_error', error };
}
if (error instanceof ImageSizeError || error instanceof ImageResizeError) {
yield createAssistantAPIErrorMessage({ content: error.message });
return { reason: 'image_error' };
}

当多个条件可能同时适用时,检查顺序决定哪个先触发:

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. 完成(最低优先级——自然结束)
条件类型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 上限时,一系列机制会依次激活:

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 溢出有四层防御,每层都比上一层更激进:

  1. Auto-compact(主动式,在可配置阈值触发)
  2. Context collapse drain(响应式,保留细粒度 context)
  3. Reactive compact(响应式,完整摘要)
  4. Blocking limit(预防式,面向用户的错误)