Claude Code内置Sub-Agent原理深度解析:多代理协作架构的工程实现

从 AgentTool 调度、Fork 缓存共享到 Coordinator 编排的多代理架构解剖

Posted by 爱折腾的工程师 on Monday, April 6, 2026

一、引言

在之前的 Claude Code源码深度技术解析 一文中,我们对 Claude Code 的整体架构进行了全局梳理。本文将聚焦其中最精巧的子系统之一——内置 Sub-Agent(子代理)系统,深入分析其类型体系、调度机制、上下文隔离、缓存优化和多模式协作的工程实现。

Claude Code 的 Sub-Agent 系统并非简单的"调用另一个 Agent",而是一套涵盖类型定义、工具过滤、权限冒泡、Prompt Cache 共享、异步生命周期管理的完整框架。理解这套系统,对于理解现代 AI Agent 的多代理编排有重要的参考价值。

Sub-Agent 系统架构总览

核心源码文件

文件路径 行数 职责
tools/AgentTool/AgentTool.tsx 1398 Agent 工具注册、输入 Schema、主调用入口
tools/AgentTool/runAgent.ts 974 Agent 实际执行的会话循环
tools/AgentTool/forkSubagent.ts 211 Fork Sub-Agent 机制
tools/AgentTool/builtInAgents.ts 73 内置 Agent 注册与 Feature Gate
tools/AgentTool/loadAgentsDir.ts 756 Agent 加载、解析与合并逻辑
tools/AgentTool/agentToolUtils.ts 687 工具过滤、结果最终化、异步生命周期
tools/AgentTool/prompt.ts 288 Agent 工具的系统提示生成
utils/forkedAgent.ts 690 创建子代理上下文、Forked Agent 查询循环
utils/model/agent.ts 158 Agent 模型选择与继承机制
coordinator/coordinatorMode.ts 370 Coordinator 模式架构

二、Agent 定义类型系统

Sub-Agent 的第一层设计始于其类型体系。Claude Code 使用 TypeScript 的联合类型定义了三种 Agent 来源:

// loadAgentsDir.ts
export type AgentDefinition =
  | BuiltInAgentDefinition    // 内置 Agent
  | CustomAgentDefinition     // 自定义 Agent
  | PluginAgentDefinition     // 插件 Agent

2.1 三种 Agent 来源

内置 Agent(BuiltInAgentDefinition)

export type BuiltInAgentDefinition = BaseAgentDefinition & {
  source: 'built-in'
  baseDir: 'built-in'
  callback?: () => void
  getSystemPrompt: (params: {
    toolUseContext: Pick<ToolUseContext, 'options'>
  }) => string
}

内置 Agent 的 getSystemPrompt 接收 toolUseContext 参数,可以根据当前会话的工具和配置动态生成系统提示。这使得像 claude-code-guide 这样的 Agent 能够注入用户当前配置的 skills、agents、MCP 服务器等上下文信息。

自定义 Agent(CustomAgentDefinition)

export type CustomAgentDefinition = BaseAgentDefinition & {
  getSystemPrompt: () => string
  source: SettingSource  // userSettings / projectSettings / policySettings / flagSettings
  filename?: string
  baseDir?: string
}

自定义 Agent 通过 .claude/agents/*.md 目录下的 Markdown 文件定义,使用 YAML frontmatter 声明元数据,正文作为系统提示。

插件 Agent(PluginAgentDefinition)

export type PluginAgentDefinition = BaseAgentDefinition & {
  getSystemPrompt: () => string
  source: 'plugin'
  filename?: string
  plugin: string
}

2.2 基础定义字段

所有 Agent 共享的 BaseAgentDefinition 包含丰富的配置项:

export type BaseAgentDefinition = {
  agentType: string              // Agent 类型标识
  whenToUse: string              // 使用场景描述(展示给主 Agent)
  tools?: string[]               // 允许的工具列表('*' 表示全部)
  disallowedTools?: string[]     // 禁止的工具列表
  skills?: string[]              // 预加载的 Skill 名称
  mcpServers?: AgentMcpServerSpec[]  // Agent 专属 MCP 服务器
  hooks?: HooksSettings          // Session 级 Hooks
  color?: AgentColorName         // UI 颜色标识
  model?: string                 // 模型选择(支持 'inherit')
  effort?: EffortValue           // 推理努力等级
  permissionMode?: PermissionMode // 权限模式
  maxTurns?: number              // 最大对话轮数
  background?: boolean           // 是否强制后台运行
  memory?: AgentMemoryScope      // 持久内存作用域
  isolation?: 'worktree' | 'remote'  // 隔离模式
  omitClaudeMd?: boolean         // 是否省略 CLAUDE.md
  // ...
}

2.3 Agent 加载优先级

多来源 Agent 通过 getActiveAgentsFromList() 按优先级合并,后者覆盖前者的同名 Agent:

// loadAgentsDir.ts
const agentGroups = [
  builtInAgents,    // 最低优先级
  pluginAgents,
  userAgents,       // ~/.claude/agents/*.md
  projectAgents,    // .claude/agents/*.md
  flagAgents,       // GrowthBook 远程配置
  managedAgents,    // 最高优先级(policy settings)
]

const agentMap = new Map<string, AgentDefinition>()
for (const agents of agentGroups) {
  for (const agent of agents) {
    agentMap.set(agent.agentType, agent)  // 后者覆盖前者
  }
}

这个设计允许企业级场景下的策略覆盖:管理员可以通过 policySettings 强制替换任何内置或用户定义的 Agent。


三、6 个内置 Agent 详解

builtInAgents.ts 中的 getBuiltInAgents() 函数根据 Feature Flag 动态注册内置 Agent:

export function getBuiltInAgents(): AgentDefinition[] {
  const agents: AgentDefinition[] = [
    GENERAL_PURPOSE_AGENT,      // 始终启用
    STATUSLINE_SETUP_AGENT,     // 始终启用
  ]

  if (areExplorePlanAgentsEnabled()) {
    agents.push(EXPLORE_AGENT, PLAN_AGENT)  // Feature flag 控制
  }

  if (isNonSdkEntrypoint) {
    agents.push(CLAUDE_CODE_GUIDE_AGENT)     // 非 SDK 入口时启用
  }

  if (feature('VERIFICATION_AGENT') && 
      getFeatureValue_CACHED_MAY_BE_STALE('tengu_hive_evidence', false)) {
    agents.push(VERIFICATION_AGENT)           // Feature flag + GrowthBook 控制
  }

  return agents
}

3.1 General Purpose Agent — 通用任务执行者

属性
agentType general-purpose
模型 inherit(继承父代模型)
工具 ['*'](全部工具)
启用条件 始终启用

这是默认的 Sub-Agent 类型。当调用 Agent 工具时未指定 subagent_type(且 Fork 功能未开启时),自动使用此 Agent。

// generalPurposeAgent.ts
export const GENERAL_PURPOSE_AGENT: BuiltInAgentDefinition = {
  agentType: 'general-purpose',
  whenToUse: 'General-purpose agent for researching complex questions, searching for code...',
  tools: ['*'],
  source: 'built-in',
  baseDir: 'built-in',
  getSystemPrompt: getGeneralPurposeSystemPrompt,
}

3.2 Explore Agent — 只读代码探索专家

属性
agentType Explore
模型 内部 inherit / 外部 haiku
权限 只读
禁止工具 Agent、Edit、Write、NotebookEdit
特殊优化 omitClaudeMd: true(省略 CLAUDE.md)

Explore Agent 被设计为快速、轻量的搜索专家,强制只读模式。系统提示明确禁止任何文件修改操作:

=== CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS ===
This is a READ-ONLY exploration task. You are STRICTLY PROHIBITED from:
- Creating new files (no Write, touch, or file creation of any kind)
- Modifying existing files (no Edit operations)
...

关键优化:设置 omitClaudeMd: true,省略 CLAUDE.md 层级文件。这个优化在 3400万+ 次 Explore 调用中节省了 5-15 Gtok/周 的 Token 消耗。

3.3 Plan Agent — 软件架构规划师

属性
agentType Plan
模型 inherit
权限 只读
特殊优化 omitClaudeMd: true,省略 gitStatus

Plan Agent 与 Explore 共享同样的只读约束,但职责不同——它专注于软件架构设计和实施方案规划。输出格式要求包含步骤化实施策略和关键文件列表。

3.4 Verification Agent — 对抗性验证专家

属性
agentType verification
模型 inherit
颜色 red
后台 true(始终后台运行)
特殊 criticalSystemReminder_EXPERIMENTAL

这是最"狠"的内置 Agent。它的设计哲学是**“不是确认实现正确,而是尝试打破它”**。

系统提示中有一段极具洞见的"自我认知"指导:

You have two documented failure patterns. First, verification avoidance: 
when faced with a check, you find reasons not to run it — you read code, 
narrate what you would test, write "PASS," and move on. Second, being 
seduced by the first 80%: you see a polished UI or a passing test suite 
and feel inclined to pass it, not noticing half the buttons do nothing...

criticalSystemReminder_EXPERIMENTAL 是一个在每个 user turn 都注入的提醒:

CRITICAL: This is a VERIFICATION-ONLY task. You CANNOT edit, write, or 
create files IN THE PROJECT DIRECTORY. You MUST end with VERDICT: PASS, 
VERDICT: FAIL, or VERDICT: PARTIAL.

输出格式严格规定为 VERDICT: PASS / FAIL / PARTIAL

3.5 Claude Code Guide Agent — 使用指南顾问

属性
agentType claude-code-guide
模型 haiku(快速响应)
permissionMode dontAsk(完全无需权限确认)
工具 Glob、Grep、Read、WebFetch、WebSearch

Guide Agent 是唯一一个使用动态上下文注入的内置 Agent。它的 getSystemPrompt 接收 toolUseContext,在运行时注入:

  1. 用户配置的自定义 Skills
  2. 自定义 Agents 列表
  3. 已连接的 MCP 服务器
  4. 插件命令
  5. 用户 settings.json
getSystemPrompt({ toolUseContext }) {
  const commands = toolUseContext.options.commands
  const contextSections: string[] = []

  // 1. 注入自定义 Skills
  const customCommands = commands.filter(cmd => cmd.type === 'prompt')
  if (customCommands.length > 0) {
    contextSections.push(`**Available custom skills:**\n${commandList}`)
  }
  // 2. 注入自定义 Agents
  // 3. 注入 MCP 服务器
  // 4. 注入用户设置
  // ...
}

3.6 Statusline Setup Agent — 状态栏配置师

属性
agentType statusline-setup
模型 sonnet
颜色 orange
工具 ['Read', 'Edit']

最轻量的内置 Agent,仅用于配置 Claude Code 终端状态栏,能够解析用户的 shell PS1 配置并转换为 Claude Code 的 statusLine 格式。


四、Sub-Agent 完整生命周期

Sub-Agent 完整生命周期

4.1 调用入口与类型路由

当主 Agent 调用 Agent 工具时,入口在 AgentTool.tsxcall() 方法。输入 Schema:

{
  description: string,         // 3-5 字简短描述
  prompt: string,              // 任务提示
  subagent_type?: string,      // Agent 类型(可选)
  model?: 'sonnet'|'opus'|'haiku',  // 模型覆盖
  run_in_background?: boolean, // 后台运行
  name?: string,               // Agent 名称(用于 SendMessage 路由)
  isolation?: 'worktree'|'remote', // 隔离模式
}

类型路由逻辑

// AgentTool.tsx
const effectiveType = subagent_type 
  ?? (isForkSubagentEnabled() ? undefined : GENERAL_PURPOSE_AGENT.agentType)

const isForkPath = effectiveType === undefined
  • subagent_type 明确指定 → 使用该类型
  • 省略且 Fork 功能开启 → 进入 Fork 路径
  • 省略且 Fork 未开启 → 默认使用 general-purpose

4.2 Agent 选择与验证

// 1. 从活跃 Agent 列表中过滤被权限规则拒绝的
const agents = filterDeniedAgents(allAgents, toolPermissionContext, AGENT_TOOL_NAME)

// 2. 查找目标 Agent
const found = agents.find(agent => agent.agentType === effectiveType)

// 3. 如果不存在但被权限拒绝,给出明确错误
if (agentExistsButDenied) {
  throw new Error(`Agent type '${effectiveType}' has been denied by permission rule...`)
}

// 4. 等待 MCP 服务器就绪(最多 30 秒)
if (requiredMcpServers?.length) {
  // ... 轮询等待 MCP 连接
}

4.3 工具池组装

Worker 独立于父代组装自己的工具池,避免工具定义的耦合:

// AgentTool.tsx
const workerPermissionContext = {
  ...appState.toolPermissionContext,
  mode: selectedAgent.permissionMode ?? 'acceptEdits'
}
const workerTools = assembleToolPool(workerPermissionContext, appState.mcp.tools)

4.4 系统提示构建(Fork vs 标准路径)

这是整个系统最关键的分叉点:

if (isForkPath) {
  // Fork 路径:继承父代的已渲染系统提示(字节级一致!)
  forkParentSystemPrompt = toolUseContext.renderedSystemPrompt
  promptMessages = buildForkedMessages(prompt, assistantMessage)
} else {
  // 标准路径:使用 Agent 自己的 getSystemPrompt()
  const agentPrompt = selectedAgent.getSystemPrompt({ toolUseContext })
  enhancedSystemPrompt = await enhanceSystemPromptWithEnvDetails([agentPrompt], ...)
  promptMessages = [createUserMessage({ content: prompt })]
}

4.5 执行模式选择

const shouldRunAsync = (
  run_in_background === true ||
  selectedAgent.background === true ||
  isCoordinator ||
  forceAsync ||              // Fork 实验:全部异步化
  assistantForceAsync ||     // KAIROS 模式
  proactiveModule?.isProactiveActive()
) && !isBackgroundTasksDisabled

4.6 进入 runAgent() 会话循环

runAgent() 是一个 async generator,核心流程:

1. 解析模型 (getAgentModel)
2. 生成 agentId
3. 构建上下文 (createSubagentContext)
4. 执行 SubagentStart hooks
5. 注册 frontmatter hooks
6. 预加载 Skills
7. 初始化 Agent MCP 服务器
8. 调用 query() 进入 LLM 对话循环
   ├── 每条消息 yield 给调用者
   ├── 记录 sidechain transcript
   └── 受 maxTurns 限制
9. finally 块清理
   ├── MCP 清理
   ├── Session hooks 清理
   ├── Prompt cache tracking 清理
   ├── File state cache 释放
   ├── Perfetto agent 注销
   ├── Transcript subdir 清除
   ├── Todos 条目清除
   └── Shell 任务终止

深入:createSubagentContext 的状态隔离模型

这是 Sub-Agent 技术实现中最关键的函数之一。它对 ToolUseContext 的每个字段采取三种策略——克隆、新建、No-op——精确控制父子 Agent 之间的状态边界:

// forkedAgent.ts — 三策略隔离模型
export function createSubagentContext(parentContext, overrides?) {
  return {
    // ===== 克隆(Clone):避免交叉污染但保留初始状态 =====
    readFileState: cloneFileStateCache(overrides?.readFileState ?? parentContext.readFileState),
    // 关键:contentReplacementState 必须克隆而非新建!
    // 原因:fork 子代处理父消息中的 tool_use_id 时,
    // 如果是新建状态,会将这些 ID 视为"从未见过" → 做出不同的替换决策
    // → wire prefix 不同 → Prompt Cache miss
    // 克隆确保做出相同决策 → 相同的 wire prefix → Cache hit
    contentReplacementState: overrides?.contentReplacementState 
      ?? cloneContentReplacementState(parentContext.contentReplacementState),

    // ===== 新建(Fresh):完全独立的实例 =====
    nestedMemoryAttachmentTriggers: new Set<string>(),
    discoveredSkillNames: new Set<string>(),
    localDenialTracking: createDenialTrackingState(), // 独立的拒绝追踪
    queryTracking: { chainId: randomUUID(), depth: parentDepth + 1 },

    // ===== No-op / 共享 =====
    setAppState: overrides?.shareSetAppState ? parentContext.setAppState : () => {},
    setAppStateForTasks: parentContext.setAppStateForTasks, // 始终共享!防止僵尸进程
    // UI 回调全部 undefined(子 Agent 无法控制父 UI)
    addNotification: undefined,
    setToolJSX: undefined,
  }
}

为什么 setAppStateForTasks 必须始终共享? 即使 setAppState 是 no-op(异步 Agent 无法修改父状态),bash task 的注册和 kill 必须到达 root store。否则异步 Agent 产生的后台 shell 循环(如 fake-logs.sh)会变成 PPID=1 的僵尸进程——主会话退出后仍然存活。

深入:AbortController 的 WeakRef 联动

子 Agent 的 AbortController 通过 createChildAbortController 创建,使用 WeakRef 实现单向传播 + 内存安全:

// abortController.ts — WeakRef 防止内存泄漏
export function createChildAbortController(parent) {
  const child = createAbortController(50) // maxListeners=50
  
  const weakChild = new WeakRef(child)   // 父不强持有子
  const weakParent = new WeakRef(parent) // 子不强持有父
  
  // 父 abort → 传播到子(单向)
  parent.signal.addEventListener('abort', 
    propagateAbort.bind(weakParent, weakChild), { once: true })
  
  // 子 abort → 清理父上的 listener(防止 handler 堆积)
  child.signal.addEventListener('abort',
    removeAbortHandler.bind(weakParent, new WeakRef(handler)), { once: true })
  
  return child // 子 abort 不影响父!
}

三级优先级:override.abortController > shareAbortController(共享父代) > createChildAbortController(默认)

深入:权限模式的层级覆盖

agentGetAppState() 闭包每次调用都重新获取 parent state 并做条件覆盖,实现了精细的权限降级防护:

const agentGetAppState = () => {
  const state = toolUseContext.getAppState()
  
  // 1. permissionMode 覆盖 — 但最高权限级别不被降级
  if (agentPermissionMode &&
      state.mode !== 'bypassPermissions' &&  // 最高,不可降级
      state.mode !== 'acceptEdits' &&         // 次高,不可降级
      !(feature('TRANSCRIPT_CLASSIFIER') && state.mode === 'auto')) {
    toolPermissionContext = { ...toolPermissionContext, mode: agentPermissionMode }
  }

  // 2. shouldAvoidPrompts — bubble 模式的特殊行为
  //    bubble: 即使异步也显示提示(冒泡到父终端)
  //    非 bubble 异步: 隐避提示(auto-deny)
  const shouldAvoidPrompts = agentPermissionMode === 'bubble' ? false : isAsync

  // 3. awaitAutomatedChecksBeforeDialog
  //    后台 Agent 如果能显示提示,应先等待自动化检查完成,
  //    只在自动化检查无法解决时才中断用户
  if (isAsync && !shouldAvoidPrompts) {
    toolPermissionContext.awaitAutomatedChecksBeforeDialog = true
  }
}

4.7 结果最终化

function finalizeAgentTool(agentMessages, agentId, metadata) {
  // 提取最后一条 assistant 消息的文本内容
  const lastAssistantMessage = getLastAssistantMessage(agentMessages)
  let content = lastAssistantMessage.message.content.filter(_ => _.type === 'text')
  
  // 统计 token 使用和工具调用次数
  const totalTokens = getTokenCountFromUsage(lastAssistantMessage.message.usage)
  const totalToolUseCount = countToolUses(agentMessages)

  // 发送缓存驱逐提示
  logEvent('tengu_cache_eviction_hint', { scope: 'subagent_end', ... })

  return { agentId, agentType, content, totalDurationMs, totalTokens, totalToolUseCount, usage }
}

五、Fork Sub-Agent:Prompt Cache 共享的精巧设计

Fork Sub-Agent 是 Claude Code 在 Token 优化方面最精巧的设计之一。

Fork Sub-Agent Prompt Cache 共享机制

5.1 核心思想

所有 Fork 子代共享父代的 Prompt Cache 前缀。这意味着多个并行 Fork 子代不会各自消耗 cache_creation tokens,而是复用父代已缓存的系统提示和工具定义。

5.2 Feature Gate

function isForkSubagentEnabled(): boolean {
  if (feature('FORK_SUBAGENT')) {
    if (isCoordinatorMode()) return false   // 与 Coordinator 互斥
    if (getIsNonInteractiveSession()) return false // 非交互会话不支持
    return true
  }
  return false
}

5.3 Fork vs 标准 Sub-Agent 关键差异

维度 标准 Sub-Agent Fork Sub-Agent
上下文 全新,零上下文 继承父代完整会话上下文
系统提示 Agent 自己的 getSystemPrompt() 父代的已渲染系统提示(字节级一致)
工具池 workerTools(独立构建) 父代的精确工具池
Prompt Cache 独立缓存链 共享父代缓存
思考配置 { type: 'disabled' } 继承父代的 thinkingConfig
模型 可覆盖 必须 inherit
权限模式 acceptEdits(默认) bubble(冒泡到父终端)
maxTurns Agent 定义或默认 200

5.4 Fork 消息构建

buildForkedMessages() 的核心设计目标是最大化 Prompt Cache 命中率

export function buildForkedMessages(
  directive: string,
  assistantMessage: AssistantMessage,
): MessageType[] {
  // 1. 克隆父代的完整 assistant 消息(保留所有 tool_use、thinking、text 块)
  const fullAssistantMessage = { ...assistantMessage, uuid: randomUUID(), ... }

  // 2. 为每个 tool_use 构建统一占位符 tool_result
  const toolResultBlocks = toolUseBlocks.map(block => ({
    type: 'tool_result',
    tool_use_id: block.id,
    content: [{ type: 'text', text: 'Fork started — processing in background' }]
  }))

  // 3. 构建单一 user 消息:占位符 results + 每子代独有指令
  const toolResultMessage = createUserMessage({
    content: [...toolResultBlocks, { type: 'text', text: buildChildMessage(directive) }]
  })

  return [fullAssistantMessage, toolResultMessage]
}

结构[...历史消息, assistant(所有tool_use块), user(占位符tool_results..., 每子代独有指令)]

关键细节:

  • 所有 tool_result 使用相同文本 "Fork started — processing in background"
  • 只有最后的 text 块(指令)不同
  • 所有 Fork 子代的消息前缀字节一致,最大化缓存共享

5.5 Fork 子代指令格式

<fork-boilerplate>
STOP. READ THIS FIRST.
You are a forked worker process. You are NOT the main agent.

RULES (non-negotiable):
1. Do NOT spawn sub-agents; execute directly
2. Do NOT converse, ask questions
3. USE your tools directly
4. If you modify files, commit your changes
5. Keep report under 500 words
...
</fork-boilerplate>

FORK_DIRECTIVE: {directive}

5.6 递归 Fork 保护

双重保护机制防止 Fork 子代再次 Fork:

// 方式一:检查 querySource(抗 autocompact)
if (toolUseContext.options.querySource === 'agent:builtin:fork') {
  throw new Error('Fork is not available inside a forked worker.')
}

// 方式二:消息内容扫描(备用)
function isInForkChild(messages: MessageType[]): boolean {
  return messages.some(m => {
    if (m.type !== 'user') return false
    return content.some(block =>
      block.type === 'text' && block.text.includes(`<fork-boilerplate>`)
    )
  })
}

querySource 设置在 context.options 上,能够在 autocompact(消息压缩)后存活。消息扫描作为 fallback 兜底。


六、三层工具过滤机制

Claude Code 使用三层过滤确保每个 Sub-Agent 只能使用它应该使用的工具。

三层工具过滤机制

6.1 第一层:全局过滤(filterToolsForAgent)

function filterToolsForAgent({ tools, isBuiltIn, isAsync, permissionMode }) {
  return tools.filter(tool => {
    // MCP 工具始终放行
    if (tool.name.startsWith('mcp__')) return true
    
    // 全局禁止列表
    if (ALL_AGENT_DISALLOWED_TOOLS.has(tool.name)) return false
    
    // 自定义 Agent 额外禁止列表
    if (!isBuiltIn && CUSTOM_AGENT_DISALLOWED_TOOLS.has(tool.name)) return false
    
    // 异步 Agent 白名单过滤
    if (isAsync && !ASYNC_AGENT_ALLOWED_TOOLS.has(tool.name)) return false
    
    return true
  })
}

全局禁止(ALL_AGENT_DISALLOWED_TOOLS)

  • TaskOutputExitPlanMode_v2EnterPlanMode
  • Agent(外部版本禁止嵌套;ant 版允许)
  • TaskStopWorkflow

异步 Agent 白名单(ASYNC_AGENT_ALLOWED_TOOLS)

  • Read, WebSearch, TodoWrite, Grep, WebFetch, Glob
  • Bash/Shell, Edit, Write, NotebookEdit
  • Skill, SyntheticOutput, ToolSearch
  • EnterWorktree, ExitWorktree

6.2 第二层:Agent 定义级过滤(resolveAgentTools)

function resolveAgentTools(agentDefinition, availableTools, isAsync) {
  // 先应用第一层全局过滤
  const filteredAvailableTools = filterToolsForAgent(...)
  
  // 应用 disallowedTools 排除
  const allowedAvailableTools = filteredAvailableTools.filter(
    tool => !disallowedToolSet.has(tool.name)
  )

  // tools: ['*'] → 使用全部
  // tools: ['Read', 'Bash'] → 只包含列出的
  if (hasWildcard) return { resolvedTools: allowedAvailableTools }
  
  // 精确匹配
  for (const toolSpec of agentTools) {
    const tool = availableToolMap.get(toolName)
    if (tool) resolved.push(tool)
  }
}

6.3 第三层:allowedAgentTypes 元数据

通过 Agent(worker, researcher) 语法限制可产生的子 Agent 类型:

// 解析 "Agent(worker, researcher)" 中的类型列表
if (toolName === AGENT_TOOL_NAME && ruleContent) {
  allowedAgentTypes = ruleContent.split(',').map(s => s.trim())
}

6.4 Fork 路径的特殊处理

Fork 子代使用 useExactTools: true完全跳过 resolveAgentTools()

const resolvedTools = useExactTools
  ? availableTools  // 直接使用父代的精确工具池
  : resolveAgentTools(agentDefinition, availableTools, isAsync).resolvedTools

这保证了 API 请求中工具定义的字节级一致,从而命中 Prompt Cache。


七、权限冒泡(Permission Bubbling)

7.1 bubble 权限模式

bubble 是一个内部权限模式(不对用户暴露),专为 Fork Sub-Agent 设计:

// runAgent.ts
const shouldAvoidPrompts =
  canShowPermissionPrompts !== undefined
    ? !canShowPermissionPrompts
    : agentPermissionMode === 'bubble'
      ? false   // bubble 模式:始终不隐避提示
      : isAsync  // 普通异步 Agent:隐避提示

关键行为:

  • bubble 模式下,即使 Agent 是异步的,权限提示仍然显示在父终端
  • 对于后台 Agent 且可显示提示的,设置 awaitAutomatedChecksBeforeDialog: true

7.2 工具权限作用域隔离

if (allowedTools !== undefined) {
  toolPermissionContext = {
    ...toolPermissionContext,
    alwaysAllowRules: {
      cliArg: state.toolPermissionContext.alwaysAllowRules.cliArg, // 保留 SDK 级权限
      session: [...allowedTools], // 用提供的作为会话级权限
    },
  }
}

这防止了父代的 session-level 权限泄漏到子代。


八、模型选择机制

getAgentModel() 的优先级链:

1. 环境变量 CLAUDE_CODE_SUBAGENT_MODEL → 最高
2. 工具调用指定的 model 参数 → 次高
3. Agent 定义的 model 字段 → 次低
4. getDefaultSubagentModel() → 'inherit' → 最低

'inherit' 的解析逻辑:

if (agentModelWithExp === 'inherit') {
  return getRuntimeMainLoopModel({
    permissionMode: permissionMode ?? 'default',
    mainLoopModel: parentModel,
    exceeds200kTokens: false,
  })
}

别名匹配优化:如果 sonnet 别名匹配父代的 tier(如父代已经是 Sonnet),直接使用父代的精确模型字符串,避免别名解析到不同版本。

function aliasMatchesParentTier(alias: string, parentModel: string): boolean {
  const canonical = getCanonicalName(parentModel)
  switch (alias.toLowerCase()) {
    case 'opus':   return canonical.includes('opus')
    case 'sonnet': return canonical.includes('sonnet')
    case 'haiku':  return canonical.includes('haiku')
    default:       return false
  }
}

Bedrock 区域前缀继承:子代自动继承父代的跨区域推理前缀(如 eu.us.),除非子代显式指定了自己的区域。


九、异步/后台 Agent 生命周期

9.1 触发条件

const shouldRunAsync = (
  run_in_background === true ||       // 明确请求
  selectedAgent.background === true || // Agent 定义标记
  isCoordinator ||                     // Coordinator 模式
  forceAsync ||                        // Fork 实验(全部异步化)
  assistantForceAsync ||               // KAIROS 模式
  proactiveModule?.isProactiveActive() // 主动模式
) && !isBackgroundTasksDisabled

9.2 异步生命周期(runAsyncAgentLifecycle)

1. registerAsyncAgent() → 在 AppState.tasks 中创建条目
2. 名称注册 → agentNameRegistry(用于 SendMessage 路由)
3. 创建 ProgressTracker → 追踪 token 和工具调用
4. 可选启动摘要 → startAgentSummarization()
5. for await (message of makeStream(...)):
   ├── agentMessages.push(message)
   ├── updateProgressFromMessage()
   └── updateAsyncAgentProgress()
6. finalizeAgentTool() → 生成最终结果
7. completeAsyncAgent() → 标记完成
8. classifyHandoffIfNeeded() → 安全分类器审查
9. enqueueAgentNotification() → <task-notification> 通知父代

9.3 同步到异步的转换

同步 Agent 注册为 foreground 任务后,支持中途背景化——这是一个精巧的运行时执行模式热切换

原理:同步执行的消息循环中,通过 Promise.race 同时竞争两个 Promise——下一条 LLM 消息和背景化信号。当用户触发 backgroundAll() 时,背景化 Promise 先 resolve,触发模式切换。

// 注册时设置 120 秒自动背景化计时器
const registration = registerAgentForeground({
  agentId: syncAgentId,
  autoBackgroundMs: getAutoBackgroundMs() || undefined // 120秒
})

// 核心竞争循环
while (true) {
  const nextMessagePromise = agentIterator.next()
  
  // Promise.race:消息 vs 背景化信号
  const raceResult = await Promise.race([
    nextMessagePromise.then(r => ({ type: 'message', result: r })),
    backgroundPromise  // 背景化信号(用户 ESC 或 120 秒超时)
  ])

  if (raceResult.type === 'background') {
    wasBackgrounded = true
    
    // 关键步骤 1:清理前台 async iterator
    // agentIterator.return() 触发 runAgent() 的 finally block
    // 释放 MCP 连接、session hooks、prompt cache tracking 等
    // 1 秒超时保护 + .catch() 防止未处理 rejection
    await Promise.race([
      agentIterator.return(undefined).catch(() => {}),
      sleep(1000)
    ])

    // 关键步骤 2:启动新的后台流
    void runWithAgentContext(syncAgentContext, async () => {
      // 回放已收集的消息到新的 progress tracker
      for (const existingMsg of agentMessages) {
        updateProgressFromMessage(tracker, existingMsg, ...)
      }
      // 使用正确的 agentId 和 task 自己的 abortController 创建新流
      // 注意:abortController 不链接到父代(后台 Agent 在用户 ESC 时存活)
      for await (const msg of runAgent({
        ...runAgentParams,
        isAsync: true,
        override: { agentId: backgroundedTaskId, abortController: task.abortController }
      })) { ... }
    })
    
    // 关键步骤 3:立即返回 async_launched 结果
    return { data: { status: 'async_launched', agentId: backgroundedTaskId } }
  }
}

设计要点

  1. Iterator 清理的超时保护agentIterator.return() 可能因 MCP 服务器清理挂起而阻塞,1 秒超时确保不会无限等待
  2. Progress 回放:新的后台流需要知道前台已经完成了多少工作,通过回放 agentMessages 实现进度连续性
  3. AbortController 隔离:后台 Agent 使用 task 注册时的独立 controller,不链接到父代——这确保用户 ESC 取消主线程时,后台 Agent 继续运行

9.4 安全交接:Handoff Classifier

Sub-Agent 完成后,如果处于 auto 权限模式,会触发安全分类器审查 Sub-Agent 的所有操作:

// agentToolUtils.ts — 仅在 auto 模式下触发
async function classifyHandoffIfNeeded({ agentMessages, tools, ... }) {
  if (toolPermissionContext.mode !== 'auto') return null // 非 auto 模式跳过

  // 1. 构建紧凑的操作 transcript(不是完整消息体)
  const agentTranscript = buildTranscriptForClassifier(agentMessages, tools)
  // 格式:每条 tool_use → "toolName encodedInput\n"
  // 格式:每条 user text → "User: text\n"

  // 2. 调用 YOLO 分类器
  const classifierResult = await classifyYoloAction(agentMessages, {
    role: 'user',
    content: [{ type: 'text', 
      text: "Sub-agent has finished. Review the sub-agent's work based on block rules..." }]
  }, tools, toolPermissionContext, abortSignal)

  // 3. 结果处理
  if (classifierResult.unavailable)
    return "Note: Safety classifier was unavailable. Please carefully verify..."
  if (classifierResult.shouldBlock)
    return "SECURITY WARNING: This sub-agent performed actions that may violate security policy..."
  return null // 允许通过
}

设计关键completeAsyncAgent()classifyHandoffIfNeeded() 之前调用(gh-20236),确保 TaskOutput(block=true) 立即解除阻塞。分类器是异步的"装饰"操作,不应阻塞状态转换。


十、Coordinator 模式

Coordinator 模式架构

10.1 启用条件

function isCoordinatorMode(): boolean {
  if (feature('COORDINATOR_MODE')) {
    return isEnvTruthy(process.env.CLAUDE_CODE_COORDINATOR_MODE)
  }
  return false
}

10.2 核心设计

Coordinator 是一个纯编排者,只负责调度 Worker,自己不直接执行工具。

可用工具

  • Agent — 产生新 Worker
  • SendMessage — 继续现有 Worker
  • TaskStop — 停止运行中的 Worker
  • subscribe_pr_activity / unsubscribe_pr_activity

系统提示的关键原则

1. 角色: 编排者,不直接执行
2. 并行优先: "Parallelism is your superpower"
3. 不窥探: Fork 的输出文件不要主动读取
4. 不捏造: 不预测 Worker 结果
5. 合成优先: 必须先理解研究结果再指导实施

任务工作流

Research (Workers, parallel) → Synthesis (Coordinator) → Implementation (Workers) → Verification (Workers)

10.3 Worker 上下文注入

function getCoordinatorUserContext(mcpClients, scratchpadDir?) {
  let content = `Workers have access to these tools: ${workerTools}`
  
  if (mcpClients.length > 0) {
    content += `\nWorkers also have MCP tools from: ${serverNames}`
  }
  
  if (scratchpadDir && isScratchpadGateEnabled()) {
    content += `\nScratchpad directory: ${scratchpadDir}`
    // Workers 可以在此目录无需权限提示地读写
  }
}

10.4 与 Fork 的互斥关系

export function isForkSubagentEnabled(): boolean {
  if (feature('FORK_SUBAGENT')) {
    if (isCoordinatorMode()) return false  // 互斥!
    ...
  }
}

Coordinator 和 Fork 代表了两种不同的多代理协作范式:

  • Coordinator:中央编排,Worker 无上下文
  • Fork:去中心化,子代继承完整上下文

10.5 Worker 通知机制的技术实现

Worker 完成后通过 <task-notification> XML 格式的 user-role 消息异步通知 Coordinator。关键实现细节:

// LocalAgentTask.tsx — 原子化防重复通知
function enqueueAgentNotification({ taskId, description, status, ... }) {
  // 原子化 check-and-set:防止重复通知
  let shouldEnqueue = false
  updateTaskState(taskId, setAppState, task => {
    if (task.notified) return task  // 已通知过,跳过
    shouldEnqueue = true
    return { ...task, notified: true }
  })
  if (!shouldEnqueue) return

  // 中止正在进行的推测执行(背景任务状态变更,推测结果可能过时)
  abortSpeculation(setAppState)

  // 构建 XML 通知消息
  const message = `<task-notification>
<task-id>${taskId}</task-id>
<tool-use-id>${toolUseId}</tool-use-id>
<output-file>${outputPath}</output-file>
<status>${status}</status>
<summary>${summary}</summary>
<result>${finalMessage}</result>
<usage><total_tokens>N</total_tokens><tool_uses>N</tool_uses></usage>
<worktree><worktree-path>...</worktree-path></worktree>
</task-notification>`

  enqueuePendingNotification({ value: message, mode: 'task-notification' })
}

10.6 Session 模式恢复

恢复(resume)会话时,Coordinator 模式通过实时翻转环境变量确保模式一致性:

function matchSessionMode(sessionMode: 'coordinator' | 'normal' | undefined) {
  if (currentIsCoordinator === sessionIsCoordinator) return undefined

  // 翻转 env var — isCoordinatorMode() 实时读取,无缓存
  if (sessionIsCoordinator) process.env.CLAUDE_CODE_COORDINATOR_MODE = '1'
  else delete process.env.CLAUDE_CODE_COORDINATOR_MODE
}

十一、Prompt Cache 优化策略全景

Token 优化是 Claude Code Sub-Agent 系统中最值得学习的工程实践。

Token 优化效果量化

11.1 Fork 的缓存共享

维度 实现方式
系统提示 使用父代已渲染字节 renderedSystemPrompt
工具定义 useExactTools: true 传递父代工具池
思考配置 继承父代 thinkingConfig
消息前缀 buildForkedMessages() 确保字节一致
占位符 统一 "Fork started — processing in background"

11.2 Explore/Plan Agent 的优化

// 省略 CLAUDE.md(节省 ~5-15 Gtok/周,涉及 3400万+ Explore 调用)
const shouldOmitClaudeMd = agentDefinition.omitClaudeMd && ...

// 省略 gitStatus(节省 ~1-3 Gtok/周)
const resolvedSystemContext =
  agentDefinition.agentType === 'Explore' || agentDefinition.agentType === 'Plan'
    ? systemContextNoGit : baseSystemContext

11.3 Agent 列表动态化

function shouldInjectAgentListInMessages(): boolean {
  // 将 agent 列表从工具 description 移到 attachment 消息
  // 避免 MCP/plugin/权限变化导致工具 schema 缓存失效
  // 该列表占 fleet cache_creation tokens 的 ~10.2%
}

11.4 One-Shot Agent 的 Token 节省

// Explore/Plan 是一次性 Agent,不需要 SendMessage 继续
const ONE_SHOT_BUILTIN_AGENT_TYPES = new Set(['Explore', 'Plan'])

// 跳过 agentId/SendMessage/usage 尾部
// ~135 字符 × 3400万/周 ≈ 1-2 Gtok/周
if (ONE_SHOT_BUILTIN_AGENT_TYPES.has(data.agentType) && !worktreeInfoText) {
  return { content: contentOrMarker }  // 无尾部
}

11.5 Token 优化效果汇总

优化项 节省量 触发频率
Explore/Plan 省略 CLAUDE.md ~5-15 Gtok/周 3400万+ 次/周
Explore/Plan 省略 gitStatus ~1-3 Gtok/周 3400万+ 次/周
One-shot trailer 省略 ~1-2 Gtok/周 3400万+ 次/周
Agent 列表动态化 ~10.2% cache_creation 每次工具调用
Fork 缓存共享 避免重复 cache_creation 每次 Fork

十二、Agent 内存系统

12.1 三种持久内存作用域

type AgentMemoryScope = 'user' | 'project' | 'local'

function getAgentMemoryDir(agentType: string, scope: AgentMemoryScope): string {
  switch (scope) {
    case 'user':    return join(getMemoryBaseDir(), 'agent-memory', dirName)
    case 'project': return join(getCwd(), '.claude', 'agent-memory', dirName)
    case 'local':   return join(getCwd(), '.claude', 'agent-memory-local', dirName)
  }
}
  • user~/.claude/agent-memory/<agentType>/ — 跨项目通用
  • project.claude/agent-memory/<agentType>/ — 项目级,可入版本控制
  • local.claude/agent-memory-local/<agentType>/ — 本地,不入版本控制

12.2 内存快照机制

async function checkAgentMemorySnapshot(agentType, scope) {
  // 检查是否存在项目快照
  const snapshotMeta = await readJsonFile(getSnapshotJsonPath(agentType), ...)
  
  // 本地无内存 → 从快照初始化
  if (!hasLocalMemory) return { action: 'initialize', snapshotTimestamp }
  
  // 快照比本地新 → 提示更新
  if (new Date(snapshotMeta.updatedAt) > new Date(syncedMeta.syncedFrom))
    return { action: 'prompt-update', snapshotTimestamp }
}

十二点五、Agent 恢复(Resume)机制

Sub-Agent 支持从 sidechain transcript 恢复执行,这是一个涉及消息过滤、状态重建、缓存稳定性的复杂流程。

12.6 Transcript 加载与三层过滤

// resumeAgent.ts — 三层过滤确保消息一致性
const resumedMessages = filterWhitespaceOnlyAssistantMessages(    // L3: 空白 assistant
  filterOrphanedThinkingOnlyMessages(                              // L2: 孤立 thinking
    filterUnresolvedToolUses(transcript.messages)                  // L1: 未配对 tool_use
  )
)
  • L1 filterUnresolvedToolUses:移除没有对应 tool_resulttool_use 块(防止 API 400 错误)
  • L2 filterOrphanedThinkingOnlyMessages:移除仅含 thinking 的孤立消息(autocompact 残留)
  • L3 filterWhitespaceOnlyAssistantMessages:移除仅含空白的 assistant 消息

12.7 contentReplacementState 的状态重建

恢复时需要重建 contentReplacementState 以确保 Prompt Cache 稳定性:

// toolResultStorage.ts — 从 sidechain records 重建
function reconstructForSubagentResume(parentState, resumedMessages, sidechainRecords) {
  if (!parentState) return undefined // 功能关闭

  // 从 sidechain records 中提取已知的替换记录
  // parent 的 live replacements 填充 fork 继承的 mustReapply 条目的缺口
  return reconstructContentReplacementState(
    resumedMessages, sidechainRecords, parentState.replacements
  )
}

12.8 Fork vs 非 Fork 的恢复差异

维度 Fork 恢复 非 Fork 恢复
Agent 选择 使用 FORK_AGENT 在 activeAgents 中查找,fallback 到 general-purpose
系统提示 优先用 renderedSystemPrompt,否则完整重建 undefined(runAgent 在 wrapWithCwd 内重新计算)
工具池 父代的精确工具池(cache hit) assembleToolPool 重新构建
useExactTools true 不设置
forkContextMessages undefined(transcript 已含父上下文) undefined

12.9 Worktree 路径的安全验证

// 恢复时验证 worktree 目录是否仍然存在
const resumedWorktreePath = meta?.worktreePath
  ? await fsp.stat(meta.worktreePath).then(
      s => s.isDirectory() ? meta.worktreePath : undefined,
      () => { /* 目录不存在,优雅降级到 parent cwd */ return undefined }
    )
  : undefined

// 更新 mtime 防止 stale-worktree 清理删除刚恢复的 worktree (#22355)
if (resumedWorktreePath) {
  await fsp.utimes(resumedWorktreePath, new Date(), new Date())
}

十三、MCP 服务器集成

每个 Agent 可以定义自己的 MCP 服务器,作为父代的补充

async function initializeAgentMcpServers(agentDefinition, parentClients) {
  for (const spec of agentDefinition.mcpServers) {
    if (typeof spec === 'string') {
      // 字符串引用:查找已有 MCP 配置
      config = getMcpConfigByName(spec)
    } else {
      // 内联定义:{ [name]: config } → 新建连接
      isNewlyCreated = true
    }
    
    const client = await connectToServer(name, config)
    agentClients.push(client)
  }
  
  // 清理:仅清理新创建的连接,不影响共享引用
  const cleanup = async () => {
    for (const client of newlyCreatedClients) {
      await client.cleanup()
    }
  }
  
  return { clients: [...parentClients, ...agentClients], tools: agentTools, cleanup }
}

安全约束:在 strictPluginOnlyCustomization 模式下,用户定义的 Agent 的 MCP 服务器被跳过,只允许管理员信任源的 MCP。


十四、Feature Flag 控制体系

Flag 用途 控制方式
FORK_SUBAGENT Fork Sub-Agent 代码 Feature Gate
BUILTIN_EXPLORE_PLAN_AGENTS Explore/Plan Agent GrowthBook tengu_amber_stoat
VERIFICATION_AGENT Verification Agent GrowthBook tengu_hive_evidence
COORDINATOR_MODE Coordinator 模式 环境变量 CLAUDE_CODE_COORDINATOR_MODE
TRANSCRIPT_CLASSIFIER 安全分类器 代码 Feature Gate
PROMPT_CACHE_BREAK_DETECTION Cache 断裂检测 代码 Feature Gate
AGENT_MEMORY_SNAPSHOT 内存快照 代码 Feature Gate
KAIROS Assistant/Daemon 模式 代码 Feature Gate

十五、架构总结

设计亮点总结

  1. 三策略状态隔离模型createSubagentContext 对每个字段采取克隆/新建/No-op 三种策略,其中 contentReplacementState 必须克隆而非新建以确保 Prompt Cache 命中,setAppStateForTasks 必须始终共享以防止僵尸进程——每个设计决策都有明确的技术原因。

  2. WeakRef 实现的内存安全 AbortController 联动:父到子单向传播,子被丢弃后可安全 GC,子 abort 自动清理父上的 listener 防止 handler 堆积。这解决了长会话中可能产生数百个子 Agent 的内存泄漏问题。

  3. Fork 缓存共享的五维字节一致性:系统提示(renderedSystemPrompt)、工具定义(useExactTools)、思考配置(thinkingConfig)、消息前缀(buildForkedMessages 统一占位符)、contentReplacementState(克隆确保相同替换决策)——五个维度共同保证 API 请求前缀字节级一致。

  4. 运行时执行模式热切换:通过 Promise.race 实现同步→异步的无缝转换,包含 iterator 清理的超时保护(1秒)、progress 回放确保连续性、AbortController 隔离确保后台 Agent 在 ESC 时存活。

  5. 原子化防重复通知enqueueAgentNotification 使用 updateTaskState 的原子 check-and-set 模式,配合 notified flag 防止 TaskStopTool 和 Agent 完成通知的竞态重复。completeAsyncAgent 在所有异步操作(classifier、worktree cleanup)之前执行,确保 TaskOutput(block=true) 立即解除阻塞(gh-20236)。

  6. 三层消息过滤的恢复机制filterUnresolvedToolUses → filterOrphanedThinkingOnlyMessages → filterWhitespaceOnlyAssistantMessages,配合 reconstructForSubagentResume 从 sidechain records 重建 contentReplacementState,确保恢复后的 Agent 与中断前的 Prompt Cache 前缀完全一致。

  7. Handoff Classifier 的非阻塞安全审查:仅在 auto 权限模式下触发,将 Agent 的操作 transcript 压缩为紧凑格式送入分类器。分类器不可用时降级为警告而非阻塞——体现了"安全作为装饰而非门控"的设计哲学。

  8. 对抗性验证的认知去偏:Verification Agent 的 criticalSystemReminder_EXPERIMENTAL 在每个 user turn 注入提醒,系统提示明确列举 LLM 验证者的两种已知偏见(验证回避、被 80% 完成度诱惑),这是 Anthropic 在"让 AI 审查 AI"领域的实践探索。


参考资料

「真诚赞赏,手留余香」

爱折腾的工程师

真诚赞赏,手留余香

使用微信扫描二维码完成支付