Skip to content

symfonic.capabilities.delegation.composition

composition

Children as agents, assembled into one capability.

The generated sub_agents.py builds each child through the compatibility facade, with a template persona, a config-tree model and a shared metrics argument that only that facade accepts. Four dependencies for "this agent has a helper".

(This module names none of those types, and not for brevity: this package forbids even mentioning the facade in its source, on the rule that a capability which type-checks a child against it is not extracted. The rule is text-based and that is deliberate -- a package that may not depend on something is clearest when it may not say it either.)

Everything needed to do it kernel-natively already exists, and none of it is one call: :class:DelegationCapability takes a roster, tools, a lifecycle, a context and a depth policy. Assembling those five correctly is not something a scaffold template should be teaching, and a template that got it subtly wrong would teach the wrong thing to everyone who copied it.

A child is anything with async run. Structural rather than nominal, so symfonic.Agent qualifies without delegation importing it -- and so a test double or an adapter can stand in without inheriting anything.

Observability is not here. The scaffold rolls child cost up through the facade's own metrics argument; the kernel-native answer belongs to the observability slice, and inventing a channel here would put a second one beside whatever that slice lands, with both partly wired and neither authoritative.

child_roster

child_roster(children: Sequence[Any], *, scope: Any = None, context: Any = None) -> DelegationRoster

The roster children form, refusing a duplicate name.

A duplicate is a tool the model cannot address unambiguously. Refused here rather than resolved by last-wins, because last-wins leaves the deployment's list and the model's palette disagreeing with nothing to say so.

Source code in src/symfonic/capabilities/delegation/composition.py
def child_roster(
    children: Sequence[Any], *, scope: Any = None, context: Any = None
) -> DelegationRoster:
    """The roster ``children`` form, refusing a duplicate name.

    A duplicate is a tool the model cannot address unambiguously. Refused here
    rather than resolved by last-wins, because last-wins leaves the
    deployment's list and the model's palette disagreeing with nothing to say
    so.
    """
    seen: set[str] = set()
    entries: list[RosterEntry] = []
    for child in children:
        if child.name in seen:
            raise ValueError(
                f"two children are named {child.name!r}. The model addresses a "
                "child by name, so a duplicate is a tool it cannot call "
                "unambiguously."
            )
        seen.add(child.name)
        # A ScopedChild is built here, once, for the scope the parent is being
        # composed for -- never rebound per call. A PrebuiltChild is taken as
        # it comes, which is what it is for.
        agent = child.for_scope(scope) if isinstance(child, ScopedChild) else child.agent
        entries.append(
            RosterEntry(
                name=child.name,
                description=child.description,
                # Adapted, not passed through: an ``Agent`` speaks
                # ``run(prompt, ...)`` and the roster calls
                # ``run(query=..., ...)``. Handing the child over raw made
                # every hand-off raise, and ``DelegationTools`` turns a
                # child's failure into a tool message by design -- so the
                # parent's turn succeeded carrying the TypeError where the
                # answer belonged.
                runner=adapt_child(child.name, agent, context),
                when_to_use=child.when_to_use,
            )
        )
    return DelegationRoster(tuple(entries))

delegated_children

delegated_children(children: Sequence[Any], *, max_depth: int = DEFAULT_MAX_DEPTH, scope: Any = None) -> DelegationCapability

One capability that offers children as tools the model may call.

Raises:

Type Description
ValueError

if children is empty, or two share a name.

An empty roster is refused because the capability would contribute a tool that can call nobody: the model is told it may delegate, every attempt fails, and it reads as a runtime fault rather than as a deployment with no children.

Source code in src/symfonic/capabilities/delegation/composition.py
def delegated_children(
    children: Sequence[Any],
    *,
    max_depth: int = DEFAULT_MAX_DEPTH,
    scope: Any = None,
) -> DelegationCapability:
    """One capability that offers ``children`` as tools the model may call.

    Raises:
        ValueError: if ``children`` is empty, or two share a name.

    An empty roster is refused because the capability would contribute a tool
    that can call nobody: the model is told it may delegate, every attempt
    fails, and it reads as a runtime fault rather than as a deployment with no
    children.
    """
    if not children:
        raise ValueError(
            "delegation with no children contributes a tool that can call "
            "nobody. Compose no delegation capability instead -- an agent "
            "without helpers is the ordinary case, not a degraded one."
        )
    context = DelegationContext()
    roster = child_roster(children, scope=scope, context=context)
    depth = DepthPolicy(max_depth=max_depth)
    return DelegationCapability(
        roster=roster,
        tools=DelegationTools(roster=roster, depth=depth, context=context),
        # The children were built by the caller, so the caller closes them.
        # Handing them to the lifecycle here would make this function's return
        # value own objects its caller still holds a reference to, and both
        # would close them.
        lifecycle=ChildLifecycle(()),
        context=context,
        depth=depth,
    )