Learn Claude Code
s09

Agent Teams

Collaboration

Teammates + Mailboxes

52 LOC9 toolsTeammateManager + file-based mailbox
When one agent can't finish, delegate to persistent teammates via async mailboxes

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

"When the task is too big for one, delegate to teammates" -- persistent teammates + async mailboxes.

Harness layer: Team mailboxes -- multiple models, coordinated through files.

Problem

Subagents (s04) are disposable: spawn, work, return summary, die. No identity, no memory between invocations. Background tasks (s08) run shell commands but can't make LLM-guided decisions.

Real teamwork needs: (1) persistent agents that outlive a single prompt, (2) identity and lifecycle management, (3) a communication channel between agents.

Solution

Teammate lifecycle:
  spawn -> WORKING -> IDLE -> WORKING -> ... -> SHUTDOWN

Communication:
  .team/
    config.json           <- team roster + statuses
    inbox/
      alice.jsonl         <- append-only, drain-on-read
      bob.jsonl
      lead.jsonl

              +--------+    send("alice","bob","...")    +--------+
              | alice  | -----------------------------> |  bob   |
              | loop   |    bob.jsonl << {json_line}    |  loop  |
              +--------+                                +--------+
                   ^                                         |
                   |        BUS.read_inbox("alice")          |
                   +---- alice.jsonl -> read + drain ---------+

How It Works

  1. TeammateManager maintains config.json with the team roster.
type ToolInput = Record<string, any>;

type ToolSpec = {
  name: string;
  description: string;
  input_schema: Record<string, unknown>;
};

const tool: ToolSpec = {
  name: "spawn_teammate",
  description: "agent team",
  input_schema: { type: "object", properties: {} }
};

async function handleS09Step(input: ToolInput) {
  return team.spawn(input.name, input.role);
  return tool.name;
}
  1. spawn() creates a teammate and starts its agent loop in a thread.
type ToolInput = Record<string, any>;

type ToolSpec = {
  name: string;
  description: string;
  input_schema: Record<string, unknown>;
};

const tool: ToolSpec = {
  name: "spawn_teammate",
  description: "agent team",
  input_schema: { type: "object", properties: {} }
};

async function handleS09Step(input: ToolInput) {
  return team.spawn(input.name, input.role);
  return tool.name;
}
  1. MessageBus: append-only JSONL inboxes. send() appends a JSON line; read_inbox() reads all and drains.
type ToolInput = Record<string, any>;

type ToolSpec = {
  name: string;
  description: string;
  input_schema: Record<string, unknown>;
};

const tool: ToolSpec = {
  name: "spawn_teammate",
  description: "agent team",
  input_schema: { type: "object", properties: {} }
};

async function handleS09Step(input: ToolInput) {
  return team.spawn(input.name, input.role);
  return tool.name;
}
  1. Each teammate checks its inbox before every LLM call, injecting received messages into context.
type ToolInput = Record<string, any>;

type ToolSpec = {
  name: string;
  description: string;
  input_schema: Record<string, unknown>;
};

const tool: ToolSpec = {
  name: "spawn_teammate",
  description: "agent team",
  input_schema: { type: "object", properties: {} }
};

async function handleS09Step(input: ToolInput) {
  return team.spawn(input.name, input.role);
  return tool.name;
}

What Changed From s08

ComponentBefore (s08)After (s09)
Tools69 (+spawn/send/read_inbox)
AgentsSingleLead + N teammates
PersistenceNoneconfig.json + JSONL inboxes
ThreadsBackground cmdsFull agent loops per thread
LifecycleFire-and-forgetidle -> working -> idle
CommunicationNonemessage + broadcast

Try It

cd learn-claude-code
tsx agents/s09_agent_teams.ts
  1. Spawn alice (coder) and bob (tester). Have alice send bob a message.
  2. Broadcast "status update: phase 1 complete" to all teammates
  3. Check the lead inbox for any messages
  4. Type /team to see the team roster with statuses
  5. Type /inbox to manually check the lead's inbox