AgentTool:子 Agent 调度器

📑 目录

在 Claude Code 的工具体系中,AgentTool 是一个极为特殊的存在。它不像 BashTool 那样直接与操作系统交互,也不像 FileReadTool 那样专注于文件系统读写。它的唯一职责是创建并调度另一个 Agent——一个能够独立运行、拥有自己的系统提示词、工具池和生命周期的子 Agent。

如果说 Claude Code 的其余工具扩展了 Agent 的"能力边界",那么 AgentTool 则扩展了它的"组织边界"。它让 Claude Code 从单一的对话线程,演化为一个可以并发执行多任务、分工协作的 Agent 系统。本文将深入解析 AgentTool 的源码实现,揭示它如何完成从"任务描述"到"子 Agent 运行"的完整调度链路。


一、AgentTool 的定位:从单线程到多 Agent 的分水岭

在 Claude Code 的工具注册列表中,AgentTool 通常排在第一位(AGENT_TOOL_NAME)。这不是偶然——它代表了 Claude Code 架构设计中最核心的哲学:Agent 应该是递归的、可组合的

1.1 为什么是工具池中的第一个工具

AgentTool.tsx 的源码结构可以看出,AgentTool 被赋予了极高的架构优先级:

// AgentTool.tsx:270-280
export const AgentTool = buildTool({
  async prompt({
    agents,
    tools,
    getToolPermissionContext,
    allowedAgentTypes
  }) {
    // ... 构建包含所有可用 Agent 类型的提示词
    return await getPrompt(filteredAgents, isCoordinator, allowedAgentTypes);
  },
  name: AGENT_TOOL_NAME,
  searchHint: 'delegate work to a subagent',
  aliases: [LEGACY_AGENT_TOOL_NAME],
  maxResultSizeChars: 100_000,
  // ...
});

它被赋予 searchHint: 'delegate work to a subagent',意味着当 Claude Code 的 LLM 需要"委派工作"时,会优先检索到这个工具。同时,maxResultSizeChars: 100_000 的高配额也反映了子 Agent 可能产生大量输出的场景。

1.2 单线程到多 Agent 的跃迁

在没有 AgentTool 的系统中,所有任务都在单一对话线程中顺序执行:读取文件 → 分析 → 修改 → 测试。当任务复杂度上升时,上下文窗口会被快速耗尽,且不同任务之间的状态会互相干扰。

AgentTool 引入了一个关键抽象:每个子 Agent 拥有独立的上下文和生命周期。主 Agent 可以将一个大任务拆分为多个子任务,分别派发给不同的子 Agent 并行执行,最后汇总结果。这种模型类似于操作系统中的进程 fork——父进程创建子进程,子进程独立运行,完成后向父进程汇报。

flowchart TD
    A[主 Agent
Main Thread] -->|AgentTool| B[子 Agent A
异步/同步] A -->|AgentTool| C[子 Agent B
异步/同步] A -->|AgentTool| D[子 Agent C
后台任务] B -->|TaskOutputTool| A C -->|TaskOutputTool| A D -->|通知/回调| A style A fill:#4a90d9,color:#fff style D fill:#e67e22,color:#fff

二、输入 Schema:精确控制子 Agent 的每一个维度

AgentTool 的输入参数设计体现了对子 Agent 行为的精细化控制。源码中定义了两层 Schema:baseInputSchema(基础参数)和 fullInputSchema(扩展参数,包含多 Agent 协作相关字段)。

2.1 基础输入参数

// AgentTool.tsx:96-104
const baseInputSchema = lazySchema(() => z.object({
  description: z.string().describe('A short (3-5 word) description of the task'),
  prompt: z.string().describe('The task for the agent to perform'),
  subagent_type: z.string().optional().describe('The type of specialized agent to use for this task'),
  model: z.enum(['sonnet', 'opus', 'haiku']).optional().describe("Optional model override..."),
  run_in_background: z.boolean().optional().describe('Set to true to run this agent in the background...')
}));

description

任务的简短描述(3-5 个词)。这个字段不仅是给人看的,更会被用于任务通知、进度追踪和日志记录。当子 Agent 以异步方式运行时,系统会使用这个描述生成任务状态更新。

prompt

派发给子 Agent 的具体任务指令。这是子 Agent 用户消息(User Message)的核心内容。在 Fork 子 Agent 路径中,这个 prompt 会被包装在特殊的 Fork 指令消息中(buildForkedMessages)。

subagent_type

指定要使用的专业化 Agent 类型。Claude Code 支持多种内置 Agent 类型(如 ExplorePlan、通用 Agent 等),也支持用户自定义 Agent。如果省略此参数,系统会根据 Feature Gate 决定是走 Fork 路径(isForkSubagentEnabled())还是默认使用通用 Agent(GENERAL_PURPOSE_AGENT)。

model

可选的模型覆盖参数,允许为特定子任务指定不同的 Claude 模型。取值范围限定为 'sonnet''opus''haiku'。这个参数会覆盖 Agent 定义中的模型设置,也会覆盖父 Agent 继承的模型。

run_in_background

是否以异步/后台方式运行子 Agent。这是 AgentTool 最核心的行为控制开关之一:

  • false(默认):同步运行。主 Agent 的当前回合会被阻塞,直到子 Agent 完成并返回结果。
  • true:异步运行。子 Agent 被注册到任务系统中独立运行,主 Agent 立即收到一个 async_launched 状态的结果,后续通过通知或 TaskOutputTool 获取结果。

2.2 扩展输入参数(多 Agent 协作)

KAIROSPROACTIVE Feature Gate 开启时,Schema 会扩展为 fullInputSchema,增加多 Agent 协作相关的参数:

// AgentTool.tsx:108-121
const multiAgentInputSchema = z.object({
  name: z.string().optional().describe('Name for the spawned agent...'),
  team_name: z.string().optional().describe('Team name for spawning...'),
  mode: permissionModeSchema().optional().describe('Permission mode for spawned teammate...')
});

nameteam_name

用于Agent Swarms(Agent 集群)场景。当同时提供 nameteam_name 时,AgentTool 不会创建传统的子 Agent,而是通过 spawnTeammate() 创建一个队友(Teammate)Agent。队友之间可以通过 SendMessageTool 进行双向通信,实现真正的多 Agent 协作。

isolation

隔离模式,目前支持 'worktree'。当设置为 worktree 时,系统会为子 Agent 创建一个临时的 Git worktree,使其在隔离的代码副本上工作,避免对主工作区造成意外修改。

cwd

覆盖子 Agent 的工作目录。与 isolation: 'worktree' 互斥。


三、子 Agent 创建流程:从调用到运行

当 LLM 发起一次 AgentTool 调用后,代码进入 call() 方法,经历一个复杂但井然有序的创建流程。

3.1 总体流程概览

sequenceDiagram
    participant LLM as LLM/主Agent
    participant AT as AgentTool.call()
    participant RA as runAgent()
    participant Query as query()
    participant Task as Task系统

    LLM->>AT: AgentTool({description, prompt, subagent_type, ...})
    AT->>AT: 1. 解析Agent类型 & 权限检查
    AT->>AT: 2. MCP服务器依赖检查
    AT->>AT: 3. 决定同步/异步模式
    AT->>AT: 4. 组装工具池 (assembleToolPool)
    AT->>AT: 5. 生成系统提示词
    AT->>AT: 6. 构建prompt消息
    
    alt 异步模式
        AT->>Task: registerAsyncAgent()
        AT-->>LLM: {status: 'async_launched'}
        Task->>RA: 后台启动runAgent()
    else 同步模式
        AT->>RA: runAgent()
        RA->>Query: query() 对话循环
        Query-->>RA: 消息流
        RA-->>AT: 最终消息
        AT-->>LLM: {status: 'completed'}
    end

3.2 Agent 类型解析与路由

call() 方法首先解析用户请求的 Agent 类型:

// AgentTool.tsx:311-340
const effectiveType = subagent_type ?? (isForkSubagentEnabled() ? undefined : GENERAL_PURPOSE_AGENT.agentType);
const isForkPath = effectiveType === undefined;
let selectedAgent: AgentDefinition;

if (isForkPath) {
  // Fork路径:拒绝递归Fork
  if (toolUseContext.options.querySource === `agent:builtin:${FORK_AGENT.agentType}` || isInForkChild(toolUseContext.messages)) {
    throw new Error('Fork is not available inside a forked worker...');
  }
  selectedAgent = FORK_AGENT;
} else {
  // 正常路径:从已激活的Agent列表中查找
  const found = agents.find(agent => agent.agentType === effectiveType);
  selectedAgent = found;
}

这里有两个关键分支:

Fork 路径:当 subagent_type 未指定且 isForkSubagentEnabled() 返回 true 时,系统走 Fork 子 Agent 路径。Fork Agent 的特殊之处在于它继承父 Agent 的系统提示词和工具池,以保证 API 请求的缓存前缀完全一致(prompt cache hit)。源码中的注释明确说明了这一点:

"Fork path: child inherits the PARENT’s system prompt (not FORK_AGENT’s) for cache-identical API request prefixes."

正常路径:根据 subagent_type 查找对应的 AgentDefinition。如果没有找到,会抛出错误并列出所有可用的 Agent 类型。这里还会检查权限规则——如果某个 Agent 类型被 AgentTool(AgentName) 语法显式拒绝,会返回专门的错误信息。

3.3 系统提示词的重新生成

对于正常路径的子 Agent,系统需要为其生成独立的系统提示词:

// AgentTool.tsx:440-460
const agentPrompt = selectedAgent.getSystemPrompt({ toolUseContext });
// 应用环境详情增强
enhancedSystemPrompt = await enhanceSystemPromptWithEnvDetails(
  [agentPrompt], 
  resolvedAgentModel, 
  additionalWorkingDirectories
);

这个过程与主 Agent 启动时的系统提示词生成类似,但有几点关键差异:

  1. Agent 特定的提示词:每个 AgentDefinition 可以定义自己的 getSystemPrompt,提供专业化的角色定义和能力描述。
  2. 环境上下文注入enhanceSystemPromptWithEnvDetails 会将当前工作目录、Git 状态、环境变量等信息附加到系统提示词中。
  3. Fork 路径的差异:Fork 子 Agent 不重新生成系统提示词,而是直接使用父 Agent 的 renderedSystemPrompt,这是为了保证缓存一致性。

3.4 工具池的重新裁剪

子 Agent 不应该拥有和父 Agent 完全相同的工具权限。例如,子 Agent 不应该能够创建新的子 Agent(避免无限递归),也不应该访问某些敏感工具。

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

runAgent() 内部,工具池会进一步通过 resolveAgentTools() 进行裁剪:

// runAgent.ts:508-510
const resolvedTools = useExactTools
  ? availableTools
  : resolveAgentTools(agentDefinition, availableTools, isAsync).resolvedTools

resolveAgentTools 函数(定义在 agentToolUtils.ts)执行以下过滤逻辑:

// agentToolUtils.ts:68-95
export function filterToolsForAgent({
  tools, isBuiltIn, isAsync = false, permissionMode
}: {
  tools: Tools; isBuiltIn: boolean; isAsync?: boolean; permissionMode?: PermissionMode
}): Tools {
  return tools.filter(tool => {
    if (tool.name.startsWith('mcp__')) return true;  // MCP工具始终允许
    if (ALL_AGENT_DISALLOWED_TOOLS.has(tool.name)) return false;  // 全局禁止列表
    if (!isBuiltIn && CUSTOM_AGENT_DISALLOWED_TOOLS.has(tool.name)) return false;
    if (isAsync && !ASYNC_AGENT_ALLOWED_TOOLS.has(tool.name)) return false;  // 异步Agent限制
    return true;
  });
}

这个裁剪机制确保了:

  • 安全边界:子 Agent 不能执行某些高风险操作(如直接操作主 Agent 的状态)。
  • 异步限制:后台运行的异步 Agent 只允许使用白名单中的工具(ASYNC_AGENT_ALLOWED_TOOLS),因为后台 Agent 无法与用户进行交互式确认。
  • MCP 工具继承:MCP(Model Context Protocol)工具对所有 Agent 开放,保证外部工具能力的可传递性。

四、本地 vs 远程:子 Agent 的执行环境选择

Claude Code 的子 Agent 可以在本地进程远程环境中运行,这一选择由 isolation 参数和系统配置共同决定。

4.1 本地执行:LocalAgentTask

默认情况下,子 Agent 在本地 Node.js/Bun 进程中运行。对于同步子 Agent,runAgent() 函数直接在当前的 JavaScript 执行环境中被调用,通过 query() 函数与 LLM API 交互。

对于异步子 Agent,系统会将其注册到 LocalAgentTask 系统中:

// AgentTool.tsx:640-650
const agentBackgroundTask = registerAsyncAgent({
  agentId: asyncAgentId,
  description,
  prompt,
  selectedAgent,
  setAppState: rootSetAppState,
  toolUseId: toolUseContext.toolUseId
});

registerAsyncAgent 返回一个任务对象,包含独立的 abortController,这意味着后台 Agent 不会因为用户按 ESC 取消主线程而被终止——它们必须通过专门的 chat:killAgents 命令来停止。

4.2 远程执行:RemoteAgentTask

isolation 设置为 'remote' 时(仅在 "external" === 'ant' 的内部分支中启用),子 Agent 会被发送到远程的 CCR(Claude Code Remote)环境中执行:

// AgentTool.tsx:375-395
if ("external" === 'ant' && effectiveIsolation === 'remote') {
  const eligibility = await checkRemoteAgentEligibility();
  if (!eligibility.eligible) {
    throw new Error(`Cannot launch remote agent:\n${reasons}`);
  }
  const session = await teleportToRemote({ initialMessage: prompt, description, ... });
  const { taskId, sessionId } = registerRemoteAgentTask({
    remoteTaskType: 'remote-agent',
    session: { id: session.id, title: session.title || description },
    command: prompt,
    context: toolUseContext,
    toolUseId: toolUseContext.toolUseId
  });
  // 返回 remote_launched 状态
}

远程执行的优势在于:

  • 资源隔离:不占用本地机器的计算资源和上下文窗口。
  • 网络访问:远程环境可能拥有不同的网络权限和工具链。
  • 持久化:即使本地会话关闭,远程 Agent 仍可继续运行。

4.3 选择逻辑

执行环境的选择遵循以下优先级:

  1. 显式 isolation 参数:用户或 Agent 定义中指定的隔离模式优先。
  2. Agent 定义中的 background 属性:如果 Agent 定义声明了 background: true,且未禁用后台任务,则强制异步运行。
  3. Feature Gate 强制:Coordinator 模式、Fork 子 Agent 开启、KAIROS 助手模式、Proactive 模式等都会强制子 Agent 异步运行。
  4. run_in_background 参数:用户的显式请求。
// AgentTool.tsx:555-560
const forceAsync = isForkSubagentEnabled();
const assistantForceAsync = feature('KAIROS') ? appState.kairosEnabled : false;
const shouldRunAsync = (
  run_in_background === true || selectedAgent.background === true || 
  isCoordinator || forceAsync || assistantForceAsync || 
  (proactiveModule?.isProactiveActive() ?? false)
) && !isBackgroundTasksDisabled;

五、生命周期管理:与 Task 系统的深度绑定

AgentTool 最强大的能力之一是对子 Agent 全生命周期的管理。这不仅仅是一次简单的函数调用,而是一套包含注册、执行、监控、通知、清理的完整状态机。

5.1 同步 Agent 的生命周期

同步子 Agent 的生命周期相对简单:

  1. 创建runAgent() 被调用,生成新的 agentId
  2. 运行:进入 query() 循环,与 LLM API 进行多轮对话。
  3. 完成query() 返回最终消息流,runAgent() 的 AsyncGenerator 结束。
  4. 清理:销毁 MCP 连接、注销 Perfetto Trace、清理会话存储。
// runAgent.ts:280-290
const agentId = override?.agentId ? override.agentId : createAgentId();
if (isPerfettoTracingEnabled()) {
  const parentId = toolUseContext.agentId ?? getSessionId();
  registerPerfettoAgent(agentId, agentDefinition.agentType, parentId);
}
// ... query 循环 ...
// 完成后 unregisterPerfettoAgent

5.2 异步 Agent 的生命周期

异步 Agent 的生命周期要复杂得多,因为它需要在主 Agent 的回合结束后继续存活:

注册阶段 → 后台执行 → 进度追踪 → 完成通知 → 结果持久化 → 清理

注册阶段

// AgentTool.tsx:640-650
const agentBackgroundTask = registerAsyncAgent({
  agentId: asyncAgentId,
  description,
  prompt,
  selectedAgent,
  setAppState: rootSetAppState,
  toolUseId: toolUseContext.toolUseId
});

// 注册名称路由(用于SendMessage)
if (name) {
  rootSetAppState(prev => {
    const next = new Map(prev.agentNameRegistry);
    next.set(name, asAgentId(asyncAgentId));
    return { ...prev, agentNameRegistry: next };
  });
}

后台执行与进度追踪

异步 Agent 通过 runAsyncAgentLifecycle() 函数在后台执行。这个函数包装了 runAgent() 的调用,并添加了进度追踪和状态更新机制:

// AgentTool.tsx:680-690
void runWithAgentContext(asyncAgentContext, () => wrapWithCwd(() => 
  runAsyncAgentLifecycle({
    taskId: agentBackgroundTask.agentId,
    abortController: agentBackgroundTask.abortController!,
    makeStream: onCacheSafeParams => runAgent({ ... }),
    metadata,
    description,
    toolUseContext,
    rootSetAppState,
    agentIdForCleanup: asyncAgentId,
    enableSummarization: isCoordinator || isForkSubagentEnabled() || getSdkAgentProgressSummariesEnabled(),
    // ...
  })
));

runAsyncAgentLifecycle 负责:

  • 启动 Agent 执行流(makeStream)。
  • 监听消息流,将进度更新写入状态。
  • 支持周期性摘要(当 enableSummarizationtrue 时),通过 startAgentSummarization 对长运行的后台 Agent 进行进度摘要,避免主 Agent 恢复时面对海量消息。

状态、输出与通知

异步 Agent 完成后,系统会:

  1. 写入输出文件:将最终结果写入磁盘文件(getTaskOutputPath(taskId))。
  2. 发送通知:通过 enqueueAgentNotification 向主 Agent 发送 <task-notification> 事件。
  3. 更新状态:在 AppState 中将任务标记为 completedfailed
  4. 清理资源:注销 agentNameRegistry 中的名称映射,清理 worktree(如果使用了隔离)。

5.3 终止机制

子 Agent 可以通过多种方式被终止:

  • 自然完成:子 Agent 自行完成任务,返回结果。
  • 用户取消:通过 chat:killAgents 命令,调用 killAsyncAgent() 终止后台 Agent。
  • 父 Agent 取消:同步 Agent 会继承父 Agent 的 abortController,当父 Agent 被终止时,子 Agent 也会被级联取消。
  • 独立取消:异步 Agent 拥有独立的 abortController,父 Agent 的取消不会影响它(设计上如此,避免误杀后台任务)。
// AgentTool.tsx:660-670
// 异步Agent获得独立的abortController
const agentAbortController = override?.abortController
  ? override.abortController
  : isAsync
    ? new AbortController()  // 独立控制器
    : toolUseContext.abortController;  // 继承父控制器

六、与相邻工具的关系:构建完整的 Agent 协作生态

AgentTool 并非孤立存在,它与 Claude Code 工具池中的多个工具形成了紧密的协作关系,共同构建了一个完整的 Agent 协作生态。

6.1 SendMessageTool:多 Agent 通信的桥梁

当 Agent Swarms 功能启用时,AgentTool 创建的队友 Agent(Teammate)之间需要双向通信机制。SendMessageTool 就是这个机制的载体。

AgentTool 在注册异步 Agent 时,会将 name 参数映射到 agentId,存入 agentNameRegistry

// AgentTool.tsx:655-660
if (name) {
  rootSetAppState(prev => {
    const next = new Map(prev.agentNameRegistry);
    next.set(name, asAgentId(asyncAgentId));
    return { ...prev, agentNameRegistry: next };
  });
}

这使得 SendMessageTool 可以通过 name 作为地址,向特定的子 Agent 发送消息。这种设计让 Claude Code 从"主-从"式的子 Agent 调用,演进为"对等网络"式的多 Agent 协作。

值得注意的是,源码中有一个安全限制:队友不能创建其他队友(扁平化团队结构):

// AgentTool.tsx:290-295
if (isTeammate() && teamName && name) {
  throw new Error('Teammates cannot spawn other teammates — the team roster is flat...');
}

6.2 Task 系列工具

AgentTool 与 Task 系统的关系是"创建者"与"消费者"的关系:

  • AgentTool 负责创建任务(通过 registerAsyncAgentregisterRemoteAgentTask)。
  • TaskOutputTool 负责读取任务的输出文件。
  • Task 相关状态 存储在 AppState 中,供 UI 渲染任务列表。

异步 Agent 完成后,其输出文件路径会作为 async_launched 结果的一部分返回给调用者:

// AgentTool.tsx:80-90
const asyncOutputSchema = z.object({
  status: z.literal('async_launched'),
  agentId: z.string(),
  description: z.string(),
  prompt: z.string(),
  outputFile: z.string(),  // 输出文件路径
  canReadOutputFile: z.boolean().optional()
});

主 Agent 可以随后使用 TaskOutputTool 或直接的文件读取工具来检查这个结果文件,从而获取子 Agent 的最终输出。

6.3 SkillTool

SkillTool 为 Agent 提供了调用预定义技能的能力,而 AgentToolSkillTool 之间存在两层交互:

第一层:Agent 定义中的技能预加载

runAgent.ts 中,子 Agent 启动时会预加载其 AgentDefinition 中定义的技能:

// runAgent.ts:540-550
const skillsToPreload = agentDefinition.skills ?? [];
if (skillsToPreload.length > 0) {
  const allSkills = await getSkillToolCommands(getProjectRoot());
  // 解析并验证技能名称,支持插件命名空间
  for (const skillName of skillsToPreload) {
    const resolvedName = resolveSkillName(skillName, allSkills, agentDefinition);
    // ... 注册技能
  }
}

这意味着每个子 Agent 可以拥有自己独特的技能组合,与父 Agent 的技能集解耦。

第二层:子 Agent 调用 SkillTool

子 Agent 在其运行过程中,可以像使用任何其他工具一样使用 SkillTool。由于 SkillTool 不在 ALL_AGENT_DISALLOWED_TOOLS 列表中,它对所有子 Agent 都是可用的。


七、源码总结与关键设计思想

通过深入分析 AgentTool.tsxrunAgent.tsagentToolUtils.ts 的源码,我们可以提炼出 Claude Code 子 Agent 系统的几个核心设计思想:

7.1 递归与组合

Agent 可以创建 Agent,子 Agent 又可以创建孙 Agent。这种递归性使得复杂的任务可以被无限分解。但系统通过 isInForkChild 检查和 ALL_AGENT_DISALLOWED_TOOLS 等机制防止了无限递归和滥用。

7.2 上下文隔离与继承的权衡

  • Fork 路径:追求缓存一致性,继承父 Agent 的系统提示词和工具池。
  • 正常路径:追求专业化,为每个子 Agent 生成独立的系统提示词和裁剪后的工具池。

7.3 同步与异步的灵活切换

AgentTool 通过 run_in_background、Agent 定义的 background 属性、Feature Gate 等多种机制,提供了从完全同步到完全异步的灵活光谱。这使得同样的任务派发接口可以适应不同的交互场景。

7.4 生命周期与资源管理的严谨性

agentId 的生成、Perfetto Trace 的注册、MCP 服务器的初始化和清理、worktree 的创建和销毁,到 agentNameRegistry 的维护,AgentTool 展现了工业级代码对资源管理的严谨态度。每一个被创建的对象都有对应的清理逻辑,每一个异常路径都有适当的回退处理。


结语

AgentTool 是 Claude Code 从一个"聪明的命令行助手"进化为"可扩展的 Agent 平台"的关键基石。它不仅提供了创建子 Agent 的能力,更通过精细的参数控制、灵活的生命周期管理、与 Task 系统和多 Agent 通信工具的深度集成,构建了一个完整的多 Agent 协作框架。

对于开发者而言,理解 AgentTool 的实现原理,是掌握 Claude Code 架构设计的必经之路。它展示了如何在复杂的 AI 应用中管理并发、隔离上下文、调度资源——这些正是构建生产级 Agent 系统的核心挑战。