pi-squire 0.7.0 โ 0.8.1
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/CHANGELOG.md +31 -1
- package/README.md +7 -2
- package/package.json +1 -1
- package/src/child.ts +43 -16
- package/src/limits.ts +4 -2
- package/src/results.ts +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,34 @@ follows [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.8.1] - 2026-10-20
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- A 429 whose text says the endpoint is unavailable, such as OpenCode Go's
|
|
14
|
+
`429: Endpoint is unavailable`, no longer marks the provider as
|
|
15
|
+
usage-limited. It is an outage, not a used-up quota, so `/delegate` keeps
|
|
16
|
+
launching on the provider instead of refusing it for five minutes or more.
|
|
17
|
+
Pi has already retried the request by then. A running delegate that hits
|
|
18
|
+
it moves to its next fallback model and carries on, as it does on a usage
|
|
19
|
+
limit.
|
|
20
|
+
|
|
21
|
+
## [0.8.0] - 2026-10-20
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- A delegate's footer shows that it is a delegate and whether auto-exit is
|
|
26
|
+
on: `๐ฆ delegate ยท auto-exit on`, or
|
|
27
|
+
`๐ฆ delegate ยท auto-exit off`. It follows the
|
|
28
|
+
setting as typing turns it off and `/auto-exit` turns it back on.
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- The parent's footer reads `๐จ N delegates running` instead of
|
|
33
|
+
`โ N running`. Each side picks its icon at random per session: ๐จ or ๐ฉ
|
|
34
|
+
for the parent, ๐ฆ or ๐ง for a delegate. The footer tests in
|
|
35
|
+
`test/delegate.test.ts` change to match.
|
|
36
|
+
|
|
9
37
|
## [0.7.0] - 2026-10-19
|
|
10
38
|
|
|
11
39
|
### Added
|
|
@@ -257,7 +285,9 @@ author's dotfiles; the changes below are against that copy.
|
|
|
257
285
|
|
|
258
286
|
- The published file list includes `child.ts`, which `index.ts` imports.
|
|
259
287
|
|
|
260
|
-
[Unreleased]: https://github.com/gvanderclay/pi-squire/compare/v0.
|
|
288
|
+
[Unreleased]: https://github.com/gvanderclay/pi-squire/compare/v0.8.1...HEAD
|
|
289
|
+
[0.8.1]: https://github.com/gvanderclay/pi-squire/compare/v0.8.0...v0.8.1
|
|
290
|
+
[0.8.0]: https://github.com/gvanderclay/pi-squire/compare/v0.7.0...v0.8.0
|
|
261
291
|
[0.7.0]: https://github.com/gvanderclay/pi-squire/compare/v0.6.0...v0.7.0
|
|
262
292
|
[0.6.0]: https://github.com/gvanderclay/pi-squire/compare/v0.5.1...v0.6.0
|
|
263
293
|
[0.5.1]: https://github.com/gvanderclay/pi-squire/compare/v0.5.0...v0.5.1
|
package/README.md
CHANGED
|
@@ -393,6 +393,10 @@ The parent resolves each delegation's auto-exit from the call
|
|
|
393
393
|
notice: typed or RPC input, or a run the user stopped (Esc).
|
|
394
394
|
- `/auto-exit` inside the delegate turns it on again for the next normal
|
|
395
395
|
completion, whatever the launch said.
|
|
396
|
+
- The footer shows that this is a delegate and whether auto-exit is on:
|
|
397
|
+
`๐ฆ delegate ยท auto-exit on`, or
|
|
398
|
+
`๐ฆ delegate ยท auto-exit off`, with ๐ฆ or ๐ง
|
|
399
|
+
picked at random per session.
|
|
396
400
|
|
|
397
401
|
There is no `subagent_done` tool: auto-exit off only means the window stays.
|
|
398
402
|
|
|
@@ -439,8 +443,9 @@ Delegate session: <session file or id>
|
|
|
439
443
|
current tool calls, before the next model call. A follow-up would wait
|
|
440
444
|
until the parent had answered, by which time it had often read the
|
|
441
445
|
envelope itself, and the late result cost a second, redundant turn.
|
|
442
|
-
- The footer counts the running delegations (
|
|
443
|
-
per result or close and
|
|
446
|
+
- The footer counts the running delegations (`๐จ N delegates running`, with ๐จ or ๐ฉ
|
|
447
|
+
picked at random per session), clears one entry per result or close and
|
|
448
|
+
hides itself at zero.
|
|
444
449
|
- Each result and each close is recorded in the parent session, and the
|
|
445
450
|
records are rebuilt synchronously at session start. A resume still
|
|
446
451
|
recognises replies, including one that arrived while the parent was
|
package/package.json
CHANGED
package/src/child.ts
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
22
22
|
|
|
23
23
|
import { activeMark, classify, type Response, recordLimit } from "./limits.ts";
|
|
24
|
+
import { STATUS_KEY } from "./results.ts";
|
|
24
25
|
|
|
25
26
|
/** The launch variable that carries the parent's resolved setting. */
|
|
26
27
|
export const AUTO_EXIT_ENV = "PI_DELEGATE_AUTO_EXIT";
|
|
@@ -83,39 +84,68 @@ function registerTakeover(pi: ExtensionAPI, parent: string): void {
|
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
/**
|
|
86
|
-
* A usage limit moves the run to the next fallback
|
|
87
|
-
* marked, and continues it in this session; each
|
|
88
|
-
* Returns the setter for
|
|
87
|
+
* A usage limit or an unavailable endpoint moves the run to the next fallback
|
|
88
|
+
* that is usable and not marked, and continues it in this session; each
|
|
89
|
+
* fallback is tried once. Returns the setter for what ended the last run, as
|
|
90
|
+
* `<provider/model> <what happened>`.
|
|
89
91
|
*/
|
|
90
|
-
function registerFallback(pi: ExtensionAPI): (
|
|
92
|
+
function registerFallback(pi: ExtensionAPI): (failure: string | undefined) => void {
|
|
91
93
|
/** The models still to move to on a usage limit, in order. */
|
|
92
94
|
const fallback = (process.env[FALLBACK_ENV] ?? "").split(",").filter((model) => model.includes("/"));
|
|
93
|
-
let
|
|
95
|
+
let failure: string | undefined;
|
|
94
96
|
pi.on("agent_before_settle", async (_event, ctx) => {
|
|
95
|
-
const from =
|
|
96
|
-
|
|
97
|
+
const from = failure;
|
|
98
|
+
failure = undefined;
|
|
97
99
|
const next = from === undefined ? undefined : await switchModel(pi, ctx, fallback);
|
|
98
100
|
if (next === undefined) return;
|
|
99
|
-
const content = `[delegate] ${from}
|
|
101
|
+
const content = `[delegate] ${from}, so this session switched to ${next}. Carry on with the task from where it stopped.`;
|
|
100
102
|
return { continue: true, entries: [{ type: "custom_message", customType: "delegate", content, display: true }] };
|
|
101
103
|
});
|
|
102
|
-
return (
|
|
103
|
-
|
|
104
|
+
return (value) => {
|
|
105
|
+
failure = value;
|
|
104
106
|
};
|
|
105
107
|
}
|
|
106
108
|
|
|
109
|
+
/** Why a failed run should move to a fallback, as `<provider/model> <what happened>`; undefined when it should not. */
|
|
110
|
+
function fallbackReason(last: Message | undefined): string | undefined {
|
|
111
|
+
const error = last?.errorMessage;
|
|
112
|
+
if (typeof error !== "string") return undefined;
|
|
113
|
+
const model = `${String(last?.provider)}/${String(last?.model)}`;
|
|
114
|
+
if (classify(error) !== undefined) return `${model} hit a usage limit`;
|
|
115
|
+
// Pi has already retried an unavailable endpoint; it marks nothing, but this run moves on.
|
|
116
|
+
if (/unavailable/i.test(error)) return `${model} was unavailable (${error})`;
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** This delegate's footer icon, picked once per load. */
|
|
121
|
+
const ICON = Math.random() < 0.5 ? "๐ฆ" : "๐ง";
|
|
122
|
+
|
|
123
|
+
function showStatus(ctx: ExtensionContext, armed: boolean): void {
|
|
124
|
+
const { theme } = ctx.ui;
|
|
125
|
+
ctx.ui.setStatus(
|
|
126
|
+
STATUS_KEY,
|
|
127
|
+
armed
|
|
128
|
+
? `${ICON} delegate ยท auto-exit ${theme.fg("success", "on")}`
|
|
129
|
+
: `${ICON} delegate ยท auto-exit ${theme.fg("dim", "off")}`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
107
133
|
export function registerChild(pi: ExtensionAPI, parent: string): void {
|
|
108
134
|
registerTakeover(pi, parent);
|
|
109
|
-
const
|
|
135
|
+
const setFailure = registerFallback(pi);
|
|
110
136
|
|
|
111
137
|
let armed = process.env[AUTO_EXIT_ENV] === "1";
|
|
112
138
|
/** Whether the run that just ended finished without the user stopping it. */
|
|
113
139
|
let completed = false;
|
|
114
140
|
|
|
141
|
+
// Reload and session replacement clear extension statuses, then start again.
|
|
142
|
+
pi.on("session_start", async (_event, ctx) => showStatus(ctx, armed));
|
|
143
|
+
|
|
115
144
|
function disarm(ctx: ExtensionContext): void {
|
|
116
145
|
if (!armed) return;
|
|
117
146
|
armed = false;
|
|
118
147
|
ctx.ui.notify(OFF_NOTICE, "info");
|
|
148
|
+
showStatus(ctx, armed);
|
|
119
149
|
}
|
|
120
150
|
|
|
121
151
|
// Typed text and RPC input are the user's; a prompt an extension sends is not.
|
|
@@ -144,11 +174,7 @@ export function registerChild(pi: ExtensionAPI, parent: string): void {
|
|
|
144
174
|
const stopped = last?.stopReason === "aborted" || ctx.signal?.aborted === true;
|
|
145
175
|
if (stopped) disarm(ctx);
|
|
146
176
|
if (!stopped && last?.stopReason === "error") recordUsageLimit(last, ctx, response);
|
|
147
|
-
|
|
148
|
-
!stopped && typeof last?.errorMessage === "string" && classify(last.errorMessage) !== undefined
|
|
149
|
-
? `${String(last.provider)}/${String(last.model)}`
|
|
150
|
-
: undefined,
|
|
151
|
-
);
|
|
177
|
+
setFailure(stopped ? undefined : fallbackReason(last));
|
|
152
178
|
// A failure ends the work like a completion: the parent gets the error, the session file stays.
|
|
153
179
|
completed = !stopped;
|
|
154
180
|
});
|
|
@@ -166,6 +192,7 @@ export function registerChild(pi: ExtensionAPI, parent: string): void {
|
|
|
166
192
|
handler: async (_args, ctx) => {
|
|
167
193
|
armed = true;
|
|
168
194
|
ctx.ui.notify(ON_NOTICE, "info");
|
|
195
|
+
showStatus(ctx, armed);
|
|
169
196
|
},
|
|
170
197
|
});
|
|
171
198
|
}
|
package/src/limits.ts
CHANGED
|
@@ -145,10 +145,12 @@ export function headerReset(response: Response | undefined, now = Date.now()): n
|
|
|
145
145
|
|
|
146
146
|
/**
|
|
147
147
|
* Whether an error message (Pi's `<status>: <body>`) is a usage limit, and
|
|
148
|
-
* the reset time the text states, if any. Overloaded and 5xx
|
|
148
|
+
* the reset time the text states, if any. Overloaded, unavailable and 5xx
|
|
149
|
+
* errors never are: OpenCode Go sends "429: Endpoint is unavailable" for an
|
|
150
|
+
* outage, with quota left.
|
|
149
151
|
*/
|
|
150
152
|
export function classify(message: string, now = Date.now()): { resetAt?: number } | undefined {
|
|
151
|
-
if (/overloaded/i.test(message) || /^\s*5\d\d\b/.test(message)) return undefined;
|
|
153
|
+
if (/overloaded|unavailable/i.test(message) || /^\s*5\d\d\b/.test(message)) return undefined;
|
|
152
154
|
if (!LIMIT_PATTERN.test(message) && !/^\s*429\b/.test(message)) return undefined;
|
|
153
155
|
const resetAt = statedReset(message, now);
|
|
154
156
|
return resetAt !== undefined && resetAt > now ? { resetAt } : {};
|
package/src/results.ts
CHANGED
|
@@ -237,6 +237,7 @@ export function createResults(pi: ExtensionAPI): Results {
|
|
|
237
237
|
let ctx: ExtensionContext | undefined;
|
|
238
238
|
/** Whether the footer currently shows the entry, so a clear is sent only when needed. */
|
|
239
239
|
let footerShown = false;
|
|
240
|
+
const icon = Math.random() < 0.5 ? "๐จ" : "๐ฉ";
|
|
240
241
|
|
|
241
242
|
/** A delegation still going: no result and no recorded close. */
|
|
242
243
|
const running = (): Recorded[] =>
|
|
@@ -251,7 +252,7 @@ export function createResults(pi: ExtensionAPI): Results {
|
|
|
251
252
|
const visible = count > 0;
|
|
252
253
|
if (!visible && !footerShown) return;
|
|
253
254
|
footerShown = visible;
|
|
254
|
-
ctx.ui.setStatus(STATUS_KEY, visible ?
|
|
255
|
+
ctx.ui.setStatus(STATUS_KEY, visible ? `${icon} ${count} delegate${count === 1 ? "" : "s"} running` : undefined);
|
|
255
256
|
}
|
|
256
257
|
|
|
257
258
|
/** Rebuild the records from the session's entries, so a resume still recognises replies. */
|