symfonic.core.contracts.tool_policy¶
tool_policy ¶
v8.1.0 -- ToolCallPolicy declarative per-intent routing primitive.
A single declarative rule that core fans out to TWO seams of one React
iteration, sharing ONE match predicate:
- PRE-model (model select,
react.py~:814): whenmodelis set andmatch(ctx)is True, the policy supplies the React brain model for THIS iteration -- composed BEFORE the adopterrole_model_resolver. - POST-model (tool redirect,
react.py~:1147): whenredirect_tois set andmatch(ctx)is True AND the emitted tool matcheswhen_toolANDguard(if any) passes, the emitted tool call is rewritten toredirect_to-- composed BEFORE the adopteron_tool_call_dispatchcallback, then the palette gate runs last.
One policy may carry model only (pure per-intent model), redirect_to
only (pure tool enforcement), or BOTH (the synthesis: cheap model + pinned
tool from a single declaration).
Forward-compat note (v8.1.0 API decision): there is NO action field.
A redirect is inferred from redirect_to being set. The v8.1 release ships
ONLY redirect + model (reject / abstain-sentinel are DEFERRED).
A future reject action can be added as an explicit action field then,
without carrying a dead single-valued Literal today.
The policy is a @dataclass(frozen=True) (NOT pydantic) because it carries
bare callables (match, guard, args_transform). FrameworkConfig
is a frozen Pydantic model; a frozen dataclass holds callables cleanly and
mirrors the existing DispatchContext resolver-contract convention
(resolvers.py).
ToolCallPolicy
dataclass
¶
ToolCallPolicy(
name: str,
match: Callable[[DispatchContext], bool],
when_tool: str | frozenset[str] | None = None,
redirect_to: str | None = None,
model: ModelConfig | None = None,
args_transform: Callable[
[dict[str, Any], DispatchContext], dict[str, Any]
]
| None = None,
guard: Callable[[DispatchContext], bool] | None = None,
)
Declarative {intent -> model + tool-redirect} rule (v8.1.0).
Fields:
name-- required, non-empty. For logging / inspection / test assertions.match-- the intent gate,Callable[[DispatchContext], bool]. Runs at BOTH seams against an equivalentDispatchContext(the adopter classifies intent offmessages[-1].content/scope/resolved_tool_names). Core ships NO intent classifier -- intent is adopter business logic.when_tool-- POST-model emitted-tool filter.str|frozenset[str]|None.Nonematches any emitted tool. NO meaning at the PRE-model seam (no tool exists yet there). A SEPARATE field frommatchsomatchstays a pure intent predicate reusable at both seams.redirect_to-- POST-model target tool name. Setting it makes the policy a redirect policy. Subject to the palette gate (an unrouted target is dropped with a WARNING; the original dispatches).model-- PRE-model per-intent model override (ModelConfig). Applied ONLY at the PRE-model seam.args_transform-- optional POST-model arg rewriter,Callable[[args, ctx], args].Nonepasses args through.guard-- optional deterministic precondition,Callable[[DispatchContext], bool]. Distinct frommatch:match= "is this the right intent?";guard= "is the precondition satisfiable?" (e.g. the stored table exists for THIS scope).guardgates the REDIRECT only -- it does NOT gate model-selection (a missing data source must not change which model reasons). The policy redirects only whenmatch(ctx)AND (guard is None or guard(ctx)).
matches_tool ¶
True when tool_name satisfies when_tool (POST-model).