ToolSearch
Overview
The toolsearch middleware implements dynamic tool selection. When the tool library is large, passing all tools to the model would overwhelm the context. This middleware works by:
- Adding a
tool_searchmeta-tool that accepts keyword queries or direct selection to search for tools - Initially hiding all dynamic tools
- After the model calls
tool_search, matched tools become available in subsequent calls
Three operating modes are supported (two configuration values, but UseModelToolSearch=true has two distinct end-to-end behaviors):
- Default mode (
UseModelToolSearch=false): The middleware manages tool visibility itself. Before each model call, it filtersstate.ToolInfosviaBeforeModelRewriteStatebased ontool_searchcall results, progressively adding selected dynamic tools back to the model’s visible list - Model native mode — pure server-side retrieval (
UseModelToolSearch=true, model retrieves DeferredTools on its own): The middleware moves dynamic tools intostate.DeferredToolInfosand passes them to the model viamodel.WithDeferredTools. If the model natively supports server-side tool retrieval (e.g., Claude’s tool search), the model searches and selects directly from DeferredTools without calling the tool_search tool - Model native mode — client-side proxy retrieval (
UseModelToolSearch=true, model discovers tools by callingtool_search): Same middleware configuration as above, but the model lacks autonomous DeferredTools retrieval capability and instead calls thetool_searchtool (registered viamodel.WithToolSearchTool). The client-sidemodelToolSearchToolexecutes the search and returns a structuredToolSearchResult(containing matched tools’ full ToolInfo), from which the model selects tools
💡 Package path: github.com/cloudwego/eino/adk/middlewares/dynamictool/toolsearch
Architecture
Agent initialization
│
▼
┌───────────────────────────────────────────┐
│ BeforeAgent │
│ - Inject tool_search tool │
│ - Add DynamicTools to Tools list │
│ - In model native mode, set │
│ runCtx.ToolSearchTool │
└───────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────┐
│ BeforeModelRewriteState │
│ (executed before each Model call) │
│ │
│ 1. Insert <available-deferred-tools> │
│ User message listing all searchable │
│ tool names │
│ │
│ First call (initialization): │
│ Default mode: │
│ Remove DynamicTools from ToolInfos │
│ Model native mode: │
│ DynamicTools → DeferredToolInfos │
│ Remove DynamicTools and │
│ tool_search from ToolInfos │
│ │
│ Subsequent calls (default mode - │
│ forward selection): │
│ Scan message history, collect │
│ tool_search returned matches, │
│ add matching DynamicTools back to │
│ ToolInfos │
└────────────────────────────────────────────┘
│
▼
Model call
Configuration
type Config struct {
// Tools that can be dynamically searched and loaded
DynamicTools []tool.BaseTool
// Whether to use the model's native tool search capability
//
// When true, the middleware delegates tool search to
// the model's native capability.
//
// When false (default), the middleware manages tool visibility
// by filtering the tool list before each Model call based on
// tool_search results.
// Note: this approach may invalidate the model's KV-cache
// (since the tool list changes between calls).
UseModelToolSearch bool
}
Constructor
// Standard constructor, using *schema.Message
func New(ctx context.Context, config *Config) (adk.ChatModelAgentMiddleware, error)
// Generic constructor, supporting *schema.Message and *schema.AgenticMessage
func NewTyped[M adk.MessageType](ctx context.Context, config *Config) (adk.TypedChatModelAgentMiddleware[M], error)
New internally calls NewTyped[*schema.Message]. If you use TypedChatModelAgent (e.g., Agentic mode), use NewTyped directly.
tool_search Tool
The meta-tool injected by the middleware. Parameters:
| Parameter | Type | Required | Description |
query | string | Yes | Query string for finding tools. Supports three modes: keyword search, select:for direct selection, +keywordfor required matching |
max_results | integer | No | Maximum number of results to return (default: 5). Only applies to keyword search mode; direct selection mode is not subject to this limit |
Query modes:
| Mode | Syntax | Description |
| Keyword search | "weather forecast" | Matches keywords against tool names and descriptions, sorted by relevance score. Supports splitting on camelCase and _/ __(MCP) separators |
| Direct selection | "select:tool_a,tool_b" | Selects one or more tools by exact name, comma-separated. Not subject to max_results |
| Required match | "+slack send message" | Keywords with +prefix are required — tools without that keyword are filtered out. Remaining keywords are used for ranking |
Return value (default mode):
{"matches": ["tool_a", "tool_b"]}
Return value (model native mode): Returns a structured schema.ToolResult containing the full ToolInfo of matched tools for the model’s native processing.
Keyword Search Scoring Mechanism
Keyword search uses a multi-tier scoring system, calculating the highest score per keyword and then summing:
| Matching Rule | Score |
| Tool name part (after splitting) exactly matches keyword | 10 |
| Tool name part (after splitting) contains keyword (substring) | 5 |
| Full tool name contains keyword | 3 |
| Tool description contains keyword | 2 |
💡 Each keyword takes the highest score (intMax) per rule and does not stack match scores from multiple parts of the same tool. Scores from multiple keywords are summed for the total. Tools with the same score are sorted lexicographically by name.
Tool names are split into parts using _ (underscore), __ (MCP server-tool separator), and camelCase boundaries. For example, mcp__slack__send_message splits into ["mcp", "slack", "send", "message"], and NotebookEdit splits into ["Notebook", "Edit"]. Matching is case-insensitive.
Usage Examples
Default Mode (Middleware-managed Tool Visibility)
middleware, err := toolsearch.New(ctx, &toolsearch.Config{
DynamicTools: []tool.BaseTool{
weatherTool,
stockTool,
currencyTool,
// ... many tools
},
})
if err != nil {
return err
}
agent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
Model: myModel,
Handlers: []adk.ChatModelAgentMiddleware{middleware},
})
Model Native Mode
middleware, err := toolsearch.New(ctx, &toolsearch.Config{
DynamicTools: []tool.BaseTool{
weatherTool,
stockTool,
currencyTool,
},
UseModelToolSearch: true,
})
if err != nil {
return err
}
agent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
Model: myModel, // The model must support native tool search
Handlers: []adk.ChatModelAgentMiddleware{middleware},
})
The configuration is identical, but the end-to-end behavior depends on the model adapter implementation:
- If the model natively supports server-side retrieval (e.g., Claude): the model searches and selects tools directly from
DeferredToolInfos; thetool_searchtool is not called - If the model uses client-side proxy retrieval: the model calls
tool_search→ the client-sidemodelToolSearchToolperforms the search → returns a structuredToolSearchResult(with full ToolInfo) → the model selects tools accordingly
How It Works
BeforeAgent
- Gets ToolInfo for all DynamicTools and validates no duplicate tool names
- Creates the appropriate type of
tool_searchtool based onUseModelToolSearch - Adds
tool_searchand all DynamicTools torunCtx.Tools(at this point the Agent has the full set of tools) - In model native mode, sets
runCtx.ToolSearchToolwhich the framework passes to the model viamodel.WithToolSearchTool
BeforeModelRewriteState (before each Model call)
Common logic:
- Ensures an
<available-deferred-tools>reminder exists in the message list (inserted as a User message listing all searchable tool names)
First call — initialization (both modes):
Default mode: Removes all DynamicTools from state.ToolInfosso the model initially sees only static tools and tool_search |
Model native mode: 1. Extracts DynamicTools from state.ToolInfosinto state.DeferredToolInfos2. Removes tool_searchfrom state.ToolInfos(handled natively by the model) |
Subsequent calls — forward selection (default mode only):
- Traverses message history to find all JSON
matchesfields intool_searchreturn results - Collects selected tool names
- Adds matching DynamicTools back to
state.ToolInfos(cumulative; already-added tools are not removed)
Tool Selection Flow (Default Mode)
Round 1:
Model can only see tool_search + static tools
Model calls tool_search(query="weather forecast")
Returns {"matches": ["weather_forecast", "weather_history"]}
Round 2:
Model can see tool_search + static tools + weather_forecast + weather_history
Model calls weather_forecast(...)
Notes
DynamicToolsmust not be empty, and tool names must not be duplicated- Keyword search matches tool names and descriptions, case-insensitive
- In default mode, selected tools remain available (cumulative based on
tool_searchresults in message history) tool_searchcan be called multiple times; results are cumulative- In default mode, the tool list may change before each Model call, which can cause the model’s KV-cache to be invalidated
- Model native mode requires the ChatModel to support
model.WithToolSearchTooland/ormodel.WithDeferredToolsoptions. Which path is taken (pure server-side retrieval vs. client-side proxy retrieval) depends on the model adapter implementation - The
<available-deferred-tools>reminder is inserted as a User message (not a System message) into the message list, positioned before the first non-System message