Swarm
from loomflow.architecture import Swarm, HandoffPeer 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
| Type | dict[str, Agent | Handoff] |
| Default | required |
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
| Type | str |
| Default | required |
Name of the peer that receives the first user message. Must be a
key in agents; raises ValueError otherwise.
max_handoffs
| Type | int |
| Default | 8 |
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
| Type | bool |
| Default | True |
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
| Type | bool |
| Default | True |
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
| Type | str |
| 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
- Set
active_agent = agents[entry_agent]. - Active turn. The active agent runs to completion with handoff tools injected. The model can call them (or not) freely.
- Detect handoff. If a handoff tool was called, switch active agent to the named target.
- Cycle / cap check. If
detect_cyclesand a repeating chain detected → terminate. Ifhandoff_count >= max_handoffs→ terminate. - Loop or terminate.
Per-handoff events: swarm.active, swarm.handoff. Cycle/cap
events: swarm.cycle_detected, swarm.max_handoffs_reached.
Related types
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 = NonePer-peer handoff configuration.
| Field | Type | Default | Description |
|---|---|---|---|
agent | Agent | required | The peer Agent. |
input_type | type[BaseModel] | None | None | Pydantic 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_filter | Callable[[list[str], BaseModel | None], str] | None | None | Optional (history, payload) → prompt callback for selective context forwarding. Default behaviour respects the swarm’s pass_full_history flag. |
description | str | None | None | Override the generated tool’s description. Useful when the agent’s name is opaque ("billing_v2") but the description should be user-friendly. |
tool_name | str | None | None | Override 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.