文章比较长,电脑端也可以直接打开 cc.windliang.wang 查看
第 4 篇走到 runToolUse() 时,每个模型返回的 tool_use 都会交给对应 Tool。Agent 也在这张 Tool 表里,call() 内部会再启动一次 query()。
这里的子 Agent 指父 Agent 通过 Agent Tool 启动的一段独立会话。它有自己的消息历史和 Tool 循环;同步运行结束后,父 Agent 收到的是这次 Agent Tool Use 对应的 Tool Result。
假设当前任务是检查登录失败为什么没有留下审计日志。父 Agent 先看到了用户输入和前面已经做过的工作,随后模型返回下面这次 Tool Use:
// 函数体:src/query.ts::queryLoop()asyncfunction* queryLoop(state) {// ...forawait (const message of callModelAndForwardMessages()) {if (message.type === 'assistant') {const msgToolUseBlocks = message.message.content.filter(block => block.type === 'tool_use', )// 运行值 message.message.content => [// {// type: 'tool_use',// id: 'toolu_agent_01',// name: 'Agent',// input: {// subagent_type: 'Explore',// description: '查找审计日志调用链',// prompt: '只读检查登录失败路径,找出审计日志写入位置和缺失位置。返回涉及的文件与调用关系。',// },// },// ] toolUseBlocks.push(...msgToolUseBlocks) needsFollowUp = true } }// 源码位置:src/query.ts:1380-1395const toolUpdates = runTools( toolUseBlocks, assistantMessages, canUseTool, toolUseContext, )forawait (const update of toolUpdates) {yield update }// ...}Agent Tool 由父循环执行。它创建的子 Agent 可以读取、搜索、调用模型并完成自己的多轮循环;父循环最后收到的是一份 Tool Result,其中保存子 Agent 的最终文本结论,部分 Agent 还会附带使用统计。

这篇沿着图中的同步 Explore 路径向下走。后面再对照两条容易混在一起的分支:继承父对话的 fork,以及已停止子 Agent 的 resume。
1. 从 runToolUse() 进入 AgentTool.call()
第 4 篇中,runToolUse() 会根据 toolUse.name 找到 Tool,再把完整 Tool Use 的 input 传给 tool.call()。这次调用因此落在 AgentTool.call()。
源码位置:src/services/tools/toolExecution.ts、src/tools/AgentTool/AgentTool.tsx:196-499
// 函数体:src/tools/AgentTool/AgentTool.tsx::AgentTool.call()// 源码位置:230-245asyncfunctioncall( { prompt, subagent_type, description, run_in_background, }, toolUseContext, canUseTool, assistantMessage,) {// prompt => '只读检查登录失败路径,找出审计日志写入位置和缺失位置。返回涉及的文件与调用关系。'// subagent_type => 'Explore'// description => '查找审计日志调用链'// run_in_background => undefined// assistantMessage.message.content 中包含:// {// type: 'tool_use',// id: 'toolu_agent_01',// name: 'Agent',// input: {// subagent_type: 'Explore',// description: '查找审计日志调用链',// prompt: '只读检查登录失败路径,找出审计日志写入位置和缺失位置。返回涉及的文件与调用关系。',// },// }}这次调用先根据 subagent_type 在启动阶段加载的 agentDefinitions.activeAgents 中查找定义。Explore 是内置的代码探索 Agent:定义明确禁用 Agent、Edit、Write、NotebookEdit 和 ExitPlanMode,并使用面向搜索与报告的 System Prompt。
源码位置:src/tools/AgentTool/AgentTool.tsx:339-387、src/tools/AgentTool/built-in/exploreAgent.ts:59-77
// 函数体:src/tools/AgentTool/AgentTool.tsx::AgentTool.call()// 源码位置:339-387asyncfunctioncall({ subagent_type }, toolUseContext) {// ...const effectiveType = subagent_type ?? GENERAL_PURPOSE_AGENT.agentType// subagent_type => 'Explore'// effectiveType => 'Explore'const allAgents = toolUseContext.options.agentDefinitions.activeAgents// => [// {// agentType: 'general-purpose',// tools: ['*'],// model: undefined,// source: 'built-in',// },// {// agentType: 'Explore',// disallowedTools: ['Agent', 'Edit', 'Write', 'NotebookEdit', 'ExitPlanMode'],// model: 'haiku',// omitClaudeMd: true,// source: 'built-in',// },// ]const selectedAgent = allAgents.find(agent => agent.agentType === effectiveType, )// => {// agentType: 'Explore',// disallowedTools: ['Agent', 'Edit', 'Write', 'NotebookEdit', 'ExitPlanMode'],// model: 'haiku',// omitClaudeMd: true,// source: 'built-in',// }// ...}Agent Tool 根据名称选择并启动 Agent 定义;子 Agent 的 System Prompt、工具集合、模型和权限模式由这份定义提供。Explore 的定义同时给出面向探索的提示和禁用 Tool 列表。
2. 普通子 Agent 的消息历史从新任务开始
选定 Explore 后,AgentTool.call() 先把 prompt 包装成一条新的 User Message,再把它传给 runAgent()。普通路径的 forkContextMessages 为 undefined,因此不会把父会话的历史消息拼进来。
源码位置:src/tools/AgentTool/AgentTool.tsx:512-540、src/tools/AgentTool/AgentTool.tsx:603-636
// 函数体:src/tools/AgentTool/AgentTool.tsx::AgentTool.call()// 源码位置:512-540、603-636asyncfunctioncall({ prompt }, toolUseContext, canUseTool) {const promptMessages = [ createUserMessage({content: prompt, }), ]// prompt => '只读检查登录失败路径,找出审计日志写入位置和缺失位置。返回涉及的文件与调用关系。'// promptMessages => [// {// type: 'user',// isMeta: false,// uuid: 'user_agent_01',// message: {// role: 'user',// content: '只读检查登录失败路径,找出审计日志写入位置和缺失位置。返回涉及的文件与调用关系。',// },// },// ]const runAgentParams = {agentDefinition: selectedAgent, promptMessages, toolUseContext, canUseTool,isAsync: shouldRunAsync,querySource: toolUseContext.options.querySource ?? getQuerySourceForAgent( selectedAgent.agentType, isBuiltInAgent(selectedAgent), ),availableTools: workerTools,forkContextMessages: undefined, }// shouldRunAsync => false// querySource => 'agent:builtin:Explore'// forkContextMessages => undefinedreturn runAgent(runAgentParams)}「从新任务开始」只描述消息历史,不表示请求只有这一条消息。runAgent() 仍会构建子 Agent 自己的 System Prompt、userContext 和 systemContext;普通路径不带入的是父 Agent 已经积累的用户对话、Tool Use 和 Tool Result。
接着看 runAgent() 怎样把这份输入变成独立的 ToolUseContext:
源码位置:src/tools/AgentTool/runAgent.ts:368-379、src/tools/AgentTool/runAgent.ts:697-757
// 函数体:src/tools/AgentTool/runAgent.ts::runAgent()// 源码位置:368-379、697-757asyncfunction* runAgent({ toolUseContext, promptMessages, forkContextMessages, override, agentDefinition, availableTools, isAsync, agentGetAppState,}) {const agentId = override?.agentId ?? createAgentId()// agentId => 'agent_explore_01'const contextMessages = forkContextMessages ? filterIncompleteToolCalls(forkContextMessages) : []// forkContextMessages => undefined// contextMessages => []const initialMessages = [ ...contextMessages, ...promptMessages, ]// initialMessages => [// {// type: 'user',// message: {// role: 'user',// content: '只读检查登录失败路径,找出审计日志写入位置和缺失位置。返回涉及的文件与调用关系。',// },// },// ]const agentReadFileState = forkContextMessages !== undefined ? cloneFileStateCache(toolUseContext.readFileState) : createFileStateCacheWithSizeLimit(READ_FILE_STATE_CACHE_SIZE)// agentReadFileState => 空的文件状态缓存const resolvedTools = resolveAgentTools( agentDefinition, availableTools, isAsync, ).resolvedTools// resolvedTools 不包含 Explore 禁用的 Agent、Edit、Write、// NotebookEdit 与 ExitPlanMode。const agentToolUseContext = createSubagentContext(toolUseContext, {options: { ...toolUseContext.options, tools: resolvedTools }, agentId,agentType: agentDefinition.agentType,messages: initialMessages,readFileState: agentReadFileState,abortController: toolUseContext.abortController,getAppState: agentGetAppState,shareSetAppState: !isAsync, })// agentToolUseContext.agentId => 'agent_explore_01'// agentToolUseContext.messages => initialMessages// agentToolUseContext.queryTracking.depth => 1// 后面省略:构建 System Prompt、userContext、systemContext,// 再调用 query()。}普通路径的文件状态缓存从空开始,消息历史只含这次任务;第 6 节的 fork 路径才会传入父会话消息,并克隆父 Agent 的文件状态缓存。
3. runAgent() 仍然调用同一个 query()
上下文准备好后,runAgent() 进入第 2 篇中的 query()。子 Agent 也会走「模型返回 Tool Use → 执行 Tool → 下一轮模型请求」的循环,只是使用它自己的 System Prompt、工具池和消息数组。
源码位置:src/tools/AgentTool/runAgent.ts:747-805
// 函数体:src/tools/AgentTool/runAgent.ts::runAgent()// 源码位置:747-805asyncfunction* runAgent({ initialMessages, agentSystemPrompt, userContext, systemContext, canUseTool, agentToolUseContext, querySource, maxTurns,}) {// agentToolUseContext.agentId => 'agent_explore_01'// 先保存子 Agent 的初始消息。void recordSidechainTranscript( initialMessages, agentToolUseContext.agentId, )// 初始文件路径形如:// ~/.claude/projects/-Users-me-shop/session_01/subagents/agent-agent_explore_01.jsonl// 文件中的第一条消息是:// {// type: 'user',// message: {// role: 'user',// content: '只读检查登录失败路径,找出审计日志写入位置和缺失位置。返回涉及的文件与调用关系。',// },// }let lastRecordedUuid = initialMessages.at(-1)?.uuid ?? null// lastRecordedUuid => 'user_agent_01'forawait (const message of query({messages: initialMessages,// => [// {// type: 'user',// message: {// role: 'user',// content: '只读检查登录失败路径,找出审计日志写入位置和缺失位置。返回涉及的文件与调用关系。',// },// },// ]systemPrompt: agentSystemPrompt,// => Explore 的只读搜索提示。 userContext, systemContext, canUseTool,toolUseContext: agentToolUseContext, querySource,// querySource => 'agent:builtin:Explore' maxTurns, })) {if (isRecordableMessage(message)) {await recordSidechainTranscript( [message], agentToolUseContext.agentId, lastRecordedUuid, )yield message } }// ...}一次可能的子循环如下。父 Agent 在等待这次 Agent Tool 返回;Explore 在自己的循环中完成 Grep 和 Read。
// 函数体:src/tools/AgentTool/runAgent.ts::runAgent() 调用的 query() 循环。子 Agent 第 1 轮:Grep('audit|auditLog', 'src')子 Agent 第 2 轮:Read('src/auth/login.ts') 与 Read('src/services/audit.ts')子 Agent 第 3 轮:返回文本结论每条可记录消息都会写入 agent_explore_01 对应的 sidechain transcript。它保存的是子 Agent 自己的会话记录,和主会话的 transcript 分开。第 7 节中可继续的 general-purpose Agent 也使用同一种 transcript 保存过程。
sidechain transcript 就是这份按子 Agent 单独保存的消息链。getAgentTranscriptPath() 把它放在当前会话目录的 subagents/agent-{agentId}.jsonl 文件中。runAgent() 开始时先写入 initialMessages,循环中每产出一条可记录消息再追加一次;主会话的 transcript 不会混入这条链。之后 resumeAgentBackground() 通过同一个 agentId 读取文件,重建这个子 Agent 自己的历史消息,再追加追问。
源码位置:src/tools/AgentTool/runAgent.ts:731-798、src/utils/sessionStorage.ts:247-262、src/utils/sessionStorage.ts:1451-1460、src/utils/sessionStorage.ts:4190-4234
从调用关系能直接看到,普通子 Agent 复用了完整的 query() 循环,同时把「任务范围」「可见消息」「可用 Tool」放进独立容器。父会话只等待它的收口结果。
4. 子 Agent 的结果怎样回到父循环
第 3 节追的是 AgentTool.call() 内部启动的 runAgent();第 4 节回到父会话的调用栈。两者之间的关系是:
父 queryLoop()把模型给出的AgentTool Use 交给runTools();runTools()再调用runToolUse();runToolUse()经过通用校验后调用AgentTool.call();AgentTool.call()启动并消费runAgent();runAgent()结束后,调用链反向返回,最终由runToolUse()向父queryLoop()产出 Tool Result。
因此,runToolUse() 是父会话执行一次 Tool 调用的入口,runAgent() 是这次调用内部运行子会话的函数。第 3 节讲第 4 步内部的子循环;这一节讲它结束后怎样沿原调用链回到父循环。
图中上半部分是第 1~3 节已经走过的「进入子会话」路径;下半部分从 runAgent() 结束开始,依次回到 AgentTool.call()、runToolUse()、runTools(),最后才回到父 queryLoop()。

下面从 runToolUse() 开始。它收到 toolu_agent_01 后按名称找到 AgentTool,再把调用交给通用执行函数。
源码位置:src/services/tools/toolExecution.ts:337-489
// 函数体:src/services/tools/toolExecution.ts::runToolUse()// 源码位置:337-489exportasyncfunction* runToolUse( toolUse, assistantMessage, canUseTool, toolUseContext,) {const tool = findToolByName( toolUseContext.options.tools, toolUse.name, )// toolUse => {// type: 'tool_use',// id: 'toolu_agent_01',// name: 'Agent',// input: {// subagent_type: 'Explore',// prompt: '检查登录失败路径是否写入安全审计日志。',// },// }// tool.name => 'Agent'// 前面还会处理未知 Tool 与用户中断。// ...forawait (const update of streamedCheckPermissionsAndCallTool( tool, toolUse.id, toolUse.input, toolUseContext, canUseTool, assistantMessage, assistantMessage.message.id, assistantMessage.requestId, getMcpServerType(toolUse.name, toolUseContext.options.mcpClients), getMcpServerBaseUrlFromToolName( toolUse.name, toolUseContext.options.mcpClients, ), )) {yield update }}streamedCheckPermissionsAndCallTool() 只是把进度与最终结果合成可迭代输出;它会调用 checkPermissionsAndCallTool()。权限、Hook 和 Schema 校验完成后,这个函数才执行 tool.call()。由于 tool 是上面查到的 AgentTool,这里实际进入 AgentTool.call()。
源码位置:src/services/tools/toolExecution.ts:492-576、src/services/tools/toolExecution.ts:599-1480
// 函数体:src/services/tools/toolExecution.ts::streamedCheckPermissionsAndCallTool()// 源码位置:492-576functionstreamedCheckPermissionsAndCallTool( tool, toolUseID, input, toolUseContext, canUseTool, assistantMessage, messageId, requestId, mcpServerType, mcpServerBaseUrl,) {const stream = new Stream() checkPermissionsAndCallTool( tool, toolUseID, input, toolUseContext, canUseTool, assistantMessage, messageId, requestId, mcpServerType, mcpServerBaseUrl, progress => { stream.enqueue({message: createProgressMessage({toolUseID: progress.toolUseID,parentToolUseID: toolUseID,data: progress.data, }), }) }, ) .then(results => {for (const result of results) stream.enqueue(result) }) .catch(error => stream.error(error)) .finally(() => stream.done())return stream}第 3 步发生在 AgentTool.call()。同步运行时,它持续读取子 Agent 的生成器;每一条子消息都保留在 agentMessages,直到子循环结束。这个数组只属于子 Agent 的执行过程,尚未写入父会话。
源码位置:src/tools/AgentTool/AgentTool.tsx:239-1261
// 函数体:src/tools/AgentTool/AgentTool.tsx::AgentTool.call()// 源码位置:239-1261asyncfunctioncall( { prompt, subagent_type, description, model: modelParam, run_in_background, name, team_name, mode: spawnMode, isolation, cwd, }, toolUseContext, canUseTool, assistantMessage, onProgress,) {// 前面根据 subagent_type 取得 Explore 定义,并组装 runAgentParams。// subagent_type => 'Explore'// prompt => '检查登录失败路径是否写入安全审计日志。'// run_in_background => undefined// ...// 前面省略:earlyAgentId = createAgentId()。const syncAgentId = asAgentId(earlyAgentId)// syncAgentId => 'agent_explore_01'const agentMessages = []const agentIterator = runAgent({ ...runAgentParams,override: { ...runAgentParams.override,agentId: syncAgentId, }, })[Symbol.asyncIterator]()while (true) {const result = await agentIterator.next()if (result.done) {break }const message = result.value agentMessages.push(message) }// agentMessages => [// {// type: 'assistant',// message: {// role: 'assistant',// content: [{// type: 'tool_use',// id: 'toolu_grep_01',// name: 'Grep',// input: { pattern: 'audit|auditLog', path: 'src' },// }],// },// },// {// type: 'user',// message: {// role: 'user',// content: [{// type: 'tool_result',// tool_use_id: 'toolu_grep_01',// content: 'src/services/audit.ts: recordLoginFailure() 写入登录失败审计日志。',// }],// },// },// {// type: 'assistant',// message: {// role: 'assistant',// content: [{// type: 'text',// text: '登录失败路径在 src/auth/login.ts。失败分支只返回错误;审计写入位于 src/services/audit.ts 的 recordLoginFailure(),当前分支没有调用它。',// }],// },// },// ]const agentResult = finalizeAgentTool( agentMessages, syncAgentId, metadata, )return {data: {status: 'completed', prompt, ...agentResult, ...worktreeResult, }, }}finalizeAgentTool() 做的收口很窄:从子 Agent 最后的 AssistantMessage 取文本块,统计 Tool 数、耗时和 Token,再返回一个普通对象。最后一条消息只有 Tool Use 时,它会向前找最近的文本块;正常完成的本例不进入这个兜底分支。
源码位置:src/tools/AgentTool/agentToolUtils.ts:276-356
// 函数体:src/tools/AgentTool/agentToolUtils.ts::finalizeAgentTool()// 源码位置:276-356functionfinalizeAgentTool(agentMessages, agentId, metadata) {const lastAssistantMessage = getLastAssistantMessage(agentMessages)// lastAssistantMessage.message.content => [// {// type: 'text',// text: '登录失败路径在 src/auth/login.ts。失败分支只返回错误;审计写入位于 src/services/audit.ts 的 recordLoginFailure(),当前分支没有调用它。',// },// ]let content = lastAssistantMessage.message.content.filter(block => block.type === 'text', )if (content.length === 0) {for (let index = agentMessages.length - 1; index >= 0; index--) {const message = agentMessages[index]if (message.type !== 'assistant') continueconst textBlocks = message.message.content.filter(block => block.type === 'text', )if (textBlocks.length > 0) { content = textBlocksbreak } } }const totalTokens = getTokenCountFromUsage( lastAssistantMessage.message.usage, )const totalToolUseCount = countToolUses(agentMessages)const agentResult = { agentId,agentType: metadata.agentType, content,totalDurationMs: Date.now() - metadata.startTime, totalTokens, totalToolUseCount,usage: lastAssistantMessage.message.usage, }// agentResult => {// agentId: 'agent_explore_01',// agentType: 'Explore',// content: [{// type: 'text',// text: '登录失败路径在 src/auth/login.ts。失败分支只返回错误;审计写入位于 src/services/audit.ts 的 recordLoginFailure(),当前分支没有调用它。',// }],// totalToolUseCount: 3,// totalDurationMs: 4820,// totalTokens: 1298,// }return agentResult}AgentTool.call() 返回后,通用 Tool 执行层拿到 result.data。它通过当前 Tool 约定的 mapToolResultToToolResultBlockParam(),把这个结果对象转换为模型 API 所需的 Tool Result 内容块。
源码位置:src/services/tools/toolExecution.ts:599-1480
// 函数体:src/services/tools/toolExecution.ts::checkPermissionsAndCallTool()// 源码位置:599-1480asyncfunctioncheckPermissionsAndCallTool( tool, toolUseID, input, toolUseContext, canUseTool, assistantMessage, messageId, requestId, mcpServerType, mcpServerBaseUrl, onToolProgress,) {// 前面完成 Schema 校验、Hook 与权限判断。// tool.name => 'Agent'// toolUseID => 'toolu_agent_01'// ...const result = await tool.call( input, { ...toolUseContext,toolUseId: toolUseID, }, canUseTool, assistantMessage, progress => onToolProgress(progress), )// result.data => finalizeAgentTool() 产生的完成结果对象。const mappedToolResultBlock = tool.mapToolResultToToolResultBlockParam( result.data, toolUseID, )let toolOutput = result.dataconst resultingMessages = []asyncfunctionaddToolResult(toolUseResult, preMappedBlock) {const toolResultBlock = preMappedBlock ? await processPreMappedToolResultBlock( preMappedBlock, tool.name, tool.maxResultSizeChars, ) : await processToolResultBlock( tool, toolUseResult, toolUseID, ) resultingMessages.push({message: createUserMessage({content: [toolResultBlock], toolUseResult,sourceToolAssistantUUID: assistantMessage.uuid, }), })// 源码还会在这里加入权限确认时附带的反馈内容。// ... }if (!isMcpTool(tool)) {await addToolResult(toolOutput, mappedToolResultBlock) }return resultingMessages}AgentTool.mapToolResultToToolResultBlockParam() 将 finalizeAgentTool() 产生的摘要接到父 Agent 最初的 toolu_agent_01 上;子 Agent 内部的 toolu_grep_01 留在子会话中。
源码位置:src/tools/AgentTool/AgentTool.tsx:1298-1378
// 函数体:src/tools/AgentTool/AgentTool.tsx::AgentTool.mapToolResultToToolResultBlockParam()// 源码位置:1340-1362functionmapToolResultToToolResultBlockParam(data, toolUseID) {// data.agentType => 'Explore'// toolUseID => 'toolu_agent_01'// data.content => [{// type: 'text',// text: '登录失败路径在 src/auth/login.ts。失败分支只返回错误;审计写入位于 src/services/audit.ts 的 recordLoginFailure(),当前分支没有调用它。',// }]const worktreeData = dataconst worktreeInfoText = worktreeData.worktreePath ? `\nworktreePath: ${worktreeData.worktreePath}\nworktreeBranch: ${worktreeData.worktreeBranch}` : ''// data.worktreePath => undefined// worktreeInfoText => ''const contentOrMarker = data.content.length > 0 ? data.content : [{type: 'text',text: '(Subagent completed but returned no output.)', }]if ( data.agentType && ONE_SHOT_BUILTIN_AGENT_TYPES.has(data.agentType) && !worktreeInfoText ) {// 'Explore' 在 ONE_SHOT_BUILTIN_AGENT_TYPES 中,因此条件为 true。return {tool_use_id: toolUseID,type: 'tool_result',content: contentOrMarker, } }return {tool_use_id: toolUseID,type: 'tool_result',content: [ ...contentOrMarker, {type: 'text',text: `agentId: ${data.agentId} (use SendMessage with to: '${data.agentId}' to continue this agent)<usage>total_tokens: ${data.totalTokens}tool_uses: ${data.totalToolUseCount}duration_ms: ${data.totalDurationMs}</usage>`, }, ], }}父循环随后和处理其他 Tool Result 一样,把 toolu_agent_01 的结果写进 messages,再让模型决定下一步。这里的 tool_use_id 没有改成子 Agent 内部的 Tool ID:它仍然对应开头那次 Agent Tool Use,保证父会话中的 Tool Use / Tool Result 协议完整。
这次选择的是 Explore,因此结果只有结论文本。general-purpose 等非 one-shot Agent 会在同一处额外附上 agentId 和 <usage>;下一节的继续运行使用的正是这种 Agent。
5. 后台 Agent:启动和完成是两次事件
后台路径与同步路径的分界很明确:同步路径在 AgentTool.call() 中等待 runAgent() 结束,再返回结论;后台路径先返回任务已启动,runAgent() 留在后台继续执行。
下面代入 subagent_type: 'general-purpose'、description: '梳理认证模块依赖'、prompt: '检查认证模块依赖关系,列出入口、服务和测试文件。不要修改文件。' 与 run_in_background: true。 AgentTool.call() 判断为后台路径后,注册任务,启动 runAsyncAgentLifecycle(),然后立刻返回。void 使这条后台 Promise 不阻塞当前 call()。
源码位置:src/tools/AgentTool/AgentTool.tsx:567-764
// 函数体:src/tools/AgentTool/AgentTool.tsx::AgentTool.call()// 源码位置:567-764asyncfunctioncall( { prompt, subagent_type, description, run_in_background }, toolUseContext, canUseTool, assistantMessage, onProgress,) {// selectedAgent.agentType => 'general-purpose'// run_in_background => true// isBackgroundTasksDisabled => false// 前面省略:选 Agent、组装 runAgentParams。// ...const shouldRunAsync = ( run_in_background === true || selectedAgent.background === true || isCoordinator || forceAsync || assistantForceAsync || (proactiveModule?.isProactiveActive() ?? false) ) && !isBackgroundTasksDisabled// shouldRunAsync => trueif (shouldRunAsync) {// 前面省略:earlyAgentId = createAgentId()。const asyncAgentId = earlyAgentId// asyncAgentId => 'agent_background_01'const agentBackgroundTask = registerAsyncAgent({agentId: asyncAgentId, description, prompt, selectedAgent,setAppState: rootSetAppState,toolUseId: toolUseContext.toolUseId, })// agentBackgroundTask.agentId => 'agent_background_01'void runWithAgentContext( asyncAgentContext, () => wrapWithCwd(() => runAsyncAgentLifecycle({taskId: agentBackgroundTask.agentId,abortController: agentBackgroundTask.abortController,makeStream: onCacheSafeParams => runAgent({ ...runAgentParams,override: { ...runAgentParams.override,agentId: agentBackgroundTask.agentId,abortController: agentBackgroundTask.abortController, }, onCacheSafeParams, }), metadata, description, toolUseContext, rootSetAppState,agentIdForCleanup: agentBackgroundTask.agentId,enableSummarization: isCoordinator || isForkSubagentEnabled() || getSdkAgentProgressSummariesEnabled(),getWorktreeResult: cleanupWorktreeIfNeeded, })), )return {data: {isAsync: true,status: 'async_launched',agentId: agentBackgroundTask.agentId, description, prompt,outputFile: getTaskOutputPath(agentBackgroundTask.agentId),canReadOutputFile: true, }, } }// shouldRunAsync 为 false 时,才进入第 4 节的同步等待分支。// ...}主模型只等到「任务已经启动」,不等子 Agent 的最终结论。时间顺序是:
主模型调用 Agent({ run_in_background: true });AgentTool.call()很快返回async_launched,父循环把这条 Tool Result 交还给主模型;主模型可以继续调用其他 Tool,或先结束当前回答;与此同时,子 Agent 在后台继续运行; 子 Agent 完成后, runAsyncAgentLifecycle()调用finalizeAgentTool()得到结论,再调用completeAsyncAgent()更新任务状态;enqueueAgentNotification()把带有结果的<task-notification>放进待处理队列。之后主循环消费这条队列消息时,主模型才会在新的请求中看到完成结果。
因此,agent_background_01 和输出文件路径只是第一次返回时给主模型的「任务句柄」,不是结论本身。需要在完成前主动查看时,模型可以调用 TaskOutput 读取当前状态或输出;任务完成后,通知也会携带结果。
源码位置:src/tools/AgentTool/agentToolUtils.ts:508-650、src/tasks/LocalAgentTask/LocalAgentTask.tsx:197-261、src/query.ts:1572-1636
6. fork:子 Agent 继承父会话的一条独立路径
普通子 Agent 的 forkContextMessages 是 undefined。当前源码还存在一条受 FORK_SUBAGENT 功能开关控制的 fork 路径:当模型调用 Agent Tool 时省略 subagent_type,AgentTool.call() 不选择 general-purpose,而是选择内部的 FORK_AGENT。
源码位置:src/tools/AgentTool/AgentTool.tsx:330-355、src/tools/AgentTool/forkSubagent.ts:20-76
// 函数体:src/tools/AgentTool/AgentTool.tsx::AgentTool.call()// 源码位置:330-355asyncfunctioncall({ subagent_type, description, prompt }) {// subagent_type => undefined// description => '检查当前修改是否遗漏登录失败审计日志'// prompt => '只检查当前修改和已有上下文,指出仍遗漏的审计日志路径。'const effectiveType = subagent_type ?? ( isForkSubagentEnabled() ? undefined : GENERAL_PURPOSE_AGENT.agentType )// => undefinedconst isForkPath = effectiveType === undefined// => trueconst selectedAgent = isForkPath ? FORK_AGENT : findAgentByType(effectiveType)// => {// agentType: 'fork',// tools: ['*'],// model: 'inherit',// permissionMode: 'bubble',// }// ...}这条路径随后调用 buildForkedMessages(),复制父会话最后一条 AssistantMessage,从中取出全部 Tool Use,为每一个 Tool Use 生成相同的占位 Tool Result,再在末尾追加这次 fork 的指令。runAgent() 还接收父 Agent 已渲染好的 System Prompt、原始 Tool 数组和 Thinking 配置。
源码位置:src/tools/AgentTool/forkSubagent.ts:107-168、src/tools/AgentTool/AgentTool.tsx:603-633
// 函数体:src/tools/AgentTool/forkSubagent.ts::buildForkedMessages()// 源码位置:107-168functionbuildForkedMessages(directive, assistantMessage) {// directive => '只检查当前修改和已有上下文,指出仍遗漏的审计日志路径。'// assistantMessage.message.content => [// {// type: 'tool_use',// id: 'toolu_agent_02',// name: 'Agent',// input: {// description: '检查当前修改是否遗漏登录失败审计日志',// prompt: '只检查当前修改和已有上下文,指出仍遗漏的审计日志路径。',// },// },// ]const fullAssistantMessage = { ...assistantMessage,uuid: randomUUID(),message: { ...assistantMessage.message,content: [...assistantMessage.message.content], }, }const toolUseBlocks = assistantMessage.message.content.filter(block => block.type === 'tool_use', )// toolUseBlocks => [// {// type: 'tool_use',// id: 'toolu_agent_02',// name: 'Agent',// input: {// description: '检查当前修改是否遗漏登录失败审计日志',// prompt: '只检查当前修改和已有上下文,指出仍遗漏的审计日志路径。',// },// },// ]if (toolUseBlocks.length === 0) {return [ createUserMessage({content: [{ type: 'text', text: buildChildMessage(directive) }], }), ] }const toolResultBlocks = toolUseBlocks.map(block => ({type: 'tool_result',tool_use_id: block.id,content: [{ type: 'text', text: FORK_PLACEHOLDER_RESULT }], }))// toolResultBlocks => [// {// type: 'tool_result',// tool_use_id: 'toolu_agent_02',// content: [// { type: 'text', text: 'Fork started — processing in background' },// ],// },// ]const toolResultMessage = createUserMessage({content: [ ...toolResultBlocks, { type: 'text', text: buildChildMessage(directive) }, ], })return [fullAssistantMessage, toolResultMessage]}buildForkedMessages() 只创建 fork 子会话的消息前缀。随后 AgentTool.call() 根据 isForkPath 组装 runAgent() 的参数:
// 函数体:src/tools/AgentTool/AgentTool.tsx::AgentTool.call()// 源码位置:603-636asyncfunctioncall( { prompt, subagent_type: subagentType, model, description, isolation, cwd }, toolUseContext, canUseTool, assistantMessage,) {// 前面省略:计算 effectiveType、判断 isForkPath、选择 selectedAgent,// 构造 promptMessages,并在需要时创建 worktreeInfo。//// 本例是省略 subagent_type 后触发的 fork 路径:// isForkPath => trueconst runAgentParams = {// selectedAgent => {// agentType: 'fork',// tools: ['*'],// model: 'inherit',// permissionMode: 'bubble',// }agentDefinition: selectedAgent,// promptMessages => [// fullAssistantMessage,// {// type: 'user',// message: {// role: 'user',// content: [// {// type: 'tool_result',// tool_use_id: 'toolu_agent_02',// content: [{ type: 'text', text: 'Fork started — processing in background' }],// },// {// type: 'text',// text: buildChildMessage(// '只检查当前修改和已有上下文,指出仍遗漏的审计日志路径。',// ),// },// ],// },// },// ]// 如果 effectiveIsolation === 'worktree',末尾还会追加一条// buildWorktreeNotice('/Users/me/shop', '/Users/me/shop/.worktrees/agent-a1b2c3d4')。 promptMessages,// toolUseContext => 父会话这次 Agent Tool Use 的上下文,里面带有// 父会话 messages、tools、mainLoopModel、abortController 和 AppState 访问函数。 toolUseContext,// canUseTool => 父会话传下来的权限判断函数,子 Agent 调 Tool 时仍会经过它。 canUseTool,// shouldRunAsync => true 的常见原因是 fork 功能开启后 forceAsync 为 true;// 除非后台任务被禁用,否则 fork 子 Agent 会后台运行。isAsync: shouldRunAsync,// 父上下文已有 querySource 时继承;否则由 FORK_AGENT 的 agentType// 计算,例如 'agent:builtin:fork'。querySource: toolUseContext.options.querySource ?? getQuerySourceForAgent( selectedAgent.agentType, isBuiltInAgent(selectedAgent), ),// fork 不接收本次 Tool Use 的 model override。// 即使 Tool Use 传了 model: 'opus',这里也会落成 undefined。model: isForkPath ? undefined : model,// fork 子 Agent 直接使用父 Agent 已经渲染好的 System Prompt。// forkParentSystemPrompt => 父 Agent 当前已经渲染好的完整 System Prompt。override: isForkPath ? { systemPrompt: forkParentSystemPrompt } : enhancedSystemPrompt && !worktreeInfo && !cwd ? { systemPrompt: asSystemPrompt(enhancedSystemPrompt) } : undefined,// fork 子 Agent 使用父会话当前可用的完整 Tool 数组,而不是重新组装 workerTools。// availableTools => toolUseContext.options.tools// 例如包含 Read、Grep、Bash、Edit、Agent、TodoWrite、MCP tools 等父会话当前工具。availableTools: isForkPath ? toolUseContext.options.tools : workerTools,// fork 子 Agent 在 runAgent() 内部会把这份父会话 messages 拼到// promptMessages 前面,形成继承父上下文的子会话。// forkContextMessages => [// 用户原始任务消息,// 父 Agent 已经完成的若干 assistant/user tool_result 消息,// 当前包含 toolu_agent_02 的 assistant 消息之前的完整上下文,// ]forkContextMessages: isForkPath ? toolUseContext.messages : undefined,// useExactTools => true;runAgent() 会直接使用 availableTools,// 并继承父会话 thinkingConfig / isNonInteractiveSession。 ...(isForkPath && { useExactTools: true }),// worktreePath => undefined// 如果 isolation: 'worktree',则类似:// '/Users/me/shop/.worktrees/agent-a1b2c3d4'worktreePath: worktreeInfo?.worktreePath,// description => '检查当前修改是否遗漏登录失败审计日志' description, }// 后面省略:runAgent(runAgentParams) 的同步或后台执行分支。}把第 2 节的普通子 Agent 和这里的 fork 子 Agent 对照起来,差异集中在下面几项:
agentDefinition | Exploregeneral-purpose 等实际 Agent 定义 | FORK_AGENT |
promptMessages | createUserMessage({ content: prompt }) | buildForkedMessages() |
model | 'sonnet'、'opus'、'haiku' | undefined,继承父模型选择 |
override.systemPrompt | ||
availableTools | workerTools | |
forkContextMessages | undefined | |
useExactTools | runAgent() 会继续过滤工具 | truerunAgent() 直接使用父 Tool 数组并继承父 Thinking 配置 |
当前 fork 路径依赖的 FORK_SUBAGENT 开关还会令 forceAsync 为 true,从而使 shouldRunAsync 成立。因此这条 fork 子 Agent 会按第 5 节的后台生命周期运行:父 Agent 先得到 async_launched,完成结论通过任务通知或 TaskOutput 取得。
fork 子 Agent 会尽量复用父会话的请求前缀:父 System Prompt、父 Tool 定义和父消息上下文都会被带入。它仍设置独立 agentId 和新的 queryTracking.chainId;isInForkChild() 检查到 <fork-boilerplate> 时会阻止再次 fork。
7. SendMessage 如何继续已经停止的子 Agent
第 4 节里同步子 Agent 完成后,AgentTool 会把结果映射成父 Tool Use 对应的 Tool Result。这里还有一个小分支:Explore、Plan 属于 one-shot built-in,完成后只返回报告,不附带继续对话的句柄;其他可继续的 Agent,例如 general-purpose,会在 Tool Result 末尾追加一段提示:
agentId: agent_general_01 (use SendMessage with to: 'agent_general_01' to continue this agent)<usage>total_tokens: 1298tool_uses: 3duration_ms: 4820</usage>上一次 Agent Tool Result 里的 agentId 给出了继续对话的目标。父模型需要追问这个子 Agent 时,可以调用:
SendMessage Tool,并传入 to: 'agent_general_01' 与 message: '继续只读检查:确认 recordLoginFailure() 应由哪个失败分支调用。'。
SendMessageTool.call() 收到这次 Tool Use 后,先把 to 解析成 Agent ID,再看这个 ID 对应的任务还在不在当前任务表里。只有任务已经停止,或者任务状态已经从内存里清掉但 sidechain transcript 还在磁盘上时,才会进入 resumeAgentBackground()。
源码位置:src/tools/AgentTool/AgentTool.tsx:1340-1370、src/tools/SendMessageTool/SendMessageTool.ts:800-869、src/tools/AgentTool/resumeAgent.ts:42-264
// 函数体:src/tools/SendMessageTool/SendMessageTool.ts::SendMessageTool.call()// 源码位置:800-869asyncfunctioncall(input, context, canUseTool, assistantMessage) {// input => {// to: 'agent_general_01',// message: '继续只读检查:确认 recordLoginFailure() 应由哪个失败分支调用。',// }if (typeof input.message === 'string' && input.to !== '*') {const appState = context.getAppState()const registered = appState.agentNameRegistry.get(input.to)// registered => undefined;这里直接使用 raw agentId。const agentId = registered ?? toAgentId(input.to)// agentId => 'agent_general_01'if (agentId) {const task = appState.tasks[agentId]if (isLocalAgentTask(task) && !isMainSessionTask(task)) {if (task.status === 'running') { queuePendingMessage( agentId, input.message, context.setAppStateForTasks ?? context.setAppState, )return {data: {success: true,message: 'Message queued for delivery to agent_general_01 at its next tool round.', }, } }// task 存在但已经不是 running,例如 completed / failed / stopped。// 这时不会把消息塞进运行中队列,而是恢复这条子 Agent sidechain。const result = await resumeAgentBackground({ agentId,prompt: input.message,toolUseContext: context, canUseTool,invokingRequestId: assistantMessage?.requestId, })return {data: {success: true,message: `Agent "${input.to}" was stopped (${task.status}); resumed it in the background with your message. Output: ${result.outputFile}`, }, } }// task 已经不在 appState.tasks 里,但 input.to 仍能解析成 agentId。// 这时再尝试从磁盘上的 sidechain transcript 恢复。const result = await resumeAgentBackground({ agentId,prompt: input.message,toolUseContext: context, canUseTool,invokingRequestId: assistantMessage?.requestId, })return {data: {success: true,message: `Agent "${input.to}" had no active task; resumed from transcript in the background with your message. Output: ${result.outputFile}`, }, } } }// 不能解析成子 Agent ID 时,才继续走 teammate / broadcast 等其它 SendMessage 分支。}resumeAgentBackground() 先读出子 Agent 的 sidechain transcript 和 metadata,再过滤未配对的 Tool Use,把新消息追加到这份子会话末尾,最后以异步方式重新交给 runAgent()。
源码位置:src/tools/AgentTool/resumeAgent.ts:63-195
// 函数体:src/tools/AgentTool/resumeAgent.ts::resumeAgentBackground()// 源码位置:63-195asyncfunctionresumeAgentBackground({ agentId, prompt, toolUseContext,}) {// agentId => 'agent_general_01'// prompt => '继续只读检查:确认 recordLoginFailure() 应由哪个失败分支调用。'const [transcript, meta] = awaitPromise.all([ getAgentTranscript(agentId), readAgentMetadata(agentId), ])// transcript.messages => [// {// type: 'user',// message: {// role: 'user',// content: '检查登录失败路径和审计日志调用链,给出可修改的位置。',// },// },// {// type: 'assistant',// message: {// role: 'assistant',// content: [// {// type: 'text',// text: '登录失败路径在 src/auth/login.ts。失败分支只返回错误;审计写入位于 src/services/audit.ts 的 recordLoginFailure(),当前分支没有调用它。',// },// ],// },// },// ]// meta => {// agentType: 'general-purpose',// description: '定位登录审计缺口',// }const resumedMessages = filterWhitespaceOnlyAssistantMessages( filterOrphanedThinkingOnlyMessages( filterUnresolvedToolUses(transcript.messages), ), )// => 上面两条完整消息仍保留;没有只剩 thinking 或缺少 Tool Result 的记录。const selectedAgent = meta?.agentType ? toolUseContext.options.agentDefinitions.activeAgents.find(agent => agent.agentType === meta.agentType, ) ?? GENERAL_PURPOSE_AGENT : GENERAL_PURPOSE_AGENT// meta.agentType => 'general-purpose'// selectedAgent => GENERAL_PURPOSE_AGENTconst runAgentParams = {agentDefinition: selectedAgent,promptMessages: [ ...resumedMessages, createUserMessage({content: prompt, }), ],isAsync: true,querySource: getQuerySourceForAgent( selectedAgent.agentType, isBuiltInAgent(selectedAgent), ),// selectedAgent.agentType => 'general-purpose'// querySource => 'agent:builtin:general-purpose' }// ...return runAgent(runAgentParams)}这里恢复的是子 Agent 自己先前保存的 sidechain。追问能够接着子 Agent 已经找到的文件和结论继续;父 Agent 同时仍保留自己的主会话节奏。
小结
子 Agent 是父 Agent 通过 Agent Tool 启动的一段独立会话:它自己调用模型和 Tools。同步运行完成后直接交回 Tool Result;后台运行则先交回任务信息,完成结论稍后通过通知提供。
从这条调用链可以看出,子 Agent 用来承接一段可以独立完成、但过程可能很长的工作,例如搜索多个目录、做只读排查或验证一个假设。父 Agent 不必把这段工作的每次 Read、Grep 都放进自己的消息历史,只保留能继续决策的结论。
这带来三个直接好处:
主会话更聚焦:大量中间 Tool 输出留在子 Agent 的 sidechain transcript; 任务可分开运行:后台路径启动后,父 Agent 可以继续处理当前任务; 上下文可以按任务选择:普通调用从新任务开始,fork 继承父会话,resume 接回旧子会话。
代价也在源码里可见:每个子 Agent 都要单独运行一轮或多轮模型请求;父 Agent 默认只拿到结论,想追问过程需要通过 SendMessage 或 TaskOutput 回到对应子任务;后台运行还需要额外维护任务状态、通知和 transcript。
普通同步子 Agent 的主线只有这一条:
父 Agent 的 Agent Tool Use -> AgentTool.call() -> runAgent() -> 子 Agent 自己的 query()、Read、Grep -> 最终结论 -> 父 Agent 收到一条 Agent Tool Result父 Agent 不会收到子 Agent 每一次 Read、Grep 的过程,只接收最后交回的结果。
三种路径只是在「子 Agent 从哪里接着开始」和「父 Agent 要不要等待结果」上不同:
async_launchedTaskOutput 取得 | ||
SendMessageTaskOutput 取得 |
后台运行时,父 Agent 可以在子 Agent 工作期间继续当前任务。代价是要用任务状态、通知和 transcript 维护这段延后交付的结果。
夜雨聆风