Agent Teams:共享账本、协作协议与多后端编排
当你开启 CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 时,Claude Code 会从“单兵作战”切换到“团队集群”模式。这套架构的本质,是在本地环境之上搭建了一层 Swarm(蜂群)协调面。
核心概念
Agent Teams 的本质是基于本地文件系统的多代理协作协议(File-based Swarm Protocol)。
它与普通 Subagent 的区别在于:Subagent 关注的是“任务隔离”,而 Agent Teams 关注的是“专业化协作”。在 Team 模式下,多个 Agent(Teammates)被绑定在同一个团队身份下,共享一套“团队状态”。
这份状态并非存储在内存中,而是落盘为三份关键的“本地事实源”:
- Team File (
~/.claude/teams/): 记录团队成员、Leader 身份、活跃状态及权限模式。 - Shared Task List (
~/.claude/tasks/): 团队共享的任务账本。 - 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;只有当所有成员都通过 shutdown 或 idle Hook 回报已停止工作后,才会执行最终的目录清理和进程回收。
踩坑指南
- 非天然可见性:队友之间不是自动共享聊天记录的。如果不用
SendMessage明确发送消息,对方根本不知道你在干什么。 - 实验性风险:作为实验功能,Agent Teams 在异常中断后的清理工作可能不彻底。如果发现残留进程或被锁定的任务文件,需要手动清理
~/.claude/目录。 - Token 消耗倍增:每个 Teammate 都有独立的 System Prompt 和上下文初始开销。多代理协作在提高效率的同时,也会显著增加 Token 成本。
- Leader 绝对权威:Leader 决定了团队的生存期。如果 Leader 进程意外退出,整个团队的协作流可能会陷入僵死状态,直到手动干预。
延伸阅读
- 如果你想深入了解任务状态是如何在成员间流转的,请看 任务管理:共享账本与状态机。
- 如果你关心后台长驻进程的底层实现,请看 后台进程与生命周期管理。
- 如果你想了解角色指令是如何被动态注入的,请看 InstructionsLoaded:角色与约束的动态注入。
源码锚点
claude-code-opensource/src/tools/TeamCreateTool/TeamCreateTool.ts: 团队创建工具,负责初始化 Team File 和共享任务目录。
📄 src/tools/TeamCreateTool/TeamCreateTool.ts — 团队创建工具,负责初始化 Team File 和共享任务目录。
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-process、tmux、iTerm2的分流逻辑。
📄 src/utils/swarm/backends/registry.ts — 后端注册表,定义了 `in-process`、`tmux`、`iTerm2` 的分流逻辑。
* 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 — 实现基于文件的跨代理消息传递协议。
const LOCK_OPTIONS = {
retries: {
retries: 10,
minTimeout: 5,
maxTimeout: 100,
},
}claude-code-opensource/src/utils/tasks.ts: 共享任务账本的核心实现,包含文件锁与状态机逻辑。
📄 src/utils/tasks.ts — 共享任务账本的核心实现,包含文件锁与状态机逻辑。
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 — 跨代理消息、心跳与关闭请求的统一入口。
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 结构与成员活跃状态的工具集。
.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 = {