Learn Claude Code
s09

Agent Teams

Collaboration

Teammates + Mailboxes

278 LOC9 toolsTypeScriptTeammateManager + 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.
class TeammateManager {
  private configPath = resolve(teamDir, "config.json");
  private config: TeamConfig = this.loadConfig();
}
  1. spawn() creates a teammate and starts its agent loop in a thread.
spawn(name: string, role: string, prompt: string) {
  this.config.members.push({ name, role, status: "working" });
  this.saveConfig();
  void this.teammateLoop(name, role, prompt);
  return `Spawned '${name}' (role: ${role})`;
}
  1. MessageBus: append-only JSONL inboxes. send() appends a JSON line; read_inbox() reads all and drains.
class MessageBus {
  send(sender: string, to: string, content: string) {
    appendFileSync(resolve(this.inboxDir, `${to}.jsonl`), `${JSON.stringify({ from: sender, content })}\n`);
  }

  readInbox(name: string) {
    const lines = readFileSync(resolve(this.inboxDir, `${name}.jsonl`), "utf8").split(/\r?\n/).filter(Boolean);
    writeFileSync(resolve(this.inboxDir, `${name}.jsonl`), "", "utf8");
    return lines.map((line) => JSON.parse(line));
  }
}
  1. Each teammate checks its inbox before every LLM call, injecting received messages into context.
for (const message of BUS.readInbox(name)) {
  messages.push({ role: "user", content: JSON.stringify(message) });
}
const response = await client.messages.create(...);

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
cd agents-ts
npm install
npm run s09
  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