symfonic.capabilities.delegation.declarations¶
declarations ¶
What an adopter declares: a child they built, or a child they described.
Two forms, one validation. The shipped types validated the same three things
twice, in two __post_init__ bodies that had already drifted (one checked
the runner, one did not check tools at all), so the rules live on the base
class here and each form adds only what is genuinely its own.
The forms differ in exactly one structural way, and it is the one that matters
downstream: who owns the child's lifetime. A child handed over pre-built is
the caller's; a child described by a spec is built by the compiler and must be
released by whoever built it. :attr:ChildDeclaration.owned is that fact, read
by the compiler and carried onto the roster entry, so no later stage has to
re-derive it from the declaration's type.
ChildDeclaration
dataclass
¶
The vocabulary both declaration forms share.
Keyword-only on purpose. A positional (name, agent, description) and a
positional (name, description, ...) in the same package is a
transposition waiting to happen, and the two forms are meant to be
interchangeable in a single list.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
The routing key the parent's model passes to |
description |
str
|
The one-liner shown to the parent model. Required, because a roster entry nobody can choose between is not a roster. |
when_to_use |
str | None
|
Optional longer guidance appended to the roster listing. |
ChildSpec
dataclass
¶
ChildSpec(*, 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)
Bases: ChildDeclaration
A child described rather than built: the parent constructs it.
The declarative form. Everything unset is inherited from the parent — provider, behaviour flags, model settings — because a child that silently reverted to framework defaults would drift from its parent on every flag the adopter had deliberately changed. What is not inherited is the domain: the child gets a fresh one scoped to this spec, so its tool manifest derives from its own tools rather than leaking the parent's.
Attributes:
| Name | Type | Description |
|---|---|---|
tools |
Sequence[Any]
|
The child's whole palette. Normalised to a tuple so a caller's list cannot be appended to after the child is compiled. |
domain_description |
str | None
|
The child's domain directive. Defaults to
:attr: |
model_name |
str | None
|
Optional child model override; inherits the parent's. |
temperature |
float | None
|
Optional child sampling override. |
max_tokens |
int | None
|
Optional child output-ceiling override. |
provider |
Any | None
|
Optional model provider. Unset inherits the parent's, which is safe because providers are stateless. |
config |
Any | None
|
A complete child config, bypassing inheritance entirely. It is sanitised rather than trusted — this is the path an adopter reaches for precisely when they want something non-default, and a lock skipped on that path is decorative. |
effective_domain_description
property
¶
The domain directive this spec actually resolves to.
Read here rather than at the compiler so the fallback is a property of the declaration — the thing the adopter reads — instead of a rule two construction branches each have to remember.
PrebuiltChild
dataclass
¶
Bases: ChildDeclaration
A child the caller constructed and hands over.
The escape hatch, and deliberately the unsanitisable one: the child is
already built, its palette is already frozen, and there is nothing left to
clear. Registration can therefore only accept or refuse it — see
:func:~.lockdown.assert_no_write_surface.
Attributes:
| Name | Type | Description |
|---|---|---|
agent |
Any
|
Anything exposing |
ScopedChild
dataclass
¶
ScopedChild(*, name: str, description: str, when_to_use: str | None = None, build: Callable[[Any], Any])
Bases: ChildDeclaration
A child the composer builds for the scope it is serving.
The declaration that closes a leak PrebuiltChild cannot. A prebuilt
child is a finished object, so a composition root has two places to build
one and the convenient place is wrong: construct the children once beside
the provider, close over them in compose(scope, shared), and every
tenant shares them. Give such a child memory and it answers one tenant's
delegation out of another tenant's recollections -- measured, not feared.
build is called once per scope, at composition time, with the scope
the parent is being composed for. Deliberately not a per-call rebinding:
a finished agent cannot be safely re-pointed at another tenant halfway
through a turn, and a contract that pretended otherwise would be the same
leak with more machinery.
Attributes:
| Name | Type | Description |
|---|---|---|
build |
Callable[[Any], Any]
|
|
for_scope ¶
Build this child for scope, refusing an absent one.
Building against None would produce exactly the process-wide
child this declaration exists to prevent, and it would look like it
worked.