Agents
Agent Swarms
Fan an agent out to many forks at once, such as one per file in a repository, collect a one-line review from each fork, and summarize them.
A swarm runs many copies of one agent in parallel. Each fork starts with the agent’s full context, does one small job, and returns a short report. Forks are copy-on-write, so a hundred forks of a long conversation do not copy it a hundred times.
What we’ll build:
- A Pi agent that holds a team’s review rules in its conversation.
- A backend script that forks it once per file, at most 10 at a time, and asks each fork for a one-line review.
- Retries for refused forks that can never make a second copy.
- A summary of every review in the original agent’s conversation.
Install
npm add @rivet-dev/pi @earendil-works/pi-durable @earendil-works/pi-ai @earendil-works/chord rivetkit
Create the agent
import { createRegistry } from "@earendil-works/pi-durable";
import { pi } from "@rivet-dev/pi";
import { setup } from "rivetkit";
export const agent = pi({
model: "anthropic/claude-opus-5-5",
registry: createRegistry(),
actions: {
discard: (c) => {
c.destroy();
},
},
});
export const registry = setup({ use: { agent } });
registry.start();
Run the swarm
import { readFile } from "node:fs/promises";
import { ActorError, createClient } from "rivetkit/client";
import type { registry } from "./server";
const client = createClient<typeof registry>();
// How many forks may be alive at once. Each fork is a running agent.
const MAX_FORKS = 10;
const files = process.argv.slice(2);
const agent = client.agent.getOrCreate(["code-review"]);
await agent.prompt(
"You review TypeScript for our team. We care about error handling and clear names.",
{ requestId: crypto.randomUUID() },
);
// A turn in progress would be copied into every fork, so fork an idle agent.
await agent.harness.waitForIdle();
const agentId = await agent.resolve();
// One key per fork makes a retried fork safe: it can never make a second copy.
const runId = crypto.randomUUID();
const reports = await mapWithLimit(files, MAX_FORKS, async (path) => {
const fork = await forkWithRetry(agentId, ["review", runId, path]);
try {
const source = await readFile(path, "utf8");
const result = await fork.prompt(
`Review ${path} in one line.\n\n${source}`,
{
requestId: `review:${runId}:${path}`,
},
);
const line =
result.status === "done"
? result.text?.split("\n")[0]
: result.reason;
return `${path}: ${line ?? "no review"}`;
} finally {
await fork.discard();
}
});
// The summary is a normal turn, so it lands in the agent's conversation.
const summary = await agent.prompt(
`One-line reviews of every file:\n${reports.join("\n")}\n\nSummarize the biggest risks.`,
{ requestId: crypto.randomUUID() },
);
console.log(summary.status === "done" ? summary.text : summary.reason);
/** Forks the agent, waiting and retrying when storage asks the caller to slow down. */
async function forkWithRetry(sourceId: string, key: string[]) {
for (let attempt = 1; ; attempt++) {
try {
return await client.agent.fork(sourceId, { key });
} catch (error) {
if (!(error instanceof ActorError)) throw error;
// The fork exists: an earlier attempt made it, or its copy is still running.
if (
error.group === "actor" &&
(error.code === "duplicate_key" ||
error.code === "fork_pending")
) {
return client.agent.get(key);
}
// Refused before any work. Wait as long as asked, with jitter.
const overloaded =
error.group === "depot" && error.code === "overloaded";
if (!overloaded || attempt >= 5) throw error;
const metadata = error.metadata as
| { retry_after_ms?: number }
| undefined;
const waitMs =
(metadata?.retry_after_ms ?? 200) * (1 + Math.random());
await new Promise((resolve) => setTimeout(resolve, waitMs));
}
}
}
/** Runs `fn` on every item with at most `limit` running at once, keeping order. */
async function mapWithLimit<T, R>(
items: T[],
limit: number,
fn: (item: T) => Promise<R>,
): Promise<R[]> {
const results: R[] = new Array(items.length);
let next = 0;
const workers = Array.from(
{ length: Math.min(limit, items.length) },
async () => {
while (next < items.length) {
const index = next++;
results[index] = await fn(items[index]);
}
},
);
await Promise.all(workers);
return results;
}
harness.waitForIdlemakes sure the agent is not in the middle of a turn, which every fork would copy.- Each worker in
mapWithLimitforks the agent, asks the fork about one file, and discards it before it takes the next file, so at mostMAX_FORKSforks are alive. - The final
promptgoes to the original agent, so the summary lands in its conversation.
Try it
export ANTHROPIC_API_KEY=sk-ant-...
npx tsx server.ts
In another terminal, pass the files to review:
npx tsx swarm.ts src/*.ts
Handling Bursts
- Forks start immediately. Each fork becomes a running agent on your nodes as soon as it is created. Forking 500 files at once starts 500 agents at once.
MAX_FORKScaps how many forks are alive. - Keep
onForktiny. If you add anonFork, every fork in a burst runs it at the same moment, with 5 seconds each. A slow hook under load can run out of time, which fails the start and runs the hook again, adding more load. - Throttle at the caller. The same cap limits concurrent model calls and the read load on the source. Every fork reads the data it has not changed from the source’s storage, so a burst of forks concentrates reads there. Start with a small cap and raise it while watching for refusals.
- Retry refusals after the hinted delay. Under load, storage can refuse a fork with
depot.overloadedandmetadata.retry_after_ms. The client does not retry this error, soforkWithRetrywaits that long, with jitter, and tries again a few times. - Key every fork. A keyed fork can never be created twice. When a retry answers
actor.duplicate_keyoractor.fork_pending, the fork exists, and the script uses it. - Run the fan-out outside an action. An action stops after its
actionTimeout, which a large swarm can outlast. Run it from your backend as here, or from a workflow. - Discard every fork. A fork keeps its storage until it is destroyed. The
finallydiscards it even when its review fails. - Fork only agents without a sandbox. A fork of an agent with a sandbox copies the record of that sandbox, so every fork would run its tools in the same sandbox.