learn-claude-code s10:Team Protocols —— 用统一 request_id 请求-响应协议驱动团队关闭握手与计划审批
本文基于 learn-claude-code 仓库第 s10 课「Team Protocols(团队协议)」文档展开,讲解多 Agent 团队中两个核心协调场景——优雅关闭(graceful shutdown)与计划审批(plan approval)——如何被收敛为同一个 request_id 关联模式与同一个 pending → approved | rejected 有限状态机(FSM)。读完后,你能理解 agents/s10_team_protocols.py 中协议消息从生成、入箱、响应到状态落地的完整链路,并掌握如何在本仓库中实际运行 s10 演示、用测试用例验证协议的防伪造/防重放能力。
课程定位: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--> approved或pending --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_id 的 shutdown_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'}"
这里有两个值得注意的细节(从源码结构看):
if req_id in shutdown_requests的防御性判断意味着伪造或过期的 request_id 不会污染 tracker——未知 ID 只会把响应消息发出去,不会改变任何状态。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.py 的 TOOL_HANDLERS 与 工具定义):
| 工具 | 角色 | 作用 |
|---|---|---|
shutdown_request |
发起方 | 对指定 teammate 生成 request_id 并发送关闭请求,返回可跟踪的 ID |
shutdown_response |
查询方 | Leader 用它按 request_id 查询关闭请求的当前状态(而非发送响应) |
注意命名上的一点不对称:shutdown_response 在 teammate 侧是「发送回应」,在 Leader 侧是「查询状态」。这个不对称在源码中体现为 Leader 的 shutdown_response 处理器实际调用的是 _check_shutdown_status(agents/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']}'"
对比关闭协议,有三点差异:
- 发起方反转:
request_id由 teammate 生成、Leader 查询并响应; - 未知 ID 的报错路径:
handle_plan_review对不存在的request_id直接返回错误字符串(Error: Unknown plan request_id ...),而不是静默忽略——审批是「不可错过」的动作,报错比忽略更安全; - 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_request、shutdown_response、plan_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 会把消息(含 type、from、content、timestamp 以及协议扩展字段如 request_id、approve)以 JSONL 追加写入 .team/inbox/<接收者>.jsonl;read_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_stale的plan_approval_response被Ignored,plan_gates保持pending,即旧的/伪造的 ID 无法让 teammate 提前开工。
这三条防线的语义可以概括为:ID 关联一条回复与一条请求,type 防止错位响应改变状态,status 防止重复响应被二次应用(这一表述也出现在 s13_agent_teams/README.md 的协议章节)。
后续演进:s13 中的类型化协议与计划门控
s10 的协议是「状态可见」的,s13 进一步把它变成「执行可强制」的。按 s13_agent_teams/README.md,s13 将请求收敛为类型化的 ProtocolState(含 request_id、type、sender、target、status、payload 等字段),并引入计划门控(plan gate):当 teammate 的计划状态为 required / pending / rejected 时,工具分发层会直接拦截 bash、write_file、edit_file 三类变更型工具(返回 Blocked: plan status is ...),只允许读文件与提交/修订计划。也就是说,s10 建立的「提交计划—审批—放行」模式,在 s13 里从约定升级为运行时硬约束。
动手运行 s10
环境与依赖
依赖见 requirements.txt:anthropic>=0.25.0、python-dotenv>=1.0.0、pyyaml>=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 >>),按原文档的剧本逐步验证协议:
Spawn alice as a coder. Then request her shutdown.—— 观察 Leader 生成request_id、发送shutdown_request;List teammates to see alice's status after shutdown approval—— 确认 alice 批准后状态变为shutdown;Spawn bob with a risky refactoring task. Review and reject his plan.—— 体验计划被拒(plan_approval带approve=false与 feedback);Spawn charlie, have him submit a plan, then approve it.—— 体验计划批准后正常执行;- 随时输入
/team查看成员状态,/inbox查看 Leader 收件箱中的协议消息原文;q或exit退出。
观察重点是三条协议消息在终端日志中的流转:shutdown_request(lead → teammate,含 request_id)、shutdown_response(teammate → lead,回带同一 request_id 与 approve)、plan_approval_response(双向携带 request_id 与 plan / 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 的完整实现。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00