homegraph 1.1.2 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +243 -243
- package/dist/bin/homegraph.js +371 -9
- package/dist/bin/homegraph.js.map +1 -1
- package/dist/db/migrations.js +18 -18
- package/dist/db/queries.js +140 -140
- package/dist/db/schema.sql +152 -152
- package/dist/directory.js +5 -5
- package/dist/extraction/wasm/tree-sitter-c_sharp.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-pascal.wasm +0 -0
- package/dist/installer/instructions-template.js +9 -9
- package/dist/installer/targets/shared.d.ts +5 -6
- package/dist/installer/targets/shared.d.ts.map +1 -1
- package/dist/installer/targets/shared.js +5 -6
- package/dist/installer/targets/shared.js.map +1 -1
- package/dist/mcp/liveness-watchdog.js +16 -16
- package/dist/mcp/server-instructions.js +69 -69
- package/dist/mcp/tools.d.ts +17 -4
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +192 -22
- package/dist/mcp/tools.js.map +1 -1
- package/dist/reasoning/reasoner.js +32 -32
- package/dist/spec/config.d.ts +43 -0
- package/dist/spec/config.d.ts.map +1 -0
- package/dist/spec/config.js +275 -0
- package/dist/spec/config.js.map +1 -0
- package/dist/spec/db/commit-node.d.ts +23 -0
- package/dist/spec/db/commit-node.d.ts.map +1 -0
- package/dist/spec/db/commit-node.js +62 -0
- package/dist/spec/db/commit-node.js.map +1 -0
- package/dist/spec/db/fragment-node.d.ts +23 -0
- package/dist/spec/db/fragment-node.d.ts.map +1 -0
- package/dist/spec/db/fragment-node.js +120 -0
- package/dist/spec/db/fragment-node.js.map +1 -0
- package/dist/spec/db/fts.d.ts +60 -0
- package/dist/spec/db/fts.d.ts.map +1 -0
- package/dist/spec/db/fts.js +285 -0
- package/dist/spec/db/fts.js.map +1 -0
- package/dist/spec/db/index.d.ts +13 -0
- package/dist/spec/db/index.d.ts.map +1 -0
- package/dist/spec/db/index.js +50 -0
- package/dist/spec/db/index.js.map +1 -0
- package/dist/spec/db/relations.d.ts +55 -0
- package/dist/spec/db/relations.d.ts.map +1 -0
- package/dist/spec/db/relations.js +158 -0
- package/dist/spec/db/relations.js.map +1 -0
- package/dist/spec/db/schema.d.ts +30 -0
- package/dist/spec/db/schema.d.ts.map +1 -0
- package/dist/spec/db/schema.js +87 -0
- package/dist/spec/db/schema.js.map +1 -0
- package/dist/spec/db/schema.sql +107 -0
- package/dist/spec/db/spec-node.d.ts +41 -0
- package/dist/spec/db/spec-node.d.ts.map +1 -0
- package/dist/spec/db/spec-node.js +114 -0
- package/dist/spec/db/spec-node.js.map +1 -0
- package/dist/spec/evolve/impact-locator.d.ts +13 -0
- package/dist/spec/evolve/impact-locator.d.ts.map +1 -0
- package/dist/spec/evolve/impact-locator.js +25 -0
- package/dist/spec/evolve/impact-locator.js.map +1 -0
- package/dist/spec/evolve/llm-client.d.ts +50 -0
- package/dist/spec/evolve/llm-client.d.ts.map +1 -0
- package/dist/spec/evolve/llm-client.js +176 -0
- package/dist/spec/evolve/llm-client.js.map +1 -0
- package/dist/spec/evolve/logic-checker.d.ts +12 -0
- package/dist/spec/evolve/logic-checker.d.ts.map +1 -0
- package/dist/spec/evolve/logic-checker.js +48 -0
- package/dist/spec/evolve/logic-checker.js.map +1 -0
- package/dist/spec/evolve/pipeline.d.ts +40 -0
- package/dist/spec/evolve/pipeline.d.ts.map +1 -0
- package/dist/spec/evolve/pipeline.js +314 -0
- package/dist/spec/evolve/pipeline.js.map +1 -0
- package/dist/spec/evolve/spec-rewriter.d.ts +42 -0
- package/dist/spec/evolve/spec-rewriter.d.ts.map +1 -0
- package/dist/spec/evolve/spec-rewriter.js +254 -0
- package/dist/spec/evolve/spec-rewriter.js.map +1 -0
- package/dist/spec/graph/queries.d.ts +58 -0
- package/dist/spec/graph/queries.d.ts.map +1 -0
- package/dist/spec/graph/queries.js +215 -0
- package/dist/spec/graph/queries.js.map +1 -0
- package/dist/spec/mining/diff-parser.d.ts +33 -0
- package/dist/spec/mining/diff-parser.d.ts.map +1 -0
- package/dist/spec/mining/diff-parser.js +166 -0
- package/dist/spec/mining/diff-parser.js.map +1 -0
- package/dist/spec/mining/git-scanner.d.ts +91 -0
- package/dist/spec/mining/git-scanner.d.ts.map +1 -0
- package/dist/spec/mining/git-scanner.js +266 -0
- package/dist/spec/mining/git-scanner.js.map +1 -0
- package/dist/spec/mining/pipeline.d.ts +53 -0
- package/dist/spec/mining/pipeline.d.ts.map +1 -0
- package/dist/spec/mining/pipeline.js +165 -0
- package/dist/spec/mining/pipeline.js.map +1 -0
- package/dist/spec/mining/scope-resolver.d.ts +45 -0
- package/dist/spec/mining/scope-resolver.d.ts.map +1 -0
- package/dist/spec/mining/scope-resolver.js +103 -0
- package/dist/spec/mining/scope-resolver.js.map +1 -0
- package/dist/spec/mining/spec-extractor.d.ts +69 -0
- package/dist/spec/mining/spec-extractor.d.ts.map +1 -0
- package/dist/spec/mining/spec-extractor.js +369 -0
- package/dist/spec/mining/spec-extractor.js.map +1 -0
- package/dist/spec/types.d.ts +149 -0
- package/dist/spec/types.d.ts.map +1 -0
- package/dist/spec/types.js +15 -0
- package/dist/spec/types.js.map +1 -0
- package/dist/spec/utils.d.ts +166 -0
- package/dist/spec/utils.d.ts.map +1 -0
- package/dist/spec/utils.js +461 -0
- package/dist/spec/utils.js.map +1 -0
- package/package.json +58 -57
- package/scripts/add-lang/bench.sh +60 -60
- package/scripts/add-lang/check-grammar.mjs +75 -75
- package/scripts/add-lang/dump-ast.mjs +103 -103
- package/scripts/add-lang/verify-extraction.mjs +70 -70
- package/scripts/agent-eval/ab-adoption.sh +91 -91
- package/scripts/agent-eval/ab-hook.sh +86 -86
- package/scripts/agent-eval/ab-impl.sh +78 -78
- package/scripts/agent-eval/ab-new-vs-baseline.sh +102 -102
- package/scripts/agent-eval/ab-sufficiency.sh +78 -78
- package/scripts/agent-eval/arms-F.sh +21 -21
- package/scripts/agent-eval/arms-matrix.sh +37 -37
- package/scripts/agent-eval/audit.sh +68 -68
- package/scripts/agent-eval/bench-readme.sh +28 -28
- package/scripts/agent-eval/bench-why-repo.sh +22 -22
- package/scripts/agent-eval/block-read-hook.sh +19 -19
- package/scripts/agent-eval/hook-settings.json +15 -15
- package/scripts/agent-eval/itrun.sh +120 -120
- package/scripts/agent-eval/offload-eval-3arm.sh +72 -72
- package/scripts/agent-eval/offload-eval-cost.mjs +133 -133
- package/scripts/agent-eval/offload-eval-effort.mjs +108 -108
- package/scripts/agent-eval/offload-eval-frontload-matrix.sh +25 -25
- package/scripts/agent-eval/offload-eval-frontload.sh +47 -47
- package/scripts/agent-eval/offload-eval-ground-truth.json +18 -18
- package/scripts/agent-eval/offload-eval-hook.mjs +84 -84
- package/scripts/agent-eval/offload-eval-judge.mjs +103 -103
- package/scripts/agent-eval/offload-eval-matrix.sh +20 -20
- package/scripts/agent-eval/offload-eval-metrics.mjs +94 -94
- package/scripts/agent-eval/offload-eval-refs1.sh +50 -50
- package/scripts/agent-eval/offload-eval-setup.sh +24 -24
- package/scripts/agent-eval/offload-eval-styles.sh +71 -71
- package/scripts/agent-eval/offload-eval-summarize.mjs +68 -68
- package/scripts/agent-eval/offload-eval.md +76 -76
- package/scripts/agent-eval/parse-arms.mjs +116 -116
- package/scripts/agent-eval/parse-bench-readme.mjs +84 -84
- package/scripts/agent-eval/parse-run.mjs +45 -45
- package/scripts/agent-eval/parse-session.mjs +93 -93
- package/scripts/agent-eval/probe-context.mjs +21 -21
- package/scripts/agent-eval/probe-explore.mjs +40 -40
- package/scripts/agent-eval/probe-node.mjs +20 -20
- package/scripts/agent-eval/probe-sweep.mjs +119 -119
- package/scripts/agent-eval/probe-trace.mjs +20 -20
- package/scripts/agent-eval/redirect-read-hook.sh +38 -38
- package/scripts/agent-eval/run-agent.sh +34 -34
- package/scripts/agent-eval/run-all.sh +69 -69
- package/scripts/agent-eval/run-arms.sh +56 -56
- package/scripts/agent-eval/seq-matrix.mjs +137 -137
- package/scripts/build-bundle.sh +118 -118
- package/scripts/extract-release-notes.mjs +130 -130
- package/scripts/local-install.sh +41 -41
- package/scripts/npm-sdk.js +75 -75
- package/scripts/npm-shim.js +246 -246
- package/scripts/pack-npm.sh +119 -119
- package/scripts/prepare-release.mjs +270 -270
- package/scripts/qa_eval/README.md +407 -404
- package/scripts/qa_eval/_test_deveco_probe.py +41 -41
- package/scripts/qa_eval/agent_runner.py +526 -526
- package/scripts/qa_eval/data/.gitignore +4 -4
- package/scripts/qa_eval/data/test-set.jsonl +2 -22
- package/scripts/qa_eval/eval_metrics.py +274 -233
- package/scripts/qa_eval/external_agent.py +976 -671
- package/scripts/qa_eval/llm_config.py +92 -92
- package/scripts/qa_eval/memory_monitor.py +132 -132
- package/scripts/qa_eval/my_answer_accuracy.py +187 -187
- package/scripts/qa_eval/requirements.txt +2 -2
- package/scripts/qa_eval/run_pipeline.py +804 -711
- package/scripts/qa_eval/stats_efficiency.py +279 -279
- package/scripts/qa_eval/stats_scores.py +207 -207
|
@@ -1,116 +1,116 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Analyze the tool-surface ablation (/tmp/arms/<repo>/<arm>-r<n>.jsonl).
|
|
3
|
-
// Compares arms A–E on trace adoption, Read/Grep fallback, homegraph payload,
|
|
4
|
-
// round-trips, and duration — averaged across runs per arm.
|
|
5
|
-
//
|
|
6
|
-
// The decisive signal is READS: if removing a tool raises Reads on a question
|
|
7
|
-
// class, that tool was load-bearing for it (not redundant). If removing it
|
|
8
|
-
// changes nothing, it was redundant.
|
|
9
|
-
//
|
|
10
|
-
// A control all tools no steering (baseline)
|
|
11
|
-
// B steer all tools trace-first (adoption)
|
|
12
|
-
// C no-explore hide explore trace-first (is explore redundant?)
|
|
13
|
-
// D trace-centric hide explore+context trace-first (is the survey pair redundant?)
|
|
14
|
-
// E control-probe hide explore+context trace-first (NON-flow Q — should degrade)
|
|
15
|
-
//
|
|
16
|
-
// Usage: node scripts/agent-eval/parse-arms.mjs [/tmp/arms]
|
|
17
|
-
import { readFileSync, readdirSync, existsSync, statSync } from 'fs';
|
|
18
|
-
import { join } from 'path';
|
|
19
|
-
|
|
20
|
-
const ROOT = process.argv[2] || '/tmp/arms';
|
|
21
|
-
const cgShort = (n) => n.replace('mcp__homegraph__homegraph_', '').replace('mcp__homegraph__', '');
|
|
22
|
-
|
|
23
|
-
function parse(file) {
|
|
24
|
-
if (!existsSync(file)) return null;
|
|
25
|
-
const lines = readFileSync(file, 'utf8').split('\n').filter(Boolean);
|
|
26
|
-
const calls = []; let result = null, initCg = 0;
|
|
27
|
-
for (const l of lines) {
|
|
28
|
-
let ev; try { ev = JSON.parse(l); } catch { continue; }
|
|
29
|
-
if (ev.type === 'system' && ev.subtype === 'init') initCg = (ev.tools || []).filter(t => /homegraph/.test(t)).length;
|
|
30
|
-
if (ev.type === 'assistant') for (const b of (ev.message?.content || [])) if (b.type === 'tool_use')
|
|
31
|
-
calls.push({ id: b.id, name: b.name, out: 0 });
|
|
32
|
-
if (ev.type === 'user') for (const b of (ev.message?.content || [])) if (b.type === 'tool_result') {
|
|
33
|
-
const c = b.content;
|
|
34
|
-
const txt = typeof c === 'string' ? c : Array.isArray(c) ? c.map(x => x?.text || '').join('') : '';
|
|
35
|
-
const call = calls.find(k => k.id === b.tool_use_id); if (call) call.out = txt.length;
|
|
36
|
-
}
|
|
37
|
-
if (ev.type === 'result') result = ev;
|
|
38
|
-
}
|
|
39
|
-
const cg = calls.filter(c => c.name.includes('homegraph'));
|
|
40
|
-
return {
|
|
41
|
-
initCg,
|
|
42
|
-
reads: calls.filter(c => c.name === 'Read').length,
|
|
43
|
-
greps: calls.filter(c => c.name === 'Grep').length + calls.filter(c => c.name === 'Glob').length,
|
|
44
|
-
cgCalls: cg.length,
|
|
45
|
-
cgSeq: cg.map(c => cgShort(c.name)),
|
|
46
|
-
cgOut: cg.reduce((s, c) => s + c.out, 0),
|
|
47
|
-
traceUsed: cg.some(c => c.name.includes('trace')),
|
|
48
|
-
turns: result?.num_turns ?? null,
|
|
49
|
-
dur: result?.duration_ms ? Math.round(result.duration_ms / 1000) : null,
|
|
50
|
-
cost: result?.total_cost_usd || 0,
|
|
51
|
-
ok: result?.subtype === 'success',
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// repo -> arm -> [runs]
|
|
56
|
-
const data = {};
|
|
57
|
-
if (!existsSync(ROOT)) { console.error(`no ${ROOT}`); process.exit(1); }
|
|
58
|
-
for (const repo of readdirSync(ROOT)) {
|
|
59
|
-
const rdir = join(ROOT, repo);
|
|
60
|
-
if (!statSync(rdir).isDirectory()) continue;
|
|
61
|
-
for (const f of readdirSync(rdir)) {
|
|
62
|
-
const m = f.match(/^([A-I])-r(\d+)\.jsonl$/); if (!m) continue;
|
|
63
|
-
const p = parse(join(rdir, f)); if (!p || !p.ok) continue;
|
|
64
|
-
(((data[repo] ??= {})[m[1]]) ??= []).push(p);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const avg = (a, f) => a.length ? a.reduce((s, x) => s + (f(x) || 0), 0) / a.length : 0;
|
|
69
|
-
const k = (n) => (n / 1000).toFixed(1);
|
|
70
|
-
const pad = (s, n) => String(s).padEnd(n);
|
|
71
|
-
const ARMS = ['A', 'H', 'I', 'B', 'F', 'G', 'C', 'D', 'E'];
|
|
72
|
-
const LABEL = { A: 'A all/none(old)', H: 'H body-trace/none', I: 'I bodytrace+dest', B: 'B all/steer(thin)', F: 'F all/steer(body)', G: 'G ported(noprompt)', C: 'C no-explore', D: 'D trace-centric', E: 'E nonflow-probe' };
|
|
73
|
-
|
|
74
|
-
// ---- per repo × arm ----
|
|
75
|
-
console.log('\n=== PER REPO × ARM (avg over runs) ===');
|
|
76
|
-
console.log(pad('repo', 22), pad('arm', 16), 'tools', 'trace', pad('reads', 6), pad('cgOutK', 7), pad('turns', 6), 'dur');
|
|
77
|
-
for (const repo of Object.keys(data).sort()) {
|
|
78
|
-
for (const arm of ARMS) {
|
|
79
|
-
const runs = data[repo][arm]; if (!runs?.length) continue;
|
|
80
|
-
console.log(
|
|
81
|
-
pad(repo, 22), pad(LABEL[arm], 16),
|
|
82
|
-
pad(runs[0].initCg, 5),
|
|
83
|
-
pad(runs.filter(r => r.traceUsed).length + '/' + runs.length, 5),
|
|
84
|
-
pad(avg(runs, r => r.reads).toFixed(1), 6),
|
|
85
|
-
pad(k(avg(runs, r => r.cgOut)), 7),
|
|
86
|
-
pad(avg(runs, r => r.turns).toFixed(1), 6),
|
|
87
|
-
avg(runs, r => r.dur).toFixed(0) + 's',
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// ---- aggregate per arm (flow arms A–D over the flow repos; E shown apart) ----
|
|
93
|
-
console.log('\n=== AGGREGATE PER ARM (mean across repos) ===');
|
|
94
|
-
console.log(pad('arm', 16), pad('adoption', 9), pad('reads', 7), pad('greps', 7), pad('cgOutK', 8), pad('turns', 7), pad('dur', 6), 'cost');
|
|
95
|
-
for (const arm of ARMS) {
|
|
96
|
-
const all = [];
|
|
97
|
-
for (const repo of Object.keys(data)) for (const r of (data[repo][arm] || [])) all.push({ ...r, repo });
|
|
98
|
-
if (!all.length) continue;
|
|
99
|
-
const repos = new Set(all.map(r => r.repo)).size;
|
|
100
|
-
const adopt = all.filter(r => r.traceUsed).length;
|
|
101
|
-
console.log(
|
|
102
|
-
pad(LABEL[arm], 16),
|
|
103
|
-
pad(`${adopt}/${all.length}`, 9),
|
|
104
|
-
pad(avg(all, r => r.reads).toFixed(2), 7),
|
|
105
|
-
pad(avg(all, r => r.greps).toFixed(2), 7),
|
|
106
|
-
pad(k(avg(all, r => r.cgOut)), 8),
|
|
107
|
-
pad(avg(all, r => r.turns).toFixed(1), 7),
|
|
108
|
-
pad(avg(all, r => r.dur).toFixed(0) + 's', 6),
|
|
109
|
-
'$' + avg(all, r => r.cost).toFixed(3),
|
|
110
|
-
` (${repos} repos)`,
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
console.log('\nRead the signal: B vs A = does steering alone fix adoption + cut payload.');
|
|
115
|
-
console.log('C vs B = is explore redundant (reads should NOT jump). D vs C = is context redundant.');
|
|
116
|
-
console.log('E = non-flow under trace-centric; reads SHOULD jump (proves survey tools are load-bearing).');
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Analyze the tool-surface ablation (/tmp/arms/<repo>/<arm>-r<n>.jsonl).
|
|
3
|
+
// Compares arms A–E on trace adoption, Read/Grep fallback, homegraph payload,
|
|
4
|
+
// round-trips, and duration — averaged across runs per arm.
|
|
5
|
+
//
|
|
6
|
+
// The decisive signal is READS: if removing a tool raises Reads on a question
|
|
7
|
+
// class, that tool was load-bearing for it (not redundant). If removing it
|
|
8
|
+
// changes nothing, it was redundant.
|
|
9
|
+
//
|
|
10
|
+
// A control all tools no steering (baseline)
|
|
11
|
+
// B steer all tools trace-first (adoption)
|
|
12
|
+
// C no-explore hide explore trace-first (is explore redundant?)
|
|
13
|
+
// D trace-centric hide explore+context trace-first (is the survey pair redundant?)
|
|
14
|
+
// E control-probe hide explore+context trace-first (NON-flow Q — should degrade)
|
|
15
|
+
//
|
|
16
|
+
// Usage: node scripts/agent-eval/parse-arms.mjs [/tmp/arms]
|
|
17
|
+
import { readFileSync, readdirSync, existsSync, statSync } from 'fs';
|
|
18
|
+
import { join } from 'path';
|
|
19
|
+
|
|
20
|
+
const ROOT = process.argv[2] || '/tmp/arms';
|
|
21
|
+
const cgShort = (n) => n.replace('mcp__homegraph__homegraph_', '').replace('mcp__homegraph__', '');
|
|
22
|
+
|
|
23
|
+
function parse(file) {
|
|
24
|
+
if (!existsSync(file)) return null;
|
|
25
|
+
const lines = readFileSync(file, 'utf8').split('\n').filter(Boolean);
|
|
26
|
+
const calls = []; let result = null, initCg = 0;
|
|
27
|
+
for (const l of lines) {
|
|
28
|
+
let ev; try { ev = JSON.parse(l); } catch { continue; }
|
|
29
|
+
if (ev.type === 'system' && ev.subtype === 'init') initCg = (ev.tools || []).filter(t => /homegraph/.test(t)).length;
|
|
30
|
+
if (ev.type === 'assistant') for (const b of (ev.message?.content || [])) if (b.type === 'tool_use')
|
|
31
|
+
calls.push({ id: b.id, name: b.name, out: 0 });
|
|
32
|
+
if (ev.type === 'user') for (const b of (ev.message?.content || [])) if (b.type === 'tool_result') {
|
|
33
|
+
const c = b.content;
|
|
34
|
+
const txt = typeof c === 'string' ? c : Array.isArray(c) ? c.map(x => x?.text || '').join('') : '';
|
|
35
|
+
const call = calls.find(k => k.id === b.tool_use_id); if (call) call.out = txt.length;
|
|
36
|
+
}
|
|
37
|
+
if (ev.type === 'result') result = ev;
|
|
38
|
+
}
|
|
39
|
+
const cg = calls.filter(c => c.name.includes('homegraph'));
|
|
40
|
+
return {
|
|
41
|
+
initCg,
|
|
42
|
+
reads: calls.filter(c => c.name === 'Read').length,
|
|
43
|
+
greps: calls.filter(c => c.name === 'Grep').length + calls.filter(c => c.name === 'Glob').length,
|
|
44
|
+
cgCalls: cg.length,
|
|
45
|
+
cgSeq: cg.map(c => cgShort(c.name)),
|
|
46
|
+
cgOut: cg.reduce((s, c) => s + c.out, 0),
|
|
47
|
+
traceUsed: cg.some(c => c.name.includes('trace')),
|
|
48
|
+
turns: result?.num_turns ?? null,
|
|
49
|
+
dur: result?.duration_ms ? Math.round(result.duration_ms / 1000) : null,
|
|
50
|
+
cost: result?.total_cost_usd || 0,
|
|
51
|
+
ok: result?.subtype === 'success',
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// repo -> arm -> [runs]
|
|
56
|
+
const data = {};
|
|
57
|
+
if (!existsSync(ROOT)) { console.error(`no ${ROOT}`); process.exit(1); }
|
|
58
|
+
for (const repo of readdirSync(ROOT)) {
|
|
59
|
+
const rdir = join(ROOT, repo);
|
|
60
|
+
if (!statSync(rdir).isDirectory()) continue;
|
|
61
|
+
for (const f of readdirSync(rdir)) {
|
|
62
|
+
const m = f.match(/^([A-I])-r(\d+)\.jsonl$/); if (!m) continue;
|
|
63
|
+
const p = parse(join(rdir, f)); if (!p || !p.ok) continue;
|
|
64
|
+
(((data[repo] ??= {})[m[1]]) ??= []).push(p);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const avg = (a, f) => a.length ? a.reduce((s, x) => s + (f(x) || 0), 0) / a.length : 0;
|
|
69
|
+
const k = (n) => (n / 1000).toFixed(1);
|
|
70
|
+
const pad = (s, n) => String(s).padEnd(n);
|
|
71
|
+
const ARMS = ['A', 'H', 'I', 'B', 'F', 'G', 'C', 'D', 'E'];
|
|
72
|
+
const LABEL = { A: 'A all/none(old)', H: 'H body-trace/none', I: 'I bodytrace+dest', B: 'B all/steer(thin)', F: 'F all/steer(body)', G: 'G ported(noprompt)', C: 'C no-explore', D: 'D trace-centric', E: 'E nonflow-probe' };
|
|
73
|
+
|
|
74
|
+
// ---- per repo × arm ----
|
|
75
|
+
console.log('\n=== PER REPO × ARM (avg over runs) ===');
|
|
76
|
+
console.log(pad('repo', 22), pad('arm', 16), 'tools', 'trace', pad('reads', 6), pad('cgOutK', 7), pad('turns', 6), 'dur');
|
|
77
|
+
for (const repo of Object.keys(data).sort()) {
|
|
78
|
+
for (const arm of ARMS) {
|
|
79
|
+
const runs = data[repo][arm]; if (!runs?.length) continue;
|
|
80
|
+
console.log(
|
|
81
|
+
pad(repo, 22), pad(LABEL[arm], 16),
|
|
82
|
+
pad(runs[0].initCg, 5),
|
|
83
|
+
pad(runs.filter(r => r.traceUsed).length + '/' + runs.length, 5),
|
|
84
|
+
pad(avg(runs, r => r.reads).toFixed(1), 6),
|
|
85
|
+
pad(k(avg(runs, r => r.cgOut)), 7),
|
|
86
|
+
pad(avg(runs, r => r.turns).toFixed(1), 6),
|
|
87
|
+
avg(runs, r => r.dur).toFixed(0) + 's',
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ---- aggregate per arm (flow arms A–D over the flow repos; E shown apart) ----
|
|
93
|
+
console.log('\n=== AGGREGATE PER ARM (mean across repos) ===');
|
|
94
|
+
console.log(pad('arm', 16), pad('adoption', 9), pad('reads', 7), pad('greps', 7), pad('cgOutK', 8), pad('turns', 7), pad('dur', 6), 'cost');
|
|
95
|
+
for (const arm of ARMS) {
|
|
96
|
+
const all = [];
|
|
97
|
+
for (const repo of Object.keys(data)) for (const r of (data[repo][arm] || [])) all.push({ ...r, repo });
|
|
98
|
+
if (!all.length) continue;
|
|
99
|
+
const repos = new Set(all.map(r => r.repo)).size;
|
|
100
|
+
const adopt = all.filter(r => r.traceUsed).length;
|
|
101
|
+
console.log(
|
|
102
|
+
pad(LABEL[arm], 16),
|
|
103
|
+
pad(`${adopt}/${all.length}`, 9),
|
|
104
|
+
pad(avg(all, r => r.reads).toFixed(2), 7),
|
|
105
|
+
pad(avg(all, r => r.greps).toFixed(2), 7),
|
|
106
|
+
pad(k(avg(all, r => r.cgOut)), 8),
|
|
107
|
+
pad(avg(all, r => r.turns).toFixed(1), 7),
|
|
108
|
+
pad(avg(all, r => r.dur).toFixed(0) + 's', 6),
|
|
109
|
+
'$' + avg(all, r => r.cost).toFixed(3),
|
|
110
|
+
` (${repos} repos)`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
console.log('\nRead the signal: B vs A = does steering alone fix adoption + cut payload.');
|
|
115
|
+
console.log('C vs B = is explore redundant (reads should NOT jump). D vs C = is context redundant.');
|
|
116
|
+
console.log('E = non-flow under trace-centric; reads SHOULD jump (proves survey tools are load-bearing).');
|
|
@@ -1,84 +1,84 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Aggregate the README A/B (bench-readme.sh output): per repo, median of N runs
|
|
3
|
-
// per arm → time, tool calls, tokens, cost, and % saved. Plus an average row.
|
|
4
|
-
//
|
|
5
|
-
// Tokens = SUM of per-turn assistant `usage` (input + output + cache read +
|
|
6
|
-
// cache creation) — the cumulative "total tokens processed". NOTE: `result.usage`
|
|
7
|
-
// is last-turn-only in current Claude Code, so it under-counts badly; don't use it.
|
|
8
|
-
// `total_cost_usd` and `duration_ms` are already cumulative.
|
|
9
|
-
//
|
|
10
|
-
// Usage: node parse-bench-readme.mjs [/tmp/ab-readme]
|
|
11
|
-
import { readFileSync, existsSync, readdirSync } from 'fs';
|
|
12
|
-
import { join } from 'path';
|
|
13
|
-
const ROOT = process.argv[2] || '/tmp/ab-readme';
|
|
14
|
-
const REPOS = ['vscode', 'excalidraw', 'django', 'tokio', 'okhttp', 'gin', 'alamofire'];
|
|
15
|
-
|
|
16
|
-
function parse(file) {
|
|
17
|
-
if (!existsSync(file)) return null;
|
|
18
|
-
const L = readFileSync(file, 'utf8').split('\n').filter(Boolean);
|
|
19
|
-
let tools = 0, reads = 0, grep = 0, cg = 0, tokens = 0, r = null, raced = false;
|
|
20
|
-
for (const l of L) { let e; try { e = JSON.parse(l); } catch { continue; }
|
|
21
|
-
if (e.type === 'assistant') {
|
|
22
|
-
const u = e.message?.usage;
|
|
23
|
-
if (u) tokens += (u.input_tokens || 0) + (u.output_tokens || 0) + (u.cache_read_input_tokens || 0) + (u.cache_creation_input_tokens || 0);
|
|
24
|
-
for (const b of (e.message?.content || [])) if (b.type === 'tool_use') {
|
|
25
|
-
const n = b.name;
|
|
26
|
-
if (n === 'ToolSearch') continue;
|
|
27
|
-
tools++;
|
|
28
|
-
if (n === 'Read') reads++;
|
|
29
|
-
else if (n === 'Grep' || n === 'Glob') grep++;
|
|
30
|
-
else if (/homegraph/.test(n)) cg++;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
// MCP cold-start race: the headless agent fired before `homegraph serve --mcp`
|
|
34
|
-
// finished registering its tools, so early calls returned "No such tool
|
|
35
|
-
// available" and the agent floundered into grep/Read. That measures HomeGraph's
|
|
36
|
-
// startup latency, NOT its steady-state value — flag the run so the aggregate
|
|
37
|
-
// can exclude it (an artifact of headless first-turn timing, not the tool).
|
|
38
|
-
if (e.type === 'user') for (const b of (Array.isArray(e.message?.content) ? e.message.content : [])) {
|
|
39
|
-
if (b.type === 'tool_result') {
|
|
40
|
-
const t = Array.isArray(b.content) ? b.content.map(c => c.text || '').join('') : (b.content || '');
|
|
41
|
-
if (/No such tool available/.test(t)) raced = true;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
if (e.type === 'result') r = e;
|
|
45
|
-
}
|
|
46
|
-
if (!r || r.subtype !== 'success') return null;
|
|
47
|
-
return { dur: r.duration_ms / 1000, tools, reads, grep, cg, tokens, cost: r.total_cost_usd || 0, raced };
|
|
48
|
-
}
|
|
49
|
-
const median = (arr) => { const v = [...arr].sort((a, b) => a - b); const n = v.length; return n === 0 ? 0 : n % 2 ? v[(n - 1) / 2] : (v[n / 2 - 1] + v[n / 2]) / 2; };
|
|
50
|
-
const fmtTime = (s) => s >= 60 ? `${Math.floor(s / 60)}m ${Math.round(s % 60)}s` : `${Math.round(s)}s`;
|
|
51
|
-
const fmtTok = (t) => t >= 1e6 ? `${(t / 1e6).toFixed(1)}M` : `${Math.round(t / 1000)}k`;
|
|
52
|
-
const pct = (w, wo) => wo > 0 ? Math.round((1 - w / wo) * 100) : 0;
|
|
53
|
-
|
|
54
|
-
console.log('repo n(w/wo) time WITH→WITHOUT tools W→WO tokens W→WO (saved) cost W→WO (saved)');
|
|
55
|
-
const savings = { cost: [], tokens: [], time: [], tools: [] };
|
|
56
|
-
for (const repo of REPOS) {
|
|
57
|
-
const dir = join(ROOT, repo);
|
|
58
|
-
const runDirs = existsSync(dir) ? readdirSync(dir).filter(d => /^run\d+$/.test(d)) : [];
|
|
59
|
-
// Exclude MCP-cold-start-raced WITH runs by default — they measure a startup
|
|
60
|
-
// race, not steady-state value. `CG_INCLUDE_RACED=1` keeps them (to see the raw
|
|
61
|
-
// distribution). The WITHOUT arm has no MCP, so it's never raced.
|
|
62
|
-
const includeRaced = process.env.CG_INCLUDE_RACED === '1';
|
|
63
|
-
const W = [], WO = []; let racedExcluded = 0;
|
|
64
|
-
for (const rd of runDirs) {
|
|
65
|
-
const w = parse(join(dir, rd, 'run-headless-with.jsonl'));
|
|
66
|
-
if (w) { if (w.raced && !includeRaced) racedExcluded++; else W.push(w); }
|
|
67
|
-
const wo = parse(join(dir, rd, 'run-headless-without.jsonl')); if (wo) WO.push(wo);
|
|
68
|
-
}
|
|
69
|
-
if (!W.length || !WO.length) { console.log(`${repo.padEnd(11)} (incomplete: w=${W.length} wo=${WO.length})`); continue; }
|
|
70
|
-
const m = (arr, k) => median(arr.map(x => x[k]));
|
|
71
|
-
const wT = m(W, 'dur'), woT = m(WO, 'dur'), wTok = m(W, 'tokens'), woTok = m(WO, 'tokens');
|
|
72
|
-
const wC = m(W, 'cost'), woC = m(WO, 'cost'), wTl = m(W, 'tools'), woTl = m(WO, 'tools');
|
|
73
|
-
savings.time.push(pct(wT, woT)); savings.tokens.push(pct(wTok, woTok)); savings.cost.push(pct(wC, woC)); savings.tools.push(pct(wTl, woTl));
|
|
74
|
-
console.log(
|
|
75
|
-
`${repo.padEnd(11)} ${W.length}/${WO.length} ` +
|
|
76
|
-
`${(fmtTime(wT) + '→' + fmtTime(woT)).padEnd(22)}` +
|
|
77
|
-
`${(Math.round(wTl) + '→' + Math.round(woTl)).padEnd(12)}` +
|
|
78
|
-
`${(fmtTok(wTok) + '→' + fmtTok(woTok) + ' (' + pct(wTok, woTok) + '%)').padEnd(24)}` +
|
|
79
|
-
`$${wC.toFixed(2)}→$${woC.toFixed(2)} (${pct(wC, woC)}%)` +
|
|
80
|
-
(racedExcluded ? ` [${racedExcluded} raced run${racedExcluded === 1 ? '' : 's'} excluded]` : '')
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
const avg = (a) => a.length ? Math.round(a.reduce((s, x) => s + x, 0) / a.length) : 0;
|
|
84
|
-
console.log(`\nAVERAGE saved: cost ${avg(savings.cost)}% · tokens ${avg(savings.tokens)}% · time ${avg(savings.time)}% · tool calls ${avg(savings.tools)}%`);
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Aggregate the README A/B (bench-readme.sh output): per repo, median of N runs
|
|
3
|
+
// per arm → time, tool calls, tokens, cost, and % saved. Plus an average row.
|
|
4
|
+
//
|
|
5
|
+
// Tokens = SUM of per-turn assistant `usage` (input + output + cache read +
|
|
6
|
+
// cache creation) — the cumulative "total tokens processed". NOTE: `result.usage`
|
|
7
|
+
// is last-turn-only in current Claude Code, so it under-counts badly; don't use it.
|
|
8
|
+
// `total_cost_usd` and `duration_ms` are already cumulative.
|
|
9
|
+
//
|
|
10
|
+
// Usage: node parse-bench-readme.mjs [/tmp/ab-readme]
|
|
11
|
+
import { readFileSync, existsSync, readdirSync } from 'fs';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
const ROOT = process.argv[2] || '/tmp/ab-readme';
|
|
14
|
+
const REPOS = ['vscode', 'excalidraw', 'django', 'tokio', 'okhttp', 'gin', 'alamofire'];
|
|
15
|
+
|
|
16
|
+
function parse(file) {
|
|
17
|
+
if (!existsSync(file)) return null;
|
|
18
|
+
const L = readFileSync(file, 'utf8').split('\n').filter(Boolean);
|
|
19
|
+
let tools = 0, reads = 0, grep = 0, cg = 0, tokens = 0, r = null, raced = false;
|
|
20
|
+
for (const l of L) { let e; try { e = JSON.parse(l); } catch { continue; }
|
|
21
|
+
if (e.type === 'assistant') {
|
|
22
|
+
const u = e.message?.usage;
|
|
23
|
+
if (u) tokens += (u.input_tokens || 0) + (u.output_tokens || 0) + (u.cache_read_input_tokens || 0) + (u.cache_creation_input_tokens || 0);
|
|
24
|
+
for (const b of (e.message?.content || [])) if (b.type === 'tool_use') {
|
|
25
|
+
const n = b.name;
|
|
26
|
+
if (n === 'ToolSearch') continue;
|
|
27
|
+
tools++;
|
|
28
|
+
if (n === 'Read') reads++;
|
|
29
|
+
else if (n === 'Grep' || n === 'Glob') grep++;
|
|
30
|
+
else if (/homegraph/.test(n)) cg++;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// MCP cold-start race: the headless agent fired before `homegraph serve --mcp`
|
|
34
|
+
// finished registering its tools, so early calls returned "No such tool
|
|
35
|
+
// available" and the agent floundered into grep/Read. That measures HomeGraph's
|
|
36
|
+
// startup latency, NOT its steady-state value — flag the run so the aggregate
|
|
37
|
+
// can exclude it (an artifact of headless first-turn timing, not the tool).
|
|
38
|
+
if (e.type === 'user') for (const b of (Array.isArray(e.message?.content) ? e.message.content : [])) {
|
|
39
|
+
if (b.type === 'tool_result') {
|
|
40
|
+
const t = Array.isArray(b.content) ? b.content.map(c => c.text || '').join('') : (b.content || '');
|
|
41
|
+
if (/No such tool available/.test(t)) raced = true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (e.type === 'result') r = e;
|
|
45
|
+
}
|
|
46
|
+
if (!r || r.subtype !== 'success') return null;
|
|
47
|
+
return { dur: r.duration_ms / 1000, tools, reads, grep, cg, tokens, cost: r.total_cost_usd || 0, raced };
|
|
48
|
+
}
|
|
49
|
+
const median = (arr) => { const v = [...arr].sort((a, b) => a - b); const n = v.length; return n === 0 ? 0 : n % 2 ? v[(n - 1) / 2] : (v[n / 2 - 1] + v[n / 2]) / 2; };
|
|
50
|
+
const fmtTime = (s) => s >= 60 ? `${Math.floor(s / 60)}m ${Math.round(s % 60)}s` : `${Math.round(s)}s`;
|
|
51
|
+
const fmtTok = (t) => t >= 1e6 ? `${(t / 1e6).toFixed(1)}M` : `${Math.round(t / 1000)}k`;
|
|
52
|
+
const pct = (w, wo) => wo > 0 ? Math.round((1 - w / wo) * 100) : 0;
|
|
53
|
+
|
|
54
|
+
console.log('repo n(w/wo) time WITH→WITHOUT tools W→WO tokens W→WO (saved) cost W→WO (saved)');
|
|
55
|
+
const savings = { cost: [], tokens: [], time: [], tools: [] };
|
|
56
|
+
for (const repo of REPOS) {
|
|
57
|
+
const dir = join(ROOT, repo);
|
|
58
|
+
const runDirs = existsSync(dir) ? readdirSync(dir).filter(d => /^run\d+$/.test(d)) : [];
|
|
59
|
+
// Exclude MCP-cold-start-raced WITH runs by default — they measure a startup
|
|
60
|
+
// race, not steady-state value. `CG_INCLUDE_RACED=1` keeps them (to see the raw
|
|
61
|
+
// distribution). The WITHOUT arm has no MCP, so it's never raced.
|
|
62
|
+
const includeRaced = process.env.CG_INCLUDE_RACED === '1';
|
|
63
|
+
const W = [], WO = []; let racedExcluded = 0;
|
|
64
|
+
for (const rd of runDirs) {
|
|
65
|
+
const w = parse(join(dir, rd, 'run-headless-with.jsonl'));
|
|
66
|
+
if (w) { if (w.raced && !includeRaced) racedExcluded++; else W.push(w); }
|
|
67
|
+
const wo = parse(join(dir, rd, 'run-headless-without.jsonl')); if (wo) WO.push(wo);
|
|
68
|
+
}
|
|
69
|
+
if (!W.length || !WO.length) { console.log(`${repo.padEnd(11)} (incomplete: w=${W.length} wo=${WO.length})`); continue; }
|
|
70
|
+
const m = (arr, k) => median(arr.map(x => x[k]));
|
|
71
|
+
const wT = m(W, 'dur'), woT = m(WO, 'dur'), wTok = m(W, 'tokens'), woTok = m(WO, 'tokens');
|
|
72
|
+
const wC = m(W, 'cost'), woC = m(WO, 'cost'), wTl = m(W, 'tools'), woTl = m(WO, 'tools');
|
|
73
|
+
savings.time.push(pct(wT, woT)); savings.tokens.push(pct(wTok, woTok)); savings.cost.push(pct(wC, woC)); savings.tools.push(pct(wTl, woTl));
|
|
74
|
+
console.log(
|
|
75
|
+
`${repo.padEnd(11)} ${W.length}/${WO.length} ` +
|
|
76
|
+
`${(fmtTime(wT) + '→' + fmtTime(woT)).padEnd(22)}` +
|
|
77
|
+
`${(Math.round(wTl) + '→' + Math.round(woTl)).padEnd(12)}` +
|
|
78
|
+
`${(fmtTok(wTok) + '→' + fmtTok(woTok) + ' (' + pct(wTok, woTok) + '%)').padEnd(24)}` +
|
|
79
|
+
`$${wC.toFixed(2)}→$${woC.toFixed(2)} (${pct(wC, woC)}%)` +
|
|
80
|
+
(racedExcluded ? ` [${racedExcluded} raced run${racedExcluded === 1 ? '' : 's'} excluded]` : '')
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
const avg = (a) => a.length ? Math.round(a.reduce((s, x) => s + x, 0) / a.length) : 0;
|
|
84
|
+
console.log(`\nAVERAGE saved: cost ${avg(savings.cost)}% · tokens ${avg(savings.tokens)}% · time ${avg(savings.time)}% · tool calls ${avg(savings.tools)}%`);
|
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Parse a Claude Code stream-json run log: tool-call sequence + token usage.
|
|
3
|
-
import { readFileSync } from 'fs';
|
|
4
|
-
const file = process.argv[2];
|
|
5
|
-
const lines = readFileSync(file, 'utf8').split('\n').filter(Boolean);
|
|
6
|
-
|
|
7
|
-
const toolCalls = [];
|
|
8
|
-
let result = null;
|
|
9
|
-
let initTools = null;
|
|
10
|
-
|
|
11
|
-
for (const line of lines) {
|
|
12
|
-
let ev;
|
|
13
|
-
try { ev = JSON.parse(line); } catch { continue; }
|
|
14
|
-
if (ev.type === 'system' && ev.subtype === 'init') {
|
|
15
|
-
initTools = (ev.tools || []).filter(t => /homegraph/.test(t));
|
|
16
|
-
}
|
|
17
|
-
if (ev.type === 'assistant' && ev.message?.content) {
|
|
18
|
-
for (const block of ev.message.content) {
|
|
19
|
-
if (block.type === 'tool_use') {
|
|
20
|
-
let detail = '';
|
|
21
|
-
if (block.name === 'Task') detail = ` [subagent_type=${block.input?.subagent_type ?? '?'}] ${(block.input?.description ?? '').slice(0,40)}`;
|
|
22
|
-
else if (/homegraph/.test(block.name)) detail = ` ${JSON.stringify(block.input?.query ?? block.input?.task ?? block.input?.symbol ?? '').slice(0,60)}`;
|
|
23
|
-
else if (block.name === 'Bash') detail = ` ${(block.input?.command ?? '').slice(0,50)}`;
|
|
24
|
-
else if (block.name === 'Read') detail = ` ${(block.input?.file_path ?? '').split('/').slice(-1)[0]}`;
|
|
25
|
-
toolCalls.push(`${block.name}${detail}`);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
if (ev.type === 'result') result = ev;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
console.log(`\n=== ${file.split('/').pop()} ===`);
|
|
33
|
-
console.log(`homegraph tools exposed: ${initTools ? initTools.length : '?'}`);
|
|
34
|
-
console.log(`\nTool calls (${toolCalls.length}):`);
|
|
35
|
-
const counts = {};
|
|
36
|
-
for (const tc of toolCalls) { const n = tc.split(' ')[0]; counts[n] = (counts[n]||0)+1; }
|
|
37
|
-
console.log(' by type:', JSON.stringify(counts));
|
|
38
|
-
toolCalls.forEach((tc, i) => console.log(` ${i+1}. ${tc}`));
|
|
39
|
-
|
|
40
|
-
if (result) {
|
|
41
|
-
const u = result.usage || {};
|
|
42
|
-
const totalIn = (u.input_tokens||0) + (u.cache_read_input_tokens||0) + (u.cache_creation_input_tokens||0);
|
|
43
|
-
console.log(`\nResult: ${result.subtype} | duration ${(result.duration_ms/1000).toFixed(0)}s | turns ${result.num_turns}`);
|
|
44
|
-
console.log(` tokens: in=${totalIn} out=${u.output_tokens||0} | cost $${(result.total_cost_usd||0).toFixed(3)}`);
|
|
45
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Parse a Claude Code stream-json run log: tool-call sequence + token usage.
|
|
3
|
+
import { readFileSync } from 'fs';
|
|
4
|
+
const file = process.argv[2];
|
|
5
|
+
const lines = readFileSync(file, 'utf8').split('\n').filter(Boolean);
|
|
6
|
+
|
|
7
|
+
const toolCalls = [];
|
|
8
|
+
let result = null;
|
|
9
|
+
let initTools = null;
|
|
10
|
+
|
|
11
|
+
for (const line of lines) {
|
|
12
|
+
let ev;
|
|
13
|
+
try { ev = JSON.parse(line); } catch { continue; }
|
|
14
|
+
if (ev.type === 'system' && ev.subtype === 'init') {
|
|
15
|
+
initTools = (ev.tools || []).filter(t => /homegraph/.test(t));
|
|
16
|
+
}
|
|
17
|
+
if (ev.type === 'assistant' && ev.message?.content) {
|
|
18
|
+
for (const block of ev.message.content) {
|
|
19
|
+
if (block.type === 'tool_use') {
|
|
20
|
+
let detail = '';
|
|
21
|
+
if (block.name === 'Task') detail = ` [subagent_type=${block.input?.subagent_type ?? '?'}] ${(block.input?.description ?? '').slice(0,40)}`;
|
|
22
|
+
else if (/homegraph/.test(block.name)) detail = ` ${JSON.stringify(block.input?.query ?? block.input?.task ?? block.input?.symbol ?? '').slice(0,60)}`;
|
|
23
|
+
else if (block.name === 'Bash') detail = ` ${(block.input?.command ?? '').slice(0,50)}`;
|
|
24
|
+
else if (block.name === 'Read') detail = ` ${(block.input?.file_path ?? '').split('/').slice(-1)[0]}`;
|
|
25
|
+
toolCalls.push(`${block.name}${detail}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (ev.type === 'result') result = ev;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.log(`\n=== ${file.split('/').pop()} ===`);
|
|
33
|
+
console.log(`homegraph tools exposed: ${initTools ? initTools.length : '?'}`);
|
|
34
|
+
console.log(`\nTool calls (${toolCalls.length}):`);
|
|
35
|
+
const counts = {};
|
|
36
|
+
for (const tc of toolCalls) { const n = tc.split(' ')[0]; counts[n] = (counts[n]||0)+1; }
|
|
37
|
+
console.log(' by type:', JSON.stringify(counts));
|
|
38
|
+
toolCalls.forEach((tc, i) => console.log(` ${i+1}. ${tc}`));
|
|
39
|
+
|
|
40
|
+
if (result) {
|
|
41
|
+
const u = result.usage || {};
|
|
42
|
+
const totalIn = (u.input_tokens||0) + (u.cache_read_input_tokens||0) + (u.cache_creation_input_tokens||0);
|
|
43
|
+
console.log(`\nResult: ${result.subtype} | duration ${(result.duration_ms/1000).toFixed(0)}s | turns ${result.num_turns}`);
|
|
44
|
+
console.log(` tokens: in=${totalIn} out=${u.output_tokens||0} | cost $${(result.total_cost_usd||0).toFixed(3)}`);
|
|
45
|
+
}
|