Claude Code 源码解析笔记
前言
阅读Claude Code源码,并不是说要将它50w+行代码全部消化理解,我们所要学习的是它的中心思维,它的核心中的核心,所以这次阅读源码的目标是: 读懂 Claude Code 里真正的 AI 应用核心,并最终用 Python 做出一个简化版的cc(美好愿景,后续不知道能不能做到哈哈哈)
重点不放在前端 UI,而放在它的模型编排,tool的调用,内部的核心机制等等,相信学习源码过后,自己的AI应用开发能力会更上一层楼!!!
Phase 1:Agent 最小闭环
1. 阅读目标
Phase 1 先只追这条主线:
text
state.messages
-> messagesForQuery
-> deps.callModel(...)
-> assistantMessages
-> toolUseBlocks
-> runTools(...)
-> toolResults
-> state.messages = messagesForQuery + assistantMessages + toolResults
-> continue 下一轮
最重要的理解:
text
模型不会真的执行工具。
模型只会输出 tool_use。
本地 runtime 看到 tool_use 后执行工具。
工具结果变成 tool_result。
tool_result 再作为下一轮 messages 的一部分发给模型。
2. src/query.ts 文件结构
src/query.ts 的顶层结构大致是:
text
src/query.ts:129-168 yieldMissingToolResultBlocks() 辅助函数
src/query.ts:181-185 isWithheldMaxOutputTokens() 辅助函数
src/query.ts:187-205 QueryParams 类型
src/query.ts:210-223 State 类型
src/query.ts:225-245 query() 外层包装器
src/query.ts:247-1741 queryLoop() 主循环
也就是说,query.ts 的主体几乎全是 queryLoop()。
阅读策略:
text
不要逐行硬啃 queryLoop 的 1500 行。
先按 agent loop 主线分段读。
支线如 compact、fallback、hook、token budget 先知道位置和作用,后续再深挖。
3. QueryParams:启动 Agent Loop 的参数包
位置:src/query.ts:187-205
ts
export type QueryParams = {
messages: Message[]
systemPrompt: SystemPrompt
userContext: { [k: string]: string }
systemContext: { [k: string]: string }
canUseTool: CanUseToolFn
toolUseContext: ToolUseContext
fallbackModel?: string
querySource: QuerySource
maxOutputTokensOverride?: number
maxTurns?: number
skipCacheWrite?: boolean
taskBudget?: { total: number }
deps?: QueryDeps
}
QueryParams 不是用户最新输入的一句话,而是启动一次 agent loop 需要的完整参数包。
最关键的三个字段:
messages:当前对话历史,不是单条 prompt。canUseTool:工具权限判断函数。模型只能请求工具,是否允许执行由本地 runtime 决定。toolUseContext:工具执行上下文,包含工具列表、权限状态、abortController、agentId 等运行环境。
用 C++ 类比:
cpp
struct QueryParams {
std::vector<Message> messages;
SystemPrompt systemPrompt;
std::unordered_map<std::string, std::string> userContext;
std::unordered_map<std::string, std::string> systemContext;
CanUseToolFn canUseTool;
ToolUseContext toolUseContext;
std::optional<std::string> fallbackModel;
QuerySource querySource;
std::optional<int> maxOutputTokensOverride;
std::optional<int> maxTurns;
std::optional<bool> skipCacheWrite;
std::optional<TaskBudget> taskBudget;
std::optional<QueryDeps> deps;
};
为什么不能只传用户的一句话?
因为 agent 要继续工作时,除了用户输入,还要知道:
text
之前聊了什么?
当前 system prompt 是什么?
当前有哪些工具?
工具权限是什么?
是不是 subagent?
最多还能跑几轮?
有没有 fallback model?
有没有 compact / token budget 状态?
所以更像:
cpp
QueryParams params = {
.messages = 当前完整对话历史,
.systemPrompt = 系统提示词,
.userContext = 用户环境信息,
.systemContext = 系统环境信息,
.canUseTool = 工具权限判断函数,
.toolUseContext = 工具运行环境,
.querySource = 这次 query 来自哪里,
.maxTurns = 最大循环轮数,
};
query(params);
一句话总结:
text
QueryParams = 让 agent loop 能从当前状态继续跑下去的一整包输入。
4. State:每一轮循环之间传递的状态
位置:src/query.ts:210-223
ts
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
}
QueryParams 是外部传入的初始参数。
State 是 queryLoop() 内部 while(true) 每一轮之间传递的可变状态。
Phase 1 最重要的是:
messagestoolUseContext
因为 agent loop 每一轮最核心的变化就是:
text
messages 变长,或者被 compact 后替换;
toolUseContext 可能因为工具执行而更新。
例子:
text
第一轮:
messages = [
user: "读一下 src/query.ts"
]
模型返回:
assistant: tool_use Read(src/query.ts)
本地工具执行:
user/tool_result: "文件内容是..."
第二轮:
messages = [
user: "读一下 src/query.ts",
assistant: tool_use Read(src/query.ts),
user/tool_result: "文件内容是..."
]
C++ 简化版:
cpp
State state;
while (true) {
auto messages = state.messages;
auto toolUseContext = state.toolUseContext;
Response response = callModel(messages);
if (response.isText()) {
return Completed;
}
if (response.isToolUse()) {
ToolResult result = runTool(response.toolUse);
state.messages.push_back(response.assistantMessage);
state.messages.push_back(result.toMessage());
continue;
}
}
5. query():外层包装器
位置:src/query.ts:225-245
ts
export async function* query(
params: QueryParams,
): AsyncGenerator<
| StreamEvent
| RequestStartEvent
| Message
| TombstoneMessage
| ToolUseSummaryMessage,
Terminal
> {
const consumedCommandUuids: string[] = []
const terminal = yield* queryLoop(params, consumedCommandUuids)
for (const uuid of consumedCommandUuids) {
notifyCommandLifecycle(uuid, 'completed')
}
return terminal
}
query() 是异步生成器函数。
它运行过程中可以多次 yield 中间事件,最后 return terminal。
这里的 Terminal 不是终端窗口,而是"这次 query 为什么结束",例如:
text
completed
model_error
aborted_tools
max_turns
核心代码:
ts
const terminal = yield* queryLoop(params, consumedCommandUuids)
意思是:
text
query() 把 queryLoop() 产生的所有中间事件原样转发出去。
queryLoop() 正常结束后,query() 拿到它的 return 值 terminal。
C++ 协程类比:
cpp
using QueryEvent = std::variant<
StreamEvent,
RequestStartEvent,
Message,
TombstoneMessage,
ToolUseSummaryMessage
>;
AsyncGenerator<QueryEvent, Terminal> query(QueryParams params) {
std::vector<std::string> consumedCommandUuids;
Terminal terminal =
co_yield_from queryLoop(params, consumedCommandUuids);
for (const std::string& uuid : consumedCommandUuids) {
notifyCommandLifecycle(uuid, "completed");
}
co_return terminal;
}
一句话:
text
query() 是外层代理生成器。
真正的 agent loop 在 queryLoop()。
6. queryLoop() 总览
位置:src/query.ts:247-1741
queryLoop() 是 Agent Loop 的心脏。它太长,所以先用主线地图看:
text
1. 初始化 state
2. while(true) 进入 agent loop
3. 从 state 取出本轮状态
4. 准备 messagesForQuery
5. 做上下文处理:tool_result budget / snip / microcompact / collapse / autocompact
6. 初始化本轮临时容器:assistantMessages / toolResults / toolUseBlocks
7. 调用模型并流式接收 assistant message
8. 从 assistant message 中收集 tool_use
9. 如果没有 tool_use,走 completed 路径
10. 如果有 tool_use,执行工具
11. 收集工具产生的 tool_result
12. 注入附件类上下文
13. 构造下一轮 State
14. continue 回到 while 顶部
极简 C++ 伪代码:
cpp
Terminal queryLoop(QueryParams params) {
State state = initState(params);
while (true) {
auto messagesForQuery = prepareMessages(state.messages);
std::vector<AssistantMessage> assistantMessages;
std::vector<ToolUseBlock> toolUseBlocks;
std::vector<Message> toolResults;
callModelAndCollect(
messagesForQuery,
assistantMessages,
toolUseBlocks
);
if (toolUseBlocks.empty()) {
return Completed;
}
runToolsAndCollectResults(toolUseBlocks, toolResults);
state.messages = concat(
messagesForQuery,
assistantMessages,
toolResults
);
continue;
}
}
7. queryLoop() 入口与 State 初始化
7.1 函数头和固定参数
位置:src/query.ts:247-269
ts
async function* queryLoop(
params: QueryParams,
consumedCommandUuids: string[],
): AsyncGenerator<
| StreamEvent
| RequestStartEvent
| Message
| TombstoneMessage
| ToolUseSummaryMessage,
Terminal
> {
const {
systemPrompt,
userContext,
systemContext,
canUseTool,
fallbackModel,
querySource,
maxTurns,
skipCacheWrite,
} = params
const deps = params.deps ?? productionDeps()
queryLoop() 也是异步生成器。它会边执行边 yield 事件,最后返回 Terminal。
consumedCommandUuids 是外层 query() 传入的数组。TypeScript 数组是引用对象,所以 queryLoop() 内部 push 进去的 uuid,外层 query() 结束时还能看到。
C++ 类比时要用引用:
cpp
AsyncGenerator<QueryEvent, Terminal> queryLoop(
QueryParams params,
std::vector<std::string>& consumedCommandUuids
);
deps 是依赖注入:
ts
const deps = params.deps ?? productionDeps()
意思是:
text
测试时可以传假的 deps;
生产环境默认用 productionDeps()。
7.2 初始化 State
位置:src/query.ts:271-286
ts
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,
}
这一步把外部参数里的 messages 和 toolUseContext 放入内部状态。
源码注释说:
text
Continue sites write `state = { ... }` instead of 9 separate assignments.
意思是:后面进入下一轮时,不是单独改字段,而是整体构造新 state:
ts
state = {
messages: newMessages,
toolUseContext: newContext,
...
}
continue
这个风格让每个 continue 分支都明确表达:"下一轮到底带着什么状态继续"。
8. while(true):每一轮从 State 取当前状态
位置:src/query.ts:312-327
ts
while (true) {
let { toolUseContext } = state
const {
messages,
autoCompactTracking,
maxOutputTokensRecoveryCount,
hasAttemptedReactiveCompact,
maxOutputTokensOverride,
pendingToolUseSummary,
stopHookActive,
turnCount,
} = state
while(true) 是 agent loop 主体。
它不是死循环 bug,而是:
text
只要模型还在请求 tool_use,就继续循环;
直到模型没有 tool_use,才 completed。
toolUseContext 用 let,因为本轮内部可能重新赋值。
其他字段用 const,本轮内通常不直接改;如果需要进入下一轮,会整体重建 state。
9. 准备 messagesForQuery
位置:src/query.ts:371-373
ts
let messagesForQuery = [...getMessagesAfterCompactBoundary(messages)]
let tracking = autoCompactTracking
这里要分清:
text
messages
= state.messages,完整历史 / 当前长期状态
messagesForQuery
= 本轮模型实际输入的工作副本
messagesForQuery 后面会继续被裁剪、压缩、预算处理。
9.1 getMessagesAfterCompactBoundary()
定义位置:src/utils/messages.ts:4643-4656
ts
export function getMessagesAfterCompactBoundary<
T extends Message | NormalizedMessage,
>(messages: T[], options?: { includeSnipped?: boolean }): T[] {
const boundaryIndex = findLastCompactBoundaryIndex(messages)
const sliced = boundaryIndex === -1 ? messages : messages.slice(boundaryIndex)
if (!options?.includeSnipped && feature('HISTORY_SNIP')) {
const { projectSnippedView } =
require('../services/compact/snipProjection.js') as typeof import('../services/compact/snipProjection.js')
return projectSnippedView(sliced as Message[]) as T[]
}
return sliced
}
它的作用:
text
如果没有 compact boundary,返回全部 messages。
如果有 compact boundary,只返回最后一个 boundary 之后的消息。
查找 boundary 的函数在 src/utils/messages.ts:4618-4629:
ts
export function findLastCompactBoundaryIndex<
T extends Message | NormalizedMessage,
>(messages: T[]): number {
for (let i = messages.length - 1; i >= 0; i--) {
const message = messages[i]
if (message && isCompactBoundaryMessage(message)) {
return i
}
}
return -1
}
boundary 判断在 src/utils/messages.ts:4608-4612:
ts
export function isCompactBoundaryMessage(
message: Message | NormalizedMessage,
): message is SystemCompactBoundaryMessage {
return message?.type === 'system' && message.subtype === 'compact_boundary'
}
一句话:
text
Claude Code 不一定把完整历史都发给模型。
它先从最近一次 compact 边界之后取消息,形成本轮模型输入基础。
10. 上下文处理:继续加工 messagesForQuery
从 src/query.ts:375-549,代码连续做了几类上下文处理。
它们都在处理同一个对象:
text
messagesForQuery
可以理解成流水线:
text
state.messages
-> getMessagesAfterCompactBoundary
-> messagesForQuery
-> applyToolResultBudget
-> HISTORY_SNIP
-> microcompact
-> contextCollapse
-> autocompact
-> 最终本轮发给模型的 messagesForQuery
10.1 applyToolResultBudget:限制工具结果大小
位置:src/query.ts:375-400
ts
messagesForQuery = await applyToolResultBudget(
messagesForQuery,
toolUseContext.contentReplacementState,
persistReplacements
? records =>
void recordContentReplacement(
records,
toolUseContext.agentId,
).catch(logError)
: undefined,
new Set(
toolUseContext.options.tools
.filter(t => !Number.isFinite(t.maxResultSizeChars))
.map(t => t.name),
),
)
解决的问题:
text
工具结果可能很大。
比如 Read 读了一个 5MB 文件,或者 Grep 返回几万行。
如果完整放入下一轮 messages,模型上下文会爆。
所以它专门处理 tool_result 的体积。
10.2 HISTORY_SNIP:裁剪历史视图
位置:src/query.ts:402-416
ts
let snipTokensFreed = 0
if (feature('HISTORY_SNIP')) {
queryCheckpoint('query_snip_start')
const snipResult = snipModule!.snipCompactIfNeeded(messagesForQuery)
messagesForQuery = snipResult.messages
snipTokensFreed = snipResult.tokensFreed
if (snipResult.boundaryMessage) {
yield snipResult.boundaryMessage
}
queryCheckpoint('query_snip_end')
}
作用:
text
模型调用前进一步减少 messagesForQuery。
它偏向"裁剪模型看到的历史视图"。
snipTokensFreed 会被后面的 autocompact 使用,用来判断前面已经释放了多少 token。
10.3 microcompact:小粒度压缩
位置:src/query.ts:418-432
ts
const microcompactResult = await deps.microcompact(
messagesForQuery,
toolUseContext,
querySource,
)
messagesForQuery = microcompactResult.messages
const pendingCacheEdits = feature('CACHED_MICROCOMPACT')
? microcompactResult.compactionInfo?.pendingCacheEdits
: undefined
它通过 deps.microcompact 注入。
先记住:
text
microcompact 是 autocompact 之前的小粒度上下文压缩/整理。
10.4 contextCollapse:折叠上下文视图
位置:src/query.ts:434-453
ts
if (feature('CONTEXT_COLLAPSE') && contextCollapse) {
const collapseResult = await contextCollapse.applyCollapsesIfNeeded(
messagesForQuery,
toolUseContext,
querySource,
)
messagesForQuery = collapseResult.messages
}
注释里说它是:
text
collapsed view
read-time projection
它不是简单删除,也不一定是生成一个大 summary,而是在读取时把上下文投影成折叠后的视图。
它放在 autocompact 前面。如果折叠后上下文已经足够小,后面的 autocompact 可能就不需要跑。
10.5 autocompact:兜底的大型自动压缩
位置:src/query.ts:455-549
先构造完整 system prompt:
ts
const fullSystemPrompt = asSystemPrompt(
appendSystemContext(systemPrompt, systemContext),
)
再调用 autocompact:
ts
const { compactionResult, consecutiveFailures } = await deps.autocompact(
messagesForQuery,
toolUseContext,
{
systemPrompt,
userContext,
systemContext,
toolUseContext,
forkContextMessages: messagesForQuery,
},
querySource,
tracking,
snipTokensFreed,
)
如果成功:
位置:src/query.ts:534-542
ts
const postCompactMessages = buildPostCompactMessages(compactionResult)
for (const message of postCompactMessages) {
yield message
}
messagesForQuery = postCompactMessages
也就是:
text
把压缩结果变成 postCompactMessages;
yield 给外部;
用 postCompactMessages 替换 messagesForQuery。
如果失败:
位置:src/query.ts:542-549
ts
} else if (consecutiveFailures !== undefined) {
tracking = {
...(tracking ?? { compacted: false, turnId: '', turnCounter: 0 }),
consecutiveFailures,
}
}
失败时不改 messagesForQuery,只更新失败计数,避免后续无限重试。
10.6 这些上下文处理的区别
| 机制 | 处理对象 | 主要目的 | 粒度 | 你可以怎么记 |
|---|---|---|---|---|
applyToolResultBudget |
tool_result |
防止工具输出撑爆上下文 | 工具结果级 | 管工具输出 |
HISTORY_SNIP |
历史视图 | 裁剪模型看到的旧历史 | 历史片段级 | 管历史裁剪 |
microcompact |
局部 messages | 小规模压缩/缓存友好处理 | 小粒度 | 小压缩 |
contextCollapse |
上下文视图 | 折叠旧上下文,保留可投影结构 | 视图级 | 折叠视图 |
autocompact |
整体上下文 | 上下文太长时总结压缩 | 大粒度 | 兜底压缩 |
11. 同步 messagesForQuery 到 toolUseContext
位置:src/query.ts:551-555
ts
toolUseContext = {
...toolUseContext,
messages: messagesForQuery,
}
这一步不是更新 state.messages。
它只是把本轮处理后的模型输入上下文同步到当前 toolUseContext,让后面的工具执行、权限判断、hook、subagent 等逻辑能看到本轮实际使用的 messages。
12. 初始化本轮临时容器
位置:src/query.ts:557-564
ts
const assistantMessages: AssistantMessage[] = []
const toolResults: (UserMessage | AttachmentMessage)[] = []
const toolUseBlocks: ToolUseBlock[] = []
let needsFollowUp = false
这四个变量是 Agent Loop 主线的关键。
| 变量 | 作用 |
|---|---|
assistantMessages |
收集本轮模型返回的 assistant message |
toolUseBlocks |
收集 assistant message 里的 tool_use |
toolResults |
收集工具执行后产生的 tool_result 和后续附件 |
needsFollowUp |
标记本轮是否需要继续执行工具 / 进入下一轮 |
重要协议:
text
assistant 产生 tool_use。
本地 runtime 执行工具。
工具结果作为 user message 中的 tool_result 回填给模型。
13. 调模型前准备
位置:src/query.ts:566-586
ts
const useStreamingToolExecution = config.gates.streamingToolExecution
let streamingToolExecutor = useStreamingToolExecution
? new StreamingToolExecutor(
toolUseContext.options.tools,
canUseTool,
toolUseContext,
)
: null
const appState = toolUseContext.getAppState()
const permissionMode = appState.toolPermissionContext.mode
let currentModel = getRuntimeMainLoopModel({
permissionMode,
mainLoopModel: toolUseContext.options.mainLoopModel,
exceeds200kTokens:
permissionMode === 'plan' &&
doesMostRecentAssistantMessageExceed200k(messagesForQuery),
})
这里还没有真正调用模型,只是在准备:
- 是否启用
StreamingToolExecutor - 本轮实际使用哪个模型
currentModel - 当前权限模式
permissionMode
14. 调用模型:deps.callModel
位置:src/query.ts:661-719
ts
let attemptWithFallback = true
while (attemptWithFallback) {
attemptWithFallback = false
try {
let streamingFallbackOccured = false
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: {
async getToolPermissionContext() {
const appState = toolUseContext.getAppState()
return appState.toolPermissionContext
},
model: currentModel,
fallbackModel,
querySource,
...
},
})) {
...
}
} catch (innerError) {
...
}
}
这里有两个循环,不要混:
text
外层 while(true):agent loop。
内层 while(attemptWithFallback):当前这一轮模型调用的 fallback 重试。
真正的模型调用是:
ts
for await (const message of deps.callModel(...)) {
这说明 callModel() 也是异步流,不是一次性返回完整答案。
核心参数:
messages:prependUserContext(messagesForQuery, userContext)systemPrompt: 完整 system prompttools: 当前可用工具列表model: 当前模型signal: abort 信号
关键理解:
text
模型每一轮都会收到 messages 和 tools。
模型必须知道 tools,才可能返回 tool_use。
15. 从 assistant 输出中收集 tool_use
位置:src/query.ts:834-856
ts
if (!withheld) {
yield yieldMessage
}
if (message.type === 'assistant') {
assistantMessages.push(message)
const msgToolUseBlocks = message.message.content.filter(
content => content.type === 'tool_use',
) as ToolUseBlock[]
if (msgToolUseBlocks.length > 0) {
toolUseBlocks.push(...msgToolUseBlocks)
needsFollowUp = true
}
if (
streamingToolExecutor &&
!toolUseContext.abortController.signal.aborted
) {
for (const toolBlock of msgToolUseBlocks) {
streamingToolExecutor.addTool(toolBlock, message)
}
}
}
这段做了四件事:
- 正常情况下,把模型消息
yield给外部/UI。 - 如果是 assistant message,保存到
assistantMessages。 - 从 assistant content 中筛出
type === 'tool_use'的 block。 - 如果有 tool_use,加入
toolUseBlocks,并把needsFollowUp置为true。
这就是模型输出到本地 runtime 的关键转折点:
text
模型说:我想调用工具。
本地 runtime 记录下来:待会儿我来执行。
16. Streaming 模式下提前收集工具结果
位置:src/query.ts:858-873
ts
if (
streamingToolExecutor &&
!toolUseContext.abortController.signal.aborted
) {
for (const result of streamingToolExecutor.getCompletedResults()) {
if (result.message) {
yield result.message
toolResults.push(
...normalizeMessagesForAPI(
[result.message],
toolUseContext.options.tools,
).filter(_ => _.type === 'user'),
)
}
}
}
如果启用了 streaming tool execution,那么工具可能在模型还没完全输出结束时就已经开始执行,甚至已经完成。
结果会:
text
yield 给外部/UI
normalize 成 user/tool_result message
push 到 toolResults
17. 没有 tool_use:Agent Loop 结束
位置:src/query.ts:1073-1368
主线入口:
ts
if (!needsFollowUp) {
needsFollowUp 是前面发现 tool_use 时才置为 true 的。
所以:
text
!needsFollowUp
= 模型这一轮没有请求工具
= 不需要 runTools
= agent loop 可以结束
这个分支里有很多工程增强逻辑:
- prompt too long recovery
- media recovery
- max output tokens recovery
- stop hooks
- token budget continuation
但主线最终是:
位置:src/query.ts:1368
ts
return { reason: 'completed' }
18. 有 tool_use:执行工具
如果 needsFollowUp === true,说明本轮 assistant 输出了工具调用请求,于是跳过 completed 分支,继续往下执行工具。
位置:src/query.ts:1371-1394
ts
let shouldPreventContinuation = false
let updatedToolUseContext = toolUseContext
const toolUpdates = streamingToolExecutor
? streamingToolExecutor.getRemainingResults()
: runTools(toolUseBlocks, assistantMessages, canUseTool, toolUseContext)
两条路径:
text
streamingToolExecutor 存在:
工具可能已经执行了一部分,这里取剩余结果。
streamingToolExecutor 不存在:
调用 runTools(...) 执行 toolUseBlocks。
runTools 定义在 src/services/tools/toolOrchestration.ts:19-24:
ts
export async function* runTools(
toolUseMessages: ToolUseBlock[],
assistantMessages: AssistantMessage[],
canUseTool: CanUseToolFn,
toolUseContext: ToolUseContext,
): AsyncGenerator<MessageUpdate, void> {
MessageUpdate 类型在 src/services/tools/toolOrchestration.ts:14-17:
ts
export type MessageUpdate = {
message?: Message
newContext: ToolUseContext
}
19. 收集工具执行产生的 tool_result
位置:src/query.ts:1396-1420
ts
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],
toolUseContext.options.tools,
).filter(_ => _.type === 'user'),
)
}
if (update.newContext) {
updatedToolUseContext = {
...update.newContext,
queryTracking,
}
}
}
这段是工具执行结果进入下一轮模型上下文的关键。
19.1 工具结果先 yield 给外部
ts
yield update.message
这个 yield 是给 UI / transcript / 上层调用方看的,不是直接发给模型。
19.2 工具结果规范化成 user message
ts
toolResults.push(
...normalizeMessagesForAPI(
[update.message],
toolUseContext.options.tools,
).filter(_ => _.type === 'user'),
)
为什么只保留 type === 'user'?
因为工具协议里:
text
assistant 发出 tool_use。
user 回填 tool_result。
所以工具结果要作为 user message 进入下一轮模型输入。
19.3 工具执行可能更新上下文
ts
if (update.newContext) {
updatedToolUseContext = {
...update.newContext,
queryTracking,
}
}
工具执行不仅会产生消息,也可能更新工具上下文。下一轮要用更新后的 updatedToolUseContext。
20. 工具执行后的收尾
位置:src/query.ts:1421-1532
这段还没有进入下一轮,只是在工具执行后做收尾。
20.1 可选生成 tool use summary
位置:src/query.ts:1423-1494
条件:
ts
if (
config.gates.emitToolUseSummaries &&
toolUseBlocks.length > 0 &&
!toolUseContext.abortController.signal.aborted &&
!toolUseContext.agentId
) {
它会把每个 tool_use 和对应 tool_result 配对:
位置:src/query.ts:1450-1477
ts
const toolInfoForSummary = toolUseBlocks.map(block => {
const toolResult = toolResults.find(
result =>
result.type === 'user' &&
Array.isArray(result.message.content) &&
result.message.content.some(
content =>
content.type === 'tool_result' &&
content.tool_use_id === block.id,
),
)
...
return {
name: block.name,
input: block.input,
output:
resultContent && 'content' in resultContent
? resultContent.content
: null,
}
})
核心匹配关系:
text
tool_use.id === tool_result.tool_use_id
这也是工具协议中最重要的关联方式。
20.2 工具阶段被中断
位置:src/query.ts:1496-1528
ts
if (toolUseContext.abortController.signal.aborted) {
if (toolUseContext.abortController.signal.reason !== 'interrupt') {
yield createUserInterruptionMessage({
toolUse: true,
})
}
return { reason: 'aborted_tools' }
}
如果工具执行时用户中断,agent loop 直接结束。
20.3 hook 阻止继续
位置:src/query.ts:1530-1532
ts
if (shouldPreventContinuation) {
return { reason: 'hook_stopped' }
}
如果工具执行过程中某个 hook 要求停止,就不再进入下一轮。
21. 工具结果后的附件注入
位置:src/query.ts:1535-1688
这一段容易误解:它不是普通工具执行,而是把额外上下文附件也放进 toolResults。
关键注释在 src/query.ts:1547-1548:
ts
// Be careful to do this after tool calls are done, because the API
// will error if we interleave tool_result messages with regular user messages.
意思是:
text
必须先收完 tool_result,再加入普通附件/用户上下文消息。
API 不允许 tool_result 和普通 user message 乱序交错。
21.1 queued command attachments
位置:src/query.ts:1592-1602
ts
for await (const attachment of getAttachmentMessages(
null,
updatedToolUseContext,
null,
queuedCommandsSnapshot,
[...messagesForQuery, ...assistantMessages, ...toolResults],
querySource,
)) {
yield attachment
toolResults.push(attachment)
}
这一步会生成附件消息:
text
yield 给外部/UI;
push 到 toolResults;
因此下一轮模型也能看到这些附件。
21.2 memory attachments
位置:src/query.ts:1611-1626
ts
const memoryAttachments = filterDuplicateMemoryAttachments(
await pendingMemoryPrefetch.promise,
toolUseContext.readFileState,
)
for (const memAttachment of memoryAttachments) {
const msg = createAttachmentMessage(memAttachment)
yield msg
toolResults.push(msg)
}
memory prefetch 完成后,也会变成 attachment message,加入 toolResults。
21.3 skill discovery attachments
位置:src/query.ts:1632-1640
ts
const skillAttachments =
await skillPrefetch.collectSkillDiscoveryPrefetch(pendingSkillPrefetch)
for (const att of skillAttachments) {
const msg = createAttachmentMessage(att)
yield msg
toolResults.push(msg)
}
skill discovery 结果也会作为附件进入下一轮上下文。
所以这时 toolResults 的含义变宽了:
text
toolResults =
工具执行结果 tool_result
+ queued command attachments
+ memory attachments
+ skill attachments
+ 其他 attachment messages
21.4 刷新工具列表
位置:src/query.ts:1671-1688
ts
if (updatedToolUseContext.options.refreshTools) {
const refreshedTools = updatedToolUseContext.options.refreshTools()
if (refreshedTools !== updatedToolUseContext.options.tools) {
updatedToolUseContext = {
...updatedToolUseContext,
options: {
...updatedToolUseContext.options,
tools: refreshedTools,
},
}
}
}
const toolUseContextWithQueryTracking = {
...updatedToolUseContext,
queryTracking,
}
进入下一轮前刷新工具列表。比如新的 MCP server 连接后,工具列表可能变化。
22. 闭环完成:构造下一轮 State
位置:src/query.ts:1690-1739
这就是 Phase 1 最小闭环最关键的闭合点。
22.1 turnCount + 1
位置:src/query.ts:1690-1691
ts
// Each time we have tool results and are about to recurse, that's a turn
const nextTurnCount = turnCount + 1
只要有工具结果并准备进入下一轮,就算一个新 turn。
22.2 maxTurns 限制
位置:src/query.ts:1716-1724
ts
if (maxTurns && nextTurnCount > maxTurns) {
yield createAttachmentMessage({
type: 'max_turns_reached',
maxTurns,
turnCount: nextTurnCount,
})
return { reason: 'max_turns', turnCount: nextTurnCount }
}
如果超过最大轮数,就不再继续。
22.3 构造下一轮 State
位置:src/query.ts:1727-1739
ts
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
这行是整个 agent loop 的闭环:
ts
messages: [...messagesForQuery, ...assistantMessages, ...toolResults]
它把三段拼起来:
text
messagesForQuery
本轮模型看到的上下文
assistantMessages
本轮模型输出,包括 text / tool_use
toolResults
本地工具执行结果,包括 user/tool_result 和附件
拼完后写回:
ts
state = next
然后 while(true) 自然进入下一次循环。
C++ 伪代码:
cpp
State next{
.messages = concat(
messagesForQuery,
assistantMessages,
toolResults
),
.toolUseContext = toolUseContextWithQueryTracking,
.turnCount = turnCount + 1,
.transition = Continue{ .reason = "next_turn" },
};
state = next;
// while(true) 下一轮开始
这就是最小闭环:
text
模型 tool_use
-> 本地 runTools
-> tool_result
-> 写回 state.messages
-> 下一轮 callModel
22. Phase1总结
最后以一张完整流程图结束Phase1阶段Claude源码的阅读
text
query(params)
|
v
queryLoop(params)
|
v
State 初始化
|
v
while (true)
|
v
messagesForQuery = getMessagesAfterCompactBoundary(state.messages)
|
v
上下文处理:budget / snip / microcompact / collapse / autocompact
|
v
deps.callModel(messagesForQuery, tools, systemPrompt, ...)
|
v
assistantMessages.push(message)
|
v
从 assistant content 中提取 tool_use -> toolUseBlocks
|
+-- 没有 tool_use --> return completed
|
+-- 有 tool_use
|
v
runTools(toolUseBlocks, ...)
|
v
toolResults.push(user/tool_result)
|
v
state.messages =
messagesForQuery + assistantMessages + toolResults
|
v
continue 下一轮