《Claude Code 设计与实现》完整目录
- 前言
- 第1章 为什么需要理解 Claude Code
- 第2章 架构总览
- 第3章 CLI 启动与性能优化
- 第4章 Query 引擎:Agent 的心脏
- 第5章 流式消息与状态机
- 第6章 工具类型系统设计
- 第7章 工具编排与并发执行
- 第8章 核心工具实现剖析
- 第9章 多模式权限模型
- 第10章 Bash 安全与沙箱
- 第11章 MCP 协议集成
- 第12章 IDE Bridge 通信架构
- 第13章 LSP 与语言服务
- 第14章 多 Agent 协调与 Swarm(当前)
- 第15章 Skill 与插件系统
- 第16章 上下文管理与自动压缩
- 第17章 React + Ink 终端 UI
- 第18章 设计模式与架构决策
第14章 多 Agent 协调与 Swarm
在软件工程任务中,一个复杂的需求往往可以分解为若干彼此独立又相互关联的子任务:调研代码库结构、实现功能变更、运行测试验证、编写提交信息。当这些子任务由单一 Agent 串行执行时,用户不得不等待漫长的上下文轮次;而当其中某个子任务出错时,整个对话的上下文都会被错误信息污染。Claude Code 的多 Agent 协调系统正是为了突破这一瓶颈而设计的------它将一个主 Agent 转变为"协调器"(Coordinator),由协调器将任务分派给多个 Worker Agent 并行执行,最终汇聚结果。
这套系统的设计哲学可以用三个关键词来概括:并行化 、专业化 、隔离化。并行化让独立的调研任务同时运行;专业化让每个 Worker 专注于自己擅长的领域;隔离化则确保 Worker 之间的上下文不会互相干扰,一个 Worker 的失败不会拖垮整个系统。
本章将从源码层面深入剖析 Claude Code 的多 Agent 协调架构,涵盖协调器模式的激活机制、Worker Agent 的生成与约束、任务类型体系、通信机制、以及并行执行与结果聚合的完整流程。
:::tip 本章要点
- 协调器模式 :理解
coordinatorMode.ts如何将主 Agent 转变为纯粹的任务编排者,以及协调器的系统提示词设计 - Worker 生成 :
AgentTool如何根据不同场景生成同步或异步 Worker,Worker 的工具子集约束与递归防护 - 任务类型体系 :
LocalShellTask、LocalAgentTask、InProcessTeammateTask、RemoteAgentTask四种任务类型的使用场景与生命周期 - 通信机制 :
SendMessageTool的三种消息路由路径------进程内队列、文件信箱系统、跨会话桥接 - Scratchpad 隔离:Worker 之间如何通过 Scratchpad 目录实现持久化的知识共享
- 并行执行 :Worker 独立运行查询循环、结果通过
<task-notification>XML 格式汇聚回协调器 :::
14.1 为什么需要多 Agent
14.1.1 单 Agent 的天然局限
在传统的单 Agent 模式下,Claude Code 的所有操作都在一条对话流中串行执行。用户提出需求后,Agent 依次读取文件、分析代码、执行修改、运行测试,每一步都必须等待上一步完成。这种模式存在三个根本性问题。
上下文膨胀。一个复杂任务可能涉及数十个文件的读取和修改,每次工具调用的输入输出都会累积在对话上下文中。当上下文接近模型的窗口限制时,早期的关键信息可能被截断或压缩,导致 Agent "遗忘"已经获取的信息。
错误传播。当 Agent 在执行链的中间环节遇到错误------比如一个测试失败------错误信息和修复尝试会污染后续所有操作的上下文。Agent 可能在错误的修复方向上越陷越深,因为它的全部注意力都被前面的失败尝试所占据。
时间效率低下。调研代码库结构和运行测试验证是两个完全独立的任务,没有任何理由必须串行执行。在单 Agent 模式下,用户不得不等待所有步骤依次完成,即使其中许多步骤本可以同时进行。
14.1.2 多 Agent 的三大优势
Claude Code 的多 Agent 架构从根本上解决了上述问题,其优势体现在三个维度:
并行执行带来的时间收益。协调器可以同时启动多个 Worker 进行独立调研,比如一个 Worker 分析认证模块的源码结构,另一个 Worker 查找相关的测试文件和测试覆盖情况,第三个 Worker 检索项目的 Git 历史以了解相关变更的上下文。这些调研任务之间没有数据依赖,完全可以同时进行。正如源码中协调器系统提示词所强调的:
"Parallelism is your superpower. Workers are async. Launch independent workers concurrently whenever possible -- don't serialize work that can run simultaneously."
在实践中,一个需要调研五个不同模块的任务,在单 Agent 模式下可能需要五轮串行的文件读取和分析;在多 Agent 模式下,五个 Worker 可以同时出发,总耗时接近于最慢的那一个 Worker 的执行时间。
专业化与隔离带来的质量收益。每个 Worker 拥有独立的对话上下文,互不干扰。一个 Worker 在调试过程中产生的大量错误日志和修复尝试,不会污染另一个 Worker 正在进行的代码分析。更重要的是,Worker 的工具集被严格约束------它们不能生成子 Agent,不能访问协调器专有的管理工具,这种约束确保了系统不会出现无限递归。每个 Worker 的上下文都是"干净的",只包含与其任务直接相关的信息,这大大提高了 Worker 对其特定任务的专注度和完成质量。
综合决策带来的准确性收益。协调器可以汇集多个 Worker 的调研结果,从全局视角做出判断。当一个 Worker 报告了 bug 的具体位置和类型特征,另一个 Worker 报告了测试覆盖情况和缺失的边界测试用例,第三个 Worker 报告了相关模块的依赖关系,协调器能够综合这些来自不同视角的信息,制定出完整而精确的修复方案,然后再派发给实现 Worker。这种"分散调研、集中决策"的模式,比单 Agent 的"边调研边决策"模式更不容易遗漏关键信息。
14.2 协调器模式
下图展示了 Coordinator 模式下多 Agent 协作的完整架构,从任务分派到结果聚合的全流程:
14.2.1 模式激活与切换
协调器模式的激活逻辑定义在 src/coordinator/coordinatorMode.ts 中。整个机制基于环境变量和特性标志的组合判断:
typescript
// 源码文件:src/coordinator/coordinatorMode.ts
export function isCoordinatorMode(): boolean {
if (feature('COORDINATOR_MODE')) {
return isEnvTruthy(process.env.CLAUDE_CODE_COORDINATOR_MODE)
}
return false
}
这里有两层控制,形成了一个"编译时+运行时"的双重开关机制。第一层 feature('COORDINATOR_MODE') 是编译时特性标志,由 Bun 的 bundle 系统在构建阶段决定是否包含协调器相关代码。如果特性未启用,整个协调器分支在打包时就会被死代码消除,这意味着发布给普通用户的二进制文件中根本不包含协调器的任何代码,既减小了包体积,也避免了意外激活实验性功能的可能。第二层 CLAUDE_CODE_COORDINATOR_MODE 环境变量是运行时开关,允许在不重新构建的情况下启用或禁用协调器模式,适合在灰度测试期间按用户或按环境逐步开放。
当恢复一个已有的会话时,matchSessionMode 函数确保当前模式与会话存储的模式一致:
typescript
// 源码文件:src/coordinator/coordinatorMode.ts
export function matchSessionMode(
sessionMode: 'coordinator' | 'normal' | undefined,
): string | undefined {
const currentIsCoordinator = isCoordinatorMode()
const sessionIsCoordinator = sessionMode === 'coordinator'
if (currentIsCoordinator === sessionIsCoordinator) {
return undefined
}
// 翻转环境变量以匹配会话模式
if (sessionIsCoordinator) {
process.env.CLAUDE_CODE_COORDINATOR_MODE = '1'
} else {
delete process.env.CLAUDE_CODE_COORDINATOR_MODE
}
// ...
}
这个设计解决了一个实际问题:用户可能在协调器模式下创建了一个会话,然后在普通模式下恢复它。如果不进行模式匹配,恢复的会话中包含的 Worker 通知消息会让普通模式的 Agent 困惑不已。
14.2.2 协调器的系统提示词
当协调器模式激活后,getCoordinatorSystemPrompt() 函数会返回一份精心设计的系统提示词,完全替代普通 Agent 的系统提示词。这份提示词是整个多 Agent 系统的"灵魂"------它定义了协调器的角色、可用工具、工作流程和交互规范:
typescript
// 源码文件:src/coordinator/coordinatorMode.ts
export function getCoordinatorSystemPrompt(): string {
return `You are Claude Code, an AI assistant that orchestrates
software engineering tasks across multiple workers.
## 1. Your Role
You are a **coordinator**. Your job is to:
- Help the user achieve their goal
- Direct workers to research, implement and verify code changes
- Synthesize results and communicate with the user
- Answer questions directly when possible
## 2. Your Tools
- **Agent** - Spawn a new worker
- **SendMessage** - Continue an existing worker
- **TaskStop** - Stop a running worker
...`
}
提示词的设计体现了几个关键的架构决策。
严格的角色分离。协调器被明确告知"Every message you send is to the user",Worker 的结果是"internal signals, not conversation partners"。这避免了协调器错误地将 Worker 当作对话伙伴来"感谢"或"确认"。
任务阶段化。提示词定义了四个标准阶段:Research(调研)、Synthesis(综合)、Implementation(实现)、Verification(验证),并明确了哪些阶段由 Worker 执行、哪些由协调器自己完成。特别强调 Synthesis 阶段"是协调器最重要的工作"------协调器必须理解调研结果后再制定具体方案,不能懒惰地把理解工作推给 Worker。
并发管理规则。提示词明确规定了并发策略:只读任务(调研)可以自由并行;写操作(实现)同一组文件一次只能一个 Worker;验证可以与不同文件区域的实现并行。这些规则防止多个 Worker 同时修改同一个文件导致冲突。
14.2.3 Worker 工具上下文注入
协调器模式还通过 getCoordinatorUserContext 向用户上下文中注入 Worker 可用工具的信息,让协调器了解 Worker 的能力范围:
typescript
// 源码文件:src/coordinator/coordinatorMode.ts
export function getCoordinatorUserContext(
mcpClients: ReadonlyArray<{ name: string }>,
scratchpadDir?: string,
): { [k: string]: string } {
if (!isCoordinatorMode()) {
return {}
}
const workerTools = isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)
? [BASH_TOOL_NAME, FILE_READ_TOOL_NAME, FILE_EDIT_TOOL_NAME]
.sort().join(', ')
: Array.from(ASYNC_AGENT_ALLOWED_TOOLS)
.filter(name => !INTERNAL_WORKER_TOOLS.has(name))
.sort().join(', ')
// ...
}
这里可以看到两种截然不同的工具配置模式。CLAUDE_CODE_SIMPLE 模式下,Worker 只有最基本的 Bash、Read、Edit 三个工具------这是一种极简配置,适用于对安全性和可预测性要求极高的场景,Worker 只能执行最原始的文件操作和命令执行。正常模式下,Worker 拥有 ASYNC_AGENT_ALLOWED_TOOLS 集合中除内部管理工具外的所有工具,包括文件搜索、代码搜索、网络检索、笔记本编辑等丰富的能力。
被排除的 INTERNAL_WORKER_TOOLS 是一个值得特别关注的集合。它包括 TeamCreate(创建团队)、TeamDelete(删除团队)、SendMessage(发送消息)和 SyntheticOutput(结构化输出)四种工具。这些工具是 Swarm 团队管理和结构化输出的内部机制,不应暴露给普通 Worker。协调器在告诉 Worker 自己可用的工具时,会将这些内部工具从列表中过滤掉,确保 Worker 不会尝试调用它们。
如果存在 Scratchpad 目录,还会注入 Scratchpad 的路径信息:
typescript
if (scratchpadDir && isScratchpadGateEnabled()) {
content += `\n\nScratchpad directory: ${scratchpadDir}
Workers can read and write here without permission prompts.
Use this for durable cross-worker knowledge.`
}
这为 Worker 之间提供了一种持久化的知识共享通道------任何 Worker 都可以在 Scratchpad 目录中留下文件,供后续的 Worker 读取。
14.3 Worker Agent 生成
14.3.1 AgentTool 的路由逻辑
Worker 的生成由 AgentTool(src/tools/AgentTool/AgentTool.tsx)负责,这是整个多 Agent 系统最核心的工具。它的 call 方法包含了复杂的路由逻辑,根据输入参数的组合决定走哪条路径:
typescript
// 源码文件:src/tools/AgentTool/AgentTool.tsx
async call({
prompt, subagent_type, description, model: modelParam,
run_in_background, name, team_name, mode: spawnMode,
isolation, cwd
}: AgentToolInput, toolUseContext, canUseTool, ...) {
// 路径1:团队 Teammate 生成(name + team_name 都存在)
if (teamName && name) {
const result = await spawnTeammate({ name, prompt, ... });
return { data: spawnResult };
}
// 路径2:Fork 子代理(特性标志启用时)
const isForkPath = effectiveType === undefined;
if (isForkPath) {
selectedAgent = FORK_AGENT;
}
// 路径3:常规 Agent 生成
// 异步或同步取决于多个条件的组合
const shouldRunAsync = (run_in_background === true
|| selectedAgent.background === true
|| isCoordinator || forceAsync || assistantForceAsync)
&& !isBackgroundTasksDisabled;
}
三条路径对应三种截然不同的使用场景,每种路径在隔离级别、上下文继承和能力范围上都有本质区别:
- Teammate 路径 :当
name和team_name参数同时存在时,生成一个具名的团队成员。团队成员拥有持久的身份标识(格式为name@team_name),可以通过 SendMessage 互相通信,支持空闲等待和持续任务认领。这是 Swarm 模式的核心路径,生成的 Teammate 可以在 tmux 分屏或进程内方式运行,并注册到团队文件(Team File)中供其他成员发现。 - Fork 路径 :当 fork 特性启用且未指定
subagent_type时,创建一个继承父 Agent 完整上下文的分支 Worker。Fork Worker 的特殊之处在于它使用父 Agent 的系统提示词和工具集,并且通过buildForkedMessages()复制父 Agent 的完整对话历史作为初始上下文。这种设计确保了 Fork Worker 与父 Agent 共享 prompt cache,大幅降低了 API 成本。代价是 Fork Worker 携带了父 Agent 的全部上下文"包袱",不如独立 Worker 那样"轻装上阵"。 - 常规 Agent 路径 :最通用的路径,根据
subagent_type选择预定义的 Agent 定义(如worker、researcher等),构建独立的系统提示词和工具集。这种 Worker 从零开始,只接收协调器给定的 prompt 作为输入,具有最清晰的上下文边界。在协调器模式下,这是最常用的路径。
一个重要的安全防护是递归检测。系统通过两重机制防止 Fork Worker 无限递归地生成子 Fork:一是检查 querySource 是否标记为 fork agent 类型,二是扫描上下文消息中是否包含 fork 标记。如果检测到递归,直接抛出错误:"Fork is not available inside a forked worker."
14.3.2 异步 Agent 的生命周期
在协调器模式下,几乎所有 Worker 都走异步路径。异步 Agent 的生命周期管理是一个精密的流程:
typescript
// 源码文件:src/tools/AgentTool/AgentTool.tsx
if (shouldRunAsync) {
const asyncAgentId = earlyAgentId;
// 步骤1:注册后台任务
const agentBackgroundTask = registerAsyncAgent({
agentId: asyncAgentId,
description,
prompt,
selectedAgent,
setAppState: rootSetAppState,
toolUseId: toolUseContext.toolUseId
});
// 步骤2:注册名称到 agentId 的映射(用于 SendMessage 路由)
if (name) {
rootSetAppState(prev => {
const next = new Map(prev.agentNameRegistry);
next.set(name, asAgentId(asyncAgentId));
return { ...prev, agentNameRegistry: next };
});
}
// 步骤3:后台启动 Agent 执行循环(fire-and-forget)
void runWithAgentContext(asyncAgentContext, () =>
wrapWithCwd(() => runAsyncAgentLifecycle({
taskId: agentBackgroundTask.agentId,
abortController: agentBackgroundTask.abortController!,
makeStream: onCacheSafeParams => runAgent({
...runAgentParams,
override: { agentId: ..., abortController: ... },
onCacheSafeParams
}),
metadata, description, toolUseContext,
rootSetAppState,
enableSummarization: isCoordinator || isForkSubagentEnabled(),
getWorktreeResult: cleanupWorktreeIfNeeded
}))
);
// 步骤4:立即返回任务ID,不等待完成
return {
data: {
status: 'async_launched',
agentId: agentBackgroundTask.agentId,
description, prompt,
outputFile: getTaskOutputPath(agentBackgroundTask.agentId),
}
};
}
这里的关键设计决策是 void 关键字------runAsyncAgentLifecycle 被 fire-and-forget 地启动,AgentTool 的 call 方法立即返回。这意味着协调器可以在单次响应中并行启动多个 Worker,而不必等待任何一个完成。Agent 完成后,会通过 <task-notification> XML 格式将结果注入协调器的用户消息流,触发协调器的下一轮响应。
14.3.3 Worker 的工具约束
Worker 并非拥有与主 Agent 相同的工具集。src/constants/tools.ts 中定义了一套严格的工具访问控制:
typescript
// 源码文件:src/constants/tools.ts
// 所有 Agent 都不能使用的工具
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,
])
// 异步 Agent 允许使用的工具(白名单模式)
export const ASYNC_AGENT_ALLOWED_TOOLS = new Set([
FILE_READ_TOOL_NAME,
GREP_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,
WEB_SEARCH_TOOL_NAME,
WEB_FETCH_TOOL_NAME,
TODO_WRITE_TOOL_NAME,
ENTER_WORKTREE_TOOL_NAME,
EXIT_WORKTREE_TOOL_NAME,
])
这套约束遵循"最小权限"原则。Worker 能读取文件、搜索代码、执行 Shell 命令、编辑文件------这些是完成实际工作所必需的。但 Worker 不能:
- 生成子 Agent (
AGENT_TOOL_NAME被禁止):防止 Worker 递归生成更多的 Worker,导致资源爆炸 - 向用户提问 (
ASK_USER_QUESTION_TOOL_NAME被禁止):Worker 应该自主完成任务,不能中断用户 - 停止其他任务 (
TASK_STOP_TOOL_NAME被禁止):只有协调器有权管理 Worker 的生命周期 - 切换权限模式(Plan Mode 相关工具被禁止):权限管理是主线程的职责
工具过滤的具体实现在 agentToolUtils.ts 的 filterToolsForAgent 函数中:
typescript
// 源码文件:src/tools/AgentTool/agentToolUtils.ts
export function filterToolsForAgent({
tools, isBuiltIn, isAsync = false, permissionMode,
}: { ... }): Tools {
return tools.filter(tool => {
// MCP 工具始终允许
if (tool.name.startsWith('mcp__')) return true
// Plan 模式下允许 ExitPlanMode
if (toolMatchesName(tool, EXIT_PLAN_MODE_V2_TOOL_NAME)
&& permissionMode === 'plan') return true
// 全局禁止列表
if (ALL_AGENT_DISALLOWED_TOOLS.has(tool.name)) return false
// 自定义 Agent 额外禁止列表
if (!isBuiltIn && CUSTOM_AGENT_DISALLOWED_TOOLS.has(tool.name))
return false
// 异步 Agent 白名单模式
if (isAsync && !ASYNC_AGENT_ALLOWED_TOOLS.has(tool.name)) {
// 例外:进程内 Teammate 可以使用 AgentTool 生成同步子 Agent
if (isAgentSwarmsEnabled() && isInProcessTeammate()) {
if (toolMatchesName(tool, AGENT_TOOL_NAME)) return true
if (IN_PROCESS_TEAMMATE_ALLOWED_TOOLS.has(tool.name)) return true
}
return false
}
return true
})
}
值得注意的是 InProcess Teammate 的特殊处理:它虽然以异步方式运行,但被允许使用 AgentTool 生成同步子 Agent、以及使用 SendMessageTool 等团队通信工具。这是因为 Teammate 运行在与 Leader 相同的进程中,其生命周期受到更紧密的管控。
14.3.4 Scratchpad 隔离与知识共享
Worker 之间需要一种机制来共享中间结果,但又不能直接访问彼此的对话上下文。Scratchpad 目录提供了解决方案。
Scratchpad 是一个每会话的临时目录,路径格式为 /tmp/claude-{uid}/{sanitized-cwd}/{sessionId}/scratchpad/。在这个目录下的文件读写不需要权限提示,任何 Worker 都可以自由使用:
typescript
// 源码文件:src/utils/permissions/filesystem.ts
export function getScratchpadDir(): string {
return join(getProjectTempDir(), getSessionId(), 'scratchpad')
}
// 检查路径是否在 scratchpad 目录内
function isScratchpadPath(absolutePath: string): boolean {
if (!isScratchpadEnabled()) return false
const scratchpadDir = getScratchpadDir()
const normalizedPath = normalize(absolutePath)
return (
normalizedPath === scratchpadDir ||
normalizedPath.startsWith(scratchpadDir + sep)
)
}
安全方面,代码对路径进行了 normalize 处理以防止路径穿越攻击(如 scratchpad/../../../etc/passwd)。Scratchpad 的典型使用场景包括:一个调研 Worker 将分析结果写入 Scratchpad 文件,实现 Worker 从中读取具体的文件路径和行号信息。
14.4 任务类型体系
Claude Code 定义了四种任务类型,覆盖从简单命令执行到远程 Agent 协作的完整场景。下图展示了各任务类型的特征与适用范围:
Claude Code 的任务系统定义在 src/tasks/ 目录下,所有任务类型通过一个联合类型统一管理:
typescript
// 源码文件:src/tasks/types.ts
export type TaskState =
| LocalShellTaskState
| LocalAgentTaskState
| RemoteAgentTaskState
| InProcessTeammateTaskState
| LocalWorkflowTaskState
| MonitorMcpTaskState
| DreamTaskState
与多 Agent 协调直接相关的有四种核心任务类型,每种对应不同的执行模式和隔离级别。
14.4.1 LocalShellTask -- Shell 命令任务
LocalShellTask(src/tasks/LocalShellTask/)是最基础的任务类型,代表一个后台运行的 Shell 命令。其状态定义为:
typescript
// 源码文件:src/tasks/LocalShellTask/guards.ts
export type LocalShellTaskState = TaskStateBase & {
type: 'local_bash'
command: string
result?: { code: number; interrupted: boolean }
shellCommand: ShellCommand | null
isBackgrounded: boolean
agentId?: AgentId // 生成此任务的 Agent ID
}
agentId 字段建立了任务与 Agent 之间的从属关系,这在多 Agent 场景中尤为重要。当一个 Worker Agent 退出时(无论是正常完成还是被终止),系统会通过 killShellTasksForAgent 函数遍历所有活跃的 Shell 任务,清理属于该 Agent 的所有后台命令,防止孤儿进程持续占用系统资源。isBackgrounded 字段标记该任务是否已被推入后台------前台任务会阻塞 Agent 的执行流等待完成,后台任务则不会。Shell 任务不涉及 LLM 调用,主要用于运行测试套件、编译项目、执行 linter 检查等确定性操作,是 Worker 执行实际工程任务的基础设施。
14.4.2 LocalAgentTask -- 本地异步 Agent 任务
LocalAgentTask(src/tasks/LocalAgentTask/LocalAgentTask.tsx)是协调器模式下最常用的任务类型。它代表一个在本地运行的异步 Agent Worker,拥有独立的 LLM 对话循环。
typescript
// 源码文件:src/tasks/LocalAgentTask/LocalAgentTask.tsx
export type AgentProgress = {
toolUseCount: number
tokenCount: number
lastActivity?: ToolActivity
recentActivities?: ToolActivity[]
summary?: string
}
LocalAgentTask 的进度追踪系统是其核心特性。ProgressTracker 分别追踪输入 token(累计值,取最新)和输出 token(增量值,持续累加),因为 Claude API 的 input_tokens 在每轮请求中包含了所有之前的上下文,而 output_tokens 是每轮独立的。
任务完成后,enqueueAgentNotification 会将结果打包为 <task-notification> XML,注入协调器的消息流。这是协调器感知 Worker 完成的唯一通道。
14.4.3 InProcessTeammateTask -- 进程内队友任务
InProcessTeammateTask(src/tasks/InProcessTeammateTask/)是 Swarm 模式下的核心任务类型。与 LocalAgentTask 不同,InProcess Teammate 运行在与 Leader 相同的 Node.js 进程中,使用 AsyncLocalStorage 实现上下文隔离。
typescript
// 源码文件:src/tasks/InProcessTeammateTask/types.ts
export type TeammateIdentity = {
agentId: string // 例如 "researcher@my-team"
agentName: string // 例如 "researcher"
teamName: string
color?: string
planModeRequired: boolean
parentSessionId: string
}
export type InProcessTeammateTaskState = TaskStateBase & {
type: 'in_process_teammate'
identity: TeammateIdentity
prompt: string
model?: string
selectedAgent?: AgentDefinition
abortController?: AbortController
currentWorkAbortController?: AbortController
awaitingPlanApproval: boolean
permissionMode: PermissionMode
messages?: Message[]
pendingUserMessages: string[]
isIdle: boolean
shutdownRequested: boolean
lastReportedToolCount: number
lastReportedTokenCount: number
}
这个类型定义揭示了 InProcess Teammate 的几个独特特性:
- 双层 AbortController :
abortController终止整个 Teammate 的生命周期,currentWorkAbortController只中止当前正在执行的工作轮次。这允许 Leader 在不杀死 Teammate 的情况下打断它正在做的事情。 - Plan 模式审批 :
awaitingPlanApproval和planModeRequired支持一种"先计划后执行"的工作流------Teammate 必须先提交方案,得到 Leader 审批后才能开始实施。 - 空闲/工作状态 :
isIdle标志让 Teammate 在完成当前任务后不会退出,而是进入空闲状态等待下一个任务,避免了频繁创建和销毁 Teammate 的开销。 - 消息上限管理 :
messages数组有上限(TEAMMATE_MESSAGES_UI_CAP = 50),因为实际的分析(2026-03-20)显示在 500+ 轮对话中每个 Agent 消耗约 20MB RSS,一个极端案例中 292 个 Agent 导致了 36.8GB 的内存占用。
14.4.4 RemoteAgentTask -- 远程 Agent 任务
RemoteAgentTask(src/tasks/RemoteAgentTask/RemoteAgentTask.tsx)代表在远程 Claude Code Runtime (CCR) 环境中运行的 Agent。这是隔离级别最高的任务类型:
typescript
// 源码文件:src/tasks/RemoteAgentTask/RemoteAgentTask.tsx
export type RemoteAgentTaskState = TaskStateBase & {
type: 'remote_agent'
remoteTaskType: RemoteTaskType
sessionId: string
command: string
title: string
todoList: TodoList
log: SDKMessage[]
pollStartedAt: number
}
Remote Agent 通过 teleportToRemote 函数将代码库快照和执行上下文打包发送到远程环境执行。本地端只保留一个轮询器,以固定间隔检查远程会话的状态和输出。pollStartedAt 字段记录轮询开始的时间,用于超时计算------当恢复一个之前创建的远程任务时,超时起点从恢复时刻开始计算,而非任务的原始创建时间,避免已经创建了很长时间的任务被立即超时。
远程任务类型通过 RemoteTaskType 枚举定义,包括:remote-agent(通用远程 Agent,提供最高级别的执行环境隔离)、ultraplan(大规模计划执行,支持多阶段审批)、ultrareview(深度代码评审,带有进度跟踪的 bug 发现/验证/反驳流程)、autofix-pr(PR 自动修复,监控特定 PR 并自动应用修复)和 background-pr(后台 PR 处理)。这些任务类型共享同一套轮询和通知基础设施,但各自拥有特定的完成判定逻辑,通过 RemoteTaskCompletionChecker 接口实现可插拔的完成检测。
下面的架构图展示了四种任务类型的层次关系:
arduino
┌─────────────────────┐
│ Coordinator │
│ (主 Agent / 协调器) │
└─────────┬───────────┘
│
┌──────────────────┼──────────────────┐
│ │ │
┌──────▼──────┐ ┌──────▼──────┐ ┌───────▼───────┐
│ LocalAgent │ │ InProcess │ │ RemoteAgent │
│ Task │ │ Teammate │ │ Task │
│ │ │ Task │ │ │
│ 独立进程 │ │ 同进程/ALS │ │ 远程 CCR │
│ 后台运行 │ │ 隔离 │ │ 环境 │
└──────┬──────┘ └──────┬──────┘ └───────────────┘
│ │
┌──────▼──────┐ ┌──────▼──────┐
│ LocalShell │ │ LocalShell │
│ Task │ │ Task │
│ (测试/编译) │ │ (测试/编译) │
└─────────────┘ └─────────────┘
14.5 通信机制
多 Agent 之间的消息路由根据收件人类型选择不同的通信通道。下图展示了 SendMessageTool 的三种路由路径:
14.5.1 SendMessageTool 概览
SendMessageTool(src/tools/SendMessageTool/SendMessageTool.ts)是多 Agent 系统中的消息路由枢纽。它根据收件人地址的不同格式,将消息路由到三种完全不同的通信通道:
typescript
// 源码文件:src/tools/SendMessageTool/SendMessageTool.ts
const inputSchema = lazySchema(() =>
z.object({
to: z.string().describe(
'Recipient: teammate name, "*" for broadcast, ...'
),
summary: z.string().optional().describe(
'A 5-10 word summary shown as preview in the UI'
),
message: z.union([
z.string().describe('Plain text message content'),
StructuredMessage(),
]),
}),
)
消息可以是纯文本字符串,也可以是结构化消息。结构化消息支持三种类型:
typescript
const StructuredMessage = lazySchema(() =>
z.discriminatedUnion('type', [
z.object({ type: z.literal('shutdown_request'), reason: z.string().optional() }),
z.object({ type: z.literal('shutdown_response'), request_id: z.string(), approve: semanticBoolean(), reason: z.string().optional() }),
z.object({ type: z.literal('plan_approval_response'), request_id: z.string(), approve: semanticBoolean(), feedback: z.string().optional() }),
]),
)
这三种结构化消息分别用于关闭请求、关闭响应和计划审批------都是 Swarm 团队管理的核心协议。
14.5.2 进程内消息路由
当协调器向一个 LocalAgentTask Worker 发送消息时,SendMessageTool 的 call 方法首先尝试进程内路由:
typescript
// 源码文件:src/tools/SendMessageTool/SendMessageTool.ts
async call(input, context, canUseTool, assistantMessage) {
// 进程内路由:通过名称注册表或 agentId 查找
if (typeof input.message === 'string' && input.to !== '*') {
const appState = context.getAppState()
const registered = appState.agentNameRegistry.get(input.to)
const agentId = registered ?? toAgentId(input.to)
if (agentId) {
const task = appState.tasks[agentId]
if (isLocalAgentTask(task) && !isMainSessionTask(task)) {
if (task.status === 'running') {
// 正在运行:将消息加入待处理队列
queuePendingMessage(agentId, input.message, ...)
return { data: { success: true, message: `Message queued...` } }
}
// 已停止:自动恢复 Agent
const result = await resumeAgentBackground({
agentId, prompt: input.message, ...
})
return { data: { success: true, message: `Agent resumed...` } }
}
}
}
// ...
}
进程内路由的流程如下:
- 名称解析 :先从
agentNameRegistry(一个 Map<string, AgentId>)查找名称对应的 Agent ID - 状态检查 :如果 Agent 正在运行,将消息加入其待处理队列(
queuePendingMessage),Agent 在下一轮工具调用间隙会检查这个队列 - 自动恢复 :如果 Agent 已停止(completed/failed/killed),通过
resumeAgentBackground自动恢复它,以利用已有的对话上下文
这个自动恢复机制是协调器系统提示词中"Continue workers whose work is complete via SendMessage"指令的底层实现。
14.5.3 文件信箱系统
对于 Swarm 模式下的 Teammate(无论是 tmux 进程还是 InProcess),消息通过基于文件的信箱系统传递。信箱的实现在 src/utils/teammateMailbox.ts 中:
typescript
// 源码文件:src/utils/teammateMailbox.ts
export type TeammateMessage = {
from: string
text: string
timestamp: string
read: boolean
color?: string
summary?: string
}
export function getInboxPath(agentName: string, teamName?: string): string {
const team = teamName || getTeamName() || 'default'
const safeTeam = sanitizePathComponent(team)
const safeAgentName = sanitizePathComponent(agentName)
return join(getTeamsDir(), safeTeam, 'inboxes', `${safeAgentName}.json`)
}
每个 Teammate 在 ~/.claude/teams/{team_name}/inboxes/ 下有一个 JSON 文件作为信箱。写入操作使用文件锁(lockfile 模块)保护并发安全:
typescript
const LOCK_OPTIONS = {
retries: {
retries: 10,
minTimeout: 5,
maxTimeout: 100,
},
}
信箱支持三种消息模式:
- 单播 (
handleMessage):向特定 Teammate 的信箱写入消息 - 广播 (
handleBroadcast):向团队中除发送者外的所有成员信箱写入消息(to: "*") - 结构化消息:用于关闭请求/响应和计划审批等管理协议
广播消息的实现展示了 Swarm 团队通信的核心逻辑:
typescript
// 源码文件:src/tools/SendMessageTool/SendMessageTool.ts
async function handleBroadcast(
content: string, summary: string | undefined, context: ToolUseContext,
): Promise<{ data: BroadcastOutput }> {
const teamFile = await readTeamFileAsync(teamName)
const recipients: string[] = []
for (const member of teamFile.members) {
if (member.name.toLowerCase() === senderName.toLowerCase()) continue
recipients.push(member.name)
}
for (const recipientName of recipients) {
await writeToMailbox(recipientName, {
from: senderName, text: content, summary,
timestamp: new Date().toISOString(), color: senderColor,
}, teamName)
}
// ...
}
14.5.4 InProcess Teammate 的消息接收
InProcess Teammate 通过轮询机制接收消息。inProcessRunner.ts 中的 waitForNextPromptOrShutdown 函数实现了一个 500ms 间隔的轮询循环:
typescript
// 源码文件:src/utils/swarm/inProcessRunner.ts
async function waitForNextPromptOrShutdown(
identity: TeammateIdentity,
abortController: AbortController,
taskId: string, getAppState, setAppState, taskListId,
): Promise<WaitResult> {
while (!abortController.signal.aborted) {
// 1. 先检查进程内内存队列(来自用户界面的直接注入)
const task = appState.tasks[taskId]
if (task?.type === 'in_process_teammate'
&& task.pendingUserMessages.length > 0) {
const message = task.pendingUserMessages[0]!
// 弹出队列中的消息
setAppState(prev => ({ ...prev, tasks: {
...prev.tasks, [taskId]: {
...prevTask,
pendingUserMessages: prevTask.pendingUserMessages.slice(1),
}
}}))
return { type: 'new_message', message, from: 'user' }
}
await sleep(500)
// 2. 检查文件信箱
const allMessages = await readMailbox(identity.agentName, identity.teamName)
// 优先处理关闭请求
for (let i = 0; i < allMessages.length; i++) {
const parsed = isShutdownRequest(m.text)
if (parsed) {
return { type: 'shutdown_request', request: parsed, ... }
}
}
// 再处理普通消息
// ...
}
}
消息接收的优先级设计很有讲究:关闭请求优先于普通消息。这是因为在高流量的 Swarm 中,Teammate 的信箱可能堆积大量未读消息。如果不优先处理关闭请求,Teammate 可能在被要求关闭后还继续消费大量消息并执行不必要的工作。
14.5.5 权限代理机制
InProcess Teammate 在执行需要权限确认的工具时(如执行 Shell 命令),面临一个特殊挑战:它没有自己的终端界面来显示权限提示。解决方案是通过 Leader 的 UI 代理权限确认:
typescript
// 源码文件:src/utils/swarm/inProcessRunner.ts
function createInProcessCanUseTool(
identity: TeammateIdentity,
abortController: AbortController,
): CanUseToolFn {
return async (tool, input, toolUseContext, ...) => {
const result = await hasPermissionsToUseTool(tool, input, ...)
if (result.behavior !== 'ask') return result
// 先尝试 Bash 分类器自动审批
if (feature('BASH_CLASSIFIER') && tool.name === BASH_TOOL_NAME
&& result.pendingClassifierCheck) {
const classifierDecision = await awaitClassifierAutoApproval(...)
if (classifierDecision) return { behavior: 'allow', ... }
}
// 使用 Leader 的 ToolUseConfirm 对话框
const setToolUseConfirmQueue = getLeaderToolUseConfirmQueue()
if (setToolUseConfirmQueue) {
return new Promise<PermissionDecision>(resolve => {
setToolUseConfirmQueue(queue => [...queue, {
tool, description, input, toolUseContext,
workerBadge: identity.color
? { name: identity.agentName, color: identity.color }
: undefined,
onAllow(...) { resolve({ behavior: 'allow', ... }) },
onReject(...) { resolve({ behavior: 'ask', message: ... }) },
}])
})
}
// 降级:使用信箱系统
// ...
}
}
权限代理有两条路径。主路径直接将权限请求注入 Leader 的 ToolUseConfirm 队列,请求会显示一个带 Worker 标识(名称和颜色)的权限对话框。降级路径通过信箱系统异步传递权限请求和响应,适用于 tmux 进程等没有共享 UI 队列的场景。
14.6 并行执行与结果聚合
14.6.1 Worker 独立运行查询循环
每个 Worker Agent 都运行自己独立的查询循环(query loop)。无论是 LocalAgentTask 还是 InProcessTeammateTask,核心执行逻辑都通过 runAgent 函数驱动:
typescript
// 源码文件:src/tools/AgentTool/runAgent.ts
export async function* runAgent({
agentDefinition, promptMessages, toolUseContext,
canUseTool, isAsync, querySource, model,
availableTools, ...
}): AsyncGenerator<Message, void> {
const agentId = override?.agentId ? override.agentId : createAgentId()
// 构建子代理上下文:独立的文件状态缓存、内容替换状态
// 这确保了 Worker 的文件读取结果不会污染主线程的缓存
const subagentContext = createSubagentContext(toolUseContext, ...)
// 运行查询循环
for await (const message of query({
systemPrompt, userContext, systemContext,
messages, tools: resolvedTools,
model: resolvedAgentModel,
abortController: effectiveAbortController,
...
})) {
if (isRecordableMessage(message)) {
yield message
}
}
}
runAgent 是一个 AsyncGenerator,它将 query() 函数产出的消息逐条向上传递。每条消息都会被记录到磁盘上的 sidechain transcript 中,用于调试和会话恢复。
Worker 的查询循环与主 Agent 的查询循环完全独立------它有自己的消息历史、自己的系统提示词、自己的工具集、自己的 AbortController。这种完全隔离确保了一个 Worker 的失败不会影响其他 Worker 或协调器。
14.6.2 结果通知与聚合
异步 Agent 完成后,runAsyncAgentLifecycle(在 agentToolUtils.ts 中)负责将结果打包为 <task-notification> 格式并注入协调器的消息流。通知的 XML 格式在协调器的系统提示词中有详细定义:
xml
<task-notification>
<task-id>{agentId}</task-id>
<status>completed|failed|killed</status>
<summary>{human-readable status summary}</summary>
<result>{agent's final text response}</result>
<usage>
<total_tokens>N</total_tokens>
<tool_uses>N</tool_uses>
<duration_ms>N</duration_ms>
</usage>
</task-notification>
通知被注入为 user-role 消息,这是一个巧妙的设计。在 Claude 的对话模型中,消息必须遵循 user/assistant 严格交替的格式。将 Worker 结果包装为 user 消息,让协调器能够在不修改消息处理逻辑的情况下接收 Worker 结果------从协调器的视角来看,这些通知与用户输入出现在同一个消息流中,只是内容格式不同。协调器通过 <task-notification> 开头标签来区分真实的用户输入和 Worker 通知。
当多个 Worker 并行运行时,它们的完成通知可能在任意时刻到达,形成一种异步的事件驱动模式。协调器的每一轮响应都会处理自上次响应以来累积的所有通知,然后根据全局状态决定下一步行动:
- 如果所有调研 Worker 都完成了,协调器综合各方结果,理解问题全貌,然后制定具体的实现方案
- 如果实现 Worker 完成了,协调器检查实现结果,决定是否需要启动验证 Worker 来运行测试和类型检查
- 如果某个 Worker 失败了,协调器分析失败原因,通过
SendMessage继续该 Worker(让它利用已有的错误上下文进行修复),或者在判断方向完全错误时启动一个全新的 Worker - 如果用户在 Worker 执行过程中发来了新的消息或要求变更,协调器可以通过
TaskStop停止偏离方向的 Worker,然后用新的指令重新调度
这种模式让协调器始终处于决策中心的位置,所有信息向它汇聚,所有行动从它发出。
14.6.3 进度追踪与摘要
对于长时间运行的 Worker,协调器需要知道它们的进展情况。进度追踪由 ProgressTracker 实现:
typescript
// 源码文件:src/tasks/LocalAgentTask/LocalAgentTask.tsx
export type ProgressTracker = {
toolUseCount: number
latestInputTokens: number
cumulativeOutputTokens: number
recentActivities: ToolActivity[]
}
export function updateProgressFromMessage(
tracker: ProgressTracker, message: Message,
resolveActivityDescription?: ActivityDescriptionResolver,
): void {
if (message.type !== 'assistant') return
const usage = message.message.usage
tracker.latestInputTokens = usage.input_tokens
+ (usage.cache_creation_input_tokens ?? 0)
+ (usage.cache_read_input_tokens ?? 0)
tracker.cumulativeOutputTokens += usage.output_tokens
for (const content of message.message.content) {
if (content.type === 'tool_use') {
tracker.toolUseCount++
// ...
}
}
}
当协调器模式或 fork 子代理特性启用时,还会启动 Agent 摘要服务(startAgentSummarization),定期从 Worker 的对话中生成进度摘要。这些摘要会更新到任务状态的 progress.summary 字段中,让协调器和用户界面都能了解 Worker 当前在做什么。
14.6.4 错误处理与恢复
多 Agent 系统的错误处理遵循"就近处理、逐层上报"的原则:
- Worker 内部错误:Worker 在执行过程中遇到的工具调用失败(如文件不存在、命令执行出错)由 Worker 自己处理------它会尝试修正或选择替代方案。
- Worker 级别失败 :如果 Worker 的整个执行失败(API 错误、超时、被中止),
runAsyncAgentLifecycle会捕获错误并发送status: "failed"的通知给协调器。 - 协调器级别恢复 :协调器收到失败通知后,可以通过
SendMessage继续该 Worker(利用其已有的错误上下文),或者启动一个新的 Worker 尝试不同的方案。
协调器系统提示词中对此有明确指导:
"When a worker reports failure: Continue the same worker with SendMessage -- it has the full error context. If a correction attempt fails, try a different approach or report to the user."
14.7 设计决策分析
14.7.1 为什么选择 XML 通知而非函数调用
Worker 的结果通过 <task-notification> XML 格式作为 user-role 消息注入,而不是通过某种 RPC 机制或函数调用返回。这个决策有几个深层的技术和架构原因。
首先是消息模型的兼容性 。LLM 的对话模型天然支持 user/assistant 消息的交替。将 Worker 结果包装为 user 消息,让协调器可以自然地"回应"这些结果,无需引入额外的交互模式或修改底层的查询引擎。协调器收到一条包含 <task-notification> 的 user 消息,就像收到用户的普通问题一样,自然地进入下一轮 assistant 回复。
其次是结构化与容错的平衡 。XML 格式具有自描述性,<task-id>、<status>、<result> 等标签让协调器能够可靠地提取关键信息。但与严格的 JSON schema 不同,XML 对格式偏差更具容忍度------即使 LLM 在某些边界情况下生成了略有偏差的格式,人类可读的标签名仍然能帮助 LLM 正确理解内容语义。
最后是统一的消息流 。这种设计使得异步 Worker 通知和用户真实消息可以在同一个消息流中交错出现。协调器无需维护多个消息通道或实现复杂的消息合并逻辑,只需检查消息是否以 <task-notification> 开头即可区分两者。这大大简化了协调器的实现,也让会话的序列化和恢复变得简单直接。
14.7.2 InProcess vs 独立进程的权衡
Claude Code 同时支持两种 Teammate 运行方式:InProcess(同进程,通过 AsyncLocalStorage 隔离)和独立进程(通过 tmux/iTerm2 在独立终端中运行)。两种方式各有优劣,适用于不同的场景。
InProcess 模式的优势在于:启动速度快(无需启动新的 Node.js 进程,只需创建新的 AsyncLocalStorage 上下文)、资源开销低(共享 Node.js 运行时、V8 引擎和已加载的模块)、通信延迟低(权限请求可以直接注入 Leader 的 UI 队列,而非通过文件信箱异步传递)。但它也有明确的限制:不能生成后台 Agent(代码中有显式的检查 isInProcessTeammate() && run_in_background === true 会抛出错误)、所有 Teammate 共享同一个进程的内存上限(292 个并发 Agent 可能消耗 36.8GB 内存的实际案例促使了 TEAMMATE_MESSAGES_UI_CAP 的引入)、Leader 进程的崩溃会导致所有 InProcess Teammate 同时终止。
独立进程模式提供了更强的隔离------每个 Teammate 有自己的内存空间和进程生命周期,一个 Teammate 的内存泄漏不会影响其他成员。这种模式特别适合长时间运行的、资源消耗较大的任务。但代价是启动时间更长(需要初始化完整的 Claude Code 运行时)、需要 tmux 或 iTerm2 作为终端复用器的支持、通信必须通过文件信箱系统异步进行。系统在 spawnMultiAgent.ts 中实现了自动检测逻辑,优先使用 InProcess 模式,在条件不满足时降级到独立进程模式。
14.7.3 Scratchpad 的设计取舍
Scratchpad 目录作为 Worker 之间的知识共享通道,采用了文件系统而非内存共享。这个选择看似低效,实则有深意。文件系统是所有任务类型------无论是 InProcess、独立进程还是远程------都能访问的公共介质。一个 InProcess Teammate 写入 Scratchpad 的文件,一个 tmux 进程中的 Teammate 也能读取,甚至在会话恢复后新启动的 Worker 也能看到之前 Worker 留下的信息。这种跨任务类型、跨会话的持久性是内存共享无法提供的。
Scratchpad 的使用是无需权限确认的。在权限系统中,isScratchpadPath 函数检测目标路径是否位于 Scratchpad 目录内,如果是,则跳过通常的文件写入权限确认流程。这让 Worker 可以高效地写入中间结果,而不会因为频繁的权限弹窗打断 Leader 的工作流。同时,安全设计确保了这种便利不会成为漏洞:路径归一化(normalize)防止路径穿越攻击,目录权限 0o700 限制为当前用户访问,会话级别的隔离确保不同会话的 Worker 不能互相访问。
从架构的角度看,Scratchpad 是一种"共享黑板"模式(Shared Blackboard Pattern)的实现------多个独立的 Agent 通过一个公共空间交换信息,每个 Agent 可以自由读取和写入,而无需知道其他 Agent 的存在。这种模式的优雅之处在于它完全解耦了 Agent 之间的依赖关系:生产者和消费者不需要同时在线,也不需要知道彼此的身份。
14.8 小结
Claude Code 的多 Agent 协调系统是一个精心设计的分层架构。在最顶层,协调器模式将主 Agent 的角色从"万能执行者"转变为"任务编排者",通过精心设计的系统提示词引导协调器进行任务分解、Worker 调度和结果综合。在中间层,AgentTool 根据任务特征选择合适的执行路径------同步或异步、进程内或独立进程、本地或远程。在最底层,SendMessageTool 和文件信箱系统提供了灵活的通信基础设施,支持单播、广播和结构化协议消息。
这套系统的设计体现了几个核心原则:工具约束 (Worker 只能使用完成任务所必需的工具子集)、上下文隔离 (每个 Worker 拥有独立的对话历史和文件状态缓存)、优雅降级(权限代理有主路径和降级路径、消息路由有进程内和文件信箱两种通道)。
从更宏观的视角来看,多 Agent 协调系统将"如何并行化软件工程任务"这个难题分解为了几个可管理的子问题:任务分解由协调器的 LLM 能力处理,执行隔离由类型系统和工具约束保证,通信协调由 XML 通知和信箱系统实现,而最终的综合判断又回到了协调器的 LLM 能力。这种"LLM 做决策、代码做保障"的分工,正是 Claude Code 整体架构哲学在多 Agent 领域的自然延伸。