机制专题 · 11

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」形态#

形态入口生命周期上下文用途
普通 subagentAgentToolrunAgent.ts一次性,跑完即返回全新"帮我查一下 X"
fork subagent省略 subagent_type一次性,async继承父消息前缀(命中 prompt cache)并行干同类活
teammate / swarm隐式团队 + Agentname长期存活,多轮、可通信独立,靠 mailbox/task 协调真正的「团队协作」

只有第三种是 "team"。teammate 又有两种执行后端(utils/swarm/backends/types.ts):

抽象成两个接口:PaneBackend(管分屏:create/kill/hide/show pane)与 TeammateExecutor(管生命周期:spawn/sendMessage/terminate/kill)。

2. 身份系统:name@team#

agentId 是确定性的:formatAgentId(name, team) → 例如 researcher@my-teamteam-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
}

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_notificationworker→leader"我闲下来了"
permission_request/responseworker↔leader工具权限审批
sandbox_permission_*worker↔leader沙箱网络访问审批
plan_approval_*worker↔leader计划审批
shutdown_request/approved/rejected双向关闭协商
task_assignmentleader→worker派活
team_permission_updateleader→worker广播权限规则
mode_set_requestleader→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 继续循环

执行层的几个硬细节:

pane 队友则不同:spawn 一个真正的 claude 子进程到 tmux / iTerm2 分屏,身份靠 CLI args(--agent-id)+ 环境变量(CLAUDE_CODE_AGENT_COLORCLAUDE_CODE_PLAN_MODE_REQUIRED)传递。

7. 协作协议(四种)#

① 任务认领(去中心化工作拉取)

leader 建任务清单,队友 idle 时自己 tryClaimNextTask:挑 pending && 无 owner && blockedBy 全完成 的任务,claimTask 抢占 + 设 in_progress。这是工作窃取模型,不是 leader 主动派发。

② 权限审批(worker → leader)

还保留了老的 file-based 双目录机制(permissionSync.ts):permissions/pending/{id}.jsonresolved/{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. 并发与安全的硬核细节#

问题解法位置
多进程并发写 inboxproper-lockfile + retry backoffteammateMailbox.ts
同进程多队友身份串号AsyncLocalStorage 隔离teammateContext.ts
leader 中断误杀队友队友独立 AbortControllerspawnInProcess.ts
Escape 误杀整个队友per-turn work controllerinProcessRunner.ts
权限多路响应重复执行createResolveOnce/claimswarmWorkerHandler.ts
崩溃丢消息标记已读放在投递成功后useInboxPoller.ts
普通消息淹没 shutdownshutdown 优先 + 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.tsutils/teammateContext.ts
文件锁 mailbox + 消息类型utils/teammateMailbox.ts
进程内内存 mailboxutils/mailbox.ts
执行后端抽象utils/swarm/backends/types.ts
in-process 启动 / killutils/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. 设计哲学(值得借鉴)#

  1. 用文件 + 锁做统一 IPC,抹平"同进程 / 跨进程 / 跨机器"差异。
  2. 一切协作编码成带 type 的消息,靠轮询路由,而非回调地狱。
  3. 队友是长生命周期 + 任务拉取,而非一次性 subagent。
  4. 权限 / 计划 / 关闭都做成可审计的异步协商协议,且对"谁能批准"做了防伪造校验。
  5. 并发安全靠文件锁 + AsyncLocalStorage + resolve-once + 已读延后这组朴素但可靠的手段,而不是复杂的分布式协议。