Skip to Content

Swarm

from loomflow.architecture import Swarm, Handoff

Peer agents pass control via a handoff tool. OpenAI Swarm reference (late 2024, experimental); Anthropic Agent Teams (Feb 2026) is the production answer that improved on the original swarm idea by adding lightweight coordination.

For the conceptual page see Swarm.

Production warning. Use only for exploratory or research-mode systems where flow can’t be pre-specified. For production, prefer Supervisor (clear authority) or Router (single specialist owns the answer). Swarm has goal-drift and deadlock failure modes that hierarchical / graph topologies don’t.


Class signature

class Swarm: name: str = "swarm" def __init__( self, *, agents: dict[str, Agent | Handoff], entry_agent: str, max_handoffs: int = 8, detect_cycles: bool = True, pass_full_history: bool = True, handoff_tool_name: str = "handoff", ) -> None: ...

Constructor parameters

agents

Typedict[str, Agent | Handoff]
Defaultrequired

Peer agents keyed by name. Plain Agent values get an empty Handoff config (legacy untyped behaviour); wrap with Handoff(...) to declare typed handoffs (per-target tools with structured argument schemas). Must be non-empty.

entry_agent

Typestr
Defaultrequired

Name of the peer that receives the first user message. Must be a key in agents; raises ValueError otherwise.

max_handoffs

Typeint
Default8

Maximum total handoffs before the run terminates with session.interruption_reason = "max_handoffs". Must be >= 0. 0 disables handoffs entirely (the entry agent owns the response).

detect_cycles

Typebool
DefaultTrue

When True (default), a repeating chain (A→B→A→B) trips cycle detection and terminates the run with session.interruption_reason = "swarm_cycle". Detection looks at the last 4 handoffs. Set to False if you genuinely want long handoff sequences.

pass_full_history

Typebool
DefaultTrue

When True (default), each receiving agent gets the full chain of prior outputs concatenated as its prompt. When False, only the most recent prior output is passed. Per-handoff input_filter (see Handoff) overrides this default.

handoff_tool_name

Typestr
Default"handoff"

Name of the generic handoff tool used in legacy mode (when no peer declares input_type). In typed mode (any Handoff has input_type=), per-target tools transfer_to_<key> are generated instead, using the peer’s key in the agents dict.


Methods

declared_workers

def declared_workers(self) -> dict[str, Agent]: return dict(self._agents)

Returns the peer mapping (the Handoff-wrapped agents are unwrapped to the underlying Agent).

run

  1. Set active_agent = agents[entry_agent].
  2. Active turn. The active agent runs to completion with handoff tools injected. The model can call them (or not) freely.
  3. Detect handoff. If a handoff tool was called, switch active agent to the named target.
  4. Cycle / cap check. If detect_cycles and a repeating chain detected → terminate. If handoff_count >= max_handoffs → terminate.
  5. Loop or terminate.

Per-handoff events: swarm.active, swarm.handoff. Cycle/cap events: swarm.cycle_detected, swarm.max_handoffs_reached.


Handoff

@dataclass class Handoff: agent: Agent input_type: type[BaseModel] | None = None input_filter: Callable | None = None description: str | None = None tool_name: str | None = None

Per-peer handoff configuration.

FieldTypeDefaultDescription
agentAgentrequiredThe peer Agent.
input_typetype[BaseModel] | NoneNonePydantic model for typed handoff payloads. When set, the generated handoff tool’s input schema mirrors this model. Calling models emit structured payloads instead of free-form strings. The validated payload is exposed to input_filter and surfaces in the swarm.handoff event.
input_filterCallable[[list[str], BaseModel | None], str] | NoneNoneOptional (history, payload) → prompt callback for selective context forwarding. Default behaviour respects the swarm’s pass_full_history flag.
descriptionstr | NoneNoneOverride the generated tool’s description. Useful when the agent’s name is opaque ("billing_v2") but the description should be user-friendly.
tool_namestr | NoneNoneOverride the auto-generated tool name. Default is "transfer_to_<key>" where <key> is the peer’s key in the swarm’s agents dict.

Two handoff modes

Legacy mode

When no peer has input_type=, the swarm exposes a single generic handoff(target, message) tool. The model picks the target name and writes a free-form message.

team = Team.swarm( agents={"triage": triage, "billing": billing, "tech": tech}, entry_agent="triage", model="claude-opus-4-7", )

Typed mode

When ANY peer declares input_type=, the swarm switches to per-target tools transfer_to_<key>(...) with the peer’s input schema. Mixed mode is allowed. Peers without input_type still get the generic handoff tool.

class BillingHandoff(BaseModel): customer_id: str issue_summary: str priority: Literal["low", "medium", "high"] team = Team.swarm( agents={ "triage": triage, "billing": Handoff(billing, input_type=BillingHandoff), "tech": Handoff(tech, input_type=TechHandoff), }, entry_agent="triage", model="claude-opus-4-7", )

Source

loomflow/architecture/swarm.py

When (sparingly) to use Swarm. Exploratory pipelines where the flow shape will change weekly, or research-mode systems where you’re testing what handoff patterns emerge. For production multi-agent: use Supervisor or Router.

Last updated on