Skip to content

symfonic.kernel.contracts.stages

stages

The eight-phase ladder and the stage descriptors that populate it (STG).

A capability declares what it contributes and what it must sit beside; it never declares a position. Position is computed, once, by the compiler — which is what keeps two capabilities from fighting over an ordering neither of them can see.

CompiledStage dataclass

CompiledStage(stage_id: str, phase: Phase, capability: str, priority: int, effects: frozenset[str] = frozenset(), emits: frozenset[str] = frozenset(), kind: StageKind = StageKind.COMPILATION, config: Mapping[str, Any] = (lambda: MappingProxyType({}))(), tie_break: str = 'first-in-phase')

A stage with its position decided and the reason recorded (STG-12).

Coerces kind for the same reason :class:StageDescriptor does, and it matters more here: this is the type the dispatcher actually reads, and its identity comparison is only valid because of a guarantee this class has to make itself. ordering.py copies an already-coerced descriptor, so the shipped path was safe -- but CompiledStage and StageProgram are public, and any other construction site got the vanishing-stage defect back with no refusal anywhere.

Phase

Bases: StrEnum

The eight phases, in the only order they ever run (STG-1).

StageDescriptor dataclass

StageDescriptor(stage_id: str, phase: Phase, capability: str = _KERNEL_CAPABILITY, priority: int = 0, after: tuple[str, ...] = (), before: tuple[str, ...] = (), optional_after: tuple[str, ...] = (), optional_before: tuple[str, ...] = (), effects: frozenset[str] = frozenset(), emits: frozenset[str] = frozenset(), kind: StageKind = StageKind.COMPILATION, config: Mapping[str, Any] = (lambda: MappingProxyType({}))())

One capability's declaration of a stage it contributes (CON-C-1).

validate

validate() -> None

Shape validation, performed the moment a capability registers (STG-4).

Resolution of the constraints against the full stage set happens later, at compile time, when the whole set is finally knowable.

Source code in src/symfonic/kernel/contracts/stages.py
def validate(self) -> None:
    """Shape validation, performed the moment a capability registers (STG-4).

    Resolution of the constraints against the full stage set happens later,
    at compile time, when the whole set is finally knowable.
    """
    if not self.stage_id:
        raise ConfigurationError("a stage descriptor must declare a non-empty stage_id.")
    if self.phase not in PHASE_LADDER:
        raise ConfigurationError(
            f"stage {self.stage_id!r} declares unknown phase {self.phase!r}; "
            f"the ladder is {[phase.value for phase in PHASE_LADDER]}."
        )
    if self.phase in KERNEL_OWNED_PHASES and self.capability != _KERNEL_CAPABILITY:
        raise ConfigurationError(
            f"stage {self.stage_id!r} (capability {self.capability!r}) targets the "
            f"kernel-owned phase {self.phase.value!r}; only the kernel registers "
            "into 'bind' and 'teardown'."
        )
    constrained = (
        *self.after,
        *self.before,
        *self.optional_after,
        *self.optional_before,
    )
    if self.stage_id in constrained:
        raise ConfigurationError(
            f"stage {self.stage_id!r} declares a constraint on itself."
        )
    # Checked here rather than at dispatch: an unknown family that reaches
    # dispatch is indistinguishable from one that was simply not granted,
    # so a typo would read as a policy refusal.
    require_known_families(self.effects, subject=f"stage {self.stage_id!r}")
    # Scoped to PROMPT_ASSEMBLY on purpose. Only that phase is split today,
    # because only that phase has a pure consumer that a prior effect must
    # not contaminate. A POST_MODEL stage writing to memory is effectful and
    # has no compiler downstream to keep pure, so the distinction has
    # nothing to say about it -- and applying the rule there would have made
    # every effectful stage outside P1 declare a kind that means nothing.
    if (
        self.phase is Phase.PROMPT_ASSEMBLY
        and self.kind is StageKind.COMPILATION
        and self.effects
    ):
        raise ConfigurationError(
            f"stage {self.stage_id!r} (capability {self.capability!r}) is a "
            f"compilation stage and declares effect(s) {sorted(self.effects)}. "
            "STG-7: compilation is a pure function of (plan, request, "
            "snapshot). A stage that reaches outside declares "
            "kind=StageKind.RESOLUTION, runs before every compilation stage "
            "in its phase, and contributes to the snapshot rather than to "
            "the assembly."
        )

StageKind

Bases: StrEnum

Which half of a phase a stage belongs to (STG-7, as reformulated).

Only prompt-assembly is split today, and the split is what lets an effectful retrieval inform a prompt without the compiler losing its purity:

  • resolution — may perform I/O, once per invocation, under an STG-8 grant. Produces entries in the turn's resolved-input snapshot and nothing else; it never writes the assembly.
  • compilation — a pure function of (plan, request, snapshot). No I/O, no port that performs an effect, no mutation of the snapshot.

The default is COMPILATION because a stage that declares no effect is pure, and because every stage that existed before this distinction was one. Declaring RESOLUTION is how a capability says "I am the one that reaches outside", and it is checked: a compilation stage may not declare effects.