首页
/ learn-claude-code s10:Team Protocols —— 用统一 request_id 请求-响应协议驱动团队关闭握手与计划审批

learn-claude-code s10:Team Protocols —— 用统一 request_id 请求-响应协议驱动团队关闭握手与计划审批

2026-09-05 23:29:01作者:劳婵绚Shirley

本文基于 learn-claude-code 仓库第 s10 课「Team Protocols(团队协议)」文档展开,讲解多 Agent 团队中两个核心协调场景——优雅关闭(graceful shutdown)与计划审批(plan approval)——如何被收敛为同一个 request_id 关联模式与同一个 pending → approved | rejected 有限状态机(FSM)。读完后,你能理解 agents/s10_team_protocols.py 中协议消息从生成、入箱、响应到状态落地的完整链路,并掌握如何在本仓库中实际运行 s10 演示、用测试用例验证协议的防伪造/防重放能力。

Team Protocols 协议总览图

课程定位:Harness 层的「协议」环节

learn-claude-code 用 s01 > s02 > ... > s12 的递进结构把一个「类 Claude Code 的 agent harness」从 0 到 1 搭出来。其中 s07–s12 属于团队/任务方向的递进分支:

s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > [ s10 ] > s11 > s12

s10 被定位为 Harness 层的 Protocols(协议)环节:模型之间的结构化握手(structured handshakes between models)。s09(Agent Teams)已经让团队成员(teammate)可以常驻运行、通过文件收件箱互相通信,但通信只有「普通消息」这一种能力,缺乏结构化协调规则。s10 在 s09 的基础上补齐了这一层:一条 request-response 模式同时驱动所有「协商类」交互

问题:s09 的两个协调缺口

按原文档描述,s09 中团队成员能干活、能通信,但没有结构化协调,具体表现为两个场景:

场景一:关闭(Shutdown)。直接杀掉(kill)一个 teammate 线程,会导致文件写到一半、config.json 停在过时状态。正确的做法是一次握手:Leader 发出关闭请求,teammate 自行决定是批准(收尾后退出)还是拒绝(继续工作)。

场景二:计划审批(Plan approval)。Leader 说一句「重构认证模块」,teammate 会立刻动手。对高风险变更,Leader 应该在执行前 review 一份计划。

关键在于:这两个场景是同一个结构——一方发送带唯一 ID 的请求,另一方引用该 ID 回应。因此可以用一套协议骨架同时解决两个问题,这也是 s10 的核心洞察:「Same request_id correlation pattern, two domains」(同一个 request_id 关联模式,两个领域)

解决方案:一个共享 FSM + 两个协议

原文档给出的整体结构如下:

Shutdown Protocol            Plan Approval Protocol
==================           ======================

Lead             Teammate    Teammate           Lead
  |                 |           |                 |
  |--shutdown_req-->|           |--plan_req------>|
  | {req_id:"abc"}  |           | {req_id:"xyz"}  |
  |                 |           |                 |
  |<--shutdown_resp-|           |<--plan_resp-----|
  | {req_id:"abc",  |           | {req_id:"xyz",  |
  |  approve:true}  |           |  approve:true}  |

Shared FSM:
  [pending] --approve--> [approved]
  [pending] --reject---> [rejected]

Trackers:
  shutdown_requests = {req_id: {target, status}}
  plan_requests     = {req_id: {from, plan, status}}

三个要素:

  • 共享 FSM:每条请求的生命周期都是 pending --approve--> approvedpending --reject--> rejected。两个协议复用同一套状态迁移。
  • Tracker(跟踪表):两个内存字典分别按 request_id 索引,shutdown_requests 记录 {"target", "status"}plan_requests 记录 {"from", "plan", "status"}。响应到达时凭 request_id 定位原请求并迁移状态。
  • request_id 关联:请求方生成 ID,响应方回带同一个 ID,从而把「一条回复」精确绑定到「一条请求」。

源码中这两个 tracker 与一把全局锁定义在 agents/s10_team_protocols.py

# -- Request trackers: correlate by request_id --
shutdown_requests = {}
plan_requests = {}
_tracker_lock = threading.Lock()

_tracker_lock 的存在说明 tracker 会被多个线程访问——Leader 主循环与各 teammate 的 daemon 线程都会读写这些字典,加锁保证状态迁移的原子性。

关闭协议:从生成 request_id 到线程退出

第 1 步:Leader 发起关闭请求

Leader 侧的处理器 handle_shutdown_request 生成一个 8 位 request_id,登记到 tracker,再通过消息总线把 shutdown_request 写入目标 teammate 的收件箱(见 agents/s10_team_protocols.py):

shutdown_requests = {}

def handle_shutdown_request(teammate: str) -> str:
    req_id = str(uuid.uuid4())[:8]
    shutdown_requests[req_id] = {"target": teammate, "status": "pending"}
    BUS.send("lead", teammate, "Please shut down gracefully.",
             "shutdown_request", {"request_id": req_id})
    return f"Shutdown request {req_id} sent (status: pending)"

要点:

  • str(uuid.uuid4())[:8]:取 UUID 前 8 位作为短 ID,足够在单机团队场景内唯一,又便于日志阅读。
  • 消息先落 tracker 再发送,且初始状态就是 pending——即使响应迟迟不来,Leader 也能凭 request_id 查询这条请求当前处于什么状态。

第 2 步:teammate 收到请求并回应

teammate 在工具分发处处理 shutdown_response 工具调用:更新 tracker 状态,并把带同一 request_idshutdown_response 消息发回 Lead(见 agents/s10_team_protocols.py):

if tool_name == "shutdown_response":
    req_id = args["request_id"]
    approve = args["approve"]
    with _tracker_lock:
        if req_id in shutdown_requests:
            shutdown_requests[req_id]["status"] = "approved" if approve else "rejected"
    BUS.send(
        sender, "lead", args.get("reason", ""),
        "shutdown_response", {"request_id": req_id, "approve": approve},
    )
    return f"Shutdown {'approved' if approve else 'rejected'}"

这里有两个值得注意的细节(从源码结构看):

  1. if req_id in shutdown_requests 的防御性判断意味着伪造或过期的 request_id 不会污染 tracker——未知 ID 只会把响应消息发出去,不会改变任何状态。
  2. reason 字段让「拒绝」也携带语义:teammate 可以解释为什么不现在停。

第 3 步:teammate 线程如何真正退出

协议不止是「换个状态」。teammate 的执行循环在每次执行工具后检查是否批准了关闭(见 agents/s10_team_protocols.py):

if block.name == "shutdown_response" and block.input.get("approve"):
    should_exit = True
...
member = self._find_member(name)
if member:
    member["status"] = "shutdown" if should_exit else "idle"
    self._save_config()

也就是说:teammate 会先执行完当前这一步(把已进行中的工具结果回写给模型),然后线程退出,config.json 中该成员状态落为 shutdown(自然退出则为 idle)。这正是「graceful」的含义——不写半截文件、不让配置过时。

Leader 侧的两个协议工具

s10 中 Leader 有 12 个工具,其中两个直接服务于关闭协议(见 agents/s10_team_protocols.pyTOOL_HANDLERS工具定义):

工具 角色 作用
shutdown_request 发起方 对指定 teammate 生成 request_id 并发送关闭请求,返回可跟踪的 ID
shutdown_response 查询方 Leader 用它按 request_id 查询关闭请求的当前状态(而非发送响应)

注意命名上的一点不对称:shutdown_response 在 teammate 侧是「发送回应」,在 Leader 侧是「查询状态」。这个不对称在源码中体现为 Leader 的 shutdown_response 处理器实际调用的是 _check_shutdown_statusagents/s10_team_protocols.py),返回 tracker 中该 request_id 的 JSON 状态。

计划审批协议:同一模式,方向相反

计划审批与关闭协议共用同一 FSM,但方向相反:请求方是 teammate,审批方是 Leader。teammate 调用 plan_approval 工具提交计划时,request_id 由执行端生成并登记到 plan_requests(见 agents/s10_team_protocols.py):

if tool_name == "plan_approval":
    plan_text = args.get("plan", "")
    req_id = str(uuid.uuid4())[:8]
    with _tracker_lock:
        plan_requests[req_id] = {"from": sender, "plan": plan_text, "status": "pending"}
    BUS.send(
        sender, "lead", plan_text, "plan_approval_response",
        {"request_id": req_id, "plan": plan_text},
    )
    return f"Plan submitted (request_id={req_id}). Waiting for lead approval."

Leader 侧用同一个 request_id 完成 review(见 agents/s10_team_protocols.py):

plan_requests = {}

def handle_plan_review(request_id: str, approve: bool, feedback: str = "") -> str:
    with _tracker_lock:
        req = plan_requests.get(request_id)
    if not req:
        return f"Error: Unknown plan request_id '{request_id}'"
    with _tracker_lock:
        req["status"] = "approved" if approve else "rejected"
    BUS.send(
        "lead", req["from"], feedback, "plan_approval_response",
        {"request_id": request_id, "approve": approve, "feedback": feedback},
    )
    return f"Plan {req['status']} for '{req['from']}'"

对比关闭协议,有三点差异:

  1. 发起方反转request_id 由 teammate 生成、Leader 查询并响应;
  2. 未知 ID 的报错路径handle_plan_review 对不存在的 request_id 直接返回错误字符串(Error: Unknown plan request_id ...),而不是静默忽略——审批是「不可错过」的动作,报错比忽略更安全;
  3. feedback 字段:拒绝时 Leader 可以附上修改意见,teammate 收到后可修订并重新提交(会生成新的 request_id)。

teammate 的系统提示词也显式写入了协议义务(agents/s10_team_protocols.py):

sys_prompt = (
    f"You are '{name}', role: {role}, at {WORKDIR}. "
    f"Submit plans via plan_approval before major work. "
    f"Respond to shutdown_request with shutdown_response."
)

即:重大工作前先提交计划;收到 shutdown_request 必须用 shutdown_response 回应。协议不仅靠代码强制,也靠提示词约定。

s10 相对 s09 的变化

Component Before (s09) After (s10)
Tools 9 12(+shutdown_req / +shutdown_response / +plan)
Shutdown 仅自然退出(Natural exit only) Request-response 握手
Plan gating Submit/review with approval(提交/审批)
Correlation 每个请求一个 request_id
FSM pending → approved / rejected

工具数从 9 到 12,与源码一一对应:s09 的 9 个 Leader 工具(bash、read_file、write_file、edit_file、spawn_teammate、list_teammates、send_message、read_inbox、broadcast)加上 s10 新增的 shutdown_requestshutdown_responseplan_approval,正好 12 个(agents/s10_team_protocols.py)。

协议的地基:MessageBus 与 5 种消息类型

s10 的协议消息不是凭空传输的,全部复用 s09 引入的文件收件箱消息总线。消息类型集合定义在 agents/s10_team_protocols.py(与 agents/s09_agent_teams.py 完全一致):

VALID_MSG_TYPES = {
    "message",
    "broadcast",
    "shutdown_request",
    "shutdown_response",
    "plan_approval_response",
}

MessageBus.send 会把消息(含 typefromcontenttimestamp 以及协议扩展字段如 request_idapprove)以 JSONL 追加写入 .team/inbox/<接收者>.jsonlread_inbox 读出后默认清空(drain 语义,agents/s10_team_protocols.py)。这种「append-only 文件信箱」带来两个协议相关的好处:

  • 通信经过文件系统而非进程内存,teammate 即使崩溃,未读消息仍留在 JSONL 里,重启后可继续消费;
  • 非法消息类型在入口处就被拒绝Error: Invalid type ...),保证协议消息类型不会被随意扩展污染。

交互入口还支持两个观察命令(agents/s10_team_protocols.py):/team 打印团队成员与状态,/inbox 不 drain 地查看 Leader 收件箱内容,方便人工核对协议消息原文。

协议的可信边界:测试中的伪造与重放防护

虽然 s10 的 s10 本体实现较简,但仓库后续版本(s13 团队运行时)把这套协议的边界条件固化为可执行测试,见 tests/test_agent_teams_runtime.py。这些用例印证了「request_id + type + status 三者配合」才是协议完整的防线:

  • 跨成员响应无效tests/test_agent_teams_runtime.py):针对 alice 的关闭请求,bob 发来 shutdown_response 时,request_id 状态保持 pending——响应必须来自被请求的那个 teammate;
  • 伪造 request_id 被忽略tests/test_agent_teams_runtime.py):request_id: "req_unknown"shutdown_request 被标记 Ignored,teammate 状态不变;同一请求重放时也不会被二次应用;
  • 不匹配的计划响应不能解除门控tests/test_agent_teams_runtime.py):携带 req_staleplan_approval_responseIgnoredplan_gates 保持 pending,即旧的/伪造的 ID 无法让 teammate 提前开工。

这三条防线的语义可以概括为:ID 关联一条回复与一条请求,type 防止错位响应改变状态,status 防止重复响应被二次应用(这一表述也出现在 s13_agent_teams/README.md 的协议章节)。

后续演进:s13 中的类型化协议与计划门控

s10 的协议是「状态可见」的,s13 进一步把它变成「执行可强制」的。按 s13_agent_teams/README.md,s13 将请求收敛为类型化的 ProtocolState(含 request_idtypesendertargetstatuspayload 等字段),并引入计划门控(plan gate):当 teammate 的计划状态为 required / pending / rejected 时,工具分发层会直接拦截 bashwrite_fileedit_file 三类变更型工具(返回 Blocked: plan status is ...),只允许读文件与提交/修订计划。也就是说,s10 建立的「提交计划—审批—放行」模式,在 s13 里从约定升级为运行时硬约束。

动手运行 s10

环境与依赖

依赖见 requirements.txtanthropic>=0.25.0python-dotenv>=1.0.0pyyaml>=6.0。运行前提(从源码结构看):

  • 设置 MODEL_ID 环境变量(源码中 MODEL = os.environ["MODEL_ID"] 为硬性读取,缺失会直接报错);
  • 可选设置 ANTHROPIC_BASE_URL 指向兼容端点,支持 .env 加载。

运行方式

cd learn-claude-code
python agents/s10_team_protocols.py

进入交互式 REPL(提示符 s10 >>),按原文档的剧本逐步验证协议:

  1. Spawn alice as a coder. Then request her shutdown. —— 观察 Leader 生成 request_id、发送 shutdown_request
  2. List teammates to see alice's status after shutdown approval —— 确认 alice 批准后状态变为 shutdown
  3. Spawn bob with a risky refactoring task. Review and reject his plan. —— 体验计划被拒(plan_approvalapprove=false 与 feedback);
  4. Spawn charlie, have him submit a plan, then approve it. —— 体验计划批准后正常执行;
  5. 随时输入 /team 查看成员状态,/inbox 查看 Leader 收件箱中的协议消息原文;qexit 退出。

观察重点是三条协议消息在终端日志中的流转:shutdown_request(lead → teammate,含 request_id)、shutdown_response(teammate → lead,回带同一 request_idapprove)、plan_approval_response(双向携带 request_idplan / approve / feedback 字段)。

小结

s10 的协议设计回答了一个多 Agent 系统的通用问题:如何在多个自主模型之间做「可追溯的协商」。答案是三件套——请求方生成的 request_id、按 ID 索引的 tracker、以及所有协议共用的 pending → approved | rejected FSM。关闭握手与计划审批只是它的两个实例;仓库后续的 s13 团队运行时(类型化 ProtocolState + 计划门控 + 伪造/重放防护测试)证明,这套 s10 奠定的关联模式可以进一步扩展为带运行时强制力的团队控制协议。若要继续深入,建议按仓库顺序阅读 s13_agent_teams/README.md 中的协议章节与 agents/s09_agent_teams.py 中 MessageBus 的完整实现。

登录后查看全文
热门项目推荐
相关项目推荐