portable-agent-layer 0.99.2 → 0.101.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/README.md +1 -0
- package/package.json +3 -2
- package/src/cli/cost.ts +2 -6
- package/src/cli/tree.ts +2 -0
- package/src/cli/wrapped.ts +50 -0
- package/src/hooks/handlers/agenda.ts +4 -5
- package/src/hooks/handlers/entity-extraction.ts +1 -1
- package/src/hooks/handlers/rating.ts +3 -4
- package/src/hooks/handlers/session-intelligence.ts +1 -1
- package/src/hooks/handlers/session-name.ts +1 -1
- package/src/hooks/lib/agent-registry.ts +4 -0
- package/src/hooks/lib/entity-research.ts +1 -1
- package/src/hooks/lib/events.ts +8 -4
- package/src/hooks/lib/inference-events.ts +7 -3
- package/src/hooks/lib/inference-usage.ts +205 -0
- package/src/hooks/lib/inference.ts +89 -30
- package/src/hooks/lib/skill-match.ts +1 -1
- package/src/hooks/lib/token-usage.ts +10 -7
- package/src/hooks/lib/wall-clock.ts +12 -4
- package/src/tools/lib/cost-report.ts +20 -1
- package/src/tools/lib/wrapped/agents.ts +30 -0
- package/src/tools/lib/wrapped/habits.ts +65 -0
- package/src/tools/lib/wrapped/pal.ts +53 -0
- package/src/tools/lib/wrapped/records.ts +137 -0
- package/src/tools/lib/wrapped/render.ts +145 -0
- package/src/tools/lib/wrapped/totals.ts +55 -0
- package/src/tools/lib/wrapped/year.ts +45 -0
- package/src/tools/self-model.ts +1 -2
package/README.md
CHANGED
|
@@ -86,6 +86,7 @@ pal cli status # check your setup
|
|
|
86
86
|
| `pal cli analyze [--actionable]` | Learning analysis: rating trends, failure patterns, graduation candidates |
|
|
87
87
|
| `pal cli usage` | Summarize token usage and estimated cost |
|
|
88
88
|
| `pal cli cost` | What each month was paid against its API-equivalent cost, per agent |
|
|
89
|
+
| `pal cli wrapped [--year <yyyy>]` | A year of your work in numbers, read from the event log |
|
|
89
90
|
| `pal cli content preserve` | Copy every Claude Code transcript, gzipped, into `memory/content/` before Claude Code deletes it (30 days by default). Runs on its own whenever a session closes; the content store never leaves the machine, so `export` skips it |
|
|
90
91
|
| `pal cli events discord-nag <sent\|answered>` | For a bot outside PAL: record a nag it sent (`--kind <kind>`), or the answer to one and how long it took (`--after-sec <n>`), in the local event log |
|
|
91
92
|
| `pal cli infer` | One model call for scripts and bots, on the active agent's route: prompt on stdin, reply on stdout. `--tier small\|medium`, `--system <file>`, `--schema <file>` for a JSON reply checked against it, `--timeout <seconds>`. The call loads no PAL hooks or context; exits 1 with the reason when no route answers |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "portable-agent-layer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.101.0",
|
|
4
4
|
"description": "PAL — Portable Agent Layer: persistent personal context for AI coding assistants",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"check-write": "bun .agents/scripts/check-write.ts",
|
|
49
49
|
"knip": "knip-bun",
|
|
50
50
|
"klint": "klint && klint --config test",
|
|
51
|
+
"klint:hook": "klint --json --hook && klint --config test --json --hook",
|
|
51
52
|
"jscpd": "jscpd --noTips",
|
|
52
53
|
"madge": "madge --circular --extensions ts --ts-config tsconfig.json src",
|
|
53
54
|
"lf": "bun .agents/scripts/check-lf.ts",
|
|
@@ -80,7 +81,7 @@
|
|
|
80
81
|
"@commitlint/cli": "21.0.1",
|
|
81
82
|
"@commitlint/config-conventional": "21.0.1",
|
|
82
83
|
"@hughescr/stryker-bun-runner": "1.3.8",
|
|
83
|
-
"@konvert7/klint": "0.35.
|
|
84
|
+
"@konvert7/klint": "0.35.2",
|
|
84
85
|
"@opencode-ai/plugin": "latest",
|
|
85
86
|
"@secretlint/secretlint-rule-preset-recommend": "13.0.2",
|
|
86
87
|
"@semantic-release/changelog": "^6.0.3",
|
package/src/cli/cost.ts
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { labelOfRuntime } from "../hooks/lib/agent-registry";
|
|
2
2
|
import { eventsSince } from "../hooks/lib/events";
|
|
3
3
|
import { leaf } from "../tools/lib/command";
|
|
4
4
|
import { costLines, costMonths } from "../tools/lib/cost-report";
|
|
5
5
|
|
|
6
|
-
function labelOf(runtime: string): string {
|
|
7
|
-
return AGENT_REGISTRY[runtime as AgentName]?.label ?? runtime;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
6
|
function printCost(): undefined {
|
|
11
7
|
const months = costMonths(eventsSince("model_call", new Date(0)));
|
|
12
|
-
for (const line of costLines(months,
|
|
8
|
+
for (const line of costLines(months, labelOfRuntime)) console.log(line);
|
|
13
9
|
}
|
|
14
10
|
|
|
15
11
|
export const costCommand = leaf({
|
package/src/cli/tree.ts
CHANGED
|
@@ -28,6 +28,7 @@ import { serverCommand } from "./server";
|
|
|
28
28
|
import { skillCommand } from "./skill";
|
|
29
29
|
import { subagentCommand } from "./subagent";
|
|
30
30
|
import { usageCommand } from "./usage";
|
|
31
|
+
import { wrappedCommand } from "./wrapped";
|
|
31
32
|
|
|
32
33
|
type Handler = Leaf["run"];
|
|
33
34
|
export type DebugState = "on" | "off" | "status";
|
|
@@ -155,6 +156,7 @@ export function cliTree(admin: AdminHandlers): Group {
|
|
|
155
156
|
...adminCommands(admin),
|
|
156
157
|
usage: usageCommand,
|
|
157
158
|
cost: costCommand,
|
|
159
|
+
wrapped: wrappedCommand,
|
|
158
160
|
content: contentCommand,
|
|
159
161
|
events: eventsCommand,
|
|
160
162
|
infer: inferCommand,
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { readSkillIndex } from "../hooks/lib/skill-match";
|
|
2
|
+
import { leaf, UsageError } from "../tools/lib/command";
|
|
3
|
+
import { wrappedAgents } from "../tools/lib/wrapped/agents";
|
|
4
|
+
import { wrappedHabits } from "../tools/lib/wrapped/habits";
|
|
5
|
+
import { wrappedPal } from "../tools/lib/wrapped/pal";
|
|
6
|
+
import { wrappedRecords } from "../tools/lib/wrapped/records";
|
|
7
|
+
import {
|
|
8
|
+
agentsLines,
|
|
9
|
+
habitsLines,
|
|
10
|
+
palLines,
|
|
11
|
+
recordsLines,
|
|
12
|
+
totalsLines,
|
|
13
|
+
} from "../tools/lib/wrapped/render";
|
|
14
|
+
import { wrappedTotals } from "../tools/lib/wrapped/totals";
|
|
15
|
+
import { eventsOfYear } from "../tools/lib/wrapped/year";
|
|
16
|
+
|
|
17
|
+
function yearFrom(value: string | undefined): number {
|
|
18
|
+
if (value === undefined) return new Date().getFullYear();
|
|
19
|
+
if (!/^\d{4}$/.test(value)) throw new UsageError(`--year takes a year, got: ${value}`);
|
|
20
|
+
return Number(value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function printWrapped(year: number): undefined {
|
|
24
|
+
const events = eventsOfYear(year);
|
|
25
|
+
console.log(`\n PAL Wrapped ${year}`);
|
|
26
|
+
if (events.length === 0) {
|
|
27
|
+
console.log(`\n No events logged in ${year}\n`);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const lines = [
|
|
31
|
+
...totalsLines(wrappedTotals(events)),
|
|
32
|
+
...recordsLines(wrappedRecords(events)),
|
|
33
|
+
...habitsLines(wrappedHabits(events)),
|
|
34
|
+
...agentsLines(wrappedAgents(events)),
|
|
35
|
+
...palLines(wrappedPal(events, readSkillIndex())),
|
|
36
|
+
];
|
|
37
|
+
for (const line of lines) console.log(line);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const wrappedCommand = leaf({
|
|
41
|
+
summary: "A year of your work in numbers, read from the event log",
|
|
42
|
+
options: {
|
|
43
|
+
year: {
|
|
44
|
+
type: "string",
|
|
45
|
+
value: "<yyyy>",
|
|
46
|
+
description: "Which year (default: this one)",
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
run: ({ values }) => printWrapped(yearFrom(values.year)),
|
|
50
|
+
});
|
|
@@ -17,7 +17,6 @@ import { type AgendaMove, readAgenda, writeAgenda } from "../lib/agenda-store";
|
|
|
17
17
|
import { linkInputs, readGoalLinks, writeGoalLinks } from "../lib/goal-links";
|
|
18
18
|
import { canInfer, inference } from "../lib/inference";
|
|
19
19
|
import { logDebug, logError } from "../lib/log";
|
|
20
|
-
import { SONNET_MODEL } from "../lib/models";
|
|
21
20
|
import { readAllProjects } from "../lib/projects";
|
|
22
21
|
import { isServesKind, SERVES_KINDS, setServes } from "../lib/serves";
|
|
23
22
|
import { readTelosGoals } from "../lib/telos-goals";
|
|
@@ -182,7 +181,7 @@ async function guessMissingServes(sessionId?: string): Promise<number> {
|
|
|
182
181
|
caller: "agenda-serves",
|
|
183
182
|
sessionId,
|
|
184
183
|
});
|
|
185
|
-
|
|
184
|
+
logTokenUsage("agenda-serves", result);
|
|
186
185
|
if (!result.success || !result.output) return 0;
|
|
187
186
|
|
|
188
187
|
const parsed = parsePayload<{
|
|
@@ -256,7 +255,7 @@ async function refreshGoalLinks(
|
|
|
256
255
|
caller: "agenda-goal-links",
|
|
257
256
|
sessionId,
|
|
258
257
|
});
|
|
259
|
-
|
|
258
|
+
logTokenUsage("agenda-goal-links", result);
|
|
260
259
|
if (!result.success || !result.output) return "failed";
|
|
261
260
|
|
|
262
261
|
const parsed = parsePayload<{ links: { goalId: string; projects: unknown }[] }>(
|
|
@@ -310,7 +309,7 @@ async function extractWaitingOn(sessionId?: string): Promise<number> {
|
|
|
310
309
|
caller: "agenda-waiting-on",
|
|
311
310
|
sessionId,
|
|
312
311
|
});
|
|
313
|
-
|
|
312
|
+
logTokenUsage("agenda-waiting-on", result);
|
|
314
313
|
if (!result.success || !result.output) continue;
|
|
315
314
|
|
|
316
315
|
const parsed = parsePayload<{ question: string }>(result.output, "agenda:waiting-on");
|
|
@@ -356,7 +355,7 @@ async function writeMoves(sessionId?: string): Promise<boolean> {
|
|
|
356
355
|
caller: "agenda-moves",
|
|
357
356
|
sessionId,
|
|
358
357
|
});
|
|
359
|
-
|
|
358
|
+
logTokenUsage("agenda-moves", result);
|
|
360
359
|
if (!result.success || !result.output) return false;
|
|
361
360
|
|
|
362
361
|
const parsed = parsePayload<{ moves: (AgendaMove & { project?: unknown })[] }>(
|
|
@@ -56,7 +56,7 @@ export async function extractSessionEntities(
|
|
|
56
56
|
caller: "entity-extraction",
|
|
57
57
|
sessionId,
|
|
58
58
|
});
|
|
59
|
-
|
|
59
|
+
logTokenUsage("entity-extraction", result);
|
|
60
60
|
if (!result.success || !result.output) return;
|
|
61
61
|
|
|
62
62
|
const source = sourceId(sessionId, count);
|
|
@@ -21,7 +21,6 @@ import { spawnDetachedInference } from "../lib/detached-inference";
|
|
|
21
21
|
import { canInfer, inference } from "../lib/inference";
|
|
22
22
|
import { replyEnd } from "../lib/interaction-samples";
|
|
23
23
|
import { logDebug } from "../lib/log";
|
|
24
|
-
import { HAIKU_5_5_MODEL } from "../lib/models";
|
|
25
24
|
import { paths } from "../lib/paths";
|
|
26
25
|
import { isSystemText, stripInjectedTags } from "../lib/prompt-text";
|
|
27
26
|
import {
|
|
@@ -225,13 +224,13 @@ function handleImplicitReaction(message: string, sentAt: Date, sessionId?: strin
|
|
|
225
224
|
|
|
226
225
|
async function labelReaction(reply: string, message: string, sessionId?: string) {
|
|
227
226
|
const result = await inference(reactionRequest(reply, message, sessionId));
|
|
228
|
-
|
|
227
|
+
logTokenUsage("rating", result);
|
|
229
228
|
return result.success ? parseReactionLabel(result.output) : null;
|
|
230
229
|
}
|
|
231
230
|
|
|
232
231
|
async function checkCorrection(reply: string, message: string, sessionId?: string) {
|
|
233
232
|
const result = await inference(correctionCheckRequest(reply, message, sessionId));
|
|
234
|
-
|
|
233
|
+
logTokenUsage("rating", result);
|
|
235
234
|
return result.success ? parseCorrectionCheck(result.output) : null;
|
|
236
235
|
}
|
|
237
236
|
|
|
@@ -250,7 +249,7 @@ async function draftRuleCandidate(sessionId?: string): Promise<void> {
|
|
|
250
249
|
const known = knownRules();
|
|
251
250
|
if (!canRepeat(corrections) && !canWiden(corrections, known)) return;
|
|
252
251
|
const result = await inference(drafterRequest(corrections, known, sessionId));
|
|
253
|
-
|
|
252
|
+
logTokenUsage("rule-drafter", result);
|
|
254
253
|
const candidate = result.success ? parseDraft(result.output, corrections, known) : null;
|
|
255
254
|
if (candidate) recordCandidate(candidate, turns);
|
|
256
255
|
else logDebug("rule-drafter", `no candidate: ${result.output ?? result.error ?? ""}`);
|
|
@@ -162,7 +162,7 @@ export async function captureSessionIntelligence(
|
|
|
162
162
|
sessionId,
|
|
163
163
|
});
|
|
164
164
|
|
|
165
|
-
|
|
165
|
+
logTokenUsage("session-intelligence", result);
|
|
166
166
|
|
|
167
167
|
if (result.success && result.output) {
|
|
168
168
|
output = JSON.parse(result.output) as IntelligenceOutput;
|
|
@@ -76,7 +76,7 @@ async function upgradeWithInference(
|
|
|
76
76
|
sessionId,
|
|
77
77
|
});
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
logTokenUsage("session-name", result);
|
|
80
80
|
if (!result.success || !result.output) return;
|
|
81
81
|
|
|
82
82
|
const label = labelFromModel(result.output);
|
|
@@ -86,3 +86,7 @@ export function skillsDirOf(agent: AgentName): string {
|
|
|
86
86
|
export function isAgentName(value: string): value is AgentName {
|
|
87
87
|
return Object.hasOwn(AGENT_REGISTRY, value);
|
|
88
88
|
}
|
|
89
|
+
|
|
90
|
+
export function labelOfRuntime(runtime: string): string {
|
|
91
|
+
return isAgentName(runtime) ? AGENT_REGISTRY[runtime].label : runtime;
|
|
92
|
+
}
|
|
@@ -180,7 +180,7 @@ export async function researchEntity(
|
|
|
180
180
|
caller: "entity-research",
|
|
181
181
|
web: true,
|
|
182
182
|
});
|
|
183
|
-
|
|
183
|
+
logTokenUsage("entity-research", result);
|
|
184
184
|
const profile = result.success ? parseProfile(result.output) : null;
|
|
185
185
|
if (!profile) return { found: false, reason: result.error ?? "no usable reply" };
|
|
186
186
|
if (profile.match !== "sure")
|
package/src/hooks/lib/events.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { appendFileSync, existsSync, readdirSync, readFileSync } from "node:fs";
|
|
|
8
8
|
import { resolve } from "node:path";
|
|
9
9
|
import { currentAttribution, type RecordAttribution } from "./actor";
|
|
10
10
|
import type { AgentType } from "./agent";
|
|
11
|
+
import { USAGE_FIELDS } from "./inference-usage";
|
|
11
12
|
import { logError } from "./log";
|
|
12
13
|
import { ensureDir, paths } from "./paths";
|
|
13
14
|
import { readAllProjects, resolveProjectFromCwd } from "./projects";
|
|
@@ -62,8 +63,7 @@ export const EVENT_FIELDS = {
|
|
|
62
63
|
"web",
|
|
63
64
|
"latencyMs",
|
|
64
65
|
"failure",
|
|
65
|
-
|
|
66
|
-
"outputTokens",
|
|
66
|
+
...USAGE_FIELDS,
|
|
67
67
|
],
|
|
68
68
|
pal_update: ["outcome", "from", "to", "durationSec", "skipReason"],
|
|
69
69
|
commitment: ["iscProject", "isc", "outcome", "surface", "snoozeDays"],
|
|
@@ -322,7 +322,7 @@ export function eventsInMonth(type: EventType, month: Date): LoggedEvent[] {
|
|
|
322
322
|
return parseLines(file).filter((e) => e.type === type);
|
|
323
323
|
}
|
|
324
324
|
|
|
325
|
-
export function
|
|
325
|
+
export function allEventsSince(since: Date): LoggedEvent[] {
|
|
326
326
|
const dir = eventsDir();
|
|
327
327
|
const sinceTs = since.toISOString();
|
|
328
328
|
const fromMonth = monthOf(sinceTs);
|
|
@@ -330,5 +330,9 @@ export function eventsSince(type: EventType, since: Date): LoggedEvent[] {
|
|
|
330
330
|
.filter((f) => f.endsWith(".jsonl") && f.slice(0, 7) >= fromMonth)
|
|
331
331
|
.sort()
|
|
332
332
|
.flatMap((f) => parseLines(resolve(dir, f)))
|
|
333
|
-
.filter((e) => e.
|
|
333
|
+
.filter((e) => e.ts >= sinceTs);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function eventsSince(type: EventType, since: Date): LoggedEvent[] {
|
|
337
|
+
return allEventsSince(since).filter((e) => e.type === type);
|
|
334
338
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { appendEvent, logQuietly } from "./events";
|
|
7
|
+
import { type InferenceUsage, USAGE_FIELDS } from "./inference-usage";
|
|
7
8
|
|
|
8
9
|
interface InferenceCall {
|
|
9
10
|
caller?: string;
|
|
@@ -16,7 +17,11 @@ interface InferenceOutcome {
|
|
|
16
17
|
success: boolean;
|
|
17
18
|
output?: string;
|
|
18
19
|
error?: string;
|
|
19
|
-
usage?:
|
|
20
|
+
usage?: InferenceUsage;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function knownOrNull(usage: InferenceUsage = {}) {
|
|
24
|
+
return Object.fromEntries(USAGE_FIELDS.map((field) => [field, usage[field] ?? null]));
|
|
20
25
|
}
|
|
21
26
|
|
|
22
27
|
export const SCHEMA_MISMATCH = "the reply does not match the schema";
|
|
@@ -47,8 +52,7 @@ export function logInferenceCall(
|
|
|
47
52
|
web: Boolean(call.web),
|
|
48
53
|
latencyMs: Math.round(latencyMs),
|
|
49
54
|
failure: failureOf(outcome, planned.route),
|
|
50
|
-
|
|
51
|
-
outputTokens: outcome.usage?.outputTokens ?? null,
|
|
55
|
+
...knownOrNull(outcome.usage),
|
|
52
56
|
},
|
|
53
57
|
{ session: call.sessionId ?? null }
|
|
54
58
|
)
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What one inference cost, in the token fields model_call uses, read from each
|
|
3
|
+
* route's own usage report. A field the route does not report stays undefined.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { cacheWritesOf, type TranscriptUsage } from "../../tools/lib/transcript-usage";
|
|
7
|
+
|
|
8
|
+
export interface InferenceUsage {
|
|
9
|
+
inputTokens?: number;
|
|
10
|
+
outputTokens?: number;
|
|
11
|
+
cacheWrite5m?: number;
|
|
12
|
+
cacheWrite1h?: number;
|
|
13
|
+
cacheRead?: number;
|
|
14
|
+
thinkingTokens?: number;
|
|
15
|
+
costUsd?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const USAGE_FIELDS = [
|
|
19
|
+
"inputTokens",
|
|
20
|
+
"outputTokens",
|
|
21
|
+
"cacheWrite5m",
|
|
22
|
+
"cacheWrite1h",
|
|
23
|
+
"cacheRead",
|
|
24
|
+
"thinkingTokens",
|
|
25
|
+
"costUsd",
|
|
26
|
+
] as const satisfies readonly (keyof InferenceUsage)[];
|
|
27
|
+
|
|
28
|
+
export function jsonLines(raw: string): Record<string, unknown>[] {
|
|
29
|
+
return raw.split("\n").flatMap((line) => {
|
|
30
|
+
try {
|
|
31
|
+
const value: unknown = JSON.parse(line);
|
|
32
|
+
return value && typeof value === "object" ? [value as Record<string, unknown>] : [];
|
|
33
|
+
} catch {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const sum = (values: (number | undefined)[]) =>
|
|
40
|
+
values.some((v) => v !== undefined)
|
|
41
|
+
? values.reduce<number>((total, v) => total + (v ?? 0), 0)
|
|
42
|
+
: undefined;
|
|
43
|
+
|
|
44
|
+
export function anthropicUsage(usage: TranscriptUsage, costUsd?: number): InferenceUsage {
|
|
45
|
+
return {
|
|
46
|
+
inputTokens: usage.input_tokens,
|
|
47
|
+
outputTokens: usage.output_tokens,
|
|
48
|
+
...cacheWritesOf(usage),
|
|
49
|
+
cacheRead: usage.cache_read_input_tokens,
|
|
50
|
+
thinkingTokens: usage.output_tokens_details?.thinking_tokens,
|
|
51
|
+
costUsd,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface OpenAiUsage {
|
|
56
|
+
prompt_tokens?: number;
|
|
57
|
+
completion_tokens?: number;
|
|
58
|
+
prompt_tokens_details?: { cached_tokens?: number };
|
|
59
|
+
completion_tokens_details?: { reasoning_tokens?: number };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** OpenAI counts cached tokens inside prompt_tokens. */
|
|
63
|
+
export function openAiUsage(usage: OpenAiUsage): InferenceUsage {
|
|
64
|
+
const cacheRead = usage.prompt_tokens_details?.cached_tokens;
|
|
65
|
+
return {
|
|
66
|
+
inputTokens:
|
|
67
|
+
usage.prompt_tokens === undefined
|
|
68
|
+
? undefined
|
|
69
|
+
: usage.prompt_tokens - (cacheRead ?? 0),
|
|
70
|
+
outputTokens: usage.completion_tokens,
|
|
71
|
+
cacheRead,
|
|
72
|
+
thinkingTokens: usage.completion_tokens_details?.reasoning_tokens,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function claudeResult(raw: string): Record<string, unknown> | undefined {
|
|
77
|
+
return jsonLines(raw).find((event) => event.type === "result");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function claudeSpawnUsage(raw: string): InferenceUsage | undefined {
|
|
81
|
+
const result = claudeResult(raw);
|
|
82
|
+
if (!result?.usage) return undefined;
|
|
83
|
+
return anthropicUsage(result.usage as TranscriptUsage, result.total_cost_usd as number);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface CodexUsage {
|
|
87
|
+
input_tokens?: number;
|
|
88
|
+
cached_input_tokens?: number;
|
|
89
|
+
cache_write_input_tokens?: number;
|
|
90
|
+
output_tokens?: number;
|
|
91
|
+
reasoning_output_tokens?: number;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Codex counts cache reads and writes inside input_tokens. */
|
|
95
|
+
export function codexSpawnUsage(raw: string): InferenceUsage | undefined {
|
|
96
|
+
const turns = jsonLines(raw)
|
|
97
|
+
.filter((event) => event.type === "turn.completed" && event.usage)
|
|
98
|
+
.map((event) => event.usage as CodexUsage);
|
|
99
|
+
if (turns.length === 0) return undefined;
|
|
100
|
+
const cacheRead = sum(turns.map((u) => u.cached_input_tokens));
|
|
101
|
+
const cacheWrite = sum(turns.map((u) => u.cache_write_input_tokens));
|
|
102
|
+
const input = sum(turns.map((u) => u.input_tokens));
|
|
103
|
+
return {
|
|
104
|
+
inputTokens:
|
|
105
|
+
input === undefined ? undefined : input - (cacheRead ?? 0) - (cacheWrite ?? 0),
|
|
106
|
+
outputTokens: sum(turns.map((u) => u.output_tokens)),
|
|
107
|
+
thinkingTokens: sum(turns.map((u) => u.reasoning_output_tokens)),
|
|
108
|
+
cacheWrite5m: cacheWrite,
|
|
109
|
+
cacheRead,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
interface OpencodeTokens {
|
|
114
|
+
input?: number;
|
|
115
|
+
output?: number;
|
|
116
|
+
reasoning?: number;
|
|
117
|
+
cache?: { read?: number; write?: number };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface OpencodeStep {
|
|
121
|
+
tokens?: OpencodeTokens | null;
|
|
122
|
+
cost?: number | null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function opencodeSteps(raw: string): (OpencodeTokens & { cost?: number })[] {
|
|
126
|
+
return jsonLines(raw)
|
|
127
|
+
.filter((event) => event.type === "step_finish")
|
|
128
|
+
.flatMap((event) => {
|
|
129
|
+
const { tokens, cost } = (event.part ?? {}) as OpencodeStep;
|
|
130
|
+
return tokens ? [{ ...tokens, cost: cost ?? undefined }] : [];
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** opencode reports reasoning apart from output; model_call counts it in both. */
|
|
135
|
+
export function opencodeSpawnUsage(raw: string): InferenceUsage | undefined {
|
|
136
|
+
const steps = opencodeSteps(raw);
|
|
137
|
+
if (steps.length === 0) return undefined;
|
|
138
|
+
const output = sum(steps.map((s) => s.output));
|
|
139
|
+
const reasoning = sum(steps.map((s) => s.reasoning));
|
|
140
|
+
return {
|
|
141
|
+
inputTokens: sum(steps.map((s) => s.input)),
|
|
142
|
+
outputTokens: sum([output, reasoning]),
|
|
143
|
+
thinkingTokens: reasoning,
|
|
144
|
+
cacheWrite5m: sum(steps.map((s) => s.cache?.write)),
|
|
145
|
+
cacheRead: sum(steps.map((s) => s.cache?.read)),
|
|
146
|
+
costUsd: sum(steps.map((s) => s.cost)),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
interface AntigravityUsage {
|
|
151
|
+
input_tokens?: number;
|
|
152
|
+
output_tokens?: number;
|
|
153
|
+
thinking_tokens?: number;
|
|
154
|
+
cache_read_tokens?: number;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function antigravitySpawnUsage(raw: string): InferenceUsage | undefined {
|
|
158
|
+
const result = jsonLines(raw).find((event) => event.event === "result")?.result as
|
|
159
|
+
| { usage?: AntigravityUsage }
|
|
160
|
+
| undefined;
|
|
161
|
+
const usage = result?.usage;
|
|
162
|
+
if (!usage) return undefined;
|
|
163
|
+
return {
|
|
164
|
+
inputTokens: usage.input_tokens,
|
|
165
|
+
outputTokens: usage.output_tokens,
|
|
166
|
+
thinkingTokens: usage.thinking_tokens,
|
|
167
|
+
cacheRead: usage.cache_read_tokens,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
interface CopilotModelCache {
|
|
172
|
+
prompt_tokens?: number;
|
|
173
|
+
cache_read?: number;
|
|
174
|
+
cache_write?: number;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function copilotModelCaches(event: Record<string, unknown>): CopilotModelCache[] {
|
|
178
|
+
const data = event.data as
|
|
179
|
+
| { promptCacheBreakState?: { models?: Record<string, CopilotModelCache> }[] }
|
|
180
|
+
| undefined;
|
|
181
|
+
return (data?.promptCacheBreakState ?? []).flatMap((state) =>
|
|
182
|
+
Object.values(state.models ?? {})
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Copilot's JSON output reports the prompt side of a call only, in its last
|
|
188
|
+
* usage checkpoint, and counts cache reads and writes inside prompt_tokens.
|
|
189
|
+
*/
|
|
190
|
+
export function copilotSpawnUsage(raw: string): InferenceUsage | undefined {
|
|
191
|
+
const checkpoint = jsonLines(raw).findLast(
|
|
192
|
+
(event) => event.type === "session.usage_checkpoint"
|
|
193
|
+
);
|
|
194
|
+
const models = checkpoint ? copilotModelCaches(checkpoint) : [];
|
|
195
|
+
if (models.length === 0) return undefined;
|
|
196
|
+
const prompt = sum(models.map((m) => m.prompt_tokens));
|
|
197
|
+
const cacheRead = sum(models.map((m) => m.cache_read));
|
|
198
|
+
const cacheWrite = sum(models.map((m) => m.cache_write));
|
|
199
|
+
return {
|
|
200
|
+
inputTokens:
|
|
201
|
+
prompt === undefined ? undefined : prompt - (cacheRead ?? 0) - (cacheWrite ?? 0),
|
|
202
|
+
cacheWrite5m: cacheWrite,
|
|
203
|
+
cacheRead,
|
|
204
|
+
};
|
|
205
|
+
}
|