s15:Agent Teams(团队协作)

问题背景


假设要让 Agent 重构整个后端:配置加载、认证、测试三块。一个 Agent 当然可以串行处理,但总耗时更长,且早期细节会随上下文窗口被推出去。

这类工作适合并行,但用户通常只给一句目标,不会替运行时设计团队:

重构这个示例后端。清理配置加载、认证和测试,
保持现有接口,并确保测试通过。

Harness 必须回答一组互相牵连的问题:

  1. 谁来决定要不要并行、新增几个队友? —— 不能让模型擅自开线程烧 token
  2. 每个队友如何跨任务保留身份和上下文? —— subagent 是一次性的,干完就忘
  3. 结果怎么自动回到 Lead,而不是让 Lead 反复轮询收件箱?
  4. 空闲队友能不能直接接手 ready task,不再等 Lead 一项项派发?
  5. 并行修改可能互相冲突时,任务该用哪个工作目录?
  6. 关机和计划审批,怎么变成可追踪、可执行的协议,而不是靠猜消息意图?

解决方案全景(模块架构图)


s13 复用 s10 的任务系统、Hooks、权限,再叠加一套由 Lead 管理的**“团队运行时”**。

                          ┌─────────────────────────────────────────┐
                          │              Lead (主线程)                │
                          │  提方案 → 等确认 → 建任务/worktree → 派活  │
                          └───────────────┬───────────────┬──────────┘
                               spawn      │               │ consume_lead_inbox()
                                          ▼               ▼
                          ┌──────────────────────┐   .mailboxes/lead.jsonl
                          │   TeammateRuntime     │   (队友事件 → 注入 history)
                          │   (每队友一个线程)     │
                          └───┬───────┬───────┬───┘
               WORK           │       │       │           IDLE
              ┌───────────────┘       │       └──────────────────┐
              ▼                       ▼                          ▼
      ┌──────────────┐      ┌────────────────┐         ┌──────────────────┐
      │ 基础工具      │      │ MessageBus      │         │ 空闲任务发现      │
      │ bash/read/... │      │ (.mailboxes/)   │         │ claim_next_task  │
      │ cwd=任务目录  │      │ send/read/等待  │         │ (扫 ready task)  │
      └──────┬───────┘      └───────┬────────┘         └──────────────────┘
             │                      │
             ▼                      ▼
      ┌──────────────┐      ┌────────────────┐
      │ 任务系统      │      │ 团队协议         │
      │ .tasks/*.json│      │ 关机/计划审批    │
      │ owner/status │      │ request_id 关联  │
      └──────┬───────┘      └────────────────┘
             │
             ▼
      ┌──────────────────────┐
      │ 可选 Worktree         │
      │ .worktrees/     │
      │ 分支 wt/        │
      └──────────────────────┘

七类核心机制总览

机制 职责 所在层级
Lead 和用户对话,提出分工方案并等待确认 控制层
队友(Teammate) 跑独立 Agent Loop,在 WORK 和 IDLE 之间切换直到关机 执行层
MessageBus 通过文件收件箱传递普通消息、结果和控制事件 通信层
运行时投递 消费 Lead 收件箱,把团队事件注入下一轮对话 通信层
共享任务板 空闲队友发现 ready task,并在锁内原子认领 协调层
可选 worktree 需要时把任务绑定到另一个工作目录;未绑定任务仍用仓库目录 隔离层
类型化协议 + 计划闸门 显式记录关机/审批状态,并在计划获批前锁住修改型工具 安全层

注意:s11 的后台任务、s12 的定时任务没有被带入本章——它们不参与队友通信、任务认领或计划审批。


模块逐一拆解


模块 1:任务系统(共享任务板)

做什么

把"要做什么"持久化成一条条任务记录,支持三个核心维度:

  • 依赖(blockedBy:任务之间的先后约束
  • 状态机(pending → in_progress → completed:任务的生命周期
  • 归属(owner:谁在负责这个任务

这是所有协作的真相源(Single Source of Truth)

怎么做

  • Task 数据结构:是一个 dataclass,序列化到 .tasks/task_<8位hex>.json 文件
  • ID 生成create_tasksecrets.token_hex(4) 生成唯一 ID(避免和其他进程撞 ID)
  • 依赖检查can_start(task_id) 检查 blockedBy 里每个依赖都已 completed,缺失依赖视为 blocked——这是后续"自动解锁下游任务"的基础
  • 原子认领claim_task(task_id, owner)task_store_lock() 内把 owner/status 一次性写下去,并把 {task_id, cwd} 记进 teammate_assignments[owner]每个队友同时只有一个 assignment
  • 完成任务complete_task(task_id, owner) 校验"调用者就是 owner"且"计划闸门没拦着"才置 completed,并列出因此被解锁的下游任务

关键安全点

task_store_lock() 同时拿了两把锁

锁类型 保护对象 作用范围
进程内 RLockthreading.Lock 内存中的数据结构 同一进程内的多线程并发
fcntl 文件锁(.tasks/.lock 磁盘上的任务文件 跨进程 / 重启后的安全
  • save_task 采用先写临时文件再 os.replace 原子替换的策略,防止写到一半崩溃留下半截 JSON。

为什么两把锁都要? 见下方模块 6 的详细解释。


模块 2:任务绑定的 Worktree(可选隔离目录)

做什么

当并行修改会互相踩脚(比如 A、B 都要改 config.py)时,给任务一个独立的 git worktree 目录 + 分支 wt/<name>,让两个队友各写各的,互不干扰。

怎么做

  • create_worktree(name, task_id) 只允许 Lead 调用。它先校验一长串前置条件:
    1. 名字合法
    2. 任务必须 pending 且无人认领且未绑定
    3. 当前目录是 git 仓库根
    4. 分支名合法且分支不存在
    5. 路径未注册
  • 校验全部通过后,才执行 git worktree add -b wt/<name>
  • 最后才把 task.worktree = name 写回任务文件

Partial Operation 处理

如果 git worktree add 报错了但已经留下了分支或已注册的 checkout,运行时不会偷偷删掉,而是报 “partial operation”(部分操作失败),任务保持未绑定状态,并把产物(路径/分支/注册项)列出来供人工恢复。

工作目录解析

场景 行为
任务没绑 worktree 使用仓库 WORKDIR
任务绑了 worktree 但绑定损坏 失败关闭(fail closed)——绝不像旧版那样悄悄回退到仓库目录
进程重启后 能根据 .tasks/ 里持久化的 owner/worktree 恢复进行中的 assignment;一旦 work version 对不上就失效(崩溃可重建)

清理规则

remove_worktree 只给宿主/用户调用,永远保留 wt/<name> 分支(方便事后审查);拒绝 pending/in_progress 绑定,以及当前轮次仍在占用的 lease。


模块 3:基础工具与工作目录锁

做什么

bash / read_file / write_file / edit_file / glob 五个工作区工具,全部通过 assignment_cwd(owner) 解析 cwd——队友只能改自己认领到的任务目录

怎么做

  • run_agent_bash/read/write/edit/glob 等包装器先调 _agent_cwd()没有认领任务直接返回错误,不回退仓库
  • 这把"能改哪里"从运行时层面硬锁死,而不是靠提示词劝导
  • safe_path 校验路径不逃逸 workspace
  • run_bash 超时 120s、输出截断 50k 字符

设计哲学:安全靠机制保障,不靠 LLM 自觉。


模块 4:MessageBus 邮件总线(通信放在模型上下文之外)

为什么不能共享 messages 数组?

Lead 和队友不能共享同一个 messages 数组——否则一个队友的工具结果会混进另一个队友的推理上下文,导致混乱。所以通信必须搬出模型上下文,走文件。

怎么做

每个 Agent 在磁盘上有一个文件 .mailboxes/<名字>.jsonl,这就是它的"信箱":

操作 实现 特点
发送 send 在信箱文件末尾追加一行 JSON(一行 = 一条消息) 追加后调用 notify_all() 唤醒可能在等的线程
接收 read_inbox 读文件全部内容,读完就删空(destructive read / 破坏性读) 保证每条消息只被消费一次
等待 wait_for_messages threading.Condition 锁阻塞等待 没消息就短时睡眠(IDLE 轮询间隔约 2 秒),消息一到就被唤醒——避免忙等占 CPU

结果上报机制

队友的汇报不是靠 Lead 主动去问(轮询),而是往 Lead 的信箱文件里塞一行 JSON。Lead 主循环读信箱时顺手把内容塞进下一轮对话——所以 Lead 永远是**“被动唤醒”**,不轮询。


模块 5:进程锁 + 文件锁(双重锁定机制)

为什么需要两把锁?

锁类型 锁的是什么 防的是什么
进程锁threading.Lock 同一进程内的内存 Lead 主线程 + alice/bob 线程都在同一个 Python 程序里,共享内存。谁拿到锁谁进临界区改任务板,别人阻塞等待
文件锁fcntl.flock 磁盘上的文件本身 由操作系统管理,跨进程有效。程序崩了、重启成新进程,OS 仍按文件锁裁决,新进程拿不到锁就被挡

少一把都有洞

  • 只有进程锁 → 程序一重启,锁随进程消失,两个新进程能同时改任务板 → 数据竞争
  • 只有文件锁 → 同进程内频繁加文件锁开销大、逻辑绕、性能差
  • 两把一起 = 双保险:队内用进程锁(轻量快速),跨进程/重启用文件锁(安全可靠),谁都抢不过去

模块 6:团队协议(关机 / 计划审批的类型化消息)

做什么

普通协作可以用自由文本,但关机和审批不能靠猜消息意图。它们用结构化协议来确保可靠传递和正确匹配。

怎么做

  • ProtocolState 记录一条控制消息,包含以下字段:

    字段 作用
    request_id 把回复关联到原始请求(防错配)
    type 消息类型(如 shutdown_requestplan_approval_response)——阻止不匹配的回复改状态
    sender 发送者身份
    target 目标接收者
    status 当前状态——阻止同一回复重复生效
    payload 消息载荷(具体内容)
    work_version 工作版本号(防越权)
    task_id 关联的任务 ID
  • 三字段联合防错配

    1. request_id:把回复关联到请求
    2. type:阻止不匹配的回复改状态(关机回复不能改计划状态)
    3. status:阻止同一回复重复生效
  • consume_lead_inbox:CLI 主循环消费 Lead 收件箱时,对 *_response 类型调用 match_response 更新协议状态,再把事件注入 history。


模块 7:空闲任务发现(队友自己找活,不靠 Lead 派发)

做什么

空闲队友自动发现 ready task 并接手,Lead 不必逐项派发。这大大减少了 Lead 的协调负担。

怎么做

  • scan_unclaimed_tasks() 只做"快照":返回满足以下所有条件的任务列表:
    • status = pending
    • owner = None(无人认领)
    • can_start = True(依赖已满足)
    • worktree 可用(如果需要的话)
  • claim_next_task(name) 遍历快照逐个调 claim_task只认领第一个真正可用的;且当前已有 assignment 的队友直接返回 None(永不拿第二份活
  • 关键设计:扫描只是快照,真正的原子认领在 claim_task 持锁内进行——多个队友同时发现同一任务,也只有一个能把它推进到 in_progress

模块 8:队友运行时 TeammateRuntime(持久队友的 WORK/IDLE 循环)

做什么

一个持久执行单元——拥有独立 system prompt、messages、工具,在线程里跑 WORK → IDLE → WORK 直到关机。

和 s06 一次性 subagent 的根本区别:subagent 干完就销毁;TeammateRuntime 是持久的,可以在多个任务之间切换。

怎么做

初始化 __init__
  • 构造专属 system prompt(如"你是 alice,一个 coder…")
  • 把已分配任务注入首条 user 消息
  • require_plan 则附上"需先交计划"的指示
  • handlers 表挂载:bash / read / write / edit / glob / send_message / submit_plan / list_tasks / claim / complete
工作模式 work()

每轮的流程:

  1. handle_inbox 消化消息(可能触发停机)
  2. 跑一次 LLM
  3. 如果返回 tool_use → 执行工具 → 返回 continue(继续工作循环)
  4. 如果返回文本总结 → 发 result 消息给 Lead
  5. 如果计划闸门是 pending → 转 waiting_approval(等待审批)
  6. 否则 → 释放 assignment → 转 idle → 发 idle_notification
空闲等待 wait_for_work()

IDLE 循环里的优先级顺序:

  1. 先查收件箱(直接消息优先——可能是 Lead 的指令或关机请求)
  2. claim_next_task 找 ready task 注入 messages(自动接活)
消息处理 handle_inbox()

分流处理不同类型的消息:

消息类型 处理方式
shutdown_request shutdown_responsereturn stop(退出循环)
plan_approval_response 更新计划闸门状态
plan_request 处理计划提交
普通 message 注入 messages 让 LLM 处理
主循环 run()

continue → idle → stop 主循环;退出时 release_teammate_assignment 把没干完的活退回任务板,并清理 active_teammates / plan_gates


模块 9:启动队友 spawn_teammate(先认领初始任务,再起线程)

做什么

在一个子线程里启动一个持久队友。

怎么做

  1. 校验名字:1–64 字母数字/下划线/短横,不能是 lead/agent 保留名,不与现有队友重名
  2. 注册 active_teammates[name]=workingplan_gatesassignment_versions
  3. 若带了 task_id,先 claim_task(认领失败就整段回滚,绝不留下半启动的队友
  4. 构造 TeammateRuntime、起 daemon 线程

重要边界

Lead 的系统提示词里写死**“先提方案、等用户确认,确认前不许调 spawn_teammate。所以"开不开团队"这个成本/并发决策始终握在用户手里**,模型不能擅自开队友烧 token。


模块 10:计划闸门 Plan Gate(改文件前先看计划)

做什么

在队友动文件/跑 shell 之前,强制它先交计划并获批。防止队友"拿到任务就直接开干"导致方向偏差。

怎么做

  • 闸门状态机plan_gates[teammate] ∈ { not_required, required, pending, rejected, approved }
  • 拦截时机_run_teammate_toolbash / write_file / edit_file 前检查 gate——不是 approved 就直接 Blocked
  • 提交流程submit_plan 建 pending 请求 + 把 gate 设 pending
  • 审批流程review_plan(Lead 侧)审批通过才设 approved,且校验 work_version / task_id 仍一致才生效
  • 版本联动:认领或释放任务会 advance_assignment_version,使旧审批自动失效——防止"换了个任务还沿用旧计划"的越权行为

模块 11:钩子与权限(工具调用前后拦截)

做什么

在所有工具调用前后插一道拦截层——覆盖危险命令、工作区外路径、日志记录、大输出截断、会话总结等场景。

怎么做

  • 四类 HookUserPromptSubmit / PreToolUse / PostToolUse / Stop
  • 权限检查 check_permission
    • 比对 DENY_LIST(如 rm -rf /sudo
    • 比对 DESTRUCTIVE(如 rm > /etc/
    • 队友线程里 prompt_user=False:直接返回 permission 错误交 Lead 处理(队友不能从后台线程读用户输入)
    • Lead 线程里才交互式问用户
  • 执行链路execute_tool 串起 PreToolUse → handler → PostToolUse

模块 12:Lead 主循环 + CLI 事件驱动(双路唤醒)

做什么

Lead 主循环同时监听终端输入收件箱消息两条通路,实现双路唤醒。

怎么做

  • agent_loop:跑 LLM,遇到 tool_useexecute_tool,否则结束本轮
  • wait_for_cli_event:用 select 同时等 sys.stdin(用户输入)和 BUS.peek("lead")(收件箱消息):
    • 收件箱有消息 → 返回 "wake"
    • 否则显示 s13 >> 提示符等用户输入
  • 主循环逻辑
    • 收到用户输入 → agent_loop
    • 收到 "wake"consume_lead_inbox 把事件格式化成 [Team events] 注入 history → agent_loop

核心优势:队友事件到达会自动唤醒 Lead 新一轮,Lead 不必轮询收件箱或 list_teammates


端到端完整流程


以一个具体的重构需求为例,串联所有模块:

用户:把后端重构拆到共享任务板,尽量并行完成配置、认证和测试。
     认证任务用 worktree。

① Lead 提方案(模块 8 边界:用户确认后才启动)
   "建议按 config / auth / tests 三个方向分工,是否启动?"
   用户:"开始吧"

② 建任务 + 绑 worktree(模块 1、2:Lead 侧操作)
   create_task(config) → create_task(auth) → create_task(tests)
   create_worktree("auth-refactor", auth_task_id)   # auth 走独立目录

③ 派活:spawn_teammate 在线程启动前原子认领初始任务(模块 8)
   spawn alice → claim config (cwd=仓库根目录)
   spawn bob   → claim auth   (cwd=.worktrees/auth-refactor)

④ 队友 WORK 循环(模块 7、3:队友独立执行)
   alice/bob 各自跑 LLM + 工具干活(cwd 锁定到各自任务目录)
   → complete_task → 发 result + idle_notification 到 lead.jsonl

⑤ 运行时投递(模块 4、11:消息回到 Lead)
   主循环 consume_lead_inbox
   [Team events] bob: result "认证已重构,测试通过"
   → 注入 history → 唤醒 Lead 新一轮 → Lead 继续协调

⑥ 空闲自动接活(模块 6:不需 Lead 逐个派发)
   alice 干完 config → IDLE → wait_for_work
   → 扫到 tests 还没人认领 → claim_next_task → 自动开干

⑦ 平滑关机(模块 5、6:协议驱动)
   Lead request_shutdown(bob)
   → shutdown_request → bob 完成当前步 → shutdown_response
   → bob 循环退出,release_teammate_assignment 把活退回任务板

落盘文件一览

路径 内容 用途
.tasks/*.json 任务状态/归属/依赖 协作的真相源,崩溃可重建
.mailboxes/*.jsonl 消息/结果/协议响应 通信记录,支持投递和恢复
.worktrees/<name> 隔离工作目录 并行修改的文件隔离(分支 wt/<name> 始终保留)

崩溃后可由 .tasks/ + .mailboxes/ 重建现场。

一句话串全流程

用户给目标 → Lead 提方案等确认(模块 8 边界)→ 建任务/绑 worktree(模块 1、2)→ spawn 时原子认领初始任务(模块 8)→ 队友 WORK 循环用 cwd 锁干活(模块 3、7)→ 结果经 MessageBus 回到 Lead(模块 4、11)→ 空闲队友自动 claim 下游任务(模块 6)→ 协议驱动的关机/计划审批收尾(模块 5、9)


相对 s10 的变化对比


维度 s10(单 Agent 任务系统) s13(Agent Teams)
Agent 数量 单个 Agent 一个 Lead + N 个持久队友
用户流程 直接执行请求 先提团队方案,用户确认后再启动
通信机制 文件收件箱 + 运行时自动投递
生命周期 一个循环 队友 WORK / IDLE / shutdown 多状态
共享工作方式 单 Agent 用任务工具 IDLE 扫描 + 队友原子认领
工作目录 仓库 WORKDIR 必须认领任务;任务可选绑定 worktree
结果上报 当前 Agent 输出 分开的 resultidle_notification
控制机制 类型化关机与计划审批协议
执行约束 无团队约束 计划闸门会锁住修改型工具