Skip to content

sub_agents

Level 2 · Tools and Skills — delegation: a parent agent hands work to a named child.

Delegation lets one agent call another as a tool. You declare a child as a SubAgent, pass it to the parent via SymfonicAgent(sub_agents=[...]), and the framework auto-registers run_agent / list_agents tools plus a concrete AgentStore. The parent's model can then emit run_agent('researcher', <task>) to delegate. This example drives the same SubAgentRegistry the engine builds internally, so the delegated call is deterministic without a live LLM.

  • Lines: ~90
  • Prerequisites: none (runs on MockModelProvider)
  • Key concepts: SubAgent, SubAgentRegistry, sub_agents= wiring, run_agent / list_agents, depth guard

Run it

Installed via pip? Copy this example into your project with the CLI (no checkout needed):

pip install "symfonic-core[cli]"
symfonic examples add sub_agents
python -m sub_agents

Or, from a source checkout (run from the repo root):

python -m examples.sub_agents

Full code

"""Sub-agents (delegation) -- a parent agent hands work to a named child.

Usage: python -m examples.sub_agents

Runs entirely on ``MockModelProvider`` (no API key needed). It demonstrates
the three moving parts of the delegation primitive:

  1. Declaring a child as a :class:`SubAgent` (name / description / when_to_use).
  2. Wiring children into a parent via ``SymfonicAgent(sub_agents=[...])`` --
     this auto-registers the ``run_agent`` / ``list_agents`` tools and a
     concrete ``AgentStore`` before the graph compiles.
  3. A delegated call: the parent's model would emit
     ``run_agent('researcher', <task>)``; here we drive the same
     :class:`SubAgentRegistry` the engine builds internally so the flow is
     deterministic without a live LLM.

With a real provider you would simply ask the parent a question and its model
decides whether to delegate. See ``docs/guides/17-sub-agents.md``.
"""

from __future__ import annotations

import asyncio

from symfonic.agent import SubAgent, SymfonicAgent
from symfonic.agent.config import FrameworkConfig
from symfonic.agent.domain import DomainTemplate
from symfonic.agent.subagents import SubAgentRegistry
from symfonic.core.config import AgentConfig, ModelConfig
from symfonic.core.testing import MockModelProvider


def build_researcher() -> SymfonicAgent:
    """A lightweight specialist child: its own model + persona, no HMS backends.

    The child is just another ``SymfonicAgent``. Capping ``max_tokens`` keeps a
    delegated answer short (the framework default of 16k is large for a helper).
    """
    return SymfonicAgent(
        model_provider=MockModelProvider(
            response="The sky is blue because of Rayleigh scattering.",
        ),
        config=FrameworkConfig(
            domain=DomainTemplate(
                name="researcher",
                description="Answers self-contained background questions concisely.",
            ),
            agent=AgentConfig(model=ModelConfig(max_tokens=512, temperature=0.4)),
        ),
    )


async def main() -> None:
    researcher = build_researcher()

    # Declare the child the parent may delegate to.
    researcher_sa = SubAgent(
        name="researcher",
        agent=researcher,
        description="Background research specialist.",
        when_to_use="Explain / define / compare questions with no dedicated tool.",
    )

    # Wire it into a parent. Passing ``sub_agents`` auto-registers the
    # run_agent / list_agents tools -- the parent's model can now delegate.
    parent = SymfonicAgent(
        model_provider=MockModelProvider(response="(a real model would delegate here)"),
        config=FrameworkConfig(
            domain=DomainTemplate(name="assistant", description="Helpful parent agent."),
        ),
        sub_agents=[researcher_sa],
    )
    print(f"Parent built with delegation enabled: {parent is not None}")

    # The engine builds a SubAgentRegistry from ``sub_agents``; build the same
    # one here to show the delegated call deterministically (a live parent model
    # would emit run_agent('researcher', ...) instead of us calling it).
    registry = SubAgentRegistry([researcher_sa])
    print(f"Children the parent can reach: {registry.names()}")

    definitions = await registry.list()
    for d in definitions:
        print(f"  - {d.name}: {d.description}")

    # Delegate a task to the child and print its answer.
    response = await registry.run("researcher", "Why is the sky blue?")
    print("\nDelegated task -> researcher")
    print(f"Child answered: {response.final_response}")


if __name__ == "__main__":
    asyncio.run(main())

Step by step

1. Build the child agent

def build_researcher() -> SymfonicAgent:
    return SymfonicAgent(
        model_provider=MockModelProvider(response="..."),
        config=FrameworkConfig(
            domain=DomainTemplate(name="researcher", description="..."),
            agent=AgentConfig(model=ModelConfig(max_tokens=512, temperature=0.4)),
        ),
    )

A child is just another SymfonicAgent — its own provider, persona (DomainTemplate), and model config. Capping max_tokens=512 keeps a delegated answer short; the framework default (16k) is large for a helper. The child can have its own tools and memory too.

2. Declare the delegation contract

researcher_sa = SubAgent(
    name="researcher",
    agent=researcher,
    description="Background research specialist.",
    when_to_use="Explain / define / compare questions with no dedicated tool.",
)

SubAgent wraps the child with the metadata the parent's model needs to decide whether to delegate: name (how it's addressed), description (what it does), and when_to_use (the routing hint the model reads). Write when_to_use carefully — it is the model's only guidance on when to hand off.

3. Wire children into the parent

parent = SymfonicAgent(
    model_provider=MockModelProvider(response="..."),
    config=FrameworkConfig(domain=DomainTemplate(name="assistant", ...)),
    sub_agents=[researcher_sa],
)

Passing sub_agents=[...] is all it takes. Before the graph compiles, the engine auto-registers two tools — run_agent(name, task) and list_agents() — and a concrete AgentStore backed by your children. The parent's model now sees these tools and can delegate on its own.

4. Delegate a call

registry = SubAgentRegistry([researcher_sa])
response = await registry.run("researcher", "Why is the sky blue?")
print(f"Child answered: {response.final_response}")

With a real provider you'd just ask the parent a question and its model would emit run_agent('researcher', ...) when appropriate. To keep this demo deterministic, we build the same SubAgentRegistry the engine builds internally and invoke the child directly — the flow is identical, minus the model's routing decision.

Depth guard

Delegation is recursive-capable (a child can have its own children), so the framework enforces a depth guard to prevent runaway chains. A child cannot delegate infinitely; the guard caps the delegation depth. See the Sub-agents guide for tuning it.

What to try next

See also