feat(tool_search): config-gated builtin tool deferral (#6839) - #43521
Open
tgmerritt wants to merge 1 commit into
Open
feat(tool_search): config-gated builtin tool deferral (#6839)#43521tgmerritt wants to merge 1 commit into
tgmerritt wants to merge 1 commit into
Conversation
Extends the in-tree Tool Search progressive-disclosure layer to builtin tools as a strict opt-in, addressing the builtin-toolset half of NousResearch#6839 (most measurements there are installs with few/no MCP servers, where Hermes' own ~50 builtin schemas dominate per-turn overhead). Two new keys under tools.tool_search: - include_builtin (default false): builtin tools outside always_include join the same catalog, threshold gate, BM25 retrieval, and bridge dispatch as MCP/plugin tools. Off = byte-for-byte identical classification to today. - always_include: names that never defer. Defaults to a lean hot set; a user list replaces the hot set but is always unioned with an un-removable agent-loop floor (todo, memory, session_search, delegate_task, clarify). Also pins MCP/plugin names; exclusion-only, never adds tools outside the session's toolset scope. The "core tools never defer" invariant becomes "never defer by default" — its regression tests pass unmodified. _classify_source now reports a "builtin" source kind for catalog entries. Addresses the builtin-toolset portion of NousResearch#6839; complementary to the MCP-only NousResearch#33052. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 tasks
6 tasks
13 tasks
teknium1
reviewed
Jul 14, 2026
teknium1
left a comment
Contributor
There was a problem hiding this comment.
Thanks for extending the existing Tool Search bridge rather than adding another discovery path. The builtin-schema premise is real on current main: tools/tool_search.py:163-184 still keeps every core tool direct.
Problems
- The resolved deferral policy is not session-stable. Assembly uses an explicit config (
tools/tool_search.py:632), buttool_describereloads it (tools/tool_search.py:715) andtool_callreachesis_deferrable_tool_name()without passing one (tools/tool_search.py:791). After a config edit, an existing agent can retain bridge-only schemas fromagent/agent_init.py:1189-1198while refusing to describe/call the tool it deferred. Tool configuration must remain stable for the session to preserve cache behavior. - A custom
always_includecan defer skills, kanban, or computer use, but their system-prompt guidance is gated on model-visiblevalid_tool_namesinagent/system_prompt.py:221-249. Preserve a pre-assembly available-name set for guidance and add an initialized-agent regression test.
Suggested changes
- Snapshot and thread one Tool Search config through assembly and all bridge dispatch/scope paths.
- Add available-vs-visible tool-name handling plus lifecycle and prompt-guidance coverage.
Automated hermes-sweeper review.
| current_tool_defs: List[Dict[str, Any]], | ||
| config: Optional[ToolSearchConfig] = None) -> str: | ||
| """Execute the ``tool_describe`` bridge tool. Returns a JSON string.""" | ||
| if config is None: |
Contributor
There was a problem hiding this comment.
This re-loads mutable user config after assembly. An existing agent can still expose the bridge schemas assembled with include_builtin: true, then reject a deferred builtin after config changes because resolve_underlying_call() also resolves the new config. Snapshot the resolved policy with the agent/tool definition snapshot and thread it through describe/call/scope checks so a session's tool surface remains cache-stable.
This was referenced Jul 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Implements the builtin-toolset half of #6839 (lazy tool schema loading) by extending the existing in-tree Tool Search progressive-disclosure layer — strictly opt-in, off by default.
Today
tools/tool_search.pycan defer MCP and non-core plugin tools behind thetool_search/tool_describe/tool_callbridge, but builtin tools are categorically excluded ("Core tools are never deferred. No exceptions."). That covers MCP-heavy installs — yet most of the hard measurements in #6839 are about installs with few or no MCP servers, where Hermes' own builtin schemas are the per-turn overhead:hermes-clitoolset: a trivial no-tool turn on a local model takes 558s vs 31s with a 2-tool toolset — the schema preamble dominates time-to-first-token on self-hosted backends with no provider prompt cachingThis PR adds two config keys under the existing
tools.tool_searchblock:include_builtin(defaultfalse): builtin tools outsidealways_includebecome deferrable, joining the same catalog, threshold gate, BM25 retrieval, and bridge dispatch as MCP/plugin tools. With the flag off, classification behavior is byte-for-byte identical to today.always_include: names that never defer. Defaults to a lean hot set (terminal/process, file tools, web tools,execute_code, skill tools). A user-provided list replaces the default hot set but is always unioned with a hard floor —todo,memory,session_search,delegate_task,clarify— because those are serviced by the agent loop itself (model_tools._AGENT_LOOP_TOOLS) and deferring them would break the loop. The pin also works for MCP/plugin names (keep one hot MCP tool while the rest defer); pinning is exclusion-only and can never add tools outside the session's toolset scope.Design decisions worth flagging for review:
scoped_deferrable_names) already prevents the bridge from widening a restricted session's tool surface. The diff is small because everything funnels throughis_deferrable_tool_name().include_builtinAND tool-search enabled/threshold) preserve that intent while unlocking the Feature: Lazy Tool Schema Loading — Two-Pass Tool Injection to Reduce Token Overhead #6839 use case. The existing regression tests for the invariant still pass unmodified.always_includeextends — never replaces — the agent-loop floor. A config typo can make the hot set smaller than intended, but it cannot produce an agent that can't todo/remember/delegate/clarify.automode nothing activates until the (now larger) deferrable surface crossesthreshold_pctof the model's context window, so small setups stay pass-through even withinclude_builtin: true.Complementary to #33052 (MCP-side stub/promotion plugin) — different tool population, composable config surfaces. If maintainers prefer consolidating the #6839/#13332 design space first, happy to adjust scope; this was announced in #6839 before implementation.
Related Issue
Addresses the builtin-toolset portion of #6839
Type of Change
Changes Made
tools/tool_search.py—ToolSearchConfiggainsinclude_builtin+always_include(withALWAYS_INCLUDE_FLOOR/DEFAULT_ALWAYS_INCLUDE);is_deferrable_tool_name()/classify_tools()/scoped_deferrable_names()/dispatch_tool_describe()accept an optional config (default = user config, resolved once per assembly);_classify_source()reports abuiltinsource kind for catalog hitsmodel_tools.py— assembly comment + activation message updated (no logic change)hermes_cli/config.py—DEFAULT_CONFIGdocuments the two new keystests/tools/test_tool_search.py— 18 new tests (config parsing, floor enforcement, classification, assembly, describe gating, scoping), all with explicit configs so results never depend on the developer's~/.hermes/config.yamlwebsite/docs/user-guide/features/tool-search.md— new "Deferring builtin tools (opt-in)" section + config table rowsHow to Test
scripts/run_tests.sh tests/tools/ tests/test_model_tools.py tests/hermes_cli/—tests/tools/andtests/test_model_tools.pyfully green, including the pre-existingtest_core_tools_never_deferinvariant test unmodified. (5tests/hermes_cli/failures on my machine reproduce identically on cleanmain— they live-querylocalhost:11434, where I run a real Ollama, and collide with aresearchershim in my PATH; unrelated to this diff.)include_builtin: false),classify_tools()output is identical to main for any input — builtin tools never enter the deferred catalogtools.tool_search: {enabled: on, include_builtin: true}, start a CLI session, and observe the activation line — browser/cron/kanban schemas leave the tools array whileterminal, file tools, and web tools stay direct;tool_search("click a button")→tool_describe("browser_click")→tool_call(...)round-trips through the standard bridge dispatch with hooks/guardrails firing against the real tool nameChecklist
Code
fix(scope):,feat(scope):, etc.)scripts/run_tests.sh(see How to Test)Documentation & Housekeeping
docs/, docstrings) —website/docs/user-guide/features/tool-search.mdcli-config.yaml.exampleif I added/changed config keys — N/A (the example file doesn't carry thetools.tool_searchblock; keys documented inDEFAULT_CONFIGand the docs page)CONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — N/AI had Claude (Fable 5) do this work — same arrangement as my previous PRs (#43045, #43058): testing how far the model gets on an unfamiliar codebase with minimal direction from me, and using my Claude-time to give something back to the project. The scoping decision (extend in-tree Tool Search rather than add a parallel lazy-loading mechanism), the implementation, and the tests are its work; I reviewed and take responsibility for the submission.
🤖 Generated with Claude Code