pi-extension-too-dumb 0.1.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/README.md +27 -0
- package/extensions/index.ts +255 -0
- package/package.json +26 -0
package/README.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# pi-extension-too-dumb
|
|
2
|
+
|
|
3
|
+
A [pi coding agent](https://pi.dev) extension that monitors session health and warns when the model's reasoning ability is likely to be compromised.
|
|
4
|
+
|
|
5
|
+
Instead of displaying raw token metrics for the user to interpret, this extension stays completely silent during normal operation. When a meaningful threshold is crossed, a single warning widget appears above the editor naming the triggered signal — educating as it warns.
|
|
6
|
+
|
|
7
|
+
The implied action for every warning is always `/compact or /new`.
|
|
8
|
+
|
|
9
|
+
## Signals
|
|
10
|
+
|
|
11
|
+
| Signal | Orange | Red |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| **Context window fill** | > 70% | > 90% |
|
|
14
|
+
| **Context fill rate** | On pace to hit 70% in ~4 turns | — |
|
|
15
|
+
| **Cache efficiency** | Reuse < 30% | Reuse < 10% |
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pi install npm:pi-extension-too-dumb
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or project-local:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pi install -l npm:pi-extension-too-dumb
|
|
27
|
+
```
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* too-dumb — session health monitor
|
|
3
|
+
*
|
|
4
|
+
* Watches for signals that suggest the model's reasoning ability may be
|
|
5
|
+
* compromised and surfaces a single warning widget above the editor.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
10
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
11
|
+
|
|
12
|
+
// ── ANSI color constants ─────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
const ORANGE_FG = "\x1b[38;2;255;140;0m"; // #ff8c00
|
|
15
|
+
const ORANGE_BG = "\x1b[48;2;26;18;0m"; // #1a1200
|
|
16
|
+
const RED_FG = "\x1b[38;2;204;34;0m"; // #cc2200
|
|
17
|
+
const RED_BG = "\x1b[48;2;26;4;0m"; // #1a0400
|
|
18
|
+
const RESET = "\x1b[0m";
|
|
19
|
+
|
|
20
|
+
// ── Types ────────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
type Severity = "orange" | "red";
|
|
23
|
+
|
|
24
|
+
interface Warning {
|
|
25
|
+
severity: Severity;
|
|
26
|
+
message: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ── Widget rendering ─────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
function buildLines(warning: Warning, width: number): string[] {
|
|
32
|
+
const fg = warning.severity === "red" ? RED_FG : ORANGE_FG;
|
|
33
|
+
const bg = warning.severity === "red" ? RED_BG : ORANGE_BG;
|
|
34
|
+
const icon = warning.severity === "red" ? "⛔" : "⚠";
|
|
35
|
+
|
|
36
|
+
const renderLine = (text: string): string => {
|
|
37
|
+
const truncated = truncateToWidth(text, width);
|
|
38
|
+
// Pad to full width so the background tint spans the whole line.
|
|
39
|
+
const pad = " ".repeat(Math.max(0, width - visibleWidth(truncated)));
|
|
40
|
+
return `${bg}${fg}${truncated}${pad}${RESET}`;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return [
|
|
44
|
+
renderLine(`▏ ${icon} ${warning.message}`),
|
|
45
|
+
renderLine(`▏ /compact to summarize or /new to start fresh`),
|
|
46
|
+
];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── Signal 1 — Context Window Fill ──────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
function computeSignal1(contextPercent: number | null): Warning | null {
|
|
52
|
+
if (contextPercent === null) return null;
|
|
53
|
+
|
|
54
|
+
if (contextPercent > 90) {
|
|
55
|
+
return {
|
|
56
|
+
severity: "red",
|
|
57
|
+
message: `Context window at ${Math.round(contextPercent)}% — reasoning is severely impacted`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
if (contextPercent > 70) {
|
|
61
|
+
return {
|
|
62
|
+
severity: "orange",
|
|
63
|
+
message: `Context window at ${Math.round(contextPercent)}% — model may be losing early context`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── Signal 2 — Context Fill Rate ────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
function computeSignal2(
|
|
72
|
+
branch: any[],
|
|
73
|
+
contextWindow: number,
|
|
74
|
+
contextPercent: number | null,
|
|
75
|
+
): Warning | null {
|
|
76
|
+
// Signal 1 already covers anything >= 70%.
|
|
77
|
+
if (contextPercent !== null && contextPercent >= 70) return null;
|
|
78
|
+
if (contextWindow <= 0) return null;
|
|
79
|
+
|
|
80
|
+
// Collect assistant messages (valid usage only), preserving branch index.
|
|
81
|
+
const points: { branchIdx: number; inputPercent: number }[] = [];
|
|
82
|
+
|
|
83
|
+
for (let i = 0; i < branch.length; i++) {
|
|
84
|
+
const e = branch[i];
|
|
85
|
+
if (
|
|
86
|
+
e.type === "message" &&
|
|
87
|
+
e.message.role === "assistant"
|
|
88
|
+
) {
|
|
89
|
+
const msg = e.message as AssistantMessage;
|
|
90
|
+
if (
|
|
91
|
+
msg.usage &&
|
|
92
|
+
msg.stopReason !== "aborted" &&
|
|
93
|
+
msg.stopReason !== "error"
|
|
94
|
+
) {
|
|
95
|
+
points.push({
|
|
96
|
+
branchIdx: i,
|
|
97
|
+
inputPercent: ((msg.usage.input + msg.usage.cacheRead) / contextWindow) * 100,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (points.length < 4) return null;
|
|
104
|
+
|
|
105
|
+
const last4 = points.slice(-4);
|
|
106
|
+
const oldestBranchIdx = last4[0]!.branchIdx;
|
|
107
|
+
|
|
108
|
+
// Post-compaction guard: skip if a compaction entry exists after the oldest
|
|
109
|
+
// of the last 4 assistant messages. This means fewer than 4 clean turns of
|
|
110
|
+
// rate data exist post-compaction.
|
|
111
|
+
for (let i = oldestBranchIdx + 1; i < branch.length; i++) {
|
|
112
|
+
if (branch[i].type === "compaction") return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Average percentage-point gain per turn over the last 4 data points.
|
|
116
|
+
// (last4[3] - last4[0]) / 3 intervals
|
|
117
|
+
const rate = (last4[3]!.inputPercent - last4[0]!.inputPercent) / 3;
|
|
118
|
+
if (rate <= 0) return null; // Not growing — no forward risk.
|
|
119
|
+
|
|
120
|
+
const current = last4[3]!.inputPercent;
|
|
121
|
+
const projected = current + rate * 4;
|
|
122
|
+
|
|
123
|
+
if (projected >= 70 && current < 70) {
|
|
124
|
+
const turnsUntil = Math.max(1, Math.ceil((70 - current) / rate));
|
|
125
|
+
return {
|
|
126
|
+
severity: "orange",
|
|
127
|
+
message: `Filling at ~${Math.round(rate)}%/turn — context will likely degrade in ~${turnsUntil} turns`,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── Signal 3 — Cache Efficiency ──────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
function computeSignal3(branch: any[]): Warning | null {
|
|
137
|
+
let totalCacheRead = 0;
|
|
138
|
+
let totalInput = 0;
|
|
139
|
+
let assistantCount = 0;
|
|
140
|
+
|
|
141
|
+
for (const e of branch) {
|
|
142
|
+
if (
|
|
143
|
+
e.type === "message" &&
|
|
144
|
+
e.message.role === "assistant"
|
|
145
|
+
) {
|
|
146
|
+
const msg = e.message as AssistantMessage;
|
|
147
|
+
if (
|
|
148
|
+
msg.usage &&
|
|
149
|
+
msg.stopReason !== "aborted" &&
|
|
150
|
+
msg.stopReason !== "error"
|
|
151
|
+
) {
|
|
152
|
+
totalCacheRead += msg.usage.cacheRead;
|
|
153
|
+
totalInput += msg.usage.input;
|
|
154
|
+
assistantCount++;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Gates
|
|
160
|
+
if (totalCacheRead <= 10_000) return null;
|
|
161
|
+
if (assistantCount < 5) return null;
|
|
162
|
+
|
|
163
|
+
const ratio = totalCacheRead / (totalCacheRead + totalInput);
|
|
164
|
+
const pct = Math.round(ratio * 100);
|
|
165
|
+
|
|
166
|
+
if (ratio < 0.10) {
|
|
167
|
+
return {
|
|
168
|
+
severity: "red",
|
|
169
|
+
message: `Very low cache reuse (${pct}%) — context flooding likely`,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
if (ratio < 0.30) {
|
|
173
|
+
return {
|
|
174
|
+
severity: "orange",
|
|
175
|
+
message: `Low cache reuse (${pct}%) — tool outputs may be flooding context`,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ── Priority selection ───────────────────────────────────────────────────────
|
|
183
|
+
//
|
|
184
|
+
// Priority order (highest first):
|
|
185
|
+
// 1. 🔴 Signal 1: context% > 90
|
|
186
|
+
// 2. 🔴 Signal 3: cache ratio < 0.10
|
|
187
|
+
// 3. 🟠 Signal 1: context% > 70
|
|
188
|
+
// 4. 🟠 Signal 2: fill rate projection
|
|
189
|
+
// 5. 🟠 Signal 3: cache ratio < 0.30
|
|
190
|
+
|
|
191
|
+
function computeWarning(
|
|
192
|
+
contextPercent: number | null,
|
|
193
|
+
branch: any[],
|
|
194
|
+
contextWindow: number,
|
|
195
|
+
): Warning | null {
|
|
196
|
+
const s1 = computeSignal1(contextPercent);
|
|
197
|
+
if (s1?.severity === "red") return s1;
|
|
198
|
+
|
|
199
|
+
const s3 = computeSignal3(branch);
|
|
200
|
+
if (s3?.severity === "red") return s3;
|
|
201
|
+
|
|
202
|
+
if (s1?.severity === "orange") return s1;
|
|
203
|
+
|
|
204
|
+
const s2 = computeSignal2(branch, contextWindow, contextPercent);
|
|
205
|
+
if (s2) return s2;
|
|
206
|
+
|
|
207
|
+
if (s3?.severity === "orange") return s3;
|
|
208
|
+
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ── Extension entry point ────────────────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
export default function (pi: ExtensionAPI) {
|
|
215
|
+
// Track the last rendered warning to avoid redundant setWidget calls.
|
|
216
|
+
let lastKey: string | null | undefined ; // undefined = never computed
|
|
217
|
+
|
|
218
|
+
function warningKey(w: Warning | null): string | null {
|
|
219
|
+
return w ? `${w.severity}:${w.message}` : null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
223
|
+
lastKey = undefined;
|
|
224
|
+
ctx.ui.setWidget("too-dumb", undefined);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
pi.on("message_end", async (_event, ctx) => {
|
|
228
|
+
const contextUsage = ctx.getContextUsage();
|
|
229
|
+
const contextPercent = contextUsage?.percent ?? null;
|
|
230
|
+
const contextWindow =
|
|
231
|
+
contextUsage?.contextWindow ??
|
|
232
|
+
ctx.model?.contextWindow ??
|
|
233
|
+
128_000;
|
|
234
|
+
|
|
235
|
+
const branch = ctx.sessionManager.getBranch();
|
|
236
|
+
const warning = computeWarning(contextPercent, branch, contextWindow);
|
|
237
|
+
const key = warningKey(warning);
|
|
238
|
+
|
|
239
|
+
if (key === lastKey) return; // No change — skip re-render.
|
|
240
|
+
lastKey = key;
|
|
241
|
+
|
|
242
|
+
if (warning) {
|
|
243
|
+
// Capture `warning` value for the render closure.
|
|
244
|
+
const w = warning;
|
|
245
|
+
ctx.ui.setWidget("too-dumb", (_tui, _theme) => ({
|
|
246
|
+
render(width: number): string[] {
|
|
247
|
+
return buildLines(w, width);
|
|
248
|
+
},
|
|
249
|
+
invalidate() {},
|
|
250
|
+
}));
|
|
251
|
+
} else {
|
|
252
|
+
ctx.ui.setWidget("too-dumb", undefined);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-extension-too-dumb",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A pi extension that warns when session reasoning ability may be compromised",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package"
|
|
7
|
+
],
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/justinclayton/pi-extension-too-dumb.git"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"extensions",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
19
|
+
"@earendil-works/pi-tui": "*"
|
|
20
|
+
},
|
|
21
|
+
"pi": {
|
|
22
|
+
"extensions": [
|
|
23
|
+
"./extensions"
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
}
|