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 |
|
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 |
|
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.cwdis 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
writeand noedit.No network. combo grants none, and a swarm is the last place to start.
Every cap has a default.
roundsis 3,concurrency4, 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.
escin the extension, anAbortSignalfrom 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.