Skip to content
源码分析手册

Agent Teams:共享账本、协作协议与多后端编排

当你开启 CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 时,Claude Code 会从“单兵作战”切换到“团队集群”模式。这套架构的本质,是在本地环境之上搭建了一层 Swarm(蜂群)协调面。

核心概念

Agent Teams 的本质是基于本地文件系统的多代理协作协议(File-based Swarm Protocol)

它与普通 Subagent 的区别在于:Subagent 关注的是“任务隔离”,而 Agent Teams 关注的是“专业化协作”。在 Team 模式下,多个 Agent(Teammates)被绑定在同一个团队身份下,共享一套“团队状态”。

这份状态并非存储在内存中,而是落盘为三份关键的“本地事实源”:

  1. Team File (~/.claude/teams/): 记录团队成员、Leader 身份、活跃状态及权限模式。
  2. Shared Task List (~/.claude/tasks/): 团队共享的任务账本。
  3. Mailbox (~/.claude/teams/<team>/inboxes/): 成员间收发结构化消息的邮局。

源码级拆解

Agent Teams 的运作逻辑由三个层级组成:实验性准入、后端承载和协作原语。

1. 实验性准入网关

claude-code-opensource/src/utils/agentSwarmsEnabled.ts 中,系统设置了严格的 Gate:必须同时满足环境变量开启和内部功能开关(GrowthBook)未关闭,协作功能才会被激活。

2. 多样化的后端承载(Backends)

虽然用户看到的通常是 tmux 或 iTerm2 里的分屏,但 claude-code-opensource/src/utils/swarm/backends/registry.ts 揭示了三种后端模式:

  • In-process:在同一个 Node.js 进程中模拟多个 Worker,适合非交互式环境。
  • tmux / iTerm2:在真实的终端窗口中拉起独立进程,提供可视化的多路并行体验。

无论使用哪种后端,它们在逻辑上都遵循统一的协作协议。

3. 基于文件锁的协作原语

协作的核心不靠模型“心有灵犀”,而是靠硬性的本地同步:

  • 共享任务账本:所有成员通过 claude-code-opensource/src/utils/tasks.ts 访问同一个任务目录。通过文件锁机制,成员可以相互“领用(Claim)”任务、更新状态或处理依赖阻塞。
  • Mailbox 通信协议:成员间的 SendMessage 并不是把文字发给对方看,而是调用 claude-code-opensource/src/utils/teammateMailbox.ts 将结构化指令写入对方的收件箱文件。

4. 权限与回收机制

为了安全,Teammates 通常共享 Leader 的权限桥。当任务结束时,claude-code-opensource/src/tools/TeamDeleteTool/TeamDeleteTool.ts 会检查 Team File;只有当所有成员都通过 shutdownidle Hook 回报已停止工作后,才会执行最终的目录清理和进程回收。

踩坑指南

  • 非天然可见性:队友之间不是自动共享聊天记录的。如果不用 SendMessage 明确发送消息,对方根本不知道你在干什么。
  • 实验性风险:作为实验功能,Agent Teams 在异常中断后的清理工作可能不彻底。如果发现残留进程或被锁定的任务文件,需要手动清理 ~/.claude/ 目录。
  • Token 消耗倍增:每个 Teammate 都有独立的 System Prompt 和上下文初始开销。多代理协作在提高效率的同时,也会显著增加 Token 成本。
  • Leader 绝对权威:Leader 决定了团队的生存期。如果 Leader 进程意外退出,整个团队的协作流可能会陷入僵死状态,直到手动干预。

延伸阅读

源码锚点

  • claude-code-opensource/src/tools/TeamCreateTool/TeamCreateTool.ts: 团队创建工具,负责初始化 Team File 和共享任务目录。
📄 src/tools/TeamCreateTool/TeamCreateTool.ts — 团队创建工具,负责初始化 Team File 和共享任务目录。L20-26 of 241
typescript
  getTeamFilePath,
  readTeamFile,
  registerTeamForSessionCleanup,
  sanitizeName,
  writeTeamFileAsync,
} from '../../utils/swarm/teamHelpers.js'
import { assignTeammateColor } from '../../utils/swarm/teammateLayoutManager.js'
  • claude-code-opensource/src/utils/swarm/backends/registry.ts: 后端注册表,定义了 in-processtmuxiTerm2 的分流逻辑。
📄 src/utils/swarm/backends/registry.ts — 后端注册表,定义了 `in-process`、`tmux`、`iTerm2` 的分流逻辑。L51-79 of 465
typescript
 * was available (e.g., iTerm2 without it2 or tmux installed). Once set,
 * isInProcessEnabled() returns true so UI (banner, teams menu) reflects reality.
 */
let inProcessFallbackActive = false

/**
 * Placeholder for TmuxBackend - will be replaced with actual implementation.
 * This allows the registry to compile before the backend implementations exist.
 */
let TmuxBackendClass: (new () => PaneBackend) | null = null

/**
 * Placeholder for ITermBackend - will be replaced with actual implementation.
 * This allows the registry to compile before the backend implementations exist.
 */
let ITermBackendClass: (new () => PaneBackend) | null = null

/**
 * Ensures backend classes are dynamically imported so getBackendByType() can
 * construct them. Unlike detectAndGetBackend(), this never spawns subprocesses
 * and never throws — it's the lightweight option when you only need class
 * registration (e.g., killing a pane by its stored backendType).
 */
export async function ensureBackendsRegistered(): Promise<void> {
  if (backendsRegistered) return
  await import('./TmuxBackend.js')
  await import('./ITermBackend.js')
  backendsRegistered = true
}
  • claude-code-opensource/src/utils/teammateMailbox.ts: 实现基于文件的跨代理消息传递协议。
📄 src/utils/teammateMailbox.ts — 实现基于文件的跨代理消息传递协议。L35-41 of 1184
typescript
const LOCK_OPTIONS = {
  retries: {
    retries: 10,
    minTimeout: 5,
    maxTimeout: 100,
  },
}
  • claude-code-opensource/src/utils/tasks.ts: 共享任务账本的核心实现,包含文件锁与状态机逻辑。
📄 src/utils/tasks.ts — 共享任务账本的核心实现,包含文件锁与状态机逻辑。L18-37 of 863
typescript
const tasksUpdated = createSignal()

/**
 * Team name set by the leader when creating a team.
 * Used by getTaskListId() so the leader's tasks are stored under the team name
 * (matching where tmux/iTerm2 teammates look), not under the session ID.
 */
let leaderTeamName: string | undefined

/**
 * Sets the leader's team name for task list resolution.
 * Called by TeamCreateTool when a team is created.
 */
export function setLeaderTeamName(teamName: string): void {
  if (leaderTeamName === teamName) return
  leaderTeamName = teamName
  // Changing the task list ID is a "tasks updated" event for subscribers —
  // they're now looking at a different directory.
  notifyTasksUpdated()
}
  • claude-code-opensource/src/tools/SendMessageTool/SendMessageTool.ts: 跨代理消息、心跳与关闭请求的统一入口。
📄 src/tools/SendMessageTool/SendMessageTool.ts — 跨代理消息、心跳与关闭请求的统一入口。L46-51 of 918
typescript
const StructuredMessage = lazySchema(() =>
  z.discriminatedUnion('type', [
    z.object({
      type: z.literal('shutdown_request'),
      reason: z.string().optional(),
    }),
  • claude-code-opensource/src/utils/swarm/teamHelpers.ts: 管理 Team File 结构与成员活跃状态的工具集。
📄 src/utils/swarm/teamHelpers.ts — 管理 Team File 结构与成员活跃状态的工具集。L22-45 of 684
typescript
      .enum(['spawnTeam', 'cleanup'])
      .describe(
        'Operation: spawnTeam to create a team, cleanup to remove team and task directories.',
      ),
    agent_type: z
      .string()
      .optional()
      .describe(
        'Type/role of the team lead (e.g., "researcher", "test-runner"). ' +
          'Used for team file and inter-agent coordination.',
      ),
    team_name: z
      .string()
      .optional()
      .describe('Name for the new team to create (required for spawnTeam).'),
    description: z
      .string()
      .optional()
      .describe('Team description/purpose (only used with spawnTeam).'),
  }),
)

// Output types for different operations
export type SpawnTeamOutput = {

基于 Claude Code v2.1.88 开源快照的深度分析