4.8 子代理工具 — 多 Agent 编排的调度内核与 Fork-and-Delegate 实现

对应原书:第4章 4.2.1节"按职责分类的工具体系"(Agent协作类)、4.3.3节"工具排序与缓存"(Agent列表从工具描述移到attachment message)、4.9节"延迟工具与自修复引导"(Explore/Plan只读代理)、第6章 6.2-6.10节(Fork-and-Delegate / Agent Type Registry / Tool Sandboxing / Coordinator-Worker / Async Agent Lifecycle / Context Cache Sharing / Fork vs Coordinator Trade-off)
辅助源码tools/AgentTool/(AgentTool.tsx 主文件、constants.ts 13行、prompt.ts 287行、loadAgentsDir.ts 756行、forkSubagent.ts 211行、runAgent.ts 974行、agentToolUtils.ts 687行、builtInAgents.ts 73行、built-in/exploreAgent.ts 84行、built-in/planAgent.ts 93行、built-in/generalPurposeAgent.ts 35行、agentColorManager.ts 67行、agentDisplay.ts 105行、resumeAgent.ts 266行、UI.tsx 872行)
重点关注:三种 Agent 定义类型与四层加载源优先级覆盖、Markdown Frontmatter 定义格式与 Zod 验证、6种内置 Agent 与 Feature Flag 矩阵、三层工具沙箱过滤、runAgent 核心执行引擎(上下文继承/权限覆盖/MCP初始化/Hooks/Skills/10步清理链)、Fork-and-Delegate 模式(三重守卫/占位符统一化/递归防护双重防线/10条不可协商规则)、Context Cache Sharing 四维度一致性、runAsyncAgentLifecycle 五阶段生命周期、resumeAgentBackground 恢复机制、shouldInjectAgentListInMessages 缓存优化


1. 导语:子代理工具在工具体系中的定位

原书 4.2.1 节将 Agent 协作类工具的安全级别标记为需确认,并指出其核心特征是"跨进程隔离":

“Agent协作 | 需确认 | Agent、TeamCreate | 跨进程隔离 | 详见第6章”

原书 4.3.3 节进一步指出一个关键的缓存优化决策——Agent 列表从工具描述中移出:

“Agent 列表从工具描述移到 attachment message,避免 MCP/plugin/permission 变化导致工具 schema 缓存失效。原占 fleet cache_creation tokens ~10.2%。”

第6章是整个子代理体系的理论核心,覆盖了从定义注册到执行编排的全链路:

主题 核心源码
6.2 Fork-and-Delegate forkSubagent.ts
6.3 Agent Type Registry loadAgentsDir.ts
6.4 Tool Sandboxing agentToolUtils.ts
6.5 Scoped Memory loadAgentsDir.ts (memory字段)
6.6 Coordinator-Worker builtInAgents.ts + prompt.ts
6.7 Async Agent Lifecycle agentToolUtils.ts
6.8 Context Cache Sharing forkSubagent.ts + runAgent.ts
6.9 Fork vs Coordinator Trade-off forkSubagent.ts vs builtInAgents.ts
6.10 模式协作全景 全目录

本篇聚焦 tools/AgentTool/ 目录的全部实现,分析 Claude Code 如何通过Agent 定义注册表 + 工具沙箱过滤 + Fork-and-Delegate 执行引擎 + 异步生命周期管理四层架构,将"子代理"这一抽象概念落地为可调度、可隔离、可缓存、可恢复的工程系统。


2. 常量定义与一次性代理优化

2.1 工具名常量(constants.ts — 13行)

// constants.ts
export const AGENT_TOOL_NAME = 'Agent'
export const LEGACY_AGENT_TOOL_NAME = 'Task'    // 向后兼容:旧版用 Task 名
export const VERIFICATION_AGENT_TYPE = 'verification'
export const ONE_SHOT_BUILTIN_AGENT_TYPES = new Set(['Explore', 'Plan'])

关键设计

  • 双名称兼容Agent 是当前工具名,Task 是遗留名称。系统同时注册两个名称指向同一工具,确保旧版用户配置和 hook 脚本不中断。
  • ONE_SHOT_BUILTIN_AGENT_TYPES:标记 Explore 和 Plan 为"一次性代理"——它们不需要 SendMessage 工具提示(因为这两个代理是只读的,执行完毕即返回,不会持续对话)。原书指出这一优化每周节省约 135 chars × 34M Explore runs/week ≈ 4.6 Gchars 的 token 开销。

2.2 一次性代理的 prompt 优化

prompt.tsgetPrompt() 中,对于 ONE_SHOT_BUILTIN_AGENT_TYPES 中的代理类型,跳过"如何与子代理持续对话"的说明段落(SendMessage 相关提示),从而减少每次调用的 prompt 长度。


3. 三种 Agent 定义类型与四层加载源优先级覆盖

3.1 三种定义类型(loadAgentsDir.ts)

Claude Code 支持三种 Agent 定义来源,通过联合类型表达:

// 三种 Agent 定义的类型联合
type AgentDefinition = BuiltInAgentDefinition | CustomAgentDefinition | PluginAgentDefinition

① BuiltInAgentDefinition(内置代理)

type BuiltInAgentDefinition = {
  source: 'built-in'
  agentType: string           // 如 'general-purpose', 'Explore'
  getSystemPrompt: () => string  // 动态生成 prompt(可访问运行时状态)
  // ...tools, disallowedTools, model, omitClaudeMd 等
}

特点:source: 'built-in',system prompt 通过 getSystemPrompt() 函数动态生成,可访问运行时状态(如 GrowthBook flag、环境变量)。

② CustomAgentDefinition(自定义代理)

type CustomAgentDefinition = {
  source: 'user' | 'project' | 'flag' | 'managed'  // 四种来源
  getSystemPrompt: () => string  // 闭包捕获 Markdown 正文内容
  filename: string              // 源文件路径
  baseDir: string               // 基目录
  // ...frontmatter 字段
}

特点:从 Markdown 文件解析,getSystemPrompt 是一个闭包,在解析时捕获 Markdown 正文内容。source 区分四种来源层级。

③ PluginAgentDefinition(插件代理)

type PluginAgentDefinition = {
  source: 'plugin'
  plugin: { name: string; version: string }  // 插件元数据
  // ...同 CustomAgentDefinition
}

特点:source: 'plugin',携带插件元数据(名称+版本),用于显示和权限控制。

3.2 四层加载源优先级覆盖算法

getActiveAgentsFromList(allAgents) 使用 Map 的"后写覆盖"语义实现优先级:

// loadAgentsDir.ts — getActiveAgentsFromList
function getActiveAgentsFromList(allAgents: AgentDefinition[]): AgentDefinition[] {
  const agentMap = new Map<string, AgentDefinition>()
  // 按优先级从低到高依次写入,后写覆盖先写
  // 1. builtIn(最低优先级)
  // 2. plugin
  // 3. user(~/.claude/agents/)
  // 4. project(.claude/agents/)
  // 5. flag(Feature Flag 注入)
  // 6. managed(最高优先级,策略管理)
  for (const agent of allAgents) {
    agentMap.set(agent.agentType, agent)  // 后写覆盖
  }
  return [...agentMap.values()]
}

优先级链(从低到高):

builtIn → plugin → user → project → flag → managed

这意味着用户可以通过在 .claude/agents/ 目录放置同名 Markdown 文件来覆盖内置 Agent 定义,项目级覆盖用户级,策略管理级覆盖一切。

3.3 BaseAgentDefinition 公共字段

所有三种类型共享 BaseAgentDefinition 基础结构:

type BaseAgentDefinition = {
  agentType: string                    // 代理类型标识
  whenToUse: string                    // "何时使用"描述(用于 Agent 工具 prompt)
  tools?: string[]                     // 允许的工具列表(支持 ['*'] 通配符)
  disallowedTools?: string[]           // 禁止的工具列表
  skills?: string[]                    // 预加载的 skill 名称
  mcpServers?: AgentMcpServerSpec[]    // Agent 专属 MCP 服务器
  hooks?: AgentHooks                   // SubagentStart/Stop hooks
  color?: string                       // UI 显示颜色
  model?: string                       // 模型选择('inherit' 或具体模型ID)
  effort?: string                      // 推理努力级别
  permissionMode?: PermissionMode      // 权限模式覆盖
  maxTurns?: number                    // 最大交互轮次
  filename?: string                    // 源文件路径
  baseDir?: string                     // 基目录
  criticalSystemReminder_EXPERIMENTAL?: string  // 实验性系统提醒
  requiredMcpServers?: string[]        // 必需的 MCP 服务器(缺失则不可用)
  background?: boolean                 // 是否默认后台运行
  initialPrompt?: string               // 初始 prompt 注入
  memory?: boolean                     // 是否启用 scoped memory
  isolation?: 'worktree' | 'remote'    // 隔离模式
  omitClaudeMd?: boolean               // 是否省略 CLAUDE.md(节省 token)
}

3.4 AgentMcpServerSpec — Agent 专属 MCP

type AgentMcpServerSpec = string | { [name: string]: McpServerConfig }
  • string 形式:引用父级已连接的 MCP 服务器名称(共享 client,不清理)
  • 对象形式:内联定义新的 MCP 服务器配置(创建新 client,用后清理)

4. Markdown Frontmatter 定义格式与 Zod 验证

4.1 Frontmatter 字段全集

用户通过 Markdown 文件定义自定义 Agent,格式为 YAML frontmatter + Markdown 正文:

---
name: code-reviewer
description: "Reviews code for bugs, security issues, and style"
tools:
  - Read
  - Grep
  - Glob
disallowedTools:
  - FileEdit
model: inherit
effort: high
permissionMode: plan
mcpServers:
  - github            # 引用已有服务器
  - my-server:        # 内联定义
      command: npx
      args: ["-y", "@my/mcp-server"]
hooks:
  SubagentStart:
    - command: "echo 'reviewer started'"
hooks:
  Stop:
    - command: "echo 'reviewer stopped'"
maxTurns: 20
skills:
  - code-review-standards
initialPrompt: "Focus on security first"
memory: true
background: true
isolation: worktree
---

You are a code reviewer. Review code for bugs, security issues, and style violations.
...

4.2 AgentJsonSchema — Zod 验证

// loadAgentsDir.ts — AgentJsonSchema (Zod)
const AgentJsonSchema = z.object({
  description: z.string(),           // 必填:whenToUse
  tools: z.array(z.string()).optional(),
  disallowedTools: z.array(z.string()).optional(),
  prompt: z.string().optional(),     // JSON 格式的 system prompt
  model: z.string().optional(),
  effort: z.string().optional(),
  permissionMode: z.enum(['default', 'plan', 'acceptEdits', 'bypassPermissions']).optional(),
  mcpServers: z.array(AgentMcpServerSpecSchema).optional(),
  hooks: z.record(z.array(HookSchema)).optional(),
  maxTurns: z.number().optional(),
  skills: z.array(z.string()).optional(),
  initialPrompt: z.string().optional(),
  memory: z.boolean().optional(),
  background: z.boolean().optional(),
  isolation: z.enum(['worktree', 'remote']).optional(),
})

4.3 parseAgentFromMarkdown — 解析流程

function parseAgentFromMarkdown(
  filePath: string,
  baseDir: string,
  frontmatter: Record<string, unknown>,
  content: string,
  source: 'user' | 'project' | 'flag' | 'managed'
): CustomAgentDefinition

关键逻辑:

  • 从 frontmatter 解析所有字段
  • content(Markdown 正文)被捕获到 getSystemPrompt 闭包中
  • memory: true 时自动注入 WriteEditRead 工具到 tools 列表(用于 scoped memory 读写)
  • requiredMcpServersmcpServers 中提取引用名,用于 hasRequiredMcpServers 检查

4.4 getAgentDefinitionsWithOverrides — memoize 缓存

// 使用 memoize 避免重复解析
const getAgentDefinitionsWithOverrides = memoize(async () => {
  const agents: AgentDefinition[] = []
  // 1. 加载内置 Agent
  agents.push(...getBuiltInAgents())
  // 2. 加载插件 Agent
  agents.push(...await loadPluginAgents())
  // 3. 加载用户级 Markdown(~/.claude/agents/*.md)
  agents.push(...await loadAgentsFromDir(userAgentsDir, 'user'))
  // 4. 加载项目级 Markdown(.claude/agents/*.md)
  agents.push(...await loadAgentsFromDir(projectAgentsDir, 'project'))
  // 5. 初始化 memory snapshots
  await initializeAgentMemorySnapshots(agents)
  // 6. 应用优先级覆盖
  return getActiveAgentsFromList(agents)
})

5. 内置 Agent 矩阵与 Feature Flag 控制

5.1 内置 Agent 清单(builtInAgents.ts — 73行)

// builtInAgents.ts
export function areExplorePlanAgentsEnabled(): boolean {
  return feature('tengu_amber_stoat')  // GrowthBook,默认 true
}

export function getBuiltInAgents(): BuiltInAgentDefinition[] {
  // SDK 场景禁用内置 Agent
  if (process.env.CLAUDE_AGENT_SDK_DISABLE_BUILTIN_AGENTS) return []

  // Coordinator 模式返回专用列表
  if (isCoordinatorMode()) return getCoordinatorAgents()

  const agents: BuiltInAgentDefinition[] = [
    GENERAL_PURPOSE_AGENT,
    STATUSLINE_SETUP_AGENT,
  ]

  // Explore + Plan(Feature Flag 控制)
  if (areExplorePlanAgentsEnabled()) {
    agents.push(EXPLORE_AGENT, PLAN_AGENT)
  }

  // Claude Code Guide(非 SDK 入口才注册)
  if (!isSdkEntrypoint()) {
    agents.push(CLAUDE_CODE_GUIDE_AGENT)
  }

  // Verification Agent(Feature Flag 控制)
  if (feature('tengu_hive_evidence')) {
    agents.push(VERIFICATION_AGENT)
  }

  return agents
}

5.2 六种内置 Agent 特性矩阵

Agent agentType tools model omitClaudeMd Feature Flag 特点
general-purpose general-purpose ['*'] inherit false 全能执行者
Explore Explore 只读子集 haiku/inherit true tengu_amber_stoat 只读搜索专家
Plan Plan 只读子集 inherit true tengu_amber_stoat 只读规划专家
statusline-setup statusline-setup 配置工具 inherit false 状态行配置
verification verification 测试工具 inherit false tengu_hive_evidence 验证代理
claude-code-guide claude-code-guide 文档工具 inherit false 文档指南(非SDK)

5.3 Explore Agent — 只读搜索专家(built-in/exploreAgent.ts — 84行)

export const EXPLORE_AGENT: BuiltInAgentDefinition = {
  source: 'built-in',
  agentType: 'Explore',
  whenToUse: 'Fast agent specialized for exploring codebases...',
  disallowedTools: [
    AGENT_TOOL_NAME,     // 禁止嵌套 Agent
    'ExitPlanMode',
    'FileEdit',
    'FileWrite',
    'NotebookEdit',
  ],
  model: process.env.USER_TYPE === 'ant' ? 'inherit' : 'haiku',
  omitClaudeMd: true,
  getSystemPrompt: () => `
You are a fast, read-only codebase explorer. Your job is to quickly find files,
search code, and answer questions about the codebase.

READ-ONLY MODE: You cannot modify any files. Use only search and read tools.
Call tools in parallel when possible for speed.
Return findings concisely — do not explain your process.
`,
}

关键设计

  • omitClaudeMd: true — 省略 CLAUDE.md 注入,节省 5-15 Gtok/week(tengu_slim_subagent_claudemd 默认 true)
  • model: 'haiku'(非 ant 用户)— 使用更小更快的模型,降低成本
  • 禁止所有写工具 + Agent 工具本身(防止递归嵌套)
  • system prompt 强调并行工具调用和简洁返回

5.4 Plan Agent — 只读规划专家(built-in/planAgent.ts — 93行)

export const PLAN_AGENT: BuiltInAgentDefinition = {
  source: 'built-in',
  agentType: 'Plan',
  whenToUse: 'Fast agent specialized for exploring codebases...',
  disallowedTools: [  // 同 Explore
    AGENT_TOOL_NAME, 'ExitPlanMode', 'FileEdit', 'FileWrite', 'NotebookEdit',
  ],
  model: 'inherit',       // 规划需要更强推理能力
  omitClaudeMd: true,
  getSystemPrompt: () => `
You are a software architect and planning expert. Your job is to explore the codebase
and create a detailed implementation plan.

READ-ONLY MODE: You cannot modify any files.

Follow this process:
1. Understand the requirements
2. Explore relevant code
3. Design a solution approach
4. Create a detailed plan

End your response with a "Critical Files for Implementation" section listing
the key files that will need to be modified.
`,
}

与 Explore 的区别

  • model: 'inherit' — 规划需要与主 Agent 同级的推理能力
  • 四步流程 prompt — 理解→探索→设计→计划
  • 输出 “Critical Files for Implementation” 段落 — 供主 Agent 直接使用

5.5 general-purpose Agent — 全能执行者(built-in/generalPurposeAgent.ts — 35行)

export const GENERAL_PURPOSE_AGENT: BuiltInAgentDefinition = {
  source: 'built-in',
  agentType: 'general-purpose',
  whenToUse: 'General-purpose agent for researching complex questions...',
  tools: ['*'],            // 全部工具
  getSystemPrompt: () => `
You are a general-purpose agent. You can use all available tools to complete tasks.
Execute tasks fully and report back with a concise summary of what you did.
`,
}

特点:tools: ['*'] 拥有全部工具权限(受沙箱过滤约束),用于需要完整执行能力的任务。

5.6 Feature Flag 矩阵

                    ┌─────────────────────────────────────┐
                    │        getBuiltInAgents()           │
                    └─────────────┬───────────────────────┘
                                  │
              ┌───────────────────┼───────────────────┐
              ▼                   ▼                   ▼
     SDK 禁用检查          Coordinator 检查       Feature Flags
     (返回 [])             (返回专用列表)         │
                                        ┌─────────┼─────────┐
                                        ▼         ▼         ▼
                              tengu_amber_stoat  tengu_hive_evidence
                              (Explore+Plan)     (Verification)
                              默认: true         默认: false

6. 三层工具沙箱过滤

6.1 filterToolsForAgent — 三层过滤管线(agentToolUtils.ts)

// agentToolUtils.ts
function filterToolsForAgent({
  tools,
  isBuiltIn,
  isAsync,
  permissionMode,
}: {
  tools: Tool[]
  isBuiltIn: boolean
  isAsync: boolean
  permissionMode?: PermissionMode
}): Tool[] {
  return tools.filter(tool => {
    // 层1:MCP 工具始终放行(信任边界委托)
    if (tool.name.startsWith('mcp__')) return true

    // 层2:全局禁止列表(所有 Agent 都不能用的工具)
    if (ALL_AGENT_DISALLOWED_TOOLS.has(tool.name)) return false

    // 层3a:自定义 Agent 额外禁止列表
    if (!isBuiltIn && CUSTOM_AGENT_DISALLOWED_TOOLS.has(tool.name)) {
      // 特殊例外:in-process teammate 允许 AgentTool
      if (isInProcessTeammate && IN_PROCESS_TEAMMATE_ALLOWED_TOOLS.has(tool.name)) {
        return true
      }
      return false
    }

    // 层3b:异步 Agent 白名单
    if (isAsync && !ASYNC_AGENT_ALLOWED_TOOLS.has(tool.name)) {
      return false
    }

    return true
  })
}

三层过滤逻辑

条件 作用 设计意图
层1 mcp__ 前缀 MCP 工具始终放行 信任边界委托——MCP 工具的权限由 Agent frontmatter 的 mcpServers 控制
层2 ALL_AGENT_DISALLOWED_TOOLS 全局禁止 所有子代理都不能使用的危险工具
层3a CUSTOM_AGENT_DISALLOWED_TOOLS 自定义 Agent 额外禁止 自定义 Agent 不可信,额外限制;包含 AgentTool 本身防止递归嵌套
层3b ASYNC_AGENT_ALLOWED_TOOLS 异步白名单 异步 Agent 只允许安全工具子集

6.2 权限矩阵 — “编排者不执行,执行者不编排”

原书 6.4 节总结了工具沙箱的核心原则:

┌─────────────────┬──────────┬──────────┬──────────┐
│  Agent 类型      │ 编排能力  │ 执行能力  │ 写权限    │
├─────────────────┼──────────┼──────────┼──────────┤
│ general-purpose  │    ✗     │    ✓     │    ✓     │  全能执行
│ Explore          │    ✗     │    ✓     │    ✗     │  只读搜索
│ Plan             │    ✗     │    ✓     │    ✗     │  只读规划
│ Coordinator      │    ✓     │    ✗     │    ✗     │  纯编排
│ Fork             │    ✗     │    ✓     │    ✓     │  分叉执行
│ Custom (用户定义) │    ✗     │    ✓     │  受限    │  按定义
└─────────────────┴──────────┴──────────┴──────────┘
  • 编排者(Coordinator):只有编排工具(Agent/TeamCreate/SendMessage 等),没有执行工具
  • 执行者(general-purpose/Fork):有执行工具,但没有 AgentTool(不能递归编排)

6.3 resolveAgentTools — 工具解析与通配符展开

// agentToolUtils.ts
function resolveAgentTools(
  agentDefinition: AgentDefinition,
  availableTools: Tool[],
  isAsync: boolean,
  isMainThread: boolean = false
): Tool[] {
  const { tools: toolSpecs, disallowedTools: disallowedSpecs } = agentDefinition

  // 步骤1:解析允许列表
  let resolvedTools: Tool[]
  if (toolSpecs?.includes('*')) {
    // 通配符:取全部可用工具
    resolvedTools = [...availableTools]
  } else if (toolSpecs && toolSpecs.length > 0) {
    // 精确匹配
    resolvedTools = toolSpecs
      .map(spec => resolveToolSpec(spec, availableTools, agentDefinition))
      .filter(Boolean)
  } else {
    // 未指定 tools:使用默认工具集
    resolvedTools = [...availableTools]
  }

  // 步骤2:应用否定列表
  if (disallowedSpecs) {
    resolvedTools = resolvedTools.filter(
      tool => !disallowedSpecs.includes(tool.name)
    )
  }

  // 步骤3:应用沙箱过滤
  resolvedTools = filterToolsForAgent({
    tools: resolvedTools,
    isBuiltIn: agentDefinition.source === 'built-in',
    isAsync,
    permissionMode: agentDefinition.permissionMode,
  })

  return resolvedTools
}

Agent 工具规格的 allowedAgentTypes 元数据

工具规格可以携带元数据,例如 Agent(worker, researcher) 表示该 Agent 工具只允许启动 workerresearcher 类型的子代理。这在 Coordinator 模式中用于限制编排者能调度的 Agent 类型。


7. runAgent 核心执行引擎(runAgent.ts — 974行)

7.1 函数签名与核心参数

// runAgent.ts
async function* runAgent({
  agentDefinition,
  promptMessages,          // 子代理的 prompt 消息
  toolUseContext,          // 工具使用上下文(权限、AbortController 等)
  canUseTool,              // 工具权限检查回调
  isAsync,                 // 是否异步执行
  forkContextMessages,     // Fork 模式的父级上下文消息
  querySource,             // 查询来源标识(如 'agent:builtin:fork')
  override,                // 覆盖参数(systemPrompt、model 等)
  model,                   // 模型覆盖
  maxTurns,                // 最大轮次覆盖
  preserveToolUseResults,  // 是否保留工具使用结果
  availableTools,          // 可用工具池
  allowedTools,            // 会话级允许工具列表
  onCacheSafeParams,       // 缓存安全参数回调
  contentReplacementState, // 内容替换状态
  useExactTools,           // 是否直接使用父级工具池(Fork 模式)
  worktreePath,            // worktree 隔离路径
  description,             // Agent 描述
  transcriptSubdir,        // transcript 子目录
  onQueryProgress,         // 查询进度回调
}: RunAgentParams): AsyncGenerator<Message>

7.2 执行流程全景

runAgent() 入口
    │
    ├── 1. Fork 上下文处理
    │   ├── filterIncompleteToolCalls(forkContextMessages) — 过滤不完整 tool calls
    │   ├── cloneFileStateCache (fork) / createFileStateCacheWithSizeLimit (非fork)
    │   └── shouldOmitClaudeMd 检查(Explore/Plan 省略 CLAUDE.md + gitStatus)
    │
    ├── 2. 权限模式覆盖
    │   ├── agentGetAppState() — 构建子代理 AppState
    │   ├── permissionMode 覆盖(不覆盖 bypassPermissions/acceptEdits/auto)
    │   ├── shouldAvoidPermissionPrompts(async agent)
    │   ├── awaitAutomatedChecksBeforeDialog(async+bubble)
    │   └── allowedTools 替换 session 级 alwaysAllowRules(保留 cliArg)
    │
    ├── 3. 工具解析
    │   ├── useExactTools=true → 直接使用父级工具池(Fork 字节一致)
    │   ├── useExactTools=false → resolveAgentTools(agentDefinition, availableTools, isAsync)
    │   └── initializeAgentMcpServers → 合并 Agent 专属 MCP 工具
    │
    ├── 4. Agent 配置
    │   ├── thinkingConfig — fork 继承父级,非 fork disabled
    │   ├── createSubagentContext — sync 共享/async 隔离
    │   └── getAgentSystemPrompt() — agentDefinition.getSystemPrompt() + enhanceSystemPromptWithEnvDetails
    │
    ├── 5. Hooks 与 Skills
    │   ├── executeSubagentStartHooks — SubagentStart hook 执行 + additionalContexts 注入
    │   ├── registerFrontmatterHooks — Stop→SubagentStop 转换
    │   └── Skills 预加载(三级名称解析:exact → plugin前缀 → suffix)
    │
    ├── 6. 持久化
    │   ├── recordSidechainTranscript — 创建 transcript 文件
    │   └── writeAgentMetadata — 写入 metadata
    │
    ├── 7. query() 循环 — 产出 Message
    │   └── recordSidechainTranscript — 增量记录每条消息
    │
    └── 8. finally 块 — 10 步清理链
        ├── mcpCleanup — 清理 Agent 专属 MCP 服务器
        ├── clearSessionHooks — 清除 session hooks
        ├── cleanupAgentTracking — 清理 Agent 跟踪
        ├── readFileState.clear() — 清除文件状态缓存
        ├── initialMessages.length=0 — 释放初始消息引用
        ├── unregisterPerfettoAgent — 注销 Perfetto 追踪
        ├── clearAgentTranscriptSubdir — 清理 transcript 目录
        ├── delete todos[agentId] — 删除 Agent 的 todo 列表
        ├── killShellTasksForAgent — 杀死 Agent 的 shell 任务
        └── killMonitorMcpTasksForAgent — 杀死 Agent 的 MCP 监控任务

7.3 Fork 上下文继承

// Fork 模式:继承父级上下文
if (forkContextMessages) {
  // 过滤不完整的 tool calls(没有 tool_result 的 tool_use)
  const filteredMessages = filterIncompleteToolCalls(forkContextMessages)

  // 克隆文件状态缓存(而非创建新的)
  // 这样子代理能看到父代理已读过的文件状态
  fileStateCache = cloneFileStateCache(parentFileStateCache)
} else {
  // 非 Fork 模式:创建新的文件状态缓存
  fileStateCache = createFileStateCacheWithSizeLimit(DEFAULT_CACHE_SIZE)
}

7.4 CLAUDE.md 与 gitStatus 省略优化

// Explore/Plan 代理省略 CLAUDE.md(节省 5-15 Gtok/week)
const shouldOmitClaudeMd =
  agentDefinition.omitClaudeMd &&
  feature('tengu_slim_subagent_claudemd')  // 默认 true

// Explore/Plan 代理省略 gitStatus(节省 1-3 Gtok/week)
const shouldOmitGitStatus =
  agentDefinition.omitClaudeMd  // 同样标记

if (shouldOmitClaudeMd) {
  // 不注入 CLAUDE.md 内容到子代理 system prompt
  agentOptions.injectClaudeMd = false
}
if (shouldOmitGitStatus) {
  // 不注入 git status 到子代理 system prompt
  agentOptions.injectGitStatus = false
}

成本影响(原书 6.8 节数据):

  • CLAUDE.md 省略:每周节省 5-15 Gtok
  • gitStatus 省略:每周节省 1-3 Gtok
  • 合计:每周节省 6-18 Gtok,按 $3/Mtok 输入计 ≈ $18,000-$54,000/week

7.5 权限模式覆盖

// agentGetAppState() — 构建子代理 AppState
function agentGetAppState(parentState, agentDefinition, isAsync) {
  const permissionMode = agentDefinition.permissionMode ?? parentState.permissionMode

  return {
    ...parentState,
    // 权限模式覆盖(但不覆盖以下三种)
    permissionMode,
    // 以下三种权限模式不被 Agent 定义覆盖:
    // - bypassPermissions(安全:子代理不能自行提升到绕过权限)
    // - acceptEdits(安全:子代理不能自行接受编辑)
    // - auto(安全:子代理不能自行启用自动模式)

    // 异步 Agent 避免权限提示
    shouldAvoidPermissionPrompts: isAsync || parentState.shouldAvoidPermissionPrompts,

    // async + bubble 模式:等待自动化检查
    awaitAutomatedChecksBeforeDialog: isAsync && permissionMode === 'bubble',

    // allowedTools 替换 session 级 alwaysAllowRules
    // 保留 cliArg 规则,防止父 Agent 权限泄露
    alwaysAllowRules: [
      ...parentState.cliArgAllowRules,
      ...(allowedTools?.map(tool => ({ tool, source: 'agent' })) ?? []),
    ],
  }
}

安全设计要点

  • 权限不可提升:子代理不能通过 permissionMode 覆盖来获取 bypassPermissions/acceptEdits/auto 权限
  • 权限隔离allowedTools 替换 session 级 alwaysAllowRules,防止父 Agent 的权限泄露给子代理
  • 异步避免提示:异步 Agent 设置 shouldAvoidPermissionPrompts,因为异步执行时用户可能不在线

7.6 initializeAgentMcpServers — Agent 专属 MCP

// runAgent.ts
async function initializeAgentMcpServers(
  agentDefinition: AgentDefinition,
  parentClients: McpClient[]
): Promise<{ clients: McpClient[]; cleanup: () => Promise<void> }> {
  const specs = agentDefinition.mcpServers
  if (!specs || specs.length === 0) {
    return { clients: parentClients, cleanup: async () => {} }
  }

  // plugin-only 策略下,仅 admin-trusted Agent 允许定义 MCP
  if (isPluginOnlyPolicy() && !isAdminTrusted(agentDefinition)) {
    throw new Error('Only admin-trusted agents can define MCP servers')
  }

  const newlyCreated: McpClient[] = []
  const allClients = [...parentClients]

  for (const spec of specs) {
    if (typeof spec === 'string') {
      // 引用名:在父级 clients 中查找
      const existing = parentClients.find(c => c.name === spec)
      if (!existing) {
        throw new Error(`Agent requires MCP server "${spec}" which is not connected`)
      }
      // 共享 client,不需要清理
    } else {
      // 内联定义:创建新的 MCP client
      for (const [name, config] of Object.entries(spec)) {
        const client = await createMcpClient(name, config)
        newlyCreated.push(client)
        allClients.push(client)
      }
    }
  }

  // 检查 requiredMcpServers
  if (!hasRequiredMcpServers(agentDefinition, allClients.map(c => c.name))) {
    throw new Error('Agent requires MCP servers that are not available')
  }

  // cleanup 只清理 newlyCreated 的内联定义 client
  const cleanup = async () => {
    await Promise.all(newlyCreated.map(c => c.disconnect()))
  }

  return { clients: allClients, cleanup }
}

两类 MCP 服务器的区别

类型 形式 生命周期 清理
引用名 string(如 'github' 共享父级 client 不清理(父级管理)
内联定义 { name: config } Agent 独立 client Agent 结束时清理

7.7 SubagentStart Hooks 与 Skills 预加载

// 1. 执行 SubagentStart hooks
const hookResults = await executeSubagentStartHooks(agentDefinition, context)
if (hookResults.additionalContext) {
  // 注入 hook 产生的额外上下文
  initialMessages.push({
    role: 'user',
    content: hookResults.additionalContext,
  })
}

// 2. 注册 frontmatter hooks(Stop → SubagentStop 转换)
registerFrontmatterHooks(agentDefinition.hooks, agentId)
// Agent frontmatter 中定义的 Stop hook 会被转换为 SubagentStop hook
// 这样 hook 不会在主 Agent 停止时触发,只在子代理停止时触发

// 3. Skills 预加载(三级名称解析)
if (agentDefinition.skills) {
  for (const skillName of agentDefinition.skills) {
    const resolvedSkill = resolveSkillName(skillName, allSkills, agentDefinition)
    if (resolvedSkill) {
      await loadSkill(resolvedSkill, agentId)
    } else {
      logWarning(`Skill "${skillName}" not found for agent "${agentDefinition.agentType}"`)
    }
  }
}

三级 Skill 名称解析

function resolveSkillName(
  skillName: string,
  allSkills: Skill[],
  agentDefinition: AgentDefinition
): Skill | undefined {
  // 级别1:精确匹配
  const exact = allSkills.find(s => s.name === skillName)
  if (exact) return exact

  // 级别2:plugin 前缀匹配(如 'my-plugin:code-review' → skill 'code-review' in plugin 'my-plugin')
  const prefixed = allSkills.find(s =>
    s.plugin?.name && `${s.plugin.name}:${s.name}` === skillName
  )
  if (prefixed) return prefixed

  // 级别3:suffix 匹配(如 'review' → 'code-review-standards')
  const suffix = allSkills.find(s => s.name.endsWith(skillName))
  if (suffix) return suffix

  return undefined
}

7.8 finally 块 — 10 步清理链

// runAgent.ts — finally 块
finally {
  // 1. MCP 清理(仅清理内联定义的 client)
  await mcpCleanup()

  // 2. 清除 session hooks(frontmatter 注册的 SubagentStop 等)
  clearSessionHooks(agentId)

  // 3. 清理 Agent 跟踪(从全局 Agent 列表中移除)
  cleanupAgentTracking(agentId)

  // 4. 清除文件状态缓存(释放内存)
  readFileState.clear()

  // 5. 释放初始消息引用(帮助 GC)
  initialMessages.length = 0

  // 6. 注销 Perfetto 追踪(如果启用了性能追踪)
  unregisterPerfettoAgent(agentId)

  // 7. 清理 transcript 目录(如果不是持久化的)
  clearAgentTranscriptSubdir(agentId)

  // 8. 删除 Agent 的 todo 列表
  delete todos[agentId]

  // 9. 杀死 Agent 创建的 shell 任务
  killShellTasksForAgent(agentId)

  // 10. 杀死 Agent 创建的 MCP 监控任务
  killMonitorMcpTasksForAgent(agentId)
}

设计意图:每个清理步骤都是独立的——即使某一步失败,后续步骤仍会执行。这确保了子代理退出后不会留下任何资源泄漏。


8. Fork-and-Delegate 模式(forkSubagent.ts — 211行)

8.1 原书 6.2 节:Unix fork 隐喻

原书将 Fork-and-Delegate 模式类比为 Unix 的 fork() 系统调用:

“Fork-and-Delegate 模式借鉴了 Unix fork 的设计哲学:继承(子进程获得父进程的完整上下文)、独立(子进程有自己的执行空间)、隔离(子进程的修改不影响父进程)。”

在 Claude Code 中,这一模式通过 forkSubagent.ts 实现:

  • 继承:子代理获得父代理的完整消息历史和已渲染的 system prompt
  • 独立:子代理有自己的执行循环和工具调用
  • 隔离:子代理的权限被限制在 bubble 模式内

8.2 isForkSubagentEnabled — 三重守卫

// forkSubagent.ts
export function isForkSubagentEnabled(): boolean {
  // 守卫1:Feature Flag
  if (!feature('FORK_SUBAGENT')) return false

  // 守卫2:与 Coordinator 模式互斥
  if (isCoordinatorMode()) return false

  // 守卫3:非交互式会话禁用(Fork 需要用户交互确认权限)
  if (getIsNonInteractiveSession()) return false

  return true
}

三重互斥的原因

  • Feature Flag:渐进式发布控制
  • Coordinator 互斥:Coordinator 和 Fork 是两种不同的编排模式,不能同时使用
  • 非交互式禁用:Fork 模式使用 bubble 权限模式,需要将权限提示冒泡到父终端,非交互式会话无法处理

8.3 FORK_AGENT — 合成代理定义

// forkSubagent.ts
export const FORK_SUBAGENT_TYPE = 'fork'

export const FORK_AGENT: BuiltInAgentDefinition = {
  source: 'built-in',
  agentType: FORK_SUBAGENT_TYPE,
  whenToUse: 'Fork the current conversation to handle a subtask',
  tools: ['*'],                    // 全部工具(受沙箱过滤约束)
  maxTurns: 200,                   // 较大的轮次限制
  model: 'inherit',                // 继承父代理模型(缓存一致性)
  permissionMode: 'bubble',        // 权限冒泡到父终端
  // getSystemPrompt 返回空字符串——Fork 模式不使用 Agent 自带的 prompt
  // 而是传递父代理已渲染的 system prompt 字节(缓存一致性)
  getSystemPrompt: () => '',
}

getSystemPrompt: () => '' 的深意

Fork 模式的核心创新是字节级 Prompt Cache 共享。如果子代理使用自己的 system prompt,那么父代理和子代理的 API 请求前缀就会不同,无法共享缓存。通过传递父代理已渲染的 system prompt 字节,子代理的 API 请求前缀与父代理完全一致,实现缓存命中。

8.4 buildForkedMessages — 占位符统一化

// forkSubagent.ts
export const FORK_PLACEHOLDER_RESULT = 'Fork started — processing in background'

export function buildForkedMessages(
  directive: string,
  assistantMessage: Message
): Message[] {
  const messages: Message[] = []

  // 1. 保留父代理完整的 assistant 消息
  messages.push({
    role: 'assistant',
    content: assistantMessage.content,  // 完整保留(包括 thinking、tool_use 等)
  })

  // 2. 为所有 tool_use 创建统一的占位 tool_result
  const toolUseBlocks = extractToolUseBlocks(assistantMessage.content)
  for (const toolUse of toolUseBlocks) {
    messages.push({
      role: 'user',
      content: [{
        type: 'tool_result',
        tool_use_id: toolUse.id,
        content: FORK_PLACEHOLDER_RESULT,  // 统一占位符
      }],
    })
  }

  // 3. 追加子代理特有的指令
  messages.push({
    role: 'user',
    content: buildChildMessage(directive),
  })

  return messages
}

为什么需要统一占位符?

原始对话中,每个 tool_use 的 tool_result 内容各不相同(文件内容、搜索结果、命令输出等)。如果子代理直接继承这些原始结果,那么不同 Fork 的前缀就会不同(因为 tool_result 内容不同),无法共享缓存。

通过将所有 tool_result 替换为统一的 FORK_PLACEHOLDER_RESULT,所有 Fork 的消息前缀完全一致:

父代理 assistant 消息(含 thinking + tool_use)
  ↓
统一占位 tool_result: "Fork started — processing in background"
  ↓
子代理指令(buildChildMessage)

这使得 Prompt Cache 可以共享父代理的前缀部分,大幅降低 token 成本。

8.5 递归防护双重防线

// forkSubagent.ts

// 防线1:消息历史标记扫描
export function isInForkChild(messages: Message[]): boolean {
  // 扫描消息历史中是否存在 <fork_boilerplate> 标签
  return messages.some(msg =>
    typeof msg.content === 'string'
      ? msg.content.includes('<fork_boilerplate>')
      : Array.isArray(msg.content) &&
        msg.content.some(block =>
          block.type === 'text' && block.text.includes('<fork_boilerplate>')
        )
  )
}

// 防线2:querySource 不可变属性检查
// 在 runAgent 中检查 querySource === 'agent:builtin:fork'
// querySource 是调用时传入的,子代理无法修改

双重防线的原因

  • 消息标记可被 LLM 伪造(如果 LLM 在输出中包含 <fork_boilerplate> 标签),因此需要第二道防线
  • querySource 是调用时传入的不可变属性,LLM 无法修改,提供可靠的后备检查

8.6 buildChildMessage — 10条不可协商规则

// forkSubagent.ts
export function buildChildMessage(directive: string): Message {
  return {
    role: 'user',
    content: `
<fork_boilerplate>
You are a forked sub-agent. Follow these rules:

1. Do NOT fork again. You cannot create additional forks.
2. Do NOT ask questions. Make reasonable assumptions and proceed.
3. Do NOT comment on the task. Just do it.
4. Use tools directly. Do not explain what you're about to do.
5. Commit any changes you make.
6. Do NOT output intermediate text. Only use tools.
7. Stay strictly within the scope of the directive below.
8. Keep your final response under 500 words.
9. Start your response with "Scope:" followed by a one-line summary.
10. Provide a structured report of what you did.

Directive:
${directive}
</fork_boilerplate>
`.trim(),
  }
}

10条规则的设计意图

规则 意图
1. 不分叉 防止递归 Fork 导致资源爆炸
2. 不询问 Fork 是非交互式的,不能阻塞等待用户输入
3. 不评论 减少 token 浪费
4. 直接用工具 减少 LLM 思考时间,加速执行
5. 提交变更 Fork 完成的标志是 git commit
6. 不输出中间文本 减少 token 浪费
7. 严守范围 防止子代理超出任务边界
8. 500字以内 控制返回内容长度
9. “Scope:” 开头 结构化输出,便于主代理解析
10. 结构化报告 便于主代理理解子代理的执行结果

8.7 buildWorktreeNotice — worktree 隔离

export function buildWorktreeNotice(
  parentCwd: string,
  worktreeCwd: string
): string {
  return `
You are running in an isolated git worktree.
- Parent directory: ${parentCwd}
- Your worktree: ${worktreeCwd}
Changes you make are isolated to this worktree.
Use git to commit your changes when done.
`.trim()
}

isolation: 'worktree' 时,子代理在独立的 git worktree 中运行,文件修改与父代理隔离。


9. Context Cache Sharing — 四维度一致性

9.1 原书 6.8 节:缓存共享的工程意义

原书指出,缓存共享不是一个孤立的优化,而是贯穿多个子系统的"跨切面架构关注点":

“成本优化是跨切面架构关注点。Prompt Cache 的命中率取决于 API 请求前缀的字节级一致性,而这又取决于 system prompt、工具列表、消息历史、文件状态缓存等多个维度的协同。”

9.2 四维度一致性矩阵

维度 机制 源码位置 效果
1. 消息前缀 统一占位符 FORK_PLACEHOLDER_RESULT forkSubagent.ts buildForkedMessages 所有 Fork 的 tool_result 内容一致
2. 工具列表 useExactTools: true runAgent.ts 直接使用父级工具池,不做 resolveAgentTools
3. 模型 model: 'inherit' forkSubagent.ts FORK_AGENT 使用与父代理相同的模型
4. 文件状态缓存 cloneFileStateCache runAgent.ts 继承父代理的文件读取状态

9.3 useExactTools — 精确工具列表

// runAgent.ts
if (useExactTools) {
  // Fork 模式:直接使用父级工具池
  // 不调用 resolveAgentTools,不做任何过滤
  // 确保工具列表字节与父代理完全一致
  resolvedTools = availableTools
} else {
  // 非 Fork 模式:解析 Agent 定义的 tools/disallowedTools
  resolvedTools = resolveAgentTools(agentDefinition, availableTools, isAsync)
}

为什么 Fork 可以跳过沙箱过滤?

因为 Fork 模式的 permissionMode: 'bubble'——子代理的工具调用权限会冒泡到父终端,由用户在父终端确认。这意味着即使子代理拥有全部工具,实际执行时仍受权限控制。

9.4 thinkingConfig 继承

// runAgent.ts
agentOptions.thinkingConfig = isFork
  ? parentThinkingConfig     // Fork:继承父级 thinking 配置
  : { type: 'disabled' }     // 非 Fork:禁用 thinking(节省成本)

Fork 继承父级 thinking 配置的原因:如果父代理启用了 extended thinking,子代理也应该启用,否则 API 请求的 thinking 配置不同会导致缓存失效。


10. runAsyncAgentLifecycle — 五阶段生命周期(agentToolUtils.ts)

10.1 原书 6.7 节:异步 Agent 生命周期

原书描述了异步 Agent 的五阶段状态机:

Created → Running → ┬→ Completed
                    ├→ Failed
                    └→ Killed

10.2 五阶段实现

// agentToolUtils.ts
async function runAsyncAgentLifecycle({
  taskId,
  abortController,
  makeStream,           // () => AsyncGenerator<Message>,通常是 runAgent
  metadata,
  description,
  toolUseContext,
  rootSetAppState,
  agentIdForCleanup,
  enableSummarization,
  getWorktreeResult,
}: RunAsyncAgentLifecycleParams): Promise<void> {
  // ── 阶段1: Created ──
  const progressTracker = createProgressTracker()
  const activityResolver = createActivityDescriptionResolver()

  // ── 阶段2: Running ──
  const agentMessages: Message[] = []

  // 缓存安全参数回调——在缓存就绪后启动摘要
  onCacheSafeParams?.(cacheSafeParams => {
    if (enableSummarization) {
      startAgentSummarization(taskId, cacheSafeParams)
    }
  })

  try {
    for await (const message of makeStream()) {
      agentMessages.push(message)

      // 更新根状态(retain 模式:保留在 UI 中)
      rootSetAppState(prev => ({
        ...prev,
        agentMessages: retain(prev.agentMessages, message),
      }))

      // 更新进度追踪器
      updateProgressFromMessage(progressTracker, message)

      // 发射任务进度事件
      emitTaskProgress(taskId, progressTracker)
    }

    // ── 阶段3a: Completed ──
    const finalized = finalizeAgentTool(agentMessages, taskId, metadata)

    // 先标记完成(UI 更新)
    await completeAsyncAgent(taskId, finalized)

    // 再做安全审查(不阻塞 UI)
    const handoffWarning = await classifyHandoffIfNeeded({
      agentMessages,
      tools: toolUseContext.availableTools,
      toolPermissionContext: toolUseContext.toolPermissionContext,
      abortSignal: abortController.signal,
      subagentType: metadata.agentType,
      totalToolUseCount: finalized.totalToolUseCount,
    })

    // worktree 结果收集
    if (getWorktreeResult) {
      await getWorktreeResult()
    }

    // 通知(completed)
    await enqueueAgentNotification({
      taskId,
      status: 'completed',
      description,
    })

  } catch (error) {
    if (abortController.signal.aborted) {
      // ── 阶段3c: Killed ──
      const partialResult = extractPartialResult(agentMessages)
      await completeAsyncAgent(taskId, partialResult)
      await enqueueAgentNotification({
        taskId,
        status: 'killed',
        description,
      })
    } else {
      // ── 阶段3b: Failed ──
      await completeAsyncAgent(taskId, { error: error.message })
      await enqueueAgentNotification({
        taskId,
        status: 'failed',
        description,
        error: error.message,
      })
    }
  } finally {
    clearInvokedSkillsForAgent(agentIdForCleanup)
    clearDumpState(agentIdForCleanup)
  }
}

10.3 进度追踪器 — token 统计

// createProgressTracker
function createProgressTracker(): ProgressTracker {
  return {
    inputTokens: 0,
    outputTokens: 0,
    totalToolUseCount: 0,
    lastActivity: undefined,
    // ...
  }
}

// updateProgressFromMessage
function updateProgressFromMessage(
  tracker: ProgressTracker,
  message: Message
): void {
  // 输入 token:取最新值(API 返回的是累计值,取最新即可)
  if (message.usage?.input_tokens) {
    tracker.inputTokens = message.usage.input_tokens
  }

  // 输出 token:逐轮累加
  if (message.usage?.output_tokens) {
    tracker.outputTokens += message.usage.output_tokens
  }

  // 工具使用计数
  if (message.content?.some(b => b.type === 'tool_use')) {
    tracker.totalToolUseCount++
  }

  // 最后活动描述
  tracker.lastActivity = activityResolver.resolve(message)
}

输入 token 取最新值 vs 输出 token 累加的原因:

  • API 返回的 input_tokens累计值(包含缓存命中的 token),取最新值即可
  • API 返回的 output_tokens单轮值(本轮生成的 token),需要累加

10.4 classifyHandoffIfNeeded — 安全审查

// agentToolUtils.ts
async function classifyHandoffIfNeeded({
  agentMessages,
  tools,
  toolPermissionContext,
  abortSignal,
  subagentType,
  totalToolUseCount,
}: ClassifyHandoffParams): Promise<string | null> {
  // TRANSCRIPT_CLASSIFIER auto 模式下对 subagent 输出做安全审查
  // 提取最后一条 assistant 消息的文本内容
  // 使用分类器判断是否存在安全风险(如数据泄露、权限滥用等)
  // 返回警告字符串或 null

  const lastAssistantText = extractLastAssistantText(agentMessages)
  if (!lastAssistantText) return null

  const classification = await classifyTranscript({
    text: lastAssistantText,
    subagentType,
    totalToolUseCount,
    tools,
    toolPermissionContext,
    abortSignal,
  })

  return classification.warning ?? null
}

10.5 finalizeAgentTool — 结果提取

// agentToolUtils.ts
function finalizeAgentTool(
  agentMessages: Message[],
  agentId: string,
  metadata: AgentMetadata
): AgentToolResult {
  // 提取最后一条 assistant 消息的文本内容
  const lastAssistantMessage = [...agentMessages]
    .reverse()
    .find(m => m.role === 'assistant')

  const content = extractTextFromMessage(lastAssistantMessage)

  // 统计工具使用次数
  const totalToolUseCount = agentMessages.reduce((count, msg) => {
    return count + (msg.content?.filter(b => b.type === 'tool_use').length ?? 0)
  }, 0)

  // 统计总 token
  const totalTokens = agentMessages.reduce((sum, msg) => {
    return sum + (msg.usage?.input_tokens ?? 0) + (msg.usage?.output_tokens ?? 0)
  }, 0)

  // 计算总时长
  const totalDurationMs = Date.now() - metadata.startTime

  // 记录事件
  logEvent('agent_completed', {
    agentId,
    agentType: metadata.agentType,
    totalToolUseCount,
    totalTokens,
    totalDurationMs,
  })

  return {
    agentId,
    agentType: metadata.agentType,
    content,
    totalToolUseCount,
    totalDurationMs,
    totalTokens,
    usage: { inputTokens: ..., outputTokens: ... },
  }
}

11. resumeAgentBackground — 恢复机制(resumeAgent.ts — 266行)

11.1 从磁盘恢复异步 Agent

// resumeAgent.ts
export async function resumeAgentBackground({
  agentId,
  prompt,                  // 新的追加 prompt
  toolUseContext,
  canUseTool,
  invokingRequestId,
}: ResumeAgentParams): Promise<void> {
  // 1. 从磁盘并行读取 transcript 和 metadata
  const [transcript, metadata] = await Promise.all([
    getAgentTranscript(agentId),
    readAgentMetadata(agentId),
  ])

  // 2. 三层消息过滤
  let messages = transcript.messages

  // 层1:过滤未解决的 tool_use(没有对应 tool_result 的)
  messages = filterUnresolvedToolUses(messages)

  // 层2:过滤孤立的 thinking-only 消息(只有 thinking 块,没有其他内容)
  messages = filterOrphanedThinkingOnlyMessages(messages)

  // 层3:过滤纯空白的 assistant 消息
  messages = filterWhitespaceOnlyAssistantMessages(messages)

  // 3. 重建 contentReplacementState(用于内容替换)
  const contentReplacementState = reconstructForSubagentResume(messages)

  // 4. worktree 路径检查
  if (metadata.worktreePath) {
    // 检查路径是否仍然存在
    if (!await pathExists(metadata.worktreePath)) {
      throw new Error('Worktree no longer exists')
    }
    // 更新 mtime(防 stale 清理)
    await touch(metadata.worktreePath)
  }

  // 5. 构建 resume 参数
  const resumeParams: RunAgentParams = {
    agentDefinition: metadata.agentDefinition,
    promptMessages: [
      ...messages,
      { role: 'user', content: prompt },
    ],
    toolUseContext,
    canUseTool,
    isAsync: true,
    contentReplacementState,
    worktreePath: metadata.worktreePath,
    // ...
  }

  // 6. Fork resume 特殊处理
  if (metadata.isFork) {
    // 传递父级已渲染的 system prompt 字节
    // 避免 GrowthBook cold→warm 导致缓存失效
    resumeParams.override = {
      systemPrompt: metadata.renderedSystemPrompt,
    }
    resumeParams.useExactTools = true
  } else {
    // 非 Fork:重新组装工具池
    resumeParams.availableTools = await assembleToolPool(metadata)
  }

  // 7. 注册并启动
  registerAsyncAgent(agentId, metadata)
  await runAsyncAgentLifecycle({
    taskId: agentId,
    makeStream: () => runAgent(resumeParams),
    metadata,
    // ...
  })
}

11.2 Fork resume 的缓存一致性

问题:GrowthBook Feature Flag 可能在 Agent 运行期间从 cold(默认值)变为 warm(服务端配置值)。如果 resume 时重新渲染 system prompt,可能得到不同的字节,导致缓存失效。

解决方案:Fork resume 传递 metadata.renderedSystemPrompt——即首次运行时父代理已渲染的 system prompt 字节。这样 resume 后的 API 请求前缀与首次运行完全一致,缓存仍然有效。

// Fork resume 的关键区别
if (metadata.isFork) {
  resumeParams.override = {
    systemPrompt: metadata.renderedSystemPrompt,  // 父级已渲染的 prompt 字节
  }
  resumeParams.useExactTools = true  // 精确工具列表(不重新解析)
}

11.3 三层消息过滤

过滤层 函数 作用 原因
层1 filterUnresolvedToolUses 移除没有 tool_result 的 tool_use API 要求 tool_use 必须有对应的 tool_result
层2 filterOrphanedThinkingOnlyMessages 移除只含 thinking 块的消息 thinking-only 消息无实际内容,resume 后无用
层3 filterWhitespaceOnlyAssistantMessages 移除纯空白的 assistant 消息 空白消息无信息量,浪费 token

12. shouldInjectAgentListInMessages — 缓存优化(prompt.ts)

12.1 原书 4.3.3 节:Agent 列表移到 attachment message

// prompt.ts
export function shouldInjectAgentListInMessages(): boolean {
  // GrowthBook gate: tengu_agent_list_attach
  // 将 Agent 列表从工具描述移到 attachment message
  return feature('tengu_agent_list_attach')
}

问题:Agent 列表(可用子代理的名称、描述、工具列表)原本嵌入在 AgentTool 的 description()prompt() 中。当 MCP 服务器连接/断开、plugin 安装/卸载、permission 变化时,Agent 列表会变化,导致 AgentTool 的 schema 变化,进而导致所有工具的 schema 缓存失效(因为工具 schema 是一个整体)。

原书指出这占据了 fleet cache_creation tokens 的 ~10.2%

解决方案:将 Agent 列表从工具描述中移出,放到 attachment message(每次请求动态注入的 user 消息)中。这样 Agent 列表的变化不会影响工具 schema,缓存保持有效。

优化前:
  [system prompt] [工具 schema(含Agent列表)] [消息历史]  → 缓存断点
                                                    ↑ Agent列表变化导致这里失效

优化后:
  [system prompt] [工具 schema(固定)] [消息历史] [attachment: Agent列表]  → 缓存断点
                                  ↑ 缓存有效                    ↑ 动态注入,不影响缓存

12.2 getPrompt — 多模式 prompt 生成

// prompt.ts
export function getPrompt(
  agentDefinitions: AgentDefinition[],
  isCoordinator: boolean,
  allowedAgentTypes?: string[]
): string {
  // 1. 过滤 allowedAgentTypes
  let agents = agentDefinitions
  if (allowedAgentTypes) {
    agents = agents.filter(a => allowedAgentTypes.includes(a.agentType))
  }

  // 2. 构建工具描述部分
  const agentLines = agents
    .map(agent => formatAgentLine(agent))
    .join('\n')

  // 3. 根据模式选择不同的 prompt 段落
  let modeSpecificPrompt: string

  if (isForkMode) {
    // Fork 模式:插入"When to fork"段落 + "Writing the prompt"段落 + fork 示例
    modeSpecificPrompt = `
## When to fork
Fork when you need to run a subtask in the background while continuing your own work.

## Writing the prompt
Write a clear, directive-style prompt. The forked agent will:
- Not ask questions
- Not fork again
- Commit changes
- Report back concisely

${FORK_EXAMPLE}
`
  } else if (isCoordinator) {
    // Coordinator 模式:精简 prompt(系统提示已覆盖编排逻辑)
    modeSpecificPrompt = ''  // 大部分逻辑在 system prompt 中
  } else {
    // 标准模式:"When NOT to use"段落 + 标准示例
    modeSpecificPrompt = `
## When NOT to use
- Don't use for simple lookups — use Grep/Glob directly
- Don't use for file reads — use Read directly
- Don't use for single-step tasks

${STANDARD_EXAMPLE}
`
  }

  // 4. 并行提示(非 pro 订阅)
  const concurrencyNote = !isProSubscription
    ? '\nNote: You can start multiple agents in parallel.'
    : ''

  // 5. run_in_background 参数说明(非 Fork 模式)
  const backgroundNote = !isForkMode
    ? '\nUse run_in_background to start an agent that runs while you continue working.'
    : ''

  // 6. isolation 说明(ant-only)
  const isolationNote = isAntUser
    ? '\nUse isolation: "worktree" or isolation: "remote" for isolated execution.'
    : ''

  return [
    AGENT_INTRO,
    agentLines,
    modeSpecificPrompt,
    concurrencyNote,
    backgroundNote,
    isolationNote,
  ].filter(Boolean).join('\n')
}

12.3 formatAgentLine — Agent 描述格式

// prompt.ts
function formatAgentLine(agent: AgentDefinition): string {
  const toolsDescription = getToolsDescription(agent)
  return `- ${agent.agentType}: ${agent.whenToUse}${toolsDescription}`
}

function getToolsDescription(agent: AgentDefinition): string {
  if (agent.tools?.includes('*')) {
    return ' (all tools)'
  }
  if (agent.tools && agent.tools.length > 0) {
    return ` (Tools: ${agent.tools.join(', ')})`
  }
  if (agent.disallowedTools && agent.disallowedTools.length > 0) {
    return ` (all tools except: ${agent.disallowedTools.join(', ')})`
  }
  return ''
}

13. UI 渲染(UI.tsx — 872行)

13.1 进度消息处理

// UI.tsx
const MAX_PROGRESS_MESSAGES_TO_SHOW = 3
const ESTIMATED_LINES_PER_TOOL = 9
const TERMINAL_BUFFER_LINES = 7

// processProgressMessages — 连续 search/read/REPL 操作分组为 summary
function processProgressMessages(
  messages: ProgressMessage[]
): ProcessedProgress[] {
  // 将连续的 search/read/REPL 操作分组
  // 例如:3次连续 Grep → "Searching (3 operations)"
  // 减少 UI 显示的消息数量,避免刷屏
  const groups: ProcessedProgress[] = []
  let currentGroup: ProcessedProgress | null = null

  for (const msg of messages) {
    if (isSearchReadOrRepl(msg) && currentGroup?.type === msg.type) {
      currentGroup.count++
    } else {
      if (currentGroup) groups.push(currentGroup)
      currentGroup = { type: msg.type, count: 1, ...msg }
    }
  }
  if (currentGroup) groups.push(currentGroup)
  return groups
}

13.2 condensed mode — 终端行数不足

// renderToolUseProgressMessage
function renderToolUseProgressMessage(
  message: ProgressMessage,
  terminalLines: number
): ReactNode {
  // 当终端行数不足时,启用 condensed mode
  const isCondensed = terminalLines < CONDENSED_THRESHOLD

  if (isCondensed) {
    // 压缩显示:只显示最后 N 条消息
    return renderCondensedSummary(message)
  }

  // 正常显示
  return (
    <Box>
      <Text>{message.summary}</Text>
      {message.hiddenToolUseCount > 0 && (
        <Text dimColor>+{message.hiddenToolUseCount} hidden</Text>
      )}
    </Box>
  )
}

13.3 renderGroupedAgentToolUse — 多 Agent 并行展示

// UI.tsx
function renderGroupedAgentToolUse(
  agentStats: Map<string, AgentStat>
): ReactNode {
  // 多 Agent 并行时,按 Agent 分组展示
  return (
    <Box flexDirection="column">
      {Array.from(agentStats.entries()).map(([agentId, stat]) => (
        <AgentProgressLine
          key={agentId}
          agentId={agentId}
          stat={stat}
        />
      ))}
    </Box>
  )
}

13.4 userFacingName — worker → “Agent”

// UI.tsx
function userFacingName(subagentType: string): string {
  // 'worker' → 'Agent'(用户友好)
  // 其他类型直接显示 subagent_type
  if (subagentType === 'worker') return 'Agent'
  return subagentType
}

13.5 extractLastToolInfo — 从尾部统计

// UI.tsx
function extractLastToolInfo(messages: Message[]): ToolInfo {
  // 从消息尾部开始统计连续的 search/read 操作
  // 以及最后一个 tool_use 的摘要
  let searchCount = 0
  let readCount = 0

  for (let i = messages.length - 1; i >= 0; i--) {
    const msg = messages[i]
    const toolUseBlocks = msg.content?.filter(b => b.type === 'tool_use') ?? []

    for (const block of toolUseBlocks) {
      if (block.name === 'Grep' || block.name === 'Glob') searchCount++
      else if (block.name === 'Read') readCount++
      else return { searchCount, readCount, lastTool: block }
    }
  }

  return { searchCount, readCount, lastTool: undefined }
}

14. Agent 颜色管理(agentColorManager.ts — 67行)

14.1 8种颜色映射

// agentColorManager.ts
const AGENT_COLORS = ['red', 'blue', 'green', 'yellow', 'purple', 'orange', 'pink', 'cyan'] as const

const AGENT_COLOR_TO_THEME_COLOR: Record<string, string> = {
  red: 'RED_FOR_SUBAGENTS_ONLY',
  blue: 'BLUE_FOR_SUBAGENTS_ONLY',
  green: 'GREEN_FOR_SUBAGENTS_ONLY',
  yellow: 'YELLOW_FOR_SUBAGENTS_ONLY',
  purple: 'PURPLE_FOR_SUBAGENTS_ONLY',
  orange: 'ORANGE_FOR_SUBAGENTS_ONLY',
  pink: 'PINK_FOR_SUBAGENTS_ONLY',
  cyan: 'CYAN_FOR_SUBAGENTS_ONLY',
}

// agentType → color 的映射表
const agentColorMap = new Map<string, string>()

export function getAgentColor(agentType: string): string | undefined {
  // general-purpose 不分配颜色(它是最常用的,不需要视觉区分)
  if (agentType === 'general-purpose') return undefined
  return agentColorMap.get(agentType)
}

export function setAgentColor(agentType: string, color: string): void {
  agentColorMap.set(agentType, color)
}

设计意图

  • 8种颜色用于在 UI 中区分不同的并行子代理
  • *_FOR_SUBAGENTS_ONLY 主题色确保子代理颜色不会与主 Agent 的 UI 元素冲突
  • general-purpose 不分配颜色——它是默认的子代理类型,不需要视觉区分

15. Agent 显示工具(agentDisplay.ts — 105行)

15.1 AGENT_SOURCE_GROUPS — 7组显示顺序

// agentDisplay.ts
const AGENT_SOURCE_GROUPS = [
  { source: 'user',     label: 'User Agents' },
  { source: 'project',  label: 'Project Agents' },
  { source: 'flag',     label: 'Local Agents' },
  { source: 'managed',  label: 'Managed Agents' },
  { source: 'plugin',   label: 'Plugin Agents' },
  { source: 'cliArg',   label: 'CLI arg Agents' },
  { source: 'built-in', label: 'Built-in Agents' },
] as const

15.2 resolveAgentOverrides — 覆盖标注

// agentDisplay.ts
function resolveAgentOverrides(
  allAgents: AgentDefinition[],
  activeAgents: AgentDefinition[]
): DisplayAgent[] {
  const activeSet = new Set(activeAgents.map(a => a.agentType))

  return allAgents.map(agent => ({
    ...agent,
    isOverridden: !activeSet.has(agent.agentType),  // 被覆盖的标记
    // 同名不同 source 的 Agent 去重
  })).filter((agent, index, self) =>
    index === self.findIndex(a =>
      a.agentType === agent.agentType && a.source === agent.source
    )
  )
}

15.3 resolveAgentModelDisplay — 模型显示

function resolveAgentModelDisplay(agent: AgentDefinition): string {
  if (!agent.model) return 'default'
  if (agent.model === 'inherit') return 'inherit'
  // 显示模型别名(如 'claude-sonnet-4-20250514' → 'sonnet')
  return getModelAlias(agent.model) ?? agent.model
}

16. 原书第6章对照验证表

16.1 章节级对照

原书节 主题 源码位置 对照结论
6.2 Fork-and-Delegate forkSubagent.ts ✅ 完全对应:三重守卫、占位符统一化、递归防护双重防线、10条规则
6.3 Agent Type Registry loadAgentsDir.ts ✅ 完全对应:四层加载源、Map 后写覆盖、Markdown Frontmatter、Feature Flag 矩阵
6.4 Tool Sandboxing agentToolUtils.ts ✅ 完全对应:三层过滤、权限矩阵、MCP 特殊地位
6.5 Scoped Memory loadAgentsDir.ts (memory字段) ✅ 对应:memory: true 注入 Write/Edit/Read、initializeAgentMemorySnapshots
6.6 Coordinator-Worker builtInAgents.ts + prompt.ts ✅ 对应:isCoordinatorMode() 返回专用列表、精简 prompt
6.7 Async Agent Lifecycle agentToolUtils.ts ✅ 完全对应:五阶段状态机、进度追踪器、10步清理链
6.8 Context Cache Sharing forkSubagent.ts + runAgent.ts ✅ 完全对应:四维度一致性、成本数据吻合
6.9 Fork vs Coordinator Trade-off forkSubagent.ts vs builtInAgents.ts ✅ 对应:三重互斥守卫
6.10 模式协作全景 全目录 ✅ 对应:Fork/Coordinator/Async 三种模式并存

16.2 Fork vs Coordinator Trade-off 对照表(原书 6.9 节)

维度 Fork Coordinator
上下文共享 ✅ 完整继承父级消息 ❌ 自包含 prompt,Worker 看不到 Coordinator 对话
工具权限 ✅ useExactTools 全部工具 ❌ Worker 受工具列表限制
缓存效率 ✅ 字节级前缀一致 ❌ 每个 Worker 独立 prompt
任务编排 ❌ 无编排能力 ✅ 四阶段工作流(Research→Synthesis→Implementation→Verification)
互动能力 ❌ 不询问/不评论 ✅ XML 通知协议可交互
递归深度 ❌ 不可递归(双重防线) ✅ 可多级编排
适用场景 明确的独立子任务 复杂的多步骤编排
成本模型 低(缓存共享) 高(独立 prompt)

17. 架构总结

17.1 四层架构全景

┌─────────────────────────────────────────────────────────────┐
│                    用户 / 主 Agent                           │
│                        │                                     │
│                        ▼                                     │
│  ┌─── AgentTool.tsx (入口) ───────────────────────────┐    │
│  │  解析参数 → 选择 Agent → 调用 runAgent              │    │
│  └───────────────────────┬─────────────────────────────┘    │
│                          │                                   │
│  ┌─── 层1: 定义注册表 ───┴──────────────────────────────┐   │
│  │  loadAgentsDir.ts                                     │   │
│  │  • 四层加载源优先级覆盖(builtIn→plugin→user→project)│   │
│  │  • Markdown Frontmatter 解析 + Zod 验证              │   │
│  │  • 6种内置 Agent + Feature Flag 矩阵                 │   │
│  │  • memoize 缓存                                       │   │
│  └───────────────────────┬─────────────────────────────┘   │
│                          │                                   │
│  ┌─── 层2: 工具沙箱 ────┴───────────────────────────────┐   │
│  │  agentToolUtils.ts                                    │   │
│  │  • 三层过滤(MCP放行→全局禁止→自定义禁止/异步白名单)│   │
│  │  • resolveAgentTools(通配符/否定列表/allowedAgentTypes)│  │
│  │  • 权限矩阵(编排者不执行,执行者不编排)             │   │
│  └───────────────────────┬─────────────────────────────┘   │
│                          │                                   │
│  ┌─── 层3: 执行引擎 ────┴───────────────────────────────┐   │
│  │  runAgent.ts (974行)                                  │   │
│  │  • 上下文继承(forkContextMessages / 文件缓存克隆)   │   │
│  │  • 权限覆盖(不提升/隔离/async避免提示)              │   │
│  │  • MCP 初始化(引用名共享/内联定义独立)              │   │
│  │  • Hooks(SubagentStart / Stop→SubagentStop)         │   │
│  │  • Skills 预加载(三级名称解析)                      │   │
│  │  • 10步清理链                                         │   │
│  └───────────────────────┬─────────────────────────────┘   │
│                          │                                   │
│  ┌─── 层4: 生命周期 ────┴───────────────────────────────┐   │
│  │  agentToolUtils.ts + resumeAgent.ts                   │   │
│  │  • 五阶段状态机(Created→Running→Completed/Failed/Killed)│ │
│  │  • 进度追踪器(输入token取最新/输出token累加)        │   │
│  │  • 安全审查(classifyHandoffIfNeeded)                │   │
│  │  • 恢复机制(三层过滤 + Fork prompt 字节传递)        │   │
│  └─────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘

17.2 三种编排模式对比

维度 Fork-and-Delegate Coordinator-Worker Async Agent
触发方式 主 Agent 主动 Fork Coordinator 模式自动 run_in_background 参数
上下文 继承父级完整消息 自包含 prompt 继承 + 独立执行
工具权限 全部(bubble 权限) 受限(编排/执行分离) 异步白名单
缓存共享 ✅ 字节级一致 ❌ 独立 prompt 部分(forkContextMessages)
交互能力 无(10条规则禁止) XML 通知协议 无(异步执行)
递归 禁止(双重防线) 允许多级 禁止
适用场景 独立子任务 复杂编排 长时间任务
恢复 ✅ resumeAgentBackground ✅ resumeAgentBackground
Feature Flag FORK_SUBAGENT Coordinator 模式 默认启用

17.3 关键工程决策

  1. 占位符统一化FORK_PLACEHOLDER_RESULT):用 1 行常量换取 Prompt Cache 字节级共享,是整个 Fork 模式的基石
  2. getSystemPrompt: () => '':Fork Agent 不使用自己的 system prompt,而是传递父代理已渲染的字节——看似反直觉,实则是缓存一致性的关键
  3. 三层工具沙箱:MCP 放行 → 全局禁止 → 自定义禁止/异步白名单,层层递进,既保证安全又不过度限制
  4. 10步清理链:每个步骤独立执行,确保资源零泄漏
  5. Agent 列表移到 attachment message:一个 GrowthBook flag 解决 10.2% 的缓存失效问题
  6. Explore/Plan 省略 CLAUDE.md:每周节省 5-15 Gtok,成本优化是跨切面关注点
  7. 递归防护双重防线:消息标记(可被 LLM 伪造)+ querySource(不可变属性),纵深防御
  8. Fork resume 传递 renderedSystemPrompt:避免 GrowthBook cold→warm 导致缓存失效,体现了对运行时环境的深刻理解

18. 思考题与延伸

  1. Fork 模式为什么不支持递归? 如果允许 Fork 递归,每个 Fork 都会克隆父级的完整上下文,N 层递归会导致 2^N 的上下文副本,资源爆炸。双重防线(消息标记 + querySource)确保递归不可能发生。

  2. Coordinator 模式为什么与 Fork 互斥? 两者都是"编排"模式,但设计哲学不同:Fork 是"继承+独立"(共享上下文),Coordinator 是"委托+隔离"(自包含 prompt)。同时启用会导致编排逻辑冲突。

  3. 为什么 Explore 使用 haiku 而 Plan 使用 inherit? Explore 的任务是"快速搜索"——haiku 更快更便宜,且搜索任务不需要深度推理。Plan 的任务是"架构设计"——需要与主 Agent 同级的推理能力,因此 inherit。

  4. shouldInjectAgentListInMessages 优化为什么重要? 工具 schema 是一个整体——任何一个工具的 schema 变化都会导致整个工具 schema 部分的缓存失效。Agent 列表嵌入在 AgentTool 的 description 中,而 Agent 列表受 MCP/plugin/permission 影响,变化频繁。将 Agent 列表移到 attachment message,工具 schema 部分就固定了,缓存命中率提升 10.2%。

  5. 异步 Agent 的 token 统计为什么输入取最新、输出累加? API 返回的 input_tokens 包含缓存命中的 token,是累计值——每次请求的 input_tokens 都包含之前所有请求的输入 token,取最新值即可。而 output_tokens 是本轮生成的 token,需要逐轮累加才能得到总值。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐