Skip to content

symfonic.capabilities.delegation.capability

capability

The capability as one object: declarations in, everything else out.

A composition root wanting delegation has one call to make and four things to read afterwards — the roster to bind as an agent store, the tool specs to bind into the palette, the run scope to enter per run, and the lifecycle to tear down. Nothing else about delegation should appear in the wiring, because everything else about delegation is a rule, and a rule restated at a call site is a rule that can be restated differently at the next one.

Since TA8.12 there is a fifth thing, and it is a seam rather than a fact to read: :meth:DelegationCapability.contribute makes this capability foldable by symfonic.kernel.contracts.contributions.fold_contributions, which is what lets a composition root hand its tools to a compiled plan instead of binding them into a graph by hand.

DelegationCapability

DelegationCapability(*, roster: DelegationRoster, tools: DelegationTools, lifecycle: ChildLifecycle, context: DelegationContext, depth: DepthPolicy)

Roster, tools, run scope, and lifecycle for one parent agent.

Source code in src/symfonic/capabilities/delegation/capability.py
def __init__(
    self,
    *,
    roster: DelegationRoster,
    tools: DelegationTools,
    lifecycle: ChildLifecycle,
    context: DelegationContext,
    depth: DepthPolicy,
) -> None:
    self.roster = roster
    self.tools = tools
    self.lifecycle = lifecycle
    self.context = context
    self.depth = depth

active property

active: bool

True when at least one child is declared.

A parent that declared none must not be offered run_agent: a tool whose whole roster is "(none declared)" is an invitation to attempt a delegation that can only ever be refused.

compile classmethod

compile(declarations: Iterable[Any], *, builder: Any, inheritance: Any, parent_config: Any, parent_provider: Any = None, max_depth: int = 3, snapshots: Any | None = None, resolve_scope: Callable[[], Any] | None = None, shared: dict[str, Any] | None = None, record_delegations: bool = True) -> DelegationCapability

Compile declarations into a wired capability.

Every argument after declarations is either a port or a policy. There is no argument that is a piece of the agent being wired, which is what keeps this a capability rather than a second constructor for the thing that composes it.

Source code in src/symfonic/capabilities/delegation/capability.py
@classmethod
def compile(
    cls,
    declarations: Iterable[Any],
    *,
    builder: Any,
    inheritance: Any,
    parent_config: Any,
    parent_provider: Any = None,
    max_depth: int = 3,
    snapshots: Any | None = None,
    resolve_scope: Callable[[], Any] | None = None,
    shared: dict[str, Any] | None = None,
    record_delegations: bool = True,
) -> DelegationCapability:
    """Compile ``declarations`` into a wired capability.

    Every argument after ``declarations`` is either a port or a policy.
    There is no argument that is a piece of the agent being wired, which is
    what keeps this a capability rather than a second constructor for the
    thing that composes it.
    """
    compiled: CompiledChildren = ChildCompiler(
        builder=builder, inheritance=inheritance
    ).compile(
        declarations,
        parent_config=parent_config,
        parent_provider=parent_provider,
        shared=shared,
    )
    context = DelegationContext(snapshots=snapshots, resolve_scope=resolve_scope)
    roster = compiled.roster()
    return cls(
        roster=roster,
        tools=DelegationTools(
            roster=roster,
            depth=DepthPolicy(max_depth=max_depth),
            context=context,
            record=record_delegations,
        ),
        lifecycle=ChildLifecycle(compiled.owned),
        context=context,
        depth=DepthPolicy(max_depth=max_depth),
    )

contribute

contribute(request: CapabilityRequest) -> CapabilityContribution

Offer run_agent / list_agents to the turn being compiled.

Tools only, and no stage. Delegation contributes nothing to the prompt and performs no effect of its own before the model runs: what it offers is a pair of callables the model may reach for, and the child's own run is the child's effect, not this capability's. Declaring a stage that did nothing would be the declared-and-never-read shape the fold exists to refuse -- validate() would even accept it, because an empty handler answers an empty stage.

Contributing nothing at all when no child is declared is the same rule :attr:tool_specs states, applied one layer out: a run_agent whose whole roster reads "(none declared)" invites the model to attempt a delegation that can only be refused. The contribution still carries the capability's name, so a plan records that delegation was folded and found nothing rather than that it was never folded.

request is read for its grants and found to need none: delegation declares no :class:~symfonic.kernel.contracts.effects.EffectFamily, because every effect a child performs is granted to the child's plan by whoever composed it. A parent that could widen its children's authority by delegating to them would make the grant list a description rather than a bound.

Source code in src/symfonic/capabilities/delegation/capability.py
def contribute(self, request: CapabilityRequest) -> CapabilityContribution:
    """Offer ``run_agent`` / ``list_agents`` to the turn being compiled.

    **Tools only, and no stage.** Delegation contributes nothing to the
    prompt and performs no effect of its own before the model runs: what it
    offers is a pair of callables the model may reach for, and the child's
    own run is the child's effect, not this capability's. Declaring a stage
    that did nothing would be the declared-and-never-read shape the fold
    exists to refuse -- ``validate()`` would even accept it, because an
    empty handler answers an empty stage.

    Contributing *nothing at all* when no child is declared is the same
    rule :attr:`tool_specs` states, applied one layer out: a ``run_agent``
    whose whole roster reads "(none declared)" invites the model to attempt
    a delegation that can only be refused. The contribution still carries
    the capability's name, so a plan records that delegation was folded and
    found nothing rather than that it was never folded.

    ``request`` is read for its grants and found to need none: delegation
    declares no :class:`~symfonic.kernel.contracts.effects.EffectFamily`,
    because every effect a child performs is granted to the *child's* plan
    by whoever composed it. A parent that could widen its children's
    authority by delegating to them would make the grant list a description
    rather than a bound.
    """
    return CapabilityContribution(
        capability=CAPABILITY_NAME,
        tools=self.tool_specs(),
    )

run_scope async

run_scope(*, depth: Any = 0) -> AsyncIterator[ActiveRun]

Enter one run's delegation scope.

Entered by every entry point, not only the one that happens to delegate. The scope is where a delegated run's prompt-block snapshot and its delegation tally live, and an entry point that skipped it gave the same agent different behaviour depending on how it was called.

Source code in src/symfonic/capabilities/delegation/capability.py
@asynccontextmanager
async def run_scope(self, *, depth: Any = 0) -> AsyncIterator[ActiveRun]:
    """Enter one run's delegation scope.

    Entered by *every* entry point, not only the one that happens to
    delegate. The scope is where a delegated run's prompt-block snapshot
    and its delegation tally live, and an entry point that skipped it gave
    the same agent different behaviour depending on how it was called.
    """
    async with self.context.run_scope(depth=depth) as run:
        yield run

shutdown async

shutdown() -> TeardownReport

Flush and release the children this capability built.

Source code in src/symfonic/capabilities/delegation/capability.py
async def shutdown(self) -> TeardownReport:
    """Flush and release the children this capability built."""
    return await self.lifecycle.shutdown()

tool_specs

tool_specs() -> tuple[DelegationToolSpec, ...]

The tools to bind, or nothing at all when no child is declared.

Source code in src/symfonic/capabilities/delegation/capability.py
def tool_specs(self) -> tuple[DelegationToolSpec, ...]:
    """The tools to bind, or nothing at all when no child is declared."""
    return self.tools.specs() if self.active else ()