ToolSearch (Phase 79.2 — MVP)

ToolSearch is the discovery surface for deferred tools — tools whose full JSONSchema lives behind a single ToolSearch(...) lookup instead of inline in the system prompt. The savings are real once the tool surface gets wide (40+ tools after Phase 13 + 77 + 79); the MVP shipped here lays the foundation.

Lift from upstream agent CLI (input schema, select: prefix, keyword search with +token required prefix, scoring weights for name parts vs description vs searchHint).

How a tool becomes "deferred"

The runtime does not infer this from the tool itself. Callers opt in when registering:

#![allow(unused)]
fn main() {
use nexo_core::agent::tool_registry::{ToolMeta, ToolRegistry};

registry.register_with_meta(
    MyTool::tool_def(),
    MyTool,
    ToolMeta::deferred_with_hint("send a slack message"),
);
}

ToolMeta has two fields:

FieldDefaultEffect
deferred: boolfalseWhen true, surfaces only via ToolSearch
search_hint: Option<String>NoneCurated phrase used by keyword ranking — beats raw description scoring

Existing register(...) calls keep working unchanged — the side channel is opt-in.

MVP caveat

The four LLM provider clients (anthropic, minimax, gemini, openai_compat) still emit every registered tool's full schema in the request body. The actual token-cost savings land when a follow-up wires those four clients to consult ToolRegistry::deferred_tools() and filter accordingly. Tracked in FOLLOWUPS.md::Phase 79.2. Until then, ToolSearch is useful as a discovery API the model can use today, and the registration path is correct so the upgrade is a four-file change.

Query forms

Lifted verbatim from the upstream CLI:

select:Read,Edit,Grep      → fetch these exact tools by name (comma-separated)
notebook jupyter           → keyword search, up to max_results best matches
+slack send                → require "slack" in name/desc/hint, rank by remaining terms

Tokens prefixed with + are required: tools that don't match all required tokens are filtered out before scoring. Other tokens are optional but still contribute to the score.

Scoring (mirror of the upstream CLI)

Match siteNon-MCP scoreMCP score
Exact name part1012
Substring within name part56
search_hint contains term+4+4
description contains term+2+2

Matches with score == 0 are dropped. Results sorted by score desc then name asc; truncated to max_results (default 5, hard cap 25).

mcp__server__action-shaped names are tokenised by splitting on __ and _; CamelCase names split on case boundaries; dotted plugin names (whatsapp.send) split on .. So a query "slack" matches mcp__slack__send_message on the exact server-name part.

Response shape

{
  "query": "...",
  "query_kind": "select" | "keyword",
  "total_deferred_tools": 7,
  "matches": [
    {
      "name": "FileEdit",
      "score": 12,            // omitted in `select` responses
      "description": "...",
      "parameters": { ... }   // full JSONSchema
    }
  ],
  "missing": ["UnknownTool"]   // only present in `select` responses
}

The model can read the schema directly out of parameters and call the tool on the next turn. When the runtime starts filtering deferred tools out of the request body (follow-up), the matched tool will also be auto-injected into that turn's available tool list.

References

  • PRIMARY: upstream agent CLI, upstream agent CLI,62-108 (isDeferredTool).
  • SECONDARY: OpenClaw research/ — no equivalent. Single-process TS reference does not face the wide-surface MCP token cost that motivates this tool.
  • Plan + spec: proyecto/PHASES.md::79.2.