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):
Or, from a source checkout (run from the repo root):
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¶
- Full delegation guide with a live model → Sub-agents (Delegation)
- Give the parent or child real tools → Writing Tools