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. lifetime defaults 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.