@adecore/agents 0.41.0 → 0.42.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.
- package/dist/chat/chat-core.d.ts +1 -0
- package/dist/chat/chat-core.js +21 -3
- package/dist/chat/chat-store.d.ts +7 -1
- package/dist/chat/chat-store.js +68 -7
- package/dist/chat/errors.d.ts +1 -1
- package/dist/chat/payload-store.d.ts +28 -0
- package/dist/chat/payload-store.js +350 -0
- package/dist/chat/projector.js +9 -1
- package/dist/host/handlers.js +1 -0
- package/dist/providers/antigravity-models.json +1 -1
- package/dist/providers/claude-models.json +1 -1
- package/dist/providers/codex-models.json +1 -1
- package/dist/providers/copilot-models.json +1 -1
- package/dist/providers/grok-models.json +3 -3
- package/dist/usage/history-file.d.ts +25 -0
- package/dist/usage/history-file.js +72 -0
- package/dist/usage/index-file.d.ts +1 -1
- package/dist/usage/index-file.js +33 -114
- package/dist/usage/rows.d.ts +23 -0
- package/dist/usage/rows.js +94 -0
- package/dist/usage/scan-state.d.ts +21 -0
- package/dist/usage/scan-state.js +53 -0
- package/dist/usage/scanner.d.ts +1 -4
- package/dist/usage/scanner.js +37 -50
- package/dist/usage/usage-store.d.ts +26 -0
- package/dist/usage/usage-store.js +197 -0
- package/package.json +3 -3
- package/src/chat/chat-core.ts +27 -3
- package/src/chat/chat-store.ts +83 -8
- package/src/chat/errors.ts +2 -0
- package/src/chat/payload-store.ts +459 -0
- package/src/chat/projector.ts +11 -1
- package/src/host/handlers.ts +1 -0
- package/src/host/wiring.ts +1 -1
- package/src/providers/antigravity-models.json +1 -1
- package/src/providers/claude-models.json +1 -1
- package/src/providers/codex-models.json +1 -1
- package/src/providers/copilot-models.json +1 -1
- package/src/providers/grok-models.json +3 -3
- package/src/usage/history-file.ts +140 -0
- package/src/usage/index-file.ts +35 -143
- package/src/usage/rows.ts +136 -0
- package/src/usage/scan-state.ts +105 -0
- package/src/usage/scanner.ts +39 -54
- package/src/usage/usage-store.ts +237 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"updatedAt": "2026-10-
|
|
2
|
+
"updatedAt": "2026-10-27T14:38:15.597Z",
|
|
3
3
|
"defaultModel": "default",
|
|
4
4
|
"profiles": {
|
|
5
5
|
"agent": { "options": [], "contextWindowTokens": {} },
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
{ "slug": "default", "name": "Default", "profile": "agent" },
|
|
10
10
|
{ "slug": "grok-4.7", "name": "Grok 4.7", "profile": "agent" },
|
|
11
11
|
{ "slug": "grok-4.7-build-fast", "name": "Grok 4.7 Build Fast", "profile": "agent" },
|
|
12
|
-
{ "slug": "grok-4.6", "name": "Grok 4.6", "profile": "agent-256k" },
|
|
13
|
-
{ "slug": "grok-4.5", "name": "Grok 4.5", "profile": "agent-256k" }
|
|
12
|
+
{ "slug": "grok-4.6", "name": "Grok 4.6", "profile": "agent-256k", "legacy": true },
|
|
13
|
+
{ "slug": "grok-4.5", "name": "Grok 4.5", "profile": "agent-256k", "legacy": true }
|
|
14
14
|
]
|
|
15
15
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type UsageProvider } from '@adecore/agent-contracts';
|
|
2
|
+
import { type UsageRecord } from './record.ts';
|
|
3
|
+
export interface FileUsage {
|
|
4
|
+
provider: UsageProvider;
|
|
5
|
+
records: UsageRecord[];
|
|
6
|
+
tail: UsageRecord[];
|
|
7
|
+
}
|
|
8
|
+
export interface HistoryEntry {
|
|
9
|
+
path: string;
|
|
10
|
+
op: 'set' | 'add';
|
|
11
|
+
usage: FileUsage;
|
|
12
|
+
}
|
|
13
|
+
export interface HistoryHeader {
|
|
14
|
+
gen: string;
|
|
15
|
+
base: number;
|
|
16
|
+
}
|
|
17
|
+
export declare function encodeHeader(header: HistoryHeader): string;
|
|
18
|
+
export declare function encodeEntry(entry: HistoryEntry): string;
|
|
19
|
+
export declare function applyEntry(usage: Map<string, FileUsage>, entry: HistoryEntry): void;
|
|
20
|
+
export interface DecodedHistory {
|
|
21
|
+
header: HistoryHeader | null;
|
|
22
|
+
usage: Map<string, FileUsage>;
|
|
23
|
+
complete: number;
|
|
24
|
+
}
|
|
25
|
+
export declare function decodeHistory(bytes: Uint8Array): DecodedHistory;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { UsageProviderSchema } from '@adecore/agent-contracts';
|
|
2
|
+
import { foldByKey } from './record.js';
|
|
3
|
+
import { CURRENT_ROW, decodeRows, RowWriter } from './rows.js';
|
|
4
|
+
const HISTORY_VERSION = 1;
|
|
5
|
+
const NEWLINE = 0x0a;
|
|
6
|
+
export function encodeHeader(header) {
|
|
7
|
+
return `${JSON.stringify({ v: HISTORY_VERSION, gen: header.gen, base: header.base })}\n`;
|
|
8
|
+
}
|
|
9
|
+
export function encodeEntry(entry) {
|
|
10
|
+
const writer = new RowWriter();
|
|
11
|
+
const rows = entry.usage.records.map((record) => writer.row(record));
|
|
12
|
+
const tail = entry.usage.tail.map((record) => writer.row(record));
|
|
13
|
+
const { models, sessions, folders, accounts } = writer.tables;
|
|
14
|
+
const line = { p: entry.path, k: entry.usage.provider, op: entry.op, m: models, s: sessions, f: folders, a: accounts, r: rows, t: tail };
|
|
15
|
+
return `${JSON.stringify(line)}\n`;
|
|
16
|
+
}
|
|
17
|
+
function parseLine(text) {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(text);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function decodeHeader(text) {
|
|
26
|
+
const header = parseLine(text);
|
|
27
|
+
if (header?.v !== HISTORY_VERSION || typeof header.gen !== 'string' || typeof header.base !== 'number') {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
return { gen: header.gen, base: header.base };
|
|
31
|
+
}
|
|
32
|
+
function decodeEntry(text) {
|
|
33
|
+
const line = parseLine(text);
|
|
34
|
+
if (typeof line?.p !== 'string' || (line.op !== 'set' && line.op !== 'add') || !UsageProviderSchema.safeParse(line.k).success) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const provider = line.k;
|
|
38
|
+
const tables = { models: line.m, sessions: line.s, folders: line.f, accounts: line.a };
|
|
39
|
+
const records = decodeRows(line.r, tables, provider, CURRENT_ROW);
|
|
40
|
+
const tail = decodeRows(line.t, tables, provider, CURRENT_ROW);
|
|
41
|
+
if (records === null || tail === null) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return { path: line.p, op: line.op, usage: { provider, records, tail } };
|
|
45
|
+
}
|
|
46
|
+
/* Folds one line into what every path counted. */
|
|
47
|
+
export function applyEntry(usage, entry) {
|
|
48
|
+
const known = usage.get(entry.path);
|
|
49
|
+
if (entry.op === 'set' || known === undefined || known.provider !== entry.usage.provider) {
|
|
50
|
+
usage.set(entry.path, entry.usage);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
usage.set(entry.path, { provider: known.provider, records: foldByKey([...known.records, ...entry.usage.records]), tail: entry.usage.tail });
|
|
54
|
+
}
|
|
55
|
+
/*
|
|
56
|
+
* Every line stands alone, so a line that does not read back costs only that line. Only the end can
|
|
57
|
+
* be half-written, since lines are only ever appended.
|
|
58
|
+
*/
|
|
59
|
+
export function decodeHistory(bytes) {
|
|
60
|
+
const buffer = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
61
|
+
const complete = buffer.lastIndexOf(NEWLINE) + 1;
|
|
62
|
+
const lines = buffer.toString('utf8', 0, complete).split('\n');
|
|
63
|
+
const usage = new Map();
|
|
64
|
+
const header = complete === 0 ? null : decodeHeader(lines[0]);
|
|
65
|
+
for (let i = header === null ? 0 : 1; i < lines.length; i++) {
|
|
66
|
+
const entry = lines[i] === '' ? null : decodeEntry(lines[i]);
|
|
67
|
+
if (entry !== null) {
|
|
68
|
+
applyEntry(usage, entry);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { header, usage, complete };
|
|
72
|
+
}
|
|
@@ -15,4 +15,4 @@ export interface IndexedFile {
|
|
|
15
15
|
}
|
|
16
16
|
export type UsageIndex = Map<string, IndexedFile>;
|
|
17
17
|
export declare function encodeIndex(index: UsageIndex): string;
|
|
18
|
-
export declare function decodeIndex(text: string): UsageIndex;
|
|
18
|
+
export declare function decodeIndex(text: string): UsageIndex | null;
|
package/dist/usage/index-file.js
CHANGED
|
@@ -1,52 +1,15 @@
|
|
|
1
1
|
import { UsageProviderSchema } from '@adecore/agent-contracts';
|
|
2
|
-
|
|
2
|
+
import { CURRENT_ROW, decodeRows, RowWriter } from './rows.js';
|
|
3
|
+
/*
|
|
4
|
+
* The single index usage was kept in before history and scan state went apart. The store reads it
|
|
5
|
+
* once to migrate it; nothing writes it any more but the tests that need one to migrate.
|
|
6
|
+
*/
|
|
3
7
|
const INDEX_VERSION = 3;
|
|
4
8
|
/* Before accounts: every record was the default account's, so it reads as that without a scan. */
|
|
5
9
|
const ACCOUNTLESS_VERSION = 1;
|
|
6
|
-
class Interner {
|
|
7
|
-
values = [];
|
|
8
|
-
at = new Map();
|
|
9
|
-
index(value) {
|
|
10
|
-
const seen = this.at.get(value);
|
|
11
|
-
if (seen !== undefined) {
|
|
12
|
-
return seen;
|
|
13
|
-
}
|
|
14
|
-
const next = this.values.length;
|
|
15
|
-
this.values.push(value);
|
|
16
|
-
this.at.set(value, next);
|
|
17
|
-
return next;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
10
|
export function encodeIndex(index) {
|
|
21
|
-
const
|
|
22
|
-
const sessions = new Interner();
|
|
23
|
-
const folders = new Interner();
|
|
24
|
-
const accounts = new Interner();
|
|
11
|
+
const writer = new RowWriter();
|
|
25
12
|
const files = {};
|
|
26
|
-
const row = (record) => {
|
|
27
|
-
const counted = [
|
|
28
|
-
record.timestampMs,
|
|
29
|
-
models.index(record.model),
|
|
30
|
-
sessions.index(record.sessionId),
|
|
31
|
-
folders.index(record.cwd),
|
|
32
|
-
record.totals.calls,
|
|
33
|
-
record.totals.input,
|
|
34
|
-
record.totals.cacheRead,
|
|
35
|
-
record.totals.cacheWrite,
|
|
36
|
-
record.totals.cacheWrite1h,
|
|
37
|
-
record.totals.output,
|
|
38
|
-
record.totals.reasoning,
|
|
39
|
-
record.dedupeKey ?? 0,
|
|
40
|
-
record.account === undefined ? 0 : accounts.index(record.account) + 1
|
|
41
|
-
];
|
|
42
|
-
if (record.costUsd !== undefined || record.serviceTier !== undefined) {
|
|
43
|
-
counted[13] = record.costUsd;
|
|
44
|
-
}
|
|
45
|
-
if (record.serviceTier !== undefined) {
|
|
46
|
-
counted[14] = record.serviceTier;
|
|
47
|
-
}
|
|
48
|
-
return counted;
|
|
49
|
-
};
|
|
50
13
|
for (const [path, file] of index) {
|
|
51
14
|
files[path] = {
|
|
52
15
|
p: file.provider,
|
|
@@ -54,107 +17,63 @@ export function encodeIndex(index) {
|
|
|
54
17
|
m: file.mtimeMs,
|
|
55
18
|
o: file.offset,
|
|
56
19
|
c: file.codex,
|
|
57
|
-
r: file.records.map(row),
|
|
58
|
-
t: file.tail.map(row),
|
|
20
|
+
r: file.records.map((record) => writer.row(record)),
|
|
21
|
+
t: file.tail.map((record) => writer.row(record)),
|
|
59
22
|
...(file.head === null ? {} : { h: file.head }),
|
|
60
23
|
...(file.parserVersion === undefined ? {} : { v: file.parserVersion }),
|
|
61
24
|
...(file.compressed === undefined ? {} : { z: file.compressed })
|
|
62
25
|
};
|
|
63
26
|
}
|
|
64
|
-
const document = {
|
|
65
|
-
version: INDEX_VERSION,
|
|
66
|
-
models: models.values,
|
|
67
|
-
sessions: sessions.values,
|
|
68
|
-
folders: folders.values,
|
|
69
|
-
accounts: accounts.values,
|
|
70
|
-
files
|
|
71
|
-
};
|
|
27
|
+
const document = { version: INDEX_VERSION, ...writer.tables, files };
|
|
72
28
|
return JSON.stringify(document);
|
|
73
29
|
}
|
|
74
|
-
function isRow(row, version) {
|
|
75
|
-
return (Array.isArray(row) &&
|
|
76
|
-
(version === ACCOUNTLESS_VERSION ? row.length === 12 : row.length >= 13 && row.length <= (version === INDEX_VERSION ? 15 : 14)) &&
|
|
77
|
-
row.slice(0, 11).every((value) => typeof value === 'number') &&
|
|
78
|
-
(typeof row[11] === 'string' || row[11] === 0) &&
|
|
79
|
-
(version === ACCOUNTLESS_VERSION || typeof row[12] === 'number') &&
|
|
80
|
-
(row.length < 14 || row[13] === null || row[13] === undefined || typeof row[13] === 'number') &&
|
|
81
|
-
(row.length < 15 || typeof row[14] === 'string'));
|
|
82
|
-
}
|
|
83
30
|
/*
|
|
84
|
-
*
|
|
85
|
-
* so
|
|
31
|
+
* Null for text that is no index of a version this reads. A file whose rows do not read back is left
|
|
32
|
+
* out, so only that file's usage is missing from what migrates, never a wrong total.
|
|
86
33
|
*/
|
|
87
34
|
export function decodeIndex(text) {
|
|
88
|
-
const index = new Map();
|
|
89
35
|
let document;
|
|
90
36
|
try {
|
|
91
37
|
document = JSON.parse(text);
|
|
92
38
|
}
|
|
93
39
|
catch {
|
|
94
|
-
return
|
|
40
|
+
return null;
|
|
95
41
|
}
|
|
96
42
|
const version = document?.version;
|
|
97
43
|
if ((version !== INDEX_VERSION && version !== 2 && version !== ACCOUNTLESS_VERSION) ||
|
|
98
44
|
!Array.isArray(document.models) ||
|
|
99
45
|
!Array.isArray(document.sessions) ||
|
|
100
46
|
!Array.isArray(document.folders)) {
|
|
101
|
-
return
|
|
47
|
+
return null;
|
|
102
48
|
}
|
|
103
|
-
const
|
|
104
|
-
const
|
|
49
|
+
const index = new Map();
|
|
50
|
+
const tables = { ...document, accounts: Array.isArray(document.accounts) ? document.accounts : [] };
|
|
51
|
+
const shape = { accountless: version === ACCOUNTLESS_VERSION, maxLength: version === INDEX_VERSION ? CURRENT_ROW.maxLength : 14 };
|
|
105
52
|
for (const [path, file] of Object.entries(document.files ?? {})) {
|
|
106
53
|
// Every provider the schema knows, so a new one is never dropped from the index it was saved in.
|
|
107
54
|
if (!UsageProviderSchema.safeParse(file.p).success) {
|
|
108
55
|
continue;
|
|
109
56
|
}
|
|
110
|
-
if (typeof file.s !== 'number' || typeof file.m !== 'number' || typeof file.o !== 'number' || file.o < 0
|
|
57
|
+
if (typeof file.s !== 'number' || typeof file.m !== 'number' || typeof file.o !== 'number' || file.o < 0) {
|
|
111
58
|
continue;
|
|
112
59
|
}
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const valid = isRow(row, version);
|
|
118
|
-
const model = valid ? name(document.models, row[1]) : null;
|
|
119
|
-
const sessionId = valid ? name(document.sessions, row[2]) : null;
|
|
120
|
-
const cwd = valid ? name(document.folders, row[3]) : null;
|
|
121
|
-
const accountAt = valid ? (row[12] ?? 0) : 0;
|
|
122
|
-
const account = accountAt === 0 ? undefined : name(accounts, accountAt - 1);
|
|
123
|
-
if (!valid || model === null || sessionId === null || cwd === null || account === null) {
|
|
124
|
-
broken = true;
|
|
125
|
-
return records;
|
|
126
|
-
}
|
|
127
|
-
records.push({
|
|
128
|
-
provider: file.p,
|
|
129
|
-
timestampMs: row[0],
|
|
130
|
-
model,
|
|
131
|
-
sessionId,
|
|
132
|
-
cwd,
|
|
133
|
-
...(account === undefined ? {} : { account }),
|
|
134
|
-
totals: { calls: row[4], input: row[5], cacheRead: row[6], cacheWrite: row[7], cacheWrite1h: row[8], output: row[9], reasoning: row[10] },
|
|
135
|
-
...(typeof row[13] === 'number' ? { costUsd: row[13] } : {}),
|
|
136
|
-
...(typeof row[14] === 'string' ? { serviceTier: row[14] } : {}),
|
|
137
|
-
dedupeKey: row[11] === 0 ? null : row[11]
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
return records;
|
|
141
|
-
};
|
|
142
|
-
const records = decode(file.r);
|
|
143
|
-
const tail = decode(Array.isArray(file.t) ? file.t : []);
|
|
144
|
-
if (!broken) {
|
|
145
|
-
index.set(path, {
|
|
146
|
-
provider: file.p,
|
|
147
|
-
size: file.s,
|
|
148
|
-
mtimeMs: file.m,
|
|
149
|
-
offset: file.o,
|
|
150
|
-
records,
|
|
151
|
-
tail,
|
|
152
|
-
codex: file.c ?? null,
|
|
153
|
-
head: typeof file.h === 'string' ? file.h : null,
|
|
154
|
-
...(typeof file.v === 'number' ? { parserVersion: file.v } : {}),
|
|
155
|
-
...(typeof file.z === 'boolean' ? { compressed: file.z } : {})
|
|
156
|
-
});
|
|
60
|
+
const records = decodeRows(file.r, tables, file.p, shape);
|
|
61
|
+
const tail = decodeRows(Array.isArray(file.t) ? file.t : [], tables, file.p, shape);
|
|
62
|
+
if (records === null || tail === null) {
|
|
63
|
+
continue;
|
|
157
64
|
}
|
|
65
|
+
index.set(path, {
|
|
66
|
+
provider: file.p,
|
|
67
|
+
size: file.s,
|
|
68
|
+
mtimeMs: file.m,
|
|
69
|
+
offset: file.o,
|
|
70
|
+
records,
|
|
71
|
+
tail,
|
|
72
|
+
codex: file.c ?? null,
|
|
73
|
+
head: typeof file.h === 'string' ? file.h : null,
|
|
74
|
+
...(typeof file.v === 'number' ? { parserVersion: file.v } : {}),
|
|
75
|
+
...(typeof file.z === 'boolean' ? { compressed: file.z } : {})
|
|
76
|
+
});
|
|
158
77
|
}
|
|
159
78
|
return index;
|
|
160
79
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { UsageProvider } from '@adecore/agent-contracts';
|
|
2
|
+
import type { UsageRecord } from './record.ts';
|
|
3
|
+
export type RecordRow = [number, number, number, number, number, number, number, number, number, number, number, string | 0, number?, number?, string?];
|
|
4
|
+
export interface RowTables {
|
|
5
|
+
models: string[];
|
|
6
|
+
sessions: string[];
|
|
7
|
+
folders: string[];
|
|
8
|
+
accounts: string[];
|
|
9
|
+
}
|
|
10
|
+
export declare class RowWriter {
|
|
11
|
+
private readonly models;
|
|
12
|
+
private readonly sessions;
|
|
13
|
+
private readonly folders;
|
|
14
|
+
private readonly accounts;
|
|
15
|
+
get tables(): RowTables;
|
|
16
|
+
row(record: UsageRecord): RecordRow;
|
|
17
|
+
}
|
|
18
|
+
export interface RowShape {
|
|
19
|
+
accountless: boolean;
|
|
20
|
+
maxLength: number;
|
|
21
|
+
}
|
|
22
|
+
export declare const CURRENT_ROW: RowShape;
|
|
23
|
+
export declare function decodeRows(rows: unknown, tables: Partial<Record<keyof RowTables, unknown>>, provider: UsageProvider, shape: RowShape): UsageRecord[] | null;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
class Interner {
|
|
2
|
+
values = [];
|
|
3
|
+
at = new Map();
|
|
4
|
+
index(value) {
|
|
5
|
+
const seen = this.at.get(value);
|
|
6
|
+
if (seen !== undefined) {
|
|
7
|
+
return seen;
|
|
8
|
+
}
|
|
9
|
+
const next = this.values.length;
|
|
10
|
+
this.values.push(value);
|
|
11
|
+
this.at.set(value, next);
|
|
12
|
+
return next;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/* Turns records into rows, collecting the names they share into one set of tables. */
|
|
16
|
+
export class RowWriter {
|
|
17
|
+
models = new Interner();
|
|
18
|
+
sessions = new Interner();
|
|
19
|
+
folders = new Interner();
|
|
20
|
+
accounts = new Interner();
|
|
21
|
+
get tables() {
|
|
22
|
+
return { models: this.models.values, sessions: this.sessions.values, folders: this.folders.values, accounts: this.accounts.values };
|
|
23
|
+
}
|
|
24
|
+
row(record) {
|
|
25
|
+
const counted = [
|
|
26
|
+
record.timestampMs,
|
|
27
|
+
this.models.index(record.model),
|
|
28
|
+
this.sessions.index(record.sessionId),
|
|
29
|
+
this.folders.index(record.cwd),
|
|
30
|
+
record.totals.calls,
|
|
31
|
+
record.totals.input,
|
|
32
|
+
record.totals.cacheRead,
|
|
33
|
+
record.totals.cacheWrite,
|
|
34
|
+
record.totals.cacheWrite1h,
|
|
35
|
+
record.totals.output,
|
|
36
|
+
record.totals.reasoning,
|
|
37
|
+
record.dedupeKey ?? 0,
|
|
38
|
+
record.account === undefined ? 0 : this.accounts.index(record.account) + 1
|
|
39
|
+
];
|
|
40
|
+
if (record.costUsd !== undefined || record.serviceTier !== undefined) {
|
|
41
|
+
counted[13] = record.costUsd;
|
|
42
|
+
}
|
|
43
|
+
if (record.serviceTier !== undefined) {
|
|
44
|
+
counted[14] = record.serviceTier;
|
|
45
|
+
}
|
|
46
|
+
return counted;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export const CURRENT_ROW = { accountless: false, maxLength: 15 };
|
|
50
|
+
function isRow(row, shape) {
|
|
51
|
+
return (Array.isArray(row) &&
|
|
52
|
+
(shape.accountless ? row.length === 12 : row.length >= 13 && row.length <= shape.maxLength) &&
|
|
53
|
+
row.slice(0, 11).every((value) => typeof value === 'number') &&
|
|
54
|
+
(typeof row[11] === 'string' || row[11] === 0) &&
|
|
55
|
+
(shape.accountless || typeof row[12] === 'number') &&
|
|
56
|
+
(row.length < 14 || row[13] === null || row[13] === undefined || typeof row[13] === 'number') &&
|
|
57
|
+
(row.length < 15 || typeof row[14] === 'string'));
|
|
58
|
+
}
|
|
59
|
+
function nameAt(table, at) {
|
|
60
|
+
return Array.isArray(table) && typeof table[at] === 'string' ? table[at] : null;
|
|
61
|
+
}
|
|
62
|
+
/* The records of `rows`, or null when any row does not read back against its tables. */
|
|
63
|
+
export function decodeRows(rows, tables, provider, shape) {
|
|
64
|
+
if (!Array.isArray(rows)) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const records = [];
|
|
68
|
+
for (const row of rows) {
|
|
69
|
+
if (!isRow(row, shape)) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
const model = nameAt(tables.models, row[1]);
|
|
73
|
+
const sessionId = nameAt(tables.sessions, row[2]);
|
|
74
|
+
const cwd = nameAt(tables.folders, row[3]);
|
|
75
|
+
const accountAt = row[12] ?? 0;
|
|
76
|
+
const account = accountAt === 0 ? undefined : nameAt(tables.accounts, accountAt - 1);
|
|
77
|
+
if (model === null || sessionId === null || cwd === null || account === null) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
records.push({
|
|
81
|
+
provider,
|
|
82
|
+
timestampMs: row[0],
|
|
83
|
+
model,
|
|
84
|
+
sessionId,
|
|
85
|
+
cwd,
|
|
86
|
+
...(account === undefined ? {} : { account }),
|
|
87
|
+
totals: { calls: row[4], input: row[5], cacheRead: row[6], cacheWrite: row[7], cacheWrite1h: row[8], output: row[9], reasoning: row[10] },
|
|
88
|
+
...(typeof row[13] === 'number' ? { costUsd: row[13] } : {}),
|
|
89
|
+
...(typeof row[14] === 'string' ? { serviceTier: row[14] } : {}),
|
|
90
|
+
dedupeKey: row[11] === 0 ? null : row[11]
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return records;
|
|
94
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type UsageProvider } from '@adecore/agent-contracts';
|
|
2
|
+
import type { CodexParserState } from './readers/codex.ts';
|
|
3
|
+
export interface ScanState {
|
|
4
|
+
provider: UsageProvider;
|
|
5
|
+
size: number;
|
|
6
|
+
mtimeMs: number;
|
|
7
|
+
offset: number;
|
|
8
|
+
codex: CodexParserState | null;
|
|
9
|
+
head: string | null;
|
|
10
|
+
parserVersion?: number;
|
|
11
|
+
compressed?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface HistoryMark {
|
|
14
|
+
gen: string;
|
|
15
|
+
bytes: number;
|
|
16
|
+
}
|
|
17
|
+
export declare function encodeState(state: ReadonlyMap<string, ScanState>, history: HistoryMark): string;
|
|
18
|
+
export declare function decodeState(text: string): {
|
|
19
|
+
history: HistoryMark;
|
|
20
|
+
files: Map<string, ScanState>;
|
|
21
|
+
} | null;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { UsageProviderSchema } from '@adecore/agent-contracts';
|
|
2
|
+
const STATE_VERSION = 1;
|
|
3
|
+
export function encodeState(state, history) {
|
|
4
|
+
const files = {};
|
|
5
|
+
for (const [path, file] of state) {
|
|
6
|
+
files[path] = {
|
|
7
|
+
p: file.provider,
|
|
8
|
+
s: file.size,
|
|
9
|
+
m: file.mtimeMs,
|
|
10
|
+
o: file.offset,
|
|
11
|
+
c: file.codex,
|
|
12
|
+
...(file.head === null ? {} : { h: file.head }),
|
|
13
|
+
...(file.parserVersion === undefined ? {} : { v: file.parserVersion }),
|
|
14
|
+
...(file.compressed === undefined ? {} : { z: file.compressed })
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
const document = { version: STATE_VERSION, history, files };
|
|
18
|
+
return JSON.stringify(document);
|
|
19
|
+
}
|
|
20
|
+
/* Null for text that is no state of this version; an entry that does not read only costs a read of its transcript. */
|
|
21
|
+
export function decodeState(text) {
|
|
22
|
+
let document;
|
|
23
|
+
try {
|
|
24
|
+
document = JSON.parse(text);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const history = document?.history;
|
|
30
|
+
if (document?.version !== STATE_VERSION || typeof history?.gen !== 'string' || typeof history.bytes !== 'number') {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const files = new Map();
|
|
34
|
+
for (const [path, file] of Object.entries(document.files ?? {})) {
|
|
35
|
+
if (!UsageProviderSchema.safeParse(file.p).success) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (typeof file.s !== 'number' || typeof file.m !== 'number' || typeof file.o !== 'number' || file.o < 0) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
files.set(path, {
|
|
42
|
+
provider: file.p,
|
|
43
|
+
size: file.s,
|
|
44
|
+
mtimeMs: file.m,
|
|
45
|
+
offset: file.o,
|
|
46
|
+
codex: file.c ?? null,
|
|
47
|
+
head: typeof file.h === 'string' ? file.h : null,
|
|
48
|
+
...(typeof file.v === 'number' ? { parserVersion: file.v } : {}),
|
|
49
|
+
...(typeof file.z === 'boolean' ? { compressed: file.z } : {})
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return { history: { gen: history.gen, bytes: history.bytes }, files };
|
|
53
|
+
}
|
package/dist/usage/scanner.d.ts
CHANGED
|
@@ -20,14 +20,11 @@ export interface UsageScannerOptions {
|
|
|
20
20
|
type AccountOf = (record: UsageRecord) => string | undefined;
|
|
21
21
|
export declare function accountResolver(root: UsageRootPath, sessionAccounts: () => ReadonlyMap<string, string>): AccountOf;
|
|
22
22
|
export declare class UsageScanner {
|
|
23
|
-
private readonly home;
|
|
24
23
|
private readonly roots;
|
|
25
24
|
private readonly sessionAccounts;
|
|
26
|
-
private
|
|
25
|
+
private readonly store;
|
|
27
26
|
private loaded;
|
|
28
|
-
private dirty;
|
|
29
27
|
constructor(home: string, roots?: UsageRootPath[] | (() => UsageRootPath[]), options?: UsageScannerOptions);
|
|
30
|
-
private get file();
|
|
31
28
|
load(): Promise<void>;
|
|
32
29
|
scan(): Promise<ScanReport>;
|
|
33
30
|
records(): UsageRecord[];
|