10-AgentTool-子Agent生成
10. AgentTool - 子 Agent 生成
所属分组:工具系统
概述
AgentTool 是 Claude Code 实现"任务委派"与"上下文隔离"的核心机制:主 Agent 通过它派生子 Agent,把多步复杂任务交给一个独立的对话线程去完成。子 Agent 有三种形态——built-in(内置通用 Agent)、custom(用户/项目配置的 Markdown Agent)、plugin(插件提供的 Agent),它们都通过 loadAgentsDir.ts 统一加载。AgentTool 还引入了"Fork subagent"机制:不指定 subagent_type 时 fork 当前 Agent 的上下文,让廉价的研究型任务可以继承父对话的 prompt cache。
UI 层面 AgentTool 是最复杂的工具之一——它要渲染子 Agent 的进度流(progress messages)、把连续的搜索/读取操作折叠成 summary、在 condensed/verbose/transcript 三种模式下展示不同密度的信息。agentColorManager.ts 给每个 agentType 分配主题色,让用户能在 teams 面板中一眼区分多个并行 Agent。
源码位置
- [tools/AgentTool/prompt.ts](file:///e:/2026plan/AI_Lab/claude-code-sourcemap-main/restored-src/src/tools/AgentTool/prompt.ts)
- [tools/AgentTool/UI.tsx](file:///e:/2026plan/AI_Lab/claude-code-sourcemap-main/restored-src/src/tools/AgentTool/UI.tsx)
- [tools/AgentTool/loadAgentsDir.ts](file:///e:/2026plan/AI_Lab/claude-code-sourcemap-main/restored-src/src/tools/AgentTool/loadAgentsDir.ts)
- [tools/AgentTool/agentColorManager.ts](file:///e:/2026plan/AI_Lab/claude-code-sourcemap-main/restored-src/src/tools/AgentTool/agentColorManager.ts)
- [tools/AgentTool/forkSubagent.ts](file:///e:/2026plan/AI_Lab/claude-code-sourcemap-main/restored-src/src/tools/AgentTool/forkSubagent.ts)
- [tools/AgentTool/runAgent.ts](file:///e:/2026plan/AI_Lab/claude-code-sourcemap-main/restored-src/src/tools/AgentTool/runAgent.ts)
核心实现分析
1. prompt.ts:动态 vs 静态 agent 列表的缓存优化
getPrompt() 的开头有一个关键决策:
// When the gate is on, the agent list lives in an agent_listing_delta
// attachment (see attachments.ts) instead of inline here. This keeps the
// tool description static across MCP/plugin/permission changes so the
// tools-block prompt cache doesn't bust every time an agent loads.
const listViaAttachment = shouldInjectAgentListInMessages()
const agentListSection = listViaAttachment
? `Available agent types are listed in <system-reminder> messages in the conversation.`
: `Available agent types and the tools they have access to:
${effectiveAgents.map(agent => formatAgentLine(agent)).join('\n')}`
shouldInjectAgentListInMessages() 的注释揭示了原因:“The dynamic agent list was ~10.2% of fleet cache_creation tokens: MCP async connect, /reload-plugins, or permission-mode changes mutate the list → description changes → full tool-schema cache bust.” 把 agent 列表从工具描述里挪到 attachment 消息,能让工具描述保持静态,MCP/插件/权限变化不再触发整个 tools-block 的缓存失效——这是 10% fleet 缓存优化背后的设计。GrowthBook gate tengu_agent_list_attach 控制灰度。
2. Fork subagent 的 prompt 设计
当 isForkSubagentEnabled() 时,prompt 注入"When to fork"小节:
const whenToForkSection = forkEnabled ? `
## When to fork
Fork yourself (omit \`subagent_type\`) when the intermediate tool output isn't worth keeping in your context. The criterion is qualitative — "will I need this output again" — not task size.
- **Research**: fork open-ended questions...
- **Implementation**: prefer to fork implementation work that requires more than a couple of edits.
Forks are cheap because they share your prompt cache. Don't set \`model\` on a fork — a different model can't reuse the parent's cache. Pass a short \`name\` (one or two words, lowercase) so the user can see the fork in the teams panel and steer it mid-run.
` : ''
这里有三个核心约束:① Fork 的判据是"输出是否值得保留在上下文",不是任务大小;② Fork 不能设 model,否则无法共享父 prompt cache;③ Fork 必须给 name,方便用户在 teams 面板里识别并 mid-run 干预。
3. “Don’t peek. Don’t race.” 强约束
Fork 模式下有两段对模型行为的硬约束:
**Don't peek.** The tool result includes an `output_file` path — do not Read or tail it unless the user explicitly asks for a progress check. You get a completion notification; trust it. Reading the transcript mid-flight pulls the fork's tool noise into your context, which defeats the point of forking.
**Don't race.** After launching, you know nothing about what the fork found. Never fabricate or predict fork results in any format — not as prose, summary, or structured output. The notification arrives as a user-role message in a later turn; it is never something you write yourself.
这两条直击 LLM 的两个失败模式:① 偷读 fork 转录文件会污染父上下文,违背 fork 的初衷;② 在通知到达前编造 fork 结果,是 LLM 幻觉的常见来源。配套的 forkExamples 给出"用户中途询问 fork 进度"的标准答复——“Still waiting on the audit — that’s one of the things it’s checking. Should land shortly.”,只给状态不给猜测。
4. formatAgentLine:工具列表的三态展示
formatAgentLine 把单个 agent 格式化为一行:
export function formatAgentLine(agent: AgentDefinition): string {
const toolsDescription = getToolsDescription(agent)
return `- ${agent.agentType}: ${agent.whenToUse} (Tools: ${toolsDescription})`
}
getToolsDescription 处理 allowlist+denylist 同时存在的复杂情况:
if (hasAllowlist && hasDenylist) {
// Both defined: filter allowlist by denylist to match runtime behavior
const denySet = new Set(disallowedTools)
const effectiveTools = tools.filter(t => !denySet.has(t))
if (effectiveTools.length === 0) return 'None'
return effectiveTools.join(', ')
} else if (hasAllowlist) {
return tools.join(', ')
} else if (hasDenylist) {
return `All tools except ${disallowedTools.join(', ')}`
}
return 'All tools'
注释强调"filter allowlist by denylist to match runtime behavior"——prompt 文本必须与运行时实际可用工具一致,否则模型会按描述调用一个被 deny 的工具而失败。
5. loadAgentsDir.ts:三种 agent 来源的统一加载
getAgentDefinitionsWithOverrides() 是 agent 加载的入口,被 memoize:
export const getAgentDefinitionsWithOverrides = memoize(
async (cwd: string): Promise<AgentDefinitionsResult> => {
if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) {
const builtInAgents = getBuiltInAgents()
return { activeAgents: builtInAgents, allAgents: builtInAgents }
}
// ...
const markdownFiles = await loadMarkdownFilesForSubdir('agents', cwd)
const customAgents = markdownFiles.map(/* parseAgentFromMarkdown */)
let pluginAgentsPromise = loadPluginAgents()
if (feature('AGENT_MEMORY_SNAPSHOT') && isAutoMemoryEnabled()) {
const [pluginAgents_] = await Promise.all([
pluginAgentsPromise,
initializeAgentMemorySnapshots(customAgents),
])
pluginAgentsPromise = Promise.resolve(pluginAgents_)
}
const pluginAgents = await pluginAgentsPromise
const builtInAgents = getBuiltInAgents()
const allAgentsList = [...builtInAgents, ...pluginAgents, ...customAgents]
const activeAgents = getActiveAgentsFromList(allAgentsList)
// Initialize colors for all active agents
for (const agent of activeAgents) {
if (agent.color) setAgentColor(agent.agentType, agent.color)
}
return { activeAgents, allAgents: allAgentsList, failedFiles }
},
)
三个来源并行加载:built-in(代码内嵌)、plugin(插件 manifest)、custom(agents/ 目录的 Markdown)。getActiveAgentsFromList 按优先级去重——builtIn < plugin < user < project < flag < managed,后者覆盖前者同名 agentType。initializeAgentMemorySnapshots 在 memory 启用时并发初始化快照——这是把"加载插件"和"初始化 memory"用 Promise.all 并发的优化点。
6. AgentDefinition 类型族
export type AgentDefinition =
| BuiltInAgentDefinition
| CustomAgentDefinition
| PluginAgentDefinition
三者共享 BaseAgentDefinition,区别在 source 字段和 system prompt 获取方式:
- BuiltInAgentDefinition:
source: 'built-in',getSystemPrompt接收toolUseContext参数(动态 prompt) - CustomAgentDefinition:
source: 'userSettings' | 'projectSettings' | 'policySettings' | 'flagSettings',getSystemPrompt是无参闭包 - PluginAgentDefinition:
source: 'plugin',附带plugin: string元数据
BaseAgentDefinition 的字段极丰富:tools / disallowedTools / skills / mcpServers / hooks / color / model / effort / permissionMode / maxTurns / memory / isolation / omitClaudeMd 等。omitClaudeMd 的注释揭示了大规模优化:“Read-only agents (Explore, Plan) don’t need commit/PR/lint guidelines — the main agent has full CLAUDE.md and interprets their output. Saves ~5-15 Gtok/week across 34M+ Explore spawns.” Explore 这种只读 agent 不注入 CLAUDE.md,每周节省 5-15 Gtok。
7. parseAgentFromMarkdown:宽松解析 + 错误隔离
Markdown agent 解析对每个字段都做容错:
const backgroundRaw = frontmatter['background']
if (
backgroundRaw !== undefined &&
backgroundRaw !== 'true' && backgroundRaw !== 'false' &&
backgroundRaw !== true && backgroundRaw !== false
) {
logForDebugging(`Agent file ${filePath} has invalid background value '${backgroundRaw}'. Must be 'true', 'false', or omitted.`)
}
const background = backgroundRaw === 'true' || backgroundRaw === true ? true : undefined
每个字段解析失败都只是 logForDebugging + 默认值,不抛错——避免一个 agent 文件格式错误导致整个 agent 系统不可用。文件级别也做了隔离:getParseError 区分"缺少 name 字段"和"缺少 description 字段"等具体错误,便于用户排查。文件没有 name 字段时直接静默跳过,注释说"they’re likely co-located reference documentation"——agents 目录可能并存非 agent 文档。
8. Memory 工具的自动注入
agent 启用 memory 时,自动注入文件操作工具:
// If memory is enabled, inject Write/Edit/Read tools for memory access
if (isAutoMemoryEnabled() && memory && tools !== undefined) {
const toolSet = new Set(tools)
for (const tool of [FILE_WRITE_TOOL_NAME, FILE_EDIT_TOOL_NAME, FILE_READ_TOOL_NAME]) {
if (!toolSet.has(tool)) {
tools = [...tools, tool]
}
}
}
getSystemPrompt 也在 memory 启用时拼上 loadAgentMemoryPrompt。这是"agent 配置驱动运行时能力"的设计——声明 memory: user 后,工具集和 prompt 自动扩展,无需用户手动加 FileEdit/FileRead 到 tools 列表。
9. agentColorManager.ts:8 色分配
export type AgentColorName =
| 'red' | 'blue' | 'green' | 'yellow'
| 'purple' | 'orange' | 'pink' | 'cyan'
export const AGENT_COLOR_TO_THEME_COLOR = {
red: 'red_FOR_SUBAGENTS_ONLY',
blue: 'blue_FOR_SUBAGENTS_ONLY',
// ...
} as const satisfies Record<AgentColorName, keyof Theme>
注释里 _FOR_SUBAGENTS_ONLY 后缀是关键——这些颜色只用于 subagent,主 Agent 不会用,避免视觉混淆。general-purpose agentType 显式返回 undefined(不着色):
export function getAgentColor(agentType: string): keyof Theme | undefined {
if (agentType === 'general-purpose') return undefined
const agentColorMap = getAgentColorMap()
const existingColor = agentColorMap.get(agentType)
if (existingColor && AGENT_COLORS.includes(existingColor)) {
return AGENT_COLOR_TO_THEME_COLOR[existingColor]
}
return undefined
}
getAgentColorMap() 来自 bootstrap/state.ts,是全局持久化的 Map——同一 agentType 在整个会话中颜色稳定,不随重载变化。
10. UI.tsx:progress messages 的折叠算法
processProgressMessages 是 AgentTool UI 最复杂的函数之一,它把子 Agent 的连续搜索/读取操作折叠成 summary:
function processProgressMessages(messages, tools, isAgentRunning): ProcessedMessage[] {
// Only process for ants
if ("external" !== 'ant') {
return messages.filter(/* ... */).map(m => ({ type: 'original', message: m }))
}
// ...
let currentGroup = null
function flushGroup(isActive) {
if (currentGroup && (currentGroup.searchCount > 0 || currentGroup.readCount > 0 || currentGroup.replCount > 0)) {
result.push({ type: 'summary', searchCount, readCount, replCount, uuid, isActive })
}
currentGroup = null
}
// ...
}
注释说"Only process for ants"——这个折叠算法只对 ant 用户启用,外部用户看到原始消息流。currentGroup 累积连续的 search/read/REPL 操作,遇到非此类操作就 flush。isActive 标记最后一组是否还在运行中(影响 spinner 显示)。getSearchOrReadInfo 用 toolUseByID Map 反查 tool_use 块,避免依赖 normalizedMessages。
11. extractLastToolInfo:从尾部倒推
extractLastToolInfo 从 progress messages 尾部倒推最后一个工具信息,用于 grouped 渲染:
// Count trailing consecutive search/read operations from the end
let searchCount = 0
let readCount = 0
for (let i = progressMessages.length - 1; i >= 0; i--) {
const msg = progressMessages[i]!
if (!hasProgressMessage(msg.data)) continue
const info = getSearchOrReadInfo(msg, tools, toolUseByID)
if (info && (info.isSearch || info.isRead)) {
if (msg.data.message.type === 'user') {
if (info.isSearch) searchCount++
else if (info.isRead) readCount++
}
} else {
break
}
}
if (searchCount + readCount >= 2) {
return getSearchReadSummaryText(searchCount, readCount, true)
}
如果尾部有 ≥2 个连续 search/read,就显示 summary(如"3 searches, 2 reads");否则找最后一个 tool_result,用 tool.getToolUseSummary 获取摘要。这是把"子 Agent 正在做什么"压缩成一行展示给用户的算法。
12. renderToolResultMessage:三状态分支
子 Agent 结果有三种状态分支:
if (internal.status === 'remote_launched') { /* 远程 agent 启动 */ }
if (data.status === 'async_launched') { /* 后台 agent 启动 */ }
if (data.status !== 'completed') return null // 未完成不渲染结果
// completed 状态
const result = [`${totalToolUseCount} tool uses`, `${formatNumber(totalTokens)} tokens`, formatDuration(totalDurationMs)]
const completionMessage = `Done (${result.join(' · ')})`
completed 状态渲染"Done (N tool uses · M tokens · duration)"摘要 + transcript 模式下的完整子 Agent 转录。async_launched 显示"Backgrounded agent"加 ↓ 快捷键提示。remote_launched 显示 task ID 和 session URL。
13. renderGroupedAgentToolUse:并行 Agent 分组渲染
当一条消息里发起多个 Agent 调用时,renderGroupedAgentToolUse 把它们分组展示:
const allSameType = agentStats.length > 0 && agentStats.every(stat => stat.agentType === agentStats[0]?.agentType)
const commonType = allSameType && agentStats[0]?.agentType !== 'Agent' ? agentStats[0]?.agentType : null
const allAsync = agentStats.every(stat => stat.isAsync)
return <Box flexDirection="column" marginTop={1}>
<Box flexDirection="row">
<ToolUseLoader shouldAnimate={shouldAnimate && anyUnresolved} isUnresolved={anyUnresolved} isError={anyError} />
<Text>
{allComplete ? allAsync ? <>
<Text bold>{toolUses.length}</Text> background agents launched{' '}
<Text dimColor><KeyboardShortcutHint shortcut="↓" action="manage" parens /></Text>
</> : <>
<Text bold>{toolUses.length}</Text>{' '}
{commonType ? `${commonType} agents` : 'agents'} finished
</> : <>
Running <Text bold>{toolUses.length}</Text>{' '}
{commonType ? `${commonType} agents` : 'agents'}…
</>}{' '}
</Text>
{!allAsync && <CtrlOToExpand />}
</Box>
{agentStats.map((stat, index) => <AgentProgressLine /* ... */ />)}
</Box>
三态展示:运行中(“Running N agents…”)、完成(“N agents finished”)、后台启动(“N background agents launched”)。allSameType 检测是否所有 agent 同类型,是则在标题里显示类型名(如"3 test-runner agents finished"),更紧凑。每个 agent 用 AgentProgressLine 组件展示,颜色来自 getAgentColor。
关键设计要点
- agent 列表外置到 attachment:
shouldInjectAgentListInMessages把动态 agent 列表从工具描述挪到 attachment 消息,避免 MCP/插件变化触发 tools-block 缓存失效,节省约 10% fleet cache_creation tokens。 - Fork 的三条铁律:fork 不设 model(保 cache)、不 peek output_file(防上下文污染)、不 race(防幻觉)——prompt 里反复强调并在示例中展示正确行为。
- 宽松解析 + 错误隔离:agent Markdown 解析对每字段做容错,单文件错误不拖垮整个 agent 系统,无
name字段的文件被静默跳过(视为参考文档)。 - Memory 自动工具注入:声明
memory: user/project/local后,FileEdit/FileWrite/FileRead 自动加入 tools 列表,prompt 自动拼接 memory 内容——配置驱动运行时能力。 - subagent 专属颜色隔离:8 种颜色全部带
_FOR_SUBAGENTS_ONLY后缀的主题键,主 Agent 永不着色,避免视觉混淆;颜色映射持久化在 bootstrap state,全会话稳定。 - progress 折叠算法仅 ant 启用:连续 search/read/REPL 操作折叠成 summary 是 ant-only 优化,外部用户看原始流——反映内部用户对密度更高的信息展示耐受度更好。
与其他模块的关系
- forkSubagent.ts / runAgent.ts:fork 与 fresh subagent 的实际派生逻辑,分别走不同的
createSubagentContext路径;fork 共享父renderedSystemPrompt以命中 prompt cache。 - bootstrap/state.ts:
getAgentColorMap持久化 agentType→color 映射,跨会话稳定。 - memdir 模块:
isAutoMemoryEnabled、loadAgentMemoryPrompt、checkAgentMemorySnapshot给 memory-capable agent 提供记忆能力。 - markdownConfigLoader:
loadMarkdownFilesForSubdir('agents', cwd)是统一的 Markdown 配置加载器,也服务于 commands、skills 等。 - plugins/loadPluginAgents:插件 agent 加载器,memoized 并可与 memory snapshot 初始化并发。
- SendMessageTool:prompt 末尾指引"To continue a previously spawned agent, use SendMessageTool with the agent’s ID or name"——持续会话走 SendMessage 而非新派生。
- utils/collapseReadSearch:
getSearchOrReadFromContent、getSearchReadSummaryText是 progress 折叠算法的共享工具。
小结
AgentTool 是 Claude Code 工具系统里最复杂的一个——它本身是工具,又派生出新的 Agent 上下文。它的 prompt 在 fork 启用与否、coordinator 模式与否、agent 列表外置与否之间动态切换,每条分支都对应具体的缓存/隔离/治理目标。它的 UI 渲染要处理三种完成状态、三种渲染密度(condensed/verbose/transcript)、并行分组、progress 折叠——一份 progress 流可能包含数十条消息,要压缩成"Running 3 agents… +2 more tool uses"这样的高密度摘要。loadAgentsDir.ts 把 built-in/plugin/custom 三源 agent 统一加载并按优先级去重,agentColorManager.ts 给每个 agentType 分配持久化颜色。AgentTool 是工具系统与 Agent 系统的交汇点,理解它就理解了 Claude Code 的"任务委派"哲学。
更多推荐




所有评论(0)