@adecore/agents 0.31.2 → 0.31.4
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/acp-backend.d.ts +11 -1
- package/dist/chat/acp-backend.js +49 -17
- package/dist/chat/acp-protocol.d.ts +26 -5
- package/dist/chat/acp-protocol.js +53 -5
- package/dist/chat/index.d.ts +2 -2
- package/dist/providers/acp-provider.d.ts +1 -0
- package/dist/providers/acp-provider.js +32 -2
- package/dist/providers/acp-usage-log.d.ts +2 -0
- package/dist/providers/acp-usage-log.js +21 -0
- package/dist/providers/catalog.d.ts +2 -2
- package/dist/providers/catalog.js +6 -3
- package/dist/providers/index.d.ts +1 -0
- package/dist/providers/index.js +1 -0
- package/dist/providers/shell-commands.d.ts +1 -0
- package/dist/providers/shell-commands.js +119 -0
- package/dist/testing/fake-acp.d.ts +2 -0
- package/dist/testing/fake-acp.js +23 -2
- package/dist/usage/aggregate.js +6 -5
- package/dist/usage/index-file.js +25 -17
- package/dist/usage/limits/monitor.js +5 -0
- package/dist/usage/readers/grok.d.ts +3 -0
- package/dist/usage/readers/grok.js +85 -0
- package/dist/usage/record.d.ts +1 -0
- package/dist/usage/record.js +5 -1
- package/dist/usage/roots.js +7 -2
- package/dist/usage/scanner.d.ts +1 -0
- package/dist/usage/scanner.js +39 -5
- package/package.json +3 -3
- package/src/chat/acp-backend.ts +67 -17
- package/src/chat/acp-protocol.ts +95 -7
- package/src/chat/index.ts +14 -2
- package/src/providers/acp-provider.ts +39 -4
- package/src/providers/acp-usage-log.ts +26 -0
- package/src/providers/catalog.ts +7 -4
- package/src/providers/index.ts +1 -0
- package/src/providers/shell-commands.ts +122 -0
- package/src/testing/fake-acp.ts +29 -2
- package/src/usage/aggregate.ts +6 -5
- package/src/usage/index-file.ts +27 -18
- package/src/usage/limits/monitor.ts +5 -0
- package/src/usage/readers/grok.ts +89 -0
- package/src/usage/record.ts +7 -1
- package/src/usage/roots.ts +7 -2
- package/src/usage/scanner.ts +42 -5
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { basename, dirname } from 'node:path';
|
|
2
|
+
import { asObject, asString, int, type UsageRecord } from '../record.ts';
|
|
3
|
+
|
|
4
|
+
/* The file Grok keeps the counts of a session in, beside its other files in the session's folder. */
|
|
5
|
+
export const GROK_USAGE_FILE = 'usage.json';
|
|
6
|
+
|
|
7
|
+
// `costUsdTicks` counts in ticks of 10^-10 USD, as Grok's own docs on `grok usage` say.
|
|
8
|
+
const TICKS_PER_USD = 1e10;
|
|
9
|
+
|
|
10
|
+
/* The folder above a session's is its working directory, URL-encoded into one name. */
|
|
11
|
+
function cwdOf(path: string): string {
|
|
12
|
+
try {
|
|
13
|
+
return decodeURIComponent(basename(dirname(dirname(path))));
|
|
14
|
+
} catch {
|
|
15
|
+
return '';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function recordOf(counts: Record<string, unknown>, model: string, base: Omit<UsageRecord, 'model' | 'totals' | 'dedupeKey'>, turn: string): UsageRecord | null {
|
|
20
|
+
const cacheRead = int(counts.cachedReadTokens);
|
|
21
|
+
const cacheWrite = int(counts.cacheCreationTokens);
|
|
22
|
+
const totals = {
|
|
23
|
+
calls: int(counts.modelCalls),
|
|
24
|
+
// Grok's input holds both cache kinds: its total is input plus output.
|
|
25
|
+
input: Math.max(0, int(counts.inputTokens) - cacheRead - cacheWrite),
|
|
26
|
+
cacheRead,
|
|
27
|
+
cacheWrite,
|
|
28
|
+
cacheWrite1h: 0,
|
|
29
|
+
output: int(counts.outputTokens),
|
|
30
|
+
reasoning: int(counts.reasoningTokens)
|
|
31
|
+
};
|
|
32
|
+
if (totals.input + totals.cacheRead + totals.cacheWrite + totals.output === 0) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
const ticks = int(counts.costUsdTicks);
|
|
36
|
+
return {
|
|
37
|
+
...base,
|
|
38
|
+
model,
|
|
39
|
+
totals,
|
|
40
|
+
// A missing or zero cost is one Grok did not report, which the price table then estimates.
|
|
41
|
+
...(ticks === 0 ? {} : { costUsd: ticks / TICKS_PER_USD }),
|
|
42
|
+
dedupeKey: `grok\0${base.sessionId}\0${turn}\0${model}`
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/*
|
|
47
|
+
* A session's `usage.json` under `$GROK_HOME/sessions/<cwd>/<session id>/`, which Grok rewrites whole
|
|
48
|
+
* at the end of every turn. One record per turn per model the turn called; null when the file does not
|
|
49
|
+
* read as one, which leaves what the scanner held of it.
|
|
50
|
+
*/
|
|
51
|
+
export function parseGrokUsage(text: string, path: string): UsageRecord[] | null {
|
|
52
|
+
let parsed: unknown;
|
|
53
|
+
try {
|
|
54
|
+
parsed = JSON.parse(text);
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const file = asObject(parsed);
|
|
59
|
+
if (file === null || !Array.isArray(file.turns)) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
const sessionId = asString(file.sessionId) || basename(dirname(path));
|
|
63
|
+
const cwd = cwdOf(path);
|
|
64
|
+
const records: UsageRecord[] = [];
|
|
65
|
+
for (const entry of file.turns) {
|
|
66
|
+
const turn = asObject(entry);
|
|
67
|
+
const timestampMs = turn === null ? Number.NaN : Date.parse(asString(turn.endedAt));
|
|
68
|
+
if (turn === null || Number.isNaN(timestampMs)) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const number = typeof turn.turnNumber === 'number' ? String(turn.turnNumber) : asString(turn.endedAt);
|
|
72
|
+
const base = { provider: 'grok' as const, timestampMs, sessionId, cwd };
|
|
73
|
+
const byModel = asObject(turn.modelUsage);
|
|
74
|
+
const models: Array<[string, Record<string, unknown>]> =
|
|
75
|
+
byModel === null
|
|
76
|
+
? [[asString(turn.primaryModelId), turn]]
|
|
77
|
+
: Object.entries(byModel).flatMap(([model, counts]) => {
|
|
78
|
+
const object = asObject(counts);
|
|
79
|
+
return object === null ? [] : [[model, object] as [string, Record<string, unknown>]];
|
|
80
|
+
});
|
|
81
|
+
for (const [model, counts] of models) {
|
|
82
|
+
const record = model === '' ? null : recordOf(counts, model, base, number);
|
|
83
|
+
if (record !== null) {
|
|
84
|
+
records.push(record);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return records;
|
|
89
|
+
}
|
package/src/usage/record.ts
CHANGED
|
@@ -11,6 +11,8 @@ export interface UsageRecord {
|
|
|
11
11
|
/* The account the call was made under; absent is the default account of the CLI. */
|
|
12
12
|
account?: string;
|
|
13
13
|
totals: UsageTotals;
|
|
14
|
+
/* What the CLI itself says the call cost, which wins over a price from the table; absent where it says nothing. */
|
|
15
|
+
costUsd?: number;
|
|
14
16
|
/* What identifies the message this line reports on, or null when nothing does. */
|
|
15
17
|
dedupeKey: string | null;
|
|
16
18
|
}
|
|
@@ -54,7 +56,11 @@ export function foldByKey(records: readonly UsageRecord[]): UsageRecord[] {
|
|
|
54
56
|
folded.push({ ...record, totals: { ...record.totals } });
|
|
55
57
|
continue;
|
|
56
58
|
}
|
|
57
|
-
|
|
59
|
+
const into = folded[seen]!;
|
|
60
|
+
maxTotals(into.totals, record.totals);
|
|
61
|
+
if (record.costUsd !== undefined) {
|
|
62
|
+
into.costUsd = Math.max(into.costUsd ?? 0, record.costUsd);
|
|
63
|
+
}
|
|
58
64
|
}
|
|
59
65
|
return folded;
|
|
60
66
|
}
|
package/src/usage/roots.ts
CHANGED
|
@@ -10,14 +10,19 @@ export interface UsageRootPath {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
/* Where in its config folder each CLI leaves its transcripts. A provider with no CLI has a log its host passes as a root of its own. */
|
|
13
|
-
const TRANSCRIPTS: Partial<Record<UsageProvider, string>> & Record<'claude' | 'codex', string> = {
|
|
13
|
+
const TRANSCRIPTS: Partial<Record<UsageProvider, string>> & Record<'claude' | 'codex' | 'grok', string> = {
|
|
14
|
+
claude: 'projects',
|
|
15
|
+
codex: 'sessions',
|
|
16
|
+
grok: 'sessions'
|
|
17
|
+
};
|
|
14
18
|
|
|
15
19
|
/* The transcripts of the default accounts, found through the variables the CLIs themselves read. */
|
|
16
20
|
export function usageRoots(env: Record<string, string | undefined> = process.env): UsageRootPath[] {
|
|
17
21
|
const home = homeFolder(env);
|
|
18
22
|
return [
|
|
19
23
|
{ provider: 'claude', path: join(env.CLAUDE_CONFIG_DIR ?? join(home, '.claude'), TRANSCRIPTS.claude) },
|
|
20
|
-
{ provider: 'codex', path: join(env.CODEX_HOME ?? join(home, '.codex'), TRANSCRIPTS.codex) }
|
|
24
|
+
{ provider: 'codex', path: join(env.CODEX_HOME ?? join(home, '.codex'), TRANSCRIPTS.codex) },
|
|
25
|
+
{ provider: 'grok', path: join(env.GROK_HOME ?? join(home, '.grok'), TRANSCRIPTS.grok) }
|
|
21
26
|
];
|
|
22
27
|
}
|
|
23
28
|
|
package/src/usage/scanner.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { isNotFound, writeAtomic } from '../fs.ts';
|
|
|
7
7
|
import { decodeIndex, encodeIndex, type IndexedFile, type UsageIndex } from './index-file.ts';
|
|
8
8
|
import { foldByKey, type UsageRecord } from './record.ts';
|
|
9
9
|
import { claudeMightCarryUsage, parseClaudeLine } from './readers/claude.ts';
|
|
10
|
+
import { GROK_USAGE_FILE, parseGrokUsage } from './readers/grok.ts';
|
|
10
11
|
import { hostLogMightCarryUsage, parseHostUsageLine } from './readers/host-log.ts';
|
|
11
12
|
import { cloneCodexState, codexMightCarryUsage, createCodexState, parseCodexLine, type CodexParserState } from './readers/codex.ts';
|
|
12
13
|
import { usageRoots, type UsageRootPath } from './roots.ts';
|
|
@@ -75,7 +76,16 @@ interface ParsedFile {
|
|
|
75
76
|
codex: CodexParserState | null;
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
|
|
79
|
+
/* A log of lines for every provider but Grok, which keeps one JSON file per session that it rewrites whole. */
|
|
80
|
+
function readsWhole(provider: UsageProvider): boolean {
|
|
81
|
+
return provider === 'grok';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isUsageFile(provider: UsageProvider, name: string): boolean {
|
|
85
|
+
return readsWhole(provider) ? name === GROK_USAGE_FILE : name.endsWith('.jsonl');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function listUsageFiles(dir: string, provider: UsageProvider, out: string[] = []): Promise<string[]> {
|
|
79
89
|
let entries;
|
|
80
90
|
try {
|
|
81
91
|
entries = await readdir(dir, { withFileTypes: true });
|
|
@@ -86,8 +96,8 @@ async function listJsonl(dir: string, out: string[] = []): Promise<string[]> {
|
|
|
86
96
|
for (const entry of entries) {
|
|
87
97
|
const path = join(dir, entry.name);
|
|
88
98
|
if (entry.isDirectory()) {
|
|
89
|
-
await
|
|
90
|
-
} else if (entry.isFile() && entry.name
|
|
99
|
+
await listUsageFiles(path, provider, out);
|
|
100
|
+
} else if (entry.isFile() && isUsageFile(provider, entry.name)) {
|
|
91
101
|
out.push(path);
|
|
92
102
|
}
|
|
93
103
|
}
|
|
@@ -128,7 +138,7 @@ function parseChunk(text: string, provider: UsageProvider, from: number, state:
|
|
|
128
138
|
}
|
|
129
139
|
continue;
|
|
130
140
|
}
|
|
131
|
-
// Claude Code and Codex write transcripts of their own; every other provider is a log its host keeps.
|
|
141
|
+
// Claude Code and Codex write transcripts of their own, Grok a file read whole; every other provider is a log its host keeps.
|
|
132
142
|
if (hostLogMightCarryUsage(line)) {
|
|
133
143
|
const record = parseHostUsageLine(line, provider);
|
|
134
144
|
if (record !== null) {
|
|
@@ -245,7 +255,7 @@ export class UsageScanner {
|
|
|
245
255
|
}
|
|
246
256
|
let failure: string | null = null;
|
|
247
257
|
const accountOf = accountResolver(root, sessionAccounts);
|
|
248
|
-
for (const path of await
|
|
258
|
+
for (const path of await listUsageFiles(root.path, root.provider)) {
|
|
249
259
|
files += 1;
|
|
250
260
|
try {
|
|
251
261
|
if (await this.readFileInto(path, root.provider, accountOf)) {
|
|
@@ -282,6 +292,9 @@ export class UsageScanner {
|
|
|
282
292
|
if (known !== undefined && known.provider === provider && known.size === info.size && known.mtimeMs === info.mtimeMs) {
|
|
283
293
|
return false;
|
|
284
294
|
}
|
|
295
|
+
if (readsWhole(provider)) {
|
|
296
|
+
return this.readWholeInto(path, provider, accountOf, info);
|
|
297
|
+
}
|
|
285
298
|
const appended = known !== undefined && known.provider === provider && info.size > known.size && known.head?.startsWith('sha256:') === true;
|
|
286
299
|
let read = await readHashed(path, appended ? known.offset : 0);
|
|
287
300
|
// A file only counts as grown while it starts the way it did; one rewritten larger is read again from the start.
|
|
@@ -308,4 +321,28 @@ export class UsageScanner {
|
|
|
308
321
|
this.dirty = true;
|
|
309
322
|
return true;
|
|
310
323
|
}
|
|
324
|
+
|
|
325
|
+
/*
|
|
326
|
+
* A file rewritten as a whole replaces what the index held of it, so a turn it still lists is never
|
|
327
|
+
* counted twice. One that does not parse, such as one caught halfway through a write, keeps the
|
|
328
|
+
* records it had and is read again on the next scan.
|
|
329
|
+
*/
|
|
330
|
+
private async readWholeInto(path: string, provider: UsageProvider, accountOf: AccountOf, info: { size: number; mtimeMs: number }): Promise<boolean> {
|
|
331
|
+
const records = parseGrokUsage(await readFile(path, 'utf8'), path);
|
|
332
|
+
if (records === null) {
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
this.index.set(path, {
|
|
336
|
+
provider,
|
|
337
|
+
size: info.size,
|
|
338
|
+
mtimeMs: info.mtimeMs,
|
|
339
|
+
offset: info.size,
|
|
340
|
+
records: foldByKey(withAccount(records, accountOf)),
|
|
341
|
+
tail: [],
|
|
342
|
+
codex: null,
|
|
343
|
+
head: null
|
|
344
|
+
});
|
|
345
|
+
this.dirty = true;
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
311
348
|
}
|