s09
Agent Teams
CollaborationTeammates + 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
- TeammateManager maintains config.json with the team roster.
class TeammateManager {
private configPath = resolve(teamDir, "config.json");
private config: TeamConfig = this.loadConfig();
}
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})`;
}
- 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));
}
}
- 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
| Component | Before (s08) | After (s09) |
|---|---|---|
| Tools | 6 | 9 (+spawn/send/read_inbox) |
| Agents | Single | Lead + N teammates |
| Persistence | None | config.json + JSONL inboxes |
| Threads | Background cmds | Full agent loops per thread |
| Lifecycle | Fire-and-forget | idle -> working -> idle |
| Communication | None | message + broadcast |
Try It
cd learn-claude-code
cd agents-ts
npm install
npm run s09
Spawn alice (coder) and bob (tester). Have alice send bob a message.Broadcast "status update: phase 1 complete" to all teammatesCheck the lead inbox for any messages- Type
/teamto see the team roster with statuses - Type
/inboxto manually check the lead's inbox