Team Protocols
Multi-Agent PlatformShared Request-Response Rules|557 LOC|10 tools
A protocol request is a structured message with an ID; the response must reference the same ID.
s01 > s02 > s03 > s04 > s05 > s06 > s07 > s08 > s09 > s10 > s11 > s12 > s13 > s14 > s15 > [ s16 ] > s17 > s18 > s19
What You'll Learn
- How a request-response pattern with a tracking ID structures multi-agent negotiation
- How the shutdown protocol lets a lead gracefully stop a teammate
- How plan approval gates risky work behind a review step
- How one reusable FSM (a simple status tracker with defined transitions) covers both protocols
In s15 your teammates can send messages freely, but that freedom comes with chaos. One agent tells another "please stop," and the other ignores it. A teammate starts a risky database migration without asking first. The problem is not communication itself -- you solved that with inboxes -- but the lack of coordination rules. In this chapter you will add structured protocols: a standardized message wrapper with a tracking ID that turns loose messages into reliable handshakes.
The Problem
Two coordination gaps become obvious once your team grows past toy examples:
Shutdown. Killing a teammate's thread leaves files half-written and the config roster stale. You need a handshake: the lead requests shutdown, and the teammate approves (finishes current work and exits cleanly) or rejects (keeps working because it has unfinished obligations).
Plan approval. When the lead says "refactor the auth module," the teammate starts immediately. But for high-risk changes, the lead should review the plan before any code gets written.
Both scenarios share an identical structure: one side sends a request carrying a unique ID, the other side responds referencing that same ID. That single pattern is enough to build any coordination protocol you need.
The Solution
Both shutdown and plan approval follow one shape: send a request with a request_id, receive a response referencing that same request_id, and track the outcome through a simple status machine (pending -> approved or pending -> rejected).
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}}
How It Works
Step 1. The lead initiates shutdown by generating a unique request_id and sending the request through the teammate's inbox. The request is tracked in a dictionary so the lead can check its status later.
// Tree-shaken bundle: chapter wiring + only used runtime code.
// agents_self_contained/_runtime.ts
import Anthropic from "@anthropic-ai/sdk";
import dotenv from "dotenv";
import { execSync, spawn, spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import process from "node:process";
import readline from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";
dotenv.config({ override: true });
var WORKDIR = process.cwd();
var DEFAULT_MODEL = "claude-3-5-sonnet-latest";
var anthropicClient = null;
function getModelId() {
return process.env.MODEL_ID || DEFAULT_MODEL;
}
function getAnthropicClient() {
if (anthropicClient) {
return anthropicClient;
}
anthropicClient = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN || "missing-api-key",
baseURL: process.env.ANTHROPIC_BASE_URL || void 0
});
return anthropicClient;
}
function createLoopContext() {
return { workdir: WORKDIR, messages: [], meta: {} };
}
function safePath(relativePath) {
const resolved = path.resolve(WORKDIR, relativePath);
Step 2. The teammate receives the request in its inbox and responds with approve or reject. The response carries the same request_id so the lead can match it to the original request -- this is the correlation that makes the protocol reliable.
// Tree-shaken bundle: chapter wiring + only used runtime code.
// agents_self_contained/_runtime.ts
import Anthropic from "@anthropic-ai/sdk";
import dotenv from "dotenv";
import { execSync, spawn, spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import process from "node:process";
import readline from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";
dotenv.config({ override: true });
var WORKDIR = process.cwd();
var DEFAULT_MODEL = "claude-3-5-sonnet-latest";
var anthropicClient = null;
function getModelId() {
return process.env.MODEL_ID || DEFAULT_MODEL;
}
function getAnthropicClient() {
if (anthropicClient) {
return anthropicClient;
}
anthropicClient = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN || "missing-api-key",
baseURL: process.env.ANTHROPIC_BASE_URL || void 0
});
return anthropicClient;
}
function createLoopContext() {
return { workdir: WORKDIR, messages: [], meta: {} };
}
function safePath(relativePath) {
const resolved = path.resolve(WORKDIR, relativePath);
Step 3. Plan approval follows the identical pattern but in the opposite direction. The teammate submits a plan (generating a request_id), and the lead reviews it (referencing the same request_id to approve or reject).
// Tree-shaken bundle: chapter wiring + only used runtime code.
// agents_self_contained/_runtime.ts
import Anthropic from "@anthropic-ai/sdk";
import dotenv from "dotenv";
import { execSync, spawn, spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import process from "node:process";
import readline from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";
dotenv.config({ override: true });
var WORKDIR = process.cwd();
var DEFAULT_MODEL = "claude-3-5-sonnet-latest";
var anthropicClient = null;
function getModelId() {
return process.env.MODEL_ID || DEFAULT_MODEL;
}
function getAnthropicClient() {
if (anthropicClient) {
return anthropicClient;
}
anthropicClient = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN || "missing-api-key",
baseURL: process.env.ANTHROPIC_BASE_URL || void 0
});
return anthropicClient;
}
function createLoopContext() {
return { workdir: WORKDIR, messages: [], meta: {} };
}
function safePath(relativePath) {
const resolved = path.resolve(WORKDIR, relativePath);
In this teaching demo, one FSM shape covers both protocols. A production system might treat different protocol families differently, but the teaching version intentionally keeps one reusable template so you can see the shared structure clearly.
Read Together
- If plain messages and protocol requests are starting to blur together, revisit
glossary.mdandentity-map.mdto see how they differ. - If you plan to continue into s17 and s18, read
team-task-lane-model.mdfirst so autonomy and worktree lanes do not collapse into one idea. - If you want to trace how a protocol request returns to the main system, pair this chapter with
s00b-one-request-lifecycle.md.
How It Plugs Into The Team System
The real upgrade in s16 is not "two new message types." It is a durable coordination path:
requester starts a protocol action
->
write RequestRecord
->
send ProtocolEnvelope through inbox
->
receiver drains inbox on its next loop
->
update request status by request_id
->
send structured response
->
requester continues based on approved / rejected
That is the missing layer between "agents can chat" and "agents can coordinate reliably."
Message vs Protocol vs Request vs Task
| Object | What question it answers | Typical fields |
|---|---|---|
MessageEnvelope | who said what to whom | from, to, content |
ProtocolEnvelope | is this a structured request / response | type, request_id, payload |
RequestRecord | where is this coordination flow now | kind, status, from, to |
TaskRecord | what actual work item is being advanced | subject, status, blockedBy, owner |
Do not collapse them:
- a protocol request is not the task itself
- the request store is not the task board
- protocols track coordination flow
- tasks track work progression
What Changed From s15
| Component | Before (s15) | After (s16) |
|---|---|---|
| Tools | 9 | 12 (+shutdown_req/resp +plan) |
| Shutdown | Natural exit only | Request-response handshake |
| Plan gating | None | Submit/review with approval |
| Correlation | None | request_id per request |
| FSM | None | pending -> approved/rejected |
Try It
npm run s16
- Ask the agent to run
pwd - Ask it to run
ls -la - Ask it to summarize the current workspace in one sentence
- Ask it to create
notes/hello.tsand print the file content
What You've Mastered
At this point, you can:
- Build request-response protocols that use a unique ID for correlation
- Implement graceful shutdown through a two-step handshake
- Gate risky work behind a plan approval step
- Reuse a single FSM pattern (
pending -> approved/rejected) for any new protocol you invent
What's Next
Your team now has structure and rules, but the lead still has to babysit every teammate -- assigning tasks one by one, nudging idle workers. In s17, you will make teammates autonomous: they scan the task board themselves, claim unclaimed work, and resume after context compression without losing their identity.
Key Takeaway
A protocol request is a structured message with a tracking ID, and the response must reference that same ID -- that single pattern is enough to build any coordination handshake.