第 12 篇:Agent 系统 — 从单体到多智能体协作
本篇是《深入 Claude Code 源码》系列的第 12 篇。我们将深入剖析 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:Agent 的数据蓝图 1.1 三种 Agent 类型 Claude Code 的 Agent 系统有一个清晰的类型体系,定义在 tools/AgentTool/loadAgentsDir.ts 中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 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):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 export type BaseAgentDefinition = { agentType : string whenToUse : string tools ?: string [] disallowedTools ?: string [] skills ?: string [] mcpServers ?: AgentMcpServerSpec [] hooks ?: HooksSettings color ?: AgentColorName model ?: string effort ?: EffortValue permissionMode ?: PermissionMode maxTurns ?: number background ?: boolean initialPrompt ?: string memory ?: AgentMemoryScope isolation ?: 'worktree' | 'remote' omitClaudeMd ?: boolean criticalSystemReminder_EXPERIMENTAL ?: string requiredMcpServers ?: string [] 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 包装的异步函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 export const getAgentDefinitionsWithOverrides = memoize ( async (cwd : string ): Promise <AgentDefinitionsResult > => { if (isEnvTruthy (process.env .CLAUDE_CODE_SIMPLE )) { return { activeAgents : getBuiltInAgents (), allAgents : getBuiltInAgents () } } 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 ) const pluginAgents = await loadPluginAgents () const builtInAgents = getBuiltInAgents () const allAgentsList = [...builtInAgents, ...pluginAgents, ...customAgents] const activeAgents = getActiveAgentsFromList (allAgentsList) return { activeAgents, allAgents : allAgentsList } } )
getActiveAgentsFromList() 的去重策略值得关注——它按 [built-in, plugin, user, project, flag, managed] 的顺序遍历,后面的覆盖 前面的同名 Agent:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 --- name: my-researcher description: "Deep codebase researcher for complex architectural questions" tools: - FileRead - Glob - Grep - Bashmodel: 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 加载失败,而是记录警告并忽略该字段。
二、runAgent():子 Agent 的完整生命周期 runAgent() 是 Agent 系统的核心引擎,定义在 tools/AgentTool/runAgent.ts:248-860。它是一个 AsyncGenerator,逐步 yield 出子 Agent 产生的消息。
2.1 生命周期时序图 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 sequenceDiagram participant Parent as 父 Agent (AgentTool.call) participant RA as runAgent() participant MCP as MCP Servers participant CTX as createSubagentContext() participant Q as query() 循环 participant API as Anthropic API participant Cleanup as 清理阶段 Parent->>RA: 调用 runAgent(agentDefinition, ...) Note over RA: Phase 1: 初始化 RA->>RA: 解析模型 (getAgentModel) RA->>RA: 创建 agentId RA->>RA: 构建 context messages (fork 或 fresh) RA->>RA: 构建 userContext / systemContext RA->>RA: 省略 CLAUDE.md (omitClaudeMd) RA->>RA: 省略 gitStatus (Explore/Plan) Note over RA: Phase 2: 权限与 Prompt RA->>RA: 解析权限模式 (agentGetAppState) RA->>RA: 解析工具集 (resolveAgentTools) RA->>RA: 构建 System Prompt RA->>RA: 执行 SubagentStart hooks RA->>RA: 注册 frontmatter hooks RA->>RA: 预加载 Skills Note over RA: Phase 3: MCP 初始化 RA->>MCP: initializeAgentMcpServers() MCP-->>RA: 合并后的 clients + tools Note over RA: Phase 4: 创建隔离 Context RA->>CTX: createSubagentContext(parentCtx, overrides) CTX-->>RA: agentToolUseContext Note over RA: Phase 5: 对话循环 RA->>RA: recordSidechainTranscript (初始消息) loop query() 循环 RA->>Q: query(messages, systemPrompt, ...) Q->>API: 发送请求 API-->>Q: 流式响应 Q-->>RA: yield message RA->>RA: recordSidechainTranscript (增量) RA-->>Parent: yield message end Note over RA: Phase 6: 清理 (finally) RA->>MCP: mcpCleanup() RA->>Cleanup: clearSessionHooks() RA->>Cleanup: cleanupAgentTracking() RA->>Cleanup: readFileState.clear() RA->>Cleanup: killShellTasksForAgent() RA->>Cleanup: 释放 todos / perfetto / transcript
2.2 Phase 1-2:初始化与准备 runAgent() 的前半段是一系列精细的准备工作。
模型解析 ——Agent 使用的模型经过多级 fallback:
1 2 3 4 5 6 7 const resolvedAgentModel = getAgentModel ( agentDefinition.model , toolUseContext.options .mainLoopModel , model, permissionMode, )
消息构建 ——子 Agent 的初始消息有两种模式:
Fork 模式 :forkContextMessages 不为空,子 Agent 继承父 Agent 的对话历史(需过滤不完整的 tool_use)
Fresh 模式 :从零开始,只有 promptMessages
1 2 3 4 5 6 7 8 9 10 const contextMessages : Message [] = forkContextMessages ? filterIncompleteToolCalls (forkContextMessages) : [] const initialMessages : Message [] = [...contextMessages, ...promptMessages]const agentReadFileState = forkContextMessages !== undefined ? cloneFileStateCache (toolUseContext.readFileState ) : createFileStateCacheWithSizeLimit (READ_FILE_STATE_CACHE_SIZE )
Token 节省优化 ——对只读 Agent 做了两个有针对性的 context 裁剪:
1 2 3 4 5 6 7 8 9 10 11 const shouldOmitClaudeMd = agentDefinition.omitClaudeMd && !override ?.userContext && getFeatureValue_CACHED_MAY_BE_STALE ('tengu_slim_subagent_claudemd' , true ) const resolvedSystemContext = agentDefinition.agentType === 'Explore' || agentDefinition.agentType === 'Plan' ? systemContextNoGit : baseSystemContext
权限模式覆盖 ——agentGetAppState 是一个精心构造的闭包,它在每次被调用时动态返回包含覆盖权限的 AppState:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 const agentGetAppState = ( ) => { const state = toolUseContext.getAppState () let toolPermissionContext = state.toolPermissionContext if (agentPermissionMode && state.toolPermissionContext .mode !== 'bypassPermissions' ...) { toolPermissionContext = { ...toolPermissionContext, mode : agentPermissionMode } } if (shouldAvoidPrompts) { toolPermissionContext = { ...toolPermissionContext, shouldAvoidPermissionPrompts : true , } } if (allowedTools !== undefined ) { toolPermissionContext = { ...toolPermissionContext, alwaysAllowRules : { cliArg : state.toolPermissionContext .alwaysAllowRules .cliArg , session : [...allowedTools], }, } } return { ...state, toolPermissionContext, effortValue } }
注意 cliArg 规则的保留——这确保了 SDK 通过 --allowedTools 传入的权限在子 Agent 中仍然有效,而父 Agent 的 session 级权限不会泄漏给子 Agent。
2.3 Phase 3:Agent 专属 MCP 服务器 Agent 可以通过 frontmatter 定义自己的 MCP 服务器,这些服务器是对父级的增量扩展 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 async function initializeAgentMcpServers ( agentDefinition : AgentDefinition , parentClients : MCPServerConnection [], ) { for (const spec of agentDefinition.mcpServers ) { if (typeof spec === 'string' ) { config = getMcpConfigByName (spec) } else { 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, } }
这个设计有一个精妙之处:按名称引用的 MCP 服务器由于 connectToServer 是 memoized 的,不会重复连接;而 cleanup 只关闭 Agent 自己内联定义的服务器,不会影响父级共享的连接。
2.4 Phase 4:Context 隔离的核心 createSubagentContext() 定义在 utils/forkedAgent.ts:345-462,是整个 Agent 系统中最关键的函数之一。它为子 Agent 创建一个默认全隔离、显式 opt-in 共享 的 ToolUseContext:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 export function createSubagentContext ( parentContext : ToolUseContext , overrides ?: SubagentContextOverrides , ): ToolUseContext { const abortController = overrides?.abortController ?? (overrides?.shareAbortController ? parentContext.abortController : createChildAbortController (parentContext.abortController )) 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 , contentReplacementState : overrides?.contentReplacementState ?? (parentContext.contentReplacementState ? cloneContentReplacementState (parentContext.contentReplacementState ) : undefined ), abortController, getAppState, setAppState : overrides?.shareSetAppState ? parentContext.setAppState : () => {}, setAppStateForTasks : parentContext.setAppStateForTasks ?? parentContext.setAppState , localDenialTracking : overrides?.shareSetAppState ? parentContext.localDenialTracking : createDenialTrackingState (), setInProgressToolUseIDs : () => {}, setResponseLength : overrides?.shareSetResponseLength ? parentContext.setResponseLength : () => {}, pushApiMetricsEntry : overrides?.shareSetResponseLength ? parentContext.pushApiMetricsEntry : undefined , updateFileHistoryState : () => {}, updateAttributionState : parentContext.updateAttributionState , 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 , 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 影响
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() 循环:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 try { for await (const message of query ({ messages : initialMessages, systemPrompt : agentSystemPrompt, userContext : resolvedUserContext, systemContext : resolvedSystemContext, canUseTool, toolUseContext : agentToolUseContext, querySource, maxTurns : maxTurns ?? agentDefinition.maxTurns , })) { onQueryProgress?.() if (message.type === 'stream_event' && message.event .type === 'message_start' ) { toolUseContext.pushApiMetricsEntry ?.(message.ttftMs ) continue } if (message.type === 'attachment' && message.attachment .type === 'max_turns_reached' ) { break } if (isRecordableMessage (message)) { await recordSidechainTranscript ([message], agentId, lastRecordedUuid) yield message } } }
每条消息都被增量记录到 sidechain transcript(磁盘),这使得 Agent 可以在崩溃后恢复执行(resumeAgent.ts)。
2.6 Phase 6:清理——不留任何痕迹 finally 块的清理工作是防止资源泄漏的最后防线:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 finally { await mcpCleanup () if (agentDefinition.hooks ) { clearSessionHooks (rootSetAppState, agentId) } if (feature ('PROMPT_CACHE_BREAK_DETECTION' )) { cleanupAgentTracking (agentId) } agentToolUseContext.readFileState .clear () initialMessages.length = 0 unregisterPerfettoAgent (agentId) clearAgentTranscriptSubdir (agentId) rootSetAppState (prev => { if (!(agentId in prev.todos )) return prev const { [agentId]: _removed, ...todos } = prev.todos return { ...prev, todos } }) killShellTasksForAgent (agentId, toolUseContext.getAppState , rootSetAppState) 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 都不能使用的工具:
1 2 3 4 5 6 7 8 9 10 export const ALL_AGENT_DISALLOWED_TOOLS = new Set ([ TASK_OUTPUT_TOOL_NAME , EXIT_PLAN_MODE_V2_TOOL_NAME , ENTER_PLAN_MODE_TOOL_NAME , ...(process.env .USER_TYPE === 'ant' ? [] : [AGENT_TOOL_NAME ]), ASK_USER_QUESTION_TOOL_NAME , TASK_STOP_TOOL_NAME , ])
但 filterToolsForAgent() 在执行全局禁止检查之前,有两个优先级更高的放行通道:
1 2 3 4 5 if (tool.name .startsWith ('mcp__' )) return true if (toolMatchesName (tool, EXIT_PLAN_MODE_V2_TOOL_NAME ) && permissionMode === 'plan' ) return true
第二层:异步 Agent 限制 ——后台运行的 Agent 只能用白名单中的工具:
1 2 3 4 5 6 7 8 9 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 字段:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 export function resolveAgentTools (agentDefinition, availableTools, isAsync ) { const filteredAvailableTools = filterToolsForAgent ({ tools, isBuiltIn, isAsync }) const allowedAvailableTools = filteredAvailableTools.filter ( tool => !disallowedToolSet.has (tool.name ) ) 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 列表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 export function getBuiltInAgents ( ): AgentDefinition [] { if (isEnvTruthy (process.env .CLAUDE_AGENT_SDK_DISABLE_BUILTIN_AGENTS ) && getIsNonInteractiveSession ()) { return [] } if (feature ('COORDINATOR_MODE' ) && isEnvTruthy (process.env .CLAUDE_CODE_COORDINATOR_MODE )) { return getCoordinatorAgents () } const agents : AgentDefinition [] = [ GENERAL_PURPOSE_AGENT , STATUSLINE_SETUP_AGENT , ] if (areExplorePlanAgentsEnabled ()) { agents.push (EXPLORE_AGENT , PLAN_AGENT ) } if (isNonSdkEntrypoint) { agents.push (CLAUDE_CODE_GUIDE_AGENT ) } 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 的循环依赖。
Claude Code Guide Agent :这是唯一一个在 getSystemPrompt() 中使用 toolUseContext 参数的内置 Agent——它动态注入用户当前配置的 skills、agents、MCP servers 和 settings,使得对 “How do I…” 类问题的回答能结合用户的实际环境。使用 haiku 模型 + dontAsk 权限模式(自动拒绝需要确认的操作),确保快速且安全。
4.2 Explore Agent:只读搜索专家 1 2 3 4 5 6 7 8 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:只读架构师 1 2 3 4 5 6 7 8 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 采用了对抗性设计 :
1 2 3 4 5 6 7 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 重复注入关键约束:
1 2 3 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:通用工人 1 2 3 4 5 6 7 8 export const GENERAL_PURPOSE_AGENT : BuiltInAgentDefinition = { agentType : 'general-purpose' , tools : ['*' ], source : 'built-in' , getSystemPrompt : getGeneralPurposeSystemPrompt, }
General-purpose Agent 是通用的全工具 Agent。在传统模式下,当用户不指定 subagent_type 时默认使用它。但需要注意:当 Fork Subagent 实验开启时 (isForkSubagentEnabled() 返回 true),省略 subagent_type 会走隐式 Fork 路径(继承父级上下文),而不是创建一个 fresh 的 general-purpose Agent。此时只有显式指定 subagent_type: "general-purpose" 才会使用它。
五、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 请求前缀:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 export function buildForkedMessages ( directive : string , assistantMessage : AssistantMessage , ): MessageType [] { const fullAssistantMessage = { ...assistantMessage, uuid : randomUUID () } const toolResultBlocks = toolUseBlocks.map (block => ({ type : 'tool_result' , tool_use_id : block.id , content : [{ type : 'text' , text : 'Fork started — processing in background' }], })) const toolResultMessage = createUserMessage ({ content : [ ...toolResultBlocks, { 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() 扫描对话历史中的标签:
1 2 3 4 5 6 7 8 9 10 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 中:
1 2 3 4 5 ...(useExactTools && { querySource }),
AgentTool.call() 在执行时同时检查两者:先看 options.querySource === 'agent:builtin:fork'(抗 autocompact),再用 isInForkChild() 作为后备(覆盖非 useExactTools 路径)。
六、异步 Agent 的生命周期管理 同步 Agent 在前台运行,父 Agent 等待其完成。异步 Agent 在后台运行,父 Agent 可以继续处理其他任务。异步 Agent 的完整生命周期由 runAsyncAgentLifecycle() 驱动(agentToolUtils.ts:508-686):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 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) rootSetAppState (prev => { }) 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 操作导致的死锁——即使通知丰富化失败,任务状态也已经正确转换。
七、Agent 记忆系统 Agent 可以拥有跨会话的持久化记忆,定义在 tools/AgentTool/agentMemory.ts:
1 2 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 尾部:
1 2 3 4 5 6 export function loadAgentMemoryPrompt (agentType, scope ) { const memoryDir = getAgentMemoryDir (agentType, scope) void ensureMemoryDirExists (memoryDir) return buildMemoryPrompt ({ displayName : 'Persistent Agent Memory' , memoryDir }) }
注意 ensureMemoryDirExists 是 fire-and-forget——因为 getSystemPrompt() 是同步调用的(在 React render 中),不能 await。Agent 的第一个 API 请求至少需要一个网络往返,到那时目录创建早已完成。
八、可迁移的设计模式 模式 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(先标记构建完成,再生成报告)。
下一篇预告 第 13 篇:内置 Agent 设计模式 — Explore、Plan、Verification 的 prompt 设计
我们将深入分析每个内置 Agent 的 System Prompt 设计技巧:Explore 如何引导高效并行搜索,Plan 如何引导结构化规划输出,Verification 如何用对抗性 prompt 防止模型”偷懒”。这些 prompt 工程技巧可以直接应用到你自己的 Agent 系统中。
全部内容请关注 https://github.com/luyao618/Claude-Code-Source-Study (求一颗免费的小星星)