symfonic.kernel.turn_guards¶
turn_guards ¶
What ends a turn during prompt-assembly, and why it is raised and not logged.
Both guards answer the same question — may this turn continue? — and both
exist because an earlier version answered "yes" by default and let the model be
called on a prompt that was missing something. They live together because the
policy is one policy: raise, do not degrade. Degradation has to be asked
for, and the way to ask is no_change(reason); a handler that raised, or
returned something that could not be applied, chose nothing.
Split out of :mod:.prompt_assembly when it crossed the 300-line budget. The
split is not arbitrary: that module is the composition of a phase with the
dispatcher, and this is the policy about failure inside it. They are read for
different reasons and by different people.
require_no_crashed_stage ¶
A handler that raised ends the turn. A stage nothing answered does not.
The narrower half of :func:require_no_failed_stage, and the distinction is
not cosmetic. StageOutcome.FAILED covers two different facts:
- the handler ran and raised —
erroris set. That is #92's case, and the reasoning is entirely about it: a handler that raised chose nothing, so continuing would answer as though nothing was wrong. - no handler resolved for the stage at all —
errorisNone. The stage never ran, and for aCapabilityContributionthis cannot happen:validate()refuses a declared stage with no handler at fold time. It is reachable only on the hand-assembled path, where the extension-seam probe declares apost-modelstage with no body on purpose and has a test saying so.
prompt-assembly refuses both, and keeps doing so: it has never met the
second case, so narrowing it there would be changing a behaviour on the
strength of a case that does not arise. The two new phases use this instead,
and the asymmetry is stated here rather than left to be discovered.
Source code in src/symfonic/kernel/turn_guards.py
require_no_failed_stage ¶
EVT-7: a stage that raised ends the turn, in whichever phase it ran.
phase names the phase in the message and nothing else. It exists
because #23 wired post-model and pre-tool and neither called this,
so a handler that raised in either was turned into a FAILED trace by the
dispatcher and then read by nobody -- the turn finished normally with a
stage having crashed. That is precisely the hole described below,
reintroduced at two new call sites by the change that created them.
The policy is the same at every phase on purpose. If it were not, which phase a capability attaches to would silently change what happens when its handler crashes, and no capability author reads the ladder for that.
Found in review of this module's first version, which returned the traces and let the caller decide — and then the caller decided nothing, so a capability whose handler raised had its contribution silently dropped and the model was called anyway, on a prompt missing whatever that capability was registered to add.
The reasoning that produced the hole is worth keeping: the traces were
dropped at the call site because RequestContext has no runtime
diagnostic slot, which is true. But a FAILED outcome is not a diagnostic —
it is a control-flow fact — and "I cannot record where this went wrong" was
treated as "I may ignore that it went wrong".
Raising rather than degrading, because degradation has to be asked for.
A capability that wants a turn to continue without it can return
no_change(reason), which is exactly what that outcome is for; a handler
that raised did not choose anything.
Source code in src/symfonic/kernel/turn_guards.py
require_no_malformed ¶
A stage whose contribution could not be applied ends the turn.
Raised from two places: after resolution, so compilation never sees a snapshot the kernel already knows is incomplete, and after compilation for its own stages. The first call site is the one that matters -- without it a compilation stage failing for want of the missing entry raised first, and the operator was told that stage failed. The producer broke the contract; the consumer got named.