pi-squire 0.6.0 → 0.7.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 CHANGED
@@ -6,6 +6,24 @@ follows [Semantic Versioning](https://semver.org/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.7.0] - 2026-10-19
10
+
11
+ ### Added
12
+
13
+ - `delegate`, `delegation_status` and `delegation_close` declare an
14
+ `outputSchema` and return matching `structuredContent`, so a codemode
15
+ script gets objects instead of text. `delegation_status` gives each
16
+ delegation its derived `state` (`running`, `done`, `gone`, `closed`,
17
+ `window-closed` or `unknown`), also added to its details.
18
+ `delegation_close` returns one shape on every path:
19
+ `{ id, windowId, windowName, closed, alreadyClosed, killed }`; its details
20
+ gain the fields each path lacked. The model-facing text is unchanged.
21
+ - The three tools declare MCP-style `annotations`: `delegation_status` is
22
+ read-only and idempotent; `delegation_close` is destructive and idempotent;
23
+ `delegate` is neither, and is the only one that reaches an open world,
24
+ since the delegate it starts calls a remote model and runs tools
25
+ unattended.
26
+
9
27
  ## [0.6.0] - 2026-10-15
10
28
 
11
29
  ### Added
@@ -239,7 +257,8 @@ author's dotfiles; the changes below are against that copy.
239
257
 
240
258
  - The published file list includes `child.ts`, which `index.ts` imports.
241
259
 
242
- [Unreleased]: https://github.com/gvanderclay/pi-squire/compare/v0.6.0...HEAD
260
+ [Unreleased]: https://github.com/gvanderclay/pi-squire/compare/v0.7.0...HEAD
261
+ [0.7.0]: https://github.com/gvanderclay/pi-squire/compare/v0.6.0...v0.7.0
243
262
  [0.6.0]: https://github.com/gvanderclay/pi-squire/compare/v0.5.1...v0.6.0
244
263
  [0.5.1]: https://github.com/gvanderclay/pi-squire/compare/v0.5.0...v0.5.1
245
264
  [0.5.0]: https://github.com/gvanderclay/pi-squire/compare/v0.4.0...v0.5.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-squire",
3
- "version": "0.6.0",
3
+ "version": "0.7.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",
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/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
- if (record.closed === true)
322
- return toolResult(`Delegation ${record.id} is already closed.`, { id: record.id, closed: true });
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
  }