symfonic.agent.cutover.lifecycle_contract¶
lifecycle_contract ¶
The public lifecycle contract: which entry points honour which row (TA8.41).
TA8.25's subgroups C1-L (response_model, streaming_enabled,
transcript_persistence_enabled) and C1-R (run_id) are the public
run/stream/checkpoint lifecycle and its telemetry correlation. TA8.27 labelled
two of the four RETAIN_REQUIRES_CONTRACT and one
RETAIN_REQUIRES_MIGRATION -- and contract is the operative word: the
inventory could only record that no guard stopped these values, never that
anything honoured them.
The contract is written here, and the code depends on it rather than the
other way round. :data:LIFECYCLE_ROWS names, per row, the surface it
governs, the consumer that reads it and the observable that moves when the
value moves. tests/agent/cutover/test_public_lifecycle_contract.py drives
each row on run, stream and stream_typed against exactly those statements, so a
row cannot quietly become a value threaded to nothing -- the defect TA8.19 found
for session_id and TA8.12 was written to prevent for agent_depth.
A surface is part of the contract, not an accident of the signature. Two rows here are not honoured everywhere, and saying so by name is the whole point:
streaming_enabledgoverns the two streaming entry points and nothing else. Onrunit is inert by contract -- a blocking turn has no stream to disable -- which is why 11.0 admits it there instead of sending every turn of a stream-disabled deployment back to the legacy body.response_modelis a blocking-turn contract. Structured output is bound to a plan and delivered onAgentResponse; neither streaming projection has anywhere to put it, and inventing one on the kernel route alone would make the capability route-conditional, which is the shape this programme refuses.
Both refuse by name on the entry points that do not serve them, in
:mod:~symfonic.agent.cutover.lifecycle_refusals. A refusal that names the
field is a contract; falling to legacy because the envelope is default-deny is
silence.
This module is the contract as data, and
:mod:~symfonic.agent.cutover.lifecycle_refusals is the rule that enforces it
-- the same split :mod:~symfonic.agent.cutover.settings_contract and
:mod:~symfonic.agent.cutover.config_retirement already make, for the same
reason: the published surface should be readable end to end without the
raise sites in the way, and each piece stays inside the repository's module
budget.
LifecycleRow
dataclass
¶
LifecycleRow(path: str, subgroup: str, owner: str, served_by: tuple[str, ...], refused_by: tuple[str, ...], inert_on: tuple[str, ...], consumer: str, observable: str, disposition: str)
One row of the public lifecycle contract.
Three dispositions per entry point, not two, because collapsing the third into either of the others is how a surface gets misreported:
served_by-- the entry point honours the row; the named consumer reads the value and the named observable moves with it.refused_by-- the entry point refuses it by name, which is a contract. Falling to legacy because the envelope is default-deny is not a refusal and never counts here.inert_on-- the entry point neither reads it nor refuses it, and that is the published behaviour rather than an omission.streaming_enabledonrunis the case: a blocking turn has no stream to disable, so the field asks nothing of it. An inert entry point is why a row can be admitted to the kernel without a value-reading consumer there, and saying so out loud is what stops "admitted" from being read as "honoured".
The three must partition :data:PUBLIC_ENTRY_POINTS exactly, because "the
third entry point was never measured" is the gap that made two thirds of
this programme's earlier evidence unusable, and a row with a hole in its
surface would reintroduce it as a data structure.
consumer is file::symbol and observable is what an operator sees
change when the value changes. Both are prose the tests assert against a
driven turn, not decoration.