Skip to content

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

ToolPalette(names: frozenset[str] | None = None, forced: str | None = None)

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

check(registered: frozenset[str]) -> None

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.

Source code in src/symfonic/kernel/contracts/palette.py
def check(self, registered: frozenset[str]) -> None:
    """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.
    """
    if self.forced is None:
        return
    if self.forced not in registered:
        raise ForcedToolUnavailable(
            f"this turn forces {self.forced!r}, which is not a registered "
            f"tool. Registered: {sorted(registered) or 'none'}."
        )
    if self.names is not None and self.forced not in self.names:
        raise ForcedToolUnavailable(
            f"this turn forces {self.forced!r} and resolves a palette that "
            f"excludes it ({sorted(self.names)}). A selection stage that "
            "declares protects_forced = False dropped it deliberately, so "
            "the force and that stage disagree about this call. Align them "
            "rather than letting one win silently."
        )