symfonic.kernel.contracts.palette¶
palette ¶
What a turn resolved about its tools: which ones, and which one it must use.
Two answers, and until now only the first had anywhere to go. A capability
resolved a set of names, the kernel bound that subset, and the model chose
freely within it. Forcing a choice -- "this turn must call
get_integration_status" -- had a public stage that computed it and no
transport, so procedural_force_tool_choice='hard' was a lever with a class
behind it and no path to the model.
The two are one value because they are one decision made at one moment, and
splitting them would let a turn bind a palette that excludes the very tool it
forces. :meth:ToolPalette.check refuses that combination rather than leaving
a provider to fail on it three layers down.
ForcedToolUnavailable ¶
Bases: ContractViolationError
A forced tool that cannot be offered. Raised before the model is called.
A ContractViolationError because that is what it is: two parts of a
composition disagreeing, or a composition naming a tool that does not
exist. A caller catching the framework's contract error catches this
without knowing the name.
ToolPalette
dataclass
¶
The tools this turn offers, and the one it requires.
names of None means the full palette -- the no-router case, and
the case a refusing router lands on. It must stay indistinguishable from
the behaviour before routing existed.
forced of None means the model chooses. Anything else is bound as
the provider's tool_choice, which is a requirement rather than a
suggestion: the model returns that call or the provider errors.
check ¶
Refuse a palette that cannot be honoured, here rather than later.
Two ways it cannot be, and both are deterministic -- the same composition and the same turn give the same refusal every time, which is what makes a forced selection something an operator can rely on rather than something that usually works.
Forcing a tool the deployment never registered is a configuration error wearing a runtime disguise: the provider would reject the call with its own message, on its own schedule, naming its own field.
Forcing a tool this turn's palette excludes is the sharper one. The
pipeline reinstates a forced tool that a protecting stage dropped,
so reaching here means a stage that declared protects_forced =
False -- a hard filter -- deliberately removed it. Two rules then
disagree about one call, and picking either silently is worse than
saying so: honour the force and the hard filter was decorative;
honour the filter and the force was.