Skip to content

symfonic.capabilities.prompting.assembly

assembly

The bridge from a compiled prompt to the kernel's own vocabulary.

The capability declares a stage and produces a value; it never calls the kernel, and the kernel never imports this package. Both halves talk through :mod:symfonic.kernel.contracts, which is the only import edge the dependency matrix licenses from a capability — and the reason this module is four functions rather than a runtime coupling.

The stage is registered into prompt-assembly with the compiled digest in its frozen config. That is deliberate: the digest is what makes "this plan compiled this prompt" checkable from the plan alone, without the plan carrying the prompt text into every diagnostic dump.

compiled_instructions

compiled_instructions(compiled: CompiledPrompt) -> str | None

The compiled prompt as kernel instructions, or None when it is empty.

None rather than "" because the kernel's instructions field is optional, and an empty string is a system prompt that says nothing — indistinguishable downstream from a deployment that meant to send one and lost it.

Source code in src/symfonic/capabilities/prompting/assembly.py
def compiled_instructions(compiled: CompiledPrompt) -> str | None:
    """The compiled prompt as kernel instructions, or ``None`` when it is empty.

    ``None`` rather than ``""`` because the kernel's ``instructions`` field is
    optional, and an empty string is a system prompt that says nothing —
    indistinguishable downstream from a deployment that meant to send one and
    lost it.
    """
    text = compiled.text
    return text or None

prompt_stage_descriptor

prompt_stage_descriptor(compiled: CompiledPrompt, *, priority: int = -900, awaited_sources: int = 0, deferred_render_gate: bool = False) -> StageDescriptor

The stage this capability contributes, carrying the compile's identity.

No effects and no emitted events are declared: this is a compilation stage (STG-7), a pure function of plan, request and the resolved-input snapshot. A stage that claimed an effect it does not perform would widen its own admission for nothing, and under the reformulated STG-7 it would also be refused outright.

static_prompt_digest identifies this compile, not the final prompt. The name is deliberate and the distinction is not pedantic. The digest covers sources — what the capability was configured with, compiled here at contribution time. It does not cover:

  • Agent(instructions=...), which the kernel's own stage puts on the assembly and this capability composes onto afterwards;
  • anything a resolution stage put in the turn's snapshot, which by definition does not exist yet when this descriptor is built;
  • any contribution whose source must be awaited (S01). contribute() is synchronous, so an aread-only source cannot be read here at all. awaited_sources counts them in the same config, because "the digest does not cover N of my sources" is a fact a plan reader needs and a silently narrower digest is exactly the false "checkable from the plan alone" claim the paragraph below retires.
  • the request's render_when policy, when one is set (S01). deferred_render_gate records that the digest compile ran without it. The gate is a turn-time decision over the whole set and may be awaited, so the synchronous declaration-time compile cannot honour it; running it here would refuse the plan outright for a policy the turn supports.

The field was called prompt_digest and read as "this plan compiled this prompt, checkable from the plan alone". That claim was already false before memory existed — the adopter's instructions were outside it — and the reformulated STG-7 makes the gap structural rather than accidental. Naming it for what it actually covers is the honest half; recording the final digest is a runtime artifact and needs a carrier the kernel does not have yet (see the debt table in the phase-4 sizing note).

Source code in src/symfonic/capabilities/prompting/assembly.py
def prompt_stage_descriptor(
    compiled: CompiledPrompt,
    *,
    priority: int = -900,
    awaited_sources: int = 0,
    deferred_render_gate: bool = False,
) -> StageDescriptor:
    """The stage this capability contributes, carrying the compile's identity.

    No effects and no emitted events are declared: this is a *compilation*
    stage (STG-7), a pure function of plan, request and the resolved-input
    snapshot. A stage that claimed an effect it does not perform would widen
    its own admission for nothing, and under the reformulated STG-7 it would
    also be refused outright.

    **``static_prompt_digest`` identifies this compile, not the final prompt.**
    The name is deliberate and the distinction is not pedantic. The digest
    covers ``sources`` — what the capability was configured with, compiled here
    at contribution time. It does *not* cover:

    * ``Agent(instructions=...)``, which the kernel's own stage puts on the
      assembly and this capability composes onto afterwards;
    * anything a resolution stage put in the turn's snapshot, which by
      definition does not exist yet when this descriptor is built;
    * any contribution whose source must be **awaited** (S01). ``contribute()``
      is synchronous, so an ``aread``-only source cannot be read here at all.
      ``awaited_sources`` counts them in the same config, because "the digest
      does not cover N of my sources" is a fact a plan reader needs and a
      silently narrower digest is exactly the false "checkable from the plan
      alone" claim the paragraph below retires.
    * the request's ``render_when`` policy, when one is set (S01).
      ``deferred_render_gate`` records that the digest compile ran without it.
      The gate is a turn-time decision over the whole set and may be awaited,
      so the synchronous declaration-time compile cannot honour it; running it
      here would refuse the plan outright for a policy the turn supports.

    The field was called ``prompt_digest`` and read as "this plan compiled this
    prompt, checkable from the plan alone". That claim was already false before
    memory existed — the adopter's instructions were outside it — and the
    reformulated STG-7 makes the gap structural rather than accidental. Naming
    it for what it actually covers is the honest half; recording the *final*
    digest is a runtime artifact and needs a carrier the kernel does not have
    yet (see the debt table in the phase-4 sizing note).
    """
    return StageDescriptor(
        stage_id=PROMPTING_STAGE,
        phase=Phase.PROMPT_ASSEMBLY,
        capability="prompting",
        priority=priority,
        config={
            "static_prompt_digest": compiled.digest,
            "awaited_sources": awaited_sources,
            "deferred_render_gate": deferred_render_gate,
            "strategy": compiled.strategy,
            "regions": len(compiled.regions),
            "cached_regions": sum(1 for r in compiled.regions if r.directive.cacheable),
        },
    )

to_prompt_assembly

to_prompt_assembly(compiled: CompiledPrompt, *, prompt: str, attachments: tuple[Any, ...] = (), history: tuple[Any, ...] = ()) -> PromptAssembly

Project a compiled prompt into the kernel's prompt-assembly value.

Source code in src/symfonic/capabilities/prompting/assembly.py
def to_prompt_assembly(
    compiled: CompiledPrompt,
    *,
    prompt: str,
    attachments: tuple[Any, ...] = (),
    history: tuple[Any, ...] = (),
) -> PromptAssembly:
    """Project a compiled prompt into the kernel's ``prompt-assembly`` value."""
    return PromptAssembly(
        instructions=compiled_instructions(compiled),
        prompt=prompt,
        attachments=attachments,
        history=history,
        # ``CompiledPrompt.text`` remains the portable, readable projection.
        # The blocks retain the separate cache annotations until the provider
        # adapter makes its dialect decision.
        system_blocks=tuple(compiled.cache_annotations()),
    )