Skip to content

Commit 751d229

Browse files
authored
Merge pull request #1013 from DeusData/feat/compact-tool-output
feat(mcp): TOON compact tool output — ~10x fewer response tokens across the query surface
2 parents bd996c8 + be9976d commit 751d229

10 files changed

Lines changed: 1455 additions & 156 deletions

File tree

Makefile.cbm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ STORE_SRCS = src/store/store.c
174174
CYPHER_SRCS = src/cypher/cypher.c
175175

176176
# MCP server module (new)
177-
MCP_SRCS = src/mcp/mcp.c src/mcp/index_supervisor.c
177+
MCP_SRCS = src/mcp/mcp.c src/mcp/index_supervisor.c src/mcp/compact_out.c
178178

179179
# Discover module (new)
180180
DISCOVER_SRCS = \

scripts/smoke-test.sh

Lines changed: 52 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,9 @@ PROJECT=$(echo "$RESULT" | python3 -c "import json,sys; d=json.loads(sys.stdin.r
174174
if ! SEARCH=$(cli search_graph --project "$PROJECT" --name-pattern compute); then
175175
echo "FAIL: search_graph (flag form) exited non-zero"; cat "$CLI_STDERR"; exit 1
176176
fi
177-
TOTAL=$(echo "$SEARCH" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(d.get('total',0))" 2>/dev/null || echo "0")
177+
# search_graph default output is TOON (key: value scalars + header/rows tables)
178+
TOTAL=$(echo "$SEARCH" | sed -n 's/^total: //p' | head -1)
179+
TOTAL=${TOTAL:-0}
178180
if [ "$TOTAL" -lt 1 ]; then
179181
echo "FAIL: search_graph for 'compute' returned 0 results"
180182
exit 1
@@ -185,7 +187,9 @@ echo "OK: search_graph found $TOTAL result(s) for 'compute'"
185187
if ! TRACE=$(cli trace_path --project "$PROJECT" --function-name compute --direction inbound --depth 1); then
186188
echo "FAIL: trace_path (flag form) exited non-zero"; cat "$CLI_STDERR"; exit 1
187189
fi
188-
CALLERS=$(echo "$TRACE" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(len(d.get('callers',[])))" 2>/dev/null || echo "0")
190+
# trace_path default output is TOON: the callers[N]{...} header carries the count
191+
CALLERS=$(echo "$TRACE" | sed -n 's/^callers\[\([0-9]*\)\].*/\1/p' | head -1)
192+
CALLERS=${CALLERS:-0}
189193
if [ "$CALLERS" -lt 1 ]; then
190194
echo "FAIL: trace_path found 0 callers for 'compute'"
191195
exit 1
@@ -207,7 +211,8 @@ echo "OK: schema has $LABELS node labels"
207211
if ! FOLDERS=$(cli search_graph --project "$PROJECT" --label Folder); then
208212
echo "FAIL: search_graph --label Folder exited non-zero"; cat "$CLI_STDERR"; exit 1
209213
fi
210-
FOLDER_COUNT=$(echo "$FOLDERS" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(d.get('total',0))" 2>/dev/null || echo "0")
214+
FOLDER_COUNT=$(echo "$FOLDERS" | sed -n 's/^total: //p' | head -1)
215+
FOLDER_COUNT=${FOLDER_COUNT:-0}
211216
if [ "$FOLDER_COUNT" -lt 2 ]; then
212217
echo "FAIL: expected at least 2 Folder nodes (src, src/pkg), got $FOLDER_COUNT"
213218
exit 1
@@ -217,7 +222,8 @@ echo "OK: $FOLDER_COUNT Folder nodes (init.py didn't clobber them)"
217222
# 3d-cypher: query_graph Cypher capabilities
218223
# #238 WITH DISTINCT — all functions share label "Function" → collapses to 1 row.
219224
CYPHER_WD=$(cli query_graph --project "$PROJECT" --query "MATCH (f:Function) WITH DISTINCT f.label AS lbl RETURN lbl")
220-
WD_ROWS=$(echo "$CYPHER_WD" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(len(d.get('rows',[])))" 2>/dev/null || echo "0")
225+
WD_ROWS=$(echo "$CYPHER_WD" | sed -n 's/^total: //p' | head -1)
226+
WD_ROWS=${WD_ROWS:-0}
221227
if [ "$WD_ROWS" -lt 1 ]; then
222228
echo "FAIL: query_graph WITH DISTINCT returned 0 rows"
223229
echo "$CYPHER_WD"
@@ -227,7 +233,8 @@ echo "OK: query_graph WITH DISTINCT returned $WD_ROWS row(s)"
227233

228234
# #241 WHERE label test — f:Function is true for every Function node.
229235
CYPHER_LBL=$(cli query_graph --project "$PROJECT" --query "MATCH (f:Function) WHERE f:Function RETURN f.name")
230-
LBL_ROWS=$(echo "$CYPHER_LBL" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(len(d.get('rows',[])))" 2>/dev/null || echo "0")
236+
LBL_ROWS=$(echo "$CYPHER_LBL" | sed -n 's/^total: //p' | head -1)
237+
LBL_ROWS=${LBL_ROWS:-0}
231238
if [ "$LBL_ROWS" -lt 1 ]; then
232239
echo "FAIL: query_graph WHERE label-test returned 0 rows"
233240
echo "$CYPHER_LBL"
@@ -237,7 +244,8 @@ echo "OK: query_graph WHERE f:Function returned $LBL_ROWS row(s)"
237244

238245
# #242 label alternation — (n:Function|Module) seeds either label.
239246
CYPHER_ALT=$(cli query_graph --project "$PROJECT" --query "MATCH (n:Function|Module) RETURN n.name")
240-
ALT_ROWS=$(echo "$CYPHER_ALT" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(len(d.get('rows',[])))" 2>/dev/null || echo "0")
247+
ALT_ROWS=$(echo "$CYPHER_ALT" | sed -n 's/^total: //p' | head -1)
248+
ALT_ROWS=${ALT_ROWS:-0}
241249
if [ "$ALT_ROWS" -lt 1 ]; then
242250
echo "FAIL: query_graph label alternation returned 0 rows"
243251
echo "$CYPHER_ALT"
@@ -247,7 +255,8 @@ echo "OK: query_graph (n:Function|Module) returned $ALT_ROWS row(s)"
247255

248256
# #239 count(DISTINCT) — must parse and return a single aggregate row.
249257
CYPHER_CD=$(cli query_graph --project "$PROJECT" --query "MATCH (f:Function) RETURN count(DISTINCT f.label)")
250-
CD_ROWS=$(echo "$CYPHER_CD" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(len(d.get('rows',[])))" 2>/dev/null || echo "0")
258+
CD_ROWS=$(echo "$CYPHER_CD" | sed -n 's/^total: //p' | head -1)
259+
CD_ROWS=${CD_ROWS:-0}
251260
if [ "$CD_ROWS" -ne 1 ]; then
252261
echo "FAIL: query_graph count(DISTINCT) expected 1 row, got $CD_ROWS"
253262
echo "$CYPHER_CD"
@@ -261,7 +270,7 @@ cyp_first_cell() {
261270
# argv token, so string-literal args (e.g. replace(f.name,"a","A")) and Cypher
262271
# metacharacters {}|=~<>" need no JSON escaping.
263272
cli query_graph --project "$PROJECT" --query "$1" |
264-
python3 -c "import json,sys; d=json.loads(sys.stdin.read()); rows=d.get('rows',[]); print(rows[0][0] if rows and rows[0] else '')" 2>/dev/null || echo ""
273+
sed -n '/^rows\[/{n;p;}' | sed 's/^ //' | sed 's/^"//;s/"$//;s/\\"/"/g'
265274
}
266275

267276
# labels(n) → JSON list like ["Function"]
@@ -316,15 +325,17 @@ echo "OK: query_graph coalesce(f.nonesuch, f.name) = $COALV"
316325

317326
# EXISTS { } pattern predicate (edge-type-specific existence)
318327
CYPHER_EX=$(cli query_graph --project "$PROJECT" --query "MATCH (f:Function) WHERE EXISTS { (f)-[:CALLS]->() } RETURN f.name")
319-
EX_ROWS=$(echo "$CYPHER_EX" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(len(d.get('rows',[])))" 2>/dev/null || echo "0")
328+
EX_ROWS=$(echo "$CYPHER_EX" | sed -n 's/^total: //p' | head -1)
329+
EX_ROWS=${EX_ROWS:-0}
320330
if [ "$EX_ROWS" -lt 1 ]; then
321331
echo "FAIL: query_graph EXISTS{} predicate returned 0 rows"; echo "$CYPHER_EX"; exit 1
322332
fi
323333
echo "OK: query_graph EXISTS { (f)-[:CALLS]->() } returned $EX_ROWS row(s)"
324334

325335
# =~ regex match in WHERE
326336
CYPHER_RX=$(cli query_graph --project "$PROJECT" --query 'MATCH (f:Function) WHERE f.name =~ ".+" RETURN f.name')
327-
RX_ROWS=$(echo "$CYPHER_RX" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(len(d.get('rows',[])))" 2>/dev/null || echo "0")
337+
RX_ROWS=$(echo "$CYPHER_RX" | sed -n 's/^total: //p' | head -1)
338+
RX_ROWS=${RX_ROWS:-0}
328339
if [ "$RX_ROWS" -lt 1 ]; then
329340
echo "FAIL: query_graph WHERE =~ regex returned 0 rows"; echo "$CYPHER_RX"; exit 1
330341
fi
@@ -347,7 +358,7 @@ LEFTV=$(cyp_first_cell 'MATCH (f:Function) RETURN left(f.name, 3) AS l LIMIT 1')
347358

348359
# NOT EXISTS dead-code query (functions with no caller)
349360
CYPHER_NX=$(cli query_graph --project "$PROJECT" --query "MATCH (f:Function) WHERE NOT EXISTS { (f)<-[:CALLS]-() } RETURN f.name")
350-
NX_OK=$(echo "$CYPHER_NX" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print('rows' in d)" 2>/dev/null || echo "False")
361+
NX_OK=$(echo "$CYPHER_NX" | grep -qE '^rows\[[0-9]+\]\{' && echo "True" || echo "False")
351362
[ "$NX_OK" = "True" ] && echo "OK: query_graph NOT EXISTS dead-code query executed" || { echo "FAIL: NOT EXISTS query"; echo "$CYPHER_NX" | head -c 300; exit 1; }
352363

353364
# CASE expression in RETURN
@@ -370,19 +381,33 @@ esac
370381
if ! ARCH=$(cli get_architecture --project "$PROJECT" --aspects clusters); then
371382
echo "FAIL: get_architecture (flag form) exited non-zero"; cat "$CLI_STDERR"; exit 1
372383
fi
373-
NCLUST=$(echo "$ARCH" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(len(d.get('clusters',[])))" 2>/dev/null || echo "0")
384+
# get_architecture default output is TOON: clusters[N]{...} header carries the count
385+
NCLUST=$(echo "$ARCH" | sed -n 's/^clusters\[\([0-9]*\)\].*/\1/p' | head -1)
386+
NCLUST=${NCLUST:-0}
374387
if [ "$NCLUST" -lt 1 ]; then
375388
echo "FAIL: get_architecture returned 0 community clusters"; echo "$ARCH" | head -c 400; exit 1
376389
fi
377390
echo "OK: get_architecture returned $NCLUST community cluster(s)"
378391

379392
# 3g: search_code — basic search reports elapsed_ms + matches
380393
SC=$(cli search_code --project "$PROJECT" --pattern cbm_ --mode compact --limit 5)
381-
echo "$SC" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); assert 'elapsed_ms' in d; print('OK: search_code elapsed_ms='+str(d['elapsed_ms'])+' total_grep_matches='+str(d.get('total_grep_matches')))" 2>/dev/null || { echo "FAIL: search_code basic / no elapsed_ms"; echo "$SC" | head -c 400; exit 1; }
394+
# compact mode emits TOON scalars: `elapsed_ms: N` + `total_grep_matches: N`
395+
SC_ELAPSED=$(echo "$SC" | sed -n 's/^elapsed_ms: //p' | head -1)
396+
SC_GREPM=$(echo "$SC" | sed -n 's/^total_grep_matches: //p' | head -1)
397+
if [ -n "$SC_ELAPSED" ]; then
398+
echo "OK: search_code elapsed_ms=$SC_ELAPSED total_grep_matches=${SC_GREPM:-0}"
399+
else
400+
echo "FAIL: search_code basic / no elapsed_ms"; echo "$SC" | head -c 400; exit 1
401+
fi
382402

383403
# 3g: search_code — literal '|' under regex=false must surface a warning (#282)
384404
SCW=$(cli search_code --project "$PROJECT" --pattern "cbm_init|cbm_nope" --regex false --limit 5)
385-
echo "$SCW" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); w=' '.join(d.get('warnings',[])); assert 'regex=true' in w; print('OK: search_code literal-| warning surfaced')" 2>/dev/null || { echo "FAIL: search_code literal-| warning missing"; echo "$SCW" | head -c 400; exit 1; }
405+
# TOON scalar `warning: ... regex=true ...`
406+
if echo "$SCW" | grep -q "regex=true"; then
407+
echo "OK: search_code literal-| warning surfaced"
408+
else
409+
echo "FAIL: search_code literal-| warning missing"; echo "$SCW" | head -c 400; exit 1
410+
fi
386411

387412
# 3g: search_code — '&' in file_pattern accepted, not rejected as invalid (#272)
388413
SCA=$(cli search_code --project "$PROJECT" --pattern cbm_ --file-pattern "*R&D*.c" --limit 5)
@@ -396,36 +421,39 @@ echo "=== Phase 3h: CLI input-mode guards (flags / stdin / --args-file / --help
396421

397422
# Small helper: assert its stdin is a JSON object (exit non-zero otherwise).
398423
assert_json_obj() { python3 -c "import json,sys; d=json.loads(sys.stdin.read()); sys.exit(0 if isinstance(d,dict) else 1)" 2>/dev/null; }
424+
# search_graph emits TOON by default: a results/semantic table header proves
425+
# the tool parsed its typed flags and produced a well-formed response.
426+
assert_toon_table() { grep -qE '^(results|semantic)\[[0-9]+\]\{'; }
399427

400-
# B1: INTEGER flag — --limit is schema-typed integer; must parse to valid JSON.
428+
# B1: INTEGER flag — --limit is schema-typed integer; must parse and answer.
401429
if ! IM_INT=$(cli search_graph --project "$PROJECT" --name-pattern compute --limit 5); then
402430
echo "FAIL B1: search_graph --limit 5 exited non-zero"; cat "$CLI_STDERR"; exit 1
403431
fi
404-
if echo "$IM_INT" | assert_json_obj; then
405-
echo "OK B1: INTEGER flag (--limit 5) parsed → valid JSON"
432+
if echo "$IM_INT" | assert_toon_table; then
433+
echo "OK B1: INTEGER flag (--limit 5) parsed → TOON results table"
406434
else
407-
echo "FAIL B1: --limit 5 did not produce valid JSON"; echo "$IM_INT" | head -c 300; exit 1
435+
echo "FAIL B1: --limit 5 did not produce a TOON results table"; echo "$IM_INT" | head -c 300; exit 1
408436
fi
409437

410438
# B2: BOOLEAN bare flag — --exclude-entry-points with no value → true; must succeed.
411439
if ! IM_BOOL=$(cli search_graph --project "$PROJECT" --exclude-entry-points); then
412440
echo "FAIL B2: search_graph --exclude-entry-points exited non-zero"; cat "$CLI_STDERR"; exit 1
413441
fi
414-
if echo "$IM_BOOL" | assert_json_obj; then
442+
if echo "$IM_BOOL" | assert_toon_table; then
415443
echo "OK B2: BOOLEAN bare flag (--exclude-entry-points) → success"
416444
else
417-
echo "FAIL B2: --exclude-entry-points did not produce valid JSON"; echo "$IM_BOOL" | head -c 300; exit 1
445+
echo "FAIL B2: --exclude-entry-points did not produce a TOON results table"; echo "$IM_BOOL" | head -c 300; exit 1
418446
fi
419447

420448
# B3: ARRAY flag — repeated --semantic-query accumulates into a JSON array.
421-
# semantic_results may be empty (index-mode dependent); only assert valid JSON.
449+
# Semantic-only calls emit ONLY the semantic table (may be empty, header stays).
422450
if ! IM_ARR=$(cli search_graph --project "$PROJECT" --semantic-query send --semantic-query publish); then
423451
echo "FAIL B3: search_graph repeated --semantic-query exited non-zero"; cat "$CLI_STDERR"; exit 1
424452
fi
425-
if echo "$IM_ARR" | assert_json_obj; then
426-
echo "OK B3: ARRAY flag (repeated --semantic-query) → valid JSON"
453+
if echo "$IM_ARR" | grep -qE '^semantic\[[0-9]+\]\{'; then
454+
echo "OK B3: ARRAY flag (repeated --semantic-query) → semantic TOON table"
427455
else
428-
echo "FAIL B3: repeated --semantic-query did not produce valid JSON"; echo "$IM_ARR" | head -c 300; exit 1
456+
echo "FAIL B3: repeated --semantic-query did not produce a semantic table"; echo "$IM_ARR" | head -c 300; exit 1
429457
fi
430458

431459
# B4: STDIN — piped JSON resolves; this path must NOT emit a deprecation warning.

src/cli/hook_augment.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,9 @@ static char *ha_build_args(const char *project, const char *token) {
146146
yyjson_mut_obj_add_str(doc, root, "project", project);
147147
yyjson_mut_obj_add_str(doc, root, "name_pattern", name_pattern);
148148
yyjson_mut_obj_add_int(doc, root, "limit", HA_RESULT_LIMIT);
149+
/* Programmatic consumer: search_graph defaults to TOON text, but
150+
* ha_format_context parses the inner payload as JSON ("results"). */
151+
yyjson_mut_obj_add_str(doc, root, "format", "json");
149152

150153
char *out = yyjson_mut_write(doc, 0, NULL);
151154
yyjson_mut_doc_free(doc);

src/foundation/constants.h

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,11 @@ enum {
7070

7171
/* ── Default pagination limits ───────────────────────────────── */
7272
/* Default page size for search_graph and the underlying store-layer search.
73-
* Chosen so a typical broad query (e.g. file_pattern="**" on a 12k-node
74-
* project) stays well within MCP tool-result size budgets. Callers that
75-
* want more results paginate via offset+limit; the response always carries
76-
* 'total' and 'has_more' so agents can detect truncation. */
77-
enum { CBM_DEFAULT_SEARCH_LIMIT = 200 };
73+
* Responses land in an LLM agent's context window, so the default favors a
74+
* cheap first page (~50 TOON rows ≈ 1.5K tokens) over raw coverage; the
75+
* response always carries 'total' and 'has_more', and agents page via
76+
* offset+limit or narrow with label/file_pattern when has_more is true. */
77+
enum { CBM_DEFAULT_SEARCH_LIMIT = 50 };
7878

7979
/* ── Time conversion factors ─────────────────────────────────── */
8080
#define CBM_NSEC_PER_SEC 1000000000ULL

0 commit comments

Comments
 (0)