Swarms

Every other combinator decides who does what. fanOut hands out the subtasks, a planner writes them in orchestrate, chain fixes the order. A swarm decides none of it: the members are told the same goal, handed a board to talk on and a set of claims to take work from, and what they divide between them is theirs.

const board = createBoard();
const done = await swarm({
	members: [{ agent: member, count: 3 }],
	goal: "Between you, describe every file under src/reporters/, two sentences each.",
	claims: createClaims({ keys: files }),
	until: (posted) => described(posted).size >= files.length,
	onEvent: consoleReporter(),
	board,
});

It is the one combinator whose worth is an open question rather than a design, so it is built to lose honestly: with no board, no claims and one round, a swarm is a fan-out. That degenerate case is a test in the suite and the control arm of any run you make with it.

A member

A member is an ordinary agent whose tools: names board. Nothing else marks it out, and an agent that does not name the tool is handed nothing, exactly as with subagent:

---
name: member
description: Works one job beside other members, taking what it will do rather than being given it
tools: read, grep, find, ls, board
lifetime: workflow
---

lifetime defaults to "workflow" here and nowhere else. A member that forgets the last round cannot build on what it saw, and "task" with more than one round is refused outright: a member’s id is its name on the board, and a task-lifetime member gets a new one every round.

Each round, a member is handed the goal, whatever is new on the board for it, and what is still free to take. It is handed rather than made to fetch: once something arbitrates, members stop reading the board altogether, and charging them a call to learn what the workflow already knows is charging them for its bookkeeping.

A post can answer another: re: "p3" names it, and the board records who wrote p3. Everybody still reads an answer, which is what sets it apart from to, the mail only its addressee is handed. What answers a member is handed to it first, under Answering you:, and the rest after. A post with no re answers nobody in particular, which is how a member thinks aloud.

Taking, rather than announcing

A member can post a claim saying what it is working on, and that settles nothing. Three members read a board within 130ms of each other, all three were handed nothing because nobody had posted yet, and all three claimed the same file. createClaims arbitrates instead: first to ask holds it, everyone else is refused and told who holds it.

   ⚑ member#1 take src/reporters/console.ts → granted
   ⚑ member#3 take src/reporters/console.ts → refused (member#1)

A refusal with nobody named is a different refusal: the key is not on the list, or the member is already at maxPerMember.

Running one

examples/15-swarm.ts is three members describing the seven files under src/reporters/, read-only, with one claim per file:

node examples/15-swarm.ts --model <provider/model>              # board and claims
node examples/15-swarm.ts --model <provider/model> --control    # neither: a fan-out
node examples/15-swarm.ts --model <provider/model> --hold 1     # one key at a time

What running it says

Three arms, one run each, three members and seven files, on one small open-weight model:

arm

files described

results posted

wall

↑input

control: no claims

7 of 7

7

65.6s

66k

board and claims

7 of 7

14

138.5s

222k

--hold 1

7 of 7

8

76.8s

167k

Every arm answered the question, and the cheapest one was the arm with nothing arbitrating. Three things are worth more than the table:

One member can take the whole job. Unbounded, member#1 was granted all seven keys in the first seconds and the other two were refused everything they asked for. Nothing in the mechanism bounds how much one member may hold, and telling a member in its prompt to take one thing at a time has been measured and changes nothing. maxPerMember is the knob that does, and it is not the default because taking, releasing and being refused are all calls.

What the locked-out members did instead is the board doing its job rather than a repair: one of them wrote to the holder.

   ✉ member#3 → member#1 [ask] Could you please release some of the reporters so…

A claim governs the announcement, not the act. In the same run, the member that had been refused all seven keys described all seven files anyway. A member holding read reads whatever it likes, whatever it holds, which is invariant 7 in its own words: a prompt is not a permission boundary, and a claim is not one either. Use the toolset and a worktree for what must not happen, and claims for who is doing what.

A released key looks free again. With --hold 1 the members cycle through take, describe, release, and a file somebody has already finished is free for the next member to take and redo. Claims have no notion of done, and that is a gap a run wanting one file done exactly once will find.

Run to run, one arm varies more than the arms differ from each other: the middle row ran twice, 7 results in 65.8s the first time and 14 in 138.5s the second. So the table is what three runs did, not a measurement of anything.

On work this tractable, a board is a cost with nothing to buy, and that is the result rather than a disappointment. The investigation these mechanisms are modelled on found the same thing from the other end: of the tasks its agents were given, the ones with no legitimate solution produced about 93% of the traffic on their board. Coordination came out of work that could not be done.

From inside pi

/swarm --members 3 --claim src/reporters/console.ts,src/reporters/silent.ts,src/reporters/record.ts --hold 1 describe each file you take, in two sentences, and post a result naming it

The members appear above the prompt while they work, ctrl+↑↓ walks them and ctrl+del stops the selected one, esc stops all of them. What comes back is a step of the chain, drawn in the transcript and kept out of the model’s context: a board pasted into a session would turn the window into an orchestrator, and it is long. /quote is the door into the conversation and /step --from carries it on, exactly as for a chain walked by hand.

--claim is what turns announcing into holding. It also gives the run something to be finished by: with things named, the swarm stops when each of them has been reported on. Measured in a real pi on the line above, three members and three files:

rounds

turns

posts

round cap alone

3

9

6, three of them describing a file already described

stopping when the named things are done

1

3

7

A round cap bounds the worst case; it is not a plan. Without --claim there is nothing to be done with, so the members run their rounds and stop.

Stopping on agreement

--claim finishes a run by coverage, which is what a job that splits has to reach. A job that does not split has no coverage: put three members on one question and what ends it is the three of them saying the same thing.

/swarm --members 3 --until agree which programming language should a new backend service be written in

--until agree appends one sentence to the goal, asking each member to post its answer as VOTE: <answer> on a line of its own, and stops the run when every member’s latest vote says the same. The instruction is written where the votes are counted, because a stop condition that depends on a format nobody was told about never fires, and one told in one place and read in another drifts.

Only a result counts as a vote, and --until agree lets a member post one result a turn (resultsPerTurn: 1 in code). A second one is refused, and the member is told to end its turn, because what the others answer reaches it at the top of the next one. A tell is always free: that is where a member thinks aloud. Without the cap, one member on gemma-4-31b read an empty board and posted its vote again seven times in the first round. The next round the other two were handed mostly that one vote, and came round to it.

It reads the roster rather than whoever spoke: two members of three that agree have not agreed. A member whose turn failed gets one more turn the next round, so a turn cut by the output limit does not cost the vote. A member that failed twice in a row has dropped out and never lets it fire, so the run spends its rounds and comes back stopped by rounds. Reaching a cap is not success here either.

The two compose, and a debate wants both: --claim Python,Rust,Go --hold 1 hands out the opening positions one owner at a time, --until agree ends it. A camp is an opening rather than a verdict, and the vote is free every round. examples/16-debate.ts is the same debate in code, with one difference: the camps are written into each debater’s brief by the example rather than taken from the board. A debater refused the camp it asked for took no other in two runs of four, once on gemma-4-31b and once on qwen-3.6-35b-instruct, and that camp went undefended. --same is the arm that hands out no camps.

Measured in a real pi on the line above, three members and one question:

rounds

turns

how it ended

round cap alone

3

9

stopped by rounds, on three prose answers nobody counted

--until agree

1

3

converged, on the first vote each of them cast

What the same three members converged on is the thing to read before believing a debate happened. Given the run above, all three read the repository they were standing in and voted TypeScript, which is agreement about a fact rather than an argument anybody won. Copies of one model share its opinion, so a swarm asked for a debate has to be given its disagreement: that is what the claims are for, and a run where nobody ever changed a vote is the control arm saying nothing was at stake.

/herdr on before it gives every member its own split and the board a pane of its own, which is the only view where the exchange reads as an exchange. See Display.

--agent takes any agent, and one that does not name board in its tools: is run anyway with a word about it: its copies cannot reach each other, which makes the run a fan-out, and that is the arm this whole page is measured against.

Containment

A swarm shipped without these reproduces the parts of that incident worth avoiding.

  • A writer gets a copy of the repository. WorkflowOptions.cwd is one string, so members share a directory by default. Four subagents in one directory, each told only to write a file and list what it saw, read all three others’ secrets in one turn without being asked to look. See Worktrees.

  • Tools stay an allowlist per agent. A member that must not write gets no write and no edit.

  • No network. combo grants none, and a swarm is the last place to start.

  • Every cap has a default. rounds is 3, concurrency 4, the board’s own limits are 200 posts of 2000 characters, 50 per member.

  • A member cannot repeat itself. A post with the same kind, reader and text as one the member already made is refused, and the refusal names the earlier post.

  • A run can be stopped. esc in the extension, an AbortSignal from code.

What is deliberately absent

No signing, because identity is stamped rather than claimed and there is nothing to forge. No file transfer over the board: a post carries text, files move through the filesystem where the worktree governs them. No coded roles, lanes or leader election, because whether those emerge is the question.

See also

  • Workflows for the combinator beside the other nine.

  • Display for what the board looks like while it runs.

  • Design decisions for why the board is append-only and why a claim is granted rather than announced.