Claude Code 源码分析(九):子 Agent 如何分叉、继续与回到父会话
英文标题:Claude Code 源码分析(九):子 Agent 如何分叉、继续与回到父会话
第 4 篇走到 runToolUse() 时,每个模型返回的 tool_use 都会交给对应 Tool。Agent 也在这张 Tool 表里,call() 内部会再启动一次 query()。 这里的子
74.3 KB
40.0 KB
7.6 KB
中文
第 4 篇走到 runToolUse() 时,每个模型返回的 tool_use 都会交给对应 Tool。Agent 也在这张 Tool 表里,call() 内部会再启动一次 query()。 这里的子
中文全文翻译正在补齐,可先查看下方英文原文。
English / 原文
第 4 篇走到 runToolUse() 时,每个模型返回的 tool_use 都会交给对应 Tool。Agent 也在这张 Tool 表里,call() 内部会再启动一次 query()。
这里的子 Agent 指父 Agent 通过 Agent Tool 启动的一段独立会话。它有自己的消息历史和 Tool 循环;同步运行结束后,父 Agent 收到的是这次 Agent Tool Use 对应的 Tool Result。
假设当前任务是检查登录失败为什么没有留下审计日志。父 Agent 先看到了用户输入和前面已经做过的工作,随后模型返回下面这次 Tool Use:
async function* queryLoop(state) {
for await (const message of callModelAndForwardMessages()) {
if (message.type === 'assistant') {
const msgToolUseBlocks = message.message.content.filter(
block => block.type === 'tool_use',
)
toolUseBlocks.push(...msgToolUseBlocks)
needsFollowUp = true
}
}
const toolUpdates = runTools(
toolUseBlocks,
assistantMessages,
canUseTool,
toolUseContext,
)
for await (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
async function call(
{
prompt,
subagent_type,
description,
run_in_background,
},
toolUseContext,
canUseTool,
assistantMessage,
) {
}
这次调用先根据 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
async function call({ subagent_type }, toolUseContext) {
const effectiveType = subagent_type ?? GENERAL_PURPOSE_AGENT.agentType
const allAgents = toolUseContext.options.agentDefinitions.activeAgents
const selectedAgent = allAgents.find(
agent => agent.agentType === effectiveType,
)
}
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
async function call({ prompt }, toolUseContext, canUseTool) {
const promptMessages = [
createUserMessage({
content: prompt,
}),
]
const runAgentParams = {
agentDefinition: selectedAgent,
promptMessages,
toolUseContext,
canUseTool,
isAsync: shouldRunAsync,
querySource:
toolUseContext.options.querySource ??
getQuerySourceForAgent(
selectedAgent.agentType,
isBuiltInAgent(selectedAgent),
),
availableTools: workerTools,
forkContextMessages: undefined,
}
return 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
async function* runAgent({
toolUseContext,
promptMessages,
forkContextMessages,
override,
agentDefinition,
availableTools,
isAsync,
agentGetAppState,
}) {
const agentId = override?.agentId ?? createAgentId()
const contextMessages = forkContextMessages
? filterIncompleteToolCalls(forkContextMessages)
: []
const initialMessages = [
...contextMessages,
...promptMessages,
]
const agentReadFileState = forkContextMessages !== undefined
? cloneFileStateCache(toolUseContext.readFileState)
: createFileStateCacheWithSizeLimit(READ_FILE_STATE_CACHE_SIZE)
const resolvedTools = resolveAgentTools(
agentDefinition,
availableTools,
isAsync,
).resolvedTools
const agentToolUseContext = createSubagentContext(toolUseContext, {
options: { ...toolUseContext.options, tools: resolvedTools },
agentId,
agentType: agentDefinition.agentType,
messages: initialMessages,
readFileState: agentReadFileState,
abortController: toolUseContext.abortController,
getAppState: agentGetAppState,
shareSetAppState: !isAsync,
})
}
普通路径的文件状态缓存从空开始,消息历史只含这次任务;第 6 节的 fork 路径才会传入父会话消息,并克隆父 Agent 的文件状态缓存。
3. runAgent() 仍然调用同一个 query()
上下文准备好后,runAgent() 进入第 2 篇中的 query()。子 Agent 也会走「模型返回 Tool Use → 执行 Tool → 下一轮模型请求」的循环,只是使用它自己的 System Prompt、工具池和消息数组。
源码位置:src/tools/AgentTool/runAgent.ts:747-805
async function* runAgent({
initialMessages,
agentSystemPrompt,
userContext,
systemContext,
canUseTool,
agentToolUseContext,
querySource,
maxTurns,
}) {
void recordSidechainTranscript(
initialMessages,
agentToolUseContext.agentId,
)
let lastRecordedUuid = initialMessages.at(-1)?.uuid ?? null
for await (const message of query({
messages: initialMessages,
systemPrompt: agentSystemPrompt,
userContext,
systemContext,
canUseTool,
toolUseContext: agentToolUseContext,
querySource,
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
export async function* runToolUse(
toolUse,
assistantMessage,
canUseTool,
toolUseContext,
) {
const tool = findToolByName(
toolUseContext.options.tools,
toolUse.name,
)
for await (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
function streamedCheckPermissionsAndCallTool(
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
async function call(
{
prompt,
subagent_type,
description,
model: modelParam,
run_in_background,
name,
team_name,
mode: spawnMode,
isolation,
cwd,
},
toolUseContext,
canUseTool,
assistantMessage,
onProgress,
) {
const syncAgentId = asAgentId(earlyAgentId)
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)
}
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
function finalizeAgentTool(agentMessages, agentId, metadata) {
const lastAssistantMessage = getLastAssistantMessage(agentMessages)
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') continue
const textBlocks = message.message.content.filter(
block => block.type === 'text',
)
if (textBlocks.length > 0) {
content = textBlocks
break
}
}
}
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,
}
return agentResult
}
AgentTool.call() 返回后,通用 Tool 执行层拿到 result.data。它通过当前 Tool 约定的 mapToolResultToToolResultBlockParam(),把这个结果对象转换为模型 API 所需的 Tool Result 内容块。
源码位置:src/services/tools/toolExecution.ts:599-1480
async function checkPermissionsAndCallTool(
tool,
toolUseID,
input,
toolUseContext,
canUseTool,
assistantMessage,
messageId,
requestId,
mcpServerType,
mcpServerBaseUrl,
onToolProgress,
) {
const result = await tool.call(
input,
{
...toolUseContext,
toolUseId: toolUseID,
},
canUseTool,
assistantMessage,
progress => onToolProgress(progress),
)
const mappedToolResultBlock = tool.mapToolResultToToolResultBlockParam(
result.data,
toolUseID,
)
let toolOutput = result.data
const resultingMessages = []
async function addToolResult(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
function mapToolResultToToolResultBlockParam(data, toolUseID) {
const worktreeData = data
const worktreeInfoText = worktreeData.worktreePath
? `\nworktreePath: ${worktreeData.worktreePath}\nworktreeBranch: ${worktreeData.worktreeBranch}`
: ''
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
) {
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
async function call(
{ prompt, subagent_type, description, run_in_background },
toolUseContext,
canUseTool,
assistantMessage,
onProgress,
) {
const shouldRunAsync = (
run_in_background === true ||
selectedAgent.background === true ||
isCoordinator ||
forceAsync ||
assistantForceAsync ||
(proactiveModule?.isProactiveActive() ?? false)
) && !isBackgroundTasksDisabled
if (shouldRunAsync) {
const asyncAgentId = earlyAgentId
const agentBackgroundTask = registerAsyncAgent({
agentId: asyncAgentId,
description,
prompt,
selectedAgent,
setAppState: rootSetAppState,
toolUseId: toolUseContext.toolUseId,
})
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,
},
}
}
}
主模型只等到「任务已经启动」,不等子 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
async function call({ subagent_type, description, prompt }) {
const effectiveType = subagent_type ?? (
isForkSubagentEnabled()
? undefined
: GENERAL_PURPOSE_AGENT.agentType
)
const isForkPath = effectiveType === undefined
const selectedAgent = isForkPath
? FORK_AGENT
: findAgentByType(effectiveType)
}
这条路径随后调用 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
function buildForkedMessages(directive, assistantMessage) {
const fullAssistantMessage = {
...assistantMessage,
uuid: randomUUID(),
message: {
...assistantMessage.message,
content: [...assistantMessage.message.content],
},
}
const toolUseBlocks = assistantMessage.message.content.filter(
block => block.type === 'tool_use',
)
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 }],
}))
const toolResultMessage = createUserMessage({
content: [
...toolResultBlocks,
{ type: 'text', text: buildChildMessage(directive) },
],
})
return [fullAssistantMessage, toolResultMessage]
}
buildForkedMessages() 只创建 fork 子会话的消息前缀。随后 AgentTool.call() 根据 isForkPath 组装 runAgent() 的参数:
async function call(
{ prompt, subagent_type: subagentType, model, description, isolation, cwd },
toolUseContext,
canUseTool,
assistantMessage,
) {
const runAgentParams = {
agentDefinition: selectedAgent,
promptMessages,
toolUseContext,
canUseTool,
isAsync: shouldRunAsync,
querySource:
toolUseContext.options.querySource ??
getQuerySourceForAgent(
selectedAgent.agentType,
isBuiltInAgent(selectedAgent),
),
model: isForkPath ? undefined : model,
override: isForkPath
? { systemPrompt: forkParentSystemPrompt }
: enhancedSystemPrompt && !worktreeInfo && !cwd
? { systemPrompt: asSystemPrompt(enhancedSystemPrompt) }
: undefined,
availableTools: isForkPath
? toolUseContext.options.tools
: workerTools,
forkContextMessages: isForkPath
? toolUseContext.messages
: undefined,
...(isForkPath && { useExactTools: true }),
worktreePath: worktreeInfo?.worktreePath,
description,
}
}
把第 2 节的普通子 Agent 和这里的 fork 子 Agent 对照起来,差异集中在下面几项:
| 字段 | 普通子 Agent | fork 子 Agent |
|---|---|---|
agentDefinition |
Explore、general-purpose 等实际 Agent 定义 |
内部 FORK_AGENT |
promptMessages |
一条新的 createUserMessage({ content: prompt }) |
buildForkedMessages() 生成的父 AssistantMessage 副本、占位 Tool Result 和 fork 指令 |
model |
可以保留 Tool Use 传入的 'sonnet'、'opus'、'haiku' |
固定为 undefined,继承父模型选择 |
override.systemPrompt |
普通路径可能使用当前 Agent 增强后的 System Prompt | 使用父 Agent 已经渲染好的 System Prompt |
availableTools |
按子 Agent 权限重新组装的 workerTools |
父会话当前完整 Tool 数组 |
forkContextMessages |
undefined |
父会话截至本次 Tool Use 的消息数组 |
useExactTools |
不出现,runAgent() 会继续过滤工具 |
true,runAgent() 直接使用父 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: 1298
tool_uses: 3
duration_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
async function call(input, context, canUseTool, assistantMessage) {
if (typeof input.message === 'string' && input.to !== '*') {
const appState = context.getAppState()
const registered = appState.agentNameRegistry.get(input.to)
const agentId = registered ?? toAgentId(input.to)
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.',
},
}
}
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}`,
},
}
}
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}`,
},
}
}
}
}
resumeAgentBackground() 先读出子 Agent 的 sidechain transcript 和 metadata,再过滤未配对的 Tool Use,把新消息追加到这份子会话末尾,最后以异步方式重新交给 runAgent()。
源码位置:src/tools/AgentTool/resumeAgent.ts:63-195
async function resumeAgentBackground({
agentId,
prompt,
toolUseContext,
}) {
const [transcript, meta] = await Promise.all([
getAgentTranscript(agentId),
readAgentMetadata(agentId),
])
const resumedMessages = filterWhitespaceOnlyAssistantMessages(
filterOrphanedThinkingOnlyMessages(
filterUnresolvedToolUses(transcript.messages),
),
)
const selectedAgent = meta?.agentType
? toolUseContext.options.agentDefinitions.activeAgents.find(
agent => agent.agentType === meta.agentType,
) ?? GENERAL_PURPOSE_AGENT
: GENERAL_PURPOSE_AGENT
const runAgentParams = {
agentDefinition: selectedAgent,
promptMessages: [
...resumedMessages,
createUserMessage({
content: prompt,
}),
],
isAsync: true,
querySource: getQuerySourceForAgent(
selectedAgent.agentType,
isBuiltInAgent(selectedAgent),
),
}
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 要不要等待结果」上不同:
| 路径 | 子 Agent 的起点 | 父 Agent 最先收到什么 |
|---|---|---|
| 普通同步调用 | 一条新任务消息 | 子 Agent 完成后的 Tool Result |
| fork | 父会话已有的消息、System Prompt 和 Tools | async_launched;结论随后通过任务通知或 TaskOutput 取得 |
| resume | 已保存的子 Agent sidechain transcript,加一条新指令 | SendMessage 的「已恢复」结果;结论随后通过任务通知或 TaskOutput 取得 |
后台运行时,父 Agent 可以在子 Agent 工作期间继续当前任务。代价是要用任务状态、通知和 transcript 维护这段延后交付的结果。
系列文更新中:cc.windliang.wang