Skip to content

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

require_id(value: str, *, what: str, owner: str) -> None

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.

Source code in src/symfonic/capabilities/extensions/declarations.py
def require_id(value: str, *, what: str, owner: str) -> None:
    """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.
    """
    _require_id(value, what=what, owner=owner)