ToolSearch
Overview
The toolsearch middleware implements dynamic tool selection. When the tool library is large, passing all tools to the model would overflow the context. This middleware’s approach is:
- Add a
tool_searchmeta-tool that accepts keyword queries or direct selection to search for tools - Initially hide all dynamic tools
- After the model calls
tool_search, matched tools become available in subsequent calls
It supports three operating modes (two configuration values, but UseModelToolSearch=true has two 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 does not have autonomous DeferredTools retrieval capability. Instead, it calls thetool_searchtool (registered viamodel.WithToolSearchTool), the client-sidemodelToolSearchToolexecutes the search and returns a structuredToolSearchResult(containing full ToolInfo of matched tools), and the model selects tools accordingly
💡 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 matches │
│ from tool_search returns, add matched │
│ 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
// (because the tool list changes between calls).
UseModelToolSearch bool
}
Constructors
// Standard constructor, uses *schema.Message
func New(ctx context.Context, config *Config) (adk.ChatModelAgentMiddleware, error)
// Generic constructor, supports *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 are using 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:direct selection, +keywordmandatory match |
max_results | integer | No | Maximum number of results to return (default: 5). Only applies to keyword search mode; direct selection mode is not limited by this |
Query Modes:
| Mode | Syntax | Description |
| Keyword search | "weather forecast" | Matches keywords in tool names and descriptions, sorted by relevance score. Supports camelCase and _/ __(MCP) separator splitting |
| Direct selection | "select:tool_a,tool_b" | Selects one or more tools by exact name, comma-separated. Not limited by max_results |
| Mandatory match | "+slack send message" | Keywords prefixed with +are mandatory match items; tools not containing that keyword are filtered out. Remaining keywords are used for sorting |
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 native model processing.
Keyword Search Scoring Mechanism
Keyword search uses a multi-layer scoring system, calculating the highest score for each keyword separately then summing:
| Match Rule | Score |
| Tool name split part exactly matches keyword | 10 |
| Tool name split part contains keyword (substring) | 5 |
| Full tool name contains keyword | 3 |
| Tool description contains keyword | 2 |
💡 Each keyword takes the highest score (intMax) for each rule and does not accumulate scores from multiple parts within the same tool. Scores from multiple keywords are summed for the total. Tools with equal scores are sorted lexicographically by name.
Tool names are split into parts by _ (underscore), __ (MCP server-tool separator), and camelCase boundaries for matching. 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 manages 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, // Model must support native tool search
Handlers: []adk.ChatModelAgentMiddleware{middleware},
})
The configuration is identical, but 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: model initiates
tool_searchcall → client-sidemodelToolSearchToolexecutes search → returns structuredToolSearchResult(with full ToolInfo) → model selects tools accordingly
How It Works
BeforeAgent
- Get all DynamicTool ToolInfos, validate no duplicate tool names
- Create the corresponding type of
tool_searchtool based onUseModelToolSearch - Add
tool_searchand all DynamicTools torunCtx.Tools(at this point the Agent has the full tool set) - In model native mode, set
runCtx.ToolSearchTool; the framework passes it to the model viamodel.WithToolSearchTool
BeforeModelRewriteState (before each Model call)
Common logic:
- Ensure the message list contains an
<available-deferred-tools>reminder (inserted as a User message, listing all searchable tool names)
First call — initialization (both modes):
Default modeRemove all DynamicTools from state.ToolInfos, so the model initially sees only static tools and tool_search |
Model native mode1. Extract DynamicTools from state.ToolInfosinto state.DeferredToolInfos2. Remove tool_searchfrom state.ToolInfos(handled natively by the model) |
Subsequent calls — forward selection (default mode only):
- Iterate through message history, find all JSON
matchesfields fromtool_searchreturn results - Collect selected tool names
- Add matched DynamicTools back to
state.ToolInfos(cumulative; previously 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
DynamicToolscannot 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 permanently (accumulated based on
tool_searchresults in message history) tool_searchcan be called multiple times; results accumulate- In default mode, the tool list may change before each Model call, which may invalidate the model’s KV-cache
- 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