learn-claude-code s10 团队协议:用 request_id 请求-响应握手实现关机协商与计划审批
本文基于 learn-claude-code 课程仓库的 s10 团队协议文档 展开。s10 解决多 Agent 团队中的一个具体问题:当 Lead(领导)和多个 Teammate(队友,各自运行在独立线程中的 LLM 循环)已经可以通信之后,如何为「关机」和「计划审批」这类高风险控制操作建立结构化握手,而不是靠杀线程或口头约定。读完本文,你可以理解同一个 pending → approved | rejected 状态机如何以统一模式套用到两种协议上,并能直接运行 agents/s10_team_protocols.py 体验完整的请求-响应协商流程。
1. 问题背景:为什么「能通信」不等于「能协调」
s09(Agent Teams)已经实现了持久命名队友:每个队友在自己的线程里跑一个 agent loop,通过文件收件箱(JSONL inbox)互相发消息,团队成员状态记录在 .team/config.json 中。但 s09 缺少结构化协调,暴露出两个具体问题:
关机问题:直接杀线程会留下写了一半的文件和过期的 config.json。正确做法需要一次握手——领导请求关机,队友批准(收尾后退出)或拒绝(继续干活)。
计划审批问题:领导说「重构认证模块」,队友立刻开干。对高风险变更,应该先提交计划、经领导审查后再动手。
两者的结构完全一样:一方发一个带唯一 ID 的请求,另一方引用同一个 ID 响应。这就是 s10 的核心洞察:一个 FSM(有限状态机),两种用途,同一套 request_id 关联模式。
2. 解决方案总览:两种协议,一个状态机
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}}
注意两个协议方向相反:关机请求由 Lead 发起、Teammate 应答;计划审批由 Teammate 提交、Lead 审查。但消息结构一致:请求方生成 request_id 并跟踪状态,响应方引用同一 request_id 返回 approve/reject。
3. 消息底座:MessageBus 与五类消息
协议必须跑在 s09 的消息总线上。agents/s10_team_protocols.py#L87-L128 中的 MessageBus 为每个成员维护一个 JSONL 收件箱(.team/inbox/<name>.jsonl):
send()以追加模式写入一行 JSON 消息,包含type、from、content、timestamp以及可选的extra元数据(request_id 就放在这里);read_inbox(clear=True)读取并清空收件箱(drain 语义);broadcast()向除发送者外的所有成员逐一投递。
s10 允许的五种消息类型在 agents/s10_team_protocols.py#L73-L79 中声明,send() 会校验类型合法性:
| 消息类型 | 用途 |
|---|---|
message |
普通文本消息 |
broadcast |
广播给全体队友 |
shutdown_request |
关机请求(s10 新增语义) |
shutdown_response |
关机批准/拒绝(s10 新增语义) |
plan_approval_response |
计划提交与审批响应(s10 新增语义) |
请求状态的跟踪器是两个全局字典,外加一把线程锁(agents/s10_team_protocols.py#L81-L84):
# -- Request trackers: correlate by request_id --
shutdown_requests = {}
plan_requests = {}
_tracker_lock = threading.Lock()
_tracker_lock 的必要性来自运行时结构:队友循环运行在独立的 daemon 线程里(见下文 spawn()),而 Lead 的工具分发在主线程,两个线程会并发读写同一组 tracker 字典,因此所有对 tracker 的读写都包在 with _tracker_lock: 中。
4. 关机协议实现:从请求到线程退出
4.1 Lead 侧:发起关机请求
Lead 的 shutdown_request 工具调用 handle_shutdown_request()(agents/s10_team_protocols.py#L352-L360):生成 8 位 request_id、登记 pending 状态、通过总线投递请求。
def handle_shutdown_request(teammate: str) -> str:
req_id = str(uuid.uuid4())[:8]
with _tracker_lock:
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 to '{teammate}' (status: pending)"
返回给 Lead 模型的字符串里同时带了 request_id 和 pending 状态,Lead 可以用它做后续追踪。
4.2 Teammate 侧:approve/reject 响应
队友的工具集里新增了 shutdown_response 工具(入参 request_id、approve、可选 reason),其执行分支在 agents/s10_team_protocols.py#L237-L247:
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'}"
这里有一个值得注意的细节:队友自己也会更新 tracker(req_id in shutdown_requests 检查),因为队友线程和 Lead 共享同一进程内存中的 tracker;随后它把带同一 request_id 的响应发回 Lead 的收件箱。队友的系统提示词里也明确写了协议义务(agents/s10_team_protocols.py#L178-L182):
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."
)
4.3 审批后的收尾:状态落盘与线程退出
队友循环在每轮模型调用前 drain 自己的收件箱,把收到的消息(含 shutdown_request)注入对话。关键在于 agents/s10_team_protocols.py#L215-L221:当队友执行 shutdown_response(approve=True) 时,循环置 should_exit = True,退出循环后把成员状态写成 shutdown 并保存 config.json:
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()
这正对应文档中「关机握手」的收益:不是直接杀线程,而是让队友在批准的时机自行收尾,最终状态在 config.json 中从 working 变为 shutdown(而非自然结束时的 idle),可通过 list_teammates 区分。
4.4 Lead 侧:查询请求状态
Lead 侧还注册了同名但语义不同的 shutdown_response 工具,它不是「响应」,而是查询——_check_shutdown_status()(agents/s10_team_protocols.py#L377-L379)按 request_id 返回 tracker 中该请求的当前状态。也就是说,在 s10 的 Lead 工具表里 shutdown_response 是「查状态」,在队友工具表里才是「回复审批」,阅读源码时不要混淆。
5. 计划审批实现:方向相反、结构相同
5.1 Teammate 侧:提交计划
队友的 plan_approval 工具(入参只有一个 plan 文本)生成 request_id、登记计划、通知 Lead(agents/s10_team_protocols.py#L248-L257):
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."
工具返回提示「等待领导审批」,引导模型在拿到批复前不要动手。从源码结构看还有一个实现细节:计划提交消息和审批回复消息使用的消息类型都是 plan_approval_response,靠 extra 中的 request_id 与 approve 字段区分方向——类型校验(VALID_MSG_TYPES)管的是「是不是协议消息」,而方向与状态由 request_id 关联保证。
5.2 Lead 侧:审查与批复
Lead 的 plan_approval 工具(入参 request_id、approve、可选 feedback)调用 handle_plan_review()(agents/s10_team_protocols.py#L363-L374):
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 直接返回错误(而不是抛异常中断 Lead 循环),以及把 req["from"] 作为回复目的地——Lead 不需要记住谁提交了计划,tracker 里记着。
6. 工具面变化:相对 s09 的 diff
文档给出的变更对比表如下,可与源码逐一核对:
| 组件 | 之前 (s09) | 之后 (s10) |
|---|---|---|
| Tools | 9 | 12 (+shutdown_req/resp +plan) |
| 关机 | 仅自然退出 | 请求-响应握手 |
| 计划门控 | 无 | 提交/审查与审批 |
| 关联 | 无 | 每个请求一个 request_id |
| FSM | 无 | pending -> approved/rejected |
源码印证:
- s09 的 Lead 工具表为 9 个(agents/s09_agent_teams.py#L310-L343:bash、read_file、write_file、edit_file、spawn_teammate、list_teammates、send_message、read_inbox、broadcast);
- s10 在 agents/s10_team_protocols.py#L383-L396 的
TOOL_HANDLERS中扩为 12 个,新增shutdown_request(发起请求)、shutdown_response(查状态)、plan_approval(审批); - 队友侧从 s09 的 6 个工具(agents/s09_agent_teams.py#L223-L238)扩为 8 个(agents/s10_team_protocols.py#L260-L279),新增
shutdown_response(审批回复)和plan_approval(提交计划); - 队友循环新增了
should_exit逻辑,而 s09 的循环在自然结束后一律置idle(agents/s09_agent_teams.py#L202-L205 与 s10 的差异)。
7. 运行与实操
7.1 环境准备
依赖见 requirements.txt:anthropic>=0.25.0、python-dotenv>=1.0.0、pyyaml>=6.0。程序入口会读取环境变量(agents/s10_team_protocols.py#L61-L68):
MODEL_ID:必填,模型 ID,未设置会直接抛 KeyError;ANTHROPIC_BASE_URL:可选,若设置则通过base_url构造 client,并自动pop掉ANTHROPIC_AUTH_TOKEN(适配兼容网关的常见做法);- API Key 由
anthropicSDK 从环境(如ANTHROPIC_API_KEY)读取,load_dotenv()会优先加载本地.env(override=True)。
7.2 启动
cd learn-claude-code
python agents/s10_team_protocols.py
启动后进入交互式 CLI(提示符 s10 >>),支持两个监控命令(agents/s10_team_protocols.py#L463-L477):
/team:打印.team/config.json中所有成员的 name、role、status;/inbox:只读打印 Lead 收件箱(read_inbox("lead", clear=False),不清空);- 输入
q/exit或空行退出。
7.3 推荐体验路径(英文 prompt 对 LLM 效果通常更好)
Spawn alice as a coder. Then request her shutdown.—— 验证关机请求-响应握手;List teammates to see alice's status after shutdown approval—— 验证 alice 的状态从working变为shutdown;Spawn bob with a risky refactoring task. Review and reject his plan.—— 验证计划提交与拒绝路径;Spawn charlie, have him submit a plan, then approve it.—— 验证批准路径;- 随时输入
/team监控成员状态,输入/inbox查看协议消息原文。
每次关机/审批请求都携带一个 8 位 request_id,你可以在 /inbox 输出里核对请求与响应是否引用同一 ID,这正是协议关联性的直接证据。
8. 设计要点与延伸
从 s10 的实现可以提炼出三点可复用的模式:
- 同一 request_id 关联模式覆盖多个协议域。tracker 只是
{req_id: {…, "status": "pending|approved|rejected"}},新增一种「需要对方点头」的操作(如危险命令审批、资源占用审批)时,只需新登记一个 tracker 和一对消息类型,状态机逻辑可以整体复用。 - 协议消息必须结构化,自由文本只做载荷。所有协商消息都经过
VALID_MSG_TYPES校验,request_id 放在extra元数据里而非混入content,避免靠「猜模型意图」来解析协商结果。 - 状态必须落盘,且退出要可区分。
config.json中shutdown与idle是不同终态;队友线程自行收尾并保存状态,而不是被外力杀死,保证「写了一半的文件」问题在协议层面被显式处理。
需要说明的是,s10 是教学阶段的轻量实现:tracker 存活于单进程内存中,计划审批是「模型自觉」级别的门控(提示词要求队友先提交计划)。课程在后续章节 s13 Agent Teams 中把协议升级为可强制执行的运行时机制——ProtocolState 数据结构统一管理 pending_requests,工具分发层会真正拦截未获批计划期间的 bash/write_file/edit_file 调用,并对关机请求做了「必须匹配活跃协议」和「防重复响应」的校验,相关测试可见 tests/test_agent_teams_runtime.py 中 test_shutdown_request_must_match_active_protocol、test_plan_approval_cannot_cross_assignment_boundary 等用例。若你要在自己的 harness 中落地团队协议,建议把 s10 的 request_id 模式作为协议骨架,再参考 s13 的思路把「门控」从事后约定变为工具分发层的硬性拦截。
附:关键文件索引
| 内容 | 路径 |
|---|---|
| s10 协议文档(本文主体依据) | docs/zh/s10-team-protocols.md |
| s10 可运行实现(12 个 Lead 工具 + 8 个队友工具) | agents/s10_team_protocols.py |
| s09 对照实现(9 个 Lead 工具) | agents/s09_agent_teams.py |
| 协议运行时演进(s13,Plan 门控强制执行) | s13_agent_teams/README.md |
| 团队运行时测试(含协议匹配/边界用例) | tests/test_agent_teams_runtime.py |
| 依赖清单 | requirements.txt |
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