# subagentSwarm (/docs/recipes/subagent-swarm)



`subagentSwarm` composes a pool of worker `Chatbot` instances
into a bounded-parallel work queue. You pass the workers (any
recipe-returned `Chatbot` satisfies the contract — `simpleChatbot`,
`observableChatbot`, `compliantChatbot`, even custom shapes that
implement `ask()`) plus a `decompose(userTask, workerCount)` that
maps the user task to per-worker prompts. The recipe runs the
prompts under a `maxFanOut` concurrency cap, polls an optional
cost ceiling between worker launches, and reduces the results.

Best fit: **a sub-agent swarm consumer**. The
defining pain is the runaway-loop case:
a buggy fan-out or stuck sequential loop can spend $10K in one
user turn. This recipe bakes the two application-layer
guardrails.

## Quickstart [#quickstart]

```ts
import { simpleChatbot } from "@pleach/recipes/chatbot";
import { subagentSwarm } from "@pleach/recipes/subagent-swarm";

const swarm = subagentSwarm({
  workers: [
    simpleChatbot({ systemPrompt: "You research equities. Be terse." }),
    simpleChatbot({ systemPrompt: "You research equities. Be terse." }),
    simpleChatbot({ systemPrompt: "You research equities. Be terse." }),
  ],
  maxFanOut: 2, // at most 2 workers in flight at once
  decompose: (task, workerCount) => {
    const symbols = ["AAPL", "GOOG", "MSFT"];
    return symbols.slice(0, workerCount).map((s) => `${task}: ${s}`);
  },
  reduce: (results) => results.map((r) => `- ${r}`).join("\n"),
  // Optional: application-layer runaway-loop guardrail
  perRootTurnCostCeilingUsd: 50,
  costReporter: () => myProviderCostMeter.getTotalUsd(),
});

console.log(await swarm.dispatch("Research big-tech earnings"));
```

## What it does [#what-it-does]

`dispatch(userTask)` runs the following sequence:

1. Call `decompose(userTask, workers.length)` to get the
   per-worker prompts.
2. Walk the prompts in order; launch at most `maxFanOut`
   concurrent workers (`Math.max(1, Math.min(maxFanOut,
   workers.length))`).
3. Before launching each new worker, poll `costReporter()`
   (if supplied) and compare against
   `perRootTurnCostCeilingUsd`. Over-ceiling stops launches;
   already-running workers finish.
4. Pass surviving results (output-order preserved) to
   `reduce(results, userTask)`. The default reduce is
   newline-join.

`maxFanOut` defaults to `1` (sequential) to preserve replay
determinism for buyers who haven't yet adopted the greenfield
runtime with byte-identical spawn-order normalization. Swarm
buyers tuning for throughput should set explicitly.

## Config reference [#config-reference]

```ts
interface SubagentSwarmConfig {
  workers: readonly Chatbot[];
  maxFanOut?: number; // default 1
  decompose: (
    userTask: string,
    workerCount: number,
  ) => readonly string[] | Promise<readonly string[]>;
  reduce?: (results: readonly string[], userTask: string) => string;
  perRootTurnCostCeilingUsd?: number;
  costReporter?: () => number | Promise<number>;
}

interface SubagentSwarm {
  readonly workers: readonly Chatbot[];
  dispatch(userTask: string): Promise<string>;
  reset(): Promise<void>;
}
```

## Common gotchas [#common-gotchas]

* **`workers` must be non-empty.** Construction throws
  otherwise.
* **Excess decompose prompts are dropped with a console
  warning.** If `decompose` returns more prompts than there
  are workers, the recipe does not silently round-robin —
  the excess are dropped and a warning fires.
* **The cost ceiling is application-layer best-effort.** This
  is the OSS-tier defense. The load-bearing swarm guardrail is
  `@pleach/gateway`'s `maxCostUsdPerRootTurn` (the
  commercial SKU). If a worker is mid-flight when the
  ceiling crosses, it still runs to completion — the ceiling
  only gates new launches.
* **`costReporter` is consumer-owned.** The recipe is
  provider-agnostic. Wire `costReporter` to your provider's
  per-call cost report or your billing meter. Omitting both
  `costReporter` and `perRootTurnCostCeilingUsd` disables
  ceiling enforcement entirely.
* **Default `maxFanOut: 1` is sequential.** Set explicitly
  when tuning for throughput. Higher values trade
  determinism for latency.
* **Worker sessions are NOT reset between dispatches by
  default.** Workers keep their conversational state across
  `dispatch()` calls. Call `await swarm.reset()` to force a
  fresh session on every worker on the next dispatch.
* **No `@pleach/core/spawn` substrate.** This recipe does not
  implement `spawn / race / waitAll / waitN / bounded` or the
  `SpawnEvent` schema — those are v1.x core-substrate changes.

## See also [#see-also]

* [`@pleach/recipes` overview](/docs/recipes-pleach-recipes) —
  every recipe in one page.
* [`Swarm agent`](/docs/swarm-agent) — full-system pattern.
* [`@pleach/gateway`](/docs/gateway) — the commercial SKU
  whose `maxCostUsdPerRootTurn` is the load-bearing runaway-
  loop guardrail.
* [`Subagents`](/docs/subagents) — the underlying
  attribution primitive.
