Skip to content

feat(mcp): TOON compact tool output — ~10x fewer response tokens across the query surface#1013

Merged
DeusData merged 4 commits into
mainfrom
feat/compact-tool-output
Jul 11, 2026
Merged

feat(mcp): TOON compact tool output — ~10x fewer response tokens across the query surface#1013
DeusData merged 4 commits into
mainfrom
feat/compact-tool-output

Conversation

@DeusData

Copy link
Copy Markdown
Owner

Tool responses are consumed by LLM agents, where every byte is context tokens. This lands the compact-output pass designed in-session: TOON (header-once + rows) as the default encoding for homogeneous result sets, context-bomb defaults fixed, and internal fields permanently blocked — with format:"json" escape hatches everywhere and zero capability loss (fields/aspects/get_code_snippet restore anything on request).

Measured (real index, identical calls)

Surface Before After
search_graph regex (14 hits) 20,793 B 1,327 B (−94%)
search_graph semantic 102,837 B 4,181 B (−96%)
search_graph BM25 (50 rows) 19,667 B 5,081 B
trace_path both 3,589 B 1,634 B
search_code compact (17 hits) 2,488 B 1,135 B
search_code full 142,435 B 24,246 B (match-anchored 60-line window + source_truncated)
get_architecture default 94,128 B 2,758 B (−97%)
get_architecture ["all"] 94,128 B 52,164 B
get_code_snippet 4,649 B 2,941 B (source untouched; blob dropped)
list_projects 10,380 B ~6 KB

A typical discovery session (1 arch + 4 searches + 2 traces + 3 snippets + 2 search_code) drops ~51K → ~4.7K tokens. TOON text is not valid JSON, so the duplicate structuredContent copy disappears at the protocol layer too.

Key changes

  • Internal similarity fields (fp/sp/bt) never emitted — they were 45% of every node payload (450 B minhash hex per hit) with zero agent value; blocklisted even when explicitly requested via fields.
  • Properties/metrics are opt-in columns (fields:["complexity","signature",...]) instead of an unconditional 28-field dump.
  • Defaults tuned: search limit 200→50 (BM25 100→50), architecture default = summary + aspects_hint, semantic-only calls no longer run the unfiltered base search (that alone was 102 KB).
  • Search rows now carry line ranges (the legacy output lacked them); degree columns documented as all-edge degree, not caller counts.
  • Quoting per TOON spec in a small new emitter module src/mcp/compact_out.{c,h}.

Quality gates

  • Retrieval-accuracy evidence: TOON benchmarks show equal-or-better LLM comprehension at 40–60% fewer tokens vs JSON (benchmarks, arXiv 2603.03306); an in-house eval A/B on the eval harness is the recommended release gate.
  • Guard tests: tool_search_graph_toon_never_leaks_internal_fields (sentinel fp/sp/bt can never appear, even when requested), tool_output_byte_budgets (absolute byte ceilings on default outputs — re-bloat goes red immediately), snippet_enriched_properties inverted to pin the no-spill contract.
  • Full suite 5,984 passed / 0 failed, lint-ci clean, smoke-test green end-to-end (all consumers migrated to the TOON contract).

Not armed for auto-merge — worth a look before it lands given the surface area. Follow-ups tracked separately: #997 (CLI strict flags, PR #1012), #998 (semantic determinism), #999 (junk infra routes), #1000 (registry regression, PR #1011).

DeusData added 2 commits July 10, 2026 08:23
… tokens

Tool responses land in an LLM agent's context window, where the legacy
per-node JSON objects were dominated by data no agent can use: the
similarity/semantic pipeline intermediates fp (~450B minhash hex), bt
(body-token bag) and sp (structural profile) plus 14 complexity metrics
were grafted onto every result by enrich_node_properties — ~64% of every
node emission, ~370 tokens per search hit.

search_graph now defaults to TOON (Token-Oriented Object Notation,
toonformat.dev): scalars as 'key: value' lines and results as a
'results[N]{qn,label,file,lines,in,out}:' header + one row per hit.
Published benchmarks show equal-or-better LLM retrieval accuracy at
40-60% fewer tokens than JSON; rows now also carry line ranges, which
the legacy output lacked.

- fields:[...] opts into per-node property columns (complexity,
  signature, docstring, ...); fp/sp/bt are blocklisted even there
- format:"json" restores the legacy verbose objects byte-identically;
  include_connected forces it (nested neighbor lists)
- BM25 and semantic modes emit the same table shape (rank/score column)
- semantic-only calls no longer run the unfiltered regex search that
  silently prepended up to 200 enriched nodes (102.8KB -> 4.2KB)
- default limit 200 -> 50 (BM25 100 -> 50): cheap first page, page via
  has_more/offset as before
- new emitter module src/mcp/compact_out.{c,h} (string builder + TOON
  scalar/table emission with spec quoting rules)

Measured on a real index (14-hit regex search): 20,793B -> 1,327B
(-94%); BM25 19,667B -> 5,081B; semantic 102,837B -> 4,181B. A typical
discovery session drops ~50.9K -> ~4.7K tokens combined with the
follow-up slices.

Guards: tool_search_graph_toon_never_leaks_internal_fields (sentinel
fp/sp/bt never emitted, even when requested), rewritten
tool_search_graph_includes_node_properties pins default-lean/fields-
opt-in/json-escape-hatch; suite 5983 passed, smoke green.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
…text-bomb defaults

Completes the compact-output pass across the remaining query tools
(consumers are LLM agents; every response byte is context tokens):

- trace_path: callees[N]{qn,hop}/callers[N]{qn,hop} tables (risk/test/
  args columns per flags); the per-hop JSON key envelope was 84% of the
  payload. 3,589B -> 1,634B on a representative trace.
- query_graph: rows[N]{cols}: table (columns double as the header);
  format:"json" restores columns/rows arrays.
- search_code compact mode: results/raw/dirs tables; drops the
  field (duplicate of the qn tail). 2,488B -> 1,135B for 17 hits.
- search_code full mode: per-hit source capped at a 60-line window
  anchored on the first match, with source_start/source_truncated
  markers — uncapped whole-symbol dumps ran to 142KB per response; the
  complete symbol stays one get_code_snippet call away.
- get_architecture: default (no aspects) is now a summary (languages,
  packages, entry_points + always-on totals/label/type counts) with an
  aspects_hint — the old default rendered EVERYTHING including the full
  file_tree (94KB -> 4.5KB -> 2.8KB with TOON tables); all sections
  emit as TOON tables, aspects:[...] and ["all"] keep full access
  (52KB vs 94KB even for the explicit full dump).
- get_code_snippet: drops the property-blob enrichment (41% of the
  response; signature/docstring are literally in the source; fp/sp/bt
  never belonged there). Metrics remain reachable via search_graph
  fields=[...].
- list_projects: per-project 12-field git block (mostly null) replaced
  by the branch name; name/root/nodes/edges/size kept.
- search_graph description: in/out documented as TOTAL all-edge degree,
  NOT caller/callee counts (agent evals mis-read it as callers).
- format:"json" escape hatch on every converted tool.

Guards: tool_output_byte_budgets pins absolute byte ceilings on default
search/trace outputs (a property blob sneaking back into row emission
blows them immediately); snippet_enriched_properties inverted to pin
the no-spill contract. Suite 5,984 green; smoke updated to the TOON
contract end-to-end.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData DeusData added this to the 0.9.1-rc milestone Jul 10, 2026
DeusData added 2 commits July 10, 2026 22:53
…ph calls

query_graph now defaults to TOON text; the guard's definition-count
probe parsed it as JSON, swallowed the parse failure, and reported the
ASCII baseline as having zero definitions (SETUP FAIL on the
test-windows-guards leg). Scripted consumers use the format="json"
escape hatch.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
…call

search_graph now defaults to TOON text, but ha_format_context parses
the inner payload as JSON ("results"), so the Grep/Glob/Read hook
augmenter silently produced no hookSpecificOutput — caught by the
test_hook_augment.py Windows guard going red. Both internal call sites
route through ha_build_args; request the JSON encoding there.

Verified end-to-end: indexed fixture + PreToolUse payload via stdin
emits the symbol context again.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData DeusData merged commit 751d229 into main Jul 11, 2026
21 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.

1 participant