learn-claude-code/s16_team_protocols
gui-yue 1baf1aca5a
Some checks are pending
CI / build (push) Waiting to run
Test / python-smoke (push) Waiting to run
Test / web-build (push) Waiting to run
Follow up PR #265: refine chapters, diagrams, and add S20 (#283)
* feat: s01-s14 docs quality overhaul — tool pipeline, single-agent, knowledge & resilience

Rewrite code.py and README (zh/en/ja) for s01-s14, each chapter building
incrementally on the previous. Key fixes across chapters:

- s01-s04: agent loop, tool dispatch, permission pipeline, hooks
- s05-s08: todo write, subagent, skill loading, context compact
- s09-s11: memory system, system prompt assembly, error recovery
- s12-s14: task graph, background tasks, cron scheduler

All chapters CC source-verified. Code inherits fixes forward (PROMPT_SECTIONS,
json.dumps cache, real-state context, can_start dep protection, etc.).

* feat: s15-s19 docs quality overhaul — multi-agent platform: teams, protocols, autonomy, worktree, MCP tools

Rewrite code.py and README (zh/en/ja) for s15-s19, the multi-agent platform
chapters. Each chapter inherits all previous fixes and adds one mechanism:

- s15: agent teams (TeamCreate, teammate threads, shared task list)
- s16: team protocols (plan approval, shutdown handshake, consume_inbox)
- s17: autonomous agents (idle polling, auto-claim, consume_lead_inbox)
- s18: worktree isolation (git worktree, bind_task, cwd switching, safety)
- s19: MCP tools (MCPClient, normalize_mcp_name, assemble_tool_pool, no cache)

All appendix source code references verified against CC source. Config priority
corrected: claude.ai < plugin < user < project < local.

* fix: 5 regressions across s05-s19 — glob safety, todo validation, memory extraction, protocol types, dep crash

- s05-s09: glob results now filter with is_relative_to(WORKDIR) (inherited from s02)
- s06-s08: todo_write validates content/status required fields (inherited from s05)
- s09: extract_memories uses pre-compression snapshot instead of compacted messages
- s16: submit_plan docstring clarifies protocol-only (not code-level gate)
- s17-s19: match_response restores type mismatch validation (from s16)
- s17-s19: claim_task deps list handles missing dep files without crashing

* fix: s12 Todo V2 logic reversal, s14/s15 cron range validation, s18/s19 worktree name validation

- s12 README (zh/en/ja): fix Todo V2 direction — interactive defaults to Task,
  non-interactive/SDK defaults to TodoWrite. Fix env var name to
  CLAUDE_CODE_ENABLE_TASKS (not TODO_V2).
- s14/s15: add _validate_cron_field with per-field range checks (minute 0-59,
  hour 0-23, dom 1-31, month 1-12, dow 0-6), step > 0, range lo <= hi.
  Replace old try/except validation that only caught exceptions.
- s18/s19: add validate_worktree_name() to remove_worktree and keep_worktree,
  not just create_worktree.

* fix: align s16-s19 teaching tool consistency

* fix pr265 chapter diagrams

* Add comprehensive s20 harness chapter

* Fix chapter smoke test regressions

* Clarify README tutorial track transition

---------

Co-authored-by: Haoran <bill-billion@outlook.com>
2026-05-20 21:45:38 +08:00
..
images Follow up PR #265: refine chapters, diagrams, and add S20 (#283) 2026-05-20 21:45:38 +08:00
code.py Follow up PR #265: refine chapters, diagrams, and add S20 (#283) 2026-05-20 21:45:38 +08:00
README.en.md Follow up PR #265: refine chapters, diagrams, and add S20 (#283) 2026-05-20 21:45:38 +08:00
README.ja.md Follow up PR #265: refine chapters, diagrams, and add S20 (#283) 2026-05-20 21:45:38 +08:00
README.md Follow up PR #265: refine chapters, diagrams, and add S20 (#283) 2026-05-20 21:45:38 +08:00

s16: Team Protocols — Teammates Need Agreements

中文 · English · 日本語

s01 → ... → s14 → s15 → s16s17 → s18 → s19 → s20

"Teammates need agreements" — request-response pattern drives all negotiation.

Harness Layer: Protocols — Structured handshakes between agents.


The Problem

s15's teammates can work, but coordination is loose: Lead sends a message, teammate replies, no structured protocol. Two scenarios expose the gap:

Shutdown: Lead wants Alice to shut down. Killing the thread outright leaves half-written files on disk. A handshake is needed: Lead sends a request, Alice confirms after wrapping up.

Plan approval: Bob wants to refactor the auth module, a high-risk operation. Lead should review Bob's plan first, approve before Bob proceeds.

Both scenarios share the same structure: one side sends a request, the other replies, both linked by the same ID. A state machine tracks: pending → approved / rejected.


The Solution

Team Protocols Overview

Teaching code continues the agent capability arc from earlier chapters and adds structured protocols on top of S15's team communication. To stay focused on the protocol mechanism, it omits full error recovery, memory, and skill systems. Added: ProtocolState (request state tracking), dispatch_message (routes incoming messages by type to handlers), match_response (correlates response to request via request_id, with type validation).

Two protocols, one mechanism:

Protocol Direction Purpose
shutdown_request / response Lead → Teammate Graceful shutdown handshake
plan_approval_request / response Teammate → Lead Plan approval protocol example

Teaching version demonstrates the request-response message flow for plan approval, but does not implement execution gating (intercepting bash/write_file when not approved). Real CC has a permission gating mechanism for teammates.


How It Works

ProtocolState: Request State

Each protocol request creates a state record tracking who sent it, to whom, current status, and payload:

@dataclass
class ProtocolState:
    request_id: str      # Unique ID, e.g. "req_004281"
    type: str            # "shutdown" | "plan_approval"
    sender: str          # Sender
    target: str          # Recipient
    status: str          # pending | approved | rejected
    payload: str         # Plan text or shutdown reason
    created_at: float    # Timestamp

pending_requests: dict[str, ProtocolState] = {}

A record is created when sending a request, found via request_id when receiving a response, and its status updated.

Four-Step Protocol Flow

Using shutdown as an example, the full chain:

1. Lead sends request
   req_id = new_request_id()           # "req_004281"
   pending_requests[req_id] = ProtocolState(type="shutdown", status="pending", ...)
   BUS.send("lead", "alice", "shutdown_request", metadata={"request_id": req_id})

2. Teammate receives → dispatch
   inbox = BUS.read_inbox("alice")
   msg_type = msg["type"]              # "shutdown_request"
   → routed to handle_shutdown_request()

3. Teammate replies
   BUS.send("alice", "lead", "shutdown_response",
            metadata={"request_id": req_id, "approve": True})

4. Lead receives response → match
   match_response("shutdown_response", req_id, approve=True)
   pending_requests[req_id].status = "approved"

request_id is the correlation key across the entire chain: the request carries it out, the response carries it back.

dispatch_message: Route by Type

A teammate's inbox receives both plain messages and protocol messages. handle_inbox_message dispatches by message type:

def handle_inbox_message(name, msg, messages):
    msg_type = msg.get("type", "message")
    req_id = msg.get("metadata", {}).get("request_id", "")

    if msg_type == "shutdown_request":
        BUS.send(name, "lead", "Shutting down.", "shutdown_response",
                 {"request_id": req_id, "approve": True})
        return True   # Stop the loop

    if msg_type == "plan_approval_response":
        approve = msg["metadata"].get("approve", False)
        messages.append({"role": "user",
            "content": "[Plan approved]" if approve else "[Plan rejected]"})
    return False       # Continue

Adding a new protocol type means adding a new if branch.

match_response: Type Validation

match_response doesn't just find state by request_id, it also validates that the response type matches the request type:

def match_response(response_type, request_id, approve):
    state = pending_requests.get(request_id)
    if not state:
        return
    if state.type == "shutdown" and response_type != "shutdown_response":
        return  # type mismatch, skip
    if state.type == "plan_approval" and response_type != "plan_approval_response":
        return
    if state.status != "pending":
        return  # already resolved, skip duplicate
    state.status = "approved" if approve else "rejected"

A shutdown_response cannot accidentally approve a plan_approval request.

Unified Inbox Consumer: consume_lead_inbox

Both the check_inbox tool and the main loop call the same consume_lead_inbox() function, routing protocol messages before returning remaining content. This prevents messages from being consumed without protocol state updates:

def consume_lead_inbox(route_protocol=True) -> list[dict]:
    msgs = BUS.read_inbox("lead")
    if route_protocol:
        for msg in msgs:
            meta = msg.get("metadata", {})
            req_id = meta.get("request_id", "")
            msg_type = msg.get("type", "")
            if req_id and msg_type.endswith("_response"):
                match_response(msg_type, req_id, meta.get("approve", False))
    return msgs

The main loop also injects inbox messages into history so the LLM can see and react to them.

Teammate Idle Loop: Wait Instead of Exit

s15's teammates exit after 10 rounds. s16's teammates enter idle waiting after the LLM returns a non-tool_use response: poll inbox, respond to shutdown_request and exit, or continue working on new messages.

LLM returns non-tool_use
  → idle: poll inbox every second
  → receives shutdown_request → reply shutdown_response → exit
  → receives new message → inject into messages → continue LLM turn

Teaching version omits idle_notification to Lead. Real CC sends idle_notification when idle, so Lead knows the teammate is free for new tasks.

Putting It Together

1. Lead: "Have Alice create a file, then shut her down"
2. Lead → spawn_teammate("alice", "backend", "Create config.py")
3. alice thread starts → write_file("config.py", "...") → done → idle
4. Lead → request_shutdown("alice")
   → BUS.send("shutdown_request", {request_id: "req_000142"})
5. alice idle poll receives → handle_shutdown_request
   → BUS.send("shutdown_response", {request_id: "req_000142", approve: True})
6. Lead consume_lead_inbox → match_response("req_000142", approve=True)
   → pending_requests["req_000142"].status = "approved"
   → inbox message injected into history, LLM sees shutdown result

Shutdown handshake complete: request → confirm → shutdown. Every step tracked by request_id.


Changes from s15

Component Before (s15) After (s16)
Coordination Loose text messages Structured request-response protocol
Request tracking None ProtocolState + pending_requests dict
Message routing All treated as text dispatch_message routes by type
Shutdown Natural exit or kill thread request_id handshake mechanism
Plan approval None Message flow example (no execution gating)
New message types message, result + shutdown_request/response, plan_approval_request/response
Teammate lifecycle Max 10 rounds Idle loop (waits for inbox messages)
Lead inbox check_inbox and main loop read separately Unified consume_lead_inbox
Lead tools 14 (s15) 14 (core tool set plus request_shutdown, request_plan, review_plan)
Teammate tools 4 (s15) + submit_plan (5)

Try It

cd learn-claude-code
python s16_team_protocols/code.py

Try these prompts:

  1. Spawn alice as a backend dev. Ask her to create a file. Then request her shutdown.
  2. Spawn bob with a refactoring task. Have him submit a plan first. Then review and approve it.

What to observe: Is the shutdown handshake complete (request → confirm → shutdown)? Does pending_requests state transition correctly? Is request_id consistent between request and response? Can the idle teammate receive shutdown_request?


What's Next

In s15-s16, Lead must assign tasks to each teammate. "Alice does this, Bob does that." With 10 unclaimed tasks on the board, Lead has to manually assign each one.

What if teammates could check the board and claim tasks themselves? Lead only needs to create tasks; teammates discover, claim, and complete them on their own.

s17 Autonomous Agents → Self-organizing teammates, no leader assignment needed.

Deep Dive into CC Source

CC's team protocol implementation (teammateMailbox.ts, 1184 lines) shares the same core structure as the teaching version: request_id + approve/reject request-response pattern. Differences:

Shutdown protocol: CC's shutdown is three-way communication (teammateMailbox.ts:720-763, SendMessageTool.ts:268-430). Lead sends shutdown_request, teammate replies shutdown_approved (or shutdown_rejected with reason), system sends teammate_terminated to notify all parties. After confirmation, system cleans up pane (tmux/iTerm2), unassigns tasks, removes member from team config (useInboxPoller.ts:677-800). Teaching version uses shutdown_response as a unified name; real source splits into shutdown_approved and shutdown_rejected as two separate message types.

Plan approval: In the real source, plan approval request is generated by ExitPlanModeV2Tool.ts:263-312 when a plan-mode-required teammate exits plan mode. useInboxPoller.ts:599-661 currently auto-writes approval and passes the request to Lead as context (regular message). SendMessageTool.ts:434-518 retains explicit approve/reject response capability — approval can simultaneously set permissionMode (e.g. "approved but run in plan mode"), response can include feedback string for teammate to revise and resubmit. Not a simple "Lead manually uses review_plan tool" flow.

Message format: CC's protocol messages are structured JSON (with Zod schema validation), teaching version uses simple type + metadata dict. Field names are also inconsistent: permission uses request_id (teammateMailbox.ts:453-462), shutdown and plan approval use requestId (teammateMailbox.ts:684-763).

Execution gating: CC's teammates have full permission gating. Unapproved high-risk operations are intercepted, not optional. Teaching version only demonstrates the message flow without execution interception.

Generality: Teaching version's single FSM (pending → approved | rejected) maps to two protocols. This simplification is correct. CC's protocol messages all share the same request id correlation mechanism.