archgraph-argo 0.25.0 → 0.26.0
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.
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* Agent search diagnosis (framework, zero-dependency).
|
|
4
|
+
*
|
|
5
|
+
* Turns ONE agent session into a self-contained diagnostic bundle so the user
|
|
6
|
+
* can hand it back for analysis:
|
|
7
|
+
*
|
|
8
|
+
* <workspace>/.argo/temp/diagnosis/<session-id>/
|
|
9
|
+
* ├─ diagnosis.md human-readable summary (incl. heuristic hints)
|
|
10
|
+
* ├─ metrics.json the numbers behind the summary
|
|
11
|
+
* ├─ session.ndjson the raw host session (full tool input/output)
|
|
12
|
+
* ├─ cost-log.slice.ndjson the matching slice of the background cost log
|
|
13
|
+
* └─ manifest.json file list + sizes + versions
|
|
14
|
+
*
|
|
15
|
+
* Data sources (user's choice: both):
|
|
16
|
+
* - PRIMARY: the host session NDJSON (opencode export / run --format json),
|
|
17
|
+
* which carries every tool's full input/output + timestamps + tokens.
|
|
18
|
+
* - SUPPLEMENT: the framework cost log <workspace>/.argo/temp/agent-cost-log.ndjson.
|
|
19
|
+
*
|
|
20
|
+
* Usage:
|
|
21
|
+
* node agentSearchDiagnose.js --session <session.ndjson> [--session-id ID]
|
|
22
|
+
* [--cost-log <path>] [--workspace <root>] [--out <dir>] [--json]
|
|
23
|
+
*
|
|
24
|
+
* Read-only except for writing the bundle dir. Never mutates the graph or repo.
|
|
25
|
+
*/
|
|
26
|
+
const fs = require('node:fs');
|
|
27
|
+
const path = require('node:path');
|
|
28
|
+
const crypto = require('node:crypto');
|
|
29
|
+
|
|
30
|
+
const BUNDLE_VERSION = 'temp2.2';
|
|
31
|
+
const SCHEMA_VERSION = 1;
|
|
32
|
+
const GRAPH_TOOLS = ['getSystemArchitecture', 'getIntentElementContext', 'getArchitectureViewContext', 'queryNeo4jGraph', 'memory_search'];
|
|
33
|
+
const GRAPH_WRITE_TOOLS = ['previewSystemArchitectureMutation', 'applySystemArchitectureMutation', 'addArchitectureElement', 'updateArchitectureElement', 'removeArchitectureElement', 'addArchitectureRelationship', 'updateArchitectureRelationship', 'removeArchitectureRelationship', 'addArchitectureView', 'updateArchitectureView', 'removeArchitectureView'];
|
|
34
|
+
const VALIDATOR_TOOLS = ['validateSystemArchitecture', 'runArchitectureTests', 'initializeWorkspace'];
|
|
35
|
+
const REPO_TOOLS = ['read', 'grep', 'glob', 'list', 'bash', 'webfetch', 'edit', 'write'];
|
|
36
|
+
|
|
37
|
+
function backendOf(tool) {
|
|
38
|
+
const n = String(tool || '');
|
|
39
|
+
const hit = (l) => l.some(t => n.includes(t));
|
|
40
|
+
if (hit(GRAPH_WRITE_TOOLS)) return 'graph';
|
|
41
|
+
if (hit(GRAPH_TOOLS)) return 'graph';
|
|
42
|
+
if (hit(VALIDATOR_TOOLS)) return 'framework';
|
|
43
|
+
if (hit(REPO_TOOLS)) return 'repo';
|
|
44
|
+
return 'other';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function classifyQuery(tool, input) {
|
|
48
|
+
const n = String(tool || '');
|
|
49
|
+
if (n.includes('getSystemArchitecture') || n.includes('memory_search')) return 'semantic';
|
|
50
|
+
if (n.includes('queryNeo4jGraph')) return 'structured-cypher';
|
|
51
|
+
if (n.includes('getIntentElementContext') || n.includes('getArchitectureViewContext')) return 'structured-context';
|
|
52
|
+
if (n.includes('read')) return 'file-read';
|
|
53
|
+
if (n.includes('grep')) return 'grep';
|
|
54
|
+
if (n.includes('glob') || n.includes('list')) return 'list';
|
|
55
|
+
if (GRAPH_WRITE_TOOLS.some(t => n.includes(t))) return 'write';
|
|
56
|
+
return 'other';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function estimateTokens(text) {
|
|
60
|
+
if (!text) return 0;
|
|
61
|
+
const s = String(text);
|
|
62
|
+
const cjk = (s.match(/[\u4e00-\u9fff\u3400-\u4dbf\u3000-\u303f\uff00-\uffef]/g) || []).length;
|
|
63
|
+
return cjk + Math.ceil((s.length - cjk) / 4);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function stableStringify(value) {
|
|
67
|
+
if (value === null || typeof value !== 'object') return JSON.stringify(value);
|
|
68
|
+
if (Array.isArray(value)) return '[' + value.map(stableStringify).join(',') + ']';
|
|
69
|
+
return '{' + Object.keys(value).sort().map(k => JSON.stringify(k) + ':' + stableStringify(value[k])).join(',') + '}';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function signature(tool, input) {
|
|
73
|
+
try { return crypto.createHash('sha1').update(String(tool) + '|' + stableStringify(input || {})).digest('hex').slice(0, 12); }
|
|
74
|
+
catch (_) { return 'unknown'; }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function inputPreview(tool, input) {
|
|
78
|
+
if (!input || typeof input !== 'object') return '';
|
|
79
|
+
const pick = input.query && (input.query.intent || input.query) || input.pattern || input.elementId || input.elementName || input.view_id || input.path || input.filePath || input.cypher || input.intent;
|
|
80
|
+
const s = typeof pick === 'string' ? pick : (pick ? JSON.stringify(pick) : JSON.stringify(input));
|
|
81
|
+
return String(s).slice(0, 160);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function eventTime(e) {
|
|
85
|
+
const cands = [e.time, e.part && e.part.time, e.part && e.part.state && e.part.state.time];
|
|
86
|
+
for (const t of cands) {
|
|
87
|
+
if (!t) continue;
|
|
88
|
+
if (typeof t === 'number') return t;
|
|
89
|
+
if (typeof t.start === 'number' && typeof t.end === 'number') return t.end;
|
|
90
|
+
if (typeof t.start === 'number') return t.start;
|
|
91
|
+
if (typeof t.end === 'number') return t.end;
|
|
92
|
+
}
|
|
93
|
+
return typeof e.timestamp === 'number' ? e.timestamp : null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function parseSession(text) {
|
|
97
|
+
const raw = String(text || '');
|
|
98
|
+
const toolCalls = [];
|
|
99
|
+
const usages = [];
|
|
100
|
+
let steps = 0; let tMin = null; let tMax = null; let cost = 0;
|
|
101
|
+
const texts = [];
|
|
102
|
+
for (const line of raw.split('\n')) {
|
|
103
|
+
const t = line.trim();
|
|
104
|
+
if (!t.startsWith('{')) continue;
|
|
105
|
+
let e; try { e = JSON.parse(t); } catch (_) { continue; }
|
|
106
|
+
const part = e.part || e;
|
|
107
|
+
if (part && part.type === 'text' && part.text) texts.push(part.text);
|
|
108
|
+
if (part && part.type === 'tool') {
|
|
109
|
+
const tool = part.tool || part.name || (part.state && part.state.tool) || 'tool';
|
|
110
|
+
const st = part.state || {};
|
|
111
|
+
const tm = st.time || {};
|
|
112
|
+
const out = st.output != null ? String(st.output) : '';
|
|
113
|
+
toolCalls.push({
|
|
114
|
+
tool, backend: backendOf(tool), queryClass: classifyQuery(tool, st.input),
|
|
115
|
+
durationMs: (typeof tm.start === 'number' && typeof tm.end === 'number') ? Math.max(0, tm.end - tm.start) : null,
|
|
116
|
+
ok: st.status ? st.status !== 'error' : true,
|
|
117
|
+
error: st.error ? String(st.error).slice(0, 200) : null,
|
|
118
|
+
signature: signature(tool, st.input), inputPreview: inputPreview(tool, st.input),
|
|
119
|
+
outputBytes: out.length, outputTokens: estimateTokens(out),
|
|
120
|
+
path: (st.input && (st.input.filePath || st.input.path || st.input.file)) || null,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
if (e.type === 'step_start') steps += 1;
|
|
124
|
+
if (e.type === 'step_finish') {
|
|
125
|
+
const fin = (e.part && e.part.finish) ? e.part.finish : (e.part || e);
|
|
126
|
+
if (fin && typeof fin.cost === 'number') cost += fin.cost;
|
|
127
|
+
const tk = fin && (fin.tokens || (fin.finish && fin.finish.tokens));
|
|
128
|
+
if (tk) usages.push({ input: tk.input || 0, output: tk.output || 0, reasoning: tk.reasoning || 0, cacheRead: (tk.cache && tk.cache.read) || 0, cacheWrite: (tk.cache && tk.cache.write) || 0 });
|
|
129
|
+
}
|
|
130
|
+
const time = eventTime(e);
|
|
131
|
+
if (time !== null) { tMin = tMin === null ? time : Math.min(tMin, time); tMax = tMax === null ? time : Math.max(tMax, time); }
|
|
132
|
+
}
|
|
133
|
+
const tokensIn = usages.reduce((a, u) => a + u.input, 0);
|
|
134
|
+
const tokensOut = usages.reduce((a, u) => a + u.output, 0);
|
|
135
|
+
const tokensReasoning = usages.reduce((a, u) => a + u.reasoning, 0);
|
|
136
|
+
return { toolCalls, usages, texts, steps, cost, wallMs: (tMin !== null && tMax !== null && tMax >= tMin) ? tMax - tMin : null, tokensIn, tokensOut, tokensReasoning, tokens: tokensIn + tokensOut + tokensReasoning };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function countRoundTrips(toolCalls) {
|
|
140
|
+
const seq = toolCalls.map(c => c.backend).filter(b => b === 'graph' || b === 'repo');
|
|
141
|
+
let n = 0;
|
|
142
|
+
for (let i = 1; i < seq.length; i++) if (seq[i] !== seq[i - 1]) n += 1;
|
|
143
|
+
return n;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function diagnose(session, opts = {}) {
|
|
147
|
+
const tc = session.toolCalls;
|
|
148
|
+
const byTool = {};
|
|
149
|
+
const byBackend = { graph: { calls: 0, ms: 0 }, repo: { calls: 0, ms: 0 }, framework: { calls: 0, ms: 0 }, other: { calls: 0, ms: 0 } };
|
|
150
|
+
const byQueryClass = {};
|
|
151
|
+
const sigCount = {};
|
|
152
|
+
const pathReads = {};
|
|
153
|
+
let toolMs = 0; let errors = 0; let empty = 0;
|
|
154
|
+
for (const c of tc) {
|
|
155
|
+
const b = byBackend[c.backend] || (byBackend[c.backend] = { calls: 0, ms: 0 });
|
|
156
|
+
b.calls += 1; b.ms += c.durationMs || 0;
|
|
157
|
+
toolMs += c.durationMs || 0;
|
|
158
|
+
const bt = byTool[c.tool] || (byTool[c.tool] = { calls: 0, ms: 0, tokens: 0, errors: 0 });
|
|
159
|
+
bt.calls += 1; bt.ms += c.durationMs || 0; bt.tokens += c.outputTokens || 0;
|
|
160
|
+
if (c.ok === false) { bt.errors += 1; errors += 1; }
|
|
161
|
+
if ((c.outputBytes || 0) < 2) empty += 1;
|
|
162
|
+
byQueryClass[c.queryClass] = (byQueryClass[c.queryClass] || 0) + 1;
|
|
163
|
+
sigCount[c.signature] = (sigCount[c.signature] || 0) + 1;
|
|
164
|
+
if (c.queryClass === 'file-read' && c.path) pathReads[c.path] = (pathReads[c.path] || 0) + 1;
|
|
165
|
+
}
|
|
166
|
+
const duplicates = [];
|
|
167
|
+
const seen = new Set();
|
|
168
|
+
for (const c of tc) {
|
|
169
|
+
if (seen.has(c.signature)) continue;
|
|
170
|
+
seen.add(c.signature);
|
|
171
|
+
if (sigCount[c.signature] > 1) duplicates.push({ tool: c.tool, queryClass: c.queryClass, count: sigCount[c.signature], preview: c.inputPreview });
|
|
172
|
+
}
|
|
173
|
+
const repeatedReads = Object.entries(pathReads).filter(([, n]) => n > 1).map(([p, n]) => ({ path: p, count: n }));
|
|
174
|
+
// longest no-progress streak: consecutive calls that were empty/failed/duplicate
|
|
175
|
+
let streak = 0; let best = 0;
|
|
176
|
+
for (const c of tc) {
|
|
177
|
+
const bad = c.ok === false || (c.outputBytes || 0) < 2 || sigCount[c.signature] > 1;
|
|
178
|
+
streak = bad ? streak + 1 : 0;
|
|
179
|
+
if (streak > best) best = streak;
|
|
180
|
+
}
|
|
181
|
+
const cumulativeInput = [];
|
|
182
|
+
let acc = 0;
|
|
183
|
+
for (const u of session.usages) { acc += u.input; cumulativeInput.push(u.input); }
|
|
184
|
+
const topByMs = [...tc].sort((a, b) => (b.durationMs || 0) - (a.durationMs || 0)).slice(0, 5).map(c => ({ tool: c.tool, ms: c.durationMs, ok: c.ok, preview: c.inputPreview }));
|
|
185
|
+
const topByTokens = [...tc].sort((a, b) => (b.outputTokens || 0) - (a.outputTokens || 0)).slice(0, 5).map(c => ({ tool: c.tool, tokens: c.outputTokens, preview: c.inputPreview }));
|
|
186
|
+
|
|
187
|
+
const wallMs = opts.wallMs != null ? opts.wallMs : session.wallMs;
|
|
188
|
+
const modelMs = wallMs != null ? Math.max(0, wallMs - toolMs) : null;
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
schemaVersion: SCHEMA_VERSION, bundleVersion: BUNDLE_VERSION, generatedAt: new Date().toISOString(),
|
|
192
|
+
workspace: opts.workspace || null, sessionId: opts.sessionId || null,
|
|
193
|
+
overview: {
|
|
194
|
+
steps: session.steps, toolCalls: tc.length, roundTrips: countRoundTrips(tc),
|
|
195
|
+
wallMs, modelMs, mcpMs: byBackend.graph.ms, repoToolMs: byBackend.repo.ms, toolMs,
|
|
196
|
+
tokensIn: session.tokensIn, tokensOut: session.tokensOut, tokensReasoning: session.tokensReasoning, tokens: session.tokens,
|
|
197
|
+
cost: session.cost, toolErrors: errors, emptyResults: empty,
|
|
198
|
+
distinctSignatures: new Set(tc.map(c => c.signature)).size,
|
|
199
|
+
},
|
|
200
|
+
byTool, byBackend, byQueryClass,
|
|
201
|
+
tokenGrowth: { perStepInput: cumulativeInput, steps: cumulativeInput.length },
|
|
202
|
+
overSearch: {
|
|
203
|
+
duplicateCalls: duplicates,
|
|
204
|
+
repeatedReads,
|
|
205
|
+
emptyOrError: errors + empty,
|
|
206
|
+
noProgressStreak: best,
|
|
207
|
+
},
|
|
208
|
+
topOffendersByTime: topByMs,
|
|
209
|
+
topOffendersByTokens: topByTokens,
|
|
210
|
+
hints: buildHints({ toolCalls: tc.length, duplicates: duplicates.length, emptyOrError: errors + empty, repeatedReads: repeatedReads.length, noProgressStreak: best, modelMs, mcpMs: byBackend.graph.ms, repoToolMs: byBackend.repo.ms, roundTrips: countRoundTrips(tc) }),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function buildHints(m) {
|
|
215
|
+
const hints = [];
|
|
216
|
+
if (m.duplicates > 0) hints.push(`重复/近似重复调用 ${m.duplicates} 组 → 考虑结果缓存或查询归一(同参不重搜)。`);
|
|
217
|
+
if (m.emptyOrError > 0) hints.push(`空结果/失败 ${m.emptyOrError} 次 → 考虑改进查询构造/回退策略,避免"空手→换词→再搜"的循环。`);
|
|
218
|
+
if (m.repeatedReads > 0) hints.push(`同一文件被重复读 ${m.repeatedReads} 处 → 考虑读取缓存或先摘要后精读。`);
|
|
219
|
+
if (m.noProgressStreak >= 3) hints.push(`最长"无进展"连续 ${m.noProgressStreak} 次 → 缺停止判据/预算,建议显式设停止条件。`);
|
|
220
|
+
if (m.roundTrips >= 2) hints.push(`图↔仓往返 ${m.roundTrips} 次 → 检查是否可在一次规划内并发取数。`);
|
|
221
|
+
if (m.modelMs != null && m.modelMs > (m.mcpMs + m.repoToolMs)) hints.push(`模型耗时主导(${m.modelMs}ms > 工具 ${(m.mcpMs + m.repoToolMs)}ms)→ 考虑上下文压缩/减少轮次。`);
|
|
222
|
+
if (hints.length === 0) hints.push('未发现明显过度搜索信号。');
|
|
223
|
+
return hints;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function renderDiagnosis(m, meta) {
|
|
227
|
+
const L = [];
|
|
228
|
+
L.push(`# Agent 搜索诊断报告`);
|
|
229
|
+
L.push('');
|
|
230
|
+
L.push(`- 生成时间:${m.generatedAt}`);
|
|
231
|
+
L.push(`- 会话:${m.sessionId || '(unknown)'} 工作区:${m.workspace || '(unknown)'}`);
|
|
232
|
+
L.push(`- bundle 版本:${m.bundleVersion} schema:${m.schemaVersion}`);
|
|
233
|
+
L.push('');
|
|
234
|
+
L.push(`## 概览`);
|
|
235
|
+
L.push('');
|
|
236
|
+
L.push(`| 指标 | 值 |`);
|
|
237
|
+
L.push(`|---|---|`);
|
|
238
|
+
L.push(`| 轮次 steps | ${m.overview.steps} |`);
|
|
239
|
+
L.push(`| 工具调用 | ${m.overview.toolCalls}(去重签名 ${m.overview.distinctSignatures}) |`);
|
|
240
|
+
L.push(`| 图↔仓往返 | ${m.overview.roundTrips} |`);
|
|
241
|
+
L.push(`| 墙钟 | ${fmtMs(m.overview.wallMs)} = 模型 ${fmtMs(m.overview.modelMs)} + MCP ${fmtMs(m.overview.mcpMs)} + 仓 ${fmtMs(m.overview.repoToolMs)} |`);
|
|
242
|
+
L.push(`| tokens | 总 ${m.overview.tokens}(in ${m.overview.tokensIn} / out ${m.overview.tokensOut} / reason ${m.overview.tokensReasoning}) |`);
|
|
243
|
+
L.push(`| 工具失败 / 空结果 | ${m.overview.toolErrors} / ${m.overview.emptyResults} |`);
|
|
244
|
+
L.push('');
|
|
245
|
+
L.push(`## 过度搜索信号`);
|
|
246
|
+
L.push('');
|
|
247
|
+
L.push(`- 重复调用组:${m.overSearch.duplicateCalls.length}`);
|
|
248
|
+
for (const d of m.overSearch.duplicateCalls.slice(0, 10)) L.push(` - ×${d.count} ${d.tool} [${d.queryClass}] ${d.preview}`);
|
|
249
|
+
L.push(`- 重复读同一文件:${m.overSearch.repeatedReads.length}`);
|
|
250
|
+
for (const r of m.overSearch.repeatedReads.slice(0, 10)) L.push(` - ×${r.count} ${r.path}`);
|
|
251
|
+
L.push(`- 空结果/失败合计:${m.overSearch.emptyOrError}`);
|
|
252
|
+
L.push(`- 最长无进展连续:${m.overSearch.noProgressStreak}`);
|
|
253
|
+
L.push('');
|
|
254
|
+
L.push(`## 后端 / 查询类分布`);
|
|
255
|
+
L.push('');
|
|
256
|
+
L.push(`- 后端:` + Object.entries(m.byBackend).map(([k, v]) => `${k}=${v.calls}`).join(' '));
|
|
257
|
+
L.push(`- 查询类:` + Object.entries(m.byQueryClass).map(([k, v]) => `${k}=${v}`).join(' '));
|
|
258
|
+
L.push('');
|
|
259
|
+
L.push(`## 最贵调用(按耗时)`);
|
|
260
|
+
L.push('');
|
|
261
|
+
for (const c of m.topOffendersByTime) L.push(`- ${fmtMs(c.ms)} ${c.tool}${c.ok ? '' : ' (error)'} ${c.preview}`);
|
|
262
|
+
L.push('');
|
|
263
|
+
L.push(`## token 随轮次`);
|
|
264
|
+
L.push('');
|
|
265
|
+
L.push(`每步 input tokens(累计上下文规模):${m.tokenGrowth.perStepInput.join(', ') || '(无)'}`);
|
|
266
|
+
L.push('');
|
|
267
|
+
L.push(`## 诊断建议(启发式,供 Agent 复核)`);
|
|
268
|
+
L.push('');
|
|
269
|
+
for (const h of m.hints) L.push(`- ${h}`);
|
|
270
|
+
L.push('');
|
|
271
|
+
L.push(`---`);
|
|
272
|
+
L.push(`> 本报告由 agentSearchDiagnose 自动生成;请 Agent 结合 metrics.json 与会话片段复核并补充解读。`);
|
|
273
|
+
L.push('');
|
|
274
|
+
return L.join('\n');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function fmtMs(ms) {
|
|
278
|
+
if (ms == null) return 'n/a';
|
|
279
|
+
return ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${ms}ms`;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function writeBundle(opts) {
|
|
283
|
+
const workspace = opts.workspace || process.cwd();
|
|
284
|
+
const sessionText = fs.readFileSync(opts.session, 'utf8');
|
|
285
|
+
const session = parseSession(sessionText);
|
|
286
|
+
const sessionId = opts.sessionId || inferSessionId(sessionText) || 'session';
|
|
287
|
+
const outDir = opts.out || path.join(workspace, '.argo', 'temp', 'diagnosis', sessionId);
|
|
288
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
289
|
+
|
|
290
|
+
const costLogPath = opts.costLog || path.join(workspace, '.argo', 'temp', 'agent-cost-log.ndjson');
|
|
291
|
+
const costLogText = readMaybe(costLogPath);
|
|
292
|
+
|
|
293
|
+
const metrics = diagnose(session, { workspace, sessionId, wallMs: opts.wallMs });
|
|
294
|
+
const files = [];
|
|
295
|
+
const write = (name, body) => { const p = path.join(outDir, name); fs.writeFileSync(p, body); files.push({ path: name, bytes: Buffer.byteLength(body) }); };
|
|
296
|
+
|
|
297
|
+
write('session.ndjson', sessionText);
|
|
298
|
+
write('cost-log.slice.ndjson', sliceCostLog(costLogText, sessionId));
|
|
299
|
+
write('metrics.json', JSON.stringify(metrics, null, 2) + '\n');
|
|
300
|
+
write('diagnosis.md', renderDiagnosis(metrics, { costLogPath }));
|
|
301
|
+
|
|
302
|
+
const manifest = {
|
|
303
|
+
schemaVersion: SCHEMA_VERSION, bundleVersion: BUNDLE_VERSION, generatedAt: metrics.generatedAt,
|
|
304
|
+
workspace, sessionId, sources: { session: path.resolve(opts.session), costLog: costLogPath },
|
|
305
|
+
files,
|
|
306
|
+
};
|
|
307
|
+
fs.writeFileSync(path.join(outDir, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n');
|
|
308
|
+
return { outDir, metrics, manifest };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function readMaybe(file) { try { return fs.readFileSync(file, 'utf8'); } catch (_) { return ''; } }
|
|
312
|
+
|
|
313
|
+
function inferSessionId(text) {
|
|
314
|
+
const m = String(text).match(/"sessionID"\s*:\s*"([^"]+)"/);
|
|
315
|
+
return m ? m[1] : null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function sliceCostLog(text, sessionId) {
|
|
319
|
+
if (!text) return '';
|
|
320
|
+
const lines = text.split('\n').filter(Boolean);
|
|
321
|
+
const hit = lines.filter(l => !sessionId || l.includes(sessionId));
|
|
322
|
+
return (hit.length ? hit : []).join('\n') + (hit.length ? '\n' : '');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function parseArgs(argv) {
|
|
326
|
+
const a = { session: null, sessionId: null, costLog: null, workspace: process.cwd(), out: null, json: false };
|
|
327
|
+
for (let i = 0; i < argv.length; i++) {
|
|
328
|
+
const k = argv[i];
|
|
329
|
+
if (k === '--session') a.session = argv[++i];
|
|
330
|
+
else if (k === '--session-id') a.sessionId = argv[++i];
|
|
331
|
+
else if (k === '--cost-log') a.costLog = argv[++i];
|
|
332
|
+
else if (k === '--workspace') a.workspace = argv[++i];
|
|
333
|
+
else if (k === '--out') a.out = argv[++i];
|
|
334
|
+
else if (k === '--json') a.json = true;
|
|
335
|
+
}
|
|
336
|
+
return a;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function main(argv) {
|
|
340
|
+
const args = parseArgs(argv || process.argv.slice(2));
|
|
341
|
+
if (!args.session || !fs.existsSync(args.session)) {
|
|
342
|
+
console.error('agentSearchDiagnose: --session <session.ndjson> is required');
|
|
343
|
+
return 2;
|
|
344
|
+
}
|
|
345
|
+
const { outDir, metrics, manifest } = writeBundle(args);
|
|
346
|
+
if (args.json) { console.log(JSON.stringify({ outDir, overview: metrics.overview, hints: metrics.hints }, null, 2)); return 0; }
|
|
347
|
+
console.log(`diagnosis bundle -> ${outDir}`);
|
|
348
|
+
console.log(`files: ${manifest.files.map(f => f.path).join(', ')}`);
|
|
349
|
+
console.log(`overview: steps=${metrics.overview.steps} tools=${metrics.overview.toolCalls} roundTrips=${metrics.overview.roundTrips} tokens=${metrics.overview.tokens} model=${fmtMs(metrics.overview.modelMs)} mcp=${fmtMs(metrics.overview.mcpMs)} repo=${fmtMs(metrics.overview.repoToolMs)}`);
|
|
350
|
+
for (const h of metrics.hints) console.log(`hint: ${h}`);
|
|
351
|
+
return 0;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
module.exports = { BUNDLE_VERSION, SCHEMA_VERSION, parseSession, classifyQuery, backendOf, signature, diagnose, renderDiagnosis, writeBundle, sliceCostLog, main };
|
|
355
|
+
|
|
356
|
+
if (require.main === module) process.exit(main());
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agent-search-diagnosis
|
|
3
|
+
description: "诊断一次 Agent 会话的检索/搜索行为(是否过度搜索、轮次/token/耗时花在哪、是否重复或空手、图↔仓往返),产出一份自包含的『诊断包』(diagnosis.md 总结 + metrics.json + 会话 NDJSON + 插件日志切片 + manifest.json),供人类伙伴打包回传以便进一步分析优化。Use when the user says an agent session searched too many rounds / wants to diagnose retrieval cost or over-search, or asks to produce a diagnostic bundle to send back. Keywords: 过度搜索, 诊断, over-search, search diagnosis, retrieval cost, diagnostic bundle, agent-search-diagnosis."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Agent Search Diagnosis
|
|
7
|
+
|
|
8
|
+
把**一次 Agent 会话**变成一份自包含的**诊断包**:现场出诊断总结 + 聚合关键数据到一个目录,用户打包回传即可。
|
|
9
|
+
|
|
10
|
+
## 数据来源
|
|
11
|
+
|
|
12
|
+
- **主**:宿主会话 NDJSON(含每个工具的**完整入参/输出**、时间戳、tokens)。
|
|
13
|
+
- **辅**:框架后台日志 `<workspace>/.argo/temp/agent-cost-log.ndjson`(跨会话成本形态)。
|
|
14
|
+
|
|
15
|
+
## 输出(固定目录)
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
<workspace>/.argo/temp/diagnosis/<session-id>/
|
|
19
|
+
├─ diagnosis.md # 人读诊断总结(含启发式建议,供 Agent 复核)
|
|
20
|
+
├─ metrics.json # 总结背后的数字
|
|
21
|
+
├─ session.ndjson # 原始会话(全保真)
|
|
22
|
+
├─ cost-log.slice.ndjson # 匹配该会话的后台日志切片
|
|
23
|
+
└─ manifest.json # 文件清单 + 大小 + 版本
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Workflow
|
|
27
|
+
|
|
28
|
+
### 1. 定位并导出会话
|
|
29
|
+
|
|
30
|
+
- 若用户给出了 sessionID:`opencode export <sessionID> > <tmp>.ndjson`。
|
|
31
|
+
- 若未给出:`opencode session list` 取最近会话,或请用户指定。
|
|
32
|
+
- 宿主不支持导出时:退化为**仅用后台日志**(`agent-cost-log.ndjson`,诊断能力受限,须在报告中注明)。
|
|
33
|
+
|
|
34
|
+
### 2. 运行诊断脚本(确定性)
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
node ~/.argo/scripts/agentSearchDiagnose.js --session <session.ndjson> \
|
|
38
|
+
[--session-id <id>] [--workspace .] [--cost-log <path>] [--json]
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
脚本**只读**会话与日志,写出上面的 bundle 目录,输出概览与启发式 hints。
|
|
42
|
+
|
|
43
|
+
### 3. 复核并补充诊断总结(Agent)
|
|
44
|
+
|
|
45
|
+
- 读 `diagnosis.md` 与 `metrics.json`。
|
|
46
|
+
- 在报告末尾追加一节 `## Agent 复核解读`:结合会话片段指出**最可能的过度搜索根因**与**建议的优化方向**(去重/缓存、查询构造、停止判据、上下文压缩、图↔仓往返合并等)。
|
|
47
|
+
- 不臆造:结论必须能从 metrics/会话片段找到依据。
|
|
48
|
+
|
|
49
|
+
### 4. 交付给用户
|
|
50
|
+
|
|
51
|
+
- 报告 bundle 目录路径,请用户**打包该目录**回传。
|
|
52
|
+
- 给一段简短结论(轮次/token/耗时拆分 + 主要信号)。
|
|
53
|
+
|
|
54
|
+
## Rules(MUST / MUST NOT)
|
|
55
|
+
|
|
56
|
+
- **MUST** 只用确定性脚本 + 会话/日志读取;**MUST NOT** 修改图谱、仓库或任何框架内容。
|
|
57
|
+
- **MUST NOT** 读取或打印 `.env`/凭据;若会话片段疑似含密钥,**MUST** 提醒用户回传前先审查 `session.ndjson`(其中是原始工具 I/O)。
|
|
58
|
+
- **MUST** 在结论中区分「事实(metrics)」与「推断(根因假设)」,推断须标注依据。
|
|
59
|
+
- 若无法导出会话或脚本不可用,**MUST** 如实说明并给出退化方案,不得臆测诊断结果。
|
package/install-argo.ps1
CHANGED
|
@@ -788,8 +788,11 @@ $skillDest = Join-Path $SkillsRoot 'argo-init'
|
|
|
788
788
|
Write-Host "[4/22] argo\skills\argo-init -> $skillDest"
|
|
789
789
|
Copy-Tree -Source $skillSrc -Destination $skillDest
|
|
790
790
|
$reconcileSkillSrc = Join-Path (Join-Path $argoDir 'skills') 'ea-human-reconcile'
|
|
791
|
+
$diagSkillSrc = Join-Path (Join-Path $argoDir 'skills') 'agent-search-diagnosis'
|
|
791
792
|
Write-Host ' argo\skills\ea-human-reconcile -> $SkillsRoot\ea-human-reconcile (EA human draft reconcile skill)'
|
|
792
793
|
Copy-Tree -Source $reconcileSkillSrc -Destination (Join-Path $SkillsRoot 'ea-human-reconcile')
|
|
794
|
+
Write-Host ' argo\skills\agent-search-diagnosis -> $SkillsRoot\agent-search-diagnosis (agent search diagnosis skill)'
|
|
795
|
+
Copy-Tree -Source $diagSkillSrc -Destination (Join-Path $SkillsRoot 'agent-search-diagnosis')
|
|
793
796
|
|
|
794
797
|
$ruleSrc = Join-Path (Join-Path $argoDir 'rules') 'archgraph.instructions.md'
|
|
795
798
|
$ruleDest = Join-Path $PromptsRoot 'archgraph.instructions.md'
|
|
@@ -807,6 +810,8 @@ Write-Host "[7/22] argo\skills\argo-init -> $cursorSkillDest (Cursor)"
|
|
|
807
810
|
Copy-Tree -Source $skillSrc -Destination $cursorSkillDest
|
|
808
811
|
Write-Host ' argo\skills\ea-human-reconcile -> $CursorSkillsRoot\ea-human-reconcile (Cursor)'
|
|
809
812
|
Copy-Tree -Source $reconcileSkillSrc -Destination (Join-Path $CursorSkillsRoot 'ea-human-reconcile')
|
|
813
|
+
Write-Host ' argo\skills\agent-search-diagnosis -> $CursorSkillsRoot\agent-search-diagnosis (Cursor)'
|
|
814
|
+
Copy-Tree -Source $diagSkillSrc -Destination (Join-Path $CursorSkillsRoot 'agent-search-diagnosis')
|
|
810
815
|
|
|
811
816
|
$mcpBridgeSrc = Join-Path $argoDir 'mcp-bridges'
|
|
812
817
|
$mcpBridgeDest = Join-Path $CursorMcpBridgesRoot ''
|
|
@@ -818,6 +823,8 @@ Write-Host "[8/22] argo\skills\argo-init -> $openCodeSkillDest (OpenCode)"
|
|
|
818
823
|
Copy-Tree -Source $skillSrc -Destination $openCodeSkillDest
|
|
819
824
|
Write-Host ' argo\skills\ea-human-reconcile -> $OpenCodeSkillsRoot\ea-human-reconcile (OpenCode)'
|
|
820
825
|
Copy-Tree -Source $reconcileSkillSrc -Destination (Join-Path $OpenCodeSkillsRoot 'ea-human-reconcile')
|
|
826
|
+
Write-Host ' argo\skills\agent-search-diagnosis -> $OpenCodeSkillsRoot\agent-search-diagnosis (OpenCode)'
|
|
827
|
+
Copy-Tree -Source $diagSkillSrc -Destination (Join-Path $OpenCodeSkillsRoot 'agent-search-diagnosis')
|
|
821
828
|
|
|
822
829
|
Write-Host "[9/22] argo\rules\archgraph.instructions.md -> $OpenCodeAgentsPath (OpenCode global AGENTS.md)"
|
|
823
830
|
Add-AgentsRule -AgentsPath $OpenCodeAgentsPath -RulePath $ruleSrc
|
|
@@ -857,6 +864,8 @@ if ($SkipDsh) {
|
|
|
857
864
|
Copy-Tree -Source (Join-Path $argoDir 'skills\argo-init') -Destination $dshSkillDest
|
|
858
865
|
Write-Host ' argo\skills\ea-human-reconcile -> $DshHome\skills\ea-human-reconcile (DeepSeek Harness skill)'
|
|
859
866
|
Copy-Tree -Source (Join-Path $argoDir 'skills\ea-human-reconcile') -Destination (Join-Path (Join-Path $DshHome 'skills') 'ea-human-reconcile')
|
|
867
|
+
Write-Host ' argo\skills\agent-search-diagnosis -> $DshHome\skills\agent-search-diagnosis (DeepSeek Harness skill)'
|
|
868
|
+
Copy-Tree -Source $diagSkillSrc -Destination (Join-Path (Join-Path $DshHome 'skills') 'agent-search-diagnosis')
|
|
860
869
|
|
|
861
870
|
Write-Host "[17/22] argo\rules\<WakeupGuideline> -> $DshHome\plugins\dsh-argo-wakeup\index.js (DeepSeek Harness wakeup plugin)"
|
|
862
871
|
$wakeupDshPath = New-DshWakeupPlugin -DshHome $DshHome -RuleText $ruleSrcContent
|
|
@@ -918,8 +927,10 @@ if ($SkipOpenClaw) {
|
|
|
918
927
|
|
|
919
928
|
Write-Host "[21/22] argo\skills\argo-init -> $openClawSkillDest (OpenClaw managed skill, all agents)"
|
|
920
929
|
Copy-Tree -Source (Join-Path $argoDir 'skills\argo-init') -Destination $openClawSkillDest
|
|
921
|
-
|
|
922
|
-
|
|
930
|
+
Write-Host ' argo\skills\ea-human-reconcile -> $OpenClawHome\skills\ea-human-reconcile (OpenClaw managed skill, all agents)'
|
|
931
|
+
Copy-Tree -Source (Join-Path $argoDir 'skills\ea-human-reconcile') -Destination (Join-Path (Join-Path $OpenClawHome 'skills') 'ea-human-reconcile')
|
|
932
|
+
Write-Host ' argo\skills\agent-search-diagnosis -> $OpenClawHome\skills\agent-search-diagnosis (OpenClaw managed skill, all agents)'
|
|
933
|
+
Copy-Tree -Source $diagSkillSrc -Destination (Join-Path (Join-Path $OpenClawHome 'skills') 'agent-search-diagnosis')
|
|
923
934
|
|
|
924
935
|
Write-Host ' OpenClaw injects AGENTS.md into Project Context on every session, so the wakeup'
|
|
925
936
|
Write-Host ' gate (UNCONDITIONAL STARTUP GATE) is active on the next OpenClaw session; restart'
|
package/package.json
CHANGED
|
@@ -1,52 +1,53 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "archgraph-argo",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Deploy the ArchGraph ARGO toolchain, skills, and rules (schema, scripts, argo-init skill, global rule) with one command.",
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"bin": {
|
|
7
|
-
"argo-deploy": "bin/argo-deploy.js"
|
|
8
|
-
},
|
|
9
|
-
"files": [
|
|
10
|
-
"argo/scripts",
|
|
11
|
-
"argo/schema",
|
|
12
|
-
"argo/defaults",
|
|
13
|
-
"argo/agents",
|
|
14
|
-
"argo/plugins",
|
|
15
|
-
"argo/mcp-bridges",
|
|
16
|
-
"argo/skills/argo-init",
|
|
17
|
-
"argo/skills/ea-human-reconcile",
|
|
18
|
-
"argo/
|
|
19
|
-
"argo/
|
|
20
|
-
"argo
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"dsh-argo-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
"./dsh-argo-
|
|
31
|
-
"./
|
|
32
|
-
"./
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "archgraph-argo",
|
|
3
|
+
"version": "0.26.0",
|
|
4
|
+
"description": "Deploy the ArchGraph ARGO toolchain, skills, and rules (schema, scripts, argo-init skill, global rule) with one command.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"bin": {
|
|
7
|
+
"argo-deploy": "bin/argo-deploy.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"argo/scripts",
|
|
11
|
+
"argo/schema",
|
|
12
|
+
"argo/defaults",
|
|
13
|
+
"argo/agents",
|
|
14
|
+
"argo/plugins",
|
|
15
|
+
"argo/mcp-bridges",
|
|
16
|
+
"argo/skills/argo-init",
|
|
17
|
+
"argo/skills/ea-human-reconcile",
|
|
18
|
+
"argo/skills/agent-search-diagnosis",
|
|
19
|
+
"argo/rules",
|
|
20
|
+
"argo/package.json",
|
|
21
|
+
"argo/.env.example",
|
|
22
|
+
"vendor",
|
|
23
|
+
"install-argo.ps1",
|
|
24
|
+
"bin",
|
|
25
|
+
"cordis.patch.yml",
|
|
26
|
+
"dsh-argo-workspace",
|
|
27
|
+
"dsh-argo-wakeup"
|
|
28
|
+
],
|
|
29
|
+
"exports": {
|
|
30
|
+
"./dsh-argo-workspace": "./dsh-argo-workspace/index.js",
|
|
31
|
+
"./dsh-argo-wakeup": "./dsh-argo-wakeup/index.js",
|
|
32
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
33
|
+
"./package.json": "./package.json"
|
|
34
|
+
},
|
|
35
|
+
"dsh": {
|
|
36
|
+
"bundle": {
|
|
37
|
+
"patch": "./cordis.patch.yml"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"neo4j-driver": "^6.2.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@resvg/resvg-js": "^2.6.2",
|
|
45
|
+
"playwright": "1.61.1"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"test": "node --test \"tests/*.test.js\""
|
|
49
|
+
},
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=18"
|
|
52
|
+
}
|
|
53
|
+
}
|