symfonic.capabilities.tools.selection¶
selection ¶
Running the selection pipeline for one turn, and refusing what cannot run.
ToolSelectionPipeline and its seven stages have been public since T3.1.2
and had no kernel consumer: ToolsCapability called resolve_palette and
nothing else, so ForcedChoiceStage was a class an adopter could construct,
compose into a pipeline, and never reach the model with. This is the half that
was missing -- what a composed pipeline means for one turn.
One writer for the force. SelectionContext.forced_choice is written by
whichever stage sets it, last writer winning, which is invisible and
order-dependent. Two stages that both answer "which tool is required" is a
composition mistake, so it is refused when the pipeline is composed rather
than resolved silently at every turn.
candidates_for ¶
The registered tools as the stages read them.
Descriptors rather than the live tool objects, and the reason is that the capability is handed names: every shipped stage is name-keyed -- an allowlist, a role palette, a policy ceiling, a forced choice -- so a descriptor carries everything they consult. The kernel binds the live objects afterwards, from the names this produces.
Source code in src/symfonic/capabilities/tools/selection.py
check_one_force_writer ¶
Refuse a pipeline where two stages answer the same question.
Detected by declaration -- a stage that resolves a force is a
ForcedChoiceStage or something that names itself one -- rather than by
running it and seeing what it writes, because a stage that forces only
under some conditions would compose cleanly and collide in production on
the turn where both fire.
Source code in src/symfonic/capabilities/tools/selection.py
run_selection
async
¶
run_selection(pipeline: ToolSelectionPipeline, registered: Sequence[str], *, state: Any = None, messages: Sequence[Any] = (), query: str = '', role: str = '') -> SelectionResult
One turn through the composed stages.
state is the turn's own declared state, passed by identity because an
adopter's resolver reads its own keys out of it -- the same contract the
pre-kernel react node had.