perf(tools): shrink lazy tool catalog overhead

This commit is contained in:
Carbon 2026-07-29 12:25:18 +03:00 committed by kshitij
parent c2c95f5339
commit 48e12a06fa
4 changed files with 96 additions and 10 deletions

View File

@ -2370,7 +2370,7 @@ DEFAULT_CONFIG = {
"listing": "auto",
# Absolute cap on the embedded listing in tokens (chars/4
# estimate), regardless of context size. Range 200..60000.
"listing_max_tokens": 20000,
"listing_max_tokens": 4000,
},
},

View File

@ -231,6 +231,32 @@ class TestBridgeDispatch:
result = dispatch_tool_search({}, current_tool_defs=[])
assert "error" in json.loads(result)
def test_empty_search_keeps_connected_sources_discoverable(self):
from tools.registry import registry
from tools.tool_search import dispatch_tool_search
name = "recovery_catalog_create_record"
tool_def = _td(name, "Create a record in the connected catalog service.")
registry.register(
name=name,
handler=lambda args, **kwargs: "{}",
schema=tool_def,
toolset="mcp-recovery-catalog",
)
result = json.loads(dispatch_tool_search(
{"query": "unrelated vocabulary"},
current_tool_defs=[tool_def],
))
assert result["matches"] == []
assert result["total_available"] == 1
assert result["available_sources"] == [
{"name": "recovery-catalog", "tool_count": 1},
]
assert "remain available" in result["hint"]
assert "before concluding" in result["hint"]
def test_resolve_underlying_call_parses_object_args(self):
from tools.tool_search import resolve_underlying_call
@ -403,11 +429,43 @@ class TestCatalogListing:
from tools.tool_search import ToolSearchConfig
cfg = ToolSearchConfig.from_raw(None)
assert cfg.listing == "auto"
assert cfg.listing_max_tokens == 20000
assert cfg.listing_max_tokens == 4000
# legacy bool shapes keep defaults too
assert ToolSearchConfig.from_raw(True).listing == "auto"
def test_default_listing_cap_bounds_fixed_catalog_overhead(self):
"""The default manifest must not grow back to the old 20K-token cap."""
from tools.registry import registry
from tools.tool_search import (
ToolSearchConfig,
assemble_tool_defs,
estimate_tokens_from_schemas,
)
defs = []
for i in range(500):
name = f"lean_catalog_tool_{i:04d}"
registry.register(
name=name,
handler=lambda args, **kwargs: "{}",
schema=_td(name, "Perform a deliberately verbose connected service action."),
toolset="mcp-lean-catalog",
)
defs.append(_td(name, "Perform a deliberately verbose connected service action."))
cfg = ToolSearchConfig.from_raw(None)
result = assemble_tool_defs(defs, context_length=1_000_000, config=cfg)
search = next(
td for td in result.tool_defs
if td["function"]["name"] == "tool_search"
)
description_tokens = estimate_tokens_from_schemas([search])
# Includes the bridge schema around the listing, so allow modest
# framing overhead above the 4K listing budget.
assert description_tokens < 4500
assert result.listing_form in {"names", "groups", "mixed"}
def test_short_desc_first_sentence_and_clip(self):
from tools.tool_search import _short_desc
assert _short_desc("Open an issue. Second sentence dropped.") == "Open an issue."

View File

@ -95,7 +95,7 @@ class ToolSearchConfig:
listing: str = "auto" # "auto" | "on" | "off"
# Absolute cap on the embedded listing, regardless of context size.
# Effective budget = min(listing_max_tokens, threshold_pct% of context).
listing_max_tokens: int = 20000
listing_max_tokens: int = 4000
@classmethod
def from_raw(cls, raw: Any) -> "ToolSearchConfig":
@ -143,7 +143,7 @@ class ToolSearchConfig:
listing = listing_raw
else:
listing = "auto"
listing_max_tokens = max(200, min(60000, _safe_int(raw.get("listing_max_tokens"), 20000)))
listing_max_tokens = max(200, min(60000, _safe_int(raw.get("listing_max_tokens"), 4000)))
return cls(
enabled=enabled,
@ -512,7 +512,7 @@ def _listing_group_label(source_name: str) -> str:
def build_catalog_listing(
deferrable: List[Dict[str, Any]],
*,
max_tokens: int = 20000,
max_tokens: int = 4000,
) -> Optional[str]:
"""Render a skills-style manifest of the deferred catalog.
@ -545,7 +545,7 @@ def build_catalog_listing(
def build_catalog_listing_with_form(
deferrable: List[Dict[str, Any]],
*,
max_tokens: int = 20000,
max_tokens: int = 4000,
) -> Tuple[Optional[str], str]:
"""Like :func:`build_catalog_listing` but also reports the form used.
@ -861,6 +861,25 @@ def _format_search_hit(entry: CatalogEntry) -> Dict[str, Any]:
}
def _available_source_summary(catalog: List[CatalogEntry]) -> List[Dict[str, Any]]:
"""Return a compact, deterministic summary of connected deferred sources.
Included only when search returns no matches. This gives the model enough
evidence to retry with a source/action query instead of treating a lexical
miss as proof that the capability is unavailable, without adding anything
to the fixed per-turn prompt.
"""
counts: Dict[str, int] = {}
for entry in catalog:
source_name = entry.source_name or entry.source or "other"
label = _listing_group_label(source_name)
counts[label] = counts.get(label, 0) + 1
return [
{"name": name, "tool_count": counts[name]}
for name in sorted(counts)
]
def dispatch_tool_search(args: Dict[str, Any],
*,
current_tool_defs: List[Dict[str, Any]],
@ -881,11 +900,20 @@ def dispatch_tool_search(args: Dict[str, Any],
_, deferrable = classify_tools(current_tool_defs)
catalog = build_catalog(deferrable)
hits = search_catalog(catalog, query, limit=limit)
return json.dumps({
result: Dict[str, Any] = {
"query": query,
"total_available": len(catalog),
"matches": [_format_search_hit(h) for h in hits],
}, ensure_ascii=False)
}
if not hits and catalog:
result["available_sources"] = _available_source_summary(catalog)
result["hint"] = (
"No lexical match was found, but the sources above are connected "
"and their tools remain available. Retry tool_search with the "
"service name plus a concrete action or object before concluding "
"the capability is unavailable."
)
return json.dumps(result, ensure_ascii=False)
def dispatch_tool_describe(args: Dict[str, Any],

View File

@ -80,7 +80,7 @@ tools:
search_default_limit: 5
max_search_limit: 20
listing: auto # embed a grouped name+description catalog manifest
listing_max_tokens: 20000
listing_max_tokens: 4000
```
| Key | Default | Meaning |
@ -90,7 +90,7 @@ tools:
| `search_default_limit` | `5` | Hits returned when the model calls `tool_search` without a `limit`. |
| `max_search_limit` | `20` | Hard upper bound the model can request via `limit`. Range 1–50. |
| `listing` | `auto` | Embed a skills-style manifest of every deferred tool (name + first sentence of its description, ≤60 chars, grouped by MCP server) in the `tool_search` bridge description. `auto` includes it when it fits the budget (falling back to names-only, then to the tier-2 server summary); `on`/`off` force either way. |
| `listing_max_tokens` | `20000` | Absolute cap on the embedded listing, regardless of context size. Range 200–60000. |
| `listing_max_tokens` | `4000` | Absolute cap on the embedded listing, regardless of context size. Range 200–60000. Large catalogs degrade to names-only or per-server summaries, keeping full schemas available through search. |
### Why the listing exists