symfonic.kernel.contracts.results¶
results ¶
What a stage hands back, and why the envelope is not just a value.
Decision 2 of W2/1b: a stage returns a contribution, it does not mutate the request context. Decision 3: it returns its events in the same result, so one execution is one transactional unit — the dispatcher validates, applies the contribution, then publishes a contiguous block of events. A stage that fails leaves no partial events announcing a change that never landed.
The envelope exists because an empty contribution does not explain itself. This
codebase reached ~27,000 unreachable lines by letting "declared" and "does
something" be indistinguishable, six separate times. NoChange(reason) is the
antidote: a stage that legitimately had nothing to do says so and says why,
and is therefore distinguishable from a stage that is broken, refused, or inert.
Three rules that are easy to get wrong and are load-bearing here:
- Returning is not applying.
proposedandappliedare different trace states. The dispatcher may validate, merge, reject, or find a proposal changes nothing — so a handler cannot assume its contribution took effect. emitsis an upper bound, checked. A stage may emit fewer events than it declared (emission is conditional); it may never emit one it did not declare. That is what stopsCompiledStage.emitsfrom being the next field that is declared and never read.- No universal contribution type. Each phase returns its own — a
PromptContributionis not a tool decision. They share only this envelope. A generic bag would push the "what does this mean?" question to every reader.
ModelPortBinding
dataclass
¶
ModelPortBinding(run_id: str, scope_path: str, session_id: str, prompt_admissions: tuple[PromptAdmissionReceipt, ...] = ())
Kernel-stamped turn metadata; compiler admission is not delivery.
ModelPortInputAttempt
dataclass
¶
Attempted bound Runnable handoff, not provider receipt or acknowledgement.
PromptAdmissionReceipt
dataclass
¶
PromptAdmissionReceipt(run_id: str, scope_path: str, admitted: tuple[str, ...] = (), dropped: tuple[str, ...] = (), session_id: str = '', compiler_scope_path: str = '', compiler_session_id: str = '')
Private compiler decisions, never evidence of provider delivery.
Turn-bound contribution identities only; contains no rendered payload. Arbitrary model adapters may discard the compiled prompt. Admission says nothing about delivery, acknowledgement, or prospective trigger resolution. This value stays inside the invocation and is never put in stage traces.
StageOutcome ¶
Bases: StrEnum
What happened to one stage execution.
Four states, not two, because "nothing changed" has three distinct causes and conflating them is how an inert stage hides behind a healthy one.
APPLIED
class-attribute
instance-attribute
¶
The stage proposed a contribution and the dispatcher applied it.
FAILED
class-attribute
instance-attribute
¶
The stage raised. Carries the error for the trace; the dispatcher decides whether that ends the turn (EVT-7) or degrades.
NO_CHANGE
class-attribute
instance-attribute
¶
The stage ran, had nothing to contribute, and said why.
REJECTED
class-attribute
instance-attribute
¶
The stage proposed something the dispatcher refused. Not an error: a contribution can be well-formed and still inadmissible.
StageResult
dataclass
¶
StageResult(outcome: StageOutcome, contribution: ContributionT | None = None, events: tuple[Any, ...] = (), reason: str = '', error: BaseException | None = None, diagnostics: tuple[str, ...] = tuple(), counts: Mapping[str, int] = (lambda: EMPTY_COUNTS)(), prompt_admission: PromptAdmissionReceipt | None = None)
Bases: Generic[ContributionT]
One stage execution: what it proposes, what it emits, and why.
contribution is None for every outcome except :attr:StageOutcome
.APPLIED — and the constructors below are the supported way to build one,
because they make that invariant unstatable rather than merely documented.
applied ¶
applied(contribution: ContributionT, *, events: tuple[Any, ...] = (), diagnostics: tuple[str, ...] = (), counts: Mapping[str, int] | None = None, prompt_admission: PromptAdmissionReceipt | None = None) -> StageResult[ContributionT]
The stage proposes a change.
Source code in src/symfonic/kernel/contracts/results.py
failed ¶
freeze_counts ¶
A read-only copy, so a stage cannot edit what it already reported.
Source code in src/symfonic/kernel/contracts/results.py
no_change ¶
no_change(reason: str, *, diagnostics: tuple[str, ...] = (), counts: Mapping[str, int] | None = None, prompt_admission: PromptAdmissionReceipt | None = None) -> StageResult[Any]
The stage ran and had nothing to do. reason is not optional.
Counts are accepted here too, and that is the interesting case: a
retrieval that found nothing reports found=0, which is a measurement.
Reporting no counts at all would leave a reader unable to tell it from a
stage that does not count.
Source code in src/symfonic/kernel/contracts/results.py
rejected ¶
The stage declined to act, and says why.