@esso0428/pi-subagents 0.17.5 → 0.17.7

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.
Files changed (263) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/CONTRIBUTING.md +4 -0
  3. package/README.md +1 -1
  4. package/dist/abortable.d.ts +13 -0
  5. package/dist/abortable.d.ts.map +1 -0
  6. package/dist/abortable.js +43 -0
  7. package/dist/abortable.js.map +1 -0
  8. package/dist/agent-color.d.ts +36 -0
  9. package/dist/agent-color.d.ts.map +1 -0
  10. package/dist/agent-color.js +124 -0
  11. package/dist/agent-color.js.map +1 -0
  12. package/dist/agent-file-toggle.d.ts +126 -0
  13. package/dist/agent-file-toggle.d.ts.map +1 -0
  14. package/dist/agent-file-toggle.js +259 -0
  15. package/dist/agent-file-toggle.js.map +1 -0
  16. package/dist/agent-history.d.ts +4 -0
  17. package/dist/agent-history.d.ts.map +1 -1
  18. package/dist/agent-history.js +47 -1
  19. package/dist/agent-history.js.map +1 -1
  20. package/dist/agent-manager.d.ts +370 -56
  21. package/dist/agent-manager.d.ts.map +1 -1
  22. package/dist/agent-manager.js +1123 -409
  23. package/dist/agent-manager.js.map +1 -1
  24. package/dist/agent-runner.d.ts +100 -10
  25. package/dist/agent-runner.d.ts.map +1 -1
  26. package/dist/agent-runner.js +166 -21
  27. package/dist/agent-runner.js.map +1 -1
  28. package/dist/agent-types.d.ts +57 -5
  29. package/dist/agent-types.d.ts.map +1 -1
  30. package/dist/agent-types.js +164 -32
  31. package/dist/agent-types.js.map +1 -1
  32. package/dist/child-context.d.ts +3 -0
  33. package/dist/child-context.d.ts.map +1 -0
  34. package/dist/child-context.js +13 -0
  35. package/dist/child-context.js.map +1 -0
  36. package/dist/cross-extension-rpc.d.ts +23 -3
  37. package/dist/cross-extension-rpc.d.ts.map +1 -1
  38. package/dist/cross-extension-rpc.js +79 -17
  39. package/dist/cross-extension-rpc.js.map +1 -1
  40. package/dist/custom-agents.d.ts +38 -1
  41. package/dist/custom-agents.d.ts.map +1 -1
  42. package/dist/custom-agents.js +164 -12
  43. package/dist/custom-agents.js.map +1 -1
  44. package/dist/index.d.ts +34 -0
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +1908 -495
  47. package/dist/index.js.map +1 -1
  48. package/dist/invocation-config.d.ts +87 -2
  49. package/dist/invocation-config.d.ts.map +1 -1
  50. package/dist/invocation-config.js +71 -3
  51. package/dist/invocation-config.js.map +1 -1
  52. package/dist/mention-clone.d.ts +88 -0
  53. package/dist/mention-clone.d.ts.map +1 -0
  54. package/dist/mention-clone.js +154 -0
  55. package/dist/mention-clone.js.map +1 -0
  56. package/dist/mention.d.ts +82 -0
  57. package/dist/mention.d.ts.map +1 -0
  58. package/dist/mention.js +132 -0
  59. package/dist/mention.js.map +1 -0
  60. package/dist/model-resolver.d.ts +17 -0
  61. package/dist/model-resolver.d.ts.map +1 -1
  62. package/dist/model-resolver.js +15 -0
  63. package/dist/model-resolver.js.map +1 -1
  64. package/dist/model-scope.d.ts +50 -0
  65. package/dist/model-scope.d.ts.map +1 -0
  66. package/dist/model-scope.js +49 -0
  67. package/dist/model-scope.js.map +1 -0
  68. package/dist/nested-tools.d.ts +57 -0
  69. package/dist/nested-tools.d.ts.map +1 -0
  70. package/dist/nested-tools.js +301 -0
  71. package/dist/nested-tools.js.map +1 -0
  72. package/dist/output-file.d.ts +22 -3
  73. package/dist/output-file.d.ts.map +1 -1
  74. package/dist/output-file.js +58 -7
  75. package/dist/output-file.js.map +1 -1
  76. package/dist/prompts.d.ts +23 -0
  77. package/dist/prompts.d.ts.map +1 -1
  78. package/dist/prompts.js +20 -2
  79. package/dist/prompts.js.map +1 -1
  80. package/dist/schedule.d.ts.map +1 -1
  81. package/dist/schedule.js +36 -15
  82. package/dist/schedule.js.map +1 -1
  83. package/dist/settings.d.ts +228 -2
  84. package/dist/settings.d.ts.map +1 -1
  85. package/dist/settings.js +94 -0
  86. package/dist/settings.js.map +1 -1
  87. package/dist/status-note.d.ts +49 -1
  88. package/dist/status-note.d.ts.map +1 -1
  89. package/dist/status-note.js +62 -1
  90. package/dist/status-note.js.map +1 -1
  91. package/dist/structured-output.d.ts +62 -0
  92. package/dist/structured-output.d.ts.map +1 -0
  93. package/dist/structured-output.js +113 -0
  94. package/dist/structured-output.js.map +1 -0
  95. package/dist/types.d.ts +176 -10
  96. package/dist/types.d.ts.map +1 -1
  97. package/dist/ui/agent-mention.d.ts +83 -0
  98. package/dist/ui/agent-mention.d.ts.map +1 -0
  99. package/dist/ui/agent-mention.js +188 -0
  100. package/dist/ui/agent-mention.js.map +1 -0
  101. package/dist/ui/agent-widget.d.ts +96 -75
  102. package/dist/ui/agent-widget.d.ts.map +1 -1
  103. package/dist/ui/agent-widget.js +397 -420
  104. package/dist/ui/agent-widget.js.map +1 -1
  105. package/dist/ui/conversation-blocks.d.ts.map +1 -1
  106. package/dist/ui/conversation-blocks.js +6 -0
  107. package/dist/ui/conversation-blocks.js.map +1 -1
  108. package/dist/ui/conversation-timeline.d.ts +10 -2
  109. package/dist/ui/conversation-timeline.d.ts.map +1 -1
  110. package/dist/ui/conversation-timeline.js +130 -23
  111. package/dist/ui/conversation-timeline.js.map +1 -1
  112. package/dist/ui/conversation-viewer.d.ts +20 -5
  113. package/dist/ui/conversation-viewer.d.ts.map +1 -1
  114. package/dist/ui/conversation-viewer.js +274 -73
  115. package/dist/ui/conversation-viewer.js.map +1 -1
  116. package/dist/ui/fleet-list.d.ts +198 -0
  117. package/dist/ui/fleet-list.d.ts.map +1 -0
  118. package/dist/ui/fleet-list.js +487 -0
  119. package/dist/ui/fleet-list.js.map +1 -0
  120. package/dist/ui/schedule-menu.d.ts.map +1 -1
  121. package/dist/ui/schedule-menu.js +6 -7
  122. package/dist/ui/schedule-menu.js.map +1 -1
  123. package/dist/ui/select-item.d.ts +28 -0
  124. package/dist/ui/select-item.d.ts.map +1 -0
  125. package/dist/ui/select-item.js +35 -0
  126. package/dist/ui/select-item.js.map +1 -0
  127. package/dist/ui/workflow-card.d.ts +176 -0
  128. package/dist/ui/workflow-card.d.ts.map +1 -0
  129. package/dist/ui/workflow-card.js +333 -0
  130. package/dist/ui/workflow-card.js.map +1 -0
  131. package/dist/ui/workflow-dialog.d.ts +306 -0
  132. package/dist/ui/workflow-dialog.d.ts.map +1 -0
  133. package/dist/ui/workflow-dialog.js +844 -0
  134. package/dist/ui/workflow-dialog.js.map +1 -0
  135. package/dist/ui/workflow-menu.d.ts +61 -0
  136. package/dist/ui/workflow-menu.d.ts.map +1 -0
  137. package/dist/ui/workflow-menu.js +148 -0
  138. package/dist/ui/workflow-menu.js.map +1 -0
  139. package/dist/usage.d.ts +86 -1
  140. package/dist/usage.d.ts.map +1 -1
  141. package/dist/usage.js +72 -1
  142. package/dist/usage.js.map +1 -1
  143. package/dist/workflow/collisions.d.ts +96 -0
  144. package/dist/workflow/collisions.d.ts.map +1 -0
  145. package/dist/workflow/collisions.js +89 -0
  146. package/dist/workflow/collisions.js.map +1 -0
  147. package/dist/workflow/entry.d.ts +33 -0
  148. package/dist/workflow/entry.d.ts.map +1 -0
  149. package/dist/workflow/entry.js +30 -0
  150. package/dist/workflow/entry.js.map +1 -0
  151. package/dist/workflow/host.d.ts +63 -0
  152. package/dist/workflow/host.d.ts.map +1 -0
  153. package/dist/workflow/host.js +363 -0
  154. package/dist/workflow/host.js.map +1 -0
  155. package/dist/workflow/journal.d.ts +98 -0
  156. package/dist/workflow/journal.d.ts.map +1 -0
  157. package/dist/workflow/journal.js +121 -0
  158. package/dist/workflow/journal.js.map +1 -0
  159. package/dist/workflow/json-schema.d.ts +52 -0
  160. package/dist/workflow/json-schema.d.ts.map +1 -0
  161. package/dist/workflow/json-schema.js +112 -0
  162. package/dist/workflow/json-schema.js.map +1 -0
  163. package/dist/workflow/meta.d.ts +68 -0
  164. package/dist/workflow/meta.d.ts.map +1 -0
  165. package/dist/workflow/meta.js +318 -0
  166. package/dist/workflow/meta.js.map +1 -0
  167. package/dist/workflow/progress.d.ts +225 -0
  168. package/dist/workflow/progress.d.ts.map +1 -0
  169. package/dist/workflow/progress.js +362 -0
  170. package/dist/workflow/progress.js.map +1 -0
  171. package/dist/workflow/runtime.d.ts +335 -0
  172. package/dist/workflow/runtime.d.ts.map +1 -0
  173. package/dist/workflow/runtime.js +831 -0
  174. package/dist/workflow/runtime.js.map +1 -0
  175. package/dist/workflow/saved.d.ts +91 -0
  176. package/dist/workflow/saved.d.ts.map +1 -0
  177. package/dist/workflow/saved.js +204 -0
  178. package/dist/workflow/saved.js.map +1 -0
  179. package/dist/workflow/task.d.ts +137 -0
  180. package/dist/workflow/task.d.ts.map +1 -0
  181. package/dist/workflow/task.js +208 -0
  182. package/dist/workflow/task.js.map +1 -0
  183. package/dist/workflow/tool-description.d.ts +39 -0
  184. package/dist/workflow/tool-description.d.ts.map +1 -0
  185. package/dist/workflow/tool-description.js +200 -0
  186. package/dist/workflow/tool-description.js.map +1 -0
  187. package/dist/workflow/worker-source.d.ts +48 -0
  188. package/dist/workflow/worker-source.d.ts.map +1 -0
  189. package/dist/workflow/worker-source.js +779 -0
  190. package/dist/workflow/worker-source.js.map +1 -0
  191. package/dist/worktree.d.ts +10 -3
  192. package/dist/worktree.d.ts.map +1 -1
  193. package/dist/worktree.js +58 -54
  194. package/dist/worktree.js.map +1 -1
  195. package/dist/xml.d.ts +11 -0
  196. package/dist/xml.d.ts.map +1 -0
  197. package/dist/xml.js +13 -0
  198. package/dist/xml.js.map +1 -0
  199. package/docs/rpc.md +183 -0
  200. package/docs/superpowers/plans/2026-09-30-conversation-viewer-scrollbar.md +216 -0
  201. package/docs/superpowers/plans/2026-09-30-upstream-event-workflow-partial-history.md +195 -0
  202. package/docs/superpowers/specs/2026-09-30-conversation-viewer-scrollbar-design.md +82 -0
  203. package/docs/superpowers/specs/2026-09-30-upstream-event-workflow-partial-history-design.md +49 -0
  204. package/docs/workflows.md +437 -0
  205. package/examples/agent-tool-description.md +7 -7
  206. package/examples/workflows/compose.js +51 -0
  207. package/examples/workflows/fan-out-audit.js +47 -0
  208. package/examples/workflows/gated-fix.js +60 -0
  209. package/examples/workflows/lib/count-child.js +27 -0
  210. package/examples/workflows/review-panel.js +63 -0
  211. package/examples/workflows/structured-findings.js +78 -0
  212. package/package.json +1 -1
  213. package/src/abortable.ts +43 -0
  214. package/src/agent-color.ts +161 -0
  215. package/src/agent-file-toggle.ts +269 -0
  216. package/src/agent-history.ts +54 -2
  217. package/src/agent-manager.ts +1263 -402
  218. package/src/agent-runner.ts +251 -27
  219. package/src/agent-types.ts +188 -32
  220. package/src/child-context.ts +15 -0
  221. package/src/cross-extension-rpc.ts +96 -20
  222. package/src/custom-agents.ts +170 -13
  223. package/src/index.ts +2024 -537
  224. package/src/invocation-config.ts +118 -3
  225. package/src/mention-clone.ts +196 -0
  226. package/src/mention.ts +141 -0
  227. package/src/model-resolver.ts +18 -0
  228. package/src/model-scope.ts +70 -0
  229. package/src/nested-tools.ts +424 -0
  230. package/src/output-file.ts +61 -6
  231. package/src/prompts.ts +45 -2
  232. package/src/schedule.ts +35 -14
  233. package/src/settings.ts +312 -2
  234. package/src/status-note.ts +66 -1
  235. package/src/structured-output.ts +130 -0
  236. package/src/types.ts +177 -10
  237. package/src/ui/agent-mention.ts +216 -0
  238. package/src/ui/agent-widget.ts +389 -441
  239. package/src/ui/conversation-blocks.ts +6 -0
  240. package/src/ui/conversation-timeline.ts +139 -25
  241. package/src/ui/conversation-viewer.ts +284 -69
  242. package/src/ui/fleet-list.ts +558 -0
  243. package/src/ui/schedule-menu.ts +9 -8
  244. package/src/ui/select-item.ts +45 -0
  245. package/src/ui/workflow-card.ts +470 -0
  246. package/src/ui/workflow-dialog.ts +1115 -0
  247. package/src/ui/workflow-menu.ts +193 -0
  248. package/src/usage.ts +109 -2
  249. package/src/workflow/collisions.ts +123 -0
  250. package/src/workflow/entry.ts +47 -0
  251. package/src/workflow/host.ts +403 -0
  252. package/src/workflow/journal.ts +164 -0
  253. package/src/workflow/json-schema.ts +128 -0
  254. package/src/workflow/meta.ts +325 -0
  255. package/src/workflow/progress.ts +550 -0
  256. package/src/workflow/runtime.ts +1219 -0
  257. package/src/workflow/saved.ts +217 -0
  258. package/src/workflow/task.ts +302 -0
  259. package/src/workflow/tool-description.ts +200 -0
  260. package/src/workflow/worker-source.ts +781 -0
  261. package/src/worktree.ts +69 -55
  262. package/src/xml.ts +13 -0
  263. package/vitest.config.ts +0 -18
@@ -6,7 +6,7 @@ import { readFileSync } from "node:fs";
6
6
  import { homedir } from "node:os";
7
7
  import { basename, dirname, isAbsolute, join, resolve } from "node:path";
8
8
  import type { Model } from "@earendil-works/pi-ai";
9
- import type { ExtensionContext, LoadExtensionsResult, ModelRuntime } from "@earendil-works/pi-coding-agent";
9
+ import type { ExtensionContext, LoadExtensionsResult } from "@earendil-works/pi-coding-agent";
10
10
  import {
11
11
  type AgentSession,
12
12
  type AgentSessionEvent,
@@ -18,14 +18,18 @@ import {
18
18
  SettingsManager,
19
19
  } from "@earendil-works/pi-coding-agent";
20
20
  import { BUILTIN_TOOL_NAMES, getAgentConfig, getConfig, getMemoryToolNames, getReadOnlyMemoryToolNames, getToolNamesForType } from "./agent-types.js";
21
+ import { runInChildSessionContext } from "./child-context.js";
21
22
  import { buildParentContext, extractText } from "./context.js";
22
23
  import { DEFAULT_AGENTS } from "./default-agents.js";
23
24
  import { detectEnv } from "./env.js";
24
25
  import { buildMemoryBlock, buildReadOnlyMemoryBlock } from "./memory.js";
26
+ import { createNestedSubagentTools, getMaxSubagentDepth, type NestedAgentManager } from "./nested-tools.js";
25
27
  import { buildAgentPrompt, type PromptExtras } from "./prompts.js";
26
28
  import { preloadSkills } from "./skill-loader.js";
29
+ import { createStructuredCapture, createStructuredOutputTool, structuredRetryPrompt } from "./structured-output.js";
27
30
  import type { SubagentType, ThinkingLevel } from "./types.js";
28
- import { createTrackedWriteTool } from "./write-execution.js";
31
+ import type { LifetimeUsage } from "./usage.js";
32
+ import type { CompiledSchema } from "./workflow/json-schema.js";
29
33
 
30
34
  /**
31
35
  * Tool names registered by THIS extension. Single source of truth so the
@@ -35,6 +39,7 @@ import { createTrackedWriteTool } from "./write-execution.js";
35
39
  */
36
40
  export const SUBAGENT_TOOL_NAMES = {
37
41
  AGENT: "Agent",
42
+ WORKFLOW: "SubagentWorkflow",
38
43
  GET_RESULT: "get_subagent_result",
39
44
  STEER: "steer_subagent",
40
45
  } as const;
@@ -231,9 +236,18 @@ export function installExtensionToolScope(
231
236
  disallowedSet: Set<string> | undefined;
232
237
  extNames: Set<string>;
233
238
  narrowing: Map<string, Set<string>>;
239
+ /**
240
+ * Injected `customTools` to keep active regardless of the built-in list.
241
+ *
242
+ * Two kinds arrive here and they are blocked for different reasons: opt-in
243
+ * nested-delegation tools share EXCLUDED_TOOL_NAMES' names, and
244
+ * StructuredOutput is simply not a built-in, so neither survives a `keep`
245
+ * seeded from `toolNames`.
246
+ */
247
+ readmitToolNames: Set<string>;
234
248
  },
235
249
  ): void {
236
- const { loader, toolNames, disallowedSet, extNames, narrowing } = ctx;
250
+ const { loader, toolNames, disallowedSet, extNames, narrowing, readmitToolNames } = ctx;
237
251
 
238
252
  // The names allowed right now. Mirrors the `ext:` opt-in flip: when any `ext:`
239
253
  // selector is present, extension tools become an explicit allowlist — a loaded
@@ -255,6 +269,11 @@ export function installExtensionToolScope(
255
269
  }
256
270
  }
257
271
  for (const name of EXCLUDED_TOOL_NAMES) keep.delete(name);
272
+ // Injected tools are legitimately active for this agent — re-admit them so
273
+ // the renarrow keeps them in the active set and beforeToolCall doesn't
274
+ // block them. Already vetted against `disallowed_tools` by the caller,
275
+ // which is the only place that knows which kind may be taken back.
276
+ for (const name of readmitToolNames) keep.add(name);
258
277
  return keep;
259
278
  };
260
279
 
@@ -303,6 +322,33 @@ export function getDefaultMaxTurns(): number | undefined { return defaultMaxTurn
303
322
  /** Set the default max turns value. undefined or 0 = unlimited, otherwise minimum 1. */
304
323
  export function setDefaultMaxTurns(n: number | undefined): void { defaultMaxTurns = normalizeMaxTurns(n); }
305
324
 
325
+ /**
326
+ * The turn limit a run of `type` will actually enforce: an explicit value if the
327
+ * caller supplied one, else the agent's own `max_turns`, else the project
328
+ * default. `undefined` = unlimited.
329
+ *
330
+ * Exported because the widget's turn counter (`↻3≤20`) has to predict this
331
+ * before the run starts, and a second copy of the expression would drift from
332
+ * the one below that enforces it.
333
+ */
334
+ export function resolveEffectiveMaxTurns(type: string, explicit?: number): number | undefined {
335
+ return normalizeMaxTurns(explicit ?? getAgentConfig(type)?.maxTurns ?? defaultMaxTurns);
336
+ }
337
+
338
+ /**
339
+ * Project default for `persist_session`, from the `rememberAgents` setting.
340
+ * On by default: a persisted session is what lets `@handle` reopen an agent's
341
+ * conversation after its record has been evicted, which is the whole point of
342
+ * addressing an agent by a name that outlives one run. Per-agent frontmatter
343
+ * still overrides it in both directions.
344
+ */
345
+ let rememberAgents = true;
346
+
347
+ /** Whether subagent sessions are persisted by default. */
348
+ export function getRememberAgents(): boolean { return rememberAgents; }
349
+ /** Set whether subagent sessions are persisted by default. */
350
+ export function setRememberAgents(b: boolean): void { rememberAgents = b; }
351
+
306
352
  /** Additional turns allowed after the soft limit steer message. */
307
353
  let graceTurns = 5;
308
354
 
@@ -315,7 +361,7 @@ export function setGraceTurns(n: number): void { graceTurns = Math.max(1, n); }
315
361
  * Try to find the right model for an agent type.
316
362
  * Priority: explicit option > config.model > parent model.
317
363
  */
318
- function resolveDefaultModel(
364
+ export function resolveDefaultModel(
319
365
  parentModel: Model<any> | undefined,
320
366
  registry: { find(provider: string, modelId: string): Model<any> | undefined; getAvailable?(): Model<any>[] },
321
367
  configModel?: string,
@@ -359,8 +405,39 @@ export interface RunOptions {
359
405
  isolated?: boolean;
360
406
  inheritContext?: boolean;
361
407
  thinkingLevel?: ThinkingLevel;
408
+ /**
409
+ * Reopen this pi session file rather than starting an empty conversation.
410
+ * `createAgentSession` seeds itself from whatever its SessionManager holds,
411
+ * so pointing it at an existing file rehydrates that agent's history and the
412
+ * prompt continues it. Everything else — tools, model, system prompt, turn
413
+ * caps — is still resolved from the agent type, so the continuation runs
414
+ * under the type's *current* definition, not the one the original run used.
415
+ */
416
+ resumeSessionFile?: string;
417
+ /**
418
+ * True when another agent spawned this one. Only top-level agents get a
419
+ * handle, so only they can be reopened by name — which is the whole reason
420
+ * `rememberAgents` persists a session at all. A nested run's transcript would
421
+ * be unreachable by anything, so it stays in memory unless its own
422
+ * frontmatter asks otherwise.
423
+ */
424
+ nested?: boolean;
425
+ /**
426
+ * True when a workflow run spawned this agent. Its final text is the value
427
+ * `agent()` resolves to rather than a report a person reads, and the prompt
428
+ * says so — but only when `structuredOutput` is unset, since that child
429
+ * already has a `StructuredOutput` tool to answer through and two competing
430
+ * "this is how you return your answer" instructions is worse than one.
431
+ */
432
+ workflow?: boolean;
362
433
  /** Override working directory (e.g. for worktree isolation). */
363
434
  cwd?: string;
435
+ /**
436
+ * Directory the worktree copy was created from. Set only when `cwd` points
437
+ * into a worktree — the prompt then tells the agent to stay in the copy
438
+ * instead of following the inherited parent prompt back to the main tree.
439
+ */
440
+ worktreeBase?: string;
364
441
  /**
365
442
  * Where .pi config is discovered (project extensions, skills, pi settings,
366
443
  * agent memory). Default: same as the working directory. The manager sets
@@ -386,13 +463,33 @@ export interface RunOptions {
386
463
  * Called once per assistant message_end with that message's usage delta.
387
464
  * Lets callers maintain a lifetime accumulator that survives compaction
388
465
  * (which replaces session.state.messages and resets stats-derived sums).
466
+ *
467
+ * `cost` is pi's own `usage.cost.total` for that message — priced from the
468
+ * model's rates, so it is 0 (not missing) for a model pi has no pricing for.
469
+ * We never price anything ourselves; every dollar figure this extension shows
470
+ * or reports traces back to this field.
389
471
  */
390
- onAssistantUsage?: (usage: { input: number; output: number; cacheWrite: number }) => void;
472
+ onAssistantUsage?: (usage: LifetimeUsage) => void;
391
473
  /**
392
474
  * Called when the session successfully compacts. `tokensBefore` is upstream's
393
475
  * pre-compaction context size estimate. Aborted compactions don't fire.
394
476
  */
395
477
  onCompaction?: (info: { reason: "manual" | "threshold" | "overflow"; tokensBefore: number }) => void;
478
+ /**
479
+ * Make this child report through a `StructuredOutput` tool built from this
480
+ * schema, and put the validated payload on {@link RunResult.structuredJson}.
481
+ *
482
+ * Already compiled by the caller, so a schema this runtime cannot validate
483
+ * fails at the call that wrote it rather than inside the child.
484
+ */
485
+ structuredOutput?: CompiledSchema;
486
+ /** Runtime bridge for opt-in child-safe nested delegation. */
487
+ nestedRuntime?: {
488
+ manager: NestedAgentManager;
489
+ parentAgentId: string;
490
+ depth: number;
491
+ maxSubagentDepth?: number;
492
+ };
396
493
  }
397
494
 
398
495
  export interface RunResult {
@@ -412,6 +509,18 @@ export interface RunResult {
412
509
  * stop that produced text (a legitimate truncated answer).
413
510
  */
414
511
  failure?: string;
512
+ /**
513
+ * The validated `StructuredOutput` payload as canonical JSON, when the caller
514
+ * asked for a schema and the child produced one.
515
+ *
516
+ * Deliberately not folded into {@link responseText}: `record.result` picks up
517
+ * a worktree branch note on the way out, which would leave the caller with
518
+ * unparseable JSON, and merging the two would make "produced structured
519
+ * output" indistinguishable from "happened to answer in JSON".
520
+ */
521
+ structuredJson?: string;
522
+ /** Whether the extra structured-output prompt had to be sent. */
523
+ structuredRetried?: boolean;
415
524
  }
416
525
 
417
526
  /**
@@ -520,6 +629,8 @@ export async function runAgent(
520
629
 
521
630
  // Build prompt extras (memory, skill preloading)
522
631
  const extras: PromptExtras = {};
632
+ if (options.worktreeBase) extras.worktreeBase = options.worktreeBase;
633
+ if (options.workflow && !options.structuredOutput) extras.workflowChild = true;
523
634
 
524
635
  // Resolve extensions/skills: isolated overrides to false
525
636
  const extensions = options.isolated ? false : config.extensions;
@@ -646,7 +757,7 @@ export async function runAgent(
646
757
  systemPromptOverride: () => systemPrompt,
647
758
  appendSystemPromptOverride: () => [],
648
759
  });
649
- await loader.reload();
760
+ await runInChildSessionContext(() => loader.reload());
650
761
 
651
762
  // Plain entries in `tools:` are expected to be built-in names (extension tools
652
763
  // go through `ext:`), so an unknown name there is unambiguously a typo. Previously
@@ -729,6 +840,53 @@ export async function runAgent(
729
840
  ? new Set(agentConfig.disallowedTools)
730
841
  : undefined;
731
842
 
843
+ // Nested delegation tools (opt-in, ownership-scoped). Empty unless the agent
844
+ // set `allowed_subagents` and a nestedRuntime was provided — and never when
845
+ // isolated. Their names collide with EXCLUDED_TOOL_NAMES by design, so the
846
+ // scoping below re-admits them explicitly (registry deny + active-set narrow).
847
+ const effectiveMaxDepth = options.nestedRuntime?.maxSubagentDepth ?? getMaxSubagentDepth();
848
+ // At (or past) the cap this agent can never spawn, so it can never own a child
849
+ // to fetch from or steer either — inject nothing rather than three tools whose
850
+ // every call is an error. This is also what makes `maxSubagentDepth` 0/1 mean
851
+ // "nesting off" instead of "nesting always fails".
852
+ const nestedRuntime = options.nestedRuntime && options.nestedRuntime.depth < effectiveMaxDepth
853
+ ? options.nestedRuntime
854
+ : undefined;
855
+ const nestedTools = agentConfig?.allowedSubagents && nestedRuntime && !options.isolated
856
+ ? createNestedSubagentTools({
857
+ manager: nestedRuntime.manager,
858
+ pi: options.pi,
859
+ parentAgentId: nestedRuntime.parentAgentId,
860
+ depth: nestedRuntime.depth,
861
+ maxSubagentDepth: effectiveMaxDepth,
862
+ allowedSubagents: agentConfig.allowedSubagents,
863
+ configCwd,
864
+ })
865
+ : [];
866
+ const nestedToolNames = new Set(nestedTools.map(tool => tool.name));
867
+
868
+ // The `agent({ schema })` contract: this child reports its answer by calling
869
+ // StructuredOutput, and `structuredJson` below is what the caller reads. The
870
+ // schema was already compiled by whoever asked for it, so a bad one failed
871
+ // before any of this ran.
872
+ const structuredCapture = options.structuredOutput ? createStructuredCapture() : undefined;
873
+ const structuredTools = options.structuredOutput && structuredCapture
874
+ ? [createStructuredOutputTool(options.structuredOutput, structuredCapture)]
875
+ : [];
876
+ const structuredToolNames = new Set(structuredTools.map(tool => tool.name));
877
+ // Re-admitted together at every gate below. Kept as one set so a new injected
878
+ // tool cannot be added to some of the three gates and forgotten at the rest.
879
+ //
880
+ // `disallowed_tools` is applied HERE rather than at the gates, because the two
881
+ // kinds answer to it differently: a nested delegation tool is an opt-in the
882
+ // agent's own frontmatter can take back, while StructuredOutput exists only
883
+ // because this call asked for a schema — removing it would make the request
884
+ // unsatisfiable by construction rather than merely restricted.
885
+ const readmitToolNames = new Set([
886
+ ...[...nestedToolNames].filter(name => !disallowedSet?.has(name)),
887
+ ...structuredToolNames,
888
+ ]);
889
+
732
890
  // ─── Tool scoping ───────────────────────────────────────────────────────
733
891
  //
734
892
  // Some extensions register their tools ASYNCHRONOUSLY, long after the
@@ -761,17 +919,35 @@ export async function runAgent(
761
919
  let sessionTools: string[] | undefined;
762
920
  let sessionExcludeTools: string[] | undefined;
763
921
  if (noExtensions) {
764
- sessionTools = toolNames.filter(
765
- (t) => !EXCLUDED_TOOL_NAMES.includes(t) && !disallowedSet?.has(t),
766
- );
922
+ // Strict allowlist: built-ins the agent asked for, plus any opt-in nested
923
+ // tools (whose names would otherwise be dropped as EXCLUDED_TOOL_NAMES).
924
+ sessionTools = [
925
+ ...toolNames.filter(
926
+ (t) => !EXCLUDED_TOOL_NAMES.includes(t) && !disallowedSet?.has(t),
927
+ ),
928
+ ...[...nestedToolNames].filter((t) => !disallowedSet?.has(t)),
929
+ // Not filtered through `disallowedSet`, unlike the nested tools above:
930
+ // the caller asked for a schema, and removing the only tool that can
931
+ // satisfy it would make the request unsatisfiable by construction rather
932
+ // than merely restricted.
933
+ ...structuredToolNames,
934
+ ];
767
935
  } else {
768
- const denyTools = new Set<string>(EXCLUDED_TOOL_NAMES);
936
+ // Deny the orchestration tools EXCEPT the nested ones this agent opted into —
937
+ // those are injected as customTools and must survive the registry gate.
938
+ const denyTools = new Set<string>(
939
+ EXCLUDED_TOOL_NAMES.filter((t) => !nestedToolNames.has(t)),
940
+ );
769
941
  // Keep only the built-ins the agent asked for — deny the rest.
770
942
  for (const name of BUILTIN_TOOL_NAMES) {
771
943
  if (!builtinToolNameSet.has(name)) denyTools.add(name);
772
944
  }
773
945
  if (disallowedSet) {
774
- for (const name of disallowedSet) denyTools.add(name);
946
+ // disallowed_tools wins even over an opt-in nested tool of the same name.
947
+ // Not over StructuredOutput, though — see the allowlist branch above.
948
+ for (const name of disallowedSet) {
949
+ if (!structuredToolNames.has(name)) denyTools.add(name);
950
+ }
775
951
  }
776
952
  sessionExcludeTools = [...denyTools];
777
953
  }
@@ -779,33 +955,48 @@ export async function runAgent(
779
955
  const settingsManager = SettingsManager.create(configCwd, agentDir);
780
956
  const configuredSessionDir = resolveConfiguredSessionDir(agentConfig?.sessionDir, effectiveCwd);
781
957
  const defaultSessionDir = process.env.PI_CODING_AGENT_SESSION_DIR ?? settingsManager.getSessionDir?.();
782
- const sessionManager = agentConfig?.persistSession
783
- ? SessionManager.create(effectiveCwd, configuredSessionDir ?? defaultSessionDir)
784
- : SessionManager.inMemory(effectiveCwd);
785
- // Keep write tracking session-local. The custom definition shadows only this
786
- // child session's built-in write entry; the parent registry is untouched.
787
- const trackedWriteTool = toolNames.includes("write")
788
- ? createTrackedWriteTool(effectiveCwd)
789
- : undefined;
958
+ // Frontmatter wins when it says anything; otherwise the project default,
959
+ // which `rememberAgents` supplies for top-level agents only. Same precedence
960
+ // as `outputTranscript`.
961
+ const persistSession = agentConfig?.persistSession ?? (options.nested ? false : rememberAgents);
962
+ const sessionManager = options.resumeSessionFile
963
+ // Reopening an existing conversation: the file already carries its own
964
+ // header (cwd, parent) and history, so none of the create-time options
965
+ // apply. `sessionDir` still matters for a later /new or /branch off it.
966
+ ? SessionManager.open(options.resumeSessionFile, configuredSessionDir ?? defaultSessionDir)
967
+ : persistSession
968
+ ? SessionManager.create(effectiveCwd, configuredSessionDir ?? defaultSessionDir, {
969
+ // Optional metadata — it only nests the subagent under its spawner in
970
+ // `/resume`. Until `rememberAgents` this ran solely for the rare
971
+ // `persist_session: true` agent; now it runs for every spawn, so a
972
+ // context without a session manager (a bare programmatic ctx) must
973
+ // still persist rather than take the whole spawn down.
974
+ parentSession: ctx.sessionManager?.getSessionFile?.(),
975
+ })
976
+ : SessionManager.inMemory(effectiveCwd);
790
977
 
791
978
  // Pi 0.80.8 replaced createAgentSession's modelRegistry option with
792
979
  // modelRuntime, but ExtensionContext still exposes only the registry facade.
793
980
  // Pass both so the full supported Pi range retains the parent's providers.
794
- const parentModelRuntime = (ctx.modelRegistry as unknown as { runtime?: ModelRuntime | null }).runtime ?? undefined;
981
+ const parentModelRuntime = (ctx.modelRegistry as unknown as { runtime?: unknown }).runtime;
795
982
  const sessionOpts: Parameters<typeof createAgentSession>[0] & {
796
983
  modelRegistry: ExtensionContext["modelRegistry"];
797
- modelRuntime?: ModelRuntime;
984
+ modelRuntime?: unknown;
798
985
  } = {
799
986
  cwd: effectiveCwd,
800
987
  agentDir,
801
988
  sessionManager,
802
989
  settingsManager,
803
990
  modelRegistry: ctx.modelRegistry,
804
- ...(parentModelRuntime !== undefined && { modelRuntime: parentModelRuntime }),
991
+ // `as never` is what keeps this assignable across the supported Pi range:
992
+ // pre-0.80.8 the field exists only via the `modelRuntime?: unknown` shim
993
+ // above, while newer Pi types it as `ModelRuntime` — a shape an opaque
994
+ // `unknown` read off the private facade field can never satisfy.
995
+ ...(parentModelRuntime !== undefined && { modelRuntime: parentModelRuntime as never }),
805
996
  model,
806
997
  tools: sessionTools,
998
+ customTools: [...nestedTools, ...structuredTools],
807
999
  resourceLoader: loader,
808
- ...(trackedWriteTool && { customTools: [trackedWriteTool as any] }),
809
1000
  };
810
1001
  if (sessionExcludeTools) {
811
1002
  sessionOpts.excludeTools = sessionExcludeTools;
@@ -814,7 +1005,7 @@ export async function runAgent(
814
1005
  sessionOpts.thinkingLevel = thinkingLevel;
815
1006
  }
816
1007
 
817
- const { session } = await createAgentSession(sessionOpts);
1008
+ const { session } = await runInChildSessionContext(() => createAgentSession(sessionOpts));
818
1009
 
819
1010
  const baseSessionName = agentConfig?.name ?? type;
820
1011
  session.setSessionName(
@@ -847,6 +1038,7 @@ export async function runAgent(
847
1038
  disallowedSet,
848
1039
  extNames,
849
1040
  narrowing,
1041
+ readmitToolNames,
850
1042
  });
851
1043
  }
852
1044
 
@@ -854,7 +1046,7 @@ export async function runAgent(
854
1046
 
855
1047
  // Track turns for graceful max_turns enforcement
856
1048
  let turnCount = 0;
857
- const maxTurns = normalizeMaxTurns(options.maxTurns ?? agentConfig?.maxTurns ?? defaultMaxTurns);
1049
+ const maxTurns = resolveEffectiveMaxTurns(type, options.maxTurns);
858
1050
  let softLimitReached = false;
859
1051
  let aborted = false;
860
1052
 
@@ -892,6 +1084,8 @@ export async function runAgent(
892
1084
  input: u.input ?? 0,
893
1085
  output: u.output ?? 0,
894
1086
  cacheWrite: u.cacheWrite ?? 0,
1087
+ cacheRead: u.cacheRead ?? 0,
1088
+ cost: u.cost?.total ?? 0,
895
1089
  });
896
1090
  }
897
1091
  if (event.type === "compaction_end" && !event.aborted && event.result) {
@@ -914,8 +1108,20 @@ export async function runAgent(
914
1108
  // Boundary for the history fallback: only assistant text produced from here
915
1109
  // on counts as this run's output (a fresh session, so usually 0).
916
1110
  const startLen = session.messages.length;
1111
+ let structuredRetried = false;
917
1112
  try {
918
1113
  await session.prompt(effectivePrompt);
1114
+
1115
+ // One more prompt when a schema was asked for and nothing usable came back
1116
+ // — the model answered in prose, or only ever called the tool invalidly.
1117
+ // Inside this `try`, so the turn tracking, the text collector and above all
1118
+ // the abort forwarding are still live: torn down first, a retry would be
1119
+ // unkillable.
1120
+ if (structuredCapture !== undefined && structuredCapture.json === undefined
1121
+ && !aborted && options.signal?.aborted !== true) {
1122
+ structuredRetried = true;
1123
+ await session.prompt(structuredRetryPrompt(structuredCapture));
1124
+ }
919
1125
  } finally {
920
1126
  unsubTurns();
921
1127
  collector.unsubscribe();
@@ -923,7 +1129,23 @@ export async function runAgent(
923
1129
  }
924
1130
 
925
1131
  const responseText = collector.getText().trim() || getLastAssistantText(session, startLen);
926
- return { responseText, session, aborted, steered: softLimitReached, failure: finalTurnError(session, startLen) };
1132
+ // A child asked for structured output that never gave any has failed, however
1133
+ // articulate its prose was. Reported through `failure` so it travels the same
1134
+ // path as a provider error rather than arriving as a successful empty answer.
1135
+ const structuredFailure = structuredCapture !== undefined && structuredCapture.json === undefined
1136
+ ? structuredCapture.lastError !== undefined
1137
+ ? `The agent's StructuredOutput call did not match the required schema: ${structuredCapture.lastError}`
1138
+ : "The agent did not report its answer through StructuredOutput."
1139
+ : undefined;
1140
+ return {
1141
+ responseText,
1142
+ session,
1143
+ aborted,
1144
+ steered: softLimitReached,
1145
+ failure: finalTurnError(session, startLen) ?? structuredFailure,
1146
+ ...(structuredCapture?.json !== undefined ? { structuredJson: structuredCapture.json } : {}),
1147
+ ...(structuredRetried ? { structuredRetried } : {}),
1148
+ };
927
1149
  }
928
1150
 
929
1151
  /**
@@ -934,7 +1156,7 @@ export async function resumeAgent(
934
1156
  prompt: string,
935
1157
  options: {
936
1158
  onToolActivity?: (activity: ToolActivity) => void;
937
- onAssistantUsage?: (usage: { input: number; output: number; cacheWrite: number }) => void;
1159
+ onAssistantUsage?: (usage: LifetimeUsage) => void;
938
1160
  onCompaction?: (info: { reason: "manual" | "threshold" | "overflow"; tokensBefore: number }) => void;
939
1161
  signal?: AbortSignal;
940
1162
  } = {},
@@ -956,6 +1178,8 @@ export async function resumeAgent(
956
1178
  input: u.input ?? 0,
957
1179
  output: u.output ?? 0,
958
1180
  cacheWrite: u.cacheWrite ?? 0,
1181
+ cacheRead: u.cacheRead ?? 0,
1182
+ cost: u.cost?.total ?? 0,
959
1183
  });
960
1184
  }
961
1185
  if (event.type === "compaction_end" && !event.aborted && event.result) {