Supervisor
from loomflow.architecture import SupervisorSupervisor puts one coordinating agent in charge of a team of
worker Agents. The coordinator breaks the task down, hands pieces
to workers with a delegate(worker, instructions) tool, and either
writes the final answer or passes a worker’s answer through with
forward_message(worker). Use it when a task spans several
specialities (research, writing, review) that need to be combined.
Several delegations in the same coordinator turn run in parallel.
For the conceptual page see Supervisor.
Class signature
class Supervisor:
name: str = "supervisor"
def __init__(
self,
*,
workers: dict[str, Agent],
base: Architecture | None = None,
instructions_template: str | None = None,
delegate_tool_name: str = "delegate",
forward_tool_name: str = "forward_message",
worker_registry: dict[str, Any] | None = None,
role_to_worker_id: dict[str, str] | None = None,
allow_spawn: bool = False,
max_spawned: int = 5,
spawn_template: Agent | None = None,
) -> None: ...Constructor parameters
workers
| Type | dict[str, Agent] |
| Default | required |
Role-name → fully-built Agent mapping. Names must be valid Python
identifiers (the model emits them as the worker argument to
delegate / forward_message). Must be non-empty.
base
| Type | Architecture | None |
| Default | None (resolves to ReAct()) |
The architecture the supervisor itself runs (default ReAct). Wrap
the Supervisor inside Reflexion
to learn better delegation across runs.
instructions_template
| Type | str | None |
| Default | None (uses built-in default) |
Format string with {worker_descriptions}. It may also use
{delegate_tool} and {forward_tool}, which are filled with the
configured tool names (the default template does, so it always names
the tools the model actually has). The template is filled with
str.format when it formats cleanly (write literal braces as {{ /
}}); when it doesn’t, as with a single-brace JSON example, only
those two placeholders are replaced and the rest is left as written. The default teaches the supervisor to
delegate effectively. The agent’s own instructions are prepended
to this template (so domain context survives).
delegate_tool_name
| Type | str |
| Default | "delegate" |
Customize to avoid clashes with user-defined tools that happen to
have the same name. The model calls this with
(worker, instructions) to dispatch a worker. The default prompt
refers to the tool by this name.
forward_tool_name
| Type | str |
| Default | "forward_message" |
The supervisor calls this with a worker name to return that worker’s last output verbatim as the supervisor’s final response, skipping a synthesis round-trip. A successful call ends the supervisor’s turn (the model isn’t called again). (Before 0.17 the model was called once more after it.) In LangChain’s “benchmarking-multi-agent-architectures” benchmark this gave about 50% better quality on tasks where the supervisor would otherwise paraphrase a worker’s output.
allow_spawn / max_spawned / spawn_template
| Type | bool / int / Agent | None |
| Default | False / 5 / None |
With allow_spawn=True the coordinator also gets a
spawn_worker(role, instructions) tool for creating new specialist
workers during a run. Spawned workers last only for that run and are
never added to the permanent roster. max_spawned caps spawns per
run (going over returns an error message to the model).
spawn_template is an Agent whose model and tools spawned workers
copy, along with its permissions, hooks, approval handler,
guardrails, rate limiter, audit log, max_turns and timeout; without
it they use the coordinator’s model and get no tools. A spawned
worker belongs to the user whose run created it: any other caller,
anonymous included, is refused.
worker_registry / role_to_worker_id
Internal wiring set by Team.supervisor
when persistent_subagents=True (the default), so each worker keeps
one conversation per user across delegations. You normally don’t
pass these yourself.
Methods
declared_workers
def declared_workers(self) -> dict[str, Agent]:
return dict(self._workers)Returns the worker mapping for AgentGraph / introspection.
add_worker
def add_worker(self, name: str, agent: Agent) -> None: ...Register a worker between runs. The new worker becomes available
for delegate(name, ...) on the next agent.run(). Calling it
during a run is undefined, because the supervisor’s prompt is built
when the run starts.
| Parameter | Type | Description |
|---|---|---|
name | str | Must be a valid Python identifier. Raises ValueError otherwise. |
agent | Agent | The worker. |
remove_worker
def remove_worker(self, name: str) -> Agent | None: ...Unregister a worker. Returns the removed Agent if it was registered,
None otherwise. Same lifecycle rules as add_worker.
run
- Open a shared event channel so worker events stream out
alongside the base architecture’s events (otherwise workers’
MODEL_CHUNKandTOOL_CALLevents would be lost). - Build the
delegateandforward_messagetools. Both share alast_outputsdict (delegate writes, forward_message reads). - Augment the supervisor’s tool host with these two extras.
- Run
base.run(session, deps_with_extras, prompt). Forward all events. - If the supervisor called
forward_message(worker), overridesession.outputwith that worker’s verbatim last output. The call ends the turn: the supervisor’s model isn’t called again after it. Aforward_messagefor a worker with no output yet fails as a tool error, so the model can delegate first.
Multiple delegate calls in one supervisor turn run in parallel
because the base ReAct’s tool dispatch is already parallel
(anyio.create_task_group over all tool calls in a turn). Emit two
delegate calls in one turn and both workers run concurrently.
Replay correctness
Each delegate call is a tool call of the supervisor’s base loop,
so it is journaled like any other tool dispatch. With a
SqliteRuntime or PostgresRuntime, a crash in the middle of a
supervisor turn replays cleanly: completed delegations return their
cached results and only the unfinished work runs again. Each worker
runs under its own session id derived from the supervisor’s.
Cost model
The supervisor itself runs an architecture (default ReAct, so N
turns of model and tools), and each delegate call also starts a
full worker agent.run(). For a typical 3-worker pipeline (research →
write → review): roughly 2 supervisor turns + 3 worker runs ≈ 2–4× a single-agent ReAct run.
Example
from loomflow import Agent
from loomflow.architecture import Supervisor
from loomflow.team import Team
researcher = Agent("Find sources, summarise key claims.",
model="claude-opus-4-7", tools=[search])
writer = Agent("Draft the article.",
model="claude-opus-4-7")
reviewer = Agent("Critique the draft for clarity and factual accuracy.",
model="gpt-4o")
# Via the Team facade
team = Team.supervisor(
workers={"researcher": researcher, "writer": writer, "reviewer": reviewer},
instructions=(
"Manage the article pipeline. Delegate research first, then "
"writing, then a review pass. Synthesize the final answer."
),
model="claude-opus-4-7",
)
# Or via the explicit Architecture form (for nesting)
agent = Agent(
"Manage the article pipeline.",
model="claude-opus-4-7",
architecture=Supervisor(workers={
"researcher": researcher,
"writer": writer,
"reviewer": reviewer,
}),
)The two forms are interchangeable. Use the Team facade for single-level teams; use the explicit form for recursive composition.
Source
loomflow/architecture/supervisor.py
Workers are real Agent instances. Each has its own model,
memory, tools, and architecture. Workers can themselves use Reflexion,
ActorCritic, or even another Supervisor. See
Recursive composition.