agy-insights 0.8.1 → 0.8.2
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 +1 -1
- package/README.md +1 -1
- package/docs/how-it-works.md +1 -1
- package/lib/history.js +61 -5
- 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.8.
|
|
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.8.2`.
|
|
40
40
|
|
|
41
41
|
**Instalándolo globalmente:**
|
|
42
42
|
|
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.8.
|
|
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.8.2`.
|
|
40
40
|
|
|
41
41
|
**Globally:**
|
|
42
42
|
|
package/docs/how-it-works.md
CHANGED
|
@@ -35,7 +35,7 @@ It is undocumented; this is what has been observed in `agy` 1.4.x.
|
|
|
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
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
|
-
- 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.
|
|
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. Some logs (seen with the VS Code plugin) have `PLANNER_RESPONSE` lines without any token field; those are not counted as 0 tokens. The same figures are also in each `gen_metadata` row, at protobuf field 1 › 4 (2 = input, 3 = output, 5 = cache read; the model's display name is at 1 › 21). When the transcript has no counts, or there is no transcript, `usageFromMeta()` takes real usage from there, keeping the day and selected model of the transcript's n-th call when there is one.
|
|
39
39
|
|
|
40
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
|
|
package/lib/history.js
CHANGED
|
@@ -114,6 +114,39 @@ function collect(blob) {
|
|
|
114
114
|
return out;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
// The top-level fields of one protobuf message: tag -> varint value or bytes (last one wins). Null if malformed.
|
|
118
|
+
function fields(buf) {
|
|
119
|
+
const out = new Map();
|
|
120
|
+
let i = 0;
|
|
121
|
+
const varint = () => {
|
|
122
|
+
let v = 0, s = 0, b;
|
|
123
|
+
do { if (i >= buf.length) throw 0; b = buf[i++]; v += (b & 0x7f) * 2 ** s; s += 7; } while (b & 0x80);
|
|
124
|
+
return v;
|
|
125
|
+
};
|
|
126
|
+
try {
|
|
127
|
+
while (i < buf.length) {
|
|
128
|
+
const k = varint(), tag = Math.floor(k / 8), wt = k & 7;
|
|
129
|
+
if (wt === 0) out.set(tag, varint());
|
|
130
|
+
else if (wt === 1) i += 8;
|
|
131
|
+
else if (wt === 5) i += 4;
|
|
132
|
+
else if (wt === 2) { const len = varint(); if (i + len > buf.length) throw 0; out.set(tag, buf.subarray(i, i + len)); i += len; } else throw 0;
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
} catch { return null; }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Token counts of one gen_metadata row (one model call): field 1 › 4 holds 2 = input, 3 = output,
|
|
139
|
+
// 5 = cache read, the same figures the transcript logs. Null when the row has none: some logs (seen
|
|
140
|
+
// with the VS Code plugin) carry no token counts, and this is then the only place that has them.
|
|
141
|
+
function metaUsage(blob) {
|
|
142
|
+
const top = blob && fields(Buffer.from(blob));
|
|
143
|
+
const call = Buffer.isBuffer(top?.get(1)) && fields(top.get(1));
|
|
144
|
+
const u = Buffer.isBuffer(call?.get(4)) && fields(call.get(4));
|
|
145
|
+
if (!u || !(u.has(2) || u.has(3))) return null;
|
|
146
|
+
const n = (k) => (typeof u.get(k) === 'number' ? u.get(k) : 0);
|
|
147
|
+
return { input: n(2), output: n(3), cache: n(5) };
|
|
148
|
+
}
|
|
149
|
+
|
|
117
150
|
// A tool call is stored as the tool name followed (within a few strings) by its JSON arguments,
|
|
118
151
|
// e.g. "view_file" + {"AbsolutePath":"C:\\dev\\...\\X.cs","StartLine":301,"EndLine":380}.
|
|
119
152
|
// The call id ("call_5859111") just before the name identifies the call: agy stores each call twice,
|
|
@@ -242,7 +275,7 @@ function realUsage(dir, id) {
|
|
|
242
275
|
if (!['input_tokens', 'cache_read_tokens', 'output_tokens'].some((k) => k in e)) {
|
|
243
276
|
u.unmetered++;
|
|
244
277
|
for (const k of Object.keys(e)) if (k.length < 40) u.unmeteredKeys.add(k);
|
|
245
|
-
u.perCall.push({ metered: false, model: selected });
|
|
278
|
+
u.perCall.push({ metered: false, model: selected, at });
|
|
246
279
|
continue;
|
|
247
280
|
}
|
|
248
281
|
u.calls++;
|
|
@@ -261,6 +294,27 @@ function realUsage(dir, id) {
|
|
|
261
294
|
return u;
|
|
262
295
|
}
|
|
263
296
|
|
|
297
|
+
// Real usage rebuilt from gen_metadata, one row per model call, for conversations whose log has no token
|
|
298
|
+
// counts (or no log). Each row's call keeps the model and day of the log's n-th call when there is one.
|
|
299
|
+
function usageFromMeta(blobs, log, end) {
|
|
300
|
+
const rows = blobs.map(metaUsage);
|
|
301
|
+
if (!rows.some(Boolean)) return null;
|
|
302
|
+
const u = { calls: 0, input: 0, cache: 0, output: 0, errors: log?.errors || 0, start: log?.start ?? null, end: log?.end ?? null,
|
|
303
|
+
days: new Map(), perCall: [], unmetered: 0, unmeteredKeys: new Set(), unrecognized: log?.unrecognized || new Set(), fromMeta: true };
|
|
304
|
+
rows.forEach((x, i) => {
|
|
305
|
+
const from = log?.perCall[i];
|
|
306
|
+
if (!x) { u.perCall.push({ metered: false, model: from?.model || null }); return; }
|
|
307
|
+
u.calls++;
|
|
308
|
+
u.input += x.input; u.cache += x.cache; u.output += x.output;
|
|
309
|
+
u.perCall.push({ metered: true, ...x, model: from?.model || null });
|
|
310
|
+
const k = dayKey(from?.at || log?.end || end);
|
|
311
|
+
const d = u.days.get(k) || { calls: 0, input: 0, cache: 0, output: 0 };
|
|
312
|
+
d.calls++; d.input += x.input; d.cache += x.cache; d.output += x.output;
|
|
313
|
+
u.days.set(k, d);
|
|
314
|
+
});
|
|
315
|
+
return u;
|
|
316
|
+
}
|
|
317
|
+
|
|
264
318
|
// --- Titles ---
|
|
265
319
|
|
|
266
320
|
// The short title agy gives each conversation ("Analyzing Package Lock Dependencies"), from
|
|
@@ -329,8 +383,8 @@ async function analyze({ dir = DEFAULT_DIR, workspaces = [], customKinds = {}, s
|
|
|
329
383
|
const log = realUsage(dir, id);
|
|
330
384
|
const end = log?.end ?? fs.statSync(path.join(dir, f)).mtimeMs;
|
|
331
385
|
const start = log?.start ?? end;
|
|
332
|
-
// Real usage
|
|
333
|
-
|
|
386
|
+
// Real usage from the log when it has token counts; else from gen_metadata (below), else the estimate.
|
|
387
|
+
let usage = log?.calls ? log : null;
|
|
334
388
|
if (since && end < since) { r.olderSkipped++; continue; }
|
|
335
389
|
let db;
|
|
336
390
|
try { db = await openDb(path.join(dir, f), tmp); } catch { r.unreadable++; continue; }
|
|
@@ -390,6 +444,8 @@ async function analyze({ dir = DEFAULT_DIR, workspaces = [], customKinds = {}, s
|
|
|
390
444
|
files.add(key(file), { ...fields, convs: once(seenFiles, key(file)) }, { abs: file, rel, root, project, kind: k });
|
|
391
445
|
});
|
|
392
446
|
}
|
|
447
|
+
const metaBlobs = db.all('select data from gen_metadata order by idx').map(([blob]) => blob);
|
|
448
|
+
if (!usage) usage = usageFromMeta(metaBlobs, log, end);
|
|
393
449
|
if (usage) {
|
|
394
450
|
r.usage.conversations++;
|
|
395
451
|
for (const k of ['calls', 'input', 'cache', 'output', 'errors']) r.usage[k] += usage[k];
|
|
@@ -404,9 +460,9 @@ async function analyze({ dir = DEFAULT_DIR, workspaces = [], customKinds = {}, s
|
|
|
404
460
|
// Model of each call. gen_metadata usually has one row per call, in the transcript's order; when the
|
|
405
461
|
// counts match, the n-th row is the n-th call. When they don't, pairing by position would mislabel
|
|
406
462
|
// calls, so the model selected in the transcript is used instead, or the conversation's only model.
|
|
407
|
-
const metaStrs =
|
|
463
|
+
const metaStrs = metaBlobs.map(collect);
|
|
408
464
|
const metaRows = metaStrs.map(modelOf);
|
|
409
|
-
const calls = log ? log.perCall : metaRows.map(() => null);
|
|
465
|
+
const calls = usage?.fromMeta ? usage.perCall : log ? log.perCall : metaRows.map(() => null);
|
|
410
466
|
const aligned = metaRows.length === calls.length;
|
|
411
467
|
const rowModels = new Set(metaRows.filter((m) => m.model).map(modelKey));
|
|
412
468
|
const onlyModel = rowModels.size === 1 ? metaRows.find((m) => m.model) : null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agy-insights",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
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": [
|