pi-bar 0.3.43 → 0.3.45
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 +103 -20
- package/extensions/status-footer.ts +549 -251
- package/extensions/usage.ts +108 -0
- package/package.json +1 -1
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { Usage } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
|
|
4
|
+
/** Numeric-only entries; never add progress prompts or responses to session context. */
|
|
5
|
+
export const PROGRESS_USAGE_ENTRY_TYPE = "pi-bar-progress-usage";
|
|
6
|
+
|
|
7
|
+
export type UsageTotals = {
|
|
8
|
+
/** All prompt tokens, including cache reads and writes. */
|
|
9
|
+
input: number;
|
|
10
|
+
/** Includes reasoning tokens when reported as part of output. */
|
|
11
|
+
output: number;
|
|
12
|
+
/** Reported/catalog dollar estimate, not an invoice. */
|
|
13
|
+
cost: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function nonNegative(value: unknown): number {
|
|
17
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function totalsFromUsage(usage: Partial<Usage> | undefined): UsageTotals {
|
|
21
|
+
return {
|
|
22
|
+
input: nonNegative(usage?.input) + nonNegative(usage?.cacheRead) + nonNegative(usage?.cacheWrite),
|
|
23
|
+
output: nonNegative(usage?.output),
|
|
24
|
+
cost: nonNegative(usage?.cost?.total),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function hasUsage(totals: UsageTotals): boolean {
|
|
29
|
+
return totals.input > 0 || totals.output > 0 || totals.cost > 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function promptCacheHitRate(usage: Partial<Usage> | undefined): number | null {
|
|
33
|
+
const promptTokens = totalsFromUsage(usage).input;
|
|
34
|
+
return promptTokens > 0 && Number.isFinite(promptTokens)
|
|
35
|
+
? (nonNegative(usage?.cacheRead) / promptTokens) * 100
|
|
36
|
+
: null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function totalsFromEntry(entry: SessionEntry): UsageTotals {
|
|
40
|
+
if (entry.type === "message" && (entry.message.role === "assistant" || entry.message.role === "toolResult")) {
|
|
41
|
+
return totalsFromUsage(entry.message.usage);
|
|
42
|
+
}
|
|
43
|
+
if (entry.type === "compaction" || entry.type === "branch_summary") {
|
|
44
|
+
return totalsFromUsage(entry.usage);
|
|
45
|
+
}
|
|
46
|
+
if (entry.type === "custom" && entry.customType === PROGRESS_USAGE_ENTRY_TYPE) {
|
|
47
|
+
const data = entry.data as Partial<UsageTotals> | undefined;
|
|
48
|
+
if (data && [data.input, data.output, data.cost].every(
|
|
49
|
+
(value) => typeof value === "number" && Number.isFinite(value) && value >= 0,
|
|
50
|
+
)) {
|
|
51
|
+
return { input: data.input!, output: data.output!, cost: data.cost! };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return { input: 0, output: 0, cost: 0 };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Totals cover the entire session file, including abandoned branches. Cache hit
|
|
59
|
+
* rate follows the active branch instead. Consume persisted entries after Pi's
|
|
60
|
+
* message_end hooks finish, so later extensions' usage corrections are honored.
|
|
61
|
+
* Rendering only reads a snapshot; it never walks session history.
|
|
62
|
+
*/
|
|
63
|
+
export class FooterUsageTracker {
|
|
64
|
+
private totals: UsageTotals = { input: 0, output: 0, cost: 0 };
|
|
65
|
+
private cacheHitRate: number | null = null;
|
|
66
|
+
private cursor = 0;
|
|
67
|
+
private lastEntryId: string | undefined;
|
|
68
|
+
|
|
69
|
+
reset(): void {
|
|
70
|
+
this.totals = { input: 0, output: 0, cost: 0 };
|
|
71
|
+
this.cacheHitRate = null;
|
|
72
|
+
this.cursor = 0;
|
|
73
|
+
this.lastEntryId = undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
syncEntries(entries: readonly SessionEntry[]): void {
|
|
77
|
+
// Session replacement or a rewritten prefix requires a fresh snapshot.
|
|
78
|
+
if (entries.length < this.cursor || (this.cursor > 0 && entries[this.cursor - 1]?.id !== this.lastEntryId)) {
|
|
79
|
+
this.reset();
|
|
80
|
+
}
|
|
81
|
+
for (; this.cursor < entries.length; this.cursor++) {
|
|
82
|
+
const entry = entries[this.cursor];
|
|
83
|
+
const totals = totalsFromEntry(entry);
|
|
84
|
+
this.totals.input += totals.input;
|
|
85
|
+
this.totals.output += totals.output;
|
|
86
|
+
this.totals.cost += totals.cost;
|
|
87
|
+
if (entry.type === "message" && entry.message.role === "assistant") {
|
|
88
|
+
this.cacheHitRate = promptCacheHitRate(entry.message.usage);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
this.lastEntryId = entries.at(-1)?.id;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
restoreBranch(branch: readonly SessionEntry[]): void {
|
|
95
|
+
this.cacheHitRate = null;
|
|
96
|
+
for (let index = branch.length - 1; index >= 0; index--) {
|
|
97
|
+
const entry = branch[index];
|
|
98
|
+
if (entry.type === "message" && entry.message.role === "assistant") {
|
|
99
|
+
this.cacheHitRate = promptCacheHitRate(entry.message.usage);
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
snapshot(): UsageTotals & { cacheHitRate: number | null } {
|
|
106
|
+
return { ...this.totals, cacheHitRate: this.cacheHitRate };
|
|
107
|
+
}
|
|
108
|
+
}
|
package/package.json
CHANGED