Tool search
When an agent carries dozens of tools — several MCP servers, a big in-process registry — shipping every full JSON schema on every model call costs tens of thousands of tokens and measurably lowers tool-selection accuracy. Seven typical MCP servers weigh in around 67K tokens of tool definitions before the model reads a single word of your prompt.
Tool search is progressive disclosure for tool definitions: ship
cheap stubs by default, let the model browse the catalogue with a
local search_tools tool, and pay for a tool’s full schema only
after the model actually uses it.
from loomflow import Agent, Tuning
agent = Agent(
"You are an ops assistant.",
model="claude-opus-4-7",
tools=big_registry, # 50+ tools, MCP servers, ...
tuning=Tuning(
tool_search=True,
tool_search_threshold_tokens=10_000, # default
keep_tools=["read", "search"], # always-on core set
),
)Off by default. Tuning(tool_search=False) is the default and the
default path is byte-identical to previous behaviour — enabling it is
an explicit opt-in per agent. Small tool sets below the threshold stay
full even when the flag is on.
Why tool search
The tool-definition block is resent on every turn of the ReAct loop. With a large catalogue that means:
- Cost — you pay for every schema on every call, even for the 45 tools the model never touches in this session.
- Accuracy — models pick tools worse when the catalogue is huge; a short list of one-liners is easier to scan than pages of JSON Schema.
A run that uses 4 of 50 tools ships 4 full schemas + 46 one-line stubs instead of 50 full schemas — in fixtures with realistic MCP schemas the tool-block token estimate drops by more than 70%.
How it works
On each turn, when tool_search=True and the estimated token weight
of the full definition block exceeds tool_search_threshold_tokens,
the ReAct loop rewrites the block:
- Stubs. Every tool not in the keep set is reduced to its
name, the first sentence of its description plus a hydration hint,
and a minimal permissive schema
(
{"type": "object", "additionalProperties": true}). search_tools. A local catalogue-search tool is auto-injected (always with its full definition). It runs entirely in-process — no model call, no network — ranking tools by substring + keyword score and returning{name, description}matches.- Hydration on use. The moment the model calls a tool — via
direct call or after finding it through
search_tools— that tool’s full definition ships on every subsequent turn of the session. Schemas are earned, then kept.
turn 1: [search_tools (full)] [read (full, kept)] [46 stubs...]
model: search_tools("database query")
turn 2: model: query_db(sql="SELECT ...") ← stub is callable
turn 3: [search_tools] [read] [query_db (full, hydrated)] [45 stubs...]Two properties make stubbing safe:
- Stubs stay callable. The permissive schema is valid JSON Schema, so any provider accepts a direct call to a stubbed tool. Correctness is preserved because argument validation and coercion always run server-side against the real tool on dispatch — the schema the model saw never weakens what the tool enforces.
- Safety metadata is preserved.
destructiveandserverpass through stubbing untouched, so the approval-gate backstop and MCP attribution behave identically for stubbed and full tools.
The knobs
All three live on Tuning:
| Field | Default | Effect |
|---|---|---|
tool_search | False | Master switch. False = byte-identical to pre-feature behaviour. |
tool_search_threshold_tokens | 10_000 | Stubbing only kicks in when the estimated token weight (chars/4 over name + description + schema JSON) of the full block exceeds this. |
keep_tools | () | Tool names that always ship full definitions — your agent’s always-on core set (read, search, …). search_tools itself is always kept. |
Use keep_tools for the tools that appear in nearly every run: they
skip the search round-trip entirely and the model sees their full
parameter docs from turn one.
tuning = Tuning(
tool_search=True,
keep_tools=["read", "edit", "bash"], # hot path, never stubbed
)Dynamic catalogues
The search_tools tool re-lists the live host on every search, so
hosts whose catalogue changes mid-run stay searchable — an MCP server
that sends listChanged and grows new tools is picked up on the next
search without any re-wiring. See
MCP resources, prompts + sampling
for how the registry refreshes.
Hydration state is session-scoped: it lives on the session’s metadata, so stop-hook re-iterations of the same run keep their earned schemas, while a fresh run starts clean — no leakage across runs or users.
When not to use it
- Small tool sets. Under the threshold nothing changes anyway; leave the flag off and save the mental overhead.
- Single-shot structured extraction. If the run is one turn with
one known tool, put that tool in
keep_tools— or skip tool search and pass only the tools you need. - Schema-critical first calls. If a tool has a subtle schema and
the model must get the first call right without a discovery turn,
keep it in
keep_tools.
Relationship to code mode. Tool search shrinks the definition
block but the model still emits one tool_use per call, and every
tool result lands in context. Code mode
attacks the other half of the problem: intermediate results stay
out of context because the model calls tools from Python code. The
two are complementary.