Build

Swarm

Hierarchical routing where one agent's tools are other agents. Triage routes to specialists; specialists may hand off back. The pattern is a Loop over Agent calls — no SwarmAgent class needed.

A user message arrives that could be billing OR technical OR a refund. You don't want one giant prompt that knows everything — that's where agents get vague and hallucinate. You want a triage agent that figures out which specialist should handle this, then hands off. The specialist focuses on its domain. That's Swarm — and it's just a Loop over Agents with a route function.

What Swarm is

Swarm = a hierarchical agent pattern where one orchestrator routes each turn to a specialist agent. The route function is sync, pure, and returns the id of the next agent (or undefined to halt). Each handoff is one iteration of a Loop:

Loop {
  current_agent = route(input)
  if current_agent is undefined → exit
  result = current_agent.run(input)
  input = result + handoff annotation
}

In agentfootprint there is no SwarmAgent class. Swarm is a recipe — swarm({ agents, route, maxHandoffs }) constructs a runnable that wraps a Loop. The 4-composition substrate (Sequence/Parallel/Conditional/Loop) is enough; we don't need a 5th primitive.

The recipe

const router = swarm({  agents: [    { id: 'triage', runner: triage },    { id: 'billing', runner: billing },    { id: 'tech', runner: tech },  ],  // Route function — pure sync over the current message. First turn goes  // to triage, then to billing or tech based on content, then halts.  route: (input) => {    const msg = input.message.toLowerCase();    if (msg.includes('[billing]')) return undefined; // billing done → halt    if (msg.includes('[tech]')) return undefined; // tech done → halt    if (msg.includes('refund') || msg.includes('bill')) return 'billing';    if (msg.includes('status') || msg.includes('error')) return 'tech';    return 'triage'; // first turn  },  maxHandoffs: 5,});

The route function is the orchestrator's brain. It inspects the current input (often the previous handoff's result) and decides which agent to dispatch next. Returning undefined halts the loop — the most recent specialist's output becomes the final answer.

maxHandoffs caps the loop so a buggy route function can't infinite-loop. Production: 3–5 handoffs covers most triage flows.

Why route is sync, not LLM-driven

route is evaluated once per branch predicate AND again in the loop's exit guard, so it cannot await — and an LLM call in there would fire several times per hand-off. Keep it as cheap as text inspection.

The standard pattern is therefore a deterministic route based on the previous specialist's output (often via convention markers like [BILLING] or [ESCALATE] in the message). The intelligence lives in the specialists; the routing is mechanical dispatch.

When you do want the model to choose, don't hand-roll the prompt: llmSwarm is this same swarm with an llmRouter wired into the right places — the roster compiles into the router's prompt from each agent's own description, the answer is validated JSON, and route stays a sync lookup of a decision already made.

Multi-tenant identity

A swarm runner's input is { message: string } — and that single message is the only thing that flows from one specialist to the next (each agent's string output becomes the next iteration's { message }). swarm() does not carry a MemoryIdentity through the handoff chain for you.

If specialists need per-tenant memory, give each agent its identity directly. Agent.run({ message, identity }) accepts an optional MemoryIdentity on its input; bind the tenant scope when you build/run each specialist (e.g. via a closure that injects it), so memory reads/writes stay isolated even though the swarm only relays the message.

When to use Swarm vs Conditional vs Sequence

SituationUse
One classifier picks ONE specialist for the whole turnConditional
Specialists may hand back to triage or to each otherSwarm (this guide)
Fixed pipeline: research → write → edit (no branching)Sequence
Multi-perspective fan-out → mergeParallel

Anti-patterns

  • Don't put expensive logic in the route function. It runs every iteration. Keep it as cheap as text inspection.
  • Don't share state between specialists via globals. Use MemoryIdentity + memory layer for cross-agent state.
  • Don't omit maxHandoffs. A buggy route returning the same agent every time runs forever.

Next steps

  • Patterns overview — Reflexion, ToT, Debate, Map-Reduce all run the same way (recipes over the substrate)
  • Conditional — single-shot routing alternative

On this page