Agent Teams
Multi-Agent PlatformPersistent Specialist Teammates|509 LOC|8 tools
Teammates persist beyond one prompt, have identity, and coordinate through durable channels.
s01 > s02 > s03 > s04 > s05 > s06 > s07 > s08 > s09 > s10 > s11 > s12 > s13 > s14 > [ s15 ] > s16 > s17 > s18 > s19
What You'll Learn
- How persistent teammates differ from disposable subagents
- How JSONL-based inboxes give agents a durable communication channel
- How the team lifecycle moves through spawn, working, idle, and shutdown
- How file-based coordination lets multiple agent loops run side by side
Sometimes one agent is not enough. A complex project -- say, building a feature that involves frontend, backend, and tests -- needs multiple workers running in parallel, each with its own identity and memory. In this chapter you will build a team system where agents persist beyond a single prompt, communicate through file-based mailboxes, and coordinate without sharing a single conversation thread.
The Problem
Subagents from s04 are disposable: you spawn one, it works, it returns a summary, and it dies. It has no identity and no memory between invocations. Background tasks from s13 can keep work running in the background, but they are not persistent teammates making their own LLM-guided decisions.
Real teamwork needs three things: (1) persistent agents that outlive a single prompt, (2) identity and lifecycle management so you know who is doing what, and (3) a communication channel between agents so they can exchange information without the lead manually relaying every message.
The Solution
The harness maintains a team roster in a shared config file and gives each teammate an append-only JSONL inbox. When one agent sends a message to another, it simply appends a JSON line to the recipient's inbox file. The recipient drains that file before every LLM call.
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
Step 1. TeammateManager maintains config.json with the team roster. It tracks every teammate's name, role, and current status.
// 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. spawn() creates a teammate entry in the roster and starts its agent loop in a separate thread. From this point on, the teammate runs independently -- it has its own conversation history, its own tool calls, and its own LLM interactions.
// 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. MessageBus provides append-only JSONL inboxes. send() appends a single JSON line to the recipient's file; read_inbox() reads all accumulated messages and then empties the file ("drains" it). The storage format is intentionally simple -- the teaching focus here is the mailbox boundary, not storage cleverness.
// 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 4. Each teammate checks its inbox before every LLM call. Any received messages get injected into the conversation context so the model can see and respond to them.
// 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);
Read Together
- If you still treat a teammate like s04's disposable subagent, revisit
entity-map.mdto see how they differ. - If you plan to continue into s16-s18, keep
team-task-lane-model.mdopen -- it separates teammate, protocol request, task, runtime slot, and worktree lane into distinct concepts. - If you are unsure how a long-lived teammate differs from a live runtime slot, pair this chapter with
s13a-runtime-task-model.md.
How It Plugs Into The Earlier System
This chapter is not just "more model calls." It adds durable executors on top of work structures you already built in s12-s14.
lead identifies work that needs a long-lived worker
->
spawn teammate
->
write roster entry in .team/config.json
->
send inbox message / task hint
->
teammate drains inbox before its next loop
->
teammate runs its own agent loop and tools
->
result returns through team messages or task updates
Keep the boundary straight:
- s12-s14 gave you tasks, runtime slots, and schedules
- s15 adds durable named workers
- s15 is still mostly lead-assigned work
- structured protocols arrive in s16
- autonomous claiming arrives in s17
Teammate vs Subagent vs Runtime Slot
| Mechanism | Think of it as | Lifecycle | Main boundary |
|---|---|---|---|
| subagent | a disposable helper | spawn -> work -> summary -> gone | isolates one exploratory branch |
| runtime slot | a live execution slot | exists while background work is running | tracks long-running execution, not identity |
| teammate | a durable worker | can go idle, resume, and keep receiving work | has a name, inbox, and independent loop |
What Changed From s14
| Component | Before (s14) | After (s15) |
|---|---|---|
| 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
npm run s15
- 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:
- Spawn persistent teammates that each run their own independent agent loop
- Send messages between agents through durable JSONL inboxes
- Track teammate status through a shared config file
- Coordinate multiple agents without funneling everything through a single conversation
What's Next
Your teammates can now communicate freely, but they lack coordination rules. What happens when you need to shut a teammate down cleanly, or review a risky plan before it executes? In s16, you will add structured protocols -- request-response handshakes that bring order to multi-agent negotiation.
Key Takeaway
Teammates persist beyond one prompt, each with identity, lifecycle, and a durable mailbox -- coordination is no longer limited to a single parent loop.