@ian-pascoe/pi-minimal-subagents 0.11.0 → 0.12.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 CHANGED
@@ -29,7 +29,7 @@ filter in `~/.pi/agent/settings.json` using the repository-relative path:
29
29
  ```
30
30
 
31
31
  From this package checkout, load the source directly with
32
- `pi -e ./src/index.ts`. Requires Node `>=22.19.0` and Pi `>=0.85.1`.
32
+ `pi -e ./src/index.ts`. Requires Node `>=22.19.0` and Pi `>=0.99.0`.
33
33
 
34
34
  ## Configuration
35
35
 
@@ -164,7 +164,7 @@ example:
164
164
  "minimalSubagents": {
165
165
  "baseToolset": ["context_*"],
166
166
  "readToolset": ["read", "grep", "find", "ls"],
167
- "modifyToolset": ["bash", "edit", "write", "lsp", "dap"]
167
+ "modifyToolset": ["bash", "edit", "write", "lsp_*", "dap_*"]
168
168
  }
169
169
  }
170
170
  ```
@@ -203,8 +203,8 @@ Pattern expansion happens when a Child Agent is created. `/reload` applies
203
203
  settings to future launches without changing existing Launch Contracts. If a tool
204
204
  in an existing contract later disappears, normal restoration dependency checks
205
205
  still apply; the saved grant is not silently rewritten. Toolsets configure names,
206
- not tool operations: granting `lsp`, `dap`, or another multifunction tool grants
207
- that tool's available operations, regardless of preset name.
206
+ not tool operations: granting a multifunction tool grants that tool's available
207
+ operations, regardless of preset name.
208
208
 
209
209
  Child Agents load Pi's built-in `llama.cpp`, `codemode`, `tool-search`, and `mcp`
210
210
  extensions unless settings disable them (`-builtin:<name>`); each child connects
@@ -260,7 +260,11 @@ tools while the Launch Contract continues to record the originally granted
260
260
  capability names.
261
261
 
262
262
  `agent_message` reports whether a message was delivered through an active
263
- parent wait, queued for the recipient, or failed. `subagent_wait` can return an
263
+ parent wait, queued for the recipient, or failed. A failed delivery and a
264
+ partially failed `subagent_delete` return an error result (`isError: true`) that
265
+ still carries the declared structured output, so the model sees an error while
266
+ codemode scripts receive the `failures` or `error` data instead of a data-less
267
+ rejection. `subagent_wait` can return an
264
268
  intermediate Wait Event containing a Coordination Message before the child turn
265
269
  settles. That event claims only its message, so later unconsumed messages and the
266
270
  terminal result retain automatic fallback. If the turn has already settled, one
@@ -323,6 +327,10 @@ process-local fork handoff is lost, recovery reads only the destination's
323
327
  selected branch and proceeds only when its canonical `parentSession` proves the
324
328
  source file; it never substitutes the source session's newer head.
325
329
 
330
+ ## Tool annotations
331
+
332
+ The coordinator tools declare MCP-style `annotations`, which Pi reports through `pi.getAllTools()` for permission extensions and does not send to model providers. `subagent` is destructive and open-world because a Child Agent can use any tool it is granted. `agent_message` is not read-only but is non-destructive and closed-world. `subagent_cancel` is non-destructive and idempotent; `subagent_delete` is destructive and idempotent. `subagent_status` and `subagent_wait` are read-only (`subagent_wait` consumes queued deliveries, so it is not idempotent).
333
+
326
334
  ## Status and TUI
327
335
 
328
336
  In TUI mode, `/subagents` or `/subagents status` opens a large, centered,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ian-pascoe/pi-minimal-subagents",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "private": false,
5
5
  "description": "Persistent nested subagents with bounded delegation for Pi",
6
6
  "keywords": [
@@ -31,14 +31,14 @@
31
31
  "provenance": true
32
32
  },
33
33
  "dependencies": {
34
- "@ian-pascoe/pi-utils": "^0.3.0",
34
+ "@ian-pascoe/pi-utils": "^0.3.1",
35
35
  "minimatch": "^10.2.6"
36
36
  },
37
37
  "peerDependencies": {
38
- "@earendil-works/pi-agent-core": "*",
39
- "@earendil-works/pi-ai": "*",
40
- "@earendil-works/pi-coding-agent": "*",
41
- "@earendil-works/pi-tui": "*",
38
+ "@earendil-works/pi-agent-core": ">=0.99.0",
39
+ "@earendil-works/pi-ai": ">=0.99.0",
40
+ "@earendil-works/pi-coding-agent": ">=0.99.0",
41
+ "@earendil-works/pi-tui": ">=0.99.0",
42
42
  "typebox": "*"
43
43
  },
44
44
  "engines": {
@@ -1,10 +1,13 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { pathToFileURL } from "node:url";
4
- import * as piCodingAgent from "@earendil-works/pi-coding-agent";
5
4
  import {
5
+ createCodemodeExtension,
6
+ createMcpExtension,
7
+ createToolSearchExtension,
6
8
  DefaultResourceLoader,
7
9
  type ExtensionFactory,
10
+ getPackageDir,
8
11
  SettingsManager,
9
12
  } from "@earendil-works/pi-coding-agent";
10
13
  import { Type } from "typebox";
@@ -25,9 +28,6 @@ type InlineExtension = NonNullable<
25
28
  ConstructorParameters<typeof DefaultResourceLoader>[0]["extensionFactories"]
26
29
  >[number];
27
30
 
28
- // SAFETY: Older Pi hosts lack these exports; the namespace yields undefined rather than failing.
29
- const sdk = piCodingAgent as Partial<typeof piCodingAgent>;
30
-
31
31
  const ExtensionModuleSchema = Type.Object({
32
32
  default: Type.Function([Type.Unknown()], Type.Unknown()),
33
33
  });
@@ -37,8 +37,7 @@ const LLAMA = "llama.cpp";
37
37
 
38
38
  /** Pi ships a file-backed llama.cpp factory but does not export it; undefined when it is absent. */
39
39
  function piLlamaExtensionPath(): string | undefined {
40
- if (!sdk.getPackageDir) return undefined;
41
- const path = join(sdk.getPackageDir(), "dist", "extensions", "llama", "index.js");
40
+ const path = join(getPackageDir(), "dist", "extensions", "llama", "index.js");
42
41
  return existsSync(path) ? path : undefined;
43
42
  }
44
43
 
@@ -62,20 +61,18 @@ export function childProviderAvailable(provider: string): boolean {
62
61
  /** Child scripts must not call models outside their Launch Contract. */
63
62
  export const CHILD_CODEMODE_MODELS = false;
64
63
 
65
- /** Pi's built-in extensions (Pi 0.99+) in Pi's order; older hosts have none, so neither do children. */
64
+ /** Pi's built-in extensions in Pi's order, so children match the root's built-in surface. */
66
65
  export function childBuiltinExtensions(): InlineExtension[] {
67
- const createCodemode = sdk.createCodemodeExtension;
68
66
  return [
69
67
  ...llamaExtension(),
70
- ...(
71
- [
72
- ["codemode", createCodemode && (() => createCodemode({ models: CHILD_CODEMODE_MODELS }))],
73
- ["tool-search", sdk.createToolSearchExtension],
74
- ["mcp", sdk.createMcpExtension],
75
- ] as const
76
- ).flatMap(([name, create]) =>
77
- create ? [{ name, factory: create(), builtin: true, replaceable: true }] : [],
78
- ),
68
+ {
69
+ name: "codemode",
70
+ factory: createCodemodeExtension({ models: CHILD_CODEMODE_MODELS }),
71
+ builtin: true,
72
+ replaceable: true,
73
+ },
74
+ { name: "tool-search", factory: createToolSearchExtension(), builtin: true, replaceable: true },
75
+ { name: "mcp", factory: createMcpExtension(), builtin: true, replaceable: true },
79
76
  ];
80
77
  }
81
78
 
@@ -130,13 +130,31 @@ function structuredToolResult<TDetails extends CoordinatorToolResultDetails>(
130
130
  };
131
131
  }
132
132
 
133
- function failedStructuredOperation(prefix: string, result: DeleteResult): never {
134
- const json = JSON.stringify(result);
135
- const truncated = truncateHead(json, {
136
- maxBytes: DEFAULT_MAX_BYTES,
137
- maxLines: DEFAULT_MAX_LINES,
138
- });
139
- throw new Error(withTroubleshootingHint(`${prefix}: ${truncated.content}`));
133
+ /**
134
+ * Report a failed operation that still carries declared output: the model sees an error and
135
+ * codemode scripts receive `structuredContent` instead of a data-less rejection.
136
+ */
137
+ function failedStructuredToolResult<TDetails extends CoordinatorToolResultDetails>(
138
+ prefix: string,
139
+ result: TDetails,
140
+ options: { troubleshootingHint: boolean },
141
+ ): AgentToolResult<TDetails> & { isError: true } {
142
+ const success = structuredToolResult(result);
143
+ const text = `${prefix}:\n${textOf(success)}`;
144
+ return {
145
+ ...success,
146
+ content: [
147
+ {
148
+ type: "text" as const,
149
+ text: options.troubleshootingHint ? withTroubleshootingHint(text) : text,
150
+ },
151
+ ],
152
+ isError: true,
153
+ };
154
+ }
155
+
156
+ function textOf(result: AgentToolResult<unknown>): string {
157
+ return result.content.map((part) => (part.type === "text" ? part.text : "")).join("\n");
140
158
  }
141
159
 
142
160
  function callerSourceTurnId(
@@ -164,6 +182,12 @@ export function createCoordinatorToolDefinitions(
164
182
  promptSnippet: "Spawn a persistent child with a prefix-free root-child ID",
165
183
  promptGuidelines: modelRolePromptGuidelines,
166
184
  parameters: options.schemas.subagent,
185
+ annotations: {
186
+ readOnlyHint: false,
187
+ destructiveHint: true,
188
+ idempotentHint: false,
189
+ openWorldHint: true,
190
+ },
167
191
  async execute(_toolCallId, parameters, _signal, _onUpdate, context) {
168
192
  return runCoordinatorToolActivity(options, async () => {
169
193
  const result = await options.coordinator.spawn(
@@ -191,6 +215,12 @@ export function createCoordinatorToolDefinitions(
191
215
  "Send one mid-turn coordination message to a direct parent, direct sibling, or direct child. The result says whether it was delivered through an active wait, queued for the recipient, or failed.",
192
216
  promptSnippet: "Coordinate required mid-turn action with one adjacent agent",
193
217
  parameters: options.schemas.agent_message,
218
+ annotations: {
219
+ readOnlyHint: false,
220
+ destructiveHint: false,
221
+ idempotentHint: false,
222
+ openWorldHint: false,
223
+ },
194
224
  async execute(toolCallId, parameters) {
195
225
  return runCoordinatorToolActivity(options, async () => {
196
226
  const result = await options.coordinator.sendAgentMessage(
@@ -201,7 +231,12 @@ export function createCoordinatorToolDefinitions(
201
231
  },
202
232
  callerSourceTurnId(options.coordinator, options.callerId, toolCallId),
203
233
  );
204
- return structuredToolResult(result);
234
+ return result.disposition === "failed"
235
+ ? failedStructuredToolResult("Minimal subagents message delivery failed", result, {
236
+ // The coordinator already appends the hint to `error`.
237
+ troubleshootingHint: false,
238
+ })
239
+ : structuredToolResult(result);
205
240
  });
206
241
  },
207
242
  ...createCoordinatorToolRendering("agent_message"),
@@ -214,6 +249,12 @@ export function createCoordinatorToolDefinitions(
214
249
  "Wait for one direct child's oldest observable turn, or select an exact retained turn_id. An active child may first return event=message; later unconsumed items still fall back automatically. An already settled turn returns event=turn once with queued messages in messages. Timeout returns event=timeout with detailed child status and never cancels the child.",
215
250
  promptSnippet: "Wait for one direct child's exact turn",
216
251
  parameters: options.schemas.subagent_wait,
252
+ annotations: {
253
+ readOnlyHint: true,
254
+ destructiveHint: false,
255
+ idempotentHint: false,
256
+ openWorldHint: false,
257
+ },
217
258
  async execute(_toolCallId, parameters, signal, onUpdate) {
218
259
  const startedAt = Date.now();
219
260
  const updateWaitingResult = () =>
@@ -262,6 +303,12 @@ export function createCoordinatorToolDefinitions(
262
303
  "List direct children when agent_id is omitted, or inspect one direct child's launch contract, result, usage, dependencies, and bounded recent activity including message text and reasoning.",
263
304
  promptSnippet: "Inspect direct child state",
264
305
  parameters: options.schemas.subagent_status,
306
+ annotations: {
307
+ readOnlyHint: true,
308
+ destructiveHint: false,
309
+ idempotentHint: true,
310
+ openWorldHint: false,
311
+ },
265
312
  async execute(_toolCallId, parameters) {
266
313
  return runCoordinatorToolActivity(options, () =>
267
314
  structuredToolResult(options.coordinator.status(options.callerId, parameters.agent_id)),
@@ -277,6 +324,12 @@ export function createCoordinatorToolDefinitions(
277
324
  "Abort active work for one direct child while preserving sessions for later continuation. Recursive cancellation includes its subtree and defaults to true.",
278
325
  promptSnippet: "Cancel active subagent turns without deleting sessions",
279
326
  parameters: options.schemas.subagent_cancel,
327
+ annotations: {
328
+ readOnlyHint: false,
329
+ destructiveHint: false,
330
+ idempotentHint: true,
331
+ openWorldHint: false,
332
+ },
280
333
  async execute(_toolCallId, parameters) {
281
334
  return runCoordinatorToolActivity(options, async () =>
282
335
  structuredToolResult(
@@ -298,6 +351,12 @@ export function createCoordinatorToolDefinitions(
298
351
  "Delete one direct child's persistent session and retain durable ID tombstones. Recursive deletion includes its subtree and defaults to true.",
299
352
  promptSnippet: "Delete subagent sessions and tombstone their IDs",
300
353
  parameters: options.schemas.subagent_delete,
354
+ annotations: {
355
+ readOnlyHint: false,
356
+ destructiveHint: true,
357
+ idempotentHint: true,
358
+ openWorldHint: false,
359
+ },
301
360
  async execute(_toolCallId, parameters) {
302
361
  return runCoordinatorToolActivity(options, async () => {
303
362
  const result = await options.coordinator.delete(
@@ -309,7 +368,9 @@ export function createCoordinatorToolDefinitions(
309
368
  options.onAttention?.(
310
369
  `Minimal subagents deletion partially failed for ${parameters.agent_id}`,
311
370
  );
312
- failedStructuredOperation("Minimal subagents deletion partially failed", result);
371
+ return failedStructuredToolResult("Minimal subagents deletion partially failed", result, {
372
+ troubleshootingHint: true,
373
+ });
313
374
  }
314
375
  return structuredToolResult(result);
315
376
  });