symfonic.capabilities.extensions.declarations¶
declarations ¶
The four things an extension may declare, one dataclass each (T4.2.2).
The bundle that carries them and the protocol that asks for them live next door
in :mod:~symfonic.capabilities.extensions.contracts, which is also where the
argument for this shape is written down. This module is only the vocabulary.
Two properties hold across all four, and both are AS-INT-3 in code rather than in prose:
- A contribution declares content, not position. No contribution names a cache region, a token budget, or an index in a list. Position over the whole set is the composer's, exactly as it is the prompt compiler's, because a contributor can only see itself.
- A contribution carries the authority of the request that admitted it, never more. A prompt fragment cannot claim an authored trust tier; a policy can refuse an action but cannot grant one; a tool cannot take a name the host already owns. Each of those is validated here or refused by the composer, and each is a thing the previous plugin API allowed.
LifecycleContribution
dataclass
¶
LifecycleContribution(hook_id: str, extension: str, phase: LifecyclePhase, run: Callable[[], Awaitable[None] | None])
A hook the composition runs when it opens or unwinds.
Lifecycle is a contract rather than a convention because the alternative is what the legacy path did: nothing. A plugin holding an HTTP client had no place to close it, so the client closed when the process did.
PolicyContribution
dataclass
¶
PolicyContribution(policy_id: str, extension: str, decide: Callable[[PolicyRequest], Awaitable[PolicyVerdict]], actions: frozenset[str] = frozenset())
One extension's veto over agent actions.
decide answers a :class:~.values.PolicyVerdict. An ALLOW from a
contributed policy is not authority: the composer combines verdicts
deny-wins and treats every non-deny as "this policy had no objection", so a
plugin can narrow what the agent does and can never widen it (AS-INT-3).
actions empty means "every action". Naming actions is the cheap way to
keep a policy off hot paths it has no opinion about.
PromptFragment
dataclass
¶
PromptFragment(fragment_id: str, text: str, extension: str, layer: str = 'l1', tier: str = 'session', scope: str = 'deployment', order: int = 100, truncated: bool = False)
Text an extension contributes to the compiled prompt.
The fields mirror the prompt compiler's contribution contract by name — layer, tier, scope, order — because the composition root's job is then a lookup rather than a translation. What it does not mirror is the tier range: a contributed fragment is restricted to the learned tiers, so no extension can place text where the model reads operator instruction.
ToolContribution
dataclass
¶
ToolContribution(name: str, extension: str, invoke: Callable[[Mapping[str, Any]], Awaitable[str]], description: str = '', input_schema: Mapping[str, Any] = dict(), origin: str = '')
One callable an extension offers the agent.
invoke is an async callable taking the bound arguments and answering a
string. It is captured at declaration time and never looked up again: the
legacy MCP provider routed each call through a mutable name→server dict, so
a later discovery could re-point an already-advertised tool at a different
server. Holding the callable makes that unrepresentable.
require_id ¶
Reject an empty or out-of-charset declared id.
Shared by every contribution and by the bundle, because the charset rule is one rule: these strings are rendered into manifests and prompt delimiters, where a separator or an angle bracket forges a boundary.