一、引言
在之前的 Claude Code源码深度技术解析 一文中,我们对 Claude Code 的整体架构进行了全局梳理。本文将聚焦其中最精巧的子系统之一——内置 Sub-Agent(子代理)系统,深入分析其类型体系、调度机制、上下文隔离、缓存优化和多模式协作的工程实现。
Claude Code 的 Sub-Agent 系统并非简单的"调用另一个 Agent",而是一套涵盖类型定义、工具过滤、权限冒泡、Prompt Cache 共享、异步生命周期管理的完整框架。理解这套系统,对于理解现代 AI 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,在运行时注入:
- 用户配置的自定义 Skills
- 自定义 Agents 列表
- 已连接的 MCP 服务器
- 插件命令
- 用户 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 完整生命周期
4.1 调用入口与类型路由
当主 Agent 调用 Agent 工具时,入口在 AgentTool.tsx 的 call() 方法。输入 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 优化方面最精巧的设计之一。
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):
TaskOutput、ExitPlanMode_v2、EnterPlanModeAgent(外部版本禁止嵌套;ant 版允许)TaskStop、Workflow
异步 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 } }
}
}
设计要点:
- Iterator 清理的超时保护:
agentIterator.return()可能因 MCP 服务器清理挂起而阻塞,1 秒超时确保不会无限等待 - Progress 回放:新的后台流需要知道前台已经完成了多少工作,通过回放
agentMessages实现进度连续性 - 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 模式
10.1 启用条件
function isCoordinatorMode(): boolean {
if (feature('COORDINATOR_MODE')) {
return isEnvTruthy(process.env.CLAUDE_CODE_COORDINATOR_MODE)
}
return false
}
10.2 核心设计
Coordinator 是一个纯编排者,只负责调度 Worker,自己不直接执行工具。
可用工具:
Agent— 产生新 WorkerSendMessage— 继续现有 WorkerTaskStop— 停止运行中的 Workersubscribe_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 系统中最值得学习的工程实践。
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_result的tool_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 |
十五、架构总结
设计亮点总结
-
三策略状态隔离模型:
createSubagentContext对每个字段采取克隆/新建/No-op 三种策略,其中contentReplacementState必须克隆而非新建以确保 Prompt Cache 命中,setAppStateForTasks必须始终共享以防止僵尸进程——每个设计决策都有明确的技术原因。 -
WeakRef 实现的内存安全 AbortController 联动:父到子单向传播,子被丢弃后可安全 GC,子 abort 自动清理父上的 listener 防止 handler 堆积。这解决了长会话中可能产生数百个子 Agent 的内存泄漏问题。
-
Fork 缓存共享的五维字节一致性:系统提示(renderedSystemPrompt)、工具定义(useExactTools)、思考配置(thinkingConfig)、消息前缀(buildForkedMessages 统一占位符)、contentReplacementState(克隆确保相同替换决策)——五个维度共同保证 API 请求前缀字节级一致。
-
运行时执行模式热切换:通过
Promise.race实现同步→异步的无缝转换,包含 iterator 清理的超时保护(1秒)、progress 回放确保连续性、AbortController 隔离确保后台 Agent 在 ESC 时存活。 -
原子化防重复通知:
enqueueAgentNotification使用updateTaskState的原子 check-and-set 模式,配合notifiedflag 防止TaskStopTool和 Agent 完成通知的竞态重复。completeAsyncAgent在所有异步操作(classifier、worktree cleanup)之前执行,确保TaskOutput(block=true)立即解除阻塞(gh-20236)。 -
三层消息过滤的恢复机制:
filterUnresolvedToolUses → filterOrphanedThinkingOnlyMessages → filterWhitespaceOnlyAssistantMessages,配合reconstructForSubagentResume从 sidechain records 重建contentReplacementState,确保恢复后的 Agent 与中断前的 Prompt Cache 前缀完全一致。 -
Handoff Classifier 的非阻塞安全审查:仅在
auto权限模式下触发,将 Agent 的操作 transcript 压缩为紧凑格式送入分类器。分类器不可用时降级为警告而非阻塞——体现了"安全作为装饰而非门控"的设计哲学。 -
对抗性验证的认知去偏:Verification Agent 的
criticalSystemReminder_EXPERIMENTAL在每个 user turn 注入提醒,系统提示明确列举 LLM 验证者的两种已知偏见(验证回避、被 80% 完成度诱惑),这是 Anthropic 在"让 AI 审查 AI"领域的实践探索。
参考资料
- Claude Code 源码(npm 包泄露,基于公开快照分析)
- Anthropic Prompt Caching 文档
- Claude Code 官方文档
「真诚赞赏,手留余香」
真诚赞赏,手留余香
使用微信扫描二维码完成支付