agy-insights 0.7.2 → 0.7.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/README.es.md +3 -1
- package/README.md +3 -1
- package/agy-insights.js +16 -0
- package/docs/how-it-works.md +2 -2
- package/lib/findings.js +1 -1
- package/lib/history.js +81 -17
- package/lib/i18n.js +18 -4
- package/lib/report.js +4 -3
- package/package.json +1 -1
package/README.es.md
CHANGED
|
@@ -36,7 +36,7 @@ npx agy-insights
|
|
|
36
36
|
npx agy-insights@latest --text
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
`@latest` hace que npx compruebe cada vez si hay una versión nueva. Sin él, una copia instalada globalmente (`npm install -g`) o en el proyecto actual tiene prioridad y no se actualiza nunca. Para fijar una versión concreta: `npx agy-insights@0.7.
|
|
39
|
+
`@latest` hace que npx compruebe cada vez si hay una versión nueva. Sin él, una copia instalada globalmente (`npm install -g`) o en el proyecto actual tiene prioridad y no se actualiza nunca. Para fijar una versión concreta: `npx agy-insights@0.7.3`.
|
|
40
40
|
|
|
41
41
|
**Instalándolo globalmente:**
|
|
42
42
|
|
|
@@ -161,6 +161,8 @@ Copia `agy-insights.example.json` a `~/.agy-insights.json` (o pásalo con `--con
|
|
|
161
161
|
|
|
162
162
|
**Uso la app o el IDE de Antigravity, no el CLI**. Por ahora solo se lee el historial del CLI.
|
|
163
163
|
|
|
164
|
+
**Muchas llamadas con modelo «desconocido»**. agy no anotó su modelo. Ejecuta `npx agy-insights@latest --debug-models` y pega el resultado en una [incidencia](https://github.com/migohe14/agy-insights/issues): solo contiene recuentos y nombres de modelo.
|
|
165
|
+
|
|
164
166
|
**El navegador no se abre**. La ruta del informe aparece en la terminal; ábrela a mano.
|
|
165
167
|
|
|
166
168
|
## Estado
|
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ npx agy-insights
|
|
|
36
36
|
npx agy-insights@latest --text
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
`@latest` makes npx check for the newest version every time. Without it, a copy installed globally (`npm install -g`) or in the current project takes precedence and is never updated. To pin a version instead: `npx agy-insights@0.7.
|
|
39
|
+
`@latest` makes npx check for the newest version every time. Without it, a copy installed globally (`npm install -g`) or in the current project takes precedence and is never updated. To pin a version instead: `npx agy-insights@0.7.3`.
|
|
40
40
|
|
|
41
41
|
**Globally:**
|
|
42
42
|
|
|
@@ -161,6 +161,8 @@ Copy `agy-insights.example.json` to `~/.agy-insights.json` (or pass it with `--c
|
|
|
161
161
|
|
|
162
162
|
**I use the Antigravity app or IDE, not the CLI**. For now only the CLI history is read.
|
|
163
163
|
|
|
164
|
+
**Many calls under model "unknown"**. agy recorded no model for them. Run `npx agy-insights@latest --debug-models` and paste the output into an [issue](https://github.com/migohe14/agy-insights/issues): it only has counts and model names.
|
|
165
|
+
|
|
164
166
|
**The browser doesn't open**. The report path is printed in the terminal; open it manually.
|
|
165
167
|
|
|
166
168
|
## Status
|
package/agy-insights.js
CHANGED
|
@@ -32,6 +32,7 @@ function parseArgs(argv) {
|
|
|
32
32
|
else if (a === '--lang') opts.lang = value();
|
|
33
33
|
else if (a === '--since') opts.since = value();
|
|
34
34
|
else if (a === '--days') opts.days = value();
|
|
35
|
+
else if (a === '--debug-models') opts.debugModels = true;
|
|
35
36
|
else fail(t.unknownOption(a));
|
|
36
37
|
}
|
|
37
38
|
return opts;
|
|
@@ -82,6 +83,20 @@ function sinceTime({ since, days }) {
|
|
|
82
83
|
return null;
|
|
83
84
|
}
|
|
84
85
|
|
|
86
|
+
// Tab-separated table of where each conversation's call models came from, for bug reports.
|
|
87
|
+
function debugModels(r) {
|
|
88
|
+
const d = t.debugModels;
|
|
89
|
+
const sum = { rows: 0, rowsWithModel: 0, calls: 0, fromTranscript: 0, unknown: 0 };
|
|
90
|
+
const lines = [d.header.join('\t')];
|
|
91
|
+
for (const s of r.sessions) {
|
|
92
|
+
const m = s.modelSources;
|
|
93
|
+
for (const k of Object.keys(sum)) sum[k] += m[k];
|
|
94
|
+
lines.push([s.id.slice(0, 8), m.calls, m.rows, m.rowsWithModel, m.fromTranscript, m.unknown, s.model ? `${s.model.model} (${s.model.effort})` : '', m.unknown ? m.candidates.join(' | ') : ''].join('\t'));
|
|
95
|
+
}
|
|
96
|
+
lines.push([d.total, sum.calls, sum.rows, sum.rowsWithModel, sum.fromTranscript, sum.unknown, '', ''].join('\t'), '', d.note, '');
|
|
97
|
+
return lines.join('\n');
|
|
98
|
+
}
|
|
99
|
+
|
|
85
100
|
// Periods the HTML report can switch to, besides the whole history.
|
|
86
101
|
const PRESET_DAYS = [90, 30, 7];
|
|
87
102
|
|
|
@@ -104,6 +119,7 @@ async function main() {
|
|
|
104
119
|
customKinds: cfg.customKinds || {},
|
|
105
120
|
});
|
|
106
121
|
const r = await run(since);
|
|
122
|
+
if (opts.debugModels) return process.stdout.write(debugModels(r));
|
|
107
123
|
if (opts.map) {
|
|
108
124
|
const m = buildMap(opts.project, { ...cfg.map, history: r.files });
|
|
109
125
|
console.log(m.block);
|
package/docs/how-it-works.md
CHANGED
|
@@ -34,10 +34,10 @@ It is undocumented; this is what has been observed in `agy` 1.4.x.
|
|
|
34
34
|
- One SQLite database per conversation: `~/.gemini/antigravity-cli/conversations/<uuid>.db`, in WAL mode.
|
|
35
35
|
- Table `steps`: one row per step. `step_payload` is a protobuf message. Tool calls store the name (`view_file`, `run_command`, `grep_search`…) followed by their JSON arguments, for example `{"AbsolutePath":"C:\\dev\\...\\X.cs","StartLine":301,"EndLine":380}`, preceded by a `call_NNN` id.
|
|
36
36
|
- Each call takes two steps with the same id: the model response that requests it (`step_type` 15) and the result (`step_type` 132), which repeats name and arguments and adds the file content or command output. `lib/history.js` counts the call once per id and adds the size of both steps, which approximates what entered the context.
|
|
37
|
-
- Table `gen_metadata`: one row per model call, in the same order as the transcript's `PLANNER_RESPONSE` lines. Each row holds the model's display name with the effort level in brackets (`Gemini 3.8 Flash (High)`) and its id (`gemini-3.8-flash-high`). `modelOf()` takes the model and effort from whole strings only, so prose in the system prompt that mentions a model is ignored, and the n-th row is matched to the n-th call to get tokens per model and effort.
|
|
37
|
+
- Table `gen_metadata`: one row per model call, in the same order as the transcript's `PLANNER_RESPONSE` lines. Each row holds the model's display name with the effort level in brackets (`Gemini 3.8 Flash (High)`) and its id (`gemini-3.8-flash-high`). `modelOf()` takes the model and effort from whole strings only, so prose in the system prompt that mentions a model is ignored, and the n-th row is matched to the n-th call to get tokens per model and effort. That pairing is only trusted when the counts match. Otherwise each call takes the model selected in the transcript (`USER_INPUT` lines carry "changed setting `Model Selection` from X to Y"), or the conversation's only model; what is left is "unknown". `modelKey()` merges the two spellings (`Gemini 3.8 Flash` / `gemini-3.8-flash`, `High` / `high`). `--debug-models` prints, per conversation, how many calls got their model from each source: counts only, safe to paste into an issue.
|
|
38
38
|
- Next to the history, `../brain/<id>/.system_generated/logs/transcript.jsonl`: one JSON line per step. `PLANNER_RESPONSE` lines are model calls and carry `input_tokens`, `cache_read_tokens` and `output_tokens`; failed steps have `status: "ERROR"`. Subagents have their own conversation and transcript. `realUsage()` in `lib/history.js` adds those figures up (it only reads the file). The total printed by `agy -p --output-format json` does not include subagents; this one does.
|
|
39
39
|
|
|
40
|
-
Each `transcript.jsonl` line also has `created_at` (ISO time). A conversation is dated by the first and last of those; without a transcript, by the `.db` file's last write. `--since` / `--days` keep a conversation when its last activity falls inside the period, and skip the rest before opening them. `../conversation_summaries.db`
|
|
40
|
+
Each `transcript.jsonl` line also has `created_at` (ISO time). A conversation is dated by the first and last of those; without a transcript, by the `.db` file's last write. `--since` / `--days` keep a conversation when its last activity falls inside the period, and skip the rest before opening them. `../conversation_summaries.db` has one row per conversation: its short `title` (or `preview` in older ones) names each session in the report instead of its id; `last_modified_time` is not used. If the file is missing, sessions are shown by id. Each model call's `created_at` also puts its tokens on a local day (`r.daily`), which feeds the usage chart.
|
|
41
41
|
|
|
42
42
|
The HTML report holds one view per period (whole history, last 90, 30 and 7 days): `agy-insights.js` runs `analyze()` and `findings()` once per period and `report.htmlViews()` renders them all, so switching in the browser needs no analysis code in the page and every number agrees with its period. The chart is inline SVG; its color (`--series`) is checked for contrast, chroma and lightness against both surfaces.
|
|
43
43
|
|
package/lib/findings.js
CHANGED
|
@@ -131,7 +131,7 @@ function findings(r, opts = {}) {
|
|
|
131
131
|
why: f.longWhy(long.length, num(o.longSessionTokens)), suggestion: f.longSuggestion,
|
|
132
132
|
items: long.slice(0, 5).map((s) => ({
|
|
133
133
|
// Real tokens when agy logged them (`exact`: shown without "~"), else the size estimate.
|
|
134
|
-
label: f.conversation(s.id.slice(0, 8), when(s.start, s.end)), fixes: [],
|
|
134
|
+
label: f.conversation(s.id.slice(0, 8), when(s.start, s.end), s.title), fixes: [],
|
|
135
135
|
tokens: s.usage ? s.usage.input + s.usage.output : s.tokens, exact: Boolean(s.usage),
|
|
136
136
|
detail: f.steps(s.steps) + (s.usage
|
|
137
137
|
? f.longUsage(s.usage.calls, num(s.usage.input + s.usage.cache + s.usage.output))
|
package/lib/history.js
CHANGED
|
@@ -29,6 +29,23 @@ function modelOf(strs) {
|
|
|
29
29
|
return { model: null, effort: null };
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
// agy notes model switches in the user's message: "changed setting `Model Selection` from None to
|
|
33
|
+
// Gemini 3.8 Flash (High)." The model in force for every call that follows.
|
|
34
|
+
const MODEL_SETTING_RE = /changed setting `Model Selection` from .+? to (.+?)\.(?:\s|$)/g;
|
|
35
|
+
function selectedModel(text) {
|
|
36
|
+
let last = null;
|
|
37
|
+
for (const m of String(text).matchAll(MODEL_SETTING_RE)) last = m[1].trim();
|
|
38
|
+
return last && modelOf([last]).model ? modelOf([last]) : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// A short string that may name a model in a format modelOf() does not know yet: mentions a model family,
|
|
42
|
+
// or is an enum-like id ("MODEL_PLACEHOLDER_M26"). Only used for --debug-models.
|
|
43
|
+
const looksLikeModel = (s) => s.length <= 60 && !/[\\/:]/.test(s)
|
|
44
|
+
&& (/\b(gemini|claude|gpt|model|flash|pro|argon)\b/i.test(s) || /^[A-Z][A-Z0-9]*(_[A-Z0-9]+)+$/.test(s));
|
|
45
|
+
|
|
46
|
+
// "Gemini 3.8 Flash" and "gemini-3.8-flash", "High" and "high": the same model and effort.
|
|
47
|
+
const modelKey = (m) => `${(m.model || '').toLowerCase().replace(/[\s_]+/g, '-')}|${(m.effort || '').toLowerCase()}`;
|
|
48
|
+
|
|
32
49
|
// --- SQLite access: node:sqlite when the runtime has it (Node >= 22.13), sql.js (WASM) otherwise. ---
|
|
33
50
|
|
|
34
51
|
let nodeSqlite = null;
|
|
@@ -203,20 +220,27 @@ function realUsage(dir, id) {
|
|
|
203
220
|
let text;
|
|
204
221
|
try { text = fs.readFileSync(file, 'utf8'); } catch { return null; }
|
|
205
222
|
// `days`: the same figures per local day of each model call (calls without a time go under null).
|
|
206
|
-
// `perCall`: each call's figures, in order
|
|
223
|
+
// `perCall`: each call's figures, in order, and the model selected in the transcript when it was made.
|
|
207
224
|
const u = { calls: 0, input: 0, cache: 0, output: 0, errors: 0, start: null, end: null, days: new Map(), perCall: [] };
|
|
225
|
+
let selected = null;
|
|
226
|
+
u.unrecognized = new Set();
|
|
208
227
|
for (const line of text.split('\n')) {
|
|
209
228
|
let e;
|
|
210
229
|
try { e = JSON.parse(line); } catch { continue; }
|
|
211
230
|
const at = Date.parse(e.created_at);
|
|
212
231
|
if (at) { u.start = Math.min(u.start ?? at, at); u.end = Math.max(u.end ?? at, at); }
|
|
213
232
|
if (e.status === 'ERROR') u.errors++;
|
|
233
|
+
if (e.type === 'USER_INPUT') {
|
|
234
|
+
selected = selectedModel(e.content) || selected;
|
|
235
|
+
// Model names agy selected that modelOf() does not understand, for --debug-models.
|
|
236
|
+
for (const m of String(e.content).matchAll(MODEL_SETTING_RE)) if (!modelOf([m[1].trim()]).model) u.unrecognized.add(m[1].trim().slice(0, 60));
|
|
237
|
+
}
|
|
214
238
|
if (e.type !== 'PLANNER_RESPONSE') continue;
|
|
215
239
|
u.calls++;
|
|
216
240
|
u.input += Number(e.input_tokens) || 0;
|
|
217
241
|
u.cache += Number(e.cache_read_tokens) || 0;
|
|
218
242
|
u.output += Number(e.output_tokens) || 0;
|
|
219
|
-
u.perCall.push({ input: Number(e.input_tokens) || 0, cache: Number(e.cache_read_tokens) || 0, output: Number(e.output_tokens) || 0 });
|
|
243
|
+
u.perCall.push({ input: Number(e.input_tokens) || 0, cache: Number(e.cache_read_tokens) || 0, output: Number(e.output_tokens) || 0, model: selected });
|
|
220
244
|
const k = at ? dayKey(at) : null;
|
|
221
245
|
const d = u.days.get(k) || { calls: 0, input: 0, cache: 0, output: 0 };
|
|
222
246
|
d.calls++;
|
|
@@ -228,6 +252,26 @@ function realUsage(dir, id) {
|
|
|
228
252
|
return u;
|
|
229
253
|
}
|
|
230
254
|
|
|
255
|
+
// --- Titles ---
|
|
256
|
+
|
|
257
|
+
// The short title agy gives each conversation ("Analyzing Package Lock Dependencies"), from
|
|
258
|
+
// <conversations>/../conversation_summaries.db; older ones only have it as `preview`. Empty map when
|
|
259
|
+
// the file is missing or unreadable: the report then falls back to the conversation id.
|
|
260
|
+
async function conversationTitles(dir, tmp) {
|
|
261
|
+
const titles = new Map();
|
|
262
|
+
const file = path.join(dir, '..', 'conversation_summaries.db');
|
|
263
|
+
if (!fs.existsSync(file)) return titles;
|
|
264
|
+
let db;
|
|
265
|
+
try {
|
|
266
|
+
db = await openDb(file, tmp);
|
|
267
|
+
for (const [id, title, preview] of db.all('select conversation_id, title, preview from conversation_summaries')) {
|
|
268
|
+
const name = String(title || preview || '').trim().slice(0, 80);
|
|
269
|
+
if (name) titles.set(String(id), name);
|
|
270
|
+
}
|
|
271
|
+
} catch { /* no titles */ } finally { db?.close(); }
|
|
272
|
+
return titles;
|
|
273
|
+
}
|
|
274
|
+
|
|
231
275
|
// --- Aggregation ---
|
|
232
276
|
|
|
233
277
|
class Counter {
|
|
@@ -269,6 +313,7 @@ async function analyze({ dir = DEFAULT_DIR, workspaces = [], customKinds = {}, s
|
|
|
269
313
|
|
|
270
314
|
try {
|
|
271
315
|
const dbs = fs.existsSync(dir) ? fs.readdirSync(dir).filter((f) => /^[0-9a-f-]{36}\.db$/i.test(f)) : [];
|
|
316
|
+
const titles = await conversationTitles(dir, tmp);
|
|
272
317
|
for (const f of dbs) {
|
|
273
318
|
// When it happened: the transcript's step times, else the database's last write.
|
|
274
319
|
const id = f.replace(/\.db$/, '');
|
|
@@ -345,26 +390,45 @@ async function analyze({ dir = DEFAULT_DIR, workspaces = [], customKinds = {}, s
|
|
|
345
390
|
daily.set(day, d);
|
|
346
391
|
}
|
|
347
392
|
}
|
|
348
|
-
//
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
393
|
+
// Model of each call. gen_metadata usually has one row per call, in the transcript's order; when the
|
|
394
|
+
// counts match, the n-th row is the n-th call. When they don't, pairing by position would mislabel
|
|
395
|
+
// calls, so the model selected in the transcript is used instead, or the conversation's only model.
|
|
396
|
+
const metaStrs = db.all('select data from gen_metadata order by idx').map(([blob]) => collect(blob));
|
|
397
|
+
const metaRows = metaStrs.map(modelOf);
|
|
398
|
+
const calls = usage ? usage.perCall : metaRows.map(() => null);
|
|
399
|
+
const aligned = metaRows.length === calls.length;
|
|
400
|
+
const rowModels = new Set(metaRows.filter((m) => m.model).map(modelKey));
|
|
401
|
+
const onlyModel = rowModels.size === 1 ? metaRows.find((m) => m.model) : null;
|
|
402
|
+
const callModels = calls.map((c, i) => {
|
|
403
|
+
if (aligned && metaRows[i].model) return metaRows[i];
|
|
404
|
+
return c?.model || onlyModel || { model: null, effort: null };
|
|
405
|
+
});
|
|
406
|
+
const seenModels = new Set(), counts = new Map();
|
|
407
|
+
callModels.forEach((m, i) => {
|
|
408
|
+
const k = modelKey(m);
|
|
409
|
+
const e = models.get(k) || { model: m.model, effort: m.effort, calls: 0, convs: 0, input: 0, cache: 0, output: 0, known: 0 };
|
|
410
|
+
// Prefer the display name ("Gemini 3.8 Flash", "High") over the id spelling.
|
|
411
|
+
if (m.model && /[A-Z ]/.test(m.model) && !/[A-Z ]/.test(e.model || '')) { e.model = m.model; e.effort = m.effort; }
|
|
356
412
|
e.calls++;
|
|
357
413
|
if (!seenModels.has(k)) { seenModels.add(k); e.convs++; }
|
|
358
|
-
const c =
|
|
414
|
+
const c = calls[i];
|
|
359
415
|
if (c) { e.known++; e.input += c.input; e.cache += c.cache; e.output += c.output; }
|
|
360
416
|
models.set(k, e);
|
|
361
|
-
|
|
417
|
+
if (m.model) counts.set(k, (counts.get(k) || 0) + 1);
|
|
418
|
+
});
|
|
362
419
|
// The session's model: the one that made most of its calls.
|
|
363
|
-
const counts = new Map();
|
|
364
|
-
for (const m of callModels) counts.set(`${m.model}|${m.effort}`, (counts.get(`${m.model}|${m.effort}`) || 0) + 1);
|
|
365
420
|
const main = [...counts].sort((a, b) => b[1] - a[1])[0];
|
|
366
|
-
const model = main ?
|
|
367
|
-
r.sessions.push({
|
|
421
|
+
const model = main ? models.get(main[0]) : null;
|
|
422
|
+
r.sessions.push({
|
|
423
|
+
id, title: titles.get(id) || null, start, end, steps: steps.length, tokens: convBytes / BYTES_PER_TOKEN, usage,
|
|
424
|
+
model: model ? { model: model.model, effort: model.effort } : null, models: counts.size,
|
|
425
|
+
// For --debug-models: where each call's model came from, and for rows where no model was recognized,
|
|
426
|
+
// the short strings that look like a model id (to spot a new format). Never prompts, paths or code.
|
|
427
|
+
modelSources: { rows: metaRows.length, rowsWithModel: metaRows.filter((m) => m.model).length, calls: calls.length,
|
|
428
|
+
fromTranscript: calls.filter((c) => c?.model).length, unknown: callModels.filter((m) => !m.model).length,
|
|
429
|
+
candidates: [...new Set([...(usage?.unrecognized || []),
|
|
430
|
+
...metaStrs.filter((_, i) => !metaRows[i].model).flat().filter(looksLikeModel)])].slice(0, 10) },
|
|
431
|
+
});
|
|
368
432
|
r.tokens += convBytes / BYTES_PER_TOKEN;
|
|
369
433
|
} catch { r.unreadable++; } finally { db.close(); }
|
|
370
434
|
}
|
|
@@ -386,4 +450,4 @@ async function analyze({ dir = DEFAULT_DIR, workspaces = [], customKinds = {}, s
|
|
|
386
450
|
};
|
|
387
451
|
}
|
|
388
452
|
|
|
389
|
-
module.exports = { analyze, DEFAULT_DIR, kind, NON_SOURCE, realUsage, dayKey, modelOf, repoFiles };
|
|
453
|
+
module.exports = { analyze, DEFAULT_DIR, kind, NON_SOURCE, realUsage, dayKey, modelOf, selectedModel, repoFiles };
|
package/lib/i18n.js
CHANGED
|
@@ -20,6 +20,7 @@ Options:
|
|
|
20
20
|
--since <YYYY-MM-DD> Only conversations active since that date
|
|
21
21
|
--days <n> Only conversations active in the last n days
|
|
22
22
|
--lang <en|es> Language of the output (default: the system's)
|
|
23
|
+
--debug-models Per conversation, where each call's model came from (counts only, to report issues)
|
|
23
24
|
--dir <folder> Another history folder (default ${dir})
|
|
24
25
|
--config <file> Configuration (default ~/.agy-insights.json if it exists)
|
|
25
26
|
--help This help
|
|
@@ -33,6 +34,11 @@ Options:
|
|
|
33
34
|
cannotRead: (f, msg) => `Cannot read ${f}: ${msg}`,
|
|
34
35
|
mapDone: (tokens, file) => `\n~${tokens} tokens on every request. Paste it into ${file} if you like it; agy-insights does not write it.`,
|
|
35
36
|
reportName: 'report',
|
|
37
|
+
debugModels: {
|
|
38
|
+
header: ['conversation', 'calls', 'gen_metadata rows', 'rows with model', 'calls with model in transcript', 'unknown', 'main model', 'unrecognized model ids'],
|
|
39
|
+
total: 'total',
|
|
40
|
+
note: 'Only counts and model names or ids: no paths, prompts or code. Paste it into an issue if models show up as unknown.',
|
|
41
|
+
},
|
|
36
42
|
done: (convs, found, out) => `${plural(convs, 'conversation', 'conversations')} analyzed, ${plural(found, 'finding', 'findings')}.\nReport: ${out}`,
|
|
37
43
|
|
|
38
44
|
kinds: {
|
|
@@ -81,7 +87,7 @@ Options:
|
|
|
81
87
|
longTitle: 'Very long sessions',
|
|
82
88
|
longWhy: (n, min) => `${plural(n, 'conversation exceeds', 'conversations exceed')} ${min} tokens. In a long session, everything accumulated is resent on every turn.`,
|
|
83
89
|
longSuggestion: 'A new conversation per task, or /compact when you change topic.',
|
|
84
|
-
conversation: (id, at) => `conversation ${id}${at ? ` · ${at}` : ''}`,
|
|
90
|
+
conversation: (id, at, title) => `${title ? `"${title}" (${id})` : `conversation ${id}`}${at ? ` · ${at}` : ''}`,
|
|
85
91
|
steps: (n) => plural(n, 'step', 'steps'),
|
|
86
92
|
longUsage: (calls, cached) => ` · ${plural(calls, 'model call', 'model calls')} · real tokens, ${cached} with cache`,
|
|
87
93
|
},
|
|
@@ -112,6 +118,7 @@ Options:
|
|
|
112
118
|
modelLine: (model, effort, calls, tokens, pct, perCall) => `${model} · ${effort} ${plural(calls, 'call', 'calls')}, ${tokens} tokens (${pct}), ~${perCall} per call`,
|
|
113
119
|
sessionModel: 'Model',
|
|
114
120
|
otherModels: (n) => ` +${n} more`,
|
|
121
|
+
modelsUnknown: (n) => `"unknown": ${n} calls for which agy recorded no model, neither in gen_metadata nor as a model change in the transcript. If there are many, run npx agy-insights@latest --debug-models and paste the result into an issue on GitHub (it only contains counts).`,
|
|
115
122
|
efforts: { minimal: 'Minimal', low: 'Low', medium: 'Medium', high: 'High', xhigh: 'Extra high', 'extra high': 'Extra high', max: 'Max' },
|
|
116
123
|
sessionsTitle: 'Sessions (real usage)',
|
|
117
124
|
sessionCols: ['Conversation', 'When', 'Model', 'Tokens', '%', 'With cache', 'Calls', 'Errors'],
|
|
@@ -158,7 +165,7 @@ Options:
|
|
|
158
165
|
realUsage: (real, input, output, cached, calls) => `Real usage: ${real} tokens (input ${input} + output ${output}), ${cached} including cache, in ${plural(calls, 'model call', 'model calls')}`,
|
|
159
166
|
noLog: (n) => ` (${plural(n, 'conversation', 'conversations')} without a log)`,
|
|
160
167
|
topSessions: (n) => (n === 1 ? ' The session that uses the most:' : ` The ${n} sessions that use the most:`),
|
|
161
|
-
sessionLine: (id, calls, errors, cached, model, at) => `conversation ${id}${at ? ` · ${at}` : ''}${model ? ` (${model})` : ''} ${plural(calls, 'call', 'calls')}${errors ? `, ${plural(errors, 'error', 'errors')}` : ''}, ${cached} with cache`,
|
|
168
|
+
sessionLine: (id, calls, errors, cached, model, at, title) => `${title ? `"${title}" (${id})` : `conversation ${id}`}${at ? ` · ${at}` : ''}${model ? ` (${model})` : ''} ${plural(calls, 'call', 'calls')}${errors ? `, ${plural(errors, 'error', 'errors')}` : ''}, ${cached} with cache`,
|
|
162
169
|
top5: (pct, reads, rereads, rpct) => `The 5 largest sessions account for ${pct}. Reads: ${reads}, re-reads ${rereads} (${rpct})`,
|
|
163
170
|
pathHidden: '(path hidden, use --paths)',
|
|
164
171
|
byKindText: 'By file type:',
|
|
@@ -182,6 +189,7 @@ Opciones:
|
|
|
182
189
|
--since <AAAA-MM-DD> Solo conversaciones con actividad desde esa fecha
|
|
183
190
|
--days <n> Solo conversaciones con actividad en los últimos n días
|
|
184
191
|
--lang <en|es> Idioma de la salida (por defecto, el del sistema)
|
|
192
|
+
--debug-models Por conversación, de dónde sale el modelo de cada llamada (solo recuentos, para reportar fallos)
|
|
185
193
|
--dir <carpeta> Otra carpeta de historial (por defecto ${dir})
|
|
186
194
|
--config <fichero> Configuración (por defecto ~/.agy-insights.json si existe)
|
|
187
195
|
--help Esta ayuda
|
|
@@ -195,6 +203,11 @@ Opciones:
|
|
|
195
203
|
cannotRead: (f, msg) => `No se puede leer ${f}: ${msg}`,
|
|
196
204
|
mapDone: (tokens, file) => `\n~${tokens} tokens en cada petición. Pégalo en ${file} si te convence; agy-insights no lo escribe.`,
|
|
197
205
|
reportName: 'informe',
|
|
206
|
+
debugModels: {
|
|
207
|
+
header: ['conversación', 'llamadas', 'filas gen_metadata', 'filas con modelo', 'llamadas con modelo en transcript', 'desconocidas', 'modelo principal', 'ids de modelo no reconocidos'],
|
|
208
|
+
total: 'total',
|
|
209
|
+
note: 'Solo recuentos y nombres o ids de modelo: ni rutas, ni prompts, ni código. Pégalo en una incidencia si salen modelos desconocidos.',
|
|
210
|
+
},
|
|
198
211
|
done: (convs, found, out) => `${plural(convs, 'conversación analizada', 'conversaciones analizadas')}, ${plural(found, 'hallazgo', 'hallazgos')}.\nInforme: ${out}`,
|
|
199
212
|
|
|
200
213
|
kinds: {
|
|
@@ -243,7 +256,7 @@ Opciones:
|
|
|
243
256
|
longTitle: 'Sesiones muy largas',
|
|
244
257
|
longWhy: (n, min) => `${plural(n, 'conversación supera', 'conversaciones superan')} ${min} tokens. En una sesión larga, todo lo acumulado se vuelve a enviar en cada turno.`,
|
|
245
258
|
longSuggestion: 'Una conversación nueva por tarea, o /compact cuando cambias de tema.',
|
|
246
|
-
conversation: (id, at) => `conversación ${id}${at ? ` · ${at}` : ''}`,
|
|
259
|
+
conversation: (id, at, title) => `${title ? `«${title}» (${id})` : `conversación ${id}`}${at ? ` · ${at}` : ''}`,
|
|
247
260
|
steps: (n) => plural(n, 'paso', 'pasos'),
|
|
248
261
|
longUsage: (calls, cached) => ` · ${plural(calls, 'llamada', 'llamadas')} al modelo · tokens reales, ${cached} con caché`,
|
|
249
262
|
},
|
|
@@ -274,6 +287,7 @@ Opciones:
|
|
|
274
287
|
modelLine: (model, effort, calls, tokens, pct, perCall) => `${model} · ${effort} ${plural(calls, 'llamada', 'llamadas')}, ${tokens} tokens (${pct}), ~${perCall} por llamada`,
|
|
275
288
|
sessionModel: 'Modelo',
|
|
276
289
|
otherModels: (n) => ` +${n} más`,
|
|
290
|
+
modelsUnknown: (n) => `«desconocido»: ${n} llamadas de las que agy no anotó el modelo, ni en gen_metadata ni como cambio de modelo en el transcript. Si son muchas, ejecuta npx agy-insights@latest --debug-models y pega el resultado en una incidencia de GitHub (solo contiene recuentos).`,
|
|
277
291
|
efforts: { minimal: 'Mínimo', low: 'Bajo', medium: 'Medio', high: 'Alto', xhigh: 'Muy alto', 'extra high': 'Muy alto', max: 'Máximo' },
|
|
278
292
|
sessionsTitle: 'Sesiones (consumo real)',
|
|
279
293
|
sessionCols: ['Conversación', 'Cuándo', 'Modelo', 'Tokens', '%', 'Con caché', 'Llamadas', 'Errores'],
|
|
@@ -319,7 +333,7 @@ Opciones:
|
|
|
319
333
|
realUsage: (real, input, output, cached, calls) => `Consumo real: ${real} tokens (entrada ${input} + salida ${output}), ${cached} contando la caché, en ${plural(calls, 'llamada', 'llamadas')} al modelo`,
|
|
320
334
|
noLog: (n) => ` (${plural(n, 'conversación', 'conversaciones')} sin registro)`,
|
|
321
335
|
topSessions: (n) => (n === 1 ? ' La sesión que más gasta:' : ` Las ${n} sesiones que más gastan:`),
|
|
322
|
-
sessionLine: (id, calls, errors, cached, model, at) => `conversación ${id}${at ? ` · ${at}` : ''}${model ? ` (${model})` : ''} ${plural(calls, 'llamada', 'llamadas')}${errors ? `, ${plural(errors, 'error', 'errores')}` : ''}, ${cached} con caché`,
|
|
336
|
+
sessionLine: (id, calls, errors, cached, model, at, title) => `${title ? `«${title}» (${id})` : `conversación ${id}`}${at ? ` · ${at}` : ''}${model ? ` (${model})` : ''} ${plural(calls, 'llamada', 'llamadas')}${errors ? `, ${plural(errors, 'error', 'errores')}` : ''}, ${cached} con caché`,
|
|
323
337
|
top5: (pct, reads, rereads, rpct) => `Las 5 sesiones más grandes suman el ${pct}. Lecturas: ${reads}, relecturas ${rereads} (${rpct})`,
|
|
324
338
|
pathHidden: '(ruta oculta, usa --paths)',
|
|
325
339
|
byKindText: 'Por tipo de fichero:',
|
package/lib/report.js
CHANGED
|
@@ -60,7 +60,7 @@ function sessionsTable(r) {
|
|
|
60
60
|
if (!rows.length) return '';
|
|
61
61
|
const max = Math.max(...rows.map((s) => billed(s.usage)), 1);
|
|
62
62
|
return `<h3>${t.report.sessionsTitle}</h3><div class="scroll"><table><thead><tr>${t.report.sessionCols.map((c, i) => `<th${i > 2 ? ' class="num"' : ''}>${c}</th>`).join('')}</tr></thead><tbody>
|
|
63
|
-
${rows.map((s) => `<tr><td><div class="bar" style="--w:${((100 * billed(s.usage)) / max).toFixed(1)}%"></div><span>${esc(s.id.slice(0, 8))}</span
|
|
63
|
+
${rows.map((s) => `<tr><td><div class="bar" style="--w:${((100 * billed(s.usage)) / max).toFixed(1)}%"></div><span>${esc(s.title || s.id.slice(0, 8))}</span>${s.title ? `<span class="detail">${esc(s.id.slice(0, 8))}</span>` : ''}</td><td class="when">${esc(when(s.start, s.end))}</td><td>${esc(sessionModel(s))}</td>
|
|
64
64
|
<td class="num">${fmt(billed(s.usage))}</td><td class="num">${pct(billed(s.usage), billed(r.usage))}</td><td class="num">${fmt(withCache(s.usage))}</td>
|
|
65
65
|
<td class="num">${s.usage.calls}</td><td class="num">${s.usage.errors || ''}</td></tr>`).join('')}
|
|
66
66
|
</tbody></table></div>`;
|
|
@@ -76,12 +76,13 @@ function modelsTable(r) {
|
|
|
76
76
|
if (!rows.length || (rows.length === 1 && !rows[0].model)) return '';
|
|
77
77
|
const total = rows.reduce((s, m) => s + m.input + m.output, 0);
|
|
78
78
|
const max = Math.max(...rows.map((m) => m.input + m.output), 1);
|
|
79
|
+
const unknown = rows.find((m) => !m.model);
|
|
79
80
|
return `<section class="models"><h2>${t.report.modelsTitle}</h2><p class="hint">${t.report.modelsHint}</p>
|
|
80
81
|
<div class="scroll"><table><thead><tr>${t.report.modelCols.map((c, i) => `<th${i > 1 ? ' class="num"' : ''}>${c}</th>`).join('')}</tr></thead><tbody>
|
|
81
82
|
${rows.map((m) => { const tok = m.input + m.output; return `<tr><td><div class="bar" style="--w:${((100 * tok) / max).toFixed(1)}%"></div><span>${esc(modelName(m))}</span></td>
|
|
82
83
|
<td>${esc(effortLabel(m.effort))}</td><td class="num">${fmt(m.calls)}</td><td class="num">${m.known ? fmt(tok) : '–'}</td><td class="num">${m.known ? pct(tok, total) : '–'}</td>
|
|
83
84
|
<td class="num">${m.known ? fmt(tok / m.known) : '–'}</td><td class="num">${m.known ? fmt(tok + m.cache) : '–'}</td><td class="num">${m.convs}</td></tr>`; }).join('')}
|
|
84
|
-
</tbody></table></div
|
|
85
|
+
</tbody></table></div>${unknown ? `<p class="hint">${esc(t.report.modelsUnknown(fmt(unknown.calls)))}</p>` : ''}</section>`;
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
// --- Usage chart ---
|
|
@@ -316,7 +317,7 @@ function text(r, found, { scope, paths }) {
|
|
|
316
317
|
const top = r.sessions.filter((s) => s.usage).slice(0, 5);
|
|
317
318
|
out.push(t.report.topSessions(top.length));
|
|
318
319
|
for (const s of top) {
|
|
319
|
-
out.push(` ${fmt(billed(s.usage)).padStart(10)} ${pct(billed(s.usage), billed(u)).padStart(4)} ${t.report.sessionLine(s.id.slice(0, 8), s.usage.calls, s.usage.errors, fmt(withCache(s.usage)), sessionModel(s), when(s.start, s.end))}`);
|
|
320
|
+
out.push(` ${fmt(billed(s.usage)).padStart(10)} ${pct(billed(s.usage), billed(u)).padStart(4)} ${t.report.sessionLine(s.id.slice(0, 8), s.usage.calls, s.usage.errors, fmt(withCache(s.usage)), sessionModel(s), when(s.start, s.end), s.title)}`);
|
|
320
321
|
}
|
|
321
322
|
}
|
|
322
323
|
out.push(t.report.top5(pct(top5, r.tokens), r.reads, r.rereads, pct(r.rereads, r.reads)));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agy-insights",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3",
|
|
4
4
|
"description": "Measures Antigravity CLI token usage and shows which context is sent repeatedly or needlessly, in English or Spanish. Run: npx agy-insights@latest",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|