How AI Agents Keep 400 Tools From Eating the Context Window
When Tools Fill the Context Window? A Deep Dive into the Implementation of On-Demand Loading for AI Agent Tool Search
This article explains the tool search mechanism in AI Agents—how to load the tools a model needs on demand when the number of tools is so large that it fills up the context window. The content covers implementation principles, matching algorithms, caching effects, and is explained with source code from x-code-cli.
Tools and MCP Tools
LLMs Cannot Execute Operations on Their Own
Large Language Models (LLMs) are pure text generators. They can only receive text input and output text responses; they cannot actively execute external operations.
The reason an AI Agent can do these things is that the model can perform these operations through tool calls. For example, the model outputs an instruction—"I want to call the readFile tool, the parameter is package.json"—the client (here referring to the MCP client, i.e., an AI Agent application that implements the MCP protocol, such as x-code-cli, Claude Code, Cursor, etc.) code will parse this instruction, execute the actual file reading operation, and then pass the read content back to the model. After receiving the file content, the model continues reasoning and decides what to do next.
These operations that can be called by the model—reading files, executing commands, searching code, querying databases, searching the web—are collectively referred to as Tools. Each tool consists of three parts: a name (name), a natural language description (description, telling the model what this tool can do), and a JSON Schema (inputSchema, defining what parameters the tool accepts). Take x-code-cli's readFile tool as an example:
{
"name": "readFile",
"description": "Read the contents of a file at the specified path.",
"inputSchema": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "The absolute path of the file to read"
},
"offset": {
"type": "number",
"description": "Line number to start reading from (1-based)"
},
"limit": {
"type": "number",
"description": "Maximum number of lines to read"
}
},
"required": ["path"]
}
}
These three parts are serialized and placed into the tools array of each API request. The model determines when to use this tool based on the description, and generates valid parameters based on the inputSchema. The more complex a tool's definition (more parameters, longer description), the more tokens it consumes.
Below is the execution flow of the Agent Loop in x-code-cli:
sequenceDiagram
autonumber
actor U as User
participant C as Client (x-code-cli)
participant LLM as LLM API
U->>C: "Help me rename the foo function in utils.ts to bar"
C->>LLM: User message + Tool list (readFile, edit, shell, grep...)
LLM-->>C: tool_call: readFile({path: "src/utils.ts"})
Note over C: Client executes readFile, reads file content
C->>LLM: tool_result: "export function foo() { ... }"
LLM-->>C: tool_call: edit({path: "src/utils.ts", old: "foo", new: "bar"})
Note over C: Client executes edit, replaces string
C->>LLM: tool_result: "File modified"
LLM-->>C: Text reply: "Renamed foo to bar"
C->>U: Display reply
Throughout the process, the model autonomously decides what tool to call and what parameters to pass. The client is only responsible for execution and passing results. This loop may execute once or many times, mainly depending on the complexity of the task.
The Growth in the Number of Tools
Built-in Tools
When an Agent product is first launched, the number of built-in tools may not be large. However, as the product iterates, built-in tools gradually increase. For example, tools for web search, web scraping, sub-agent delegation, to-do management, background task management, etc. Mainstream AI Agent products like Claude Code, Codex, and Cursor have all gone through this growth process. It is normal for a mature AI Agent product to have dozens of built-in tools.
MCP Tools
MCP (Model Context Protocol) is an open protocol that allows Agents to access external services. For example, by connecting to a GitHub MCP server, an Agent can create issues and merge PRs; by connecting to a database MCP server, an Agent can execute SQL queries.
Each MCP Server registers all its publicly available tools with the client through the tools/list interface; this interface uses a full-return mode. After establishing a connection with a server, the client can obtain the complete list of tool definitions from that server at once.
sequenceDiagram
participant C as MCP Client (AI Agent)
participant S1 as MCP Server A (Exchange)
participant S2 as MCP Server B (Database)
C->>S1: Establish connection
C->>S1: tools/list
S1-->>C: Returns 400 tool definitions
C->>S2: Establish connection
C->>S2: tools/list
S2-->>C: Returns 15 tool definitions
Note over C: Client gets the complete tool list of all servers<br/>Total 415 tools, all placed into subsequent LLM requests
Structurally, MCP tools and built-in tools are no different; both are name + description + JSON Schema. The only difference is who provides them: built-in tools are hardcoded in the product code, while MCP tools are dynamically registered by external servers.
The number of tools provided by a single MCP server often far exceeds that of built-in tools. Take an MCP server for a cryptocurrency exchange as an example; it covers spot trading, contract trading, asset transfers, market queries, balance queries, and other operations, totaling 400 tools. If the user also connects some other MCP servers, the total number of tools can easily exceed 400.
Problems Caused by an Increasing Number of Tools
If the large model needs to call these tools, all tool definitions must be sent along with the request to the model. Since the large language model itself is stateless, the complete tool list must be carried anew each time an inference request is initiated.
A moderately complex tool definition is about 150–300 tokens (the name is a few tokens, the description is one or two lines, the property names and type descriptions in the JSON Schema are the bulk). 400 tools × an average of 160 tokens ≈ 64,000 tokens. Before the user even starts asking questions, the tool definitions alone occupy 64k tokens.
Let's look at the request structure when calling the LLM in x-code-cli. Below is the core code (simplified) of the runTurn function in packages/core/src/agent/loop.ts:
result = streamText({
model, // LLM model instance
system: cached.system, // System Prompt, about 2-5k tokens
messages: cached.messages, // Messages (conversation history + user message)
tools: cached.tools, // Tools (tool definitions) ← 64k tokens when fully injected
maxRetries: 3,
abortSignal: options.abortSignal,
maxOutputTokens: getMaxOutputTokens(options.modelId),
providerOptions: mergedProviderOptions,
})
The input for a single API request consists of three fields: system, tools, and messages. The token distribution is roughly as follows:
Token Composition of a Single API Request:
┌────────────────────────────────────┐
│ system (System Prompt) │ About 2-5k tokens
├────────────────────────────────────┤
│ tools (Tool Definitions) │ ← When fully injected: 64k tokens (400 tools)
├────────────────────────────────────┤
│ messages (Conversation History + User Message) │ Varies with conversation length
└────────────────────────────────────┘
If the model's context window is 128k, the tool list occupies nearly half.
One point needs clarification regarding costs: mainstream LLM APIs have prefix caching. For the system + tools prefix parts that remain unchanged each round, the first request is billed at full price, and subsequent requests that hit the cache are billed at a discounted price (usually 10%–50% of the original price), so you don't pay full price every round.
But even if a 64k token tool list hits the cache, it still incurs ongoing costs: it occupies physical space in the context window, increases Time to First Token (TTFT), and squeezes the space left for conversation history and user messages. The larger the tool list, the smaller the window available for actual conversation.
This leads to the topic of this article—tool search.
Tool Search
Tool search is a built-in tool for "loading tool definitions on demand." By default, the complete definitions (name + description + JSON Schema) of all tools are placed into the tools array of each request. Tool search changes this strategy: only a list of tool names is placed in the system prompt, without the full definitions; when the model determines it needs to call a tool, it first loads the tool's complete schema via the built-in toolSearch tool, and then executes the call in the next round.
The model still knows what tools are available—the system prompt lists all tool names. But the complete definition (description + JSON Schema) of each tool is not pre-loaded into the request; instead, it is loaded only when the model actually wants to call it. This reduces the context usage from a full 64k to just 3-5k for the tool name list, plus the schemas of the few tools loaded on demand.
The Deferred Mechanism
"Deferred" literally means "delayed." In the context of tool search, a deferred tool is a tool whose loading is delayed—its complete definition (description + JSON Schema) is not sent with the request; it is only loaded when the model actively requests it via toolSearch. The counterpart is directly loaded tools, which are core tools whose complete schema is carried in every request round.
Let's look at how on-demand loading is specifically implemented, mainly involving the following steps:
Step 1: The client gets all tools from the MCP server as usual. The tools/list interface still returns the full set. When the client starts, it pulls the complete definitions of all tools from each MCP server—name, description, JSON Schema—and stores them all in local memory. This step is exactly the same as without tool search.
Step 2: Only tell the model the tool names. The complete tool schemas are not placed into the request's tools array. Tool names are grouped by server and written in the ## Deferred Tools section of the system prompt. The model can see this name list every round, knowing what tools are available, but cannot see the parameters each tool accepts.
Below is the actual ## Deferred Tools section generated by x-code-cli (excerpt):
## Deferred Tools
The tools below are available but NOT loaded — only their names are listed,
with no schema. To use one, first call `toolSearch` (keyword search, or
`select:<exact_name>` to load specific tools by name); its schema is then
added to your tool set and it becomes directly callable on your next step.
Core tools (readFile, writeFile, edit, shell, grep, glob, listDir, task)
are always loaded — never search for those.
### Built-in
- webSearch, webFetch, todoWrite, activateSkill
### Server: gate-mcp
- mcp__gate__cex_spot_get_ticker, mcp__gate__cex_spot_create_order,
mcp__gate__cex_spot_get_balance, mcp__gate__cex_futures_get_ticker,
... (400 tools in total)
### Server: github
- mcp__github__create_issue, mcp__github__list_pull_requests,
mcp__github__merge_pull_request, ...
As can be seen, this text contains two parts of information: first, usage instructions (telling the model how to load tools via toolSearch), and second, a list of tool names grouped by source. The model can see this list every round but cannot see any tool's parameter definitions.
Step 3: Give the model a built-in toolSearch tool. When the model needs to call a deferred tool, it first calls toolSearch, passing in keywords or a tool name. The client performs a match on the in-memory tool catalog and adds the matched tools to the tools array for the next request round. The model can then call it in the next round just like any normal tool.
sequenceDiagram
participant S as MCP Server
participant C as Client (MCP Client)
participant LLM as LLM
Note over S,C: At startup
C->>S: tools/list
S-->>C: Complete definitions of 400 tools
Note over C: All stored in memory
Note over C,LLM: User asks: "Help me check the latest price of BTC/USDT"
C->>LLM: Core tool schemas + toolSearch schema + deferred name list
Note over LLM: Sees cex_spot_get_ticker in the list,<br/>but without its schema, cannot call it directly
LLM-->>C: toolSearch({query: "select:mcp__gate__cex_spot_get_ticker"})
Note over C: Matches in memory catalog, adds this tool to the activated set
C->>LLM: Core tools + toolSearch + cex_spot_get_ticker (just activated schema)
LLM-->>C: cex_spot_get_ticker({currency_pair: "BTC_USDT"})
A key point to emphasize: deferred is something the client does. The MCP protocol's tools/list still returns the full set; the protocol itself has no capability for on-demand discovery. The deferred mechanism is entirely the client, after obtaining the full tool list, deciding on its own which ones to put into the request and which to delay loading—this is a client-side strategy, not a protocol feature.
The Model Only Sees Names, How Does It Find the Right Tool?
If only the tool name is provided without the corresponding Schema, the model cannot determine the tool's input parameter structure. To solve this problem, the system has designed multiple layers of verification mechanisms.
Tool names themselves already possess sufficient semantic description capability. Names like mcp__github__create_issue, cex_spot_get_ticker allow the model to understand the function at a glance. MCP tool names generally follow the naming convention of server__action or server__resource__verb, making the name itself a layer of semantic index. If tool names were all tool_001, tool_002, this scheme would not work.
The name list is placed where the model can see it. The ## Deferred Tools section of the system prompt lists all deferred tool names grouped by server. The model is not searching blindly—it sees the complete list, just not the parameter definitions for each tool.
The client completes the matching computation for toolSearch. toolSearch supports two calling methods: keyword search and exact match.
Method 1: Keyword Search. Used when the model is unsure of the exact tool name. The client performs keyword matching between the query parameter and the preprocessed text of each tool, sorts by score, and returns the top few.
Let's illustrate with an example. Suppose the deferred catalog has these tools:
Tool Catalog (search text preprocessed for each tool upon registration):
Tool Name Search Text (name tokenization + description + schema property names)
───────────────────────────────── ────────────────────────────────────────
mcp__gate__cex_spot_get_ticker gate cex spot get ticker Get spot market data currency_pair
mcp__gate__cex_spot_create_order gate cex spot create order Create spot order currency_pair side amount price
mcp__gate__cex_spot_cancel_order gate cex spot cancel order Cancel spot order order_id
mcp__github__create_issue github create issue Create issue title body labels
After the model passes toolSearch({query: "spot ticker"}), the client scores each tool:
query = "spot ticker"
mcp__gate__cex_spot_get_ticker:
"spot" → name tokenization has "spot" → +10
"ticker" → name tokenization has "ticker" → +10
Total score = 20 ✓
mcp__gate__cex_spot_create_order:
"spot" → name tokenization has "spot" → +10
"ticker" → not in name tokenization, not in search text → +0
Total score = 10
mcp__gate__cex_spot_cancel_order:
"spot" → name tokenization has "spot" → +10
"ticker" → not in name tokenization, not in search text → +0
Total score = 10
mcp__github__create_issue:
"spot" → none → +0
"ticker" → none → +0
Total score = 0
Results (sorted by score descending):
1. mcp__gate__cex_spot_get_ticker (20) ← Returned
2. mcp__gate__cex_spot_create_order (10) ← Returned
3. mcp__gate__cex_spot_cancel_order (10) ← Returned
4. mcp__github__create_issue (0) ← Filtered out
Codex's search logic is similar, also pre-generating search text for each tool, but uses the BM25 algorithm for scoring (a classic ranking algorithm in the field of information retrieval, used by search engines to rank search results). The core idea is the same: match the query's keywords against the tool's search text and sort by relevance.
Neither method requires embeddings or calling a model; both are string matching—the same query, the same tool set, will yield exactly the same results.
Additionally, a keyword search may hit multiple tools simultaneously. toolSearch takes the top N by score (x-code-cli defaults to 5), and all matched tools are activated—added to the activated set, all appearing in the tools array in the next round. After the model gets the schemas of these tools, it decides on its own which one to use; the client does not restrict it.
For example, if the user says "Help me place a limit order to buy BTC," the model calls toolSearch({query: "spot order"}), which might simultaneously hit cex_spot_create_order, cex_spot_get_ticker, cex_spot_cancel_order, and other tools. The cost of activating a few more tools is a few more schemas' worth of tokens, but it's much better than fully injecting 400 tools. Moreover, activation is persistent—once activated, there's no need to search for the same tool again in subsequent rounds; it can be used directly.
Method 2: select: Exact Match. The model directly passes select:tool_name, and the client performs an exact lookup by name, no scoring needed.
How does the model know the exact tool name? Because the ## Deferred Tools section of the system prompt lists the complete names of all deferred tools. The model sees this list every round; as long as it finds the target tool name in the list, it can directly use select: to load it.
When to use which method? This logic is written in the description of the toolSearch tool itself, which the model reads before calling:
Pass `query` as either:
- keywords describing the capability you need
(e.g. "search the web", "github create issue")
→ returns the best-matching deferred tools, or
- "select:<name>,<name>" to load specific tools by their
EXACT name from the Deferred Tools list
(prefer this when you already know the name).
That is, the tool description directly tells the model: If you already know the name, prefer select:; when unsure of the name, use keywords to describe the capability. In practice, the model uses select: most of the time, as it can directly see the tool names from the list. Keyword search is a fallback path for when the model is uncertain about the tool name—for example, when the tool name is long or there are multiple similar tools, the model might use keywords like "spot order" to let the client filter for it.
Comparison of the two calling methods:
Keyword Search (used when unsure of the tool name):
toolSearch({query: "spot ticker"})
→ Client scores all tools by keyword, returns the top few by score
Exact Match (used when the tool name is known, recommended method):
toolSearch({query: "select:mcp__gate__cex_spot_get_ticker"})
→ Client looks up directly by name, returns if found, no scoring
Keyword search is a fallback mechanism. The model can usually find the target tool name directly from the list and use select: for an exact match. Only when the model is unsure which specific tool to use does it fall back to keyword search to let the client filter for it.
Which Tools Should Be Deferred, Which Should Be Directly Loaded
Not all tools need to be deferred; some core tools are directly loaded, while tools used only occasionally are deferred.
Directly Loaded Tools (complete schema carried in every request round):
These tools are the Agent's basic operational capabilities, potentially needed for any user question:
| Tool | Function | Why it must be directly loaded |
|---|---|---|
readFile |
Read file | Almost all coding tasks need to read files first |
edit |
Edit file | Core operation for writing code |
writeFile |
Write file | Create new files |
shell |
Execute command | Run tests, install dependencies, git operations |
grep |
Search code | Locate code positions |
glob |
Find files | Find file paths |
task |
Sub-agent | Delegate sub-tasks |
toolSearch |
Tool search | The sole entry point for loading deferred tools |
Deferred (lazy-loaded) Tools:
- Non-core built-in tools:
webSearch(web search),webFetch(fetch web pages),todoWrite(TODO list). These tools are not needed for every task, so they are loaded when needed. - All MCP tools: MCP tools are provided by external servers, targeting specific scenarios (trading, GitHub operations, database queries, etc.). There is no MCP tool that "every task will use."
Let's use a concrete example to illustrate the classification process. Suppose the Agent has registered the following tools:
Classification Example:
readFile → Core built-in tool, used for every task → Directly loaded
edit → Core built-in tool, used for every task → Directly loaded
shell → Core built-in tool, used for every task → Directly loaded
toolSearch → Loading entry point, must be directly available → Directly loaded
webSearch → Non-core built-in, used occasionally → Deferred
webFetch → Non-core built-in, used occasionally → Deferred
mcp__gate__cex_spot_get_ticker → MCP tool → Deferred
mcp__gate__cex_spot_create_order → MCP tool → Deferred
mcp__github__create_issue → MCP tool → Deferred
...(Remaining MCP tools all deferred)
Below is the flowchart of the judgment logic:
Complete Cycle: Search → Activate → Call
Let's look at a concrete example, using "check BTC price" to demonstrate the actual calling process:
sequenceDiagram
autonumber
actor U as User
participant C as Client
participant LLM as LLM
U->>C: "Help me check the spot price of BTC"
C->>LLM: User message + Core tools + toolSearch + deferred name list
Note over LLM: Sees cex_spot_get_ticker in the list
LLM-->>C: toolSearch({query: "select:mcp__gate__cex_spot_get_ticker"})
Note over C: Exact match hit, added to activated set
C-->>LLM: tool_result: "Loaded 1 tool(s) — now callable directly on your next step"
Note over C: Next round's tools array appends the complete schema of cex_spot_get_ticker
C->>LLM: Core tools + toolSearch + cex_spot_get_ticker (activated)
LLM-->>C: cex_spot_get_ticker({currency_pair: "BTC_USDT"})
Note over C: Execute MCP tool call, get price
C->>LLM: Tool execution result
LLM-->>C: "BTC spot price is 63,521.30 USDT"
C->>U: Display reply
Note between step 3 and step 5: The model called the toolSearch tool in the first round, but cannot call cex_spot_get_ticker in the same round—because this tool does not yet exist in the first round's tools array. It must wait for the client to add its schema, and the model can only call it in the next round.
This is the cost of the deferred scheme: the first use of any deferred tool requires at least 2 rounds of API calls—first search, then call in the next round.
State Changes Throughout the Process
From startup to multi-turn conversation, the change process of various state fields:
| Phase | State Changes |
|---|---|
| Startup | catalog = Complete definitions of 400 deferred tools obtained from all MCP servers |
activated = Empty set |
|
systemPromptCache = System prompt, containing the ## Deferred Tools name list |
|
baseTools = [readFile, edit, shell, grep, ..., toolSearch] |
|
| Round 1 Request | tools = composeTurnTools(baseTools, activated) = baseTools (activated is empty) |
| User: "Check BTC price" | Sent to LLM: system + tools (9 core tools) + messages |
| Round 1 Response | LLM returns: toolSearch({query: "select:mcp__gate__cex_spot_get_ticker"}) |
handleToolSearch() performs exact match in catalog |
|
activated = { "mcp__gate__cex_spot_get_ticker" } |
|
Returns tool_result to model: Loaded 1 tool(s) — now callable directly on your next step: |
|
- mcp__gate__cex_spot_get_ticker: Get spot market data |
|
| Round 2 Request | tools = [...baseTools, complete schema of cex_spot_get_ticker] (one more tool appended) |
messages appended with previous round's tool_call + tool_result |
|
Sent to LLM: system + tools (10 tools) + messages, prefix cache misses once |
|
| Round 2 Response | LLM returns: cex_spot_get_ticker({currency_pair: "BTC_USDT"}) |
Client executes MCP tool call, gets price. activated unchanged |
|
| Round 3 | tools same as Round 2 (activated unchanged, prefix stable, cache hit) |
| LLM returns text reply | |
| Subsequent Rounds | activated persists, no need to search for cex_spot_get_ticker again |
| If a new tool is activated → activated grows → tools appended → cache misses once more |
A few key points:
catalogandsystemPromptCacheare fixed at startup and remain unchanged for the entire sessionactivatedonly increases, never decreases- The
toolsarray is dynamically composed each round =baseTools+ schemas corresponding to activated - Prefix cache only misses in the round when
activatedgrows, then stabilizes
Engineering Details
The previous section covered the core process of tool search. This section mainly discusses a few problems encountered during implementation and their solutions.
Threshold Judgment: When to enable defer. When there are few tools, full injection is simpler and doesn't require an extra round trip. x-code-cli uses DEFERRAL_THRESHOLD_PERCENT for judgment: serialize the schemas of all candidate deferred tools to estimate the token count; if it does not exceed 10% of the model's context window, skip defer and fall back to full loading.
Model Compatibility. Tool search relies on the model actively calling toolSearch. If the model doesn't support or cooperate, deferred tools will never be loaded. Different products handle this differently:
- Codex's approach is the most precise: it configures a
supports_search_toolboolean field per specific model version inmodels.json. Only models explicitly marked as supported enable tool search; unknown models default to off. - Claude Code checks API capability rather than model intelligence: it uses
modelSupportsToolReference()to check if the model supports thetool_referenceprotocol block. The default exclusion list is only['haiku'], but it can be dynamically updated via remote configuration without a release. - x-code-cli currently uses coarse-grained string matching:
WEAK_MODEL_PATTERNS = ['haiku', 'nano', 'glm-4v']. If the model name contains these substrings, it falls back to full injection. The problem with this approach is that it cannot distinguish different versions of the same series—for example, Claude 3 Haiku and Claude 3.5 Haiku have a large capability gap, but both would be uniformly disabled.
Byte Stability of the System Prompt. The prefix caching of the LLM API relies on the request prefix remaining unchanged. If the prefix composed of system + tools remains consistent in subsequent requests, it can hit the cache and be billed at a discounted price. This requires that the deferred list in the system prompt cannot be dynamically modified during runtime. x-code-cli's approach is to generate systemPromptCache at startup and freeze it, not changing it for the entire session.
This brings a side effect: after a tool is activated, the list in the system prompt still says this tool is in a deferred state, but its complete schema is already in the tools array. There is an inconsistency between the system prompt the model sees and the actual tool list: the system prompt says the tool is not loaded, but the tool is actually already in the list.
In practice, this inconsistency rarely causes problems. Strong models (Sonnet, GPT-4o level) will remember that the tool has been loaded after receiving the first toolSearch tool_result, and will call it directly in subsequent steps without repeating the search. Repeated searches only occur in extreme cases—for example, when there are very many conversation turns, early tool_results have been compressed away, and the model sees the system prompt saying "not loaded" again, it might search once more. x-code-cli adds a layer of defense: if the searched tool is already in the activated set, it returns Already loaded — call xxx directly now., preventing the model from falling into a search loop. This is defensive code, not the regular path.
Claude Code and Codex avoid this inconsistency architecturally. Claude Code relies on Anthropic API's tool_reference mechanism, where activated tools have their schemas expanded by the server side, and the client does not modify the tools array. Codex relies on the tool_search_output history item of the OpenAI Responses API, similarly handled by the server side. Neither needs to maintain a potentially stale state in the system prompt. Because x-code-cli needs to work across multiple providers, it cannot rely on the server-side capabilities of a single API and can only go through client-side re-injection—but from actual results, this inconsistency almost never triggers repeated searches under strong models.
Sub-agents do not enable defer. The tool set of a sub-agent is controlled by a whitelist; most sub-agents only have a few built-in tools. For example, the explore sub-agent only has five tools: readFile, glob, grep, listDir, shell. The plan sub-agent has even fewer, only four read-only tools. With so few tools, adding tool search is meaningless. The only exception is the general-purpose sub-agent—it inherits the MCP tools of the parent session, and the number of tools might be large, but even so, the sub-agent currently uses full injection, because the sub-agent's lifecycle is short with few turns, and the savings from defer are not significant.
Summary
A review of the complete process of tool search: At startup, tool definitions are fully obtained from MCP servers and classified into directly loaded and deferred categories based on usage frequency. For directly loaded tools, the complete schema is placed into the tools array of each request round; for deferred tools, only the names are listed in the system prompt. When the model needs a deferred tool, it calls toolSearch to load its schema, and can call it normally in the next round.
Comparison of implementations across several mainstream AI Agent CLIs:
| Full Injection? | Dynamic Mechanism | Matching Algorithm | |
|---|---|---|---|
| Claude Code | No | ToolSearch tool |
Keyword scoring + select: |
| Codex | No | tool_search tool |
BM25 full-text search |
| Gemini CLI | Yes | None | — |
| x-code-cli | No | toolSearch tool |
Keyword scoring + select: |
Claude Code, Codex, and x-code-cli share almost the same approach, differing in how the activated schema is made available to the model—Claude Code and Codex rely on the server-side capabilities of their respective APIs, while x-code-cli relies on client-side re-injection (directly adding the schema to the tools array). Gemini CLI currently does not have built-in tool search.
My Other AI Agent Articles
- Let AI Agent Systems Discover Their Own Bugs and Submit Fix PRs: The Self-Evolving Harness
- An Introductory Guide for AI Agent Engineers
- How to Implement an AI Agent CLI from Scratch
- Juejin Booklet--Building an AI Agent CLI from Scratch