pi-squire 0.3.2 → 0.4.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/CHANGELOG.md +33 -1
- package/README.md +15 -4
- package/package.json +3 -1
- package/src/child.ts +58 -9
- package/src/index.ts +20 -9
- package/src/limits.ts +1 -1
- package/src/results.ts +28 -5
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,37 @@ follows [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.4.0] - 2026-10-13
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- A delegate whose run ends on a usage limit now switches to the agent's next
|
|
14
|
+
fallback that has credentials and is not marked, and continues the task in
|
|
15
|
+
the same session instead of failing. The parent passes the candidates after
|
|
16
|
+
the launched model as `PI_DELEGATE_FALLBACK`; an explicit `model` passes
|
|
17
|
+
none. A `done` result from such a delegate carries the `Usage limit:` line,
|
|
18
|
+
saying it finished on a fallback.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- A delegate message in the chat now shows collapsed, as its first two
|
|
23
|
+
lines, and expands with the tool-output toggle (Ctrl+O), in step with tool
|
|
24
|
+
output. The model still gets the whole message.
|
|
25
|
+
- With auto-exit on, a delegate whose run fails on any provider error now
|
|
26
|
+
closes its window, as after a normal completion; before, only a usage-limit
|
|
27
|
+
failure did. The parent still gets the `failed` reply and the session file
|
|
28
|
+
stays. Only a run the user stopped keeps the window open.
|
|
29
|
+
`test/auto-exit.test.ts` and `test/harness.test.ts` change with it.
|
|
30
|
+
- Refusing an explicit `model` that is usage-limited now names the agent's
|
|
31
|
+
models that are still open, so the caller can omit `model` and let the
|
|
32
|
+
fallbacks apply.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- ChatGPT's "The ChatGPT user has reached their Subscription Sharing usage
|
|
37
|
+
limit" error now counts as a usage limit, so it records a mark and later
|
|
38
|
+
launches skip the provider. Before, it recorded nothing.
|
|
39
|
+
|
|
9
40
|
## [0.3.2] - 2026-10-09
|
|
10
41
|
|
|
11
42
|
### Added
|
|
@@ -169,7 +200,8 @@ author's dotfiles; the changes below are against that copy.
|
|
|
169
200
|
|
|
170
201
|
- The published file list includes `child.ts`, which `index.ts` imports.
|
|
171
202
|
|
|
172
|
-
[Unreleased]: https://github.com/gvanderclay/pi-squire/compare/v0.
|
|
203
|
+
[Unreleased]: https://github.com/gvanderclay/pi-squire/compare/v0.4.0...HEAD
|
|
204
|
+
[0.4.0]: https://github.com/gvanderclay/pi-squire/compare/v0.3.2...v0.4.0
|
|
173
205
|
[0.3.2]: https://github.com/gvanderclay/pi-squire/compare/v0.3.1...v0.3.2
|
|
174
206
|
[0.3.1]: https://github.com/gvanderclay/pi-squire/compare/v0.3.0...v0.3.1
|
|
175
207
|
[0.3.0]: https://github.com/gvanderclay/pi-squire/compare/v0.2.2...v0.3.0
|
package/README.md
CHANGED
|
@@ -290,7 +290,9 @@ A mark clears at the reset time the error states ("try again in 30 minutes",
|
|
|
290
290
|
hours. When the failed response's headers state a reset (`retry-after`,
|
|
291
291
|
`anthropic-ratelimit-*-reset`, `x-ratelimit-reset-requests` or `-tokens`),
|
|
292
292
|
that time wins over the error text. Until then `delegate` and `/delegate`
|
|
293
|
-
refuse a marked model passed as `model` / `--model`,
|
|
293
|
+
refuse a marked model passed as `model` / `--model`, say when it clears, and
|
|
294
|
+
name the agent's own models that are not marked, so the caller can omit the
|
|
295
|
+
model instead.
|
|
294
296
|
|
|
295
297
|
Without an explicit model, the delegation launches on the agent's model, or
|
|
296
298
|
else on the first of its `fallback` models that has credentials and is not
|
|
@@ -300,6 +302,14 @@ the result details list the skipped models under `skipped`. When every
|
|
|
300
302
|
candidate is marked or unusable, the call is refused with one line per
|
|
301
303
|
candidate and why it was skipped.
|
|
302
304
|
|
|
305
|
+
The launch also passes the candidates after the chosen one to the delegate as
|
|
306
|
+
`PI_DELEGATE_FALLBACK` (none for an explicit model). When a delegate's run
|
|
307
|
+
ends on a usage limit, it switches its session to the first of them that has
|
|
308
|
+
credentials and is not marked, adds a message saying so, and continues the
|
|
309
|
+
task there, so work done so far is kept. Each fallback is tried once; when
|
|
310
|
+
none is left, the run fails as before. A `done` result from such a delegate
|
|
311
|
+
carries the `Usage limit:` line too, saying it finished on a fallback.
|
|
312
|
+
|
|
303
313
|
Before a launch, pi-squire also reads the OpenCode Go quota for a paid
|
|
304
314
|
`opencode-go` candidate (`GET https://opencode.ai/zen/go/v1/usage`, with the
|
|
305
315
|
provider's API key). A window at 100% or more, or one the provider reports as
|
|
@@ -371,9 +381,10 @@ The parent resolves each delegation's auto-exit from the call
|
|
|
371
381
|
down (`ctx.shutdown()`) on the next event-loop turn, after the settle
|
|
372
382
|
reply has been sent, so its tmux window closes. The session file stays, and
|
|
373
383
|
`pi --session <path>` reopens it.
|
|
374
|
-
- A run
|
|
375
|
-
exits
|
|
376
|
-
|
|
384
|
+
- A failed run, such as one ending on a usage limit or another provider
|
|
385
|
+
error, exits like a normal completion; the parent still gets the failed
|
|
386
|
+
reply and the session file stays. Only a run the user stopped never exits
|
|
387
|
+
the delegate.
|
|
377
388
|
- The user taking over turns it off for the rest of the session, with a
|
|
378
389
|
notice: typed or RPC input, or a run the user stopped (Esc).
|
|
379
390
|
- `/auto-exit` inside the delegate turns it on again for the next normal
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-squire",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Starts a delegate Pi session in a background tmux window, sends it the task over message:*, and shows the parent its result",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -45,11 +45,13 @@
|
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"@earendil-works/pi-coding-agent": "*",
|
|
48
|
+
"@earendil-works/pi-tui": "*",
|
|
48
49
|
"typebox": "*"
|
|
49
50
|
},
|
|
50
51
|
"devDependencies": {
|
|
51
52
|
"@biomejs/biome": "2.5.14",
|
|
52
53
|
"@earendil-works/pi-coding-agent": "1.0.0",
|
|
54
|
+
"@earendil-works/pi-tui": "1.0.0",
|
|
53
55
|
"@types/node": "26.6.3",
|
|
54
56
|
"typebox": "1.3.27",
|
|
55
57
|
"typescript": "7.0.2"
|
package/src/child.ts
CHANGED
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
//
|
|
3
3
|
// The parent resolves the setting and passes it as `PI_DELEGATE_AUTO_EXIT`
|
|
4
4
|
// (`1` on, `0` off). While it is on, the delegate shuts Pi down once a run
|
|
5
|
-
// settles
|
|
6
|
-
//
|
|
7
|
-
//
|
|
5
|
+
// settles, so its tmux window closes; the settle reply has been sent by
|
|
6
|
+
// then, since the shutdown waits for the next event-loop turn. A failed run
|
|
7
|
+
// exits too (the parent gets the error and the session file stays); only a
|
|
8
|
+
// run the user stopped never exits. A run that ends on a usage limit first
|
|
9
|
+
// moves to the agent's next fallback, from `PI_DELEGATE_FALLBACK`, and
|
|
10
|
+
// continues there; it exits only when no fallback is left. The user
|
|
8
11
|
// taking over — typing here, or stopping a run — turns it off for the rest of
|
|
9
12
|
// the session, with a notice; `/auto-exit` turns it back on. Modelled on
|
|
10
13
|
// edxeth/pi-subagents' `auto-exit` (README "Child lifecycle", `cf6dbf4`).
|
|
@@ -17,10 +20,12 @@
|
|
|
17
20
|
// ponytail: drop the takeover once #5581 lands and the minimum Pi has it.
|
|
18
21
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
19
22
|
|
|
20
|
-
import { classify, type Response, recordLimit } from "./limits.ts";
|
|
23
|
+
import { activeMark, classify, type Response, recordLimit } from "./limits.ts";
|
|
21
24
|
|
|
22
25
|
/** The launch variable that carries the parent's resolved setting. */
|
|
23
26
|
export const AUTO_EXIT_ENV = "PI_DELEGATE_AUTO_EXIT";
|
|
27
|
+
/** The launch variable listing, comma-separated, the agent's models after the one launched. */
|
|
28
|
+
export const FALLBACK_ENV = "PI_DELEGATE_FALLBACK";
|
|
24
29
|
|
|
25
30
|
const OFF_NOTICE =
|
|
26
31
|
"delegate: auto-exit is off for this session because you took over here; the window stays open. /auto-exit turns it back on.";
|
|
@@ -47,7 +52,20 @@ function recordUsageLimit(last: Message, ctx: ExtensionContext, response?: Respo
|
|
|
47
52
|
}
|
|
48
53
|
}
|
|
49
54
|
|
|
50
|
-
|
|
55
|
+
/** Switch to the first of `fallback` that is usable and not marked, taking each off the list; the one switched to, if any. */
|
|
56
|
+
async function switchModel(pi: ExtensionAPI, ctx: ExtensionContext, fallback: string[]): Promise<string | undefined> {
|
|
57
|
+
for (let next = fallback.shift(); next !== undefined; next = fallback.shift()) {
|
|
58
|
+
const slash = next.indexOf("/");
|
|
59
|
+
const model = ctx.modelRegistry.find(next.slice(0, slash), next.slice(slash + 1));
|
|
60
|
+
if (model === undefined || !ctx.modelRegistry.hasConfiguredAuth(model)) continue;
|
|
61
|
+
if (activeMark(ctx.modelRegistry, model.provider, model.id) !== undefined) continue;
|
|
62
|
+
if (await pi.setModel(model)) return next;
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Take over the parent's task request and send it as a user prompt. */
|
|
68
|
+
function registerTakeover(pi: ExtensionAPI, parent: string): void {
|
|
51
69
|
let current: ExtensionContext | undefined;
|
|
52
70
|
pi.on("session_start", async (_event, ctx) => {
|
|
53
71
|
current = ctx;
|
|
@@ -62,9 +80,36 @@ export function registerChild(pi: ExtensionAPI, parent: string): void {
|
|
|
62
80
|
current?.ui.notify(`delegate: could not start the task: ${(err as Error).message}`, "error"),
|
|
63
81
|
);
|
|
64
82
|
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* A usage limit moves the run to the next fallback that is usable and not
|
|
87
|
+
* marked, and continues it in this session; each fallback is tried once.
|
|
88
|
+
* Returns the setter for the model whose usage limit ended the last run.
|
|
89
|
+
*/
|
|
90
|
+
function registerFallback(pi: ExtensionAPI): (model: string | undefined) => void {
|
|
91
|
+
/** The models still to move to on a usage limit, in order. */
|
|
92
|
+
const fallback = (process.env[FALLBACK_ENV] ?? "").split(",").filter((model) => model.includes("/"));
|
|
93
|
+
let limited: string | undefined;
|
|
94
|
+
pi.on("agent_before_settle", async (_event, ctx) => {
|
|
95
|
+
const from = limited;
|
|
96
|
+
limited = undefined;
|
|
97
|
+
const next = from === undefined ? undefined : await switchModel(pi, ctx, fallback);
|
|
98
|
+
if (next === undefined) return;
|
|
99
|
+
const content = `[delegate] ${from} hit a usage limit, so this session switched to ${next}. Carry on with the task from where it stopped.`;
|
|
100
|
+
return { continue: true, entries: [{ type: "custom_message", customType: "delegate", content, display: true }] };
|
|
101
|
+
});
|
|
102
|
+
return (model) => {
|
|
103
|
+
limited = model;
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function registerChild(pi: ExtensionAPI, parent: string): void {
|
|
108
|
+
registerTakeover(pi, parent);
|
|
109
|
+
const setLimited = registerFallback(pi);
|
|
65
110
|
|
|
66
111
|
let armed = process.env[AUTO_EXIT_ENV] === "1";
|
|
67
|
-
/** Whether the run that just ended finished
|
|
112
|
+
/** Whether the run that just ended finished without the user stopping it. */
|
|
68
113
|
let completed = false;
|
|
69
114
|
|
|
70
115
|
function disarm(ctx: ExtensionContext): void {
|
|
@@ -99,9 +144,13 @@ export function registerChild(pi: ExtensionAPI, parent: string): void {
|
|
|
99
144
|
const stopped = last?.stopReason === "aborted" || ctx.signal?.aborted === true;
|
|
100
145
|
if (stopped) disarm(ctx);
|
|
101
146
|
if (!stopped && last?.stopReason === "error") recordUsageLimit(last, ctx, response);
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
147
|
+
setLimited(
|
|
148
|
+
!stopped && typeof last?.errorMessage === "string" && classify(last.errorMessage) !== undefined
|
|
149
|
+
? `${String(last.provider)}/${String(last.model)}`
|
|
150
|
+
: undefined,
|
|
151
|
+
);
|
|
152
|
+
// A failure ends the work like a completion: the parent gets the error, the session file stays.
|
|
153
|
+
completed = !stopped;
|
|
105
154
|
});
|
|
106
155
|
|
|
107
156
|
pi.on("agent_settled", async (_event, ctx) => {
|
package/src/index.ts
CHANGED
|
@@ -47,7 +47,7 @@ import {
|
|
|
47
47
|
import { Type } from "typebox";
|
|
48
48
|
|
|
49
49
|
import { type Agent, isThinking, type Roster, readRoster, THINKING_LEVELS } from "./agents.ts";
|
|
50
|
-
import { AUTO_EXIT_ENV, registerChild } from "./child.ts";
|
|
50
|
+
import { AUTO_EXIT_ENV, FALLBACK_ENV, registerChild } from "./child.ts";
|
|
51
51
|
import { activeMark, clearMarks, isFree, readMarks } from "./limits.ts";
|
|
52
52
|
import { createResults, delegateName } from "./results.ts";
|
|
53
53
|
import { createTmuxClient, type TmuxClient } from "./tmux.ts";
|
|
@@ -114,6 +114,8 @@ type Start = {
|
|
|
114
114
|
skipped: Skipped[];
|
|
115
115
|
/** Lines for the parent, such as a quota reading that failed. */
|
|
116
116
|
notices: string[];
|
|
117
|
+
/** The candidates after `model`, for the delegate to move to on a usage limit; empty for an explicit model. */
|
|
118
|
+
rest: string[];
|
|
117
119
|
thinking: string;
|
|
118
120
|
label?: string;
|
|
119
121
|
autoExit: boolean;
|
|
@@ -238,7 +240,7 @@ async function chooseModel(
|
|
|
238
240
|
agent: Agent,
|
|
239
241
|
models: ModelRegistry,
|
|
240
242
|
proactive: Proactive,
|
|
241
|
-
): Promise<{ model: string; skipped: Skipped[]; notices: string[] }> {
|
|
243
|
+
): Promise<{ model: string; skipped: Skipped[]; notices: string[]; rest: string[] }> {
|
|
242
244
|
const candidates = explicit !== undefined ? [explicit] : [agent.model, ...agent.fallback];
|
|
243
245
|
const skipped: Skipped[] = [];
|
|
244
246
|
const notices: string[] = [];
|
|
@@ -262,7 +264,7 @@ async function chooseModel(
|
|
|
262
264
|
if (checked.notice !== undefined && !notices.includes(checked.notice)) notices.push(checked.notice);
|
|
263
265
|
mark = activeMark(models, provider, id) ?? checked.mark;
|
|
264
266
|
}
|
|
265
|
-
if (mark === undefined) return { model, skipped, notices };
|
|
267
|
+
if (mark === undefined) return { model, skipped, notices, rest: candidates.slice(candidates.indexOf(model) + 1) };
|
|
266
268
|
const what = mark.scope.includes("/") ? `model ${mark.scope}` : `provider ${mark.scope}`;
|
|
267
269
|
const until = new Date(mark.clearsAt).toISOString();
|
|
268
270
|
skipped.push({
|
|
@@ -273,11 +275,18 @@ async function chooseModel(
|
|
|
273
275
|
}
|
|
274
276
|
if (skipped.length === 1) {
|
|
275
277
|
const [only] = skipped;
|
|
276
|
-
throw new Error(
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
278
|
+
if (!only.why.startsWith("usage-limited")) throw new Error(only.reason);
|
|
279
|
+
// An explicit model is never substituted, but the refusal names the agent's models still open.
|
|
280
|
+
const open = [agent.model, ...agent.fallback].filter((model) => {
|
|
281
|
+
if (model === only.model || modelProblem(model, models) !== undefined) return false;
|
|
282
|
+
const slash = model.indexOf("/");
|
|
283
|
+
return activeMark(models, model.slice(0, slash), model.slice(slash + 1)) === undefined;
|
|
284
|
+
});
|
|
285
|
+
const hint =
|
|
286
|
+
explicit !== undefined && open.length > 0
|
|
287
|
+
? ` Omit model to launch ${agent.name} on ${open[0]}${open.length > 1 ? ` (also open: ${open.slice(1).join(", ")})` : ""}, or clear marks with /delegate-clear.`
|
|
288
|
+
: " Clear marks with /delegate-clear.";
|
|
289
|
+
throw new Error(`${only.model} is ${only.reason}.${hint}`);
|
|
281
290
|
}
|
|
282
291
|
throw new Error(
|
|
283
292
|
`no model is available for agent ${agent.name}:\n${skipped.map((item) => `- ${item.model}: ${item.reason}`).join("\n")}\nClear marks with /delegate-clear.`,
|
|
@@ -399,7 +408,7 @@ export default function delegate(
|
|
|
399
408
|
}
|
|
400
409
|
if (request.task.trim() === "") throw new Error("a task is required");
|
|
401
410
|
const thinking = request.thinking ?? agent.thinking;
|
|
402
|
-
const { model, skipped, notices } = await chooseModel(request.model, agent, models, proactive);
|
|
411
|
+
const { model, skipped, notices, rest } = await chooseModel(request.model, agent, models, proactive);
|
|
403
412
|
const badThinking = thinkingProblem(thinking);
|
|
404
413
|
if (badThinking !== undefined) throw new Error(badThinking);
|
|
405
414
|
const badLabel = request.label === undefined ? undefined : labelProblem(request.label);
|
|
@@ -409,6 +418,7 @@ export default function delegate(
|
|
|
409
418
|
model,
|
|
410
419
|
skipped,
|
|
411
420
|
notices,
|
|
421
|
+
rest,
|
|
412
422
|
thinking,
|
|
413
423
|
label: request.label,
|
|
414
424
|
autoExit: request.autoExit ?? agent.autoExit,
|
|
@@ -459,6 +469,7 @@ export default function delegate(
|
|
|
459
469
|
[PARENT_ENV]: parent,
|
|
460
470
|
[AUTO_EXIT_ENV]: start.autoExit ? "1" : "0",
|
|
461
471
|
};
|
|
472
|
+
if (start.rest.length > 0) env[FALLBACK_ENV] = start.rest.join(",");
|
|
462
473
|
const sessionDir = process.env[SESSION_DIR_ENV];
|
|
463
474
|
if (sessionDir !== undefined && sessionDir !== "") env[SESSION_DIR_ENV] = sessionDir;
|
|
464
475
|
// Listeners may only append to `args` and add to `env`; there is no veto.
|
package/src/limits.ts
CHANGED
|
@@ -85,7 +85,7 @@ export function activeMark(models: Models, provider: string, id: string, now = D
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
const LIMIT_PATTERN =
|
|
88
|
-
/GoUsageLimitError|FreeUsageLimitError|Monthly usage limit reached|insufficient_quota|quota exceeded|subscription_sharing_usage_limit_exceeded|usage_limit_reached|usage limit reached|rate_limit_error/i;
|
|
88
|
+
/GoUsageLimitError|FreeUsageLimitError|Monthly usage limit reached|insufficient_quota|quota exceeded|subscription_sharing_usage_limit_exceeded|usage_limit_reached|usage limit reached|reached (?:their|your) .{0,40}usage limit|rate_limit_error/i;
|
|
89
89
|
|
|
90
90
|
/** The reset time an error text states: "try again in N units", "retry after", or a `resets_at` field. */
|
|
91
91
|
function statedReset(message: string, now: number): number | undefined {
|
package/src/results.ts
CHANGED
|
@@ -7,7 +7,13 @@
|
|
|
7
7
|
import { readdirSync } from "node:fs";
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
|
|
10
|
-
import
|
|
10
|
+
import {
|
|
11
|
+
type ExtensionAPI,
|
|
12
|
+
type ExtensionContext,
|
|
13
|
+
keyText,
|
|
14
|
+
type MessageRenderer,
|
|
15
|
+
} from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import { Box, Text } from "@earendil-works/pi-tui";
|
|
11
17
|
|
|
12
18
|
import { readMarks } from "./limits.ts";
|
|
13
19
|
|
|
@@ -159,8 +165,8 @@ export function findSession(dir: string | undefined, id: string): string | undef
|
|
|
159
165
|
return name === undefined ? undefined : join(dir, name);
|
|
160
166
|
}
|
|
161
167
|
|
|
162
|
-
/** The line naming a usage-limit mark this delegation
|
|
163
|
-
function limitLine(id: string): string | undefined {
|
|
168
|
+
/** The line naming a usage-limit mark this delegation recorded, if any; a `done` delegation finished on a fallback. */
|
|
169
|
+
function limitLine(id: string, status: string): string | undefined {
|
|
164
170
|
const now = Date.now();
|
|
165
171
|
const mark = readMarks(now).find((item) => item.delegations.includes(id));
|
|
166
172
|
if (mark === undefined) return undefined;
|
|
@@ -168,7 +174,7 @@ function limitLine(id: string): string | undefined {
|
|
|
168
174
|
const until = new Date(mark.clearsAt).toISOString();
|
|
169
175
|
return `Usage limit: the delegate hit a usage limit on ${scope}; ${
|
|
170
176
|
mark.clearsAt > now ? `it is marked until ${until} and later delegations skip it.` : `it was marked until ${until}.`
|
|
171
|
-
}`;
|
|
177
|
+
}${status === "done" ? " It finished on a fallback model; its session names which." : ""}`;
|
|
172
178
|
}
|
|
173
179
|
|
|
174
180
|
/** The one message the parent sees for a settled delegation. */
|
|
@@ -190,7 +196,7 @@ function resultText(delegation: Delegation, payload: Inbound, session: string |
|
|
|
190
196
|
const cut = cap(reply.body, BODY_CAP);
|
|
191
197
|
const result =
|
|
192
198
|
cut === undefined ? reply.body : `${cut}\n[delegate] Body cut at 32 KiB; the full envelope is ${payload.path}`;
|
|
193
|
-
const limit = reply.status === "
|
|
199
|
+
const limit = reply.status === "stopped" ? undefined : limitLine(delegation.id, reply.status);
|
|
194
200
|
return [
|
|
195
201
|
`[delegate] Result from ${delegation.agent} (${delegation.model}), delegation ${delegation.id}, request ${delegation.requestId}.`,
|
|
196
202
|
`Status: ${reply.status}`,
|
|
@@ -203,7 +209,24 @@ function resultText(delegation: Delegation, payload: Inbound, session: string |
|
|
|
203
209
|
].join("\n");
|
|
204
210
|
}
|
|
205
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Collapsed, a delegate message shows its first two lines (who answered and the
|
|
214
|
+
* status) and a hint; expanded, Pi's default rendering of the whole message.
|
|
215
|
+
* The model always gets the full content. Ctrl+O toggles it with tool output.
|
|
216
|
+
*/
|
|
217
|
+
export const renderMessage: MessageRenderer = (message, { expanded, outputPad }, theme) => {
|
|
218
|
+
if (expanded || typeof message.content !== "string") return undefined;
|
|
219
|
+
const lines = message.content.split("\n");
|
|
220
|
+
const shown = lines.slice(0, 2).join("\n");
|
|
221
|
+
const hint =
|
|
222
|
+
lines.length > 2 ? `\n${theme.fg("dim", keyText("app.tools.expand"))}${theme.fg("muted", " to expand")}` : "";
|
|
223
|
+
const box = new Box(outputPad, 1, (text) => theme.bg("customMessageBg", text));
|
|
224
|
+
box.addChild(new Text(`${theme.fg("customMessageText", shown)}${hint}`, 0, 0));
|
|
225
|
+
return box;
|
|
226
|
+
};
|
|
227
|
+
|
|
206
228
|
export function createResults(pi: ExtensionAPI): Results {
|
|
229
|
+
pi.registerMessageRenderer(CUSTOM_TYPE, renderMessage);
|
|
207
230
|
/** Every recorded delegation by its id, in the order recorded. */
|
|
208
231
|
const byId = new Map<string, Recorded>();
|
|
209
232
|
/** The same records by their request's envelope id, for matching replies. */
|