pi-squire 0.6.0 โ 0.8.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 +37 -1
- package/README.md +7 -2
- package/package.json +1 -1
- package/src/child.ts +19 -0
- package/src/index.ts +19 -0
- package/src/results.ts +2 -1
- package/src/tracking.ts +77 -12
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,40 @@ follows [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.8.0] - 2026-10-20
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- A delegate's footer shows that it is a delegate and whether auto-exit is
|
|
14
|
+
on: `๐ฆ delegate ยท auto-exit on`, or
|
|
15
|
+
`๐ฆ delegate ยท auto-exit off`. It follows the
|
|
16
|
+
setting as typing turns it off and `/auto-exit` turns it back on.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- The parent's footer reads `๐จ N delegates running` instead of
|
|
21
|
+
`โ N running`. Each side picks its icon at random per session: ๐จ or ๐ฉ
|
|
22
|
+
for the parent, ๐ฆ or ๐ง for a delegate. The footer tests in
|
|
23
|
+
`test/delegate.test.ts` change to match.
|
|
24
|
+
|
|
25
|
+
## [0.7.0] - 2026-10-19
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- `delegate`, `delegation_status` and `delegation_close` declare an
|
|
30
|
+
`outputSchema` and return matching `structuredContent`, so a codemode
|
|
31
|
+
script gets objects instead of text. `delegation_status` gives each
|
|
32
|
+
delegation its derived `state` (`running`, `done`, `gone`, `closed`,
|
|
33
|
+
`window-closed` or `unknown`), also added to its details.
|
|
34
|
+
`delegation_close` returns one shape on every path:
|
|
35
|
+
`{ id, windowId, windowName, closed, alreadyClosed, killed }`; its details
|
|
36
|
+
gain the fields each path lacked. The model-facing text is unchanged.
|
|
37
|
+
- The three tools declare MCP-style `annotations`: `delegation_status` is
|
|
38
|
+
read-only and idempotent; `delegation_close` is destructive and idempotent;
|
|
39
|
+
`delegate` is neither, and is the only one that reaches an open world,
|
|
40
|
+
since the delegate it starts calls a remote model and runs tools
|
|
41
|
+
unattended.
|
|
42
|
+
|
|
9
43
|
## [0.6.0] - 2026-10-15
|
|
10
44
|
|
|
11
45
|
### Added
|
|
@@ -239,7 +273,9 @@ author's dotfiles; the changes below are against that copy.
|
|
|
239
273
|
|
|
240
274
|
- The published file list includes `child.ts`, which `index.ts` imports.
|
|
241
275
|
|
|
242
|
-
[Unreleased]: https://github.com/gvanderclay/pi-squire/compare/v0.
|
|
276
|
+
[Unreleased]: https://github.com/gvanderclay/pi-squire/compare/v0.8.0...HEAD
|
|
277
|
+
[0.8.0]: https://github.com/gvanderclay/pi-squire/compare/v0.7.0...v0.8.0
|
|
278
|
+
[0.7.0]: https://github.com/gvanderclay/pi-squire/compare/v0.6.0...v0.7.0
|
|
243
279
|
[0.6.0]: https://github.com/gvanderclay/pi-squire/compare/v0.5.1...v0.6.0
|
|
244
280
|
[0.5.1]: https://github.com/gvanderclay/pi-squire/compare/v0.5.0...v0.5.1
|
|
245
281
|
[0.5.0]: https://github.com/gvanderclay/pi-squire/compare/v0.4.0...v0.5.0
|
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";
|
|
@@ -104,6 +105,19 @@ function registerFallback(pi: ExtensionAPI): (model: string | undefined) => void
|
|
|
104
105
|
};
|
|
105
106
|
}
|
|
106
107
|
|
|
108
|
+
/** This delegate's footer icon, picked once per load. */
|
|
109
|
+
const ICON = Math.random() < 0.5 ? "๐ฆ" : "๐ง";
|
|
110
|
+
|
|
111
|
+
function showStatus(ctx: ExtensionContext, armed: boolean): void {
|
|
112
|
+
const { theme } = ctx.ui;
|
|
113
|
+
ctx.ui.setStatus(
|
|
114
|
+
STATUS_KEY,
|
|
115
|
+
armed
|
|
116
|
+
? `${ICON} delegate ยท auto-exit ${theme.fg("success", "on")}`
|
|
117
|
+
: `${ICON} delegate ยท auto-exit ${theme.fg("dim", "off")}`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
107
121
|
export function registerChild(pi: ExtensionAPI, parent: string): void {
|
|
108
122
|
registerTakeover(pi, parent);
|
|
109
123
|
const setLimited = registerFallback(pi);
|
|
@@ -112,10 +126,14 @@ export function registerChild(pi: ExtensionAPI, parent: string): void {
|
|
|
112
126
|
/** Whether the run that just ended finished without the user stopping it. */
|
|
113
127
|
let completed = false;
|
|
114
128
|
|
|
129
|
+
// Reload and session replacement clear extension statuses, then start again.
|
|
130
|
+
pi.on("session_start", async (_event, ctx) => showStatus(ctx, armed));
|
|
131
|
+
|
|
115
132
|
function disarm(ctx: ExtensionContext): void {
|
|
116
133
|
if (!armed) return;
|
|
117
134
|
armed = false;
|
|
118
135
|
ctx.ui.notify(OFF_NOTICE, "info");
|
|
136
|
+
showStatus(ctx, armed);
|
|
119
137
|
}
|
|
120
138
|
|
|
121
139
|
// Typed text and RPC input are the user's; a prompt an extension sends is not.
|
|
@@ -166,6 +184,7 @@ export function registerChild(pi: ExtensionAPI, parent: string): void {
|
|
|
166
184
|
handler: async (_args, ctx) => {
|
|
167
185
|
armed = true;
|
|
168
186
|
ctx.ui.notify(ON_NOTICE, "info");
|
|
187
|
+
showStatus(ctx, armed);
|
|
169
188
|
},
|
|
170
189
|
});
|
|
171
190
|
}
|
package/src/index.ts
CHANGED
|
@@ -99,6 +99,22 @@ function parentTrust(parent: string): string {
|
|
|
99
99
|
/** The flags `/delegate` takes before the task. */
|
|
100
100
|
const FLAGS = ["--model", "--thinking", "--label", "--auto-exit", "--no-auto-exit"] as const;
|
|
101
101
|
|
|
102
|
+
/** The `delegate` tool's structured result, which is also its details. */
|
|
103
|
+
export const DelegateSchema = Type.Object({
|
|
104
|
+
outcome: Type.Literal("started"),
|
|
105
|
+
id: Type.String({ description: "The delegation id that delegation_status and delegation_close take." }),
|
|
106
|
+
agent: Type.String(),
|
|
107
|
+
model: Type.String({ description: "The provider/id it started on, after any fallback." }),
|
|
108
|
+
skipped: Type.Array(Type.Object({ model: Type.String(), reason: Type.String() }), {
|
|
109
|
+
description: "Earlier models in the fallback list that were passed over, and why.",
|
|
110
|
+
}),
|
|
111
|
+
notices: Type.Array(Type.String()),
|
|
112
|
+
thinking: Type.String(),
|
|
113
|
+
autoExit: Type.Boolean(),
|
|
114
|
+
windowId: Type.String(),
|
|
115
|
+
windowName: Type.String(),
|
|
116
|
+
});
|
|
117
|
+
|
|
102
118
|
const NOT_IN_TMUX = "this session is not inside tmux; start it in a tmux pane to open a delegate window";
|
|
103
119
|
|
|
104
120
|
const USAGE =
|
|
@@ -551,6 +567,9 @@ export default function delegate(
|
|
|
551
567
|
}),
|
|
552
568
|
),
|
|
553
569
|
}),
|
|
570
|
+
outputSchema: DelegateSchema,
|
|
571
|
+
// Opens a new window each call and removes nothing. Open world: the delegate calls a remote model and runs its agent's tools unattended.
|
|
572
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
554
573
|
async execute(
|
|
555
574
|
_toolCallId: string,
|
|
556
575
|
params: {
|
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. */
|
package/src/tracking.ts
CHANGED
|
@@ -37,20 +37,76 @@ export type Tracking = {
|
|
|
37
37
|
stop(): void;
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
-
/** The one result every tool returns. */
|
|
40
|
+
/** The one result every tool returns. `details` doubles as `structuredContent`, so it must match the tool's `outputSchema`; Pi does not check it. */
|
|
41
41
|
export function toolResult(
|
|
42
42
|
payload: string,
|
|
43
|
-
details: unknown
|
|
44
|
-
): { content: { type: "text"; text: string }[]; details: unknown } {
|
|
45
|
-
return { content: [{ type: "text", text: payload }], details };
|
|
43
|
+
details: unknown,
|
|
44
|
+
): { content: { type: "text"; text: string }[]; details: unknown; structuredContent: unknown } {
|
|
45
|
+
return { content: [{ type: "text", text: payload }], details, structuredContent: details };
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/** Every tool here touches only local state: tmux, the session's entries and files under the agent directory. */
|
|
49
|
+
const CLOSED_WORLD = { openWorldHint: false } as const;
|
|
50
|
+
|
|
51
|
+
const DELEGATION_STATES: readonly DelegationState[] = ["closed", "done", "gone", "running", "window-closed", "unknown"];
|
|
52
|
+
|
|
53
|
+
/** One delegation in `delegation_status`'s structured result; `viewDetails` builds it. */
|
|
54
|
+
const DelegationSchema = Type.Object({
|
|
55
|
+
id: Type.String(),
|
|
56
|
+
name: Type.String(),
|
|
57
|
+
agent: Type.String(),
|
|
58
|
+
model: Type.String(),
|
|
59
|
+
thinking: Type.String(),
|
|
60
|
+
autoExit: Type.Boolean(),
|
|
61
|
+
windowId: Type.String(),
|
|
62
|
+
windowName: Type.String(),
|
|
63
|
+
state: Type.Union(
|
|
64
|
+
DELEGATION_STATES.map((state) => Type.Literal(state)),
|
|
65
|
+
{
|
|
66
|
+
description:
|
|
67
|
+
"running: window open, no result. done: a result arrived (see status, envelopePath). gone: the window went with no result; a late reply makes it done. closed: delegation_close recorded it. window-closed: the window is gone. unknown: tmux could not be asked. done and closed are final; gone and window-closed turn done if a late reply arrives.",
|
|
68
|
+
},
|
|
69
|
+
),
|
|
70
|
+
closed: Type.Boolean(),
|
|
71
|
+
closedWithoutResult: Type.Boolean(),
|
|
72
|
+
status: Type.Optional(Type.String({ description: "The result envelope's status: done, failed or stopped." })),
|
|
73
|
+
envelopePath: Type.Optional(Type.String()),
|
|
74
|
+
sessionPath: Type.Optional(Type.String()),
|
|
75
|
+
lastActiveAt: Type.Optional(
|
|
76
|
+
Type.String({ description: "ISO time the session file was last written; running only." }),
|
|
77
|
+
),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
export const StatusSchema = Type.Object({
|
|
81
|
+
delegations: Type.Array(DelegationSchema),
|
|
82
|
+
marks: Type.Array(
|
|
83
|
+
Type.Object({
|
|
84
|
+
scope: Type.String(),
|
|
85
|
+
source: Type.Union([Type.Literal("reactive"), Type.Literal("proactive")]),
|
|
86
|
+
reason: Type.String(),
|
|
87
|
+
clearsAt: Type.String(),
|
|
88
|
+
}),
|
|
89
|
+
),
|
|
90
|
+
readingErrors: Type.Array(Type.Object({ provider: Type.String(), reason: Type.String(), at: Type.String() })),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
export const CloseSchema = Type.Object({
|
|
94
|
+
id: Type.String(),
|
|
95
|
+
windowId: Type.String(),
|
|
96
|
+
windowName: Type.String(),
|
|
97
|
+
closed: Type.Literal(true),
|
|
98
|
+
alreadyClosed: Type.Boolean({ description: "An earlier delegation_close had recorded it; nothing was done." }),
|
|
99
|
+
killed: Type.Boolean({ description: "This call killed the delegate's window." }),
|
|
100
|
+
});
|
|
101
|
+
|
|
48
102
|
/** A tool as registered; `execute` takes the tool's own parameters, which the registration call does not check. */
|
|
49
103
|
type ToolDef = {
|
|
50
104
|
name: string;
|
|
51
105
|
label: string;
|
|
52
106
|
description: string;
|
|
53
107
|
parameters: unknown;
|
|
108
|
+
outputSchema: unknown;
|
|
109
|
+
annotations: { readOnlyHint: boolean; destructiveHint: boolean; idempotentHint: boolean; openWorldHint: boolean };
|
|
54
110
|
execute: (...args: never[]) => Promise<ReturnType<typeof toolResult>>;
|
|
55
111
|
};
|
|
56
112
|
|
|
@@ -149,7 +205,7 @@ function viewText({ record, state, name, sessionPath, lastActive }: View): strin
|
|
|
149
205
|
}
|
|
150
206
|
|
|
151
207
|
/** The structured details for one delegation. */
|
|
152
|
-
function viewDetails({ record, name, sessionPath, lastActive }: View): Record<string, unknown> {
|
|
208
|
+
function viewDetails({ record, state, name, sessionPath, lastActive }: View): Record<string, unknown> {
|
|
153
209
|
return {
|
|
154
210
|
id: record.id,
|
|
155
211
|
name,
|
|
@@ -159,6 +215,7 @@ function viewDetails({ record, name, sessionPath, lastActive }: View): Record<st
|
|
|
159
215
|
autoExit: record.autoExit === true,
|
|
160
216
|
windowId: record.windowId,
|
|
161
217
|
windowName: record.windowName,
|
|
218
|
+
state,
|
|
162
219
|
closed: record.closed === true,
|
|
163
220
|
closedWithoutResult: record.gone === true && record.result === undefined && record.closed !== true,
|
|
164
221
|
status: record.result?.status,
|
|
@@ -267,6 +324,8 @@ function statusTool(results: Results, tmux: TmuxClient, readingErrors: () => Rea
|
|
|
267
324
|
parameters: Type.Object({
|
|
268
325
|
id: Type.Optional(Type.String({ description: "One delegation id; omit to report every delegation." })),
|
|
269
326
|
}),
|
|
327
|
+
outputSchema: StatusSchema,
|
|
328
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, ...CLOSED_WORLD },
|
|
270
329
|
async execute(
|
|
271
330
|
_toolCallId: string,
|
|
272
331
|
params: { id?: string },
|
|
@@ -310,6 +369,9 @@ function closeTool(results: Results, tmux: TmuxClient): ToolDef {
|
|
|
310
369
|
parameters: Type.Object({
|
|
311
370
|
id: Type.String({ description: "The delegation id to close." }),
|
|
312
371
|
}),
|
|
372
|
+
outputSchema: CloseSchema,
|
|
373
|
+
// Kills a window; a second close finds it recorded and does nothing.
|
|
374
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, ...CLOSED_WORLD },
|
|
313
375
|
async execute(
|
|
314
376
|
_toolCallId: string,
|
|
315
377
|
params: { id: string },
|
|
@@ -318,8 +380,15 @@ function closeTool(results: Results, tmux: TmuxClient): ToolDef {
|
|
|
318
380
|
_ctx: ExtensionContext,
|
|
319
381
|
): Promise<ReturnType<typeof toolResult>> {
|
|
320
382
|
const [record] = selectRecords(results, params.id);
|
|
321
|
-
|
|
322
|
-
|
|
383
|
+
const closed = (alreadyClosed: boolean, killed: boolean) => ({
|
|
384
|
+
id: record.id,
|
|
385
|
+
windowId: record.windowId,
|
|
386
|
+
windowName: record.windowName,
|
|
387
|
+
closed: true,
|
|
388
|
+
alreadyClosed,
|
|
389
|
+
killed,
|
|
390
|
+
});
|
|
391
|
+
if (record.closed === true) return toolResult(`Delegation ${record.id} is already closed.`, closed(true, false));
|
|
323
392
|
let windows: TmuxWindow[];
|
|
324
393
|
try {
|
|
325
394
|
windows = await tmux.listWindows();
|
|
@@ -339,11 +408,7 @@ function closeTool(results: Results, tmux: TmuxClient): ToolDef {
|
|
|
339
408
|
const how = alive
|
|
340
409
|
? `killed window ${record.windowName} (${record.windowId})`
|
|
341
410
|
: `its window ${record.windowName} was already gone`;
|
|
342
|
-
return toolResult(`Closed delegation ${record.id}: ${how}.`,
|
|
343
|
-
id: record.id,
|
|
344
|
-
windowId: record.windowId,
|
|
345
|
-
killed: alive,
|
|
346
|
-
});
|
|
411
|
+
return toolResult(`Closed delegation ${record.id}: ${how}.`, closed(false, alive));
|
|
347
412
|
},
|
|
348
413
|
};
|
|
349
414
|
}
|