Skip to content

Integrate upstream tiered tool disclosure (#67034) into Hopebox v0.19 - #4

Merged
latentoperator merged 12 commits into
hopebox/prod-v2026.7.20from
hopebox/pr67034-integration-20260727
Jul 27, 2026
Merged

Integrate upstream tiered tool disclosure (#67034) into Hopebox v0.19#4
latentoperator merged 12 commits into
hopebox/prod-v2026.7.20from
hopebox/pr67034-integration-20260727

Conversation

@latentoperator

Copy link
Copy Markdown
Owner

What changed

Replays all 11 contributor-authored commits from NousResearch#67034 onto the current Hopebox v0.19 production base.

The integration adds:

  • tiered, cache-stable catalog disclosure for deferred MCP/plugin tools
  • provider-safe property-key sanitization with dispatch-time reverse mapping
  • fnmatch glob support for MCP tool include/exclude filters
  • full pre-assembly tool visibility in CLI/TUI /tools
  • upstream benchmark fixtures, documentation, and regression coverage

Hopebox reconciliation

The PR applied cleanly except for one stale-base import conflict in tools/mcp_tool.py. The upstream PR parent already had an unrelated errno import that v0.19 does not carry; this branch retains only the PR-required fnmatch import.

No Hopebox behavior was removed. Core Kanban tools remain non-deferrable. All 13 live profile configs parse under the new configuration and retain their existing always-defer posture with a 20K catalog-listing budget at a 200K context window.

Validation

  • 534 focused tool-search/schema/MCP/plugin/Discord tests passed
  • 197 delegation tests passed
  • 13 CLI /tools and preloaded-skill tests passed
  • Ruff passed across all changed Python files
  • git diff --check and merge-marker inspection passed

Deployment status

Not deployed. The production branch and 26 running Hermes services remain on e9f8f8f15127.

The cutover is intentionally gated because hopewell-hermes-backup.service is failing on Dante's malformed messages_fts_trigram index. A consistent database copy was repaired successfully with the standard FTS5 rebuild command, but the live database has not been modified.

teknium1 and others added 12 commits July 27, 2026 09:47
… disclosure

Deferred MCP/plugin tools become invisible once the tool_search bridge
activates — live benchmarking (48 runs, Claude Haiku 4.5) showed models
substituting visible core tools (terminal/web_search/browser) for deferred
capabilities or declaring them nonexistent instead of searching: 16/24
task success vs 24/24 with eager loading.

Skills never had this failure mode because every skill keeps a ~21-token
name+description listing line in the system prompt. This ports that exact
pattern to the tool bridge: when tool_search activates, a grouped manifest
of every deferred tool (name + first sentence of description, clipped to
60 chars, grouped per MCP server / toolset) is embedded in the tool_search
bridge description.

- tools/tool_search.py: build_catalog_listing() with deterministic
  ordering (byte-stable across assemblies -> prompt prefix stays
  cacheable); token-budget fallbacks full -> names-only -> legacy bare
  count; bridge_tool_schemas(listing=...) embeds it and instructs the
  model to skip tool_search when the exact name is visible (one fewer
  round-trip per use)
- config: tools.tool_search.listing auto|on|off (default auto),
  listing_max_tokens (default 4000, clamped 200..20000); legacy bool
  shapes keep working
- tests: 8 new tests (config parsing/clamps, short-desc clipping,
  deterministic rendering, budget fallbacks, bridge embedding, assembly
  on/off paths); full file green (47 passed)
- docs: tool-search.md config table + rationale
- scripts/tool_search_livetest2.py: benchmark harness v2 with real
  per-call token accounting (normalize_usage spy) and a third 'listing'
  mode for A/B/C comparison
…eplayed (Opus 4.8)

Replays the actual tool schemas captured from Epic's UE 5.8
ModelContextProtocol + AllToolsets plugins (830 tools / 52 toolsets) as
live registry tools with mocked editor responses, then benchmarks
eager vs bare-bridge vs bridge+listing at two scales (62-tool editor
subset, full 830) on Claude Opus 4.8 (1M ctx; eager at 830 does not fit
any 200K model — first call requests ~266K tokens).

Headline (full 830, mean per task, rescored): eager 8/8 at 810,578
input tokens ($4.05); bare bridge 16/16 at 160,844 ($0.80); listing
16/16 at 257,264 ($1.29). Frontier model erases the accuracy gap in
every mode; cost is the differentiator. At 62 tools eager wins on cost
— consistent with the auto-threshold design.

Also parameterizes livetest harness model + listing_max_tokens via
env/args (TS_UE_MODEL, TS_UE_SCALE, TS_UE_MODES, TS_UE_LISTING_MAX).
… error mocks, strict scoring

Scenarios target real confusion clusters in Epic's UE 5.8 catalog
(StaticMesh vs SkeletalMesh set_material, three tag systems, CurveTable
vs DataTable rows, Niagara Component vs System variables, four capture
variants, zero-keyword phrasing). Mocks return realistic editor errors
on wrong-type calls; scoring is strict (clean solve = correct tool with
zero distractor calls; first-call accuracy tracked separately).

Key result: first-call selection is unreliable in EVERY mode — eager
with all 199K of schemas in context managed 2/10 — but clean solves stay
75-95% because agents probe (get_components, get_material_slots) before
committing. The probe loop works through the 3-tool bridge at 1/4 the
cost of eager ($1.60-1.69 vs $6.49/task, Opus 4.8). On Haiku the
listing beats bare bridge 18/20 vs 15/20 (core-tool substitution again).
Zero distractor invocations across all 50 Opus runs.
…e the listing's structural advantage

Bridge vs listing only (Opus 4.8, 830 real UE schemas, 3 reps/cell).
Excluding one both-modes mock artifact: listing 24/24 vs bridge 20/24,
searches/task 0.2 vs 4.0. Bridge failures: core-tool substitution at
frontier tier (ran the host test suite via terminal instead of
discovering RunTests, 2/3 reps), up to 8 searches to prove a negative,
and search-vocabulary misses on paraphrase. Listing asserts absence in
zero searches and answers a 5-way capability survey in 1 API call.
…scale the listing with catalog size

Tier 0: no MCP/plugin tools -> everything eager (pass-through).
Tier 1: deferred tools whose catalog listing fits min(threshold_pct%
        of context, listing_max_tokens) -> bridge + skills-style
        listing, degrading to names-only over budget.
Tier 2: listing over budget even names-only (Cloudflare's flat API
        surface: 3,320 tools, names alone ~32K tokens) -> bare bridge,
        discovery through tool_search only.

The old activation threshold (defer only when schemas > threshold_pct
of context) let mid-size catalogs ride eager and pay full schema cost;
with servers like Cloudflare (~597K tokens of schema, would not even
fit a 200K window) the binary gate is the wrong shape. Activation is
now driven purely by deferrable-tool presence; threshold_pct is
repurposed as the listing budget's context-relative leg.

- AssemblyResult gains tier + listing_form for observability
- listing_max_tokens default 4000 -> 20000 (cap 60000) so an 830-tool
  catalog keeps a names-only listing while Cloudflare-scale drops to
  bare bridge
- E2E verified against real captures: Linear 24 tools -> tier 1 full,
  Epic UE 5.8 830 tools -> tier 1 names-only, Cloudflare 3,320 tools
  -> tier 2 (both 200K and 1M context)
…verse-map at dispatch

Cloudflare's flat API MCP ships 61 property keys that violate Anthropic's
^[a-zA-Z0-9_.-]{1,64}$ pattern (query-filter params like 'issue_class~neq'
and 'meta.<field>[<operator>]'). One bad key anywhere in the tools array
400s the ENTIRE request — measured live: Anthropic, Bedrock, Google Vertex,
and Azure all rejected an eager 3,320-tool Cloudflare request at validation,
before token limits even applied.

- schema_sanitizer: rename non-conforming property keys deterministically
  (bad chars -> '_', 64-char truncation, collision dedup with numeric
  suffixes), nested schemas included; required[] remapped alongside
- unrename_tool_args(): reverse map applied in coerce_tool_args at dispatch,
  so the MCP server receives the original wire names; recurses into object
  values and array items
- deterministic on both sides: the rename map is recomputed from the
  registry's original schema at dispatch time, no state carried

E2E on the real capture: 61 -> 0 violations across 3,320 tools; round-trip
verified on get_accounts_intel_attacksurfacereport_issues (5 '~neq' keys).
…ion; 5% default budget

Teknium review changes on the tiered policy:

1. threshold_pct default 10 -> 5 (listing budget = min(5% of context,
   listing_max_tokens)); unknown-context fallback 20K -> 10K.

2. Tier 2 no longer leaves the model blind: when even names-only doesn't
   fit, the bridge description now carries a one-line-per-server summary
   ('cloudflare (3320 tools)') plus an instruction to search FIRST rather
   than substitute a generic tool or claim the capability is missing —
   the measured tier-2 failure mode (core-tool substitution) at zero
   meaningful token cost (~50 tokens/server).

3. Listing degradation is now PER SERVER, largest first: one oversized
   server (Cloudflare) collapses to its summary line while small
   co-attached servers (Linear) keep their full per-tool listings
   ('mixed' form). Previously global: attaching Cloudflare next to
   Linear silently cost Linear its listing. Greedy fit is deterministic
   (size then label) so the rendered block stays byte-stable per catalog
   — prompt-prefix cache safe.

E2E on real captures (defaults, 200K ctx): linear alone -> tier 1 full;
unreal alone -> tier 2 groups (5% budget) / tier 1 names at 1M;
cloudflare alone -> tier 2 groups; linear+cloudflare -> tier 1 MIXED
(linear fully listed, cloudflare summarized). 48/48 tests.
The include/exclude filter matched exact names only — glob-style entries
('*_radar_*') silently matched nothing, so a Cloudflare flat-mode config
meant to trim 3,320 tools to ~1,900 actually registered 3,319. Unmatched
patterns produced no warning.

- matches_name_filter(): exact membership first (O(1) for literal lists),
  then fnmatch.fnmatchcase for entries containing * ? [ — same pattern
  semantics as approvals.deny. Entries without metacharacters stay
  strictly literal ('docs' never matches 'docs_search').
- _should_register() uses it for both include and exclude (symmetric)
- hermes mcp tools picker (mcp_config.py) pre-selection uses the same
  matcher so the UI agrees with runtime registration

E2E against the live Cloudflare capture with the real exclude list:
3,320 -> 1,905 surviving (1,415 excluded); radar/DLP gone,
purge_cache/dns_records kept. 220/220 mcp_tool tests (4 new).
…o tiered disclosure

CI slices 2 and 7 caught three tests broken by always-defer:

- /tools (CLI show_tools + TUI gateway tools.show) now passes
  skip_tool_search_assembly=True — it's a discovery/inspection surface,
  so users verifying an MCP installed must see deferred tools, not a
  collapsed bridge row. This also fixes
  test_slash_worker_mcp_discovery (profile MCP tool visible in /tools).
- test_plugins.py::test_plugin_tools_in_definitions: 'visible' becomes
  'reachable' — direct schema OR listed in the bridge description;
  scope-exclusion assertions unchanged (not direct AND not listed).
- test_discord_tool.py dynamic-schema-rebuild test reads the
  pre-assembly list (the rebuilt schema is what tool_describe serves).

Banner/status tool counts intentionally keep the post-assembly view —
they reflect what the model actually sees.
@latentoperator
latentoperator marked this pull request as ready for review July 27, 2026 18:20
@latentoperator
latentoperator merged commit bcc8784 into hopebox/prod-v2026.7.20 Jul 27, 2026
32 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants