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
active
property
¶
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
contribute ¶
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
run_scope
async
¶
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
shutdown
async
¶
tool_specs ¶
The tools to bind, or nothing at all when no child is declared.