Skip to content

symfonic.capabilities.tools.stages.forced

forced

Forced-choice stage — resolve (and release) the mandatory tool.

This stage answers which tool is forced. It deliberately does not narrow the candidate set: keeping the force alive through later stages is the pipeline's protection rule, which is what makes "force wins over the palette" a property of the composition rather than a special case buried in one consumer.

ForcedChoiceStage

ForcedChoiceStage(*, resolver: Any = None, state_key: str = 'forced_tool_choice', is_tool_message: Callable[[Any], bool] = _default_is_tool_message)

Resolve the forced tool_choice for this iteration.

Precedence, unchanged from v7.10:

  1. an explicit state["forced_tool_choice"] stamp — tests and adopters pre-stamp directly;
  2. the registered resolver, which re-runs every iteration so force and release are symmetric by construction;
  3. None — let the model choose, the safe default.

The release check runs on both paths: once a ToolMessage naming the forced tool is in history the force is spent. A resolver that raises releases too, because a stalled loop is worse than an unforced turn.

Source code in src/symfonic/capabilities/tools/stages/forced.py
def __init__(
    self,
    *,
    resolver: Any = None,
    state_key: str = "forced_tool_choice",
    is_tool_message: Callable[[Any], bool] = _default_is_tool_message,
) -> None:
    self._resolver = resolver
    self._state_key = state_key
    self._is_tool_message = is_tool_message