symfonic.agent.cutover.orchestrator_policy¶
orchestrator_policy ¶
The 11.0 memory-orchestrator policy contract (TA8.37, C1-E).
TA8.25's subgroup C1-E is the memory orchestrator read as configuration: a
container (orchestrator), ten nested knobs, and a top-level
enabled_layers that duplicates one of them. The triage recommended
REPLACE for the whole subgroup — "a container plus duplicate
enabled_layers and implementation-level factory knobs should be replaced by
a versioned orchestrator policy rather than copied wholesale" — and this module
is the contract 11.0 publishes, written before the guard below and the tests
next door depend on it.
The replacement is not one policy object, and saying so is the point. The subgroup's twelve rows are three different kinds of thing, and a single "versioned orchestrator policy" would have flattened the difference:
- two are read on the migrated route and are therefore admitted rather than
replaced —
orchestrator.default_top_kandorchestrator.scoring_weightsreach a kernel turn through the retrieval port the composition root folds. Replacing them would have retired values the migrated path demonstrably honours, which is the mistake TA8.40 found for theotel_*fields. - five are dead where they are written, and this module refuses them by
name. Three say so in their own field descriptions (
compaction_thresholdis "Deprecated -- use SleepConsolidator phases instead";llm_modelandllm_temperatureare "Deprecated -- never read at runtime"), and two are overwritten before anything reads them:SymfonicAgent._sync_enabled_layerscopies the top-levelFrameworkConfig.enabled_layersoverorchestrator.enabled_layersand the top-levelscope_blend_modeoverorchestrator.scope_blend, so an adopter who sets either nested field today gets a silent no-op. - three name a write boundary this line does not cross —
context_budget,write_policyandworking_graph_retention. They keep their recorded REPLACE unapplied, with the argument recorded per row in :data:~symfonic.agent.cutover.memory_surfaces.MEMORY_SURFACES: a REPLACE has to leave the capability standing under a new contract, and the folded bundle declaresmemory-readand folds no write capability at all, so there is no contract to move them onto yet.
Why the five are REPLACE and not RETIRE. Every one of them has a live
destination on this line, named per row in :attr:ReplacedOrchestratorSetting.
replaced_by. What does not survive is the spelling, and for the two
overwritten rows that is a strict improvement: a silent no-op becomes a named
refusal that says where the value actually goes.
Before dispatch, never from the verdict. The guard below runs at the top of
run/stream/stream_typed beside
:func:~symfonic.agent.cutover.config_retirement.refuse_retired_setting, and
for that function's three reasons: stream and stream_typed do not
consult the same verdict on every reported line, and a rolled-back switch would
re-honour a replaced field. A retirement keyed on which body served the turn is
route-conditional, which is the shape this programme refuses.
Why these rows are not in RETIRED_SETTINGS. They are nested, and
retired_setting_supplied's predicate is a flat getattr against a stock
default — the same reason TA8.42's agent.ask_user_pause_ttl_seconds is
refused by :mod:~symfonic.agent.cutover.pause_policy instead of by that
table. The mechanism is otherwise identical: same error ancestry, same group
table, same line constant, one refusal shape.
The stock baseline is derived, never stored. Each row is compared against a
default-constructed instance of the live OrchestratorConfig, through
:func:~symfonic.agent.cutover.baseline.stock_instance, so a table row can
never drift from the model it describes — and a field this table names that the
model later drops is reported as a coverage failure rather than skipped.
ReplacedOrchestratorSetting
dataclass
¶
One nested orchestrator knob replaced on the 11.0 line.
replaced_by names the live surface an adopter moves to and
adopter_break names what they lose moving there — both required, for
the reason TA8.42's :class:~symfonic.agent.cutover.pause_surfaces.
PauseSurface gives: a refusal that names the line and stops is a dead end,
and a replacement named without its break is a breaking change shipped as a
migration note.
ReplacedOrchestratorSettingError ¶
Bases: RetiredConfigurationError
A nested orchestrator field replaced on the 11.0 line was set.
Subclasses
:class:~symfonic.agent.cutover.config_retirement.RetiredConfigurationError
so except RetiredConfigurationError around the agent API keeps catching
this — the widening-never-a-rename discipline that error states for its own
ancestry, and the one
:class:~symfonic.agent.cutover.pause_policy.ReplacedPauseSettingError
already follows. A subclass rather than a fifth sibling because it refuses
the same kind of thing at the same moment: a fact about how this agent was
built, named before dispatch.
Source code in src/symfonic/agent/cutover/config_retirement.py
refuse_replaced_orchestrator_setting ¶
Refuse a replaced nested orchestrator knob on entry_point, or return.
C1-E's REPLACE, expressed once and called from all three public entry
points. The position, and the reasons for it, are
:func:~symfonic.agent.cutover.config_retirement.refuse_retired_setting's.
There is no detail parameter and no release-line branch, for that
function's reasons: the next step comes from
:data:REPLACED_ORCHESTRATOR_SETTINGS, so no call site can forget to carry
one, and 9.12 and 10.4 keep these fields in their own artefacts because
this module is not backported.
Source code in src/symfonic/agent/cutover/orchestrator_policy.py
replaced_orchestrator_setting_supplied ¶
The first replaced nested knob config actually uses, or None.
Returns (field, value) in :data:REPLACED_ORCHESTRATOR_SETTINGS order.
Emptiness is not use, the rule TA8.26 states for the twenty-five
retired fields: a knob left at the stock OrchestratorConfig default is
what a caller who asks for nothing passes, so only a value that differs is
a request. There is no separate inactivity class here — unlike a flat
credential_patterns=[], none of these five has a second "off" value
distinct from its default.
Answers None — "nothing to refuse" — when the question cannot be put: a
config object with no orchestrator attribute, an orchestrator whose
class cannot be default-constructed, or a container missing the field. That
is the same bounded skip retired_setting_supplied takes, for the same
reason: cutover reflects over whatever object it is handed, and an
object without the attribute cannot be asking for the behaviour.
The comparison is against a derived stock instance rather than a stored table of defaults, so this rule cannot disagree with the live model.