来源与授权
本文来自 Claude-Code-Source-Study 原文,固定导入版本为 31b6e07a82d4。Copyright (c) 2026 Yao,依据 MIT License 提供。
本站仅按“github-public-noncommercial-2026-07-11”用于匿名、免费、非商业学习,并调整章节编排、站内链接、重复标题与阅读标记;不代表原作者对本站的合作、认证或背书。如权利方要求删除,我们会立即下架;授权或删除请求请通过本站首页底部公开联系方式联系。
本章是《深入 Claude Code 源码》系列第 14 章。我们将深入剖析 Agent 子系统的完整架构:从 Agent 定义的数据结构与加载机制,到
runAgent()的完整生命周期,再到createSubagentContext()如何实现 context 隔离与选择性共享。
为什么需要多 Agent?
当你让 Claude Code 帮你"重构整个模块的测试"时,一个单体 Agent 会怎么做?它会搜索文件、阅读代码、编写测试、运行验证——所有步骤串行执行,上下文窗口迅速膨胀。更糟糕的是,搜索过程中产生的大量中间输出(grep 结果、文件内容)会永久占据上下文,挤压真正有价值的信息空间。
Claude Code 的解决方案是多 Agent 协作:主 Agent 可以按需生成子 Agent,每个子 Agent 拥有独立的上下文窗口和对话循环,完成任务后只返回精炼的结果。这就像一个团队 lead 把任务分派给专人,每个人独立工作后汇报结论。
这个设计解决了三个核心问题:
- 上下文污染:搜索类任务的海量中间输出不会进入主 Agent 的上下文
- 专业化分工:不同类型的子 Agent 可以有不同的工具集、权限和 System Prompt
- 并行执行:多个子 Agent 可以同时在后台运行,互不干扰
本章将回答以下问题:
- Agent 定义的数据结构长什么样?如何从多种来源加载?
runAgent()的完整生命周期经历哪些阶段?- 子 Agent 的 context 隔离是怎样实现的?哪些状态被共享,哪些被隔离?
- 内置 Agent(Explore、Plan、Verification)分别解决什么问题?
本章路线:第一节 AgentDefinition 数据蓝图 → 第二节
runAgent()完整生命周期 → 第三节 工具解析(能力边界)→ 第四节 内置 Agent 类型 → 第五节 Fork Subagent(共享上下文分支)→ 第六节 异步 Agent 生命周期 → 第七节 Agent 记忆 → 第八节 AgentSummary 后台摘要 → 第九节 可迁移模式。第一至第三节 回答「Agent 是什么、怎么跑、能用什么工具」三件事;第四至第五节 是两种不同的 Agent 形态;第六至第八节 是围绕 Agent 的横切服务。
一、AgentDefinition:Agent 的数据蓝图
1.1 三种 Agent 类型
Claude Code 的 Agent 系统有一个清晰的类型体系,定义在 tools/AgentTool/loadAgentsDir.ts 中:
// tools/AgentTool/loadAgentsDir.ts:136-165
export type BuiltInAgentDefinition = BaseAgentDefinition & {
source: 'built-in'
baseDir: 'built-in'
callback?: () => void
getSystemPrompt: (params: {
toolUseContext: Pick<ToolUseContext, 'options'>
}) => string
}
export type CustomAgentDefinition = BaseAgentDefinition & {
getSystemPrompt: () => string
source: SettingSource
filename?: string
baseDir?: string
}
export type PluginAgentDefinition = BaseAgentDefinition & {
getSystemPrompt: () => string
source: 'plugin'
filename?: string
plugin: string
}
export type AgentDefinition =
| BuiltInAgentDefinition
| CustomAgentDefinition
| PluginAgentDefinition
三种类型对应三种来源:
- Built-in:代码中硬编码的内置 Agent(Explore、Plan、general-purpose 等)
- Custom:用户通过
.claude/agents/*.md文件定义的自定义 Agent - Plugin:由插件提供的 Agent
一个关键的设计细节:getSystemPrompt 的签名在三种类型间并不一致。Built-in Agent 接受 toolUseContext 参数(可以根据当前可用工具动态调整 prompt),而 Custom 和 Plugin Agent 的 prompt 是通过闭包捕获的静态内容。
1.2 BaseAgentDefinition:核心字段解析
BaseAgentDefinition 包含了 Agent 可配置的主要维度(loadAgentsDir.ts:106-133):
// tools/AgentTool/loadAgentsDir.ts:106-133
export type BaseAgentDefinition = {
agentType: string // Agent 的唯一标识名
whenToUse: string // 描述何时使用此 Agent(展示给模型选择)
tools?: string[] // 允许使用的工具列表,undefined 或 ['*'] 表示全部
disallowedTools?: string[] // 明确禁止的工具
skills?: string[] // 预加载的 Skill 名称
mcpServers?: AgentMcpServerSpec[] // 专属 MCP 服务器
hooks?: HooksSettings // Session 级 Hook 注册
color?: AgentColorName // UI 中的颜色标识
model?: string // 使用的模型('inherit' 表示继承父级)
effort?: EffortValue // 推理努力程度
permissionMode?: PermissionMode // 权限模式覆盖
maxTurns?: number // 最大对话轮次
background?: boolean // 是否总是作为后台任务运行
initialPrompt?: string // 首轮附加提示
memory?: AgentMemoryScope // 持久化记忆范围(user/project/local)
isolation?: 'worktree' | 'remote' // 隔离模式
omitClaudeMd?: boolean // 是否省略 CLAUDE.md(为只读 Agent 节省 token)
criticalSystemReminder_EXPERIMENTAL?: string // 每轮 user turn 重注入的关键约束
requiredMcpServers?: string[] // 必须可用的 MCP 服务器模式(不满足则 Agent 不可用)
pendingSnapshotUpdate?: { snapshotTimestamp: string } // 记忆快照更新待处理
}
其中几个字段值得特别关注:
criticalSystemReminder_EXPERIMENTAL——Verification Agent 用它在每个 user turn 重复注入"不能修改文件"的硬约束,防止模型在长对话中遗忘requiredMcpServers——通过hasRequiredMcpServers()检查,模式匹配(大小写不敏感的includes)当前可用的 MCP 服务器名,不满足则 Agent 从活跃列表中过滤掉
注意 omitClaudeMd 字段——这是一个典型的大规模运营优化。源码注释写道:
Read-only agents (Explore, Plan) don't need commit/PR/lint guidelines from CLAUDE.md. Saves ~5-15 Gtok/week across 34M+ Explore spawns.
每周省 5-15 Giga token,这就是产品级工程在大规模场景下的优化思维。
1.3 Agent 定义的多源加载
Agent 的加载由 getAgentDefinitionsWithOverrides() 统一编排,它是一个 memoize 包装的异步函数:
// tools/AgentTool/loadAgentsDir.ts:296-393
export const getAgentDefinitionsWithOverrides = memoize(
async (cwd: string): Promise<AgentDefinitionsResult> => {
// 简化模式:只返回内置 Agent
if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) {
return { activeAgents: getBuiltInAgents(), allAgents: getBuiltInAgents() }
}
// 1. 从 .claude/agents/ 目录加载 Markdown 文件
const markdownFiles = await loadMarkdownFilesForSubdir('agents', cwd)
const customAgents = markdownFiles
.map(({ filePath, baseDir, frontmatter, content, source }) =>
parseAgentFromMarkdown(filePath, baseDir, frontmatter, content, source)
)
.filter(agent => agent !== null)
// 2. 加载插件 Agent(memoized,独立于 cwd)
// 当 AGENT_MEMORY_SNAPSHOT feature 且 auto-memory 开启时,
// 会与记忆快照初始化并发执行
const pluginAgents = await loadPluginAgents()
// 3. 获取内置 Agent
const builtInAgents = getBuiltInAgents()
// 4. 合并所有来源,按优先级去重
const allAgentsList = [...builtInAgents, ...pluginAgents, ...customAgents]
const activeAgents = getActiveAgentsFromList(allAgentsList)
return { activeAgents, allAgents: allAgentsList }
}
)
getActiveAgentsFromList() 的去重策略值得关注——它按 [built-in, plugin, user, project, flag, managed] 的顺序遍历,后面的覆盖前面的同名 Agent:
// tools/AgentTool/loadAgentsDir.ts:193-221
export function getActiveAgentsFromList(allAgents: AgentDefinition[]): AgentDefinition[] {
const agentGroups = [
builtInAgents, pluginAgents, userAgents,
projectAgents, flagAgents, managedAgents,
]
const agentMap = new Map<string, AgentDefinition>()
for (const agents of agentGroups) {
for (const agent of agents) {
agentMap.set(agent.agentType, agent)
}
}
return Array.from(agentMap.values())
}
这意味着 managed(企业级管理)的 Agent 定义拥有最高优先级,可以覆盖所有其他来源的同名 Agent。
1.4 Markdown Frontmatter:用户自定义 Agent 的格式
用户在 .claude/agents/ 目录下创建 Markdown 文件来定义 Agent。parseAgentFromMarkdown() 解析 frontmatter 中的配置字段,Markdown body 作为 System Prompt:
---
name: my-researcher
description: "Deep codebase researcher for complex architectural questions"
tools:
- FileRead
- Glob
- Grep
- Bash
model: inherit
permissionMode: plan
maxTurns: 30
memory: project
---
You are a deep research specialist. Your job is to thoroughly
investigate complex codebase questions...
parseAgentFromMarkdown()(loadAgentsDir.ts:541-755)对每个 frontmatter 字段都做了严格的验证和降级处理——无效值不会导致整个 Agent 加载失败,而是记录警告并忽略该字段。
1.5 /agents:交互式管理入口
用户不必手写 Markdown 文件。commands/agents/agents.tsx 注册了一个非常薄的 slash 命令——它的 React 组件只渲染一个 <AgentsMenu>,其余逻辑都委托给 components/agents/AgentsMenu.tsx(同目录还有 AgentDetail、AgentEditor、ToolSelector、ModelSelector、ColorPicker、generateAgent 等子组件)。
这条入口的存在意义在于:把 tools/AgentTool/ 里那套加载、覆盖、frontmatter 校验的规则反向暴露成一组可视化编辑器——用户在 UI 里选择工具集、模型、颜色,最终落盘的还是 .claude/agents/*.md,下一次 getAgentDefinitionsWithOverrides() 就会把它读回来。换句话说,/agents 不是新的数据通路,而是同一份数据蓝图的写入端。
二、runAgent():子 Agent 的完整生命周期
runAgent() 是 Agent 系统的核心引擎,定义在 tools/AgentTool/runAgent.ts:248-860。它是一个 AsyncGenerator,逐步 yield 出子 Agent 产生的消息。
2.1 生命周期时序图
2.2 Phase 1-2:初始化与准备
runAgent() 的前半段是一系列精细的准备工作。
模型解析——Agent 使用的模型经过多级 fallback:
// tools/AgentTool/runAgent.ts:340-345
const resolvedAgentModel = getAgentModel(
agentDefinition.model, // Agent 自己定义的模型
toolUseContext.options.mainLoopModel, // 父级主循环模型
model, // 调用时传入的覆盖
permissionMode, // 权限模式影响模型选择
)
消息构建——子 Agent 的初始消息有两种模式:
- Fork 模式:
forkContextMessages不为空,子 Agent 继承父 Agent 的对话历史(需过滤不完整的 tool_use) - Fresh 模式:从零开始,只有
promptMessages
// tools/AgentTool/runAgent.ts:370-378
const contextMessages: Message[] = forkContextMessages
? filterIncompleteToolCalls(forkContextMessages)
: []
const initialMessages: Message[] = [...contextMessages, ...promptMessages]
// Fork 模式复用父级的文件状态缓存,Fresh 模式创建新的
const agentReadFileState = forkContextMessages !== undefined
? cloneFileStateCache(toolUseContext.readFileState)
: createFileStateCacheWithSizeLimit(READ_FILE_STATE_CACHE_SIZE)
Token 节省优化——对只读 Agent 做了两个有针对性的 context 裁剪:
// tools/AgentTool/runAgent.ts:390-410
// 1. 省略 CLAUDE.md(Explore/Plan 不需要 commit/PR/lint 规则)
const shouldOmitClaudeMd = agentDefinition.omitClaudeMd
&& !override?.userContext
&& getFeatureValue_CACHED_MAY_BE_STALE('tengu_slim_subagent_claudemd', true)
// 2. 省略 gitStatus(Explore/Plan 可自行运行 git status)
const resolvedSystemContext =
agentDefinition.agentType === 'Explore' || agentDefinition.agentType === 'Plan'
? systemContextNoGit
: baseSystemContext
权限模式覆盖——agentGetAppState 是一个精心构造的闭包,它在每次被调用时动态返回包含覆盖权限的 AppState:
// tools/AgentTool/runAgent.ts:415-498
const agentGetAppState = () => {
const state = toolUseContext.getAppState()
let toolPermissionContext = state.toolPermissionContext
// 覆盖权限模式(但 bypassPermissions/acceptEdits/auto 优先级更高)
if (agentPermissionMode && state.toolPermissionContext.mode !== 'bypassPermissions' ...) {
toolPermissionContext = { ...toolPermissionContext, mode: agentPermissionMode }
}
// 异步 Agent 自动拒绝权限弹窗
if (shouldAvoidPrompts) {
toolPermissionContext = {
...toolPermissionContext,
shouldAvoidPermissionPrompts: true,
}
}
// 作用域隔离:提供 allowedTools 时替换 session 级规则
if (allowedTools !== undefined) {
toolPermissionContext = {
...toolPermissionContext,
alwaysAllowRules: {
cliArg: state.toolPermissionContext.alwaysAllowRules.cliArg, // 保留 SDK 级权限
session: [...allowedTools], // 替换 session 级权限
},
}
}
return { ...state, toolPermissionContext, effortValue }
}
注意 cliArg 规则的保留——这确保了 SDK 通过 --allowedTools 传入的权限在子 Agent 中仍然有效,而父 Agent 的 session 级权限不会泄漏给子 Agent。
2.3 Phase 3:Agent 专属 MCP 服务器
Agent 可以通过 frontmatter 定义自己的 MCP 服务器,这些服务器是对父级的增量扩展:
// tools/AgentTool/runAgent.ts:95-218
async function initializeAgentMcpServers(
agentDefinition: AgentDefinition,
parentClients: MCPServerConnection[],
) {
for (const spec of agentDefinition.mcpServers) {
if (typeof spec === 'string') {
// 按名称引用已有的 MCP 服务器——使用 memoized 连接,与父级共享
config = getMcpConfigByName(spec)
} else {
// 内联定义——Agent 专属服务器,需要在 Agent 结束时清理
isNewlyCreated = true
}
const client = await connectToServer(name, config)
agentClients.push(client)
}
// cleanup 只清理新创建的,不清理引用的共享连接
const cleanup = async () => {
for (const client of newlyCreatedClients) {
await client.cleanup()
}
}
// 返回合并后的 clients 和 tools
return {
clients: [...parentClients, ...agentClients],
tools: agentTools,
cleanup,
}
}
这个设计有一个精妙之处:按名称引用的 MCP 服务器由于 connectToServer 是 memoized 的,不会重复连接;而 cleanup 只关闭 Agent 自己内联定义的服务器,不会影响父级共享的连接。
2.4 Phase 4:Context 隔离的核心
createSubagentContext() 定义在 utils/forkedAgent.ts:345-462,是整个 Agent 系统中最关键的函数之一。它为子 Agent 创建一个默认全隔离、显式 opt-in 共享的 ToolUseContext:
// utils/forkedAgent.ts:345-462
export function createSubagentContext(
parentContext: ToolUseContext,
overrides?: SubagentContextOverrides,
): ToolUseContext {
// AbortController:显式覆盖 > 共享父级 > 新建子级(关联父级)
const abortController = overrides?.abortController
?? (overrides?.shareAbortController
? parentContext.abortController
: createChildAbortController(parentContext.abortController))
// getAppState:有覆盖就用覆盖;共享 abortController 说明是交互式 Agent,
// 直接继承父级;否则包装一层 shouldAvoidPermissionPrompts: true
const getAppState = overrides?.getAppState
? overrides.getAppState
: overrides?.shareAbortController
? parentContext.getAppState
: () => {
const state = parentContext.getAppState()
if (state.toolPermissionContext.shouldAvoidPermissionPrompts) return state
return { ...state, toolPermissionContext: {
...state.toolPermissionContext, shouldAvoidPermissionPrompts: true,
}}
}
return {
// ===== 隔离的可变状态 =====
readFileState: cloneFileStateCache(
overrides?.readFileState ?? parentContext.readFileState
),
nestedMemoryAttachmentTriggers: new Set<string>(),
loadedNestedMemoryPaths: new Set<string>(),
dynamicSkillDirTriggers: new Set<string>(),
discoveredSkillNames: new Set<string>(),
toolDecisions: undefined,
// 克隆而非新建——Fork 子 Agent 需要对父级 tool_use_id 做相同的替换决策
contentReplacementState: overrides?.contentReplacementState
?? (parentContext.contentReplacementState
? cloneContentReplacementState(parentContext.contentReplacementState)
: undefined),
// ===== AbortController 与 AppState =====
abortController,
getAppState,
setAppState: overrides?.shareSetAppState
? parentContext.setAppState : () => {},
// 任务注册必须总是到达根 Store(即使 setAppState 是 no-op)
setAppStateForTasks:
parentContext.setAppStateForTasks ?? parentContext.setAppState,
// 异步子 Agent 的 setAppState 是 no-op,需要本地 denial 追踪
// 否则拒绝计数器不会在重试间累积
localDenialTracking: overrides?.shareSetAppState
? parentContext.localDenialTracking
: createDenialTrackingState(),
// ===== 按需共享的度量回调 =====
setInProgressToolUseIDs: () => {},
setResponseLength: overrides?.shareSetResponseLength
? parentContext.setResponseLength : () => {},
pushApiMetricsEntry: overrides?.shareSetResponseLength
? parentContext.pushApiMetricsEntry : undefined,
updateFileHistoryState: () => {},
// Attribution 是函数式的(prev => next),并发调用通过 React state queue 组合,
// 即使 setAppState 被 stub 也安全
updateAttributionState: parentContext.updateAttributionState,
// ===== 子 Agent 不需要的 UI 回调 =====
addNotification: undefined,
setToolJSX: undefined,
setStreamMode: undefined,
setSDKStatus: undefined,
openMessageSelector: undefined,
// ===== 继承或覆盖的属性 =====
options: overrides?.options ?? parentContext.options,
messages: overrides?.messages ?? parentContext.messages,
agentId: overrides?.agentId ?? createAgentId(),
agentType: overrides?.agentType,
fileReadingLimits: parentContext.fileReadingLimits,
userModified: parentContext.userModified,
criticalSystemReminder_EXPERIMENTAL:
overrides?.criticalSystemReminder_EXPERIMENTAL,
requireCanUseTool: overrides?.requireCanUseTool,
// ===== 子 Agent 自己的追踪链 =====
queryTracking: {
chainId: randomUUID(),
depth: (parentContext.queryTracking?.depth ?? -1) + 1,
},
}
}
这段代码体现了一个核心设计原则:默认隔离,显式共享。下表总结了各类状态的隔离策略:
| 状态类别 | 隔离方式 | 原因 |
|---|---|---|
readFileState | 克隆(深拷贝) | 子 Agent 的文件读取不应污染父级缓存 |
getAppState | 条件包装 | 非交互式 Agent 自动注入 shouldAvoidPermissionPrompts: true |
setAppState | 默认 no-op,opt-in 共享 | 同步子 Agent 需要更新共享状态,异步不需要 |
setAppStateForTasks | 总是共享 | 后台 bash 任务的注册/清理必须到达根 Store |
localDenialTracking | 隔离时新建 | 异步 Agent 的 denial 计数器需在重试间本地累积 |
abortController | 新建子级(关联父级) | 父级 abort 传播到子级,但子级 abort 不影响父级 |
contentReplacementState | 克隆 | Fork 子 Agent 需要做一致的替换决策以命中 prompt cache |
pushApiMetricsEntry | 与 setResponseLength 联动 | 共享度量的子 Agent 需要向父级汇报 API 指标 |
updateAttributionState | 总是共享 | 函数式回调,并发安全,不受 setAppState 影响(attribution 指 token 用量、API 计数等指标的"归属"——子 Agent 的消耗回填到发起它的根会话用于计费统计) |
queryTracking | 新建(depth+1) | 每个子 Agent 有独立的调用链追踪 |
| UI 回调(5 个) | undefined | 子 Agent 不能操作父级 UI |
特别注意 setAppStateForTasks 的注释:
Task registration/kill must always reach the root store, even when setAppState is a no-op — otherwise async agents' background bash tasks are never registered and never killed (PPID=1 zombie).
这是一个实际遭遇的 bug 教训:如果后台 Agent 的 bash 任务没有被注册到根 Store,当 Agent 结束后这些任务会变成孤儿进程(PPID=1 zombie)。
2.5 Phase 5:对话循环
准备工作就绪后,runAgent() 进入核心的 query() 循环:
// tools/AgentTool/runAgent.ts:747-806
try {
for await (const message of query({
messages: initialMessages,
systemPrompt: agentSystemPrompt,
userContext: resolvedUserContext,
systemContext: resolvedSystemContext,
canUseTool,
toolUseContext: agentToolUseContext,
querySource,
maxTurns: maxTurns ?? agentDefinition.maxTurns,
})) {
onQueryProgress?.()
// 转发 API 请求指标给父级(TTFT/OTPS 更新)
if (message.type === 'stream_event' && message.event.type === 'message_start') {
toolUseContext.pushApiMetricsEntry?.(message.ttftMs)
continue
}
// 处理 max_turns_reached 信号
if (message.type === 'attachment' && message.attachment.type === 'max_turns_reached') {
break
}
// 记录到 sidechain 并 yield 给父级
if (isRecordableMessage(message)) {
await recordSidechainTranscript([message], agentId, lastRecordedUuid)
yield message
}
}
}
每条消息都被增量记录到 sidechain transcript(磁盘),这使得 Agent 可以在崩溃后恢复执行(resumeAgent.ts)。
2.6 Phase 6:清理——不留任何痕迹
finally 块的清理工作是防止资源泄漏的最后防线:
// tools/AgentTool/runAgent.ts:816-858
finally {
await mcpCleanup() // 关闭 Agent 专属 MCP 服务器
if (agentDefinition.hooks) { // 仅在注册过 hooks 时清除
clearSessionHooks(rootSetAppState, agentId)
}
if (feature('PROMPT_CACHE_BREAK_DETECTION')) { // 受 feature flag 门控
cleanupAgentTracking(agentId)
}
agentToolUseContext.readFileState.clear() // 释放文件状态缓存内存
initialMessages.length = 0 // 释放 fork context 消息
unregisterPerfettoAgent(agentId) // 释放性能追踪条目
clearAgentTranscriptSubdir(agentId) // 释放 transcript 子目录映射
// 释放 AppState 中的 todos 条目——防止内存泄漏
rootSetAppState(prev => {
if (!(agentId in prev.todos)) return prev
const { [agentId]: _removed, ...todos } = prev.todos
return { ...prev, todos }
})
// 杀死 Agent 生成的后台 bash 任务——防止孤儿进程
killShellTasksForAgent(agentId, toolUseContext.getAppState, rootSetAppState)
// 受 feature flag 门控:清理 monitor MCP 任务
if (feature('MONITOR_TOOL')) {
mcpMod.killMonitorMcpTasksForAgent(agentId, ...)
}
}
这段清理代码的注释揭示了一个真实的大规模运营问题:
Whale sessions spawn hundreds of agents; each orphaned key is a small leak that adds up.
大用户(whale)的会话可能生成数百个 Agent,每个 Agent 在 AppState.todos 中留下一个空条目——看似无害的 {} 累积起来就是显著的内存泄漏。
三、工具解析:Agent 的能力边界
3.1 工具过滤的三层机制
子 Agent 的可用工具经过三层过滤,定义在 tools/AgentTool/agentToolUtils.ts:70-225。整体趋势是逐层收敛,但每层都有明确的例外通道:
第一层:全局禁止——所有 Agent 都不能使用的工具:
// constants/tools.ts:36-46
export const ALL_AGENT_DISALLOWED_TOOLS = new Set([
TASK_OUTPUT_TOOL_NAME,
EXIT_PLAN_MODE_V2_TOOL_NAME,
ENTER_PLAN_MODE_TOOL_NAME,
// 外部版本禁止 Agent 嵌套
...(process.env.USER_TYPE === 'ant' ? [] : [AGENT_TOOL_NAME]),
ASK_USER_QUESTION_TOOL_NAME,
TASK_STOP_TOOL_NAME,
])
但 filterToolsForAgent() 在执行全局禁止检查之前,有两个优先级更高的放行通道:
// tools/AgentTool/agentToolUtils.ts:81-93
// 例外 1:MCP 工具无条件放行——任何 mcp__ 前缀的工具跳过所有过滤
if (tool.name.startsWith('mcp__')) return true
// 例外 2:plan 模式下放行 ExitPlanMode(in-process teammates 需要退出 plan)
if (toolMatchesName(tool, EXIT_PLAN_MODE_V2_TOOL_NAME) && permissionMode === 'plan') return true
第二层:异步 Agent 限制——后台运行的 Agent 只能用白名单中的工具:
// constants/tools.ts:55-71
export const ASYNC_AGENT_ALLOWED_TOOLS = new Set([
FILE_READ_TOOL_NAME, WEB_SEARCH_TOOL_NAME, TODO_WRITE_TOOL_NAME,
GREP_TOOL_NAME, WEB_FETCH_TOOL_NAME, GLOB_TOOL_NAME,
...SHELL_TOOL_NAMES,
FILE_EDIT_TOOL_NAME, FILE_WRITE_TOOL_NAME, NOTEBOOK_EDIT_TOOL_NAME,
SKILL_TOOL_NAME, SYNTHETIC_OUTPUT_TOOL_NAME, TOOL_SEARCH_TOOL_NAME,
ENTER_WORKTREE_TOOL_NAME, EXIT_WORKTREE_TOOL_NAME,
])
这里同样有例外——当 Agent Swarms 启用且当前是 in-process teammate 时,异步 Agent 还可以额外使用 AgentTool(生成同步子 Agent)以及 IN_PROCESS_TEAMMATE_ALLOWED_TOOLS(TaskCreate/Get/List/Update、SendMessage 等协调工具)。
第三层:Agent 定义级——每个 Agent 的 tools 和 disallowedTools 字段:
// tools/AgentTool/agentToolUtils.ts:122-225
export function resolveAgentTools(agentDefinition, availableTools, isAsync) {
// 先应用全局过滤
const filteredAvailableTools = filterToolsForAgent({ tools, isBuiltIn, isAsync })
// 再应用 disallowedTools
const allowedAvailableTools = filteredAvailableTools.filter(
tool => !disallowedToolSet.has(tool.name)
)
// tools 为 undefined 或 ['*'] 时允许全部
const hasWildcard = agentTools === undefined
|| (agentTools.length === 1 && agentTools[0] === '*')
if (hasWildcard) {
return { hasWildcard: true, resolvedTools: allowedAvailableTools }
}
// 否则只允许明确列出的工具
// ...按名称匹配...
}
这三层过滤形成了一个总体收敛但有例外通道的权限模型。整体趋势是每一层只能进一步收缩,但 MCP 工具(mcp__ 前缀)无条件穿透所有层级,plan 模式的 ExitPlanMode 可以绕过全局禁止,in-process teammate 可以在异步限制层获得额外工具。这些例外都是硬编码的白名单,不能被 Agent 定义扩展——安全边界仍然是可控的。
四、内置 Agent 类型剖析
4.1 内置 Agent 注册
builtInAgents.ts 按条件组装内置 Agent 列表:
// tools/AgentTool/builtInAgents.ts:22-72
export function getBuiltInAgents(): AgentDefinition[] {
// SDK 用户可通过环境变量禁用所有内置 Agent
if (isEnvTruthy(process.env.CLAUDE_AGENT_SDK_DISABLE_BUILTIN_AGENTS)
&& getIsNonInteractiveSession()) {
return []
}
// Coordinator Mode 下返回完全不同的 Agent 集合
if (feature('COORDINATOR_MODE') && isEnvTruthy(process.env.CLAUDE_CODE_COORDINATOR_MODE)) {
return getCoordinatorAgents() // 懒 require 避免循环依赖
}
const agents: AgentDefinition[] = [
GENERAL_PURPOSE_AGENT,
STATUSLINE_SETUP_AGENT,
]
if (areExplorePlanAgentsEnabled()) {
agents.push(EXPLORE_AGENT, PLAN_AGENT)
}
// 非 SDK 入口点包含 Claude Code Guide Agent
if (isNonSdkEntrypoint) {
agents.push(CLAUDE_CODE_GUIDE_AGENT)
}
// Verification Agent 受 feature flag 和 A/B 测试门控
if (feature('VERIFICATION_AGENT')
&& getFeatureValue_CACHED_MAY_BE_STALE('tengu_hive_evidence', false)) {
agents.push(VERIFICATION_AGENT)
}
return agents
}
注意两个重要细节:
- Coordinator Mode 短路:开启 Coordinator Mode 时,
getBuiltInAgents()直接返回 coordinator 专用的 Agent 集合,完全替换正常的内置 Agent。这里用了懒require避免coordinatorMode → tools → AgentTool → builtInAgents的循环依赖。 - 入口与 feature flag 门控:
CLAUDE_CODE_GUIDE_AGENT只在非 SDK 入口(即真正的 CLI 会话)下注册;VERIFICATION_AGENT受 feature flag 与 GrowthBook 双重门控。下文 第 4.2 至 4.6 节 会逐个展开这些 Agent 的设计取舍。
4.2 Explore Agent:只读搜索专家
// tools/AgentTool/built-in/exploreAgent.ts:64-83
export const EXPLORE_AGENT: BuiltInAgentDefinition = {
agentType: 'Explore',
disallowedTools: [AGENT_TOOL_NAME, FILE_EDIT_TOOL_NAME, FILE_WRITE_TOOL_NAME, ...],
model: process.env.USER_TYPE === 'ant' ? 'inherit' : 'haiku',
omitClaudeMd: true,
getSystemPrompt: () => getExploreSystemPrompt(),
}
Explore Agent 的设计哲学是快速、便宜、只读:
- 只读锁定:通过
disallowedTools明确禁止所有写操作工具,System Prompt 中用=== CRITICAL: READ-ONLY MODE ===强调 - 使用更小的模型:外部用户默认
haiku(更快更便宜),内部用户inherit(但受 GrowthBook A/B 测试控制) - 省略 CLAUDE.md:
omitClaudeMd: true每周节省数十亿 token - 并行搜索指导:System Prompt 中明确要求"try to spawn multiple parallel tool calls"
4.3 Plan Agent:只读架构师
// tools/AgentTool/built-in/planAgent.ts:73-92
export const PLAN_AGENT: BuiltInAgentDefinition = {
agentType: 'Plan',
disallowedTools: [AGENT_TOOL_NAME, FILE_EDIT_TOOL_NAME, FILE_WRITE_TOOL_NAME, ...],
model: 'inherit',
omitClaudeMd: true,
getSystemPrompt: () => getPlanV2SystemPrompt(),
}
Plan Agent 和 Explore 共享相同的只读约束,但 System Prompt 引导的方向不同:
- 引导做"架构设计"而非"搜索"
- 要求输出结构化的实施计划,包含"Critical Files for Implementation"列表
- 使用
inherit模型(规划需要更强的推理能力)
4.4 Verification Agent:对抗性验证者
Verification Agent 是最有意思的内置 Agent——它的 System Prompt 采用了对抗性设计:
// tools/AgentTool/built-in/verificationAgent.ts:10-12
const VERIFICATION_SYSTEM_PROMPT = `You are a verification specialist.
Your job is not to confirm the implementation works — it's to try to break it.
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...`
它的 System Prompt 直接列出了模型常见的"偷懒模式"并要求模型自我对抗。需要注意的是,Verification Agent 虽然不能修改项目目录中的文件,但明确允许在 /tmp 或 $TMPDIR 中编写临时测试脚本(如多步竞态测试脚本、Playwright 测试),使用后需自行清理。此外它还被引导去检查自己是否拥有浏览器自动化工具(mcp__claude-in-chrome__*、mcp__playwright__*)——这些 MCP 工具穿透了工具过滤的所有层级。
同时使用 criticalSystemReminder_EXPERIMENTAL 在每个 user turn 重复注入关键约束:
// tools/AgentTool/built-in/verificationAgent.ts:150-151
criticalSystemReminder_EXPERIMENTAL:
'CRITICAL: This is a VERIFICATION-ONLY task. You CANNOT edit, write, or create files...',
输出格式也有严格约束——必须包含实际执行的命令和输出,以 VERDICT: PASS/FAIL/PARTIAL 结尾。
4.5 General-purpose Agent:通用工人
// tools/AgentTool/built-in/generalPurposeAgent.ts:25-34
export const GENERAL_PURPOSE_AGENT: BuiltInAgentDefinition = {
agentType: 'general-purpose',
tools: ['*'], // 可以使用所有工具
source: 'built-in',
// model 故意省略——使用 getDefaultSubagentModel()
getSystemPrompt: getGeneralPurposeSystemPrompt,
}
General-purpose Agent 是通用的全工具 Agent。在传统模式下,当用户不指定 subagent_type 时默认使用它。但需要注意:当 Fork Subagent 实验开启时(isForkSubagentEnabled() 返回 true),省略 subagent_type 会走隐式 Fork 路径(继承父级上下文),而不是创建一个 fresh 的 general-purpose Agent。此时只有显式指定 subagent_type: "general-purpose" 才会使用它。
4.6 Claude Code Guide Agent:用户自助答疑
CLAUDE_CODE_GUIDE_AGENT 是 第 4.1 节 注册逻辑里那条「非 SDK 入口才注册」的 Agent。它的特殊之处在于:它是唯一一个在 getSystemPrompt() 中使用 toolUseContext 参数的内置 Agent——可以动态注入用户当前配置的 skills、agents、MCP servers 和 settings,使得对 "How do I..." 类问题的回答能结合用户的实际环境。配置上用 haiku 模型 + dontAsk 权限模式(自动拒绝需要确认的操作),确保快速且安全。这条 Agent 只对真正的 CLI 用户开放——SDK 嵌入场景里宿主应用通常有自己的帮助通道。
五、Fork Subagent:共享上下文的高效分支
Fork 是 Agent 系统中一种特殊的子 Agent 模式,定义在 tools/AgentTool/forkSubagent.ts。与 Fresh Agent(从零开始)不同,Fork 子 Agent 继承父 Agent 的完整对话历史和 System Prompt。
5.1 为什么需要 Fork?
Fork 模式解决的核心问题是 prompt cache 复用。Fresh Agent 要重新传输整个 System Prompt 和上下文,而 Fork 子 Agent 复用父级的缓存,只增量传输 fork 指令。源码注释:
A fork beats a fresh subagent for this — it inherits context and shares your cache.
5.2 Fork 消息构建
buildForkedMessages() 的设计目标是让所有 Fork 子 Agent 产生字节完全相同的 API 请求前缀:
// tools/AgentTool/forkSubagent.ts:107-169
export function buildForkedMessages(
directive: string,
assistantMessage: AssistantMessage,
): MessageType[] {
// 保留完整的 assistant message(所有 tool_use、thinking、text)
const fullAssistantMessage = { ...assistantMessage, uuid: randomUUID() }
// 为每个 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' }],
}))
// 只有最后的 directive 文本块不同
const toolResultMessage = createUserMessage({
content: [
...toolResultBlocks, // 所有 fork 子 Agent 相同
{ type: 'text', text: buildChildMessage(directive) }, // 每个不同
],
})
return [fullAssistantMessage, toolResultMessage]
}
结构是:[...history, assistant(all_tool_uses), user(placeholder_results..., directive)]。只有最后的 directive 不同,最大化 cache 命中率。
5.3 防止递归 Fork
Fork 子 Agent 保留了 Agent tool 在工具池中(为了缓存一致),但通过双重机制在运行时阻止嵌套 Fork。
机制一:消息扫描——isInForkChild() 扫描对话历史中的标签:
// tools/AgentTool/forkSubagent.ts:78-89
export 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_TAG}>`)
)
})
}
机制二:querySource 持久化——由于 autocompact 会改写消息内容(可能删除 <fork-boilerplate> 标签),仅靠消息扫描不够可靠。runAgent() 将 querySource 注入到 Fork 子 Agent 的 context.options 中:
// tools/AgentTool/runAgent.ts:688-695
// Fork children need querySource on context.options for the recursive-fork
// guard — it checks options.querySource === 'agent:builtin:fork'.
// This survives autocompact (which rewrites messages, not context.options).
...(useExactTools && { querySource }),
AgentTool.call() 在执行时同时检查两者:先看 options.querySource === 'agent:builtin:fork'(抗 autocompact),再用 isInForkChild() 作为后备(覆盖非 useExactTools 路径)。
六、异步 Agent 的生命周期管理
同步 Agent 在前台运行,父 Agent 等待其完成。异步 Agent 在后台运行,父 Agent 可以继续处理其他任务。异步 Agent 的完整生命周期由 runAsyncAgentLifecycle() 驱动(agentToolUtils.ts:508-686):
// tools/AgentTool/agentToolUtils.ts:508-686
export async function runAsyncAgentLifecycle({
taskId, abortController, makeStream, metadata, description,
toolUseContext, rootSetAppState, agentIdForCleanup, enableSummarization,
}) {
const tracker = createProgressTracker()
const resolveActivity = createActivityDescriptionResolver(tools)
// 可选:启动后台摘要生成
const onCacheSafeParams = enableSummarization
? (params) => { stopSummarization = startAgentSummarization(...) }
: undefined
try {
for await (const message of makeStream(onCacheSafeParams)) {
agentMessages.push(message)
// 实时更新 AppState 中的 task messages(UI 可见)
rootSetAppState(prev => { /* 追加 message 到 task */ })
// 更新进度指标
updateProgressFromMessage(tracker, message, resolveActivity, tools)
updateAsyncAgentProgress(taskId, getProgressUpdate(tracker), rootSetAppState)
}
// 标记任务完成
completeAsyncAgent(agentResult, rootSetAppState)
// 发送完成通知
enqueueAgentNotification({ taskId, description, status: 'completed', ... })
} catch (error) {
if (error instanceof AbortError) {
killAsyncAgent(taskId, rootSetAppState)
enqueueAgentNotification({ status: 'killed', ... })
} else {
failAsyncAgent(taskId, msg, rootSetAppState)
enqueueAgentNotification({ status: 'failed', ... })
}
}
}
关键设计:先更新状态,再发通知。注释明确说明原因:
Mark task completed FIRST so TaskOutput(block=true) unblocks immediately. classifyHandoffIfNeeded (API call) and getWorktreeResult (git exec) are notification embellishments that can hang.
这避免了 API 调用或 git 操作导致的死锁——即使通知丰富化失败,任务状态也已经正确转换。注释里 TaskOutput(block=true) 指的是 TaskOutput 工具以阻塞模式等待异步任务结束(详见第 13 章),classifyHandoffIfNeeded 是一个会发起额外 API 调用的"是否需要交接给下一位 Agent"分类器;这两个调用都可能因外部依赖卡住,所以必须放在状态转换之后。
七、Agent 记忆系统
Agent 可以拥有跨会话的持久化记忆,定义在 tools/AgentTool/agentMemory.ts:
// tools/AgentTool/agentMemory.ts:12-13
export type AgentMemoryScope = 'user' | 'project' | 'local'
三种记忆范围对应不同的存储路径:
- user:
~/.claude/agent-memory/<agentType>/— 跨项目共享 - project:
<cwd>/.claude/agent-memory/<agentType>/— 项目级,可提交到 VCS - local:
<cwd>/.claude/agent-memory-local/<agentType>/— 项目级但不提交
记忆内容通过 loadAgentMemoryPrompt() 注入到 Agent 的 System Prompt 尾部:
// tools/AgentTool/agentMemory.ts:138-177
export function loadAgentMemoryPrompt(agentType, scope) {
const memoryDir = getAgentMemoryDir(agentType, scope)
void ensureMemoryDirExists(memoryDir) // Fire-and-forget
return buildMemoryPrompt({ displayName: 'Persistent Agent Memory', memoryDir })
}
注意 ensureMemoryDirExists 是 fire-and-forget——因为 getSystemPrompt() 是同步调用的(在 React render 中),不能 await。Agent 的第一个 API 请求至少需要一个网络往返,到那时目录创建早已完成。
八、AgentSummary:后台进度摘要
异步 Agent 跑起来之后,UI 上"in progress"的一行字会让用户失去耐心。Claude Code 给出的解法不是改 UI,而是再开一个 Agent ——services/AgentSummary/agentSummary.ts 实现了一个 30 秒一拍的后台摘要器,定时 fork 当前对话,让模型自己用 3-5 个英文单词描述"我此刻正在做什么",再把这句话写回 AgentProgress 供前端显示。
// services/AgentSummary/agentSummary.ts:26
const SUMMARY_INTERVAL_MS = 30_000
整段实现里最值得抄的不是 fork 本身,而是它保护 prompt cache 的方式。每一拍生成摘要,子请求必须和父请求共用同一个 cache key,否则每 30 秒就要为整段历史付一次全价。startAgentSummarization 因此做了三件相互呼应的事:
// services/AgentSummary/agentSummary.ts:55
const { forkContextMessages: _drop, ...baseParams } = cacheSafeParams
第一步,把闭包里残留的 forkContextMessages 立刻丢掉。这条字段在 timer 启动那一刻是真实的,但 30 秒后早已过时——保留下来反而会把陈旧的对话钉死在每一拍 fork 里。每次 runSummary() 改为从 getAgentTranscript(agentId) 重新读取实时 transcript,再用 filterIncompleteToolCalls 清理掉那些没收到结果的 tool_use。
// services/AgentSummary/agentSummary.ts:94-98
const canUseTool = async () => ({
behavior: 'deny' as const,
message: 'No tools needed for summary',
decisionReason: { type: 'other' as const, reason: 'summary only' },
})
第二步是这段代码中唯一被注释着重提醒的"反直觉"细节:禁用工具必须走 canUseTool 回调,绝不能改 tools: []。后者会改变请求的 cache key(工具列表是 key 的一部分),整段历史的缓存瞬间作废;前者只在调用阶段拒绝,请求本身仍然把完整工具集发出去,cache 才能继续命中。
第三步是它没做的事——源码注释花了 8 行篇幅说为什么不能设 maxOutputTokens:
// services/AgentSummary/agentSummary.ts:100-104
// DO NOT set maxOutputTokens here. The fork piggybacks on the main
// thread's prompt cache by sending identical cache-key params (system,
// tools, model, messages prefix, thinking config). Setting maxOutputTokens
// would clamp budget_tokens, creating a thinking config mismatch that
// invalidates the cache.
maxOutputTokens 会被向下传到 thinking config 的 budget_tokens,而 thinking config 本身也参与 cache key——一个看似无害的"省点输出"会让所有缓存全部失效。把这三点(丢闭包、用 callback 拒绝、不动 token 上限)放在一起,才能解释为什么 fork 出来跑了一句话的子 Agent 不会把每次唤醒变成一次全量计费。
最后,runSummary 用 finally 块里的 scheduleNext() 排下一拍,而不是用 setInterval——这样可以保证两次摘要绝不重叠:哪怕这一拍因为网络慢跑了 50 秒,下一拍也要等它结束才计时 30 秒。摘要本身用 runForkedAgent 而非 runAgent,配合 skipTranscript: true,确保 fork 出来的临时对话不会被写回 sidechain 污染主历史。
整个文件不到 180 行,却同时示范了三件事:怎样定时地复用一段昂贵对话、怎样在共享 cache 的前提下临时收紧工具权限、以及怎样在 closure 里有意"忘掉"过期数据。当你以后要给任意 long-running Agent 加一个"它现在在干嘛"的旁路通道时,照着这三条原则做基本不会出错。
九、可迁移的设计模式
模式 1:默认隔离、显式共享的 Context Clone
createSubagentContext() 的设计原则——所有可变状态默认隔离,需要共享的通过 opt-in 参数显式开启。这比"默认共享,需要隔离的手动处理"安全得多,因为遗漏隔离会导致 bug,而遗漏共享最多导致功能不全(容易发现和修复)。
适用场景:任何需要从父级 context 中 fork 子任务的系统——Web Worker、微服务间调用、多线程任务分发。
模式 2:总体收敛 + 硬编码例外通道的权限过滤
三层工具过滤(全局禁止 → 异步限制 → Agent 定义)的整体趋势是收敛,但每层都有硬编码的例外通道(MCP 穿透、plan 模式放行、teammate 额外工具)。这种"主干收敛 + 白名单例外"的模式比"纯收敛"或"自由扩展"都更适合实际系统:主干保证安全性,白名单保证特殊场景的可用性,且例外都是编译时或注册时确定的,不能被运行时的用户输入扩展。
适用场景:多租户权限系统、插件沙箱、API 权限分级。
模式 3:先状态转换,后丰富化通知
异步 Agent 完成时,先调用 completeAsyncAgent() 更新状态,再执行 classifier 和 worktree 检查来丰富通知内容。这确保了状态机转换的原子性——即使丰富化步骤失败(网络超时、git 挂起),系统状态仍然正确。
适用场景:任何包含"完成 + 通知"两步的异步任务系统——订单系统(先更新订单状态,再发邮件)、CI/CD(先标记构建完成,再生成报告)。
下一章预告
第 15 章:内置 Agent 设计模式 — Explore、Plan、Verification 的 Prompt 设计
我们将深入 6 个内置 Agent 的 System Prompt 设计,揭示如何通过 prompt 工程将同一套工具系统塑造出截然不同的 Agent 人格与行为模式。
全部内容请关注 https://github.com/luyao618/Claude-Code-Source-Study (求一颗免费的小星星)