pi-diagnostics 0.0.0 → 0.3.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/src/native.ts ADDED
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Mirror of Pi's native cache-miss notice (`showCacheMissNotices`).
3
+ *
4
+ * Pi renders its notice straight into the interactive transcript: it is not a session entry and
5
+ * extensions cannot extend it. To complement rather than duplicate it, this module predicts when
6
+ * Pi shows that notice so provider diagnostics can render as a follow-up line underneath it.
7
+ *
8
+ * Ported from Pi's `core/cache-stats.ts` and `InteractiveMode.addCacheMissNotice` (0.87).
9
+ */
10
+
11
+ import { readFileSync } from "node:fs";
12
+ import { join } from "node:path";
13
+ import { CONFIG_DIR_NAME, getAgentDir } from "@earendil-works/pi-coding-agent";
14
+
15
+ /** Per-turn misses at or below this are cache breakpoint granularity noise. */
16
+ const NOISE_FLOOR_TOKENS = 1024;
17
+ /** Pi hides misses below both of these. */
18
+ const NOTICE_MIN_TOKENS = 20_000;
19
+ const NOTICE_MIN_COST = 0.1;
20
+
21
+ interface UsageLike {
22
+ input: number;
23
+ cacheRead: number;
24
+ cacheWrite: number;
25
+ cost?: { input?: number; cacheRead?: number; cacheWrite?: number };
26
+ }
27
+
28
+ export interface AssistantLike {
29
+ role: string;
30
+ provider?: string;
31
+ model?: string;
32
+ timestamp?: number;
33
+ usage?: UsageLike;
34
+ }
35
+
36
+ interface EntryLike {
37
+ type: string;
38
+ kind?: string;
39
+ provider?: string;
40
+ model?: string;
41
+ timestamp?: string;
42
+ usage?: UsageLike;
43
+ message?: AssistantLike;
44
+ }
45
+
46
+ interface PreviousRequest {
47
+ promptTokens: number;
48
+ modelKey: string;
49
+ timestamp: number;
50
+ reportedCache: boolean;
51
+ }
52
+
53
+ export interface NativeMiss {
54
+ missedTokens: number;
55
+ missedCost: number;
56
+ idleMs: number;
57
+ modelChanged: boolean;
58
+ }
59
+
60
+ /** Cache-read price per million tokens, used when the missed turn read nothing from cache. */
61
+ export type CacheReadPrice = (provider: string, model: string) => number | undefined;
62
+
63
+ function promptTokens(usage: UsageLike): number {
64
+ return usage.input + usage.cacheRead + usage.cacheWrite;
65
+ }
66
+
67
+ function isUsage(value: unknown): value is UsageLike {
68
+ if (typeof value !== "object" || value === null) return false;
69
+ const usage = value as Record<string, unknown>;
70
+ return typeof usage.input === "number" && typeof usage.cacheRead === "number" && typeof usage.cacheWrite === "number";
71
+ }
72
+
73
+ function previousRequest(entries: readonly EntryLike[]): PreviousRequest | undefined {
74
+ let prev: PreviousRequest | undefined;
75
+ for (const entry of entries) {
76
+ if (entry.type === "compaction" || entry.type === "branch_summary") {
77
+ prev = undefined;
78
+ } else if (entry.type === "usage" && entry.kind === "cache_warm" && isUsage(entry.usage)) {
79
+ const tokens = promptTokens(entry.usage);
80
+ if (tokens > 0) {
81
+ prev = {
82
+ promptTokens: tokens,
83
+ modelKey: `${entry.provider}/${entry.model}`,
84
+ timestamp: Date.parse(entry.timestamp ?? ""),
85
+ reportedCache: true,
86
+ };
87
+ }
88
+ } else if (entry.type === "message" && entry.message?.role === "assistant" && isUsage(entry.message.usage)) {
89
+ const usage = entry.message.usage;
90
+ const tokens = promptTokens(usage);
91
+ if (tokens > 0) {
92
+ prev = {
93
+ promptTokens: tokens,
94
+ modelKey: `${entry.message.provider}/${entry.message.model}`,
95
+ timestamp: entry.message.timestamp ?? 0,
96
+ reportedCache: (prev?.reportedCache ?? false) || usage.cacheRead + usage.cacheWrite > 0,
97
+ };
98
+ }
99
+ }
100
+ }
101
+ return prev;
102
+ }
103
+
104
+ /**
105
+ * Pi's miss detection for `message`. `entriesBefore` are all session entries (file order, every
106
+ * branch, like Pi) that precede the message.
107
+ */
108
+ export function detectNativeMiss(
109
+ entriesBefore: readonly EntryLike[],
110
+ message: AssistantLike,
111
+ cacheReadPrice?: CacheReadPrice,
112
+ ): NativeMiss | undefined {
113
+ const usage = message.usage;
114
+ if (!isUsage(usage)) return undefined;
115
+ const prev = previousRequest(entriesBefore);
116
+ const tokens = promptTokens(usage);
117
+ if (!prev || tokens <= 0 || (usage.cacheRead + usage.cacheWrite === 0 && !prev.reportedCache)) return undefined;
118
+ const missedTokens = Math.min(prev.promptTokens, tokens) - usage.cacheRead;
119
+ if (missedTokens <= NOISE_FLOOR_TOKENS) return undefined;
120
+
121
+ const cost = usage.cost ?? {};
122
+ const paidTokens = usage.input + usage.cacheWrite;
123
+ const paidPerToken = paidTokens > 0 ? ((cost.input ?? 0) + (cost.cacheWrite ?? 0)) / paidTokens : 0;
124
+ const readPerToken = usage.cacheRead > 0
125
+ ? (cost.cacheRead ?? 0) / usage.cacheRead
126
+ : (cacheReadPrice?.(message.provider ?? "", message.model ?? "") ?? 0) / 1_000_000;
127
+ return {
128
+ missedTokens,
129
+ missedCost: missedTokens * Math.max(0, paidPerToken - readPerToken),
130
+ idleMs: Math.max(0, (message.timestamp ?? 0) - prev.timestamp),
131
+ modelChanged: `${message.provider}/${message.model}` !== prev.modelKey,
132
+ };
133
+ }
134
+
135
+ /** Whether Pi renders a notice for this miss (when `showCacheMissNotices` is on). */
136
+ export function isNativeNoticeShown(miss: NativeMiss | undefined): boolean {
137
+ return miss !== undefined && (miss.missedTokens >= NOTICE_MIN_TOKENS || miss.missedCost >= NOTICE_MIN_COST);
138
+ }
139
+
140
+ interface SettingsScope {
141
+ cwd?: string;
142
+ projectTrusted?: boolean;
143
+ }
144
+
145
+ function readSetting(path: string): boolean | undefined {
146
+ try {
147
+ const value: unknown = JSON.parse(readFileSync(path, "utf8").replace(/^\uFEFF/, ""));
148
+ if (typeof value !== "object" || value === null) return undefined;
149
+ const show = (value as Record<string, unknown>).showCacheMissNotices;
150
+ return typeof show === "boolean" ? show : undefined;
151
+ } catch {
152
+ return undefined;
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Reads Pi's `showCacheMissNotices` (project overrides global, default off). Extensions cannot
158
+ * reach Pi's SettingsManager, so this reads the files, cached briefly because renderers call it
159
+ * on every frame.
160
+ */
161
+ export function createNativeNoticeSetting(ttlMs = 1_000) {
162
+ let scope: SettingsScope = {};
163
+ let cached: { value: boolean; at: number; key: string } | undefined;
164
+ return {
165
+ setScope(next: SettingsScope): void {
166
+ scope = next;
167
+ cached = undefined;
168
+ },
169
+ enabled(now = Date.now()): boolean {
170
+ const agentDir = getAgentDir();
171
+ const key = `${agentDir}\0${scope.cwd ?? ""}\0${scope.projectTrusted ?? false}`;
172
+ if (cached && cached.key === key && now - cached.at < ttlMs) return cached.value;
173
+ const project = scope.cwd && scope.projectTrusted
174
+ ? readSetting(join(scope.cwd, CONFIG_DIR_NAME, "settings.json"))
175
+ : undefined;
176
+ const value = project ?? readSetting(join(agentDir, "settings.json")) ?? false;
177
+ cached = { value, at: now, key };
178
+ return value;
179
+ },
180
+ };
181
+ }
package/index.js DELETED
@@ -1,2 +0,0 @@
1
- // Placeholder release: no public API yet.
2
- export {};