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 获取方式:

  • BuiltInAgentDefinitionsource: 'built-in'getSystemPrompt 接收 toolUseContext 参数(动态 prompt)
  • CustomAgentDefinitionsource: 'userSettings' | 'projectSettings' | 'policySettings' | 'flagSettings'getSystemPrompt 是无参闭包
  • PluginAgentDefinitionsource: '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 显示)。getSearchOrReadInfotoolUseByID 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

关键设计要点

  1. agent 列表外置到 attachmentshouldInjectAgentListInMessages 把动态 agent 列表从工具描述挪到 attachment 消息,避免 MCP/插件变化触发 tools-block 缓存失效,节省约 10% fleet cache_creation tokens。
  2. Fork 的三条铁律:fork 不设 model(保 cache)、不 peek output_file(防上下文污染)、不 race(防幻觉)——prompt 里反复强调并在示例中展示正确行为。
  3. 宽松解析 + 错误隔离:agent Markdown 解析对每字段做容错,单文件错误不拖垮整个 agent 系统,无 name 字段的文件被静默跳过(视为参考文档)。
  4. Memory 自动工具注入:声明 memory: user/project/local 后,FileEdit/FileWrite/FileRead 自动加入 tools 列表,prompt 自动拼接 memory 内容——配置驱动运行时能力。
  5. subagent 专属颜色隔离:8 种颜色全部带 _FOR_SUBAGENTS_ONLY 后缀的主题键,主 Agent 永不着色,避免视觉混淆;颜色映射持久化在 bootstrap state,全会话稳定。
  6. progress 折叠算法仅 ant 启用:连续 search/read/REPL 操作折叠成 summary 是 ant-only 优化,外部用户看原始流——反映内部用户对密度更高的信息展示耐受度更好。

与其他模块的关系

  • forkSubagent.ts / runAgent.ts:fork 与 fresh subagent 的实际派生逻辑,分别走不同的 createSubagentContext 路径;fork 共享父 renderedSystemPrompt 以命中 prompt cache。
  • bootstrap/state.tsgetAgentColorMap 持久化 agentType→color 映射,跨会话稳定。
  • memdir 模块isAutoMemoryEnabledloadAgentMemoryPromptcheckAgentMemorySnapshot 给 memory-capable agent 提供记忆能力。
  • markdownConfigLoaderloadMarkdownFilesForSubdir('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/collapseReadSearchgetSearchOrReadFromContentgetSearchReadSummaryText 是 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 的"任务委派"哲学。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐