symfonic.agent.cutover.lifecycle_refusals¶
lifecycle_refusals ¶
The refusals that enforce the public lifecycle contract (TA8.41).
The contract itself -- which entry point serves, refuses or is inert for each
row, and which consumer reads it -- is data, and it lives next door in
:mod:~symfonic.agent.cutover.lifecycle_contract. Read that first. This module
is only the three raise sites, and it is separate for the reason
:mod:~symfonic.agent.cutover.config_retirement is separate from
:mod:~symfonic.agent.cutover.settings_contract.
Before dispatch, never from the verdict. All three run at the top of
run/stream/stream_typed, beside
:func:~symfonic.agent.engine._refuse_retired_arguments, and for that guard's
three reasons: a verdict-driven refusal would be route-conditional, stream
skips the verdict entirely on a line that predates the text-delta chunk
contract, and a rolled-back switch would re-honour on the legacy body what this
line declines to serve.
Emptiness is not use, once per refusal. response_model=None and
transcript_persistence_enabled=False are what a caller who asks for nothing
passes, and neither reaches a raise.
Every error here widens, and none renames. Each subclasses both
SymfonicAgentError -- so an adopter's existing except around the agent
API keeps catching it, which for StreamingDisabledError also means the bare
error it replaces stays caught -- and CutoverPathError, which is the
vocabulary the cutover guards refuse in.
StreamingDisabledError ¶
Bases: SymfonicAgentError, CutoverPathError
streaming_enabled=False and a streaming entry point was asked.
The ancestry is a widening in both directions and a rename in neither.
SymfonicAgentError is what this raised before it had a name of its own
(a bare SymfonicAgentError("Streaming is disabled in configuration")),
so every existing except and every existing message match still catch
it. CutoverPathError is the vocabulary the cutover guards refuse in,
which is what lets an operator tell a contract refusal from a provider
failure without parsing prose.
Source code in src/symfonic/agent/cutover/lifecycle_refusals.py
StructuredOutputUnsupportedError ¶
Bases: SymfonicAgentError, CutoverPathError
response_model was supplied to a streaming entry point.
Same ancestry, same reason. Before this class the argument landed in
**state_overrides and refused as a retired argument, which named the
wrong thing: state_overrides is retired, response_model is not --
it is supported, on one surface, and this says which.
Source code in src/symfonic/agent/cutover/lifecycle_refusals.py
UnaddressableTranscriptError ¶
Bases: SymfonicAgentError, CutoverPathError
Transcript persistence was asked for on a turn nothing can read back.
transcript_persistence_enabled=True wires a LangGraph checkpointer, and
a checkpointer needs a thread_id. The engine derives one from
(scope, session_id) -- the single derivation site
:func:symfonic.capabilities.human.threads.thread_id_for, shared with
get_transcript and with working-deque rehydration -- and a turn that
supplies neither has no key to file the checkpoint under.
Before TA8.41 that turn reached LangGraph and died there with
ValueError: Checkpointer requires one or more of the following
'configurable' keys, an error that names no field of this framework and
no action for the adopter. That is what made this row's admission-inventory
outcome UNKNOWN on both driven entry points rather than a measurement.
Same ancestry, and the same reason, as the two errors above it.
Source code in src/symfonic/agent/cutover/lifecycle_refusals.py
refuse_disabled_streaming ¶
Refuse a streaming turn on an agent whose configuration disabled it.
Called from stream and stream_typed in the position the bare
SymfonicAgentError guard occupied, which is above the cutover dispatch
on purpose: the field governs the entry point, so honouring it must not
depend on which body would have served the turn.
Source code in src/symfonic/agent/cutover/lifecycle_refusals.py
refuse_streaming_structured_output ¶
Refuse response_model on a streaming entry point, or return.
None is not a request, so the ordinary streaming turn never reaches the
raise: emptiness is not use, the same rule TA8.26 states for the retired
configuration fields.
Source code in src/symfonic/agent/cutover/lifecycle_refusals.py
refuse_unaddressable_transcript ¶
Require a checkpoint identity when persistence is on, or return.
The precondition is the same one ask_user_enabled has enforced since
v7.1.1, and it is stated here in the same words for the same cause: both
flags wire the checkpointer, and a checkpoint bound to an ephemeral
LangGraph-generated thread cannot be found again.
It is a refusal rather than a generated fallback identity because a
transcript is only worth persisting if it can be read back, and the reader
-- SymfonicAgent.get_transcript(scope=..., session_id=...) -- takes
exactly the two things this turn declined to supply. Minting a per-run key
here would produce a durable row no API can address, which is the
accepted-then-no-op outcome the 11.0 guards exist to end.
Route-independent and above the dispatch, for
:func:refuse_disabled_streaming's reasons.