portable-agent-layer 0.103.1 → 0.104.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.
@@ -15,8 +15,7 @@ memory/
15
15
  │ ├── pending-failure.json # Deferred failure capture
16
16
  │ ├── captured-learnings.json # Session-intelligence dedup map
17
17
  │ ├── session-names.json # Generated session titles
18
- │ ├── debug.log[.1-.5] # Rotated hook execution logs
19
- │ └── token-usage.jsonl # Per-call inference token usage
18
+ │ └── debug.log[.1-.5] # Rotated hook execution logs
20
19
  │
21
20
  ├── signals/
22
21
  │ ├── ratings.jsonl # Append-only rating signals (explicit + implicit)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "portable-agent-layer",
3
- "version": "0.103.1",
3
+ "version": "0.104.0",
4
4
  "description": "PAL — Portable Agent Layer: persistent personal context for AI coding assistants",
5
5
  "type": "module",
6
6
  "bin": {
@@ -20,7 +20,6 @@ import { logDebug, logError } from "../lib/log";
20
20
  import { readAllProjects } from "../lib/projects";
21
21
  import { isServesKind, SERVES_KINDS, setServes } from "../lib/serves";
22
22
  import { readTelosGoals } from "../lib/telos-goals";
23
- import { logTokenUsage } from "../lib/token-usage";
24
23
 
25
24
  const FRESH_HOURS = 6;
26
25
  const MAX_PROJECTS_PER_GUESS = 40;
@@ -181,7 +180,6 @@ async function guessMissingServes(sessionId?: string): Promise<number> {
181
180
  caller: "agenda-serves",
182
181
  sessionId,
183
182
  });
184
- logTokenUsage("agenda-serves", result);
185
183
  if (!result.success || !result.output) return 0;
186
184
 
187
185
  const parsed = parsePayload<{
@@ -255,7 +253,6 @@ async function refreshGoalLinks(
255
253
  caller: "agenda-goal-links",
256
254
  sessionId,
257
255
  });
258
- logTokenUsage("agenda-goal-links", result);
259
256
  if (!result.success || !result.output) return "failed";
260
257
 
261
258
  const parsed = parsePayload<{ links: { goalId: string; projects: unknown }[] }>(
@@ -309,7 +306,6 @@ async function extractWaitingOn(sessionId?: string): Promise<number> {
309
306
  caller: "agenda-waiting-on",
310
307
  sessionId,
311
308
  });
312
- logTokenUsage("agenda-waiting-on", result);
313
309
  if (!result.success || !result.output) continue;
314
310
 
315
311
  const parsed = parsePayload<{ question: string }>(result.output, "agenda:waiting-on");
@@ -355,7 +351,6 @@ async function writeMoves(sessionId?: string): Promise<boolean> {
355
351
  caller: "agenda-moves",
356
352
  sessionId,
357
353
  });
358
- logTokenUsage("agenda-moves", result);
359
354
  if (!result.success || !result.output) return false;
360
355
 
361
356
  const parsed = parsePayload<{ moves: (AgendaMove & { project?: unknown })[] }>(
@@ -23,7 +23,6 @@ import { newResearchTargets, researchNewEntities } from "../lib/entity-research"
23
23
  import { canInfer, inference } from "../lib/inference";
24
24
  import { logDebug, logError } from "../lib/log";
25
25
  import { identity, isEnabled, isOptedIn } from "../lib/settings";
26
- import { logTokenUsage } from "../lib/token-usage";
27
26
  import { parseMessages } from "../lib/transcript";
28
27
 
29
28
  function sourceId(sessionId: string, count: number): string {
@@ -56,7 +55,6 @@ export async function extractSessionEntities(
56
55
  caller: "entity-extraction",
57
56
  sessionId,
58
57
  });
59
- logTokenUsage("entity-extraction", result);
60
58
  if (!result.success || !result.output) return;
61
59
 
62
60
  const source = sourceId(sessionId, count);
@@ -43,7 +43,6 @@ import {
43
43
  } from "../lib/rule-drafter";
44
44
  import { emitRating } from "../lib/signals";
45
45
  import { now } from "../lib/time";
46
- import { logTokenUsage } from "../lib/token-usage";
47
46
 
48
47
  /** Read cached last assistant response (written by StopOrchestrator), looked up by session */
49
48
  function getLastResponse(sessionId?: string): string {
@@ -224,13 +223,11 @@ function handleImplicitReaction(message: string, sentAt: Date, sessionId?: strin
224
223
 
225
224
  async function labelReaction(reply: string, message: string, sessionId?: string) {
226
225
  const result = await inference(reactionRequest(reply, message, sessionId));
227
- logTokenUsage("rating", result);
228
226
  return result.success ? parseReactionLabel(result.output) : null;
229
227
  }
230
228
 
231
229
  async function checkCorrection(reply: string, message: string, sessionId?: string) {
232
230
  const result = await inference(correctionCheckRequest(reply, message, sessionId));
233
- logTokenUsage("rating", result);
234
231
  return result.success ? parseCorrectionCheck(result.output) : null;
235
232
  }
236
233
 
@@ -249,7 +246,6 @@ async function draftRuleCandidate(sessionId?: string): Promise<void> {
249
246
  const known = knownRules();
250
247
  if (!canRepeat(corrections) && !canWiden(corrections, known)) return;
251
248
  const result = await inference(drafterRequest(corrections, known, sessionId));
252
- logTokenUsage("rule-drafter", result);
253
249
  const candidate = result.success ? parseDraft(result.output, corrections, known) : null;
254
250
  if (candidate) recordCandidate(candidate, turns);
255
251
  else logDebug("rule-drafter", `no candidate: ${result.output ?? result.error ?? ""}`);
@@ -27,7 +27,6 @@ import { logDebug, logError } from "../lib/log";
27
27
  import { ensureDir, paths } from "../lib/paths";
28
28
  import { sessionDir } from "../lib/session-dir";
29
29
  import { fileTimestamp, monthPath } from "../lib/time";
30
- import { logTokenUsage } from "../lib/token-usage";
31
30
  import {
32
31
  extractContent,
33
32
  extractLastAssistant,
@@ -162,8 +161,6 @@ export async function captureSessionIntelligence(
162
161
  sessionId,
163
162
  });
164
163
 
165
- logTokenUsage("session-intelligence", result);
166
-
167
164
  if (result.success && result.output) {
168
165
  output = JSON.parse(result.output) as IntelligenceOutput;
169
166
  }
@@ -18,7 +18,6 @@ import {
18
18
  readSessionNames,
19
19
  writeSessionName,
20
20
  } from "../lib/session-names";
21
- import { logTokenUsage } from "../lib/token-usage";
22
21
 
23
22
  const NAME_PROMPT =
24
23
  "You generate concise 4-word session titles for AI assistant sessions. " +
@@ -76,7 +75,6 @@ async function upgradeWithInference(
76
75
  sessionId,
77
76
  });
78
77
 
79
- logTokenUsage("session-name", result);
80
78
  if (!result.success || !result.output) return;
81
79
 
82
80
  const label = labelFromModel(result.output);
@@ -9,24 +9,11 @@ import { writeFile } from "node:fs/promises";
9
9
  import { resolve } from "node:path";
10
10
  import { assets, ensureDir, paths } from "../lib/paths";
11
11
 
12
- interface TokenSummary {
13
- input: number;
14
- output: number;
15
- }
16
-
17
- interface TokenUsage {
18
- today: TokenSummary;
19
- week: TokenSummary;
20
- month: TokenSummary;
21
- total: TokenSummary;
22
- }
23
-
24
12
  interface Counts {
25
13
  signals: number;
26
14
  telos: number;
27
15
  skills: number;
28
16
  sessions: number;
29
- tokens: TokenUsage;
30
17
  updatedAt: string;
31
18
  }
32
19
 
@@ -49,60 +36,6 @@ function countMdFiles(dir: string): number {
49
36
  }
50
37
  }
51
38
 
52
- function getTokenUsage(): TokenUsage {
53
- const today: TokenSummary = { input: 0, output: 0 };
54
- const week: TokenSummary = { input: 0, output: 0 };
55
- const month: TokenSummary = { input: 0, output: 0 };
56
- const total: TokenSummary = { input: 0, output: 0 };
57
-
58
- const filepath = resolve(paths.signals(), "token-usage.jsonl");
59
- if (!existsSync(filepath)) return { today, week, month, total };
60
-
61
- try {
62
- const content = readFileSync(filepath, "utf-8").trim();
63
- if (!content) return { today, week, month, total };
64
-
65
- const now = new Date();
66
- const todayPrefix = now.toISOString().slice(0, 10);
67
- const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString();
68
- const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString();
69
-
70
- for (const line of content.split("\n")) {
71
- try {
72
- const entry = JSON.parse(line) as {
73
- ts?: string;
74
- inputTokens?: number;
75
- outputTokens?: number;
76
- };
77
- const input = entry.inputTokens ?? 0;
78
- const output = entry.outputTokens ?? 0;
79
- const ts = entry.ts ?? "";
80
-
81
- total.input += input;
82
- total.output += output;
83
- if (ts >= monthAgo) {
84
- month.input += input;
85
- month.output += output;
86
- }
87
- if (ts >= weekAgo) {
88
- week.input += input;
89
- week.output += output;
90
- }
91
- if (ts.startsWith(todayPrefix)) {
92
- today.input += input;
93
- today.output += output;
94
- }
95
- } catch {
96
- /* skip malformed line */
97
- }
98
- }
99
- } catch {
100
- /* file read error */
101
- }
102
-
103
- return { today, week, month, total };
104
- }
105
-
106
39
  function getCounts(): Counts {
107
40
  const signalsDir = paths.signals();
108
41
  const signals = countJsonlLines(resolve(signalsDir, "ratings.jsonl"));
@@ -133,14 +66,11 @@ function getCounts(): Counts {
133
66
  /* skip */
134
67
  }
135
68
 
136
- const tokens = getTokenUsage();
137
-
138
69
  return {
139
70
  signals,
140
71
  telos,
141
72
  skills,
142
73
  sessions,
143
- tokens,
144
74
  updatedAt: new Date().toISOString(),
145
75
  };
146
76
  }
@@ -14,7 +14,6 @@ import {
14
14
  } from "./entity-extraction";
15
15
  import type { inference } from "./inference";
16
16
  import { logDebug, logError } from "./log";
17
- import { logTokenUsage } from "./token-usage";
18
17
 
19
18
  export interface ResearchTarget {
20
19
  kind: ExtractedEntity["kind"];
@@ -180,7 +179,6 @@ export async function researchEntity(
180
179
  caller: "entity-research",
181
180
  web: true,
182
181
  });
183
- logTokenUsage("entity-research", result);
184
182
  const profile = result.success ? parseProfile(result.output) : null;
185
183
  if (!profile) return { found: false, reason: result.error ?? "no usable reply" };
186
184
  if (profile.match !== "sure")
@@ -193,13 +193,63 @@ export function copilotSpawnUsage(raw: string): InferenceUsage | undefined {
193
193
  );
194
194
  const models = checkpoint ? copilotModelCaches(checkpoint) : [];
195
195
  if (models.length === 0) return undefined;
196
- const prompt = sum(models.map((m) => m.prompt_tokens));
197
196
  const cacheRead = sum(models.map((m) => m.cache_read));
198
197
  const cacheWrite = sum(models.map((m) => m.cache_write));
199
198
  return {
200
- inputTokens:
201
- prompt === undefined ? undefined : prompt - (cacheRead ?? 0) - (cacheWrite ?? 0),
199
+ inputTokens: uncachedInput(
200
+ sum(models.map((m) => m.prompt_tokens)),
201
+ cacheRead,
202
+ cacheWrite
203
+ ),
204
+ cacheWrite5m: cacheWrite,
205
+ cacheRead,
206
+ };
207
+ }
208
+
209
+ function uncachedInput(
210
+ prompt: number | undefined,
211
+ cacheRead: number | undefined,
212
+ cacheWrite: number | undefined
213
+ ): number | undefined {
214
+ return prompt === undefined ? undefined : prompt - (cacheRead ?? 0) - (cacheWrite ?? 0);
215
+ }
216
+
217
+ const COPILOT_SESSION_ID = /^[\w-]+$/;
218
+
219
+ export function copilotSessionId(raw: string): string | undefined {
220
+ const id = jsonLines(raw).findLast((event) => event.type === "result")?.sessionId;
221
+ return typeof id === "string" && COPILOT_SESSION_ID.test(id) ? id : undefined;
222
+ }
223
+
224
+ interface CopilotRunUsage {
225
+ inputTokens?: number;
226
+ outputTokens?: number;
227
+ cacheReadTokens?: number;
228
+ cacheWriteTokens?: number;
229
+ reasoningTokens?: number;
230
+ }
231
+
232
+ /** A run's session.shutdown carries all of its usage, output included; inputTokens folds in cache reads and writes. */
233
+ export function copilotShutdownUsage(eventsJsonl: string): InferenceUsage | undefined {
234
+ const shutdown = jsonLines(eventsJsonl).findLast(
235
+ (event) => event.type === "session.shutdown"
236
+ );
237
+ const data = shutdown?.data as
238
+ | { modelMetrics?: Record<string, { usage?: CopilotRunUsage }> }
239
+ | undefined;
240
+ const runs = Object.values(data?.modelMetrics ?? {}).map((model) => model.usage ?? {});
241
+ if (runs.length === 0) return undefined;
242
+ const cacheRead = sum(runs.map((u) => u.cacheReadTokens));
243
+ const cacheWrite = sum(runs.map((u) => u.cacheWriteTokens));
244
+ return {
245
+ inputTokens: uncachedInput(
246
+ sum(runs.map((u) => u.inputTokens)),
247
+ cacheRead,
248
+ cacheWrite
249
+ ),
250
+ outputTokens: sum(runs.map((u) => u.outputTokens)),
202
251
  cacheWrite5m: cacheWrite,
203
252
  cacheRead,
253
+ thinkingTokens: sum(runs.map((u) => u.reasoningTokens)),
204
254
  };
205
255
  }
@@ -20,6 +20,7 @@
20
20
  * yet wired and currently fall through to the API path.
21
21
  */
22
22
 
23
+ import { readFileSync } from "node:fs";
23
24
  import { mkdtemp, rm, writeFile } from "node:fs/promises";
24
25
  import { tmpdir } from "node:os";
25
26
  import { basename, join } from "node:path";
@@ -48,6 +49,8 @@ import {
48
49
  claudeResult,
49
50
  claudeSpawnUsage,
50
51
  codexSpawnUsage,
52
+ copilotSessionId,
53
+ copilotShutdownUsage,
51
54
  copilotSpawnUsage,
52
55
  type InferenceUsage,
53
56
  jsonLines,
@@ -64,6 +67,7 @@ import {
64
67
  } from "./models";
65
68
  import { opencodeTierModel, writeInstructionFreeConfig } from "./opencode-config";
66
69
  import { withPalEnv } from "./pal-env";
70
+ import { platform } from "./paths";
67
71
  import { buildSpawnGuardEnv, getInferenceDepth, SPAWN_GUARD_ENV } from "./spawn-guard";
68
72
  import { findBinaryOnPath } from "./which";
69
73
 
@@ -703,6 +707,26 @@ const COPILOT_REPLY: CliReply = {
703
707
  usage: copilotSpawnUsage,
704
708
  };
705
709
 
710
+ function copilotSessionUsage(home: string, raw: string): InferenceUsage | undefined {
711
+ const id = copilotSessionId(raw);
712
+ if (!id) return undefined;
713
+ try {
714
+ return copilotShutdownUsage(
715
+ readFileSync(join(home, "session-state", id, "events.jsonl"), "utf-8")
716
+ );
717
+ } catch {
718
+ return undefined;
719
+ }
720
+ }
721
+
722
+ /** stdout reports only the prompt side; the run's own session file in its home has the rest. */
723
+ function copilotReply(home: string): CliReply {
724
+ return {
725
+ ...COPILOT_REPLY,
726
+ usage: (raw) => copilotSessionUsage(home, raw) ?? copilotSpawnUsage(raw),
727
+ };
728
+ }
729
+
706
730
  const COPILOT_TOKEN_ENV = ["COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"];
707
731
 
708
732
  /** A /login account lives in COPILOT_HOME's config.json; only a token variable logs in without it. */
@@ -715,19 +739,21 @@ async function inferenceViaCopilotSpawn(
715
739
  bin: string,
716
740
  opts: InferenceOptions
717
741
  ): Promise<InferenceResult> {
718
- const spawn = (place?: SpawnPlace) =>
742
+ const spawn = (home: string, place?: SpawnPlace) =>
719
743
  inferenceViaCliSpawn(
720
744
  bin,
721
745
  buildCopilotArgs(opts),
722
746
  buildCliPrompt(opts),
723
747
  opts,
724
- COPILOT_REPLY,
748
+ copilotReply(home),
725
749
  place
726
750
  );
727
- if (!copilotLogsInWithoutHome(withPalEnv(process.env))) return spawn();
751
+ const env = withPalEnv(process.env);
752
+ if (!copilotLogsInWithoutHome(env))
753
+ return spawn(env.COPILOT_HOME || platform.copilotDir());
728
754
  const dir = await mkdtemp(join(tmpdir(), "pal-copilot-"));
729
755
  try {
730
- return await spawn({ cwd: dir, env: { COPILOT_HOME: dir } });
756
+ return await spawn(dir, { cwd: dir, env: { COPILOT_HOME: dir } });
731
757
  } finally {
732
758
  await rm(dir, { recursive: true, force: true });
733
759
  }
@@ -122,7 +122,6 @@ const HOOK_MANAGED_FILES = [
122
122
  "last-responses.json",
123
123
  "signal-cache.json",
124
124
  "pending-failure.json",
125
- "token-usage.jsonl",
126
125
  "graduated.json",
127
126
  "update-available.json",
128
127
  "opinions.json",
@@ -218,13 +218,14 @@ interface CopilotEvent {
218
218
  };
219
219
  }
220
220
 
221
- /** Copilot counts cache reads inside inputTokens. */
221
+ /** Copilot counts cache reads and writes inside inputTokens. */
222
222
  function copilotTokens(usage: CopilotModelMetrics["usage"] = {}): Tokens {
223
223
  const cacheRead = usage.cacheReadTokens ?? 0;
224
+ const cacheWrite = usage.cacheWriteTokens ?? 0;
224
225
  return {
225
- input: (usage.inputTokens ?? 0) - cacheRead,
226
+ input: (usage.inputTokens ?? 0) - cacheRead - cacheWrite,
226
227
  output: usage.outputTokens ?? 0,
227
- cacheWrite5m: usage.cacheWriteTokens ?? 0,
228
+ cacheWrite5m: cacheWrite,
228
229
  cacheWrite1h: 0,
229
230
  cacheRead,
230
231
  };
@@ -6,6 +6,7 @@
6
6
 
7
7
  import { existsSync, readdirSync, readFileSync } from "node:fs";
8
8
  import { resolve } from "node:path";
9
+ import type { LoggedEvent } from "../../hooks/lib/events";
9
10
  import { costOfUsage } from "../../hooks/lib/models";
10
11
  import { cacheWritesOf, finalPerMessage, type TranscriptUsage } from "./transcript-usage";
11
12
 
@@ -307,27 +308,27 @@ export interface PalInferenceUsage {
307
308
  byCaller: Record<string, Bucket>;
308
309
  }
309
310
 
310
- interface InferenceLine {
311
- ts: string;
312
- caller: string;
313
- model: string;
314
- inputTokens: number;
315
- outputTokens: number;
316
- }
311
+ const countOf = (value: unknown) => (typeof value === "number" ? value : null);
317
312
 
318
- /** PAL's own calls are plain prompts: no cache to write and none to read. */
319
- function inferenceTokens(entry: InferenceLine): Tokens {
313
+ /** A call whose input or output went unreported has no tokens to count. */
314
+ function inferenceTokens(call: LoggedEvent): Tokens | null {
315
+ const input = countOf(call.inputTokens);
316
+ const output = countOf(call.outputTokens);
317
+ if (input === null || output === null) return null;
320
318
  return {
321
- input: entry.inputTokens,
322
- output: entry.outputTokens,
323
- cacheWrite5m: 0,
324
- cacheWrite1h: 0,
325
- cacheRead: 0,
319
+ input,
320
+ output,
321
+ cacheWrite5m: countOf(call.cacheWrite5m) ?? 0,
322
+ cacheWrite1h: countOf(call.cacheWrite1h) ?? 0,
323
+ cacheRead: countOf(call.cacheRead) ?? 0,
326
324
  };
327
325
  }
328
326
 
327
+ const textOr = (value: unknown, fallback: string) =>
328
+ typeof value === "string" ? value : fallback;
329
+
329
330
  export function readPalInference(
330
- filepath: string,
331
+ events: LoggedEvent[],
331
332
  now: Date = new Date()
332
333
  ): PalInferenceUsage {
333
334
  const horizons = horizonsFrom(now);
@@ -336,33 +337,20 @@ export function readPalInference(
336
337
  byModel: {},
337
338
  byCaller: {},
338
339
  };
339
- if (!existsSync(filepath)) return result;
340
-
341
- const content = readFileSync(filepath, "utf-8").trim();
342
- if (!content) return result;
343
340
 
344
- for (const line of content.split("\n")) {
345
- let entry: InferenceLine;
346
- try {
347
- entry = JSON.parse(line) as InferenceLine;
348
- } catch {
349
- continue;
350
- }
351
- const tokens = inferenceTokens(entry);
341
+ for (const call of events.filter((e) => e.type === "inference_call")) {
342
+ const tokens = inferenceTokens(call);
343
+ if (tokens === null) continue;
344
+ const model = textOr(call.model, "unknown");
345
+ const caller = textOr(call.caller, "anonymous");
352
346
 
353
- addToTimeBuckets(result.buckets, entry.ts, entry.model, tokens, horizons);
347
+ addToTimeBuckets(result.buckets, call.ts, model, tokens, horizons);
354
348
 
355
- result.byModel[entry.model] ??= emptyTimeBuckets();
356
- addToTimeBuckets(
357
- result.byModel[entry.model],
358
- entry.ts,
359
- entry.model,
360
- tokens,
361
- horizons
362
- );
349
+ result.byModel[model] ??= emptyTimeBuckets();
350
+ addToTimeBuckets(result.byModel[model], call.ts, model, tokens, horizons);
363
351
 
364
- result.byCaller[entry.caller] ??= emptyBucket();
365
- addToBucket(result.byCaller[entry.caller], entry.model, tokens, entry.ts);
352
+ result.byCaller[caller] ??= emptyBucket();
353
+ addToBucket(result.byCaller[caller], model, tokens, call.ts);
366
354
  }
367
355
 
368
356
  return result;
@@ -21,7 +21,6 @@ import { parseArgs } from "node:util";
21
21
  import { inference } from "../hooks/lib/inference";
22
22
  import { ensureDir, paths } from "../hooks/lib/paths";
23
23
  import { identity as loadSettingsIdentity } from "../hooks/lib/settings";
24
- import { logTokenUsage } from "../hooks/lib/token-usage";
25
24
  import {
26
25
  archiveDateOf,
27
26
  buildPrompt,
@@ -89,7 +88,6 @@ async function composeSelfModel(days: number): Promise<string> {
89
88
  caller: "self-model",
90
89
  });
91
90
 
92
- logTokenUsage("self-model", result);
93
91
  if (result.success && result.output) {
94
92
  return result.output.trimEnd() + metaFooter(data);
95
93
  }
@@ -6,7 +6,8 @@
6
6
  import { spawnSync } from "node:child_process";
7
7
  import { resolve } from "node:path";
8
8
  import { AGENT_REGISTRY, type AgentName } from "../hooks/lib/agent-registry";
9
- import { palHome, platform } from "../hooks/lib/paths";
9
+ import { eventsSince } from "../hooks/lib/events";
10
+ import { platform } from "../hooks/lib/paths";
10
11
  import { findBinaryOnPath } from "../hooks/lib/which";
11
12
  import {
12
13
  opencodeDatabase,
@@ -60,7 +61,7 @@ function rtkGain(): RtkGain {
60
61
  export function collectUsage(project: string | undefined): UsageData {
61
62
  return {
62
63
  agents: agentReports(project),
63
- pal: readPalInference(resolve(palHome(), "memory", "signals", "token-usage.jsonl")),
64
+ pal: readPalInference(eventsSince("inference_call", new Date(0))),
64
65
  rtk: rtkGain(),
65
66
  untracked: installedUntracked(),
66
67
  };
@@ -1,53 +0,0 @@
1
- /**
2
- * Append-only JSONL logger for inference token usage.
3
- * Writes to memory/signals/token-usage.jsonl
4
- */
5
-
6
- import { appendFileSync } from "node:fs";
7
- import { resolve } from "node:path";
8
- import type { InferenceUsage } from "./inference-usage";
9
- import { ensureDir, paths } from "./paths";
10
-
11
- type TokenCaller =
12
- | "rating"
13
- | "rule-drafter"
14
- | "failure"
15
- | "session-name"
16
- | "session-intelligence"
17
- | "entity-extraction"
18
- | "entity-research"
19
- | "relationship"
20
- | "self-model"
21
- | "agenda-serves"
22
- | "agenda-goal-links"
23
- | "agenda-waiting-on"
24
- | "agenda-moves";
25
-
26
- const UNKNOWN_MODEL = "unknown";
27
-
28
- interface TokenUsageEntry {
29
- ts: string;
30
- caller: TokenCaller;
31
- model: string;
32
- inputTokens: number;
33
- outputTokens: number;
34
- }
35
-
36
- export function logTokenUsage(
37
- caller: TokenCaller,
38
- call: { usage?: InferenceUsage; model?: string }
39
- ): void {
40
- const { inputTokens, outputTokens } = call.usage ?? {};
41
- if (inputTokens === undefined || outputTokens === undefined) return;
42
- const entry: TokenUsageEntry = {
43
- ts: new Date().toISOString(),
44
- caller,
45
- model: call.model ?? UNKNOWN_MODEL,
46
- inputTokens,
47
- outputTokens,
48
- };
49
-
50
- const dir = ensureDir(paths.signals());
51
- const filepath = resolve(dir, "token-usage.jsonl");
52
- appendFileSync(filepath, `${JSON.stringify(entry)}\n`, "utf-8");
53
- }