Skip to content

symfonic.agent.subagents

subagents

Sub-agent delegation primitive.

Declare child agents a parent can delegate to via run_agent(name, task):

from symfonic.agent.subagents import SubAgent

parent = SymfonicAgent(
    model_provider=provider,
    config=FrameworkConfig(),
    sub_agents=[
        SubAgent(name="researcher", agent=research_agent,
                 description="Deep web research"),
    ],
)

Declaring sub_agents auto-registers a concrete AgentStore plus the run_agent / list_agents tools before the graph compiles. Children are isolated agents that inherit the parent's tenant scope and run under a depth guard (max_agent_depth).

SubAgent dataclass

SubAgent(
    name: str,
    agent: _Runnable,
    description: str,
    when_to_use: str | None = None,
)

A named child agent the parent may delegate to.

Attributes:

Name Type Description
name str

Unique routing key the parent's model passes to run_agent(name, ...). Must be non-empty and whitespace-free.

agent _Runnable

The child agent — any object exposing async run(query, ...) (typically another SymfonicAgent).

description str

One-line summary shown to the parent model in the run_agent tool schema so it knows when to delegate here.

when_to_use str | None

Optional longer guidance appended to the roster listing.

SubAgentRegistry

SubAgentRegistry(sub_agents: Sequence[SubAgent])

Concrete AgentStore that runs registered child agents in-process.

Satisfies the AgentStore protocol (list / read / run). The run method inherits the parent's tenant scope into the child and threads the (already incremented) agent depth so the child's own delegation guard can enforce max_agent_depth for nested delegation.

Source code in src/symfonic/agent/subagents/registry.py
def __init__(self, sub_agents: Sequence[SubAgent]) -> None:
    self._agents: dict[str, SubAgent] = {}
    for sa in sub_agents:
        if sa.name in self._agents:
            raise ValueError(f"duplicate sub-agent name: {sa.name!r}")
        self._agents[sa.name] = sa

run async

run(
    name: str,
    agent_input: Any,
    *,
    parent_scope: Any = None,
    agent_depth: int = 1,
    run_id: str | None = None,
) -> Any

Run child name on agent_input; returns its AgentResponse.

Parameters:

Name Type Description Default
name str

Registered sub-agent name.

required
agent_input Any

The delegated task (coerced to str for run).

required
parent_scope Any

Tenant scope inherited from the parent run.

None
agent_depth int

Depth to stamp on the child run (parent depth + 1).

1
run_id str | None

Optional correlation id passed to the child.

None

Raises:

Type Description
StorageError

If name is not a registered sub-agent.

Source code in src/symfonic/agent/subagents/registry.py
async def run(
    self,
    name: str,
    agent_input: Any,
    *,
    parent_scope: Any = None,
    agent_depth: int = 1,
    run_id: str | None = None,
) -> Any:
    """Run child ``name`` on ``agent_input``; returns its ``AgentResponse``.

    Args:
        name: Registered sub-agent name.
        agent_input: The delegated task (coerced to ``str`` for ``run``).
        parent_scope: Tenant scope inherited from the parent run.
        agent_depth: Depth to stamp on the child run (parent depth + 1).
        run_id: Optional correlation id passed to the child.

    Raises:
        StorageError: If ``name`` is not a registered sub-agent.
    """
    sa = self._agents.get(name)
    if sa is None:
        raise StorageError(f"unknown sub-agent: {name!r}")
    return await sa.agent.run(
        query=str(agent_input),
        scope=parent_scope,
        run_id=run_id,
        agent_depth=agent_depth,
    )

SubAgentSpec dataclass

SubAgentSpec(
    name: str,
    description: str,
    when_to_use: str | None = None,
    tools: Sequence[Any] = tuple(),
    domain_description: str | None = None,
    model_name: str | None = None,
    temperature: float | None = None,
    max_tokens: int | None = None,
    provider: Any | None = None,
    config: Any | None = None,
)

A declarative sub-agent: describe the child, let the parent build it.

v9.1.0 (issue #27). SubAgent requires a fully pre-constructed child agent; SubAgentSpec closes that ergonomics gap -- you declare what the child should be and the parent constructs the inner SymfonicAgent at wiring time, inheriting the parent's :class:ModelProvider (#26A) and base :class:FrameworkConfig via :meth:FrameworkConfig.child (#28). Pass either form in SymfonicAgent(sub_agents=[...]); mix freely.

The child is an isolated agent with its own fresh domain (scoped to name/domain_description) so its tool manifest auto-derives from its own tools rather than leaking the parent's. Every behaviour flag (auto_hydrate, lazy_tooling, enable_hms_prompt, ...) is inherited from the parent unless a full config is supplied. For a child that needs shared backends or bespoke wiring, build it yourself and use SubAgent(agent=...) as the escape hatch.

Attributes:

Name Type Description
name str

Unique routing key the parent's model passes to run_agent(name, ...). Non-empty and whitespace-free.

description str

One-line summary shown to the parent model in the run_agent tool schema so it knows when to delegate here.

when_to_use str | None

Optional longer guidance appended to the roster listing.

tools Sequence[Any]

Tools the child agent gets (its manifest auto-derives from them).

domain_description str | None

Optional child DomainTemplate.description -- the child's system-prompt domain directive. Defaults to description.

model_name str | None

Optional child model override; inherits the parent model.

temperature float | None

Optional child sampling temperature override.

max_tokens int | None

Optional child max-output-tokens override.

provider Any | None

Optional :class:ModelProvider for the child; when unset the child inherits the parent's provider (#26A -- providers are stateless, so sharing one instance is safe).

config Any | None

Optional full :class:FrameworkConfig for the child, bypassing the FrameworkConfig.child(parent, ...) inheritance path.