workflows/swarm¶
Source: src/workflows/swarm.ts
Several members on one job, for as many rounds as you allow.
Every other combinator here decides who does what: fanOut hands out the
subtasks, orchestrate has a planner write them, chain fixes the order.
A swarm decides none of it. The members are told the same goal, given a
{@link Board} to talk on and a {@link Claims} to take work from, and what
they divide between them is theirs.
That makes it the one combinator whose value is an open question, so it is built to be compared against the thing it might not beat: with no board, no claims and one round, a swarm is a fan-out. That degenerate case is a test, and it is the control arm of any experiment run with this.
Three things are decided here rather than left to a prompt, each one measured on a run before it was written down:
Members remember.
lifetimedefaults to"workflow"here and nowhere else. A member that forgets the last round cannot build on what it saw, and a swarm of amnesiacs is a fan-out that costs more.What is new is handed over, not fetched. A member is given what the board has for it at the top of its task. Measured: given something that arbitrates, members stopped reading the board entirely - they take, they are refused, they take something else. Making them spend a call to find out what happened would be charging them for the workflow’s bookkeeping.
A member that is gone holds nothing. Whatever it still had is released when it drops out or when the swarm ends, and the result says what that was. Measured: two of three members never released what they took, and claims left by a member that has closed are work nobody will do and nobody can take.
membersOutput¶
function
export function membersOutput(members: readonly SwarmMember[]): string { /* … */ }
What every member last said, under the name it posted under.
The id and not the agent, because three copies of one agent share the name and a reader finds a member on the board by its id.
MemberSpec¶
type
export type MemberSpec = {
/** Whose copies these are. */
agent: Agent;
/** How many copies of it. Each gets its own id, its own memory, its own place. */
count: number;
};
How many of one agent stand on the board. A swarm of one is a run.
swarm¶
function
export async function swarm(options: SwarmOptions): Promise<SwarmResult> { /* … */ }
Runs a swarm.
A round is one ask per live member, through mapConcurrent. A member whose
turn fails is asked again the next round; two failed turns in a row and it
drops out rather than costing every remaining round, keeping the turn that
failed as its result.
The pool closes everything in a finally, cancellation included, and the
claims of whoever is gone are released before the result is built.
SwarmClaim¶
type
export type SwarmClaim = {
/** The thing that was taken. */
key: string;
/** The member that had it when the swarm ended. */
heldBy: string;
/** True when the swarm released it because the member was gone. */
released: boolean;
};
What a member was holding when it stopped, and whether anyone took it back.
SwarmEnd¶
type
export type SwarmEnd = "until" | "rounds" | "members" | "signal";
Why the swarm stopped, apart from whether it went well.
SwarmMember¶
type
export type SwarmMember = {
/** The subagent id, which is also the name it posts and claims under. */
id: string;
/** Which agent it is a copy of. */
agent: string;
/** Its last turn. A member that dropped out early keeps the turn that failed. */
result: Result;
};
One member, and the last thing it said.
SwarmOptions¶
type
export type SwarmOptions = WorkflowOptions & {
/** Who is on it, and how many of each. */
members: readonly MemberSpec[];
/** The one thing every member is told. Broad enough to admit several routes. */
goal: string;
/** Turns per member. Defaults to 3, because "forever" must not be reachable. */
rounds?: number;
/** Members asked at once. Defaults to 4. */
concurrency?: number;
/** Defaults to a fresh in-memory board. Pass one to read it afterwards. */
board?: Board;
/** What there is to take. Absent, the members are on their honour. */
claims?: Claims;
/** Has the goal been reached? Read from the board, after every round. */
until?: (board: Board) => boolean;
/**
* How many `result`s a member may post in one turn. Absent, as many as the
* board takes: a member describing files posts one per file. A debate wants
* 1, because a vote posted again is not an argument.
*/
resultsPerTurn?: number;
};
Who is on it, what they are told, and how long they have.
SwarmResult¶
type
export type SwarmResult = WorkflowResult & {
/** One per member, in roster order, the ones that failed included. */
members: readonly SwarmMember[];
/** Every post, in order. The run's social history. */
posts: readonly Post[];
/** What was taken, and what was still held when its holder stopped. */
claims: readonly SwarmClaim[];
/** How many rounds actually ran. */
rounds: number;
/** Whether `until` fired. Reaching the round cap is not success. */
converged: boolean;
/** Which cap ended it. */
stoppedBy: SwarmEnd;
};
What the swarm did, and what it left behind.
As a Result: every member’s last turn, labelled by the name it posted
under, a failed one marked as such; ok is false when any member failed or
was never asked, error the first failure’s. steps is every turn of every
round, and usage their sum over the swarm’s own wall time.