symfonic.agent.triage.tool_routing¶
tool_routing ¶
Dynamic tool routing — per-turn resolved_tools narrowing (v7.0.1).
v7.0.0 shipped the IntentClassifier (T06/T07) but only wired it
into the hydration path. The Anthropic tools= API payload still
carried the full static manifest every turn — paying 5-7k tokens per
casual query when the tenant ran ~18 tools.
This module closes that gap. narrow_tools_for_intent computes
the per-turn tool subset from an IntentVerdict and a list of
registered BaseTool instances, reusing the
DomainTemplate.tool_trigger_keywords map that v6.1.8 already
ships for prompt-text gating. Pure-function / keyword intersection
only — zero LLM calls, zero allocations beyond the output list.
The engine wires this via FrameworkConfig.tool_routing_mode:
off(default) — helper never runs,resolved_toolsstaysNone,react.pybinds the full static list. Byte-for-byte identical to v7.0.0.observe— helper runs, narrowed set is computed, atool_routing_decisioncallback event fires, butresolved_toolsreceives the full manifest so the LLM sees no behaviour change. Safe to enable in prod while gathering data.enforce— narrowed set is written tostate["resolved_tools"]and becomes the only list the LLM sees this turn.
estimate_dropped_tokens ¶
Rough token estimate for the tools we chose not to bind.
Used for the tool_routing_decision telemetry event so
operators can correlate classifier behaviour with token savings
without instrumenting the provider payload themselves. Uses the
standard chars // 4 rule of thumb against description +
name. Schemas are ignored: the estimate is a floor, not a
ceiling.
Source code in src/symfonic/agent/triage/tool_routing.py
narrow_tools_for_intent ¶
narrow_tools_for_intent(
verdict: IntentVerdict | None,
all_tools: Sequence[Any],
domain: DomainTemplate | None,
*,
always_include: Sequence[str] = DEFAULT_ALWAYS_INCLUDE,
) -> list[Any]
Return the subset of all_tools to bind for the current turn.
Decision matrix:
verdictisNoneor the verdict isambiguous— returnall_toolsunchanged. Conservative fallback preserves recall whenever the signal is noisy.verdict.label == "knowledge"— return only thealways_includetools that exist inall_tools. The classifier is confident the turn is a lookup; the LLM should not need action tools.verdict.label == "action"— keep any tool whose name appears inverdict.matched_tool_keywords(per the domain'stool_trigger_keywordsmap). Tools with no keyword list declared in the domain are always kept for back-compat (v6.1.8 semantics: absent entry means "always include"). Always add thealways_includetools.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
verdict
|
IntentVerdict | None
|
Output of |
required |
all_tools
|
Sequence[Any]
|
The full tool manifest registered on the agent.
Each element must expose a |
required |
domain
|
DomainTemplate | None
|
The active |
required |
always_include
|
Sequence[str]
|
Tool names that must appear in every
narrowed set regardless of verdict. Defaults to
|
DEFAULT_ALWAYS_INCLUDE
|
Returns:
| Type | Description |
|---|---|
list[Any]
|
A list of tools preserving the input order. Never raises; |
list[Any]
|
unknown tool names, missing |
list[Any]
|
|