portable-agent-layer 0.102.0 → 0.103.1
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/package.json +1 -1
- package/src/cli/cost.ts +23 -3
- package/src/hooks/LoadContext.ts +6 -12
- package/src/hooks/lib/context-overhead.ts +30 -0
- package/src/hooks/lib/events.ts +1 -0
- package/src/hooks/lib/inference.ts +31 -7
- package/src/hooks/lib/semi-static.ts +1 -1
- package/src/hooks/lib/session-context.ts +10 -0
- package/src/hooks/lib/session-events.ts +1 -1
- package/src/tools/lib/cost/efficiency.ts +58 -0
- package/src/tools/lib/cost/leaderboards.ts +81 -0
- package/src/tools/lib/cost/mix.ts +50 -0
- package/src/tools/lib/cost/overhead.ts +29 -0
- package/src/tools/lib/cost/render.ts +70 -0
- package/src/tools/lib/report-rows.ts +4 -1
- package/src/tools/lib/wrapped/records.ts +4 -4
package/package.json
CHANGED
package/src/cli/cost.ts
CHANGED
|
@@ -1,11 +1,31 @@
|
|
|
1
1
|
import { labelOfRuntime } from "../hooks/lib/agent-registry";
|
|
2
|
-
import {
|
|
2
|
+
import { allEventsSince } from "../hooks/lib/events";
|
|
3
3
|
import { leaf } from "../tools/lib/command";
|
|
4
|
+
import { efficiency } from "../tools/lib/cost/efficiency";
|
|
5
|
+
import { leaderboards } from "../tools/lib/cost/leaderboards";
|
|
6
|
+
import { modelMix, palSpend } from "../tools/lib/cost/mix";
|
|
7
|
+
import { contextOverhead } from "../tools/lib/cost/overhead";
|
|
8
|
+
import {
|
|
9
|
+
efficiencyLines,
|
|
10
|
+
leaderboardLines,
|
|
11
|
+
overheadLines,
|
|
12
|
+
shareLines,
|
|
13
|
+
} from "../tools/lib/cost/render";
|
|
4
14
|
import { costLines, costMonths } from "../tools/lib/cost-report";
|
|
15
|
+
import { ofType } from "../tools/lib/wrapped/year";
|
|
5
16
|
|
|
6
17
|
function printCost(): undefined {
|
|
7
|
-
const
|
|
8
|
-
|
|
18
|
+
const events = allEventsSince(new Date(0));
|
|
19
|
+
const months = costMonths(ofType(events, "model_call"));
|
|
20
|
+
const lines = [
|
|
21
|
+
...costLines(months, labelOfRuntime),
|
|
22
|
+
...leaderboardLines(leaderboards(events)),
|
|
23
|
+
...efficiencyLines(efficiency(events)),
|
|
24
|
+
...shareLines("Model mix", modelMix(events)),
|
|
25
|
+
...shareLines("PAL's own spend per caller, already in the totals", palSpend(events)),
|
|
26
|
+
...overheadLines(contextOverhead(events)),
|
|
27
|
+
];
|
|
28
|
+
for (const line of lines) console.log(line);
|
|
9
29
|
}
|
|
10
30
|
|
|
11
31
|
export const costCommand = leaf({
|
package/src/hooks/LoadContext.ts
CHANGED
|
@@ -8,23 +8,21 @@
|
|
|
8
8
|
* Which agent gets what, in which envelope, is in lib/session-context.ts.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { mkdirSync, writeFileSync } from "node:fs";
|
|
12
|
-
import { resolve } from "node:path";
|
|
13
11
|
import { getActiveAgent } from "./lib/agent";
|
|
14
12
|
import { autoUpdateOnStart } from "./lib/auto-update";
|
|
15
13
|
import { buildClaudeMd, regenerateIfNeeded } from "./lib/claude-md";
|
|
16
14
|
import { type AgentTarget, buildSystemReminder } from "./lib/context";
|
|
15
|
+
import { logContextInjected } from "./lib/context-overhead";
|
|
17
16
|
import { duplicatesCursorHooks } from "./lib/cursor-shadow";
|
|
18
17
|
import { logContextSnapshot, logDebug, logError } from "./lib/log";
|
|
19
|
-
import { platform } from "./lib/paths";
|
|
20
18
|
import {
|
|
21
19
|
contextEnvelope,
|
|
22
20
|
contextForSource,
|
|
23
|
-
copilotInstructions,
|
|
24
21
|
isSubagentSession,
|
|
25
22
|
needsAgentsMd,
|
|
23
|
+
writeCopilotInstructions,
|
|
26
24
|
} from "./lib/session-context";
|
|
27
|
-
import { logSessionStart } from "./lib/session-events";
|
|
25
|
+
import { logSessionStart, type SessionStartPayload } from "./lib/session-events";
|
|
28
26
|
import { isPalSpawnedInference } from "./lib/spawn-guard";
|
|
29
27
|
import { readStdinJSON } from "./lib/stdin";
|
|
30
28
|
|
|
@@ -49,7 +47,7 @@ try {
|
|
|
49
47
|
|
|
50
48
|
try {
|
|
51
49
|
const active = getActiveAgent();
|
|
52
|
-
const input = await readStdinJSON<
|
|
50
|
+
const input = await readStdinJSON<SessionStartPayload>();
|
|
53
51
|
logSessionStart(input);
|
|
54
52
|
const wanted = contextForSource(active, input?.source);
|
|
55
53
|
if (wanted === "none") {
|
|
@@ -75,15 +73,11 @@ try {
|
|
|
75
73
|
);
|
|
76
74
|
if (!envelope) process.exit(0);
|
|
77
75
|
|
|
78
|
-
if (envelope.file)
|
|
79
|
-
const dir = resolve(platform.copilotDir(), "instructions");
|
|
80
|
-
mkdirSync(dir, { recursive: true });
|
|
81
|
-
const path = resolve(dir, "pal-session.instructions.md");
|
|
82
|
-
writeFileSync(path, copilotInstructions(envelope.file), "utf-8");
|
|
83
|
-
}
|
|
76
|
+
if (envelope.file) writeCopilotInstructions(envelope.file);
|
|
84
77
|
|
|
85
78
|
if (envelope.kind === "text") console.log(envelope.payload);
|
|
86
79
|
else process.stdout.write(envelope.payload);
|
|
80
|
+
logContextInjected(input, reminder);
|
|
87
81
|
logDebug("LoadContext", `Reminder injected: ${reminder.length} chars`);
|
|
88
82
|
} catch (err) {
|
|
89
83
|
logError("LoadContext:reminder", err);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { buildClaudeMd } from "./claude-md";
|
|
2
|
+
import { appendEvent, logQuietly } from "./events";
|
|
3
|
+
import { type HookTurnPayload, hookSessionId } from "./hook-turn";
|
|
4
|
+
import { getSemiStaticSources, readFileSafe } from "./semi-static";
|
|
5
|
+
|
|
6
|
+
function semiStaticChars(): number {
|
|
7
|
+
return getSemiStaticSources().reduce(
|
|
8
|
+
(sum, source) => sum + readFileSafe(source.path).length,
|
|
9
|
+
0
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Chars, not tokens: no tokenizer runs at session start. */
|
|
14
|
+
export function logContextInjected(
|
|
15
|
+
payload: HookTurnPayload | null,
|
|
16
|
+
reminder: string,
|
|
17
|
+
now: Date = new Date()
|
|
18
|
+
): void {
|
|
19
|
+
logQuietly("context_injected", () =>
|
|
20
|
+
appendEvent(
|
|
21
|
+
"context_injected",
|
|
22
|
+
{
|
|
23
|
+
instructionsChars: buildClaudeMd().length,
|
|
24
|
+
semiStaticChars: semiStaticChars(),
|
|
25
|
+
dynamicChars: reminder.length,
|
|
26
|
+
},
|
|
27
|
+
{ ts: now.toISOString(), session: hookSessionId(payload) ?? null }
|
|
28
|
+
)
|
|
29
|
+
);
|
|
30
|
+
}
|
package/src/hooks/lib/events.ts
CHANGED
|
@@ -54,6 +54,7 @@ export const EVENT_FIELDS = {
|
|
|
54
54
|
],
|
|
55
55
|
session_end: ["reason", "durationSec", "turns", "messages", "status"],
|
|
56
56
|
compaction: ["trigger", "withInstructions", "contextTokens"],
|
|
57
|
+
context_injected: ["instructionsChars", "semiStaticChars", "dynamicChars"],
|
|
57
58
|
hook_timing: ["hook", "totalMs", "handlers"],
|
|
58
59
|
inference_call: [
|
|
59
60
|
"caller",
|
|
@@ -294,13 +294,7 @@ async function routeInference(opts: InferenceOptions): Promise<InferenceResult>
|
|
|
294
294
|
const bin = getCopilotBinary();
|
|
295
295
|
if (bin) {
|
|
296
296
|
logDebug("inference", `${tag} route=copilot-spawn agent=${agent}`);
|
|
297
|
-
return
|
|
298
|
-
bin,
|
|
299
|
-
buildCopilotArgs(opts),
|
|
300
|
-
buildCliPrompt(opts),
|
|
301
|
-
opts,
|
|
302
|
-
COPILOT_REPLY
|
|
303
|
-
);
|
|
297
|
+
return inferenceViaCopilotSpawn(bin, opts);
|
|
304
298
|
}
|
|
305
299
|
}
|
|
306
300
|
if (isCursor()) {
|
|
@@ -709,6 +703,36 @@ const COPILOT_REPLY: CliReply = {
|
|
|
709
703
|
usage: copilotSpawnUsage,
|
|
710
704
|
};
|
|
711
705
|
|
|
706
|
+
const COPILOT_TOKEN_ENV = ["COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"];
|
|
707
|
+
|
|
708
|
+
/** A /login account lives in COPILOT_HOME's config.json; only a token variable logs in without it. */
|
|
709
|
+
function copilotLogsInWithoutHome(env: NodeJS.ProcessEnv): boolean {
|
|
710
|
+
return COPILOT_TOKEN_ENV.some((name) => Boolean(env[name]));
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/** --no-custom-instructions still loads COPILOT_HOME/instructions, where PAL's identity lives. */
|
|
714
|
+
async function inferenceViaCopilotSpawn(
|
|
715
|
+
bin: string,
|
|
716
|
+
opts: InferenceOptions
|
|
717
|
+
): Promise<InferenceResult> {
|
|
718
|
+
const spawn = (place?: SpawnPlace) =>
|
|
719
|
+
inferenceViaCliSpawn(
|
|
720
|
+
bin,
|
|
721
|
+
buildCopilotArgs(opts),
|
|
722
|
+
buildCliPrompt(opts),
|
|
723
|
+
opts,
|
|
724
|
+
COPILOT_REPLY,
|
|
725
|
+
place
|
|
726
|
+
);
|
|
727
|
+
if (!copilotLogsInWithoutHome(withPalEnv(process.env))) return spawn();
|
|
728
|
+
const dir = await mkdtemp(join(tmpdir(), "pal-copilot-"));
|
|
729
|
+
try {
|
|
730
|
+
return await spawn({ cwd: dir, env: { COPILOT_HOME: dir } });
|
|
731
|
+
} finally {
|
|
732
|
+
await rm(dir, { recursive: true, force: true });
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
712
736
|
function copilotTools(opts: InferenceOptions): string[] {
|
|
713
737
|
return opts.web
|
|
714
738
|
? ["--available-tools=web_fetch", "--allow-tool=web_fetch", "--allow-all-urls"]
|
|
@@ -48,7 +48,7 @@ export function antigravityRule(description: string, content: string): string {
|
|
|
48
48
|
return `---\ntrigger: always_on\ndescription: ${description}\n---\n\n${content}`;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
function readFileSafe(path: string): string {
|
|
51
|
+
export function readFileSafe(path: string): string {
|
|
52
52
|
try {
|
|
53
53
|
if (!existsSync(path)) return "";
|
|
54
54
|
return readFileSync(path, "utf-8").trim();
|
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
* context for weeks — and none of it was reachable from a test.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { resolve } from "node:path";
|
|
10
12
|
import type { AgentName } from "./agent-registry";
|
|
13
|
+
import { platform } from "./paths";
|
|
11
14
|
|
|
12
15
|
export interface ContextEnvelope {
|
|
13
16
|
/** Whether the agent parses stdout as JSON or reads it as raw text. */
|
|
@@ -96,3 +99,10 @@ export function contextEnvelope(
|
|
|
96
99
|
export function copilotInstructions(context: string): string {
|
|
97
100
|
return `---\napplyTo: "**"\n---\n\n${context}`;
|
|
98
101
|
}
|
|
102
|
+
|
|
103
|
+
export function writeCopilotInstructions(context: string): void {
|
|
104
|
+
const dir = resolve(platform.copilotDir(), "instructions");
|
|
105
|
+
mkdirSync(dir, { recursive: true });
|
|
106
|
+
const path = resolve(dir, "pal-session.instructions.md");
|
|
107
|
+
writeFileSync(path, copilotInstructions(context), "utf-8");
|
|
108
|
+
}
|
|
@@ -20,7 +20,7 @@ const SESSION_LOOKBACK_MS = 7 * 24 * 60 * 60 * 1000;
|
|
|
20
20
|
const TRANSCRIPT_TAIL_BYTES = 256 * 1024;
|
|
21
21
|
const MAX_FIELD = 64;
|
|
22
22
|
|
|
23
|
-
interface SessionStartPayload extends HookTurnPayload {
|
|
23
|
+
export interface SessionStartPayload extends HookTurnPayload {
|
|
24
24
|
source?: string;
|
|
25
25
|
model?: unknown;
|
|
26
26
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { LoggedEvent } from "../../../hooks/lib/events";
|
|
2
|
+
import { apiPriceOf } from "../cost-report";
|
|
3
|
+
import { dayOf, numberOf, ofType } from "../wrapped/year";
|
|
4
|
+
|
|
5
|
+
export interface PerUnit {
|
|
6
|
+
cost: number;
|
|
7
|
+
units: number;
|
|
8
|
+
lowerBound: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface Efficiency {
|
|
12
|
+
since: string;
|
|
13
|
+
perCommit: PerUnit | null;
|
|
14
|
+
perClosedIsc: PerUnit | null;
|
|
15
|
+
cacheHitRate: number | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const PROMPT_FIELDS = [
|
|
19
|
+
"inputTokens",
|
|
20
|
+
"cacheRead",
|
|
21
|
+
"cacheWrite5m",
|
|
22
|
+
"cacheWrite1h",
|
|
23
|
+
] as const;
|
|
24
|
+
|
|
25
|
+
function cacheHitRate(calls: LoggedEvent[]): number | null {
|
|
26
|
+
let read = 0;
|
|
27
|
+
let prompt = 0;
|
|
28
|
+
for (const call of calls) {
|
|
29
|
+
for (const field of PROMPT_FIELDS) prompt += numberOf(call[field]) ?? 0;
|
|
30
|
+
read += numberOf(call.cacheRead) ?? 0;
|
|
31
|
+
}
|
|
32
|
+
return prompt === 0 ? null : read / prompt;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function perUnit(spent: number, unpriced: number, units: number): PerUnit | null {
|
|
36
|
+
return units === 0 ? null : { cost: spent / units, units, lowerBound: unpriced > 0 };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Only since the first logged model call: older commits and ISCs cost nothing PAL saw. */
|
|
40
|
+
export function efficiency(events: LoggedEvent[]): Efficiency | null {
|
|
41
|
+
const calls = ofType(events, "model_call");
|
|
42
|
+
const [first] = [...calls].sort((a, b) => a.ts.localeCompare(b.ts));
|
|
43
|
+
if (first === undefined) return null;
|
|
44
|
+
const since = (e: LoggedEvent) => e.ts >= first.ts;
|
|
45
|
+
const prices = calls.map(apiPriceOf);
|
|
46
|
+
const spent = prices.reduce<number>((sum, p) => sum + (p ?? 0), 0);
|
|
47
|
+
const unpriced = prices.filter((p) => p === null).length;
|
|
48
|
+
const commits = ofType(events, "commit").filter(since).length;
|
|
49
|
+
const closed = ofType(events, "commitment").filter(
|
|
50
|
+
(e) => since(e) && e.outcome === "kept"
|
|
51
|
+
).length;
|
|
52
|
+
return {
|
|
53
|
+
since: dayOf(first),
|
|
54
|
+
perCommit: perUnit(spent, unpriced, commits),
|
|
55
|
+
perClosedIsc: perUnit(spent, unpriced, closed),
|
|
56
|
+
cacheHitRate: cacheHitRate(calls),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { LoggedEvent } from "../../../hooks/lib/events";
|
|
2
|
+
import { apiPriceOf } from "../cost-report";
|
|
3
|
+
import { turnCost, turnSpans, whereAndWhen } from "../wrapped/records";
|
|
4
|
+
import { dayOf, ofType, tally } from "../wrapped/year";
|
|
5
|
+
|
|
6
|
+
export interface Entry {
|
|
7
|
+
label: string;
|
|
8
|
+
cost: number;
|
|
9
|
+
unpriced: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface Leaderboards {
|
|
13
|
+
projects: Entry[];
|
|
14
|
+
sessions: Entry[];
|
|
15
|
+
days: Entry[];
|
|
16
|
+
turns: Entry[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const TOP = 5;
|
|
20
|
+
|
|
21
|
+
const mostExpensive = (entries: Entry[]) =>
|
|
22
|
+
entries.sort((a, b) => b.cost - a.cost || a.label.localeCompare(b.label)).slice(0, TOP);
|
|
23
|
+
|
|
24
|
+
function board(calls: LoggedEvent[], labelOf: (call: LoggedEvent) => string): Entry[] {
|
|
25
|
+
const entries = new Map<string, Entry>();
|
|
26
|
+
for (const call of calls) {
|
|
27
|
+
const label = labelOf(call);
|
|
28
|
+
const entry = entries.get(label) ?? { label, cost: 0, unpriced: 0 };
|
|
29
|
+
entries.set(label, entry);
|
|
30
|
+
const price = apiPriceOf(call);
|
|
31
|
+
if (price === null) entry.unpriced++;
|
|
32
|
+
else entry.cost += price;
|
|
33
|
+
}
|
|
34
|
+
return mostExpensive([...entries.values()]);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const projectOf = (call: LoggedEvent) => call.project ?? "no project";
|
|
38
|
+
|
|
39
|
+
function sessionLabel(session: string | null, own: LoggedEvent[]): string {
|
|
40
|
+
const [project] = tally(
|
|
41
|
+
own.filter((call) => call.project !== null),
|
|
42
|
+
projectOf
|
|
43
|
+
);
|
|
44
|
+
const [first] = own.sort((a, b) => a.ts.localeCompare(b.ts));
|
|
45
|
+
const id = session?.slice(0, 8) ?? "no session";
|
|
46
|
+
return `${id} (${project?.name ?? "no project"}, ${dayOf(first)})`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A session is named by the project most of its calls ran in, and the day it started. */
|
|
50
|
+
function sessionLabels(calls: LoggedEvent[]): Map<string | null, string> {
|
|
51
|
+
const bySession = new Map<string | null, LoggedEvent[]>();
|
|
52
|
+
for (const call of calls) {
|
|
53
|
+
const own = bySession.get(call.session) ?? [];
|
|
54
|
+
bySession.set(call.session, own);
|
|
55
|
+
own.push(call);
|
|
56
|
+
}
|
|
57
|
+
return new Map(
|
|
58
|
+
[...bySession].map(([session, own]) => [session, sessionLabel(session, own)])
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function turnBoard(events: LoggedEvent[]): Entry[] {
|
|
63
|
+
return mostExpensive(
|
|
64
|
+
turnSpans(events).flatMap((span) => {
|
|
65
|
+
const cost = turnCost(span);
|
|
66
|
+
return cost === null ? [] : [{ label: whereAndWhen(span.turn), cost, unpriced: 0 }];
|
|
67
|
+
})
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Every model call counts, PAL's own spawns included; its inference_call twins never do. */
|
|
72
|
+
export function leaderboards(events: LoggedEvent[]): Leaderboards {
|
|
73
|
+
const calls = ofType(events, "model_call");
|
|
74
|
+
const sessions = sessionLabels(calls);
|
|
75
|
+
return {
|
|
76
|
+
projects: board(calls, projectOf),
|
|
77
|
+
sessions: board(calls, (call) => String(sessions.get(call.session))),
|
|
78
|
+
days: board(calls, dayOf),
|
|
79
|
+
turns: turnBoard(events),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { LoggedEvent } from "../../../hooks/lib/events";
|
|
2
|
+
import { apiPriceOf } from "../cost-report";
|
|
3
|
+
import { numberOf, ofType } from "../wrapped/year";
|
|
4
|
+
|
|
5
|
+
export interface Share {
|
|
6
|
+
label: string;
|
|
7
|
+
calls: number;
|
|
8
|
+
cost: number;
|
|
9
|
+
unpriced: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function shares(
|
|
13
|
+
calls: LoggedEvent[],
|
|
14
|
+
labelOf: (call: LoggedEvent) => string,
|
|
15
|
+
priceOf: (call: LoggedEvent) => number | null
|
|
16
|
+
): Share[] {
|
|
17
|
+
const byLabel = new Map<string, Share>();
|
|
18
|
+
for (const call of calls) {
|
|
19
|
+
const label = labelOf(call);
|
|
20
|
+
const share = byLabel.get(label) ?? { label, calls: 0, cost: 0, unpriced: 0 };
|
|
21
|
+
byLabel.set(label, share);
|
|
22
|
+
share.calls++;
|
|
23
|
+
const price = priceOf(call);
|
|
24
|
+
if (price === null) share.unpriced++;
|
|
25
|
+
else share.cost += price;
|
|
26
|
+
}
|
|
27
|
+
return [...byLabel.values()].sort(
|
|
28
|
+
(a, b) => b.cost - a.cost || b.calls - a.calls || a.label.localeCompare(b.label)
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const textOr = (value: unknown, fallback: string) =>
|
|
33
|
+
typeof value === "string" ? value : fallback;
|
|
34
|
+
|
|
35
|
+
export function modelMix(events: LoggedEvent[]): Share[] {
|
|
36
|
+
return shares(
|
|
37
|
+
ofType(events, "model_call"),
|
|
38
|
+
(call) => textOr(call.model, "unknown model"),
|
|
39
|
+
apiPriceOf
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Already inside the model calls PAL spawned; never add it to a total. */
|
|
44
|
+
export function palSpend(events: LoggedEvent[]): Share[] {
|
|
45
|
+
return shares(
|
|
46
|
+
ofType(events, "inference_call"),
|
|
47
|
+
(call) => textOr(call.caller, "unknown caller"),
|
|
48
|
+
(call) => numberOf(call.costUsd)
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { LoggedEvent } from "../../../hooks/lib/events";
|
|
2
|
+
import { dayOf, numberOf, ofType } from "../wrapped/year";
|
|
3
|
+
|
|
4
|
+
type Tier = "instructionsChars" | "semiStaticChars" | "dynamicChars";
|
|
5
|
+
|
|
6
|
+
export interface Overhead {
|
|
7
|
+
since: string;
|
|
8
|
+
sessions: number;
|
|
9
|
+
perSession: Record<Tier, number>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** A compacted session is injected again; its injections add up. */
|
|
13
|
+
export function contextOverhead(events: LoggedEvent[]): Overhead | null {
|
|
14
|
+
const injections = ofType(events, "context_injected");
|
|
15
|
+
const [first] = [...injections].sort((a, b) => a.ts.localeCompare(b.ts));
|
|
16
|
+
if (first === undefined) return null;
|
|
17
|
+
const sessions = new Set(injections.map((e) => e.session ?? e.id)).size;
|
|
18
|
+
const average = (tier: Tier) =>
|
|
19
|
+
injections.reduce((sum, e) => sum + (numberOf(e[tier]) ?? 0), 0) / sessions;
|
|
20
|
+
return {
|
|
21
|
+
since: dayOf(first),
|
|
22
|
+
sessions,
|
|
23
|
+
perSession: {
|
|
24
|
+
instructionsChars: average("instructionsChars"),
|
|
25
|
+
semiStaticChars: average("semiStaticChars"),
|
|
26
|
+
dynamicChars: average("dynamicChars"),
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { count, counted } from "../report-rows";
|
|
2
|
+
import { fmtCost } from "../token-report";
|
|
3
|
+
import type { Efficiency, PerUnit } from "./efficiency";
|
|
4
|
+
import type { Entry, Leaderboards } from "./leaderboards";
|
|
5
|
+
import type { Share } from "./mix";
|
|
6
|
+
import type { Overhead } from "./overhead";
|
|
7
|
+
|
|
8
|
+
function entryLine({ label, cost, unpriced }: Entry): string {
|
|
9
|
+
const extra = unpriced > 0 ? ` + ${count(unpriced)} unpriced calls` : "";
|
|
10
|
+
return ` ${fmtCost(cost).padStart(10)} ${label}${extra}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function boardLines(title: string, entries: Entry[]): string[] {
|
|
14
|
+
const lines = entries.length === 0 ? [" unknown"] : entries.map(entryLine);
|
|
15
|
+
return [`\n Most expensive ${title}\n`, ...lines];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function leaderboardLines(boards: Leaderboards): string[] {
|
|
19
|
+
return [
|
|
20
|
+
...boardLines("projects", boards.projects),
|
|
21
|
+
...boardLines("sessions", boards.sessions),
|
|
22
|
+
...boardLines("days", boards.days),
|
|
23
|
+
...boardLines("turns", boards.turns),
|
|
24
|
+
];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function perUnitText(per: PerUnit | null, unit: string): string {
|
|
28
|
+
if (per === null) return "unknown";
|
|
29
|
+
const atLeast = per.lowerBound ? "at least " : "";
|
|
30
|
+
return `${atLeast}${fmtCost(per.cost)} over ${counted(per.units, unit)}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const percent = (rate: number | null) =>
|
|
34
|
+
rate === null ? "unknown" : `${(rate * 100).toFixed(1)}%`;
|
|
35
|
+
|
|
36
|
+
export function efficiencyLines(efficiency: Efficiency | null): string[] {
|
|
37
|
+
if (efficiency === null) return [];
|
|
38
|
+
return [
|
|
39
|
+
`\n Since ${efficiency.since}\n`,
|
|
40
|
+
` Per commit ${perUnitText(efficiency.perCommit, "commit")}`,
|
|
41
|
+
` Per closed ISC ${perUnitText(efficiency.perClosedIsc, "ISC")}`,
|
|
42
|
+
` Cache hit rate ${percent(efficiency.cacheHitRate)}`,
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function shareLine({ label, calls, cost, unpriced }: Share): string {
|
|
47
|
+
const price = unpriced === calls ? "unknown" : fmtCost(cost);
|
|
48
|
+
const extra = unpriced > 0 && unpriced < calls ? `, ${count(unpriced)} unpriced` : "";
|
|
49
|
+
return ` ${price.padStart(10)} ${label} ${counted(calls, "call")}${extra}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const chars = (n: number) => `${count(Math.round(n))} chars`.padStart(14);
|
|
53
|
+
|
|
54
|
+
export function overheadLines(overhead: Overhead | null): string[] {
|
|
55
|
+
const title = "\n PAL context per session\n";
|
|
56
|
+
if (overhead === null) return [title, " unknown"];
|
|
57
|
+
const { instructionsChars, semiStaticChars, dynamicChars } = overhead.perSession;
|
|
58
|
+
return [
|
|
59
|
+
`\n PAL context per session, over ${counted(overhead.sessions, "session")} since ${overhead.since}\n`,
|
|
60
|
+
` Instructions ${chars(instructionsChars)}`,
|
|
61
|
+
` Semi-static ${chars(semiStaticChars)}`,
|
|
62
|
+
` Dynamic ${chars(dynamicChars)}`,
|
|
63
|
+
` Total ${chars(instructionsChars + semiStaticChars + dynamicChars)}`,
|
|
64
|
+
];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function shareLines(title: string, shares: Share[]): string[] {
|
|
68
|
+
const lines = shares.length === 0 ? [" unknown"] : shares.map(shareLine);
|
|
69
|
+
return [`\n ${title}\n`, ...lines];
|
|
70
|
+
}
|
|
@@ -11,4 +11,7 @@ export function tallyLabel(tallies: Tally[]): string {
|
|
|
11
11
|
return tallies.map((t) => `${t.name} ${count(t.count)}`).join(" · ");
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
export const
|
|
14
|
+
export const counted = (n: number, noun: string) =>
|
|
15
|
+
`${count(n)} ${noun}${n === 1 ? "" : "s"}`;
|
|
16
|
+
|
|
17
|
+
export const days = (n: number) => counted(n, "day");
|
|
@@ -65,13 +65,13 @@ function longestAgentRun(events: LoggedEvent[]): Best | null {
|
|
|
65
65
|
);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
interface TurnSpan {
|
|
68
|
+
export interface TurnSpan {
|
|
69
69
|
turn: LoggedEvent;
|
|
70
70
|
inside: LoggedEvent[];
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
/** Each user turn and what happened in its session until the next one. */
|
|
74
|
-
function turnSpans(events: LoggedEvent[]): TurnSpan[] {
|
|
74
|
+
export function turnSpans(events: LoggedEvent[]): TurnSpan[] {
|
|
75
75
|
const open = new Map<string, TurnSpan>();
|
|
76
76
|
const spans: TurnSpan[] = [];
|
|
77
77
|
const inTimeOrder = [...events].sort((a, b) => a.ts.localeCompare(b.ts));
|
|
@@ -86,11 +86,11 @@ function turnSpans(events: LoggedEvent[]): TurnSpan[] {
|
|
|
86
86
|
return spans;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
const whereAndWhen = (turn: LoggedEvent) =>
|
|
89
|
+
export const whereAndWhen = (turn: LoggedEvent) =>
|
|
90
90
|
turn.project === null ? dayOf(turn) : `${dayOf(turn)}, ${turn.project}`;
|
|
91
91
|
|
|
92
92
|
/** A turn with any call PAL cannot price is left out rather than undercounted. */
|
|
93
|
-
function turnCost({ inside }: TurnSpan): number | null {
|
|
93
|
+
export function turnCost({ inside }: TurnSpan): number | null {
|
|
94
94
|
const prices = ofType(inside, "model_call").map(apiPriceOf);
|
|
95
95
|
if (prices.length === 0 || prices.includes(null)) return null;
|
|
96
96
|
return prices.reduce<number>((sum, p) => sum + (p ?? 0), 0);
|