symfonic.kernel.pre_tool¶
pre_tool ¶
The pre-tool seam: capability stages that see a round's tool calls.
23 slice 2, and the same shape as :mod:symfonic.kernel.post_model — dispatch¶
and nothing else, grants passed explicitly, no contract invented for what a contribution mutates.
Two things are particular to this phase.
It is per batch, not per call. The two governance stages this phase is for
(tool_preconditions, policy_steering) both delegate to sweep, which
asks each applicable objector about each call. One invocation covers the whole
collection, so narrowing the seam to a single call would either need the subject
narrowed first or make every objector re-examine calls it has already judged.
The requests are handed over as a tuple for that reason.
Its cardinality can be zero. The runner breaks out of the loop when a round returns no tool requests, so a turn that answers directly runs this phase no times at all. That falls out of the existing control flow for free, which is exactly why it needs a test of its own: nothing else guards it, and a dispatch moved one line earlier would run on every round without any other test noticing.
PreToolContext
dataclass
¶
PreToolContext(plan: Any, request: Any, stage: Any, resolved: Any, turn: Any, requests: tuple[Any, ...], transcript: Any = None)
What a pre-tool stage is handed.
requests is the whole batch the round asked for, already reserved by
the kernel — so a stage sees the call ids the transcript will join on, not
the provider's own. turn comes too, because a precondition is a fact
about the call and a policy is a fact about the deployment, and the second
sometimes needs what the model said to judge the first.
run_pre_tool
async
¶
run_pre_tool(plan: Any, request: Any, turn: Any, requests: Sequence[Any], *, resolved: Any = None, transcript: Any = None, handlers: Mapping[str, StageHandler] | None = None) -> tuple[tuple[StageTrace, ...], tuple[Any, ...]]
Dispatch the pre-tool stages, and return the calls to dispatch.
Called only on a round that has calls; see the module docstring on why the zero case is the control flow's and not this function's.
Returns (traces, requests). The second element is the batch the loop
must execute: the reserved calls, or whatever a resolution stage amended
them to. Before this, the loop iterated the batch built before the
stages ran, so a stage that rewrote a call had nowhere to put it --
governance_rungs._undeliverable said so in the framework's own words,
and three separate defects were that one sentence.
The mechanism is prompt-assembly's, not a new one: a
:class:~.contracts.stages.StageKind.RESOLUTION stage returns
applied(ResolvedInput(...)) and the kernel folds it. What was missing
is that this phase folded nothing.