blun-king-cli 9.1.80 → 9.1.82
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/LIESMICH.txt
CHANGED
|
@@ -9,7 +9,7 @@ Installation
|
|
|
9
9
|
------------
|
|
10
10
|
Die geprüfte Version exakt global installieren:
|
|
11
11
|
|
|
12
|
-
npm install -g blun-king-cli@9.1.
|
|
12
|
+
npm install -g blun-king-cli@9.1.82
|
|
13
13
|
|
|
14
14
|
Start
|
|
15
15
|
-----
|
|
@@ -94,6 +94,20 @@ Das Telemetrieereignis `tool_result_offloaded` enthält ausschließlich `tool_na
|
|
|
94
94
|
|
|
95
95
|
Reguläre `Agent`-Teilaufgaben verwenden eine eigene `ContextMemory` und eine eigene `wire.jsonl` in einem getrennten Agentenverzeichnis. Der Hauptagent erhält nur die Ergebniszusammenfassung, sodass lange Teilaufgaben nicht den Hauptverlauf füllen. `TodoList` speichert Pläne weiterhin maschinenlesbar im Sitzungs-Wire; `blun handoff` übergibt sie zusammen mit prüfbaren Hashes zwischen CLI, Desktop und Web.
|
|
96
96
|
|
|
97
|
+
Rein lesende Sitzungsdiagnose
|
|
98
|
+
--------------------------------
|
|
99
|
+
|
|
100
|
+
Der mitgelieferte Standardskill `blun-session-inspector` untersucht den nur ergänzten Sitzungs-Wire und die zugehörigen Telemetriedateien, ohne eine Sitzung zu verändern. Er meldet Modellschritte, Werkzeugaufrufe, Zähler zum Tokenverbrauch, Voll- und Mikroverdichtungen, fehlgeschlagene Verdichtungen sowie ausgelagerte Werkzeugergebnisse. Prompts, Systemanweisungen, Werkzeugargumente, Werkzeugergebnisse, private Pfade und verborgenes Denken erscheinen nicht in der Standardausgabe.
|
|
101
|
+
|
|
102
|
+
Zusätzlich liest der Inspektor das sitzungseigene BLUN-Protokoll und meldet, wie viele Werkzeugschemata verfügbar, ausgewählt und zurückgestellt waren, wie viele geschätzte Schema-Token vermieden wurden und warum die Werkzeugmenge reduziert wurde. Andere Protokollinhalte werden weder ausgegeben noch ausgewertet.
|
|
103
|
+
|
|
104
|
+
Beispiele:
|
|
105
|
+
|
|
106
|
+
node "$SKILL_DIR/scripts/inspect-session.cjs" --list 20
|
|
107
|
+
node "$SKILL_DIR/scripts/inspect-session.cjs" SESSION_ID
|
|
108
|
+
|
|
109
|
+
Ein eindeutiger Präfix der Sitzungs-ID genügt. `--profile NAME` wählt ein anderes Profil. `--include-metadata` zeigt zusätzlich Arbeits- und Sitzungsverzeichnis und sollte nur verwendet werden, wenn diese Pfade wirklich gebraucht werden. Der Inspektor setzt eine Sitzung niemals fort, lädt sie nicht neu, verdichtet sie nicht und löscht sie nicht.
|
|
110
|
+
|
|
97
111
|
Zug-Wächter
|
|
98
112
|
-----------
|
|
99
113
|
Läuft ein Zug 20 Minuten ohne neues Werkzeugergebnis, meldet die Konsole den
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ Voraussetzung ist Node.js 24.15 oder neuer. Die geprüfte Version wird exakt
|
|
|
9
9
|
installiert:
|
|
10
10
|
|
|
11
11
|
```powershell
|
|
12
|
-
npm install -g blun-king-cli@9.1.
|
|
12
|
+
npm install -g blun-king-cli@9.1.82
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
## Reproduzierbares Staging und Packen
|
|
@@ -116,6 +116,21 @@ Das Telemetrieereignis `tool_result_offloaded` enthält ausschließlich `tool_na
|
|
|
116
116
|
|
|
117
117
|
Reguläre `Agent`-Teilaufgaben verwenden eine eigene `ContextMemory` und eine eigene `wire.jsonl` in einem getrennten Agentenverzeichnis. Der Hauptagent erhält nur die Ergebniszusammenfassung, sodass lange Teilaufgaben nicht den Hauptverlauf füllen. `TodoList` speichert Pläne weiterhin maschinenlesbar im Sitzungs-Wire; `blun handoff` übergibt sie zusammen mit prüfbaren Hashes zwischen CLI, Desktop und Web.
|
|
118
118
|
|
|
119
|
+
## Rein lesende Sitzungsdiagnose
|
|
120
|
+
|
|
121
|
+
Der mitgelieferte Standardskill `blun-session-inspector` untersucht den nur ergänzten Sitzungs-Wire und die zugehörigen Telemetriedateien, ohne eine Sitzung zu verändern. Er meldet Modellschritte, Werkzeugaufrufe, Zähler zum Tokenverbrauch, Voll- und Mikroverdichtungen, fehlgeschlagene Verdichtungen sowie ausgelagerte Werkzeugergebnisse. Prompts, Systemanweisungen, Werkzeugargumente, Werkzeugergebnisse, private Pfade und verborgenes Denken erscheinen nicht in der Standardausgabe.
|
|
122
|
+
|
|
123
|
+
Zusätzlich liest der Inspektor das sitzungseigene BLUN-Protokoll und meldet, wie viele Werkzeugschemata verfügbar, ausgewählt und zurückgestellt waren, wie viele geschätzte Schema-Token vermieden wurden und warum die Werkzeugmenge reduziert wurde. Andere Protokollinhalte werden weder ausgegeben noch ausgewertet.
|
|
124
|
+
|
|
125
|
+
Beispiele:
|
|
126
|
+
|
|
127
|
+
```text
|
|
128
|
+
node "$SKILL_DIR/scripts/inspect-session.cjs" --list 20
|
|
129
|
+
node "$SKILL_DIR/scripts/inspect-session.cjs" SESSION_ID
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Ein eindeutiger Präfix der Sitzungs-ID genügt. `--profile NAME` wählt ein anderes Profil. `--include-metadata` zeigt zusätzlich Arbeits- und Sitzungsverzeichnis und sollte nur verwendet werden, wenn diese Pfade wirklich gebraucht werden. Der Inspektor setzt eine Sitzung niemals fort, lädt sie nicht neu, verdichtet sie nicht und löscht sie nicht.
|
|
133
|
+
|
|
119
134
|
## Zug-Wächter
|
|
120
135
|
|
|
121
136
|
Läuft ein Zug 20 Minuten ohne neues Werkzeugergebnis, meldet die Konsole den
|
package/package.json
CHANGED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: blun-session-inspector
|
|
3
|
+
description: Inspect local BLUN Code session health read-only. Use when asked about compactions, token usage, tool-call volume, offloaded tool results, provider steps, or a session that appears slow or stuck.
|
|
4
|
+
license: MIT
|
|
5
|
+
compatibility: designed for BLUN Code
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# BLUN Session Inspector
|
|
9
|
+
|
|
10
|
+
Use the bundled script instead of manually reading `wire.jsonl` or telemetry files. It reports counts and timing metadata without returning prompts, system instructions, tool arguments, tool results, private paths, or hidden reasoning.
|
|
11
|
+
|
|
12
|
+
In addition to wire and telemetry counts, the summary reads only structured `turn request tools` fields from the session's own BLUN log. It reports eligible, selected, and deferred schema counts, estimated schema tokens avoided, and suppression reasons. Other log lines are ignored and never emitted.
|
|
13
|
+
|
|
14
|
+
Resolve `SKILL_DIR` to the directory containing this file. Do not assume a user-specific installation path.
|
|
15
|
+
|
|
16
|
+
List recent sessions:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
node "$SKILL_DIR/scripts/inspect-session.cjs" --list 20
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Inspect one session by full ID or unique prefix:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
node "$SKILL_DIR/scripts/inspect-session.cjs" SESSION_ID
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Use another profile or app home only when needed:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
node "$SKILL_DIR/scripts/inspect-session.cjs" SESSION_ID --profile PROFILE
|
|
32
|
+
node "$SKILL_DIR/scripts/inspect-session.cjs" SESSION_ID --blun-home PATH
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Add `--include-metadata` only when the work directory and session directory are relevant. Summarize the JSON instead of pasting it verbatim. Distinguish stored counts from your interpretation, mention warnings, and do not claim that telemetry covers periods before an event was introduced.
|
|
36
|
+
|
|
37
|
+
Keep inspection read-only. Never edit, delete, compact, resume, reload, or restart a session as part of diagnosis.
|
|
38
|
+
|
|
39
|
+
This skill adopts the read-only, smallest-useful-view approach from the MIT-licensed Deep Agents Thread Inspector while using BLUN Code's append-only wire and telemetry formats.
|
|
40
|
+
|
|
41
|
+
Source: https://github.com/langchain-ai/deepagents/tree/main/libs/code/deepagents_code/built_in_skills/deepagents-thread-inspector
|
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const os = require('node:os');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const readline = require('node:readline');
|
|
8
|
+
|
|
9
|
+
function fail(code, details) {
|
|
10
|
+
process.stderr.write(`${JSON.stringify({ error: code, ...details })}\n`);
|
|
11
|
+
process.exitCode = 1;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function parseArgs(argv) {
|
|
15
|
+
const options = {
|
|
16
|
+
blunHome: process.env.BLUN_HOME || path.join(os.homedir(), '.blun'),
|
|
17
|
+
profile: 'default',
|
|
18
|
+
list: null,
|
|
19
|
+
includeMetadata: false,
|
|
20
|
+
selector: null,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
24
|
+
const argument = argv[index];
|
|
25
|
+
if (argument === '--blun-home') {
|
|
26
|
+
options.blunHome = argv[index += 1];
|
|
27
|
+
} else if (argument === '--profile') {
|
|
28
|
+
options.profile = argv[index += 1];
|
|
29
|
+
} else if (argument === '--list') {
|
|
30
|
+
const raw = argv[index + 1];
|
|
31
|
+
if (raw && !raw.startsWith('--')) index += 1;
|
|
32
|
+
options.list = raw && !raw.startsWith('--') ? Number(raw) : 20;
|
|
33
|
+
} else if (argument === '--include-metadata') {
|
|
34
|
+
options.includeMetadata = true;
|
|
35
|
+
} else if (argument.startsWith('--')) {
|
|
36
|
+
throw new Error(`unknown_option:${argument}`);
|
|
37
|
+
} else if (options.selector === null) {
|
|
38
|
+
options.selector = argument;
|
|
39
|
+
} else {
|
|
40
|
+
throw new Error('too_many_session_selectors');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!options.blunHome) throw new Error('missing_blun_home');
|
|
45
|
+
if (!/^[a-z0-9][a-z0-9_-]{0,31}$/u.test(options.profile)) throw new Error('invalid_profile');
|
|
46
|
+
if (options.list !== null && (!Number.isInteger(options.list) || options.list < 1 || options.list > 1000)) {
|
|
47
|
+
throw new Error('invalid_list_limit');
|
|
48
|
+
}
|
|
49
|
+
if (options.list === null && !options.selector) throw new Error('missing_session_selector');
|
|
50
|
+
return options;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function readJsonLines(filePath, onRecord, onWarning) {
|
|
54
|
+
const input = fs.createReadStream(filePath, { encoding: 'utf8' });
|
|
55
|
+
const lines = readline.createInterface({ input, crlfDelay: Infinity });
|
|
56
|
+
let lineNumber = 0;
|
|
57
|
+
for await (const line of lines) {
|
|
58
|
+
lineNumber += 1;
|
|
59
|
+
if (!line.trim()) continue;
|
|
60
|
+
try {
|
|
61
|
+
const record = JSON.parse(line);
|
|
62
|
+
if (!record || typeof record !== 'object' || Array.isArray(record)) {
|
|
63
|
+
onWarning({ code: 'unsupported_record', line: lineNumber });
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
await onRecord(record, lineNumber);
|
|
67
|
+
} catch {
|
|
68
|
+
onWarning({ code: 'invalid_json', line: lineNumber });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function loadSessionIndex(profileRoot) {
|
|
74
|
+
const indexPath = path.join(profileRoot, 'session_index.jsonl');
|
|
75
|
+
if (!fs.existsSync(indexPath)) throw new Error('session_index_not_found');
|
|
76
|
+
const sessions = new Map();
|
|
77
|
+
const warnings = [];
|
|
78
|
+
await readJsonLines(indexPath, (entry) => {
|
|
79
|
+
if (typeof entry.sessionId !== 'string' || typeof entry.sessionDir !== 'string') {
|
|
80
|
+
warnings.push({ scope: 'session_index', code: 'invalid_session_entry' });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
sessions.set(entry.sessionId, entry);
|
|
84
|
+
}, (warning) => warnings.push({ scope: 'session_index', ...warning }));
|
|
85
|
+
return { sessions: [...sessions.values()], warnings };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function sessionModifiedTime(sessionDir) {
|
|
89
|
+
try {
|
|
90
|
+
return fs.statSync(sessionDir).mtime.toISOString();
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isPathInside(parent, candidate) {
|
|
97
|
+
const relative = path.relative(path.resolve(parent), path.resolve(candidate));
|
|
98
|
+
return relative !== ''
|
|
99
|
+
&& relative !== '..'
|
|
100
|
+
&& !relative.startsWith(`..${path.sep}`)
|
|
101
|
+
&& !path.isAbsolute(relative);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function requireSafeSessionPath(profileRoot, sessionDir) {
|
|
105
|
+
if (!isPathInside(path.join(profileRoot, 'sessions'), sessionDir)) {
|
|
106
|
+
throw new Error('unsafe_session_path');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function agentWireFiles(sessionDir) {
|
|
111
|
+
const agentsDir = path.join(sessionDir, 'agents');
|
|
112
|
+
if (!fs.existsSync(agentsDir)) return [];
|
|
113
|
+
return fs.readdirSync(agentsDir, { withFileTypes: true })
|
|
114
|
+
.filter((entry) => entry.isDirectory())
|
|
115
|
+
.map((entry) => ({
|
|
116
|
+
agent: entry.name,
|
|
117
|
+
wirePath: path.join(agentsDir, entry.name, 'wire.jsonl'),
|
|
118
|
+
}))
|
|
119
|
+
.filter((entry) => fs.existsSync(entry.wirePath))
|
|
120
|
+
.sort((left, right) => left.agent.localeCompare(right.agent));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function emptyUsage() {
|
|
124
|
+
return {
|
|
125
|
+
records: 0,
|
|
126
|
+
input_other: 0,
|
|
127
|
+
input_cache_read: 0,
|
|
128
|
+
input_cache_creation: 0,
|
|
129
|
+
input_total: 0,
|
|
130
|
+
output: 0,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function emptyCompactions() {
|
|
135
|
+
return { begun: 0, applied: 0, completed: 0, cancelled: 0, compacted_records: 0 };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function numberValue(value) {
|
|
139
|
+
return Number.isFinite(Number(value)) ? Number(value) : 0;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function emptyTokenSeries() {
|
|
143
|
+
return { total: 0, min: null, max: null, last: null };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function addTokenValue(series, value) {
|
|
147
|
+
series.total += value;
|
|
148
|
+
series.min = series.min === null ? value : Math.min(series.min, value);
|
|
149
|
+
series.max = series.max === null ? value : Math.max(series.max, value);
|
|
150
|
+
series.last = value;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function usageValue(usage, camelName, snakeName) {
|
|
154
|
+
return numberValue(usage?.[camelName] ?? usage?.[snakeName]);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function summarizeWire(agent, wirePath, warnings) {
|
|
158
|
+
const summary = {
|
|
159
|
+
agent,
|
|
160
|
+
records: 0,
|
|
161
|
+
first_time: null,
|
|
162
|
+
last_time: null,
|
|
163
|
+
turn_prompts: 0,
|
|
164
|
+
turn_steers: 0,
|
|
165
|
+
turn_cancellations: 0,
|
|
166
|
+
messages: 0,
|
|
167
|
+
loop_events: 0,
|
|
168
|
+
provider_steps_started: 0,
|
|
169
|
+
provider_steps_finished: 0,
|
|
170
|
+
tool_calls: 0,
|
|
171
|
+
tool_results: 0,
|
|
172
|
+
top_tools: [],
|
|
173
|
+
usage: emptyUsage(),
|
|
174
|
+
full_compactions: emptyCompactions(),
|
|
175
|
+
};
|
|
176
|
+
const toolCounts = new Map();
|
|
177
|
+
|
|
178
|
+
await readJsonLines(wirePath, (record) => {
|
|
179
|
+
summary.records += 1;
|
|
180
|
+
const time = numberValue(record.time ?? record.created_at);
|
|
181
|
+
if (time > 0) {
|
|
182
|
+
summary.first_time = summary.first_time === null ? time : Math.min(summary.first_time, time);
|
|
183
|
+
summary.last_time = summary.last_time === null ? time : Math.max(summary.last_time, time);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (record.type === 'turn.prompt') summary.turn_prompts += 1;
|
|
187
|
+
else if (record.type === 'turn.steer') summary.turn_steers += 1;
|
|
188
|
+
else if (record.type === 'turn.cancel') summary.turn_cancellations += 1;
|
|
189
|
+
else if (record.type === 'context.append_message') summary.messages += 1;
|
|
190
|
+
else if (record.type === 'context.append_loop_event') {
|
|
191
|
+
summary.loop_events += 1;
|
|
192
|
+
if (record.event?.type === 'step.begin') summary.provider_steps_started += 1;
|
|
193
|
+
else if (record.event?.type === 'step.end') summary.provider_steps_finished += 1;
|
|
194
|
+
else if (record.event?.type === 'tool.call') {
|
|
195
|
+
summary.tool_calls += 1;
|
|
196
|
+
const name = typeof record.event.name === 'string' && record.event.name ? record.event.name : '<unknown>';
|
|
197
|
+
toolCounts.set(name, (toolCounts.get(name) || 0) + 1);
|
|
198
|
+
} else if (record.event?.type === 'tool.result') summary.tool_results += 1;
|
|
199
|
+
} else if (record.type === 'usage.record') {
|
|
200
|
+
const usage = record.usage || {};
|
|
201
|
+
const inputOther = usageValue(usage, 'inputOther', 'input_other');
|
|
202
|
+
const inputCacheRead = usageValue(usage, 'inputCacheRead', 'input_cache_read');
|
|
203
|
+
const inputCacheCreation = usageValue(usage, 'inputCacheCreation', 'input_cache_creation');
|
|
204
|
+
summary.usage.records += 1;
|
|
205
|
+
summary.usage.input_other += inputOther;
|
|
206
|
+
summary.usage.input_cache_read += inputCacheRead;
|
|
207
|
+
summary.usage.input_cache_creation += inputCacheCreation;
|
|
208
|
+
summary.usage.input_total += inputOther + inputCacheRead + inputCacheCreation;
|
|
209
|
+
summary.usage.output += usageValue(usage, 'output', 'output_tokens');
|
|
210
|
+
} else if (record.type === 'full_compaction.begin') summary.full_compactions.begun += 1;
|
|
211
|
+
else if (record.type === 'context.apply_compaction') {
|
|
212
|
+
summary.full_compactions.applied += 1;
|
|
213
|
+
summary.full_compactions.compacted_records += numberValue(record.compactedCount ?? record.compacted_count);
|
|
214
|
+
} else if (record.type === 'full_compaction.complete') summary.full_compactions.completed += 1;
|
|
215
|
+
else if (record.type === 'full_compaction.cancel') summary.full_compactions.cancelled += 1;
|
|
216
|
+
}, (warning) => warnings.push({ scope: 'wire', agent, ...warning }));
|
|
217
|
+
|
|
218
|
+
summary.top_tools = [...toolCounts.entries()]
|
|
219
|
+
.map(([name, calls]) => ({ name, calls }))
|
|
220
|
+
.sort((left, right) => right.calls - left.calls || left.name.localeCompare(right.name));
|
|
221
|
+
return summary;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function addUsage(target, source) {
|
|
225
|
+
for (const key of Object.keys(target)) target[key] += source[key];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function addCompactions(target, source) {
|
|
229
|
+
for (const key of Object.keys(target)) target[key] += source[key];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function combineAgents(agents) {
|
|
233
|
+
const totals = {
|
|
234
|
+
records: 0,
|
|
235
|
+
turn_prompts: 0,
|
|
236
|
+
turn_steers: 0,
|
|
237
|
+
turn_cancellations: 0,
|
|
238
|
+
messages: 0,
|
|
239
|
+
loop_events: 0,
|
|
240
|
+
provider_steps_started: 0,
|
|
241
|
+
provider_steps_finished: 0,
|
|
242
|
+
tool_calls: 0,
|
|
243
|
+
tool_results: 0,
|
|
244
|
+
top_tools: [],
|
|
245
|
+
usage: emptyUsage(),
|
|
246
|
+
full_compactions: emptyCompactions(),
|
|
247
|
+
};
|
|
248
|
+
const toolCounts = new Map();
|
|
249
|
+
const numericKeys = [
|
|
250
|
+
'records',
|
|
251
|
+
'turn_prompts',
|
|
252
|
+
'turn_steers',
|
|
253
|
+
'turn_cancellations',
|
|
254
|
+
'messages',
|
|
255
|
+
'loop_events',
|
|
256
|
+
'provider_steps_started',
|
|
257
|
+
'provider_steps_finished',
|
|
258
|
+
'tool_calls',
|
|
259
|
+
'tool_results',
|
|
260
|
+
];
|
|
261
|
+
|
|
262
|
+
for (const agent of agents) {
|
|
263
|
+
for (const key of numericKeys) totals[key] += agent[key];
|
|
264
|
+
addUsage(totals.usage, agent.usage);
|
|
265
|
+
addCompactions(totals.full_compactions, agent.full_compactions);
|
|
266
|
+
for (const tool of agent.top_tools) toolCounts.set(tool.name, (toolCounts.get(tool.name) || 0) + tool.calls);
|
|
267
|
+
}
|
|
268
|
+
totals.top_tools = [...toolCounts.entries()]
|
|
269
|
+
.map(([name, calls]) => ({ name, calls }))
|
|
270
|
+
.sort((left, right) => right.calls - left.calls || left.name.localeCompare(right.name));
|
|
271
|
+
return totals;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function summarizeTelemetry(telemetryDir, sessionId, warnings) {
|
|
275
|
+
const summary = {
|
|
276
|
+
micro_compactions: 0,
|
|
277
|
+
full_compaction_failures: 0,
|
|
278
|
+
offloaded_tool_results: 0,
|
|
279
|
+
offloaded_output_chars: 0,
|
|
280
|
+
truncated_tool_results: 0,
|
|
281
|
+
};
|
|
282
|
+
if (!fs.existsSync(telemetryDir)) return summary;
|
|
283
|
+
|
|
284
|
+
const files = fs.readdirSync(telemetryDir, { withFileTypes: true })
|
|
285
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.jsonl'))
|
|
286
|
+
.map((entry) => ({ name: entry.name, filePath: path.join(telemetryDir, entry.name) }));
|
|
287
|
+
for (const file of files) {
|
|
288
|
+
await readJsonLines(file.filePath, (record) => {
|
|
289
|
+
if (record.session_id !== sessionId) return;
|
|
290
|
+
if (record.event === 'micro_compaction_finished') {
|
|
291
|
+
summary.micro_compactions += 1;
|
|
292
|
+
summary.truncated_tool_results += numberValue(record.properties?.truncated_tool_result_count);
|
|
293
|
+
} else if (record.event === 'compaction_failed') {
|
|
294
|
+
summary.full_compaction_failures += 1;
|
|
295
|
+
} else if (record.event === 'tool_result_offloaded') {
|
|
296
|
+
summary.offloaded_tool_results += 1;
|
|
297
|
+
summary.offloaded_output_chars += numberValue(record.properties?.output_size_chars);
|
|
298
|
+
}
|
|
299
|
+
}, (warning) => warnings.push({ scope: 'telemetry', file: file.name, ...warning }));
|
|
300
|
+
}
|
|
301
|
+
return summary;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function summarizeToolSchemas(logPath) {
|
|
305
|
+
const summary = {
|
|
306
|
+
requests: 0,
|
|
307
|
+
deferred_requests: 0,
|
|
308
|
+
suppression_reasons: [],
|
|
309
|
+
eligible_schema_tokens: emptyTokenSeries(),
|
|
310
|
+
selected_schema_tokens: emptyTokenSeries(),
|
|
311
|
+
estimated_schema_tokens_avoided: 0,
|
|
312
|
+
last: null,
|
|
313
|
+
};
|
|
314
|
+
if (!fs.existsSync(logPath)) return summary;
|
|
315
|
+
|
|
316
|
+
const reasonCounts = new Map();
|
|
317
|
+
const input = fs.createReadStream(logPath, { encoding: 'utf8' });
|
|
318
|
+
const lines = readline.createInterface({ input, crlfDelay: Infinity });
|
|
319
|
+
for await (const line of lines) {
|
|
320
|
+
if (!line.includes('turn request tools')) continue;
|
|
321
|
+
const values = Object.fromEntries(
|
|
322
|
+
[...line.matchAll(/\b(toolSuppressionReason|eligibleToolCount|selectedToolCount|deferredToolCount|eligibleToolSchemaTokens|selectedToolSchemaTokens)=([^\s]+)/gu)]
|
|
323
|
+
.map((match) => [match[1], match[2]]),
|
|
324
|
+
);
|
|
325
|
+
const numericKeys = [
|
|
326
|
+
'eligibleToolCount',
|
|
327
|
+
'selectedToolCount',
|
|
328
|
+
'deferredToolCount',
|
|
329
|
+
'eligibleToolSchemaTokens',
|
|
330
|
+
'selectedToolSchemaTokens',
|
|
331
|
+
];
|
|
332
|
+
if (!values.toolSuppressionReason || !numericKeys.every((key) => /^\d+$/u.test(values[key] || ''))) continue;
|
|
333
|
+
|
|
334
|
+
const eligibleToolCount = Number(values.eligibleToolCount);
|
|
335
|
+
const selectedToolCount = Number(values.selectedToolCount);
|
|
336
|
+
const deferredToolCount = Number(values.deferredToolCount);
|
|
337
|
+
const eligibleSchemaTokens = Number(values.eligibleToolSchemaTokens);
|
|
338
|
+
const selectedSchemaTokens = Number(values.selectedToolSchemaTokens);
|
|
339
|
+
summary.requests += 1;
|
|
340
|
+
if (deferredToolCount > 0) summary.deferred_requests += 1;
|
|
341
|
+
reasonCounts.set(values.toolSuppressionReason, (reasonCounts.get(values.toolSuppressionReason) || 0) + 1);
|
|
342
|
+
addTokenValue(summary.eligible_schema_tokens, eligibleSchemaTokens);
|
|
343
|
+
addTokenValue(summary.selected_schema_tokens, selectedSchemaTokens);
|
|
344
|
+
summary.estimated_schema_tokens_avoided += Math.max(0, eligibleSchemaTokens - selectedSchemaTokens);
|
|
345
|
+
summary.last = {
|
|
346
|
+
eligible_tool_count: eligibleToolCount,
|
|
347
|
+
selected_tool_count: selectedToolCount,
|
|
348
|
+
deferred_tool_count: deferredToolCount,
|
|
349
|
+
eligible_schema_tokens: eligibleSchemaTokens,
|
|
350
|
+
selected_schema_tokens: selectedSchemaTokens,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
summary.suppression_reasons = [...reasonCounts.entries()]
|
|
354
|
+
.map(([reason, requests]) => ({ reason, requests }))
|
|
355
|
+
.sort((left, right) => right.requests - left.requests || left.reason.localeCompare(right.reason));
|
|
356
|
+
return summary;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function listSessions(options, profileRoot, sessions, warnings) {
|
|
360
|
+
const listed = sessions
|
|
361
|
+
.filter((entry) => {
|
|
362
|
+
try {
|
|
363
|
+
requireSafeSessionPath(profileRoot, entry.sessionDir);
|
|
364
|
+
return true;
|
|
365
|
+
} catch {
|
|
366
|
+
warnings.push({ scope: 'session_index', code: 'unsafe_session_path', session_id: entry.sessionId });
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
})
|
|
370
|
+
.map((entry) => {
|
|
371
|
+
const item = {
|
|
372
|
+
session_id: entry.sessionId,
|
|
373
|
+
agents: agentWireFiles(entry.sessionDir).length,
|
|
374
|
+
last_modified: sessionModifiedTime(entry.sessionDir),
|
|
375
|
+
};
|
|
376
|
+
if (options.includeMetadata) {
|
|
377
|
+
item.session_dir = entry.sessionDir;
|
|
378
|
+
item.work_dir = entry.workDir || null;
|
|
379
|
+
}
|
|
380
|
+
return item;
|
|
381
|
+
})
|
|
382
|
+
.sort((left, right) => String(right.last_modified).localeCompare(String(left.last_modified)))
|
|
383
|
+
.slice(0, options.list);
|
|
384
|
+
return { mode: 'list', profile: options.profile, sessions: listed, warnings };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async function inspectSession(options, profileRoot, sessions, inheritedWarnings) {
|
|
388
|
+
const matches = sessions.filter((entry) => entry.sessionId === options.selector || entry.sessionId.startsWith(options.selector));
|
|
389
|
+
if (matches.length === 0) throw new Error('session_not_found');
|
|
390
|
+
if (matches.length > 1) throw new Error('ambiguous_session_prefix');
|
|
391
|
+
const entry = matches[0];
|
|
392
|
+
requireSafeSessionPath(profileRoot, entry.sessionDir);
|
|
393
|
+
const warnings = [...inheritedWarnings];
|
|
394
|
+
const wires = agentWireFiles(entry.sessionDir);
|
|
395
|
+
if (wires.length === 0) warnings.push({ scope: 'session', code: 'wire_not_found' });
|
|
396
|
+
const agents = [];
|
|
397
|
+
for (const wire of wires) agents.push(await summarizeWire(wire.agent, wire.wirePath, warnings));
|
|
398
|
+
|
|
399
|
+
const result = {
|
|
400
|
+
mode: 'summary',
|
|
401
|
+
profile: options.profile,
|
|
402
|
+
session_id: entry.sessionId,
|
|
403
|
+
agents,
|
|
404
|
+
totals: combineAgents(agents),
|
|
405
|
+
telemetry: await summarizeTelemetry(path.join(options.blunHome, 'telemetry'), entry.sessionId, warnings),
|
|
406
|
+
tool_schemas: await summarizeToolSchemas(path.join(entry.sessionDir, 'logs', 'blun.log')),
|
|
407
|
+
warnings,
|
|
408
|
+
};
|
|
409
|
+
if (options.includeMetadata) {
|
|
410
|
+
result.session_dir = entry.sessionDir;
|
|
411
|
+
result.work_dir = entry.workDir || null;
|
|
412
|
+
}
|
|
413
|
+
return result;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async function main() {
|
|
417
|
+
let options;
|
|
418
|
+
try {
|
|
419
|
+
options = parseArgs(process.argv.slice(2));
|
|
420
|
+
const profileRoot = path.join(options.blunHome, 'profile', options.profile);
|
|
421
|
+
const { sessions, warnings } = await loadSessionIndex(profileRoot);
|
|
422
|
+
const result = options.list !== null
|
|
423
|
+
? listSessions(options, profileRoot, sessions, warnings)
|
|
424
|
+
: await inspectSession(options, profileRoot, sessions, warnings);
|
|
425
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
426
|
+
} catch (error) {
|
|
427
|
+
fail(String(error?.message || error), options?.selector ? { selector: options.selector } : {});
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
void main();
|