Claude Code 源码解析(三):queryLoop、Tool 执行与权限链

接续第一篇的主链路与第二篇的 Memory 体系,本文先展开 queryLoop 各 phase 的完整流程,再深入 Tool 执行、权限链与 API 归一化。

queryLoop 总览

query() 是对外的 async generator;真正循环在内部的 queryLoop()query.ts ~241 行),结构是 while (true) + state 对象在 iteration 间传递,而不是简单的 while (needsFollowUp)

一次用户输入可能触发 多轮 iteration(每次 tool 调用后 continue 递归),每轮 iteration 内部又分 预处理 → API 流式 → 收尾/Tool → 附件 → 状态更新 几个 phase。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
flowchart TB
ENTRY["query() 入口<br/>startRelevantMemoryPrefetch (整 turn 一次)"]
LOOP["while (true) iteration"]

subgraph P0["Phase 0: Iteration 初始化"]
P0A[destructure state]
P0B[startSkillDiscoveryPrefetch]
P0C["yield stream_request_start"]
P0D[queryTracking depth++]
end

subgraph P1["Phase 1: 上下文预处理"]
P1A[getMessagesAfterCompactBoundary]
P1B[applyToolResultBudget]
P1C[snip]
P1D[microcompact]
P1E[context collapse]
P1F[autocompact]
end

subgraph P2["Phase 2: API 前 Setup"]
P2A[StreamingToolExecutor]
P2B[getRuntimeMainLoopModel]
P2C[blocking limit 检查]
end

subgraph P3["Phase 3: API 流式"]
P3A[callModel SSE]
P3B[yield stream_event / assistant]
P3C[流式 Tool 增量执行]
P3D{needsFollowUp?}
end

subgraph P4["Phase 4: 无 Tool 收尾"]
P4A[413/media/max_tokens 恢复]
P4B[Stop hooks]
P4C[token budget]
P4D[return completed]
end

subgraph P5["Phase 5: Tool 执行"]
P5A[getRemainingResults / runTools]
P5B[generateToolUseSummary 异步]
end

subgraph P6["Phase 6: 附件与 Prefetch"]
P6A[getAttachmentMessages]
P6B[memory prefetch consume]
P6C[skill prefetch consume]
P6D[refreshTools]
end

subgraph P7["Phase 7: 递归"]
P7A["state.messages += assistant + toolResults"]
P7B[continue → 下一轮 iteration]
end

ENTRY --> LOOP
LOOP --> P0 --> P1 --> P2 --> P3
P3 -->|needsFollowUp=false| P4
P3 -->|needsFollowUp=true| P5 --> P6 --> P7
P7 --> LOOP
P4 --> END[return Terminal]

设置 CLAUDE_CODE_PROFILE_QUERY=1 可对照 queryCheckpoint 打点(见 utils/queryProfiler.ts)。


queryLoop 各 Phase 详解

Phase 0:Iteration 初始化

每次 while (true) 开头:

步骤代码位置说明
解构 state~311messagestoolUseContextturnCountpendingToolUseSummary
Skill prefetch 启动~331startSkillDiscoveryPrefetch,在模型 streaming + tool 执行期间后台跑
yield stream_request_start~337REPL spinner 切到 requesting
queryTracking.depth++~347链式追踪,每 iteration 深度 +1

整 turn 只做一次(在 while 外):startRelevantMemoryPrefetch — relevant memory 后台检索,iteration 末尾 consume。

Phase 1:上下文预处理(API 调用前)

顺序固定,snip → microcompact → collapse → autocompact,越往后越「重」:

1
2
3
4
5
6
messagesForQuery = getMessagesAfterCompactBoundary(messages)
→ applyToolResultBudget() // tool 结果体积上限,content replacement
→ snipCompactIfNeeded() // HISTORY_SNIP:删整条 message
→ microcompact() // 清空 tool 结果 / cache edit
→ contextCollapse.applyCollapsesIfNeeded() // span 摘要(不 yield,读时投影)
→ autocompact() // 整段 compact,可能替换 messagesForQuery

要点:

  • snipTokensFreed 传给 autocompact,因为 tokenCountWithEstimation 读不到 snip 释放的 token
  • context collapse 在 autocompact 之前:若 collapse 已把 token 压到阈值以下,autocompact 成为 no-op,保留更细粒度上下文
  • autocompact 成功:yield buildPostCompactMessages()messagesForQuery 替换为 compact 后消息,重置 autoCompactTracking

Phase 2:API 前 Setup

步骤说明
初始化 per-iteration 容器assistantMessages[]toolResults[]toolUseBlocks[]needsFollowUp=false
StreamingToolExecutorfeature gate streamingToolExecution 开启时创建
getRuntimeMainLoopModelplan 模式 + 超 200k 可能换模型
Blocking limitautocompact 关闭时的硬拦截;compact/session_memory/reactive compact 等路径跳过

Phase 3:API 流式

内层 while (attemptWithFallback) 包一层 模型 fallback 重试

1
for await (message of deps.callModel({ messages: prependUserContext(...), ... }))

流式循环内对每个 message

message 类型行为
stream_eventyield 给 REPL → handleMessageFromStream 更新 streaming 预览
assistantpush 到 assistantMessages;含 tool_useneedsFollowUp=true
tool_use block 完成streamingToolExecutor.addTool() + getCompletedResults() 增量 yield tool_result
streaming fallbacktombstone 旧 assistant、discard executor、清空状态、重试
withheld 413/media/max_tokens暂 yield,留到 Phase 4 恢复

needsFollowUp 的语义:不依赖 API 的 stop_reason === 'tool_use'(不可靠),而是 流式收到 tool_use block 即置 true

流式结束后:

  • 若有 pendingCacheEdits → yield microcompact boundary(用 API 回报的 cache_deleted_input_tokens
  • executePostSamplingHooks(fire-and-forget)

Abort 路径(流式刚结束):getRemainingResults() 补 synthetic tool_result → cleanup → return { reason: 'aborted_streaming' }

Phase 4:无 Tool 收尾(!needsFollowUp

模型本轮 没有 tool_use,进入收尾链:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
flowchart TD
A["!needsFollowUp"] --> B{yield pendingToolUseSummary}
B --> C{withheld 413?}
C -->|是| D[collapse drain → continue]
C -->|否| E{withheld media?}
E -->|是| F[reactive compact → continue]
E -->|否| G{withheld max_output_tokens?}
G -->|是| H[escalate 64k / meta recovery → continue]
G -->|否| I{lastMessage.isApiErrorMessage?}
I -->|是| J[return completed]
I -->|否| K[handleStopHooks]
K --> L{blockingErrors?}
L -->|是| M[inject errors → continue]
L -->|否| N{token budget continue?}
N -->|是| O[nudge meta message → continue]
N -->|否| P["return { reason: 'completed' }"]

关键 recovery transition.reason

reason触发条件
collapse_drain_retrywithheld prompt-too-long,drain staged collapses
reactive_compact_retry413/media 触发 reactive compact
max_output_tokens_escalate8k cap 命中 → 单次升到 64k 重试
max_output_tokens_recovery注入 meta「继续写,不要 recap」
stop_hook_blockingStop hook 返回 blocking error → 带 error 再调 API
token_budget_continuationtoken budget 未耗尽 → meta nudge 继续

Stop hooks 不在 API error 上跑(防 death spiral:error → hook 加 token → 再 error)。

Phase 5:Tool 执行(needsFollowUp === true

1
2
3
toolUpdates = streamingToolExecutor
? streamingToolExecutor.getRemainingResults()
: runTools(toolUseBlocks, ...)
  • 逐个 yield update.message(含 progress、tool_result、hook attachment)
  • normalizeMessagesForAPI 过滤出 user 类型 push 到 toolResults
  • update.newContext 合并 tool 产生的 context 变更
  • hook_stopped_continuationshouldPreventContinuation=true → 后续 return { reason: 'hook_stopped' }

并行启动 generateToolUseSummary(Haiku),结果存 nextPendingToolUseSummary下一轮 iteration Phase 4 开头 yield(在模型 streaming 的 5–30s 窗口内完成)。

Abort mid-tools → return { reason: 'aborted_tools' }

Phase 6:附件与 Prefetch

必须在 tool_result 全部完成之后——API 不允许 tool_result 与普通 user message 交错。

顺序:

  1. getAttachmentMessages — IDE 上下文、queued command、file change 等
  2. Memory prefetch consumesettledAt !== null 且未消费时注入 relevant memory attachment
  3. Skill prefetch consume — 注入 skill discovery 结果
  4. Drain command queue — 已消费的 prompt/task-notification 从队列移除

然后 refreshTools() — MCP 新连接的工具在下一轮可用。

Phase 7:递归(下一轮 iteration)

1
2
3
4
5
6
7
8
9
10
state = {
messages: [...messagesForQuery, ...assistantMessages, ...toolResults],
toolUseContext: toolUseContextWithQueryTracking,
turnCount: turnCount + 1,
pendingToolUseSummary: nextPendingToolUseSummary,
maxOutputTokensRecoveryCount: 0,
hasAttemptedReactiveCompact: false,
transition: { reason: 'next_turn' },
}
// continue → 回到 Phase 0
  • maxTurns 超限 → yield max_turns_reached attachment → return
  • queryCheckpoint('query_recursive_call') 标记递归点

State 对象:iteration 间传递什么

1
2
3
4
5
6
7
8
9
10
11
12
type State = {
messages: Message[] // 每轮 append assistant + toolResults
toolUseContext: ToolUseContext // tools/MCP/permissions 等可变上下文
autoCompactTracking: AutoCompactTrackingState // compact 后 turn 计数
maxOutputTokensRecoveryCount: number // max_tokens 多轮 recovery
hasAttemptedReactiveCompact: boolean // 防 reactive compact 死循环
maxOutputTokensOverride: number | undefined // 64k escalate
pendingToolUseSummary: Promise<...> | undefined // 上轮 tool summary
stopHookActive: boolean | undefined // stop hook 重入标记
turnCount: number // agent turn 计数(含 tool 轮)
transition: Continue | undefined // 上一轮为何 continue(测试/诊断)
}

queryCheckpoint 对照表

CheckpointPhase
query_fn_entry0
query_snip_start/end1
query_microcompact_start/end1
query_autocompact_start/end1
query_setup_start/end2
query_api_loop_start3
query_api_streaming_start/end3
query_tool_execution_start/end5
query_recursive_call7

Tool 子系统总览

Tool 相关逻辑主要在 Phase 3(流式增量)Phase 5(批量收尾)

1
2
3
4
flowchart LR
P3["Phase 3 流式<br/>addTool + getCompletedResults"]
P5["Phase 5<br/>getRemainingResults / runTools"]
P3 --> P5

关键文件:

模块路径职责
query 主循环src/query.ts流式消费、assistant 收集、tool 触发
Tool 编排src/services/tools/toolOrchestration.tsrunTools、batch 分区、并发/串行
流式 Tool 执行src/services/tools/StreamingToolExecutor.ts流式收到完整 tool_use 即排队执行
单 Tool 执行src/services/tools/toolExecution.tsrunToolUsecheckPermissionsAndCallTool
API 流式src/services/api/claude.tsSSE → content_block_stop yield assistant
流式 UIsrc/utils/messages.tshandleMessageFromStream
权限 Hooksrc/hooks/useCanUseTool.tsx弹窗 / classifier / coordinator
API 归一化src/utils/messages.tsnormalizeMessagesForAPI

一、checkPermissionsAndCallTool 执行管线

单 Tool 执行的入口是 runToolUse(async generator),内部调用 streamedCheckPermissionsAndCallTool,最终落到 checkPermissionsAndCallTooltoolExecution.ts ~599 行)。

1.1 完整流水线

1
2
3
4
5
6
7
8
9
10
11
12
runToolUse
└─ streamedCheckPermissionsAndCallTool (Promise → yield 包装)
└─ checkPermissionsAndCallTool
├─ ① Zod 校验 (tool.inputSchema.safeParse)
├─ ② validateInput (各 Tool 自定义)
├─ ③ Bash 预启动 classifier (与后续并行)
├─ ④ 输入预处理 (strip _simulatedSedEdit, backfillObservableInput)
├─ ⑤ PreToolUse hooks
├─ ⑥ 权限决策 (resolveHookPermissionDecision → canUseTool)
├─ ⑦ tool.call() (允许时)
├─ ⑧ PostToolUse hooks
└─ ⑨ addToolResult → createUserMessage(tool_result)

1.2 各阶段要点

① Zod 校验inputSchema.safeParse(input) 失败则直接返回 InputValidationErrortool_result,不调用 Tool。deferred tool 还会附加 schema-not-sent hint,提示模型先 ToolSearch

② validateInput:各 Tool 自定义校验(路径是否存在、命令是否合法等),失败同样提前返回 error tool_result

③ Bash 投机优化:对 Bash 提前 startSpeculativeClassifierCheck,与 PreToolUse hook、权限弹窗并行,减少 auto 模式等待。

④ 输入预处理

  • 去掉模型不应提供的 _simulatedSedEdit(仅权限系统在用户批准后注入)
  • backfillObservableInput 在浅拷贝上补全字段(如 ~ 展开为绝对路径),供 Hook/权限使用;不污染传给 tool.call() 的原始 input,避免 VCR hash 变化

⑤ PreToolUse hooksrunPreToolUseHooks)可能产生:

事件效果
message / progress中间消息、进度
hookPermissionResultHook 直接给权限结论
hookUpdatedInput修改 input
preventContinuation / stopReason阻止继续
stop直接返回 error tool_result

⑥ 权限:见下文「canUseTool 权限链」。

⑦ tool.call():权限通过后调用,传入 toolUseIduserModified、progress 回调等。

⑧ PostToolUse hooks:MCP Tool 可能修改 output;非 MCP 先 addToolResult,MCP 在 Hook 后再 addToolResult

⑨ 产出resultingMessages: MessageUpdateLazy[],含 tool_result user message、attachment、hook summary 等;streamedCheckPermissionsAndCallTool 逐个 yield 给上层。

1.3 REPL 中的 wiring

canUseTool 在 REPL 中由 React hook 创建(REPL.tsx ~2382 行):

1
const canUseTool = useCanUseTool(setToolUseConfirmQueue, setToolPermissionContext);

getToolUseContext 注入 query 链路,最终传入 checkPermissionsAndCallTool


二、Tool 并发:无读写锁,靠 isConcurrencySafe

Claude Code 没有通用读写锁,也没有 Tool 间依赖图。并发完全靠每个 Tool 声明的 isConcurrencySafe(input) + 批处理/队列规则。

2.1 谁可以并发?

每个 Tool 自己实现 isConcurrencySafe

1
2
3
4
5
6
7
8
9
// FileReadTool — 固定 true
isConcurrencySafe() { return true }

// BashTool — 只读命令可并发
isConcurrencySafe(input) {
return this.isReadOnly?.(input) ?? false
}

// Edit / Write / 多数 MCP — 默认 false(DEFAULT_TOOL_IMPL)

这不是 RW lock,而是 粗粒度「只读 vs 有副作用」 分类。

2.2 非流式:partitionToolCalls + batch

toolOrchestration.tspartitionToolCalls 按模型输出顺序分组:

  • 连续 safe batchrunToolsConcurrently(默认最多 10,CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY
  • 非 safe batchrunToolsSerially 逐个跑
  • batch 之间串行;不会[Read, Edit, Read] 里的两个 Read 跨 Edit 合并
1
2
3
4
5
6
// 连续 safe tool 合并为一批
if (isConcurrencySafe && acc[acc.length - 1]?.isConcurrencySafe) {
acc[acc.length - 1]!.blocks.push(toolUse)
} else {
acc.push({ isConcurrencySafe, blocks: [toolUse] })
}

2.3 流式:StreamingToolExecutor 队列

默认开启 streamingToolExecution 时,queryLoop 使用 StreamingToolExecutor:每个 content_block_stop 产出的完整 tool_useaddTool 排队。

核心规则(canExecuteTool):

  • 无 executing → 任意 tool 可启动
  • 有 executing → 只有 当前 + 已在跑的全是 safe 才能再启动 safe tool
  • 队首是非 safe 且前面有 executing → break,等前一个完成

结果按 模型输出顺序 yield(getCompletedResults 顺序遍历)。

2.4 「依赖」如何识别?

不识别。 没有 DAG、没有 file_path 冲突检测、没有「B 的 input 引用 A 的 output」分析。

实际策略:

  1. 假设模型按依赖顺序发 tool(Read 在前,Edit 在后)
  2. Edit/Bash-write 等非 safe → 强制串行
  3. Bash 出错会 cancel 兄弟 toolsiblingAbortController),因为命令链常有隐式依赖
  4. Read 类失败不 cancel 兄弟(彼此独立)

三、流式消息如何拼接

API 层claude.ts)和 UI 层messages.ts)两层。

3.1 API 层:SSE → 逐 block 产出 assistant message

SSE 事件作用
message_start保存 partialMessage(id、role 等)
content_block_startcontentBlocks[index] = { ...block, text/input/thinking: '' }
content_block_delta追加 delta 到对应 block
content_block_stopyield 一条完整 assistant message(content 仅含该 block)
message_delta回填最后一条 message 的 usagestop_reason

Text 拼接content_block_deltatext_deltacontentBlock.text += delta.text

Tool input 拼接input_json_deltacontentBlock.input += delta.partial_json(字符串累加,content_block_stop 时 parse 成 object)

要点:一个 turn 多个 block → 多条 assistant message(text、thinking、tool_use 各一条)。

3.2 UI 层:增量渲染

handleMessageFromStream 处理 stream_event

  • Textcontent_block_start(text) 重置 streamingTexttext_delta 追加
  • Tool input 预览content_block_start(tool_use) 加入 streamingToolUsesinput_json_delta 追加 unparsedToolInput
  • Turn 结束message_stop 清空 streamingToolUses

UI 的 streamingText / streamingToolUses预览;持久化 transcript 靠 API 层 content_block_stop yield 的完整 message。

3.3 queryLoop 如何拼成下一轮 context

1
2
3
4
5
6
7
if (message.type === 'assistant') {
assistantMessages.push(message)
// 每个 tool_use block 完成即 addTool
for (const toolBlock of msgToolUseBlocks) {
streamingToolExecutor.addTool(toolBlock, message)
}
}

一轮 iteration 结束后:

1
2
3
4
messagesForQuery
+ assistantMessages[] (可能多条:text / thinking / tool_use...)
+ toolResults[] (每个 tool_use 对应 user tool_result)
→ 下一轮 callModel

Prompt cache 注意:queryLoop 刻意 不 mutate 原始 assistant message(只 clone 后 yield),注释写明 mutating 会破坏 prompt caching 的 byte 匹配。


四、canUseTool 权限链

4.1 完整调用栈

1
2
3
4
5
checkPermissionsAndCallTool
└─ resolveHookPermissionDecision (PreToolUse hook 结果优先)
└─ canUseTool (useCanUseTool hook)
└─ hasPermissionsToUseTool (规则层,纯逻辑)
└─ [behavior=ask] → 交互 / classifier / coordinator 分支

4.2 第一层:resolveHookPermissionDecision

PreToolUse hook 可能直接给 allow / deny / ask。关键约束:Hook 的 allow 不能绕过 settings.json 的 deny/ask 规则(inc-4788 类比)。

Hook 结果后续
allow + 无 deny/ask 规则直接允许(跳过弹窗)
allow + deny 规则deny 覆盖 hook
allow + ask 规则仍走 canUseTool 弹窗
allow + 需交互 Tool(如 AskUserQuestion)必须走 canUseTool
deny直接拒绝
ask / 无 hook正常走 canUseTool,可带 forceDecision

4.3 第二层:hasPermissionsToUseTool(规则引擎)

纯配置/规则判断,返回 allow / deny / ask,不负责 UI。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
flowchart TD
A[hasPermissionsToUseToolInner] --> B{整 Tool deny 规则?}
B -->|是| DENY[deny]
B -->|否| C{整 Tool ask 规则?}
C -->|是且非 sandbox 自动允许| ASK1[ask]
C -->|否| D[tool.checkPermissions]
D --> E{Tool 返回 deny?}
E -->|是| DENY
E -->|否| F{requiresUserInteraction + ask?}
F -->|是| ASK2[ask]
F -->|否| G{内容级 ask / safetyCheck?}
G -->|是| ASK3[ask]
G -->|否| H{bypassPermissions 模式?}
H -->|是| ALLOW1[allow]
H -->|否| I{always-allow 规则?}
I -->|是| ALLOW2[allow]
I -->|否| J[passthrough → ask]

规则来源(按优先级):

  1. 整 Tool 级getDenyRuleForTool / getAskRuleForTool(settings.json、session、cli 等)
  2. Tool 实现级tool.checkPermissions()(如 Bash 子命令 Bash(git *)
  3. 模式级bypassPermissions / plan+bypass 可用 → 直接 allow
  4. always-allow 规则:前缀匹配通过 → allow
  5. 默认passthrough → 转成 ask

免疫 bypass 的检查(即使在 yolo/bypass 下也要弹窗):

  • 内容级 ask 规则(ruleBehavior === 'ask'
  • safetyCheck.git/.claude/ 等敏感路径)

4.4 第三层:模式变换与 auto classifier

规则层返回 ask 后:

模式行为
dontAskaskdeny
auto / plan+autoauto mode classifier(side query)
其他保持 ask,交给 UI

auto 模式 fast-path(跳过 classifier API):

  1. acceptEdits 探测:用虚拟 acceptEdits 模式再调 checkPermissions,若 allow 则直接放行
  2. safe allowlist:Read/Grep 等安全 Tool 直接 allow
  3. 否则 → classifyYoloAction()(YOLO classifier)

4.5 第四层:useCanUseTool(交互/UI)

规则层返回后,useCanUseTool 把决策变成 Promise

1
2
3
4
5
6
7
8
9
10
11
12
flowchart TD
START[hasPermissionsToUseTool 结果] --> ALLOW{behavior=allow?}
ALLOW -->|是| RESOLVE_ALLOW[resolve buildAllow]
ALLOW -->|否| SWITCH{behavior}
SWITCH -->|deny| RESOLVE_DENY[resolve deny]
SWITCH -->|ask| COORD{coordinator / swarm?}
COORD --> SPEC{Bash speculative classifier?}
SPEC -->|2s 内 high confidence| AUTO_ALLOW[跳过弹窗 allow]
SPEC -->|timeout| DIALOG[handleInteractivePermission]
DIALOG --> QUEUE[PermissionRequest UI]
QUEUE --> USER[用户 Allow/Reject]
USER --> DONE[resolve PermissionDecision]

handleInteractivePermission 要点:

  • toolUseConfirmQueueToolUseConfirm 组件
  • 后台并行跑 PermissionRequest hooks + Bash classifier
  • 用户先点 vs classifier 先出 → resolveOnce 防重复
  • Bridge/Kairos channel 可远程审批

Bash 投机 classifiercheckPermissionsAndCallTool 里的 startSpeculativeClassifierCheck 配合:弹窗前最多等 2s,high confidence match → 静默 allow。

4.6 决策如何回到 Tool 执行

permissionDecision.behavior结果
allowtool.call(),可能带 updatedInputacceptFeedback、粘贴图片
deny / ask 被拒error tool_result,不执行 Tool

五、normalizeMessagesForAPI 合并逻辑

运行时 transcript 里 message 类型很多(progress、attachment、system、virtual…),API 只接受 user / assistant 且格式严格。normalizeMessagesForAPIutils/messages.ts ~1989 行)是 transcript → API payload 的转换器。

5.1 何时调用

  • claude.ts:每次 callModel 发 API 前
  • query.ts:tool result 写入 messages 前(局部 normalize)

5.2 整体流水线

1
2
3
4
5
6
7
8
9
10
11
flowchart LR
IN[原始 messages] --> R1[reorderAttachmentsForAPI]
R1 --> R2[过滤 isVirtual]
R2 --> R3[逐条 walk + merge]
R3 --> R4[relocateToolReferenceSiblings]
R4 --> R5[filterOrphanedThinkingOnly]
R5 --> R6[strip trailing thinking / whitespace]
R6 --> R7[smooshSystemReminderSiblings]
R7 --> R8[sanitizeErrorToolResultContent]
R8 --> R9[appendMessageTag / validateImages]
R9 --> OUT[API-ready messages]

5.3 核心 merge 规则

User message:相邻合并

连续两条 usermergeUserMessages

  • 拼接 content[] 数组
  • text-text 接缝处插入 \n(防 "2 + 2" + "3 + 3""2 + 23 + 3"
  • hoistToolResultstool_result 块必须排在前面(API 要求)
  • attachment 也会 merge 进上一条 user

Assistant message:按 message.id 合并

流式阶段 一 block 一条 assistant;normalize 时 同 id 合并 content

1
2
3
4
5
6
7
8
9
export function mergeAssistantMessages(a, b): AssistantMessage {
return {
...a,
message: {
...a.message,
content: [...a.message.content, ...b.message.content],
},
}
}

向后遍历时 跳过中间的 tool_result user message,所以典型 turn:

1
2
3
4
5
6
7
8
9
流式 yield:
assistant(id=msg_abc, [text])
assistant(id=msg_abc, [thinking])
assistant(id=msg_abc, [tool_use])
user(tool_result)

normalize 后 API 看到:
assistant(id=msg_abc, [text, thinking, tool_use])
user(tool_result)

过滤掉的消息类型

不进入 API:progress、普通 systemlocal_command 例外 → 转 user)、isSyntheticApiErrorMessageisVirtual

Tool input 规范化

assistant 的 tool_use block 会调 normalizeToolInputForAPI;tool search 关闭时 strip caller 等非标准字段。

5.4 后处理(防 API 400)

步骤目的
filterOrphanedThinkingOnlyMessages去掉 compaction 后孤立的 thinking-only assistant
filterTrailingThinkingFromLastAssistant最后一条 assistant 不能有 trailing thinking
filterWhitespaceOnlyAssistantMessages去掉纯空白 assistant
smooshSystemReminderSiblings<system-reminder> 文本 fold 进 tool_result
sanitizeErrorToolResultContenterror tool_result 里不能有 image 等
validateImagesForAPI图片尺寸校验

5.5 运行时细粒度 vs API 粗粒度

阶段assistant 表示
流式 yieldN 条 message,各 1 个 content block
transcript 存储同上(利于 prompt cache byte 匹配)
发 API 前 normalize1 条 message,content 数组拼接

六、三条链路交汇

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
                  ┌─────────────────────────────────┐
用户输入 ────────►│ queryLoop │
│ callModel (stream) │
│ ↓ N 条 assistant (by block) │
│ StreamingToolExecutor │
│ ↓ runToolUse │
│ checkPermissionsAndCallTool│
│ resolveHookPermission │
│ canUseTool ◄── REPL UI │
│ tool.call() │
│ ↓ tool_result user msgs │
│ messages = [...prev, asst, tr] │
│ ↓ normalizeMessagesForAPI │
│ 下一轮 callModel │
└─────────────────────────────────┘

关键文件索引

主题文件
query 主循环src/query.ts
Tool 编排src/services/tools/toolOrchestration.ts
流式 Toolsrc/services/tools/StreamingToolExecutor.ts
单 Tool 执行src/services/tools/toolExecution.ts
Hook 权限桥接src/services/tools/toolHooks.ts
canUseToolsrc/hooks/useCanUseTool.tsx
规则引擎src/utils/permissions/permissions.ts
交互弹窗src/hooks/toolPermission/handlers/interactiveHandler.ts
API 流式src/services/api/claude.ts
流式 UI + normalizesrc/utils/messages.ts
Tool 接口src/Tool.ts

小结

Claude Code 的 queryLoop + Tool 子系统可以概括为:

  1. queryLoop 七 Phase:初始化 → 预处理(snip/microcompact/collapse/autocompact)→ setup → API 流式 → 无 Tool 收尾 / Tool 执行 → 附件 prefetch → 递归 continue
  2. needsFollowUp:流式收到 tool_use 即 true,不依赖 stop_reason
  3. 执行管线:Zod → validateInput → PreHook → canUseTool → call → PostHook → tool_result
  4. 并发策略:无 RW lock、无依赖图;isConcurrencySafe + batch/队列
  5. 流式拼接:delta 累加 → content_block_stop yield;normalize 时按 message.id merge
  6. 权限链:Hook 不 bypass deny/ask → 规则引擎 → auto classifier / 弹窗

下一篇:第四篇 专讲 上下文预处理全链路(budget / snip / microcompact / collapse / autocompact / cache editing)。再往后可写 compact 摘要 agent 内部流程reactive compact + 413 恢复