@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.
Files changed (45) hide show
  1. package/dist/chat/chat-core.d.ts +1 -0
  2. package/dist/chat/chat-core.js +21 -3
  3. package/dist/chat/chat-store.d.ts +7 -1
  4. package/dist/chat/chat-store.js +68 -7
  5. package/dist/chat/errors.d.ts +1 -1
  6. package/dist/chat/payload-store.d.ts +28 -0
  7. package/dist/chat/payload-store.js +350 -0
  8. package/dist/chat/projector.js +9 -1
  9. package/dist/host/handlers.js +1 -0
  10. package/dist/providers/antigravity-models.json +1 -1
  11. package/dist/providers/claude-models.json +1 -1
  12. package/dist/providers/codex-models.json +1 -1
  13. package/dist/providers/copilot-models.json +1 -1
  14. package/dist/providers/grok-models.json +3 -3
  15. package/dist/usage/history-file.d.ts +25 -0
  16. package/dist/usage/history-file.js +72 -0
  17. package/dist/usage/index-file.d.ts +1 -1
  18. package/dist/usage/index-file.js +33 -114
  19. package/dist/usage/rows.d.ts +23 -0
  20. package/dist/usage/rows.js +94 -0
  21. package/dist/usage/scan-state.d.ts +21 -0
  22. package/dist/usage/scan-state.js +53 -0
  23. package/dist/usage/scanner.d.ts +1 -4
  24. package/dist/usage/scanner.js +37 -50
  25. package/dist/usage/usage-store.d.ts +26 -0
  26. package/dist/usage/usage-store.js +197 -0
  27. package/package.json +3 -3
  28. package/src/chat/chat-core.ts +27 -3
  29. package/src/chat/chat-store.ts +83 -8
  30. package/src/chat/errors.ts +2 -0
  31. package/src/chat/payload-store.ts +459 -0
  32. package/src/chat/projector.ts +11 -1
  33. package/src/host/handlers.ts +1 -0
  34. package/src/host/wiring.ts +1 -1
  35. package/src/providers/antigravity-models.json +1 -1
  36. package/src/providers/claude-models.json +1 -1
  37. package/src/providers/codex-models.json +1 -1
  38. package/src/providers/copilot-models.json +1 -1
  39. package/src/providers/grok-models.json +3 -3
  40. package/src/usage/history-file.ts +140 -0
  41. package/src/usage/index-file.ts +35 -143
  42. package/src/usage/rows.ts +136 -0
  43. package/src/usage/scan-state.ts +105 -0
  44. package/src/usage/scanner.ts +39 -54
  45. package/src/usage/usage-store.ts +237 -0
@@ -1,8 +1,12 @@
1
1
  import { UsageProviderSchema, type UsageProvider } from '@adecore/agent-contracts';
2
2
  import type { UsageRecord } from './record.ts';
3
3
  import type { CodexParserState } from './readers/codex.ts';
4
+ import { CURRENT_ROW, decodeRows, RowWriter, type RecordRow } from './rows.ts';
4
5
 
5
- /* Old records survive deleted transcripts; the scanner reparses existing Codex files with its current reader. */
6
+ /*
7
+ * The single index usage was kept in before history and scan state went apart. The store reads it
8
+ * once to migrate it; nothing writes it any more but the tests that need one to migrate.
9
+ */
6
10
  const INDEX_VERSION = 3;
7
11
  /* Before accounts: every record was the default account's, so it reads as that without a scan. */
8
12
  const ACCOUNTLESS_VERSION = 1;
@@ -11,15 +15,10 @@ export interface IndexedFile {
11
15
  provider: UsageProvider;
12
16
  size: number;
13
17
  mtimeMs: number;
14
- /* Just past the last newline that was read, so a file that grew is read from there. */
15
18
  offset: number;
16
19
  records: UsageRecord[];
17
- /* What the line still being written at the end of the file says; read again on the next scan,
18
- so it is kept apart from the records the offset has already accounted for. */
19
20
  tail: UsageRecord[];
20
- /* What the reader has to remember to carry on halfway through a Codex file. */
21
21
  codex: CodexParserState | null;
22
- /* A versioned hash of all bytes before the offset; absent in indexes that cannot verify an append. */
23
22
  head: string | null;
24
23
  parserVersion?: number;
25
24
  compressed?: boolean;
@@ -27,9 +26,6 @@ export interface IndexedFile {
27
26
 
28
27
  export type UsageIndex = Map<string, IndexedFile>;
29
28
 
30
- /* Interned names keep the index compact; optional cost and tier fields preserve per-call pricing. */
31
- type RecordRow = [number, number, number, number, number, number, number, number, number, number, number, string | 0, number?, number?, string?];
32
-
33
29
  interface FileRow {
34
30
  p: UsageProvider;
35
31
  s: number;
@@ -52,60 +48,10 @@ interface IndexFile {
52
48
  files: Record<string, FileRow>;
53
49
  }
54
50
 
55
- class Interner {
56
- readonly values: string[] = [];
57
- private readonly at = new Map<string, number>();
58
-
59
- index(value: string): number {
60
- const seen = this.at.get(value);
61
-
62
- if (seen !== undefined) {
63
- return seen;
64
- }
65
-
66
- const next = this.values.length;
67
- this.values.push(value);
68
- this.at.set(value, next);
69
-
70
- return next;
71
- }
72
- }
73
-
74
51
  export function encodeIndex(index: UsageIndex): string {
75
- const models = new Interner();
76
- const sessions = new Interner();
77
- const folders = new Interner();
78
- const accounts = new Interner();
52
+ const writer = new RowWriter();
79
53
  const files: Record<string, FileRow> = {};
80
54
 
81
- const row = (record: UsageRecord): RecordRow => {
82
- const counted: RecordRow = [
83
- record.timestampMs,
84
- models.index(record.model),
85
- sessions.index(record.sessionId),
86
- folders.index(record.cwd),
87
- record.totals.calls,
88
- record.totals.input,
89
- record.totals.cacheRead,
90
- record.totals.cacheWrite,
91
- record.totals.cacheWrite1h,
92
- record.totals.output,
93
- record.totals.reasoning,
94
- record.dedupeKey ?? 0,
95
- record.account === undefined ? 0 : accounts.index(record.account) + 1
96
- ];
97
-
98
- if (record.costUsd !== undefined || record.serviceTier !== undefined) {
99
- counted[13] = record.costUsd;
100
- }
101
-
102
- if (record.serviceTier !== undefined) {
103
- counted[14] = record.serviceTier;
104
- }
105
-
106
- return counted;
107
- };
108
-
109
55
  for (const [path, file] of index) {
110
56
  files[path] = {
111
57
  p: file.provider,
@@ -113,50 +59,30 @@ export function encodeIndex(index: UsageIndex): string {
113
59
  m: file.mtimeMs,
114
60
  o: file.offset,
115
61
  c: file.codex,
116
- r: file.records.map(row),
117
- t: file.tail.map(row),
62
+ r: file.records.map((record) => writer.row(record)),
63
+ t: file.tail.map((record) => writer.row(record)),
118
64
  ...(file.head === null ? {} : { h: file.head }),
119
65
  ...(file.parserVersion === undefined ? {} : { v: file.parserVersion }),
120
66
  ...(file.compressed === undefined ? {} : { z: file.compressed })
121
67
  };
122
68
  }
123
69
 
124
- const document: IndexFile = {
125
- version: INDEX_VERSION,
126
- models: models.values,
127
- sessions: sessions.values,
128
- folders: folders.values,
129
- accounts: accounts.values,
130
- files
131
- };
70
+ const document: IndexFile = { version: INDEX_VERSION, ...writer.tables, files };
132
71
 
133
72
  return JSON.stringify(document);
134
73
  }
135
74
 
136
- function isRow(row: unknown, version: number): row is RecordRow {
137
- return (
138
- Array.isArray(row) &&
139
- (version === ACCOUNTLESS_VERSION ? row.length === 12 : row.length >= 13 && row.length <= (version === INDEX_VERSION ? 15 : 14)) &&
140
- row.slice(0, 11).every((value) => typeof value === 'number') &&
141
- (typeof row[11] === 'string' || row[11] === 0) &&
142
- (version === ACCOUNTLESS_VERSION || typeof row[12] === 'number') &&
143
- (row.length < 14 || row[13] === null || row[13] === undefined || typeof row[13] === 'number') &&
144
- (row.length < 15 || typeof row[14] === 'string')
145
- );
146
- }
147
-
148
75
  /*
149
- * A row that does not read back costs a cold parse of the file it belonged to, never a wrong total,
150
- * so anything unexpected drops that file's entry rather than being repaired.
76
+ * Null for text that is no index of a version this reads. A file whose rows do not read back is left
77
+ * out, so only that file's usage is missing from what migrates, never a wrong total.
151
78
  */
152
- export function decodeIndex(text: string): UsageIndex {
153
- const index: UsageIndex = new Map();
79
+ export function decodeIndex(text: string): UsageIndex | null {
154
80
  let document: IndexFile;
155
81
 
156
82
  try {
157
83
  document = JSON.parse(text) as IndexFile;
158
84
  } catch {
159
- return index;
85
+ return null;
160
86
  }
161
87
 
162
88
  const version = document?.version;
@@ -167,12 +93,12 @@ export function decodeIndex(text: string): UsageIndex {
167
93
  !Array.isArray(document.sessions) ||
168
94
  !Array.isArray(document.folders)
169
95
  ) {
170
- return index;
96
+ return null;
171
97
  }
172
98
 
173
- const accounts = Array.isArray(document.accounts) ? document.accounts : [];
174
-
175
- const name = (table: string[], at: number): string | null => (typeof table[at] === 'string' ? table[at] : null);
99
+ const index: UsageIndex = new Map();
100
+ const tables = { ...document, accounts: Array.isArray(document.accounts) ? document.accounts : [] };
101
+ const shape = { accountless: version === ACCOUNTLESS_VERSION, maxLength: version === INDEX_VERSION ? CURRENT_ROW.maxLength : 14 };
176
102
 
177
103
  for (const [path, file] of Object.entries(document.files ?? {})) {
178
104
  // Every provider the schema knows, so a new one is never dropped from the index it was saved in.
@@ -180,63 +106,29 @@ export function decodeIndex(text: string): UsageIndex {
180
106
  continue;
181
107
  }
182
108
 
183
- if (typeof file.s !== 'number' || typeof file.m !== 'number' || typeof file.o !== 'number' || file.o < 0 || !Array.isArray(file.r)) {
109
+ if (typeof file.s !== 'number' || typeof file.m !== 'number' || typeof file.o !== 'number' || file.o < 0) {
184
110
  continue;
185
111
  }
186
112
 
187
- let broken = false;
188
-
189
- const decode = (rows: RecordRow[]): UsageRecord[] => {
190
- const records: UsageRecord[] = [];
191
-
192
- for (const row of rows) {
193
- const valid = isRow(row, version);
194
- const model = valid ? name(document.models, row[1]) : null;
195
- const sessionId = valid ? name(document.sessions, row[2]) : null;
196
- const cwd = valid ? name(document.folders, row[3]) : null;
197
- const accountAt = valid ? (row[12] ?? 0) : 0;
198
- const account = accountAt === 0 ? undefined : name(accounts, accountAt - 1);
199
-
200
- if (!valid || model === null || sessionId === null || cwd === null || account === null) {
201
- broken = true;
202
-
203
- return records;
204
- }
205
-
206
- records.push({
207
- provider: file.p,
208
- timestampMs: row[0],
209
- model,
210
- sessionId,
211
- cwd,
212
- ...(account === undefined ? {} : { account }),
213
- totals: { calls: row[4], input: row[5], cacheRead: row[6], cacheWrite: row[7], cacheWrite1h: row[8], output: row[9], reasoning: row[10] },
214
- ...(typeof row[13] === 'number' ? { costUsd: row[13] } : {}),
215
- ...(typeof row[14] === 'string' ? { serviceTier: row[14] } : {}),
216
- dedupeKey: row[11] === 0 ? null : row[11]
217
- });
218
- }
219
-
220
- return records;
221
- };
113
+ const records = decodeRows(file.r, tables, file.p, shape);
114
+ const tail = decodeRows(Array.isArray(file.t) ? file.t : [], tables, file.p, shape);
222
115
 
223
- const records = decode(file.r);
224
- const tail = decode(Array.isArray(file.t) ? file.t : []);
225
-
226
- if (!broken) {
227
- index.set(path, {
228
- provider: file.p,
229
- size: file.s,
230
- mtimeMs: file.m,
231
- offset: file.o,
232
- records,
233
- tail,
234
- codex: file.c ?? null,
235
- head: typeof file.h === 'string' ? file.h : null,
236
- ...(typeof file.v === 'number' ? { parserVersion: file.v } : {}),
237
- ...(typeof file.z === 'boolean' ? { compressed: file.z } : {})
238
- });
116
+ if (records === null || tail === null) {
117
+ continue;
239
118
  }
119
+
120
+ index.set(path, {
121
+ provider: file.p,
122
+ size: file.s,
123
+ mtimeMs: file.m,
124
+ offset: file.o,
125
+ records,
126
+ tail,
127
+ codex: file.c ?? null,
128
+ head: typeof file.h === 'string' ? file.h : null,
129
+ ...(typeof file.v === 'number' ? { parserVersion: file.v } : {}),
130
+ ...(typeof file.z === 'boolean' ? { compressed: file.z } : {})
131
+ });
240
132
  }
241
133
 
242
134
  return index;
@@ -0,0 +1,136 @@
1
+ import type { UsageProvider } from '@adecore/agent-contracts';
2
+ import type { UsageRecord } from './record.ts';
3
+
4
+ /* Interned names keep the files compact; optional cost and tier fields preserve per-call pricing. */
5
+ export type RecordRow = [number, number, number, number, number, number, number, number, number, number, number, string | 0, number?, number?, string?];
6
+
7
+ /* The names the rows of one document point into. */
8
+ export interface RowTables {
9
+ models: string[];
10
+ sessions: string[];
11
+ folders: string[];
12
+ accounts: string[];
13
+ }
14
+
15
+ class Interner {
16
+ readonly values: string[] = [];
17
+ private readonly at = new Map<string, number>();
18
+
19
+ index(value: string): number {
20
+ const seen = this.at.get(value);
21
+
22
+ if (seen !== undefined) {
23
+ return seen;
24
+ }
25
+
26
+ const next = this.values.length;
27
+ this.values.push(value);
28
+ this.at.set(value, next);
29
+
30
+ return next;
31
+ }
32
+ }
33
+
34
+ /* Turns records into rows, collecting the names they share into one set of tables. */
35
+ export class RowWriter {
36
+ private readonly models = new Interner();
37
+ private readonly sessions = new Interner();
38
+ private readonly folders = new Interner();
39
+ private readonly accounts = new Interner();
40
+
41
+ get tables(): RowTables {
42
+ return { models: this.models.values, sessions: this.sessions.values, folders: this.folders.values, accounts: this.accounts.values };
43
+ }
44
+
45
+ row(record: UsageRecord): RecordRow {
46
+ const counted: RecordRow = [
47
+ record.timestampMs,
48
+ this.models.index(record.model),
49
+ this.sessions.index(record.sessionId),
50
+ this.folders.index(record.cwd),
51
+ record.totals.calls,
52
+ record.totals.input,
53
+ record.totals.cacheRead,
54
+ record.totals.cacheWrite,
55
+ record.totals.cacheWrite1h,
56
+ record.totals.output,
57
+ record.totals.reasoning,
58
+ record.dedupeKey ?? 0,
59
+ record.account === undefined ? 0 : this.accounts.index(record.account) + 1
60
+ ];
61
+
62
+ if (record.costUsd !== undefined || record.serviceTier !== undefined) {
63
+ counted[13] = record.costUsd;
64
+ }
65
+
66
+ if (record.serviceTier !== undefined) {
67
+ counted[14] = record.serviceTier;
68
+ }
69
+
70
+ return counted;
71
+ }
72
+ }
73
+
74
+ /* Rows written before accounts carry twelve fields, and before per-call pricing at most fourteen. */
75
+ export interface RowShape {
76
+ accountless: boolean;
77
+ maxLength: number;
78
+ }
79
+
80
+ export const CURRENT_ROW: RowShape = { accountless: false, maxLength: 15 };
81
+
82
+ function isRow(row: unknown, shape: RowShape): row is RecordRow {
83
+ return (
84
+ Array.isArray(row) &&
85
+ (shape.accountless ? row.length === 12 : row.length >= 13 && row.length <= shape.maxLength) &&
86
+ row.slice(0, 11).every((value) => typeof value === 'number') &&
87
+ (typeof row[11] === 'string' || row[11] === 0) &&
88
+ (shape.accountless || typeof row[12] === 'number') &&
89
+ (row.length < 14 || row[13] === null || row[13] === undefined || typeof row[13] === 'number') &&
90
+ (row.length < 15 || typeof row[14] === 'string')
91
+ );
92
+ }
93
+
94
+ function nameAt(table: unknown, at: number): string | null {
95
+ return Array.isArray(table) && typeof table[at] === 'string' ? table[at] : null;
96
+ }
97
+
98
+ /* The records of `rows`, or null when any row does not read back against its tables. */
99
+ export function decodeRows(rows: unknown, tables: Partial<Record<keyof RowTables, unknown>>, provider: UsageProvider, shape: RowShape): UsageRecord[] | null {
100
+ if (!Array.isArray(rows)) {
101
+ return null;
102
+ }
103
+
104
+ const records: UsageRecord[] = [];
105
+
106
+ for (const row of rows) {
107
+ if (!isRow(row, shape)) {
108
+ return null;
109
+ }
110
+
111
+ const model = nameAt(tables.models, row[1]);
112
+ const sessionId = nameAt(tables.sessions, row[2]);
113
+ const cwd = nameAt(tables.folders, row[3]);
114
+ const accountAt = row[12] ?? 0;
115
+ const account = accountAt === 0 ? undefined : nameAt(tables.accounts, accountAt - 1);
116
+
117
+ if (model === null || sessionId === null || cwd === null || account === null) {
118
+ return null;
119
+ }
120
+
121
+ records.push({
122
+ provider,
123
+ timestampMs: row[0],
124
+ model,
125
+ sessionId,
126
+ cwd,
127
+ ...(account === undefined ? {} : { account }),
128
+ totals: { calls: row[4], input: row[5], cacheRead: row[6], cacheWrite: row[7], cacheWrite1h: row[8], output: row[9], reasoning: row[10] },
129
+ ...(typeof row[13] === 'number' ? { costUsd: row[13] } : {}),
130
+ ...(typeof row[14] === 'string' ? { serviceTier: row[14] } : {}),
131
+ dedupeKey: row[11] === 0 ? null : row[11]
132
+ });
133
+ }
134
+
135
+ return records;
136
+ }
@@ -0,0 +1,105 @@
1
+ import { UsageProviderSchema, type UsageProvider } from '@adecore/agent-contracts';
2
+ import type { CodexParserState } from './readers/codex.ts';
3
+
4
+ const STATE_VERSION = 1;
5
+
6
+ /* Where the scanner left a transcript. All of it can be rebuilt by reading the transcript again. */
7
+ export interface ScanState {
8
+ provider: UsageProvider;
9
+ size: number;
10
+ mtimeMs: number;
11
+ /* Just past the last newline that was read, so a file that grew is read from there. */
12
+ offset: number;
13
+ /* What the reader has to remember to carry on halfway through a Codex file. */
14
+ codex: CodexParserState | null;
15
+ /* A versioned hash of all bytes before the offset; absent where an append cannot be verified. */
16
+ head: string | null;
17
+ parserVersion?: number;
18
+ compressed?: boolean;
19
+ }
20
+
21
+ /* The history this state was saved against: the rewrite it belongs to and how far into it. */
22
+ export interface HistoryMark {
23
+ gen: string;
24
+ bytes: number;
25
+ }
26
+
27
+ interface StateRow {
28
+ p: UsageProvider;
29
+ s: number;
30
+ m: number;
31
+ o: number;
32
+ c: CodexParserState | null;
33
+ h?: string;
34
+ v?: number;
35
+ z?: boolean;
36
+ }
37
+
38
+ interface StateFile {
39
+ version: number;
40
+ history: HistoryMark;
41
+ files: Record<string, StateRow>;
42
+ }
43
+
44
+ export function encodeState(state: ReadonlyMap<string, ScanState>, history: HistoryMark): string {
45
+ const files: Record<string, StateRow> = {};
46
+
47
+ for (const [path, file] of state) {
48
+ files[path] = {
49
+ p: file.provider,
50
+ s: file.size,
51
+ m: file.mtimeMs,
52
+ o: file.offset,
53
+ c: file.codex,
54
+ ...(file.head === null ? {} : { h: file.head }),
55
+ ...(file.parserVersion === undefined ? {} : { v: file.parserVersion }),
56
+ ...(file.compressed === undefined ? {} : { z: file.compressed })
57
+ };
58
+ }
59
+
60
+ const document: StateFile = { version: STATE_VERSION, history, files };
61
+
62
+ return JSON.stringify(document);
63
+ }
64
+
65
+ /* Null for text that is no state of this version; an entry that does not read only costs a read of its transcript. */
66
+ export function decodeState(text: string): { history: HistoryMark; files: Map<string, ScanState> } | null {
67
+ let document: Partial<StateFile> | null;
68
+
69
+ try {
70
+ document = JSON.parse(text) as Partial<StateFile> | null;
71
+ } catch {
72
+ return null;
73
+ }
74
+
75
+ const history = document?.history;
76
+
77
+ if (document?.version !== STATE_VERSION || typeof history?.gen !== 'string' || typeof history.bytes !== 'number') {
78
+ return null;
79
+ }
80
+
81
+ const files = new Map<string, ScanState>();
82
+
83
+ for (const [path, file] of Object.entries(document.files ?? {})) {
84
+ if (!UsageProviderSchema.safeParse(file.p).success) {
85
+ continue;
86
+ }
87
+
88
+ if (typeof file.s !== 'number' || typeof file.m !== 'number' || typeof file.o !== 'number' || file.o < 0) {
89
+ continue;
90
+ }
91
+
92
+ files.set(path, {
93
+ provider: file.p,
94
+ size: file.s,
95
+ mtimeMs: file.m,
96
+ offset: file.o,
97
+ codex: file.c ?? null,
98
+ head: typeof file.h === 'string' ? file.h : null,
99
+ ...(typeof file.v === 'number' ? { parserVersion: file.v } : {}),
100
+ ...(typeof file.z === 'boolean' ? { compressed: file.z } : {})
101
+ });
102
+ }
103
+
104
+ return { history: { gen: history.gen, bytes: history.bytes }, files };
105
+ }