Skip to content

symfonic.agent.cutover.fragments

fragments

A contributed prompt fragment, in the prompt compiler's vocabulary (TA8.21).

Two capabilities, two vocabularies, and exactly one place allowed to see both. capabilities/extensions/values.py says why the extension side speaks strings: "the capability layer does not import another capability's internals (LAY-ADR §3.4), so the tier travels as its declared name and the composition root maps it." This module is that root, and it is the same seam capabilities/memory/contribution.py describes for a recall — a mapping of "compiler-ready keyword values", so neither capability learns the other's internals.

The remap this performs was decided before this file existed. evidence/RET-PREP/decision-plugin-tier-layer.md is the decision; the tables below implement D1, D2 and D6 of it. Read it first — a remap inferred from an implementation is a description, not a decision, and this one changes what an adopter's model reads:

  • a plugin's cached contribution rendered, on legacy, verbatim and undelimited inside the cached prefix beside the operator's own instructions;
  • here it renders at L1, at the profile tier, which is a learned tier — so capabilities/prompting/gates.render_contribution wraps it in <untrusted-data source="…"> after neutralising delimiter forgeries.

The tables are written out rather than computed. "l1".upper() == "L1" is true today and would keep being true after somebody renamed one side, which is exactly the silent re-tiering the extension package's own docstring asks for a failing test about instead.

FragmentRead dataclass

FragmentRead(text: str, revision: str = '', untrusted: bool = True)

What :class:FragmentSource answers.

Structurally the prompt contract's SourceRead and deliberately not an import of it: this module is the only thing that constructs one, and the compiler reads it by attribute. Duck-typed on purpose, the way capabilities/memory/contribution.MemoryRead is.

FragmentSource dataclass

FragmentSource(text: str, revision: str = '', scope_aware: bool = False, offline_safe: bool = True)

One harvested fragment, already read, as the compiler's source.

Frozen because it travels in the turn's resolved-input snapshot, and ResolvedInput refuses any payload it cannot vouch for -- a mutable dataclass among them, since one compilation stage could otherwise rewrite what the next one compiles.

Reading performs nothing: the plugin's hook already ran, in the resolution stage, which is what lets an asynchronous plugin feed a synchronous compiler without either learning about the other.

fragment_spec

fragment_spec(fragment: Any) -> Mapping[str, Any]

Project one PromptFragment into compiler-ready keyword values.

A mapping rather than a PromptContribution: the snapshot is neutral, the prompt capability builds the contribution from its own vocabulary, and handing it a constructed object would make this module a dependency of the thing that reads it. PromptingCapability._contributions_from names the six keys it requires; every one of them is emitted here.

Raises:

Type Description
KeyError

the fragment declares a layer or tier outside the tables above. Not caught and not defaulted -- PromptFragment.validate has already refused everything these tables omit, so a miss here means the two vocabularies have drifted apart, which is the failure D6 exists to make loud.

Source code in src/symfonic/agent/cutover/fragments.py
def fragment_spec(fragment: Any) -> Mapping[str, Any]:
    """Project one ``PromptFragment`` into compiler-ready keyword values.

    A mapping rather than a ``PromptContribution``: the snapshot is neutral, the
    prompt capability builds the contribution from its *own* vocabulary, and
    handing it a constructed object would make this module a dependency of the
    thing that reads it. ``PromptingCapability._contributions_from`` names the
    six keys it requires; every one of them is emitted here.

    Raises:
        KeyError: the fragment declares a layer or tier outside the tables
            above. Not caught and not defaulted -- ``PromptFragment.validate``
            has already refused everything these tables omit, so a miss here
            means the two vocabularies have drifted apart, which is the failure
            D6 exists to make loud.
    """
    text = fragment.text
    fragment_id = f"{FRAGMENT_CAPABILITY}.{fragment.fragment_id}"
    return MappingProxyType(
        {
            "contribution_id": fragment_id,
            "source": FragmentSource(
                text=text, revision=_digest(fragment_id, text)
            ),
            "capability": FRAGMENT_CAPABILITY,
            "layer": FRAGMENT_LAYERS[fragment.layer],
            "tier": FRAGMENT_TIERS[fragment.tier],
            "scope": FRAGMENT_SCOPE,
            # D3.4: the plugin's own ordering, carried rather than flattened.
            # ``ordering_key`` is ``(layer, order, contribution_id)``, so
            # hardcoding a constant here would hand every tie to whichever
            # plugin name sorts first.
            "order": fragment.order,
            "inherit": True,
            # A fragment is context, not instruction: a turn that dropped it for
            # budget is still a correct turn, so it must not fail the compile.
            "pinned": False,
            # The hook is asked in the resolution stage, which runs under both
            # strategies. Declaring otherwise would make the JIT strategy
            # decline content that is already in hand.
            "requires_hydration": False,
        }
    )