Agent Team / Swarm 底层原理
多个 agent 如何通信、执行与协作。核心是把文件系统当 IPC:团队配置、收件箱、权限、任务都是 ~/.claude/teams/{team}/ 下的 JSON 文件 + 文件锁。
Claude Code 的「team」不用消息总线、不用 RPC、不用共享内存,而是把文件系统当成 IPC 层。不管队友是同进程、不同 tmux 进程、还是跨机器,通信模型完全一致。协作的本质 = 带 type 字段的 JSON 消息丢进对方收件箱 + 各自轮询路由。2.1.178 起 TeamCreate/TeamDelete 已移除,会话隐式成团。本文是对企业级模式第 35 节的深度展开。
1. 先分清三种「多 agent」形态#
| 形态 | 入口 | 生命周期 | 上下文 | 用途 |
|---|---|---|---|---|
| 普通 subagent | AgentTool → runAgent.ts | 一次性,跑完即返回 | 全新 | "帮我查一下 X" |
| fork subagent | 省略 subagent_type | 一次性,async | 继承父消息前缀(命中 prompt cache) | 并行干同类活 |
| teammate / swarm | 隐式团队 + Agent(name) | 长期存活,多轮、可通信 | 独立,靠 mailbox/task 协调 | 真正的「团队协作」 |
只有第三种是 "team"。teammate 又有两种执行后端(utils/swarm/backends/types.ts):
in-process:同一个 Node 进程内,用AsyncLocalStorage隔离身份。tmux/iterm2(pane-based):每个队友是一个独立的claude子进程,跑在终端分屏里。
抽象成两个接口:PaneBackend(管分屏:create/kill/hide/show pane)与 TeammateExecutor(管生命周期:spawn/sendMessage/terminate/kill)。
2. 身份系统:name@team#
agentId 是确定性的:formatAgentId(name, team) → 例如 researcher@my-team、team-lead@my-team。解析身份有三级优先级(utils/teammate.ts):
AsyncLocalStorage(in-process 队友)
> dynamicTeamContext(tmux 队友, 来自 CLI args)
> env var
一个关键设计:leader 故意不设 CLAUDE_CODE_AGENT_ID。因为 isTeammate() 靠 agentId 判定,leader 没有它 → isTeammate()=false → 不会去轮询自己的收件箱、不会被当成 worker。isSwarmWorker() = 有 team + 有 agentId + 不是 leader。
3. 团队文件:唯一的共享状态源#
~/.claude/teams/{team}/config.json,结构见 teamHelpers.ts:
TeamFile {
leadAgentId, leadSessionId, // 谁是 leader
members: [{ agentId, name, tmuxPaneId, cwd, worktreePath,
backendType, isActive, mode, subscriptions, ... }],
teamAllowedPaths, // 全队免问可编辑的目录
hiddenPaneIds
}
- Team = Project = TaskList。建 team 时
resetTaskList(),任务编号从 1 开始。 - 一个 leader 只能管一个 team;重名自动生成 word slug。
- 只有 leader 写 team file(成员增删、改 mode);teammate 只读 + 改自己那条(
syncTeammateMode)。 - session 结束自动清理(
cleanupSessionTeams);SIGINT 时先 kill 分屏再删目录,否则会留下孤儿子进程。
4. 通信层:文件锁 mailbox#
每个队友一个收件箱:~/.claude/teams/{team}/inboxes/{name}.json,是一个消息数组(teammateMailbox.ts)。
写入为什么要文件锁(writeToMailbox):多个 Claude 进程可能同时给同一个人写信。流程是:
先 writeFile('[]', flag:'wx') // 确保文件存在(proper-lockfile 要求)
→ lockfile.lock(retry 10次, 5~100ms backoff)
→ 重新读取(拿最新状态)
→ push 新消息
→ 写回
→ release
旧的 lockSync 会阻塞事件循环,所以改成 async + 显式 retry 来达到同样的串行化语义。标记已读 markMessagesAsRead 同样加锁。
消息是带类型的 JSON——一个 inbox 承载十几种协议(isStructuredProtocolMessage):
| 消息 type | 方向 | 作用 |
|---|---|---|
| (纯文本) | 任意 | 队友/leader 间对话,喂给模型 |
idle_notification | worker→leader | "我闲下来了" |
permission_request/response | worker↔leader | 工具权限审批 |
sandbox_permission_* | worker↔leader | 沙箱网络访问审批 |
plan_approval_* | worker↔leader | 计划审批 |
shutdown_request/approved/rejected | 双向 | 关闭协商 |
task_assignment | leader→worker | 派活 |
team_permission_update | leader→worker | 广播权限规则 |
mode_set_request | leader→worker | 改权限模式 |
此外还有进程内内存 mailbox(utils/mailbox.ts:一个 queue + waiters + signal 的轻量原语),以及跨会话通道(2.1.88 的 UDS_INBOX,2.1.224 起产品化):本机 UDS inbox、跨机 Remote Control / cloud 桥。队友主线仍走 file-based mailbox。跨会话按名字路由、inbound hold、权限洗钱防护见跨会话 SendMessage。
5. 收发与「唤醒」:两个 poller#
谁去读收件箱、怎么把消息变成模型的一个新 turn——这是协作的关键。两套机制,按角色分流(getAgentNameToPoll):
(A) leader 和 tmux 队友 → useInboxPoller(React hook, 1s 轮询)
读 unread → 按 type 分桶(权限/沙箱/shutdown/plan/modeSet/regular)
├─ 协议消息: 路由到对应 handler(见第 7 节)
└─ regular 对话消息:
idle → 立即 onSubmitMessage() 当成新 turn
busy → 进 AppState.inbox 队列, 等本轮结束再投递
标记已读 ← 放在"投递成功 / 可靠入队之后"(崩溃重读, 不丢消息)
消息以 <teammate-message teammate_id=.. color=.. summary=..> XML 包裹喂给模型。
(B) in-process 队友 → waitForNextPromptOrShutdown(500ms poll)
in-process 队友不能用 useInboxPoller(它和 leader 共享同一个 React context / AppState,会串)。它自己 poll,且有明确的优先级防饿死:
1. task.pendingUserMessages (用户从 transcript 视图直接注入)
2. shutdown_request (最高优先, 防被普通消息淹没)
3. team-lead 的消息 (代表用户意图 / 协调, 优先于队友闲聊)
4. 其它消息 FIFO
5. 都没有 → 去任务清单认领下一个任务(tryClaimNextTask)
6. 执行层:in-process runner 的 agent loop#
runInProcessTeammate(inProcessRunner.ts)是核心。它复用和普通 subagent 完全相同的 runAgent()(内部调 query()),只是包了一层持续循环:
spawnInProcessTeammate:
- 独立 AbortController(leader 的 query 被中断时, 队友不死)
- TeammateContext 存进 AsyncLocalStorage
- 注册 InProcessTeammateTaskState 到 AppState.tasks
runInProcessTeammate(while !abort && !shutdown):
runWithTeammateContext(ctx, () => ← 身份隔离
runWithAgentContext(...) => ← analytics 归因
for await (msg of runAgent({ ... })) ← 真正的模型循环
一轮结束 → 标记 isIdle → 发 idle_notification 给 leader
→ waitForNextPromptOrShutdown() 等下一个 prompt
→ 拿到就当新 prompt 继续循环
执行层的几个硬细节:
- 系统提示 = 主 agent prompt +
TEAMMATE_SYSTEM_PROMPT_ADDENDUM(明确告诉模型:"光写文字队友看不到,必须用 SendMessage")。 - 强制注入团队工具:即使 agent 限制了 tools,也会塞回
SendMessage和任务相关工具。TeamCreate/TeamDelete已不在。 permissionMode强制default:队友总是拿到完整工具集,不继承 leader 的受限模式。- 双层 abort:lifecycle controller(杀整个队友) vs per-turn
currentWorkAbortController(Escape 只停当前轮,队友继续活)。 - 累积
allMessages当forkContextMessages保持跨轮历史;超阈值用隔离 context 自动 compact(不污染主会话的readFileState/ UI)。 - 队友绝不自动把回复发给 leader——必须显式 SendMessage(与 tmux 队友行为一致)。
pane 队友则不同:spawn 一个真正的 claude 子进程到 tmux / iTerm2 分屏,身份靠 CLI args(--agent-id)+ 环境变量(CLAUDE_CODE_AGENT_COLOR、CLAUDE_CODE_PLAN_MODE_REQUIRED)传递。
7. 协作协议(四种)#
① 任务认领(去中心化工作拉取)
leader 建任务清单,队友 idle 时自己 tryClaimNextTask:挑 pending && 无 owner && blockedBy 全完成 的任务,claimTask 抢占 + 设 in_progress。这是工作窃取模型,不是 leader 主动派发。
② 权限审批(worker → leader)
- in-process(优先):通过
leaderPermissionBridge直接复用 leader 的ToolUseConfirmQueue,弹出和 leader 自己工具一模一样的权限 UI(带workerBadge标明是哪个队友);批准的permissionUpdates写回 leader 共享 context(preserveMode:true防 acceptEdits 泄漏)。 - mailbox(tmux worker / bridge 不可用时):worker 先试 bash classifier 自动批准 → 否则
registerPermissionCallback(先注册防竞态)→sendPermissionRequestViaMailbox→useSwarmPermissionPoller500ms 轮询自己 inbox 等响应。leader 端useInboxPoller收到请求塞进 ToolUseConfirmQueue → 用户决定 →sendPermissionResponseViaMailbox回信。createResolveOnce/claim保证只 resolve 一次。
还保留了老的 file-based 双目录机制(permissionSync.ts):permissions/pending/{id}.json 与 resolved/{id}.json,目录级 .lock,resolve 时把文件从 pending 移到 resolved,cleanupOldResolutions 1 小时清理。
③ Plan 审批
队友 planModeRequired 时,ExitPlanMode 不弹本地 UI,而是发 plan_approval_request 给 leader。leader 的 useInboxPoller 自动批准并回 plan_approval_response(带继承的 mode,plan→default)。队友收到时校验必须来自 team-lead(防伪造)才退出 plan mode。
④ Shutdown 生命周期
leader 发 shutdown_request(不自动批准,交给队友的模型决定)→ 队友模型用 SendMessage 回 approve/reject:approve 时 in-process → abort controller,pane → kill pane / gracefulShutdown,并回 shutdown_approved;leader 收到 approved → killPane + 从 team file / teamContext 移除 + unassignTeammateTasks + 标记 task completed。队友干完活变 idle 但不退出,持续等新任务 / 消息——这是它和一次性 background task 的本质区别。
8. 并发与安全的硬核细节#
| 问题 | 解法 | 位置 |
|---|---|---|
| 多进程并发写 inbox | proper-lockfile + retry backoff | teammateMailbox.ts |
| 同进程多队友身份串号 | AsyncLocalStorage 隔离 | teammateContext.ts |
| leader 中断误杀队友 | 队友独立 AbortController | spawnInProcess.ts |
| Escape 误杀整个队友 | per-turn work controller | inProcessRunner.ts |
| 权限多路响应重复执行 | createResolveOnce/claim | swarmWorkerHandler.ts |
| 崩溃丢消息 | 标记已读放在投递成功后 | useInboxPoller.ts |
| 普通消息淹没 shutdown | shutdown 优先 + leader 消息优先 | inProcessRunner.ts |
| 旧版/恶意队友注入坏权限 | permissionUpdates schema 校验过滤 | useSwarmPermissionPoller.ts |
| 队友伪造 plan/mode 批准 | 校验 from === 'team-lead' | useInboxPoller.ts |
| 跨机消息 prompt 注入 | safetyCheck(bypass-immune) | SendMessageTool.ts |
9. 端到端时序(in-process 队友干一个需要权限的活)#
leader: 会话隐式成团 → Agent(name=…) 拉队友 → resetTaskList
leader: Agent(name=researcher, team_name=..) → spawnInProcessTeammate
→ 独立 AbortController + AsyncLocalStorage ctx + AppState.task
→ startInProcessTeammate(fire-and-forget)
researcher: runAgent 循环 → 调 Bash(需权限)
→ createInProcessCanUseTool 命中 'ask'
→ leaderPermissionBridge 把请求塞进 leader 的 ToolUseConfirmQueue
leader UI: 弹出带 [researcher] 徽章的权限框 → 用户 Allow
→ permissionUpdates 写回 leader 共享 context
researcher: 拿到 allow, 继续执行 → 完成本轮 → isIdle=true
→ 写 idle_notification 到 leader 收件箱
leader: useInboxPoller(1s) 读到 → 作为新 turn 投给模型
→ 模型决定下一步, 或 SendMessage 派下一个任务
researcher: waitForNextPromptOrShutdown 收到新 prompt / 或自己 claimTask
→ 继续循环……
leader: 活干完 → SendMessage(shutdown_request)
researcher: 模型决定 approve → abort + 回 shutdown_approved
leader: 收到 → 从 team file 移除 + 标记 task completed
10. 关键文件索引#
| 关注点 | 文件 |
|---|---|
| 团队文件 / 成员管理 | utils/swarm/teamHelpers.ts |
| 拉队友 | Agent 工具的 name(隐式成团) |
| 身份解析 | utils/teammate.ts、utils/teammateContext.ts |
| 文件锁 mailbox + 消息类型 | utils/teammateMailbox.ts |
| 进程内内存 mailbox | utils/mailbox.ts |
| 执行后端抽象 | utils/swarm/backends/types.ts |
| in-process 启动 / kill | utils/swarm/spawnInProcess.ts |
| in-process 执行循环 | utils/swarm/inProcessRunner.ts |
| 发消息工具 | tools/SendMessageTool/SendMessageTool.ts |
| leader / tmux 轮询器 | hooks/useInboxPoller.ts |
| worker 权限轮询器 | hooks/useSwarmPermissionPoller.ts |
| 权限同步(双目录 + mailbox) | utils/swarm/permissionSync.ts |
| leader 权限桥 | utils/swarm/leaderPermissionBridge.ts |
11. 设计哲学(值得借鉴)#
- 用文件 + 锁做统一 IPC,抹平"同进程 / 跨进程 / 跨机器"差异。
- 一切协作编码成带
type的消息,靠轮询路由,而非回调地狱。 - 队友是长生命周期 + 任务拉取,而非一次性 subagent。
- 权限 / 计划 / 关闭都做成可审计的异步协商协议,且对"谁能批准"做了防伪造校验。
- 并发安全靠文件锁 + AsyncLocalStorage + resolve-once + 已读延后这组朴素但可靠的手段,而不是复杂的分布式协议。