Skip to content

symfonic.capabilities.extensions.legacy_prompts

legacy_prompts

Reading a legacy plugin's prompt hook (T4.2.2).

Split from the bridge because it is the only asynchronous, per-turn, failure- prone part of reading a plugin: identity, tool refusal, policy, and lifecycle are all settled once at construction, and this runs on every render.

The containment envelope is inherited deliberately from the pre-refactor dispatcher — a misbehaving plugin is skipped, never propagated, because a plugin's bug must not become the agent's outage. What changes is that each skip now emits a value. Previously the dispatcher logged at DEBUG and returned an empty list, so "this plugin had nothing to say" and "this plugin raised" were the same reading of a turn. Here they differ by a diagnostic.

Severity is what makes that value reach an operator. Anything that costs a fragment its place in the turn — a hook that raised, a hook that answered a non-list, a position outside the vocabulary, a position='kernel' claim on the layer AS-INT-3 reserves — is recorded at Severity.ERROR, because :attr:~.composition.ComposedExtensions.refusals is the ERROR-severity subset and that is the tuple ExtensionsCapability.contribute's handler logs at WARNING. A diagnostic recorded below ERROR is a value nobody is told about; the truncation note is deliberately one of those, because the fragment still lands.

Two mappings are performed and both are recorded rather than assumed:

  • positionlayer. cached → l1, volatile → l2, kernel → l1 with a diagnostic. The kernel remap matches the legacy dispatcher's, and is still correct for a different reason: L0 is the kernel's instruction layer and no extension may write it (AS-INT-3).
  • everything → the profile tier. A plugin's output is untrusted input (AS-INT-1); it used to render into the cached prefix beside operator instructions with no tier at all.

LegacyPromptReader

LegacyPromptReader(plugin: Any, name: str, *, limits: TrustLimits)

Calls one plugin's prompt hook and maps what it answered.

Source code in src/symfonic/capabilities/extensions/legacy_prompts.py
def __init__(self, plugin: Any, name: str, *, limits: TrustLimits) -> None:
    self._plugin = plugin
    self._name = name
    self._limits = limits
    self._diagnostics: list[ExtensionDiagnostic] = []

read async

read(state: Mapping[str, Any]) -> tuple[PromptFragment, ...]

Harvest this turn's fragments, resetting the diagnostics first.

Source code in src/symfonic/capabilities/extensions/legacy_prompts.py
async def read(self, state: Mapping[str, Any]) -> tuple[PromptFragment, ...]:
    """Harvest this turn's fragments, resetting the diagnostics first."""
    self._diagnostics = []
    raw = await self._call_hook(state)
    fragments: list[PromptFragment] = []
    for index, item in enumerate(raw):
        fragment = self._map(item, index)
        if fragment is not None:
            fragments.append(fragment)
    return tuple(fragments)