Skip to Content

Supervisor

from loomflow.architecture import Supervisor

Supervisor 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

Typedict[str, Agent]
Defaultrequired

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

TypeArchitecture | None
DefaultNone (resolves to ReAct())

The architecture the supervisor itself runs (default ReAct). Wrap the Supervisor inside Reflexion to learn better delegation across runs.

instructions_template

Typestr | None
DefaultNone (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

Typestr
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

Typestr
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

Typebool / int / Agent | None
DefaultFalse / 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.

ParameterTypeDescription
namestrMust be a valid Python identifier. Raises ValueError otherwise.
agentAgentThe 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

  1. Open a shared event channel so worker events stream out alongside the base architecture’s events (otherwise workers’ MODEL_CHUNK and TOOL_CALL events would be lost).
  2. Build the delegate and forward_message tools. Both share a last_outputs dict (delegate writes, forward_message reads).
  3. Augment the supervisor’s tool host with these two extras.
  4. Run base.run(session, deps_with_extras, prompt). Forward all events.
  5. If the supervisor called forward_message(worker), override session.output with that worker’s verbatim last output. The call ends the turn: the supervisor’s model isn’t called again after it. A forward_message for 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.

Last updated on