pi-error-advisor 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 ADDED
@@ -0,0 +1,101 @@
1
+ # pi-error-advisor
2
+
3
+ Makes provider errors visible to the LLM in [Pi](https://pi.dev) — without writing anything to the session.
4
+
5
+ Pi's default behavior keeps errors UI-only: assistant error messages carry the error in the `errorMessage` field with empty content (which provider serializers drop entirely), and overflow recovery removes the error message from context before retrying. The model therefore never learns that a request failed, why compaction ran, or that retries were exhausted — it either regenerates blind or gets a dead session.
6
+
7
+ ## What It Does
8
+
9
+ pi-error-advisor synthesizes a short, clearly-labeled note into the outgoing request context whenever the previous turn failed:
10
+
11
+ 1. **Retry exhaustion** — after Pi's automatic retries give up on a transient provider error (rate limit, overloaded, network), the model is told the failure was infrastructure: *retry the same approach, don't change plans*, and let the user know the provider was unavailable.
12
+
13
+ 2. **Non-retryable provider errors** — 400/401/422-class failures surface the error text so the model can inform the user (e.g. expired auth → suggest `/login`) or recognize that something in the conversation history is being rejected.
14
+
15
+ 3. **Overflow recovery** — when the conversation exceeds the context window and Pi compacts it, the model is told to keep responses concise. If compaction *didn't complete* (failure, auth problem, nothing to compact), the model is told to produce the shortest useful response and suggest `/compact` or a larger-context model.
16
+
17
+ 4. **Silence where it belongs** — user aborts (Ctrl+C during compaction or retry backoff), manual and threshold compaction, and anything after a successful assistant turn produce no notes at all.
18
+
19
+ ## Why Request-Time, Not Persisted
20
+
21
+ A previous iteration of this idea injected persisted advisory messages into the session. Upstream feedback (rightly) flagged that as detrimental: transient infrastructure noise becomes permanent conversation history, flows into compaction summaries and forks, and steers the model with fabricated context.
22
+
23
+ This extension takes the opposite approach — **everything happens in the `context` hook at request time**:
24
+
25
+ - Sessions, exports, compaction summaries, forks, and resumes are byte-identical with or without the extension.
26
+ - Notes are self-limiting: they stop as soon as a successful assistant turn lands.
27
+ - Notes are appended at the end of the message list, keeping the prompt-cache prefix stable.
28
+ - Uninstalling the extension leaves zero trace in any session.
29
+
30
+ ## Installation
31
+
32
+ ```
33
+ # Via GitHub URL
34
+ pi install https://github.com/yeshao/pi-error-advisor
35
+
36
+ # Or run ad hoc
37
+ pi --extension path/to/pi-error-advisor/src/index.ts
38
+ ```
39
+
40
+ ## How It Works
41
+
42
+ ### Trailing-error scan (retry exhaustion, non-retryable errors)
43
+
44
+ On every request, the `context` hook scans backwards for the last assistant message with `stopReason: "error"` that has no completed assistant message after it. The error is classified with pi-ai's own `isContextOverflow` / `isRetryableAssistantError`, and one matching note is appended:
45
+
46
+ ```
47
+ [error-advisor] The previous request failed with a transient provider error after
48
+ automatic retries were exhausted: "overloaded_error". This was an infrastructure
49
+ problem, not a problem with your approach — do not change your plan because of it. …
50
+ ```
51
+
52
+ The scan stops at successful or aborted assistant messages and at compaction/branch summaries, so stale errors never resurface.
53
+
54
+ ### Overflow compaction tracking
55
+
56
+ The overflow-recovery path removes the error message from context before retrying, so the scan can't see it. Instead the extension pairs `session_before_compact(reason: "overflow")` with `session_compact`:
57
+
58
+ - Both fired → compaction succeeded → one-shot "conversation was compacted, keep responses concise" note on the retry request.
59
+ - Only the first fired → by the time the next request is built, the compaction must have failed → one-shot "compaction did not complete" note plus a UI warning.
60
+ - The event's abort signal clears the pending flag, so user-cancelled compactions stay silent.
61
+
62
+ (`agent_end` can't be used for failure detection — it fires *before* post-run compaction.)
63
+
64
+ ## Project Structure
65
+
66
+ ```
67
+ pi-error-advisor/
68
+ ├── package.json # Pi extension manifest
69
+ ├── tsconfig.json
70
+ ├── vitest.config.ts
71
+ ├── README.md
72
+ ├── src/
73
+ │ └── index.ts # Extension entry
74
+ ├── tests/
75
+ │ └── error-advisor.test.ts
76
+ └── types/
77
+ └── pi-coding-agent.ts # Type stubs for standalone typecheck/tests
78
+ ```
79
+
80
+ ## Development
81
+
82
+ ```
83
+ # Install dependencies
84
+ npm install
85
+
86
+ # Type check
87
+ npm run typecheck
88
+
89
+ # Run tests
90
+ npm test
91
+ ```
92
+
93
+ Tests exercise the real pi-ai error classification (`@earendil-works/pi-ai` is a dev dependency); only `@earendil-works/pi-coding-agent` is stubbed.
94
+
95
+ ## Dependencies
96
+
97
+ Runtime: pi-ai's `isContextOverflow` / `isRetryableAssistantError` (provided by Pi as a peer dependency). No other runtime dependencies.
98
+
99
+ ## License
100
+
101
+ MIT
package/km_page1.png ADDED
Binary file
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "pi-error-advisor",
3
+ "version": "0.1.0",
4
+ "description": "Makes provider errors visible to the LLM in Pi — request-time advisory notes for retry exhaustion, non-retryable errors, and context-overflow compaction, persisting nothing to the session",
5
+ "type": "module",
6
+ "main": "./src/index.ts",
7
+ "pi": {
8
+ "extensions": [
9
+ "./src/index.ts"
10
+ ]
11
+ },
12
+ "scripts": {
13
+ "typecheck": "tsc --noEmit",
14
+ "test": "vitest run"
15
+ },
16
+ "peerDependencies": {
17
+ "@earendil-works/pi-agent-core": "*",
18
+ "@earendil-works/pi-ai": "*",
19
+ "@earendil-works/pi-coding-agent": "*"
20
+ },
21
+ "devDependencies": {
22
+ "@earendil-works/pi-agent-core": "^0.80.6",
23
+ "@earendil-works/pi-ai": "^0.80.6",
24
+ "@types/node": "^20.0.0",
25
+ "typescript": "^5.5.0",
26
+ "vitest": "^2.0.0"
27
+ },
28
+ "engines": {
29
+ "node": ">=18.0.0"
30
+ },
31
+ "license": "MIT"
32
+ }
package/src/index.ts ADDED
@@ -0,0 +1,167 @@
1
+ /**
2
+ * pi-error-advisor
3
+ *
4
+ * Makes provider errors visible to the LLM at request time — without writing
5
+ * anything to the session.
6
+ *
7
+ * Pi's default behavior keeps errors UI-only: assistant error messages carry
8
+ * the error in the `errorMessage` field with empty content (which provider
9
+ * serializers drop entirely), and overflow recovery removes the error message
10
+ * from context before retrying. The model therefore never learns that a
11
+ * request failed, why compaction ran, or that retries were exhausted.
12
+ *
13
+ * This extension synthesizes a short, clearly-labeled note into the outgoing
14
+ * request context whenever the previous turn failed:
15
+ *
16
+ * - Transient provider errors (rate limit / overloaded / network) after retry
17
+ * exhaustion: tells the model the failure was infrastructure — retry the
18
+ * same approach, don't change plans.
19
+ * - Non-retryable errors (400/401/422): surfaces the error text so the model
20
+ * can inform the user (e.g. expired auth) or adjust (e.g. rejected content).
21
+ * - Overflow recovery: notes that the conversation was compacted (or that
22
+ * compaction did not complete) so the model keeps responses concise.
23
+ *
24
+ * Nothing is persisted. The note is added in the `context` hook, which only
25
+ * affects the outgoing provider request: sessions, exports, compaction
26
+ * summaries, forks, and resumes are byte-identical with or without this
27
+ * extension. Notes are self-limiting — they stop as soon as a successful
28
+ * assistant turn lands.
29
+ */
30
+
31
+ import type { AgentMessage } from "@earendil-works/pi-agent-core";
32
+ import type { AssistantMessage } from "@earendil-works/pi-ai";
33
+ import { isContextOverflow, isRetryableAssistantError } from "@earendil-works/pi-ai/compat";
34
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
35
+
36
+ const NOTE_PREFIX = "[error-advisor]";
37
+ const MAX_ERROR_TEXT = 300;
38
+ const ADVISOR_CUSTOM_TYPE = "error-advisor";
39
+
40
+ function makeNote(text: string): AgentMessage {
41
+ return {
42
+ role: "custom",
43
+ customType: ADVISOR_CUSTOM_TYPE,
44
+ content: [{ type: "text", text: `${NOTE_PREFIX} ${text}` }],
45
+ display: false,
46
+ timestamp: Date.now(),
47
+ };
48
+ }
49
+
50
+ function truncate(text: string): string {
51
+ return text.length > MAX_ERROR_TEXT ? `${text.slice(0, MAX_ERROR_TEXT)}…` : text;
52
+ }
53
+
54
+ /**
55
+ * Find the trailing assistant error message: the last assistant message with
56
+ * stopReason "error" that has no completed assistant message after it.
57
+ *
58
+ * Stops scanning at: a successful/aborted assistant message (advice would be
59
+ * stale or unwanted — user aborts are not the model's business), or a
60
+ * compaction/branch summary (errors before a summary are ancient history).
61
+ */
62
+ function findTrailingError(messages: AgentMessage[]): AssistantMessage | undefined {
63
+ for (let i = messages.length - 1; i >= 0; i--) {
64
+ const msg = messages[i];
65
+ if (msg.role === "compactionSummary" || msg.role === "branchSummary") {
66
+ return undefined;
67
+ }
68
+ if (msg.role !== "assistant") {
69
+ continue;
70
+ }
71
+ const assistant = msg as AssistantMessage;
72
+ return assistant.stopReason === "error" ? assistant : undefined;
73
+ }
74
+ return undefined;
75
+ }
76
+
77
+ function classifyError(msg: AssistantMessage): string {
78
+ const errorText = truncate(msg.errorMessage ?? "unknown error");
79
+ if (isContextOverflow(msg, 0)) {
80
+ return (
81
+ "The previous request exceeded the model's context window and could not be completed. " +
82
+ "Keep your responses concise. If this persists, suggest the user run /compact or switch to a larger-context model."
83
+ );
84
+ }
85
+ if (isRetryableAssistantError(msg)) {
86
+ return (
87
+ `The previous request failed with a transient provider error after automatic retries were exhausted: "${errorText}". ` +
88
+ "This was an infrastructure problem, not a problem with your approach — do not change your plan because of it. " +
89
+ "Briefly let the user know the provider was unavailable and that retrying later should work."
90
+ );
91
+ }
92
+ return (
93
+ `The previous request failed with a non-retryable provider error: "${errorText}". ` +
94
+ "If it indicates an authentication problem, tell the user to check their credentials (e.g. /login). " +
95
+ "If it indicates a malformed request, part of the conversation history may contain content the provider rejects."
96
+ );
97
+ }
98
+
99
+ export default function errorAdvisor(pi: ExtensionAPI) {
100
+ // One-shot flags set by compaction events, consumed by the next context
101
+ // transform. These cover the overflow-recovery path, where the error
102
+ // message is removed from context before the retry — a stateless scan of
103
+ // the context can't see it, but the compaction events can.
104
+ //
105
+ // Failure inference: session_compact fires only on success, and there is
106
+ // no failure event. Event order matters — agent_end fires BEFORE the
107
+ // post-run compaction, so it cannot be used to detect failure. Instead:
108
+ // by the time the next `context` transform runs, an overflow compaction
109
+ // has either succeeded (session_compact cleared the pending flag before
110
+ // the retry request) or failed (the run died and this request belongs to
111
+ // the next prompt). A user abort clears the flag via the event's signal,
112
+ // so cancellations stay silent (user aborts are not the model's business).
113
+ let overflowCompacted = false;
114
+ let overflowCompactionPending = false;
115
+
116
+ pi.on("session_before_compact", async (event) => {
117
+ if (event.reason === "overflow") {
118
+ overflowCompactionPending = true;
119
+ event.signal.addEventListener("abort", () => {
120
+ overflowCompactionPending = false;
121
+ });
122
+ }
123
+ return undefined;
124
+ });
125
+
126
+ pi.on("session_compact", async (event) => {
127
+ if (event.reason === "overflow") {
128
+ overflowCompactionPending = false;
129
+ overflowCompacted = true;
130
+ }
131
+ });
132
+
133
+ pi.on("context", async (event, ctx) => {
134
+ const notes: string[] = [];
135
+
136
+ if (overflowCompacted) {
137
+ overflowCompacted = false;
138
+ notes.push(
139
+ "The conversation exceeded the model's context window and was automatically compacted. " +
140
+ "Keep your responses concise to avoid another overflow.",
141
+ );
142
+ }
143
+ if (overflowCompactionPending) {
144
+ overflowCompactionPending = false;
145
+ ctx.ui.notify("Overflow compaction did not complete; the request may fail again", "warning");
146
+ notes.push(
147
+ "The conversation exceeded the model's context window and automatic compaction did not complete. " +
148
+ "Produce the shortest useful response, and suggest the user run /compact or switch to a larger-context model.",
149
+ );
150
+ }
151
+
152
+ // Trailing provider error scan. Covers non-retryable errors (400/401)
153
+ // and retry exhaustion (the final failed attempt stays in context).
154
+ // Self-limiting: once a successful assistant message lands, the scan
155
+ // stops matching, so a note is only added while the failure is fresh.
156
+ const trailingError = findTrailingError(event.messages);
157
+ if (trailingError) {
158
+ notes.push(classifyError(trailingError));
159
+ }
160
+
161
+ if (notes.length === 0) {
162
+ return undefined;
163
+ }
164
+ // Append at the end: keeps the message prefix stable for prompt caching.
165
+ return { messages: [...event.messages, ...notes.map(makeNote)] };
166
+ });
167
+ }
package/sso_debug.png ADDED
Binary file
package/sso_html.txt ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "html": "",
3
+ "lifecycle": {
4
+ "effectiveLaunch": {
5
+ "browserLaunched": true,
6
+ "engine": "chrome",
7
+ "launchHash": 6847324482101410000
8
+ },
9
+ "launched": false,
10
+ "relaunchedBrowser": false,
11
+ "restartedBackground": false,
12
+ "restoreStatus": "not_configured",
13
+ "reused": true,
14
+ "saveStatus": "not_attempted"
15
+ }
16
+ }
package/sso_login.png ADDED
Binary file
Binary file
Binary file
Binary file
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "allowImportingTsExtensions": true,
7
+ "noEmit": true,
8
+ "strict": true,
9
+ "skipLibCheck": true,
10
+ "paths": {
11
+ "@earendil-works/pi-coding-agent": ["./types/pi-coding-agent"]
12
+ }
13
+ },
14
+ "include": ["src", "types", "tests"]
15
+ }
@@ -0,0 +1,104 @@
1
+ // Minimal type stubs for @earendil-works/pi-coding-agent used by pi-error-advisor.
2
+ // Only the members actually referenced are declared. At runtime under Pi, the
3
+ // real package provides these; the stub exists so `tsc --noEmit` and vitest can
4
+ // run standalone without the full coding-agent as a dev dependency.
5
+
6
+ import type { ImageContent, TextContent } from "@earendil-works/pi-ai";
7
+
8
+ /** Mirrors coding-agent's BashExecutionMessage. */
9
+ export interface BashExecutionMessage {
10
+ role: "bashExecution";
11
+ command: string;
12
+ output: string;
13
+ exitCode: number | undefined;
14
+ cancelled: boolean;
15
+ truncated: boolean;
16
+ fullOutputPath?: string;
17
+ timestamp: number;
18
+ excludeFromContext?: boolean;
19
+ }
20
+
21
+ /** Mirrors coding-agent's CustomMessage. */
22
+ export interface CustomMessage<T = unknown> {
23
+ role: "custom";
24
+ customType: string;
25
+ content: string | (TextContent | ImageContent)[];
26
+ display: boolean;
27
+ details?: T;
28
+ timestamp: number;
29
+ }
30
+
31
+ /** Mirrors coding-agent's BranchSummaryMessage. */
32
+ export interface BranchSummaryMessage {
33
+ role: "branchSummary";
34
+ summary: string;
35
+ fromId: string;
36
+ timestamp: number;
37
+ }
38
+
39
+ /** Mirrors coding-agent's CompactionSummaryMessage. */
40
+ export interface CompactionSummaryMessage {
41
+ role: "compactionSummary";
42
+ summary: string;
43
+ tokensBefore: number;
44
+ timestamp: number;
45
+ }
46
+
47
+ // Replicate coding-agent's declaration merging so AgentMessage includes the
48
+ // custom roles — identical to what the real package declares.
49
+ declare module "@earendil-works/pi-agent-core" {
50
+ interface CustomAgentMessages {
51
+ bashExecution: BashExecutionMessage;
52
+ custom: CustomMessage;
53
+ branchSummary: BranchSummaryMessage;
54
+ compactionSummary: CompactionSummaryMessage;
55
+ }
56
+ }
57
+
58
+ import type { AgentMessage } from "@earendil-works/pi-agent-core";
59
+
60
+ export interface ContextEvent {
61
+ type: "context";
62
+ messages: AgentMessage[];
63
+ }
64
+
65
+ export interface ContextEventResult {
66
+ messages?: AgentMessage[];
67
+ }
68
+
69
+ export interface SessionBeforeCompactEvent {
70
+ type: "session_before_compact";
71
+ reason: "manual" | "threshold" | "overflow";
72
+ willRetry: boolean;
73
+ signal: AbortSignal;
74
+ }
75
+
76
+ export interface SessionCompactEvent {
77
+ type: "session_compact";
78
+ reason: "manual" | "threshold" | "overflow";
79
+ willRetry: boolean;
80
+ }
81
+
82
+ export interface ExtensionUIContext {
83
+ notify(message: string, level?: "info" | "warning" | "error"): void;
84
+ }
85
+
86
+ export interface ExtensionContext {
87
+ ui: ExtensionUIContext;
88
+ cwd: string;
89
+ }
90
+
91
+ export interface ExtensionAPI {
92
+ on(
93
+ event: "context",
94
+ handler: (event: ContextEvent, ctx: ExtensionContext) => Promise<ContextEventResult | undefined>,
95
+ ): void;
96
+ on(
97
+ event: "session_before_compact",
98
+ handler: (event: SessionBeforeCompactEvent, ctx: ExtensionContext) => Promise<unknown>,
99
+ ): void;
100
+ on(
101
+ event: "session_compact",
102
+ handler: (event: SessionCompactEvent, ctx: ExtensionContext) => Promise<unknown>,
103
+ ): void;
104
+ }
@@ -0,0 +1,13 @@
1
+ import { resolve } from "node:path";
2
+ import { defineConfig } from "vitest/config";
3
+
4
+ export default defineConfig({
5
+ resolve: {
6
+ alias: {
7
+ "@earendil-works/pi-coding-agent": resolve(__dirname, "types/pi-coding-agent.ts"),
8
+ },
9
+ },
10
+ test: {
11
+ globals: true,
12
+ },
13
+ });