portable-agent-layer 0.103.0 → 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.
- package/assets/templates/PAL/MEMORY_SYSTEM.md +1 -2
- package/package.json +1 -1
- package/src/hooks/handlers/agenda.ts +0 -5
- package/src/hooks/handlers/entity-extraction.ts +0 -2
- package/src/hooks/handlers/rating.ts +0 -4
- package/src/hooks/handlers/session-intelligence.ts +0 -3
- package/src/hooks/handlers/session-name.ts +0 -2
- package/src/hooks/handlers/update-counts.ts +0 -70
- package/src/hooks/lib/entity-research.ts +0 -2
- package/src/hooks/lib/inference-usage.ts +53 -3
- package/src/hooks/lib/inference.ts +57 -7
- package/src/hooks/lib/security.ts +0 -1
- package/src/tools/lib/agent-usage.ts +4 -3
- package/src/tools/lib/usage-buckets.ts +26 -38
- package/src/tools/self-model.ts +0 -2
- package/src/tools/token-cost.ts +3 -2
- package/src/hooks/lib/token-usage.ts +0 -53
|
@@ -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
|
-
│
|
|
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
|
@@ -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
|
-
|
|
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
|
|
|
@@ -294,13 +298,7 @@ async function routeInference(opts: InferenceOptions): Promise<InferenceResult>
|
|
|
294
298
|
const bin = getCopilotBinary();
|
|
295
299
|
if (bin) {
|
|
296
300
|
logDebug("inference", `${tag} route=copilot-spawn agent=${agent}`);
|
|
297
|
-
return
|
|
298
|
-
bin,
|
|
299
|
-
buildCopilotArgs(opts),
|
|
300
|
-
buildCliPrompt(opts),
|
|
301
|
-
opts,
|
|
302
|
-
COPILOT_REPLY
|
|
303
|
-
);
|
|
301
|
+
return inferenceViaCopilotSpawn(bin, opts);
|
|
304
302
|
}
|
|
305
303
|
}
|
|
306
304
|
if (isCursor()) {
|
|
@@ -709,6 +707,58 @@ const COPILOT_REPLY: CliReply = {
|
|
|
709
707
|
usage: copilotSpawnUsage,
|
|
710
708
|
};
|
|
711
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
|
+
|
|
730
|
+
const COPILOT_TOKEN_ENV = ["COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"];
|
|
731
|
+
|
|
732
|
+
/** A /login account lives in COPILOT_HOME's config.json; only a token variable logs in without it. */
|
|
733
|
+
function copilotLogsInWithoutHome(env: NodeJS.ProcessEnv): boolean {
|
|
734
|
+
return COPILOT_TOKEN_ENV.some((name) => Boolean(env[name]));
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/** --no-custom-instructions still loads COPILOT_HOME/instructions, where PAL's identity lives. */
|
|
738
|
+
async function inferenceViaCopilotSpawn(
|
|
739
|
+
bin: string,
|
|
740
|
+
opts: InferenceOptions
|
|
741
|
+
): Promise<InferenceResult> {
|
|
742
|
+
const spawn = (home: string, place?: SpawnPlace) =>
|
|
743
|
+
inferenceViaCliSpawn(
|
|
744
|
+
bin,
|
|
745
|
+
buildCopilotArgs(opts),
|
|
746
|
+
buildCliPrompt(opts),
|
|
747
|
+
opts,
|
|
748
|
+
copilotReply(home),
|
|
749
|
+
place
|
|
750
|
+
);
|
|
751
|
+
const env = withPalEnv(process.env);
|
|
752
|
+
if (!copilotLogsInWithoutHome(env))
|
|
753
|
+
return spawn(env.COPILOT_HOME || platform.copilotDir());
|
|
754
|
+
const dir = await mkdtemp(join(tmpdir(), "pal-copilot-"));
|
|
755
|
+
try {
|
|
756
|
+
return await spawn(dir, { cwd: dir, env: { COPILOT_HOME: dir } });
|
|
757
|
+
} finally {
|
|
758
|
+
await rm(dir, { recursive: true, force: true });
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
712
762
|
function copilotTools(opts: InferenceOptions): string[] {
|
|
713
763
|
return opts.web
|
|
714
764
|
? ["--available-tools=web_fetch", "--allow-tool=web_fetch", "--allow-all-urls"]
|
|
@@ -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:
|
|
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
|
-
|
|
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
|
-
/**
|
|
319
|
-
function inferenceTokens(
|
|
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
|
|
322
|
-
output
|
|
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
|
-
|
|
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
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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,
|
|
347
|
+
addToTimeBuckets(result.buckets, call.ts, model, tokens, horizons);
|
|
354
348
|
|
|
355
|
-
result.byModel[
|
|
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[
|
|
365
|
-
addToBucket(result.byCaller[
|
|
352
|
+
result.byCaller[caller] ??= emptyBucket();
|
|
353
|
+
addToBucket(result.byCaller[caller], model, tokens, call.ts);
|
|
366
354
|
}
|
|
367
355
|
|
|
368
356
|
return result;
|
package/src/tools/self-model.ts
CHANGED
|
@@ -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
|
}
|
package/src/tools/token-cost.ts
CHANGED
|
@@ -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 {
|
|
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(
|
|
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
|
-
}
|