@firstpick/pi-extension-stats 0.1.7 → 0.1.9
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 +19 -2
- package/index.ts +238 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ Token and cost analytics for Pi session history.
|
|
|
9
9
|
- Parses local Pi session `.jsonl` files for the current workspace.
|
|
10
10
|
- Aggregates usage by UTC day.
|
|
11
11
|
- Displays compact daily token bars and cost bars with totals.
|
|
12
|
-
- Shows input/output/cache breakdown, prompt
|
|
12
|
+
- Shows input/output/cache breakdown, estimated initial prompt input (`PI: X tok`) with source split-up, cache hit rate, estimated cache savings, cost burn rate, and top model usage.
|
|
13
13
|
- Highlights highest-cost day, projected 30-day cost, most expensive sessions, and model cost efficiency.
|
|
14
14
|
|
|
15
15
|
## Install
|
|
@@ -26,13 +26,30 @@ No required configuration.
|
|
|
26
26
|
|
|
27
27
|
- `/stats [days|all]` — show token usage dashboard (default: last 14 days).
|
|
28
28
|
- `/stats tokens` — show current context token breakdown by source/type.
|
|
29
|
-
- `/stats-pi` — show prompt
|
|
29
|
+
- `/stats-pi` — show estimated initial prompt input token breakdown. It counts Pi's system prompt text, active provider-level tool schemas, framing overhead, and optional historical calibration.
|
|
30
|
+
- `/calibrate` — start an isolated calibration session with a fixed probe prompt, then update `/stats-pi` and the footer `PI: X tok` estimate from the first assistant response usage. `/calibrate current` reuses the current branch if it already has a suitable first-turn usage sample.
|
|
30
31
|
- `/stats-last [days|all]` — show non-zero daily usage graph.
|
|
31
32
|
- `/stats-most-expense [days|all]` — show most expensive sessions.
|
|
32
33
|
- `/stats-model-compare [days|all]` — show model token/cost comparison.
|
|
33
34
|
- `/stats-cost-trend [days|all]` — show cost trend and projections.
|
|
34
35
|
- `/stats-cache [days|all]` — show cache efficiency and token mix.
|
|
35
36
|
|
|
37
|
+
## Prompt input estimate
|
|
38
|
+
|
|
39
|
+
`/stats-pi` and the `PI: ~X tok` value in `/stats` estimate the full initial model input, not just raw prompt text. `/stats-pi` can be run before any LLM prompt in a fresh session.
|
|
40
|
+
|
|
41
|
+
The calculation is intentionally provider-agnostic:
|
|
42
|
+
|
|
43
|
+
```text
|
|
44
|
+
promptTextTokens = weighted text estimate of ctx.getSystemPrompt()
|
|
45
|
+
toolSchemaTokens = weighted text estimate of active tool definitions JSON
|
|
46
|
+
framingTokens = conservative message/request framing allowance
|
|
47
|
+
baseEstimate = promptTextTokens + toolSchemaTokens + framingTokens
|
|
48
|
+
estimatedInitialInput = baseEstimate × historicalCalibrationMultiplier
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The historical multiplier is learned opportunistically from future sessions by comparing the pre-call estimate with the provider-reported first assistant `usage.input + usage.cacheRead + usage.cacheWrite` after subtracting the first user prompt estimate. `/calibrate` performs the same calculation on demand by opening an isolated session and sending a fixed probe prompt; `/calibrate current` can reuse the current branch once its first assistant response has usage data. Without samples, `/stats-pi` reports an uncalibrated estimate and a conservative range. Provider-reported usage in Pi session JSONL remains the authoritative post-call value.
|
|
52
|
+
|
|
36
53
|
## Tools
|
|
37
54
|
|
|
38
55
|
None.
|
package/index.ts
CHANGED
|
@@ -2,7 +2,18 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { buildSessionContext, formatSkillsForPrompt } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import type { BuildSystemPromptOptions, ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
appendInitialPromptCalibrationRecord,
|
|
7
|
+
buildInitialPromptCalibrationRecord,
|
|
8
|
+
collectInitialPromptCalibration,
|
|
9
|
+
estimateInitialPromptInput,
|
|
10
|
+
estimatePromptInjectionTokens,
|
|
11
|
+
estimateTokensFromCharCount,
|
|
12
|
+
formatTokens,
|
|
13
|
+
type InitialPromptCalibration,
|
|
14
|
+
type InitialPromptInputEstimate,
|
|
15
|
+
type InitialPromptToolInfo,
|
|
16
|
+
} from "@firstpick/pi-utils";
|
|
6
17
|
|
|
7
18
|
type DayUsage = {
|
|
8
19
|
input: number;
|
|
@@ -31,6 +42,12 @@ type UsageRecord = {
|
|
|
31
42
|
|
|
32
43
|
type Totals = DayUsage;
|
|
33
44
|
|
|
45
|
+
type PendingInitialPromptMeasurement = {
|
|
46
|
+
estimate: InitialPromptInputEstimate;
|
|
47
|
+
firstUserTokens: number;
|
|
48
|
+
skipReason?: string;
|
|
49
|
+
};
|
|
50
|
+
|
|
34
51
|
type PromptInjectionSource = {
|
|
35
52
|
label: string;
|
|
36
53
|
chars: number;
|
|
@@ -44,6 +61,8 @@ type TokenBreakdownSource = {
|
|
|
44
61
|
const DEFAULT_DAYS = 14;
|
|
45
62
|
const MAX_BAR_WIDTH = 24;
|
|
46
63
|
const COST_BAR_WIDTH = 10;
|
|
64
|
+
const FIRST_USER_MESSAGE_FRAMING_TOKENS = 16;
|
|
65
|
+
const CALIBRATION_PROMPT = "Calibration probe: reply with exactly `calibration-ok` and no other text.";
|
|
47
66
|
|
|
48
67
|
function addPromptSource(sources: PromptInjectionSource[], label: string, content: string | undefined): number {
|
|
49
68
|
if (!content) return 0;
|
|
@@ -172,22 +191,65 @@ function buildPromptInjectionSources(systemPrompt: string, options: BuildSystemP
|
|
|
172
191
|
return sources;
|
|
173
192
|
}
|
|
174
193
|
|
|
175
|
-
function
|
|
176
|
-
|
|
177
|
-
|
|
194
|
+
function formatTokenCell(tokens: number): string {
|
|
195
|
+
return tokens < 0 ? `-${formatTokens(Math.abs(tokens))}` : formatTokens(tokens);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function formatCalibrationSummary(estimate: InitialPromptInputEstimate): string {
|
|
199
|
+
if (estimate.calibrationSamples <= 0) return "uncalibrated";
|
|
200
|
+
const sampleLabel = estimate.calibrationSamples === 1 ? "sample" : "samples";
|
|
201
|
+
return `learned scale ×${estimate.calibrationMultiplier.toFixed(2)} from ${estimate.calibrationSamples} ${sampleLabel}`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function distributeCalibratedTokens<T extends { tokens: number }>(sources: T[], calibratedTotal: number): T[] {
|
|
205
|
+
const uncalibratedTotal = sources.reduce((sum, source) => sum + source.tokens, 0);
|
|
206
|
+
if (uncalibratedTotal <= 0 || calibratedTotal <= 0) return sources.map((source) => ({ ...source, tokens: 0 }));
|
|
207
|
+
|
|
208
|
+
const exact = sources.map((source, index) => {
|
|
209
|
+
const scaled = (source.tokens / uncalibratedTotal) * calibratedTotal;
|
|
210
|
+
const tokens = Math.floor(scaled);
|
|
211
|
+
return { index, tokens, remainder: scaled - tokens };
|
|
212
|
+
});
|
|
213
|
+
let remaining = calibratedTotal - exact.reduce((sum, source) => sum + source.tokens, 0);
|
|
214
|
+
for (const source of [...exact].sort((a, b) => b.remainder - a.remainder || a.index - b.index)) {
|
|
215
|
+
if (remaining <= 0) break;
|
|
216
|
+
source.tokens += 1;
|
|
217
|
+
remaining -= 1;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return sources.map((source, index) => ({ ...source, tokens: exact[index]?.tokens ?? 0 }));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function formatPromptInjectionLines(systemPrompt: string, options: BuildSystemPromptOptions | null, estimate: InitialPromptInputEstimate): string[] {
|
|
224
|
+
const promptSources = buildPromptInjectionSources(systemPrompt, options)
|
|
225
|
+
.map((source) => ({
|
|
226
|
+
...source,
|
|
227
|
+
tokens: systemPrompt.length > 0 ? Math.round((source.chars / systemPrompt.length) * estimate.promptText) : estimateTokensFromCharCount(source.chars),
|
|
228
|
+
}))
|
|
178
229
|
.sort((a, b) => b.tokens - a.tokens || b.chars - a.chars);
|
|
179
|
-
const
|
|
230
|
+
const uncalibratedSources = [
|
|
231
|
+
estimate.toolSchemas > 0
|
|
232
|
+
? { label: `Active tool schemas (${estimate.toolCount})`, chars: 0, tokens: estimate.toolSchemas }
|
|
233
|
+
: null,
|
|
234
|
+
estimate.framing > 0 ? { label: "Provider/request framing", chars: 0, tokens: estimate.framing } : null,
|
|
235
|
+
...promptSources,
|
|
236
|
+
].filter((source): source is { label: string; chars: number; tokens: number } => !!source && source.tokens !== 0);
|
|
237
|
+
const sources = distributeCalibratedTokens(uncalibratedSources, estimate.total)
|
|
238
|
+
.filter((source) => source.tokens !== 0)
|
|
239
|
+
.sort((a, b) => Math.abs(b.tokens) - Math.abs(a.tokens) || b.chars - a.chars);
|
|
180
240
|
const labelWidth = Math.max("Source".length, ...sources.map((source) => source.label.length));
|
|
181
|
-
const tokenWidth = Math.max("Tokens".length, ...sources.map((source) =>
|
|
241
|
+
const tokenWidth = Math.max("Tokens".length, ...sources.map((source) => formatTokenCell(source.tokens).length));
|
|
182
242
|
const percentWidth = "%".length;
|
|
183
243
|
const separator = `├${"─".repeat(labelWidth + 2)}┼${"─".repeat(tokenWidth + 2)}┼${"─".repeat(percentWidth + 6)}┤`;
|
|
184
244
|
const rows = sources.map((source) => {
|
|
185
|
-
const percent =
|
|
186
|
-
return `│ ${source.label.padEnd(labelWidth)} │ ${
|
|
245
|
+
const percent = estimate.total > 0 ? `${((source.tokens / estimate.total) * 100).toFixed(1)}%` : "0.0%";
|
|
246
|
+
return `│ ${source.label.padEnd(labelWidth)} │ ${formatTokenCell(source.tokens).padStart(tokenWidth)} │ ${percent.padStart(percentWidth + 4)} │`;
|
|
187
247
|
});
|
|
248
|
+
const range = estimate.low !== estimate.high ? ` · range ${formatTokens(estimate.low)}–${formatTokens(estimate.high)}` : "";
|
|
188
249
|
|
|
189
250
|
return [
|
|
190
|
-
`Prompt injection: PI:
|
|
251
|
+
`Prompt injection: PI: ~${formatTokens(estimate.total)} tok initial input (${estimate.confidence}${range})`,
|
|
252
|
+
`Unscaled basis: prompt text ${formatTokens(estimate.promptText)} + tool schemas ${formatTokens(estimate.toolSchemas)} + framing ${formatTokens(estimate.framing)} · displayed rows are proportionally scaled (${formatCalibrationSummary(estimate)})`,
|
|
191
253
|
`┌${"─".repeat(labelWidth + 2)}┬${"─".repeat(tokenWidth + 2)}┬${"─".repeat(percentWidth + 6)}┐`,
|
|
192
254
|
`│ ${"Source".padEnd(labelWidth)} │ ${"Tokens".padStart(tokenWidth)} │ ${"%".padStart(percentWidth + 4)} │`,
|
|
193
255
|
separator,
|
|
@@ -606,9 +668,129 @@ function buildCacheEfficiencyLines(totals: Totals): string[] {
|
|
|
606
668
|
|
|
607
669
|
export default function statsExtension(pi: ExtensionAPI) {
|
|
608
670
|
let latestSystemPromptOptions: BuildSystemPromptOptions | null = null;
|
|
671
|
+
let pendingInitialPromptMeasurement: PendingInitialPromptMeasurement | null = null;
|
|
672
|
+
|
|
673
|
+
const getToolEstimateInputs = (): { activeTools: string[]; allTools: InitialPromptToolInfo[] } => {
|
|
674
|
+
let activeTools: string[] = [];
|
|
675
|
+
let allTools: InitialPromptToolInfo[] = [];
|
|
609
676
|
|
|
610
|
-
|
|
677
|
+
try {
|
|
678
|
+
activeTools = pi.getActiveTools();
|
|
679
|
+
} catch {
|
|
680
|
+
activeTools = [];
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
try {
|
|
684
|
+
allTools = pi.getAllTools().map((tool) => ({
|
|
685
|
+
name: tool.name,
|
|
686
|
+
description: tool.description,
|
|
687
|
+
parameters: tool.parameters,
|
|
688
|
+
}));
|
|
689
|
+
} catch {
|
|
690
|
+
allTools = [];
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
return { activeTools, allTools };
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
const getPromptCalibration = (ctx: ExtensionCommandContext): InitialPromptCalibration | null => {
|
|
697
|
+
return collectInitialPromptCalibration(ctx.sessionManager.getSessionDir());
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
const estimateInitialPromptForContext = (systemPrompt: string, calibration?: InitialPromptCalibration | null): InitialPromptInputEstimate => {
|
|
701
|
+
const { activeTools, allTools } = getToolEstimateInputs();
|
|
702
|
+
return estimateInitialPromptInput({ systemPrompt, activeTools, allTools, calibration });
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
const branchHasAssistantUsage = (ctx: { sessionManager: { getBranch(): unknown[] } }): boolean => {
|
|
706
|
+
try {
|
|
707
|
+
return ctx.sessionManager.getBranch().some((entry) => {
|
|
708
|
+
const record = (entry && typeof entry === "object" ? entry : {}) as Record<string, any>;
|
|
709
|
+
return record.type === "message" && record.message?.role === "assistant" && !!record.message?.usage;
|
|
710
|
+
});
|
|
711
|
+
} catch {
|
|
712
|
+
return true;
|
|
713
|
+
}
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
const calibrateFromCurrentBranch = (ctx: ExtensionCommandContext): { ok: true; record: NonNullable<ReturnType<typeof buildInitialPromptCalibrationRecord>> } | { ok: false; reason: string } => {
|
|
717
|
+
let firstUserTokens: number | null = null;
|
|
718
|
+
let firstAssistantWithUsage: Record<string, any> | null = null;
|
|
719
|
+
|
|
720
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
721
|
+
const record = (entry && typeof entry === "object" ? entry : {}) as Record<string, any>;
|
|
722
|
+
if (record.type !== "message") continue;
|
|
723
|
+
|
|
724
|
+
const message = (record.message && typeof record.message === "object" ? record.message : {}) as Record<string, any>;
|
|
725
|
+
if (message.role === "user" && firstUserTokens === null) {
|
|
726
|
+
firstUserTokens = estimatePromptInjectionTokens(stringifyContextValue(message.content)) + FIRST_USER_MESSAGE_FRAMING_TOKENS;
|
|
727
|
+
}
|
|
728
|
+
if (message.role === "assistant" && message.usage) {
|
|
729
|
+
firstAssistantWithUsage = message;
|
|
730
|
+
break;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if (firstUserTokens === null) return { ok: false, reason: "No initial user message found in the current branch." };
|
|
735
|
+
if (!firstAssistantWithUsage) return { ok: false, reason: "No assistant response with usage data found yet. Run /calibrate after the first assistant response finishes." };
|
|
736
|
+
|
|
737
|
+
const usage = firstAssistantWithUsage.usage as Record<string, unknown>;
|
|
738
|
+
const actualInitialInputTokens =
|
|
739
|
+
(Number(usage.input ?? 0) || 0) + (Number(usage.cacheRead ?? 0) || 0) + (Number(usage.cacheWrite ?? 0) || 0);
|
|
740
|
+
if (actualInitialInputTokens <= 0) return { ok: false, reason: "The first assistant response has no input/cache token usage to calibrate from." };
|
|
741
|
+
|
|
742
|
+
const estimate = estimateInitialPromptForContext(ctx.getSystemPrompt(), null);
|
|
743
|
+
const record = buildInitialPromptCalibrationRecord({
|
|
744
|
+
estimate,
|
|
745
|
+
actualInitialInputTokens,
|
|
746
|
+
firstUserTokens,
|
|
747
|
+
provider: String(firstAssistantWithUsage.provider ?? ctx.model?.provider ?? "unknown"),
|
|
748
|
+
model: String(firstAssistantWithUsage.responseModel ?? firstAssistantWithUsage.model ?? ctx.model?.id ?? "unknown"),
|
|
749
|
+
});
|
|
750
|
+
if (!record) return { ok: false, reason: "Calibration sample was outside the accepted sanity range (0.25×–4×)." };
|
|
751
|
+
return { ok: true, record };
|
|
752
|
+
};
|
|
753
|
+
|
|
754
|
+
pi.on("session_start", async () => {
|
|
755
|
+
pendingInitialPromptMeasurement = null;
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
611
759
|
latestSystemPromptOptions = event.systemPromptOptions;
|
|
760
|
+
|
|
761
|
+
if (!branchHasAssistantUsage(ctx)) {
|
|
762
|
+
pendingInitialPromptMeasurement = {
|
|
763
|
+
estimate: estimateInitialPromptForContext(event.systemPrompt, null),
|
|
764
|
+
firstUserTokens: estimatePromptInjectionTokens(event.prompt) + FIRST_USER_MESSAGE_FRAMING_TOKENS,
|
|
765
|
+
skipReason: event.images && event.images.length > 0 ? "image prompt" : undefined,
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
pi.on("agent_start", async (_event, ctx) => {
|
|
771
|
+
if (!pendingInitialPromptMeasurement || pendingInitialPromptMeasurement.skipReason) return;
|
|
772
|
+
pendingInitialPromptMeasurement.estimate = estimateInitialPromptForContext(ctx.getSystemPrompt(), null);
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
pi.on("message_end", async (event) => {
|
|
776
|
+
if (!pendingInitialPromptMeasurement) return;
|
|
777
|
+
const pending = pendingInitialPromptMeasurement;
|
|
778
|
+
const message = (event.message && typeof event.message === "object" ? event.message : {}) as Record<string, any>;
|
|
779
|
+
if (message.role !== "assistant" || !message.usage) return;
|
|
780
|
+
pendingInitialPromptMeasurement = null;
|
|
781
|
+
if (pending.skipReason) return;
|
|
782
|
+
|
|
783
|
+
const usage = message.usage as Record<string, unknown>;
|
|
784
|
+
const actualInitialInputTokens =
|
|
785
|
+
(Number(usage.input ?? 0) || 0) + (Number(usage.cacheRead ?? 0) || 0) + (Number(usage.cacheWrite ?? 0) || 0);
|
|
786
|
+
const record = buildInitialPromptCalibrationRecord({
|
|
787
|
+
estimate: pending.estimate,
|
|
788
|
+
actualInitialInputTokens,
|
|
789
|
+
firstUserTokens: pending.firstUserTokens,
|
|
790
|
+
provider: String(message.provider ?? "unknown"),
|
|
791
|
+
model: String(message.responseModel ?? message.model ?? "unknown"),
|
|
792
|
+
});
|
|
793
|
+
if (record) appendInitialPromptCalibrationRecord(pi.appendEntry, record);
|
|
612
794
|
});
|
|
613
795
|
|
|
614
796
|
const showCurrentContextTokens = (ctx: ExtensionCommandContext) => {
|
|
@@ -633,8 +815,9 @@ export default function statsExtension(pi: ExtensionAPI) {
|
|
|
633
815
|
const byDay = aggregateUsageByDay(records);
|
|
634
816
|
const dayKeys = getScopeDayKeys(byDay, parsedArgs);
|
|
635
817
|
const totals = sumUsage(byDay, dayKeys);
|
|
818
|
+
const calibration = collectInitialPromptCalibration(sessionDir);
|
|
636
819
|
const scopeLabel = parsedArgs.mode === "all" ? "all days" : `last ${parsedArgs.days} days`;
|
|
637
|
-
return { files, records, byDay, dayKeys, totals, scopeLabel };
|
|
820
|
+
return { files, records, byDay, dayKeys, totals, calibration, scopeLabel };
|
|
638
821
|
};
|
|
639
822
|
|
|
640
823
|
const parseStatsCommandArgs = (args: string, ctx: ExtensionCommandContext) => {
|
|
@@ -702,14 +885,51 @@ export default function statsExtension(pi: ExtensionAPI) {
|
|
|
702
885
|
);
|
|
703
886
|
|
|
704
887
|
registerScopedStatsCommand("stats-last", "Show non-zero daily usage graph. Usage: /stats-last [days|all]", (data, ctx) => {
|
|
705
|
-
const
|
|
706
|
-
return [`📊 Token stats (${data.scopeLabel}, ${data.files.length} sessions) · PI:
|
|
888
|
+
const promptEstimate = estimateInitialPromptForContext(ctx.getSystemPrompt(), data.calibration);
|
|
889
|
+
return [`📊 Token stats (${data.scopeLabel}, ${data.files.length} sessions) · PI: ~${formatTokens(promptEstimate.total)} tok`, "", ...buildGraphLines(data.byDay, data.dayKeys, true)];
|
|
707
890
|
});
|
|
708
891
|
|
|
709
892
|
pi.registerCommand("stats-pi", {
|
|
710
|
-
description: "Show prompt
|
|
893
|
+
description: "Show estimated initial prompt input token breakdown.",
|
|
711
894
|
handler: async (_args, ctx) => {
|
|
712
|
-
ctx.
|
|
895
|
+
const systemPrompt = ctx.getSystemPrompt();
|
|
896
|
+
const promptEstimate = estimateInitialPromptForContext(systemPrompt, getPromptCalibration(ctx));
|
|
897
|
+
ctx.ui.notify(formatPromptInjectionLines(systemPrompt, latestSystemPromptOptions, promptEstimate).join("\n"), "info");
|
|
898
|
+
},
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
pi.registerCommand("calibrate", {
|
|
902
|
+
description: "Start an isolated calibration turn to calibrate PI initial prompt token estimates.",
|
|
903
|
+
handler: async (args, ctx) => {
|
|
904
|
+
const mode = args.trim().toLowerCase();
|
|
905
|
+
if (mode === "current" || mode === "here") {
|
|
906
|
+
const result = calibrateFromCurrentBranch(ctx);
|
|
907
|
+
if (!result.ok) {
|
|
908
|
+
ctx.ui.notify(`Calibration failed: ${result.reason}`, "warning");
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
appendInitialPromptCalibrationRecord(pi.appendEntry, result.record);
|
|
913
|
+
const calibration = getPromptCalibration(ctx);
|
|
914
|
+
const estimate = estimateInitialPromptForContext(ctx.getSystemPrompt(), calibration);
|
|
915
|
+
ctx.ui.notify(
|
|
916
|
+
`Calibrated PI estimate: ~${formatTokens(estimate.total)} tok (scale ×${estimate.calibrationMultiplier.toFixed(2)}, ${estimate.calibrationSamples} samples). Run /stats-pi for details.`,
|
|
917
|
+
"info",
|
|
918
|
+
);
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
if (!ctx.isIdle()) {
|
|
923
|
+
ctx.ui.notify("Calibration needs an idle agent so it can start a clean probe turn.", "warning");
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
ctx.ui.notify("Starting isolated calibration session…", "info");
|
|
928
|
+
await ctx.newSession({
|
|
929
|
+
withSession: async (newCtx) => {
|
|
930
|
+
await newCtx.sendUserMessage(CALIBRATION_PROMPT);
|
|
931
|
+
},
|
|
932
|
+
});
|
|
713
933
|
},
|
|
714
934
|
});
|
|
715
935
|
|
|
@@ -726,9 +946,9 @@ export default function statsExtension(pi: ExtensionAPI) {
|
|
|
726
946
|
if (!data) return;
|
|
727
947
|
|
|
728
948
|
const systemPrompt = ctx.getSystemPrompt();
|
|
729
|
-
const
|
|
949
|
+
const promptEstimate = estimateInitialPromptForContext(systemPrompt, data.calibration);
|
|
730
950
|
const graphLines = buildGraphLines(data.byDay, data.dayKeys, true);
|
|
731
|
-
const promptInjectionLines = formatPromptInjectionLines(systemPrompt, latestSystemPromptOptions);
|
|
951
|
+
const promptInjectionLines = formatPromptInjectionLines(systemPrompt, latestSystemPromptOptions, promptEstimate);
|
|
732
952
|
const modelLines = formatModelComparisonLines(data.records, data.dayKeys, data.totals).slice(0, 7);
|
|
733
953
|
const sessionLines = formatExpensiveSessionLines(data.records, data.dayKeys).slice(0, 7);
|
|
734
954
|
const commandLines = [
|
|
@@ -737,7 +957,7 @@ export default function statsExtension(pi: ExtensionAPI) {
|
|
|
737
957
|
];
|
|
738
958
|
|
|
739
959
|
ctx.ui.notify(
|
|
740
|
-
`📊 Token stats (${data.scopeLabel}, ${data.files.length} sessions) · PI:
|
|
960
|
+
`📊 Token stats (${data.scopeLabel}, ${data.files.length} sessions) · PI: ~${formatTokens(promptEstimate.total)} tok\n\n${graphLines.join("\n")}\n\n${promptInjectionLines.join("\n")}\n\n${buildCostTrendLines(data.byDay, data.dayKeys).join("\n")}\n${buildCacheEfficiencyLines(data.totals).join("\n")}\n\n${modelLines.join("\n")}\n\n${sessionLines.join("\n")}\n\n${commandLines.join("\n")}`,
|
|
741
961
|
"info",
|
|
742
962
|
);
|
|
743
963
|
},
|