@narumitw/pi-subagents 2.0.5 → 2.1.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.
Files changed (178) hide show
  1. package/README.md +152 -50
  2. package/dist/chunks/{auto-transport-SY2VHUFH.ts → auto-transport-FUUKFDIG.ts} +6 -6
  3. package/dist/chunks/{capability-grant-CGEWOEKE.ts → capability-grant-PR72SWWS.ts} +4 -4
  4. package/dist/chunks/{chunk-DPPVEQAM.ts → chunk-4AQSF7AS.ts} +1 -1
  5. package/dist/chunks/chunk-4AQSF7AS.ts.map +7 -0
  6. package/dist/chunks/chunk-6H6TBBED.ts +108 -0
  7. package/dist/chunks/chunk-6H6TBBED.ts.map +7 -0
  8. package/dist/chunks/{chunk-DSOOH73Y.ts → chunk-7AAJEUSL.ts} +3 -3
  9. package/dist/chunks/{chunk-DSOOH73Y.ts.map → chunk-7AAJEUSL.ts.map} +2 -2
  10. package/dist/chunks/{chunk-7QPPBBXZ.ts → chunk-D4CR7T73.ts} +21 -3
  11. package/dist/chunks/chunk-D4CR7T73.ts.map +7 -0
  12. package/dist/chunks/{chunk-434NII74.ts → chunk-DIHBUR2E.ts} +54 -8
  13. package/dist/chunks/chunk-DIHBUR2E.ts.map +7 -0
  14. package/dist/chunks/{chunk-NIF42QMF.ts → chunk-FEVPWRMU.ts} +7 -4
  15. package/dist/chunks/chunk-FEVPWRMU.ts.map +7 -0
  16. package/dist/chunks/{chunk-YBUWBRF7.ts → chunk-FXI45N3J.ts} +5 -5
  17. package/dist/chunks/{chunk-S2IWVK3J.ts → chunk-ILEQ27AL.ts} +3 -2
  18. package/dist/chunks/chunk-ILEQ27AL.ts.map +7 -0
  19. package/dist/chunks/{chunk-DQMN4OYM.ts → chunk-ITVWPNU4.ts} +12 -106
  20. package/dist/chunks/chunk-ITVWPNU4.ts.map +7 -0
  21. package/dist/chunks/{chunk-QDVGH2X7.ts → chunk-IWC32VPY.ts} +2 -1
  22. package/dist/chunks/{chunk-QDVGH2X7.ts.map → chunk-IWC32VPY.ts.map} +2 -2
  23. package/dist/chunks/{chunk-I2FAK44T.ts → chunk-JSZIP73U.ts} +48 -10
  24. package/dist/chunks/chunk-JSZIP73U.ts.map +7 -0
  25. package/dist/chunks/{chunk-ZHTNCZIA.ts → chunk-JU6LUNLP.ts} +2 -2
  26. package/dist/chunks/{chunk-PSK43Y6A.ts → chunk-LASD73CM.ts} +2 -2
  27. package/dist/chunks/{chunk-C4L6P266.ts → chunk-LEOYDZI3.ts} +1 -1
  28. package/dist/chunks/{chunk-C4L6P266.ts.map → chunk-LEOYDZI3.ts.map} +2 -2
  29. package/dist/chunks/{chunk-H3BFJ7HJ.ts → chunk-LL4LP2T7.ts} +5 -5
  30. package/dist/chunks/chunk-N2T5IN4X.ts +18 -0
  31. package/dist/chunks/chunk-N2T5IN4X.ts.map +7 -0
  32. package/dist/chunks/{chunk-G3RSMSXJ.ts → chunk-NTRPLF46.ts} +1 -1
  33. package/dist/chunks/chunk-NTRPLF46.ts.map +7 -0
  34. package/dist/chunks/{chunk-CBP76ARQ.ts → chunk-PABJJYP6.ts} +174 -47
  35. package/dist/chunks/chunk-PABJJYP6.ts.map +7 -0
  36. package/dist/chunks/{chunk-EFBPISNT.ts → chunk-PBZMBTNJ.ts} +104 -103
  37. package/dist/chunks/chunk-PBZMBTNJ.ts.map +7 -0
  38. package/dist/chunks/{chunk-SCZ33MYW.ts → chunk-PGLSFLYW.ts} +2 -2
  39. package/dist/chunks/{chunk-O4VO6JUJ.ts → chunk-TM2R67J3.ts} +3 -3
  40. package/dist/chunks/{chunk-QBORTI4W.ts → chunk-TMZRHIIK.ts} +32 -3
  41. package/dist/chunks/chunk-TMZRHIIK.ts.map +7 -0
  42. package/dist/chunks/chunk-TZ34IQ3M.ts +59 -0
  43. package/dist/chunks/chunk-TZ34IQ3M.ts.map +7 -0
  44. package/dist/chunks/chunk-VBDGNNLM.ts +214 -0
  45. package/dist/chunks/chunk-VBDGNNLM.ts.map +7 -0
  46. package/dist/chunks/{chunk-7OBINKFM.ts → chunk-VDG7LTYE.ts} +11 -11
  47. package/dist/chunks/chunk-VDG7LTYE.ts.map +7 -0
  48. package/dist/chunks/{chunk-AEUYS7JC.ts → chunk-X4NMONPE.ts} +1 -1
  49. package/dist/chunks/chunk-X4NMONPE.ts.map +7 -0
  50. package/dist/chunks/{chunk-HKC4ES4B.ts → chunk-YPJEN6NU.ts} +2 -2
  51. package/dist/chunks/{completion-delivery-YPOWSSV3.ts → completion-delivery-JVLNQRWX.ts} +5 -4
  52. package/dist/chunks/{config-status-J4GPWP46.ts → config-status-FKGDECZ3.ts} +7 -6
  53. package/dist/chunks/{config-ui-2YUCUEBM.ts → config-ui-DDKERQHI.ts} +297 -231
  54. package/dist/chunks/config-ui-DDKERQHI.ts.map +7 -0
  55. package/dist/chunks/{consult-IV7UBNQQ.ts → consult-PQ6PRAKC.ts} +14 -13
  56. package/dist/chunks/consult-PQ6PRAKC.ts.map +7 -0
  57. package/dist/chunks/{create-stateful-transport-ZP3IXNV3.ts → create-stateful-transport-JWC2EFYL.ts} +6 -6
  58. package/dist/chunks/{delegation-contract-WJPT4FVS.ts → delegation-contract-LA56I5DT.ts} +2 -2
  59. package/dist/chunks/{discovery-K25T3SH4.ts → discovery-MIFB2U4Y.ts} +3 -3
  60. package/dist/chunks/{execution-LA7HN5YF.ts → execution-Q2JZLKJA.ts} +19 -16
  61. package/dist/chunks/execution-Q2JZLKJA.ts.map +7 -0
  62. package/dist/chunks/{in-process-transport-4RF3J6XK.ts → in-process-transport-HJ6TZXC3.ts} +9 -9
  63. package/dist/chunks/{inspect-XV2QALMR.ts → inspect-UH2TKH6E.ts} +25 -10
  64. package/dist/chunks/inspect-UH2TKH6E.ts.map +7 -0
  65. package/dist/chunks/{persistence-VSGXAW4P.ts → persistence-XHPJBZL7.ts} +11 -7
  66. package/dist/chunks/persistence-XHPJBZL7.ts.map +7 -0
  67. package/dist/chunks/{registry-E6XPJB7L.ts → registry-XDXPECWF.ts} +136 -15
  68. package/dist/chunks/registry-XDXPECWF.ts.map +7 -0
  69. package/dist/chunks/{retained-semantic-state-7WXSGRJU.ts → retained-semantic-state-GM75FZGE.ts} +3 -3
  70. package/dist/chunks/{rpc-transport-NEHBHWX4.ts → rpc-transport-7R7DVCEB.ts} +12 -16
  71. package/dist/chunks/rpc-transport-7R7DVCEB.ts.map +7 -0
  72. package/dist/chunks/{spawn-idempotency-QGHO2WKC.ts → spawn-idempotency-BNHOSMZW.ts} +2 -2
  73. package/dist/chunks/{subprocess-transport-NSHOGDC4.ts → subprocess-transport-VHZJRBTW.ts} +15 -14
  74. package/dist/chunks/subprocess-transport-VHZJRBTW.ts.map +7 -0
  75. package/dist/chunks/usage-recording-store-CW3EH2SZ.ts +164 -0
  76. package/dist/chunks/usage-recording-store-CW3EH2SZ.ts.map +7 -0
  77. package/dist/index.ts +427 -30
  78. package/dist/index.ts.map +3 -3
  79. package/docs/async-runtime-protocol.md +69 -0
  80. package/docs/implementation-notes/pi-subagents-capability-matrix.md +62 -0
  81. package/docs/implementation-notes/pi-subagents-current-direction.md +99 -0
  82. package/docs/implementation-notes/pi-subagents-rpc-v1.md +153 -0
  83. package/docs/pi-subagents-diagrams.md +183 -0
  84. package/package.json +9 -8
  85. package/src/agents/types.ts +2 -0
  86. package/src/async-subagent-benchmark.ts +532 -0
  87. package/src/completion-delivery.ts +71 -4
  88. package/src/completion-render.ts +1 -0
  89. package/src/completion-requirement.ts +308 -0
  90. package/src/config-registration.ts +5 -5
  91. package/src/config-status.ts +66 -70
  92. package/src/config-ui.ts +244 -150
  93. package/src/consult-resources.ts +1 -1
  94. package/src/consult.ts +3 -8
  95. package/src/delegation-contract.ts +55 -9
  96. package/src/execution-plan.ts +1 -1
  97. package/src/execution-ui.ts +40 -29
  98. package/src/execution.ts +2 -3
  99. package/src/inspect.ts +24 -1
  100. package/src/orchestration-metrics.ts +1 -1
  101. package/src/panel-execution.ts +2 -3
  102. package/src/panel-failure.ts +1 -1
  103. package/src/panel-render.ts +1 -1
  104. package/src/parallel-limit-ui.ts +6 -5
  105. package/src/params.ts +2 -1
  106. package/src/persistence.ts +9 -0
  107. package/src/process-control.ts +43 -0
  108. package/src/registry-types.ts +5 -0
  109. package/src/registry.ts +162 -18
  110. package/src/render.ts +2 -1
  111. package/src/rpc-transport.ts +1 -1
  112. package/src/runner-outcome.ts +1 -1
  113. package/src/runner-result.ts +1 -1
  114. package/src/runner-types.ts +102 -0
  115. package/src/runner.ts +11 -192
  116. package/src/settings/inspection.ts +27 -0
  117. package/src/settings/schema.ts +9 -0
  118. package/src/settings-reader.ts +7 -0
  119. package/src/settings.ts +20 -0
  120. package/src/spawn-idempotency.ts +5 -0
  121. package/src/stateful-agent-view.ts +15 -19
  122. package/src/stateful-guidance.ts +14 -4
  123. package/src/stateful-limit-ui.ts +23 -20
  124. package/src/stateful-limits.ts +10 -10
  125. package/src/stateful-registration.ts +126 -9
  126. package/src/stateful-render.ts +27 -2
  127. package/src/subagent-details.ts +43 -0
  128. package/src/subagents-extension.ts +66 -8
  129. package/src/subagents.ts +2 -0
  130. package/src/subprocess-transport.ts +2 -1
  131. package/src/supervision.ts +2 -1
  132. package/src/timeout-finalization.ts +1 -1
  133. package/src/tool-schema-compatibility.ts +73 -0
  134. package/src/transport-types.ts +6 -0
  135. package/src/transport-ui.ts +18 -46
  136. package/src/usage-recording-config.ts +13 -0
  137. package/src/usage-recording-store.ts +183 -0
  138. package/src/usage-recording.ts +478 -0
  139. package/src/verification-harness.ts +1 -1
  140. package/src/workflow-ui.ts +17 -9
  141. package/dist/chunks/chunk-434NII74.ts.map +0 -7
  142. package/dist/chunks/chunk-7OBINKFM.ts.map +0 -7
  143. package/dist/chunks/chunk-7QPPBBXZ.ts.map +0 -7
  144. package/dist/chunks/chunk-AEUYS7JC.ts.map +0 -7
  145. package/dist/chunks/chunk-CBP76ARQ.ts.map +0 -7
  146. package/dist/chunks/chunk-DPPVEQAM.ts.map +0 -7
  147. package/dist/chunks/chunk-DQMN4OYM.ts.map +0 -7
  148. package/dist/chunks/chunk-EFBPISNT.ts.map +0 -7
  149. package/dist/chunks/chunk-G3RSMSXJ.ts.map +0 -7
  150. package/dist/chunks/chunk-I2FAK44T.ts.map +0 -7
  151. package/dist/chunks/chunk-NIF42QMF.ts.map +0 -7
  152. package/dist/chunks/chunk-QBORTI4W.ts.map +0 -7
  153. package/dist/chunks/chunk-S2IWVK3J.ts.map +0 -7
  154. package/dist/chunks/config-ui-2YUCUEBM.ts.map +0 -7
  155. package/dist/chunks/consult-IV7UBNQQ.ts.map +0 -7
  156. package/dist/chunks/execution-LA7HN5YF.ts.map +0 -7
  157. package/dist/chunks/inspect-XV2QALMR.ts.map +0 -7
  158. package/dist/chunks/persistence-VSGXAW4P.ts.map +0 -7
  159. package/dist/chunks/registry-E6XPJB7L.ts.map +0 -7
  160. package/dist/chunks/rpc-transport-NEHBHWX4.ts.map +0 -7
  161. package/dist/chunks/subprocess-transport-NSHOGDC4.ts.map +0 -7
  162. /package/dist/chunks/{auto-transport-SY2VHUFH.ts.map → auto-transport-FUUKFDIG.ts.map} +0 -0
  163. /package/dist/chunks/{capability-grant-CGEWOEKE.ts.map → capability-grant-PR72SWWS.ts.map} +0 -0
  164. /package/dist/chunks/{chunk-YBUWBRF7.ts.map → chunk-FXI45N3J.ts.map} +0 -0
  165. /package/dist/chunks/{chunk-ZHTNCZIA.ts.map → chunk-JU6LUNLP.ts.map} +0 -0
  166. /package/dist/chunks/{chunk-PSK43Y6A.ts.map → chunk-LASD73CM.ts.map} +0 -0
  167. /package/dist/chunks/{chunk-H3BFJ7HJ.ts.map → chunk-LL4LP2T7.ts.map} +0 -0
  168. /package/dist/chunks/{chunk-SCZ33MYW.ts.map → chunk-PGLSFLYW.ts.map} +0 -0
  169. /package/dist/chunks/{chunk-O4VO6JUJ.ts.map → chunk-TM2R67J3.ts.map} +0 -0
  170. /package/dist/chunks/{chunk-HKC4ES4B.ts.map → chunk-YPJEN6NU.ts.map} +0 -0
  171. /package/dist/chunks/{completion-delivery-YPOWSSV3.ts.map → completion-delivery-JVLNQRWX.ts.map} +0 -0
  172. /package/dist/chunks/{config-status-J4GPWP46.ts.map → config-status-FKGDECZ3.ts.map} +0 -0
  173. /package/dist/chunks/{create-stateful-transport-ZP3IXNV3.ts.map → create-stateful-transport-JWC2EFYL.ts.map} +0 -0
  174. /package/dist/chunks/{delegation-contract-WJPT4FVS.ts.map → delegation-contract-LA56I5DT.ts.map} +0 -0
  175. /package/dist/chunks/{discovery-K25T3SH4.ts.map → discovery-MIFB2U4Y.ts.map} +0 -0
  176. /package/dist/chunks/{in-process-transport-4RF3J6XK.ts.map → in-process-transport-HJ6TZXC3.ts.map} +0 -0
  177. /package/dist/chunks/{retained-semantic-state-7WXSGRJU.ts.map → retained-semantic-state-GM75FZGE.ts.map} +0 -0
  178. /package/dist/chunks/{spawn-idempotency-QGHO2WKC.ts.map → spawn-idempotency-BNHOSMZW.ts.map} +0 -0
@@ -29,36 +29,36 @@ export interface StatefulLimitDefinition {
29
29
  export const STATEFUL_LIMIT_DEFINITIONS: readonly StatefulLimitDefinition[] = [
30
30
  {
31
31
  field: "maxAgents",
32
- label: "Retained agents",
33
- description: "Running, queued, and reusable idle detached agents",
32
+ label: "Subagents saved for follow-up",
33
+ description: "Working, queued, and reusable background subagents kept in one session",
34
34
  defaultValue: 16,
35
35
  minimum: 1,
36
36
  },
37
37
  {
38
38
  field: "maxActiveTurns",
39
- label: "Active turns",
40
- description: "Detached agent turns that may run at the same time",
39
+ label: "Subagents working at once",
40
+ description: "Background subagent turns that may run at the same time",
41
41
  defaultValue: 4,
42
42
  minimum: 1,
43
43
  },
44
44
  {
45
45
  field: "maxChildrenPerAgent",
46
- label: "Children per agent",
47
- description: "Direct child agents retained beneath one parent",
46
+ label: "Direct children per subagent",
47
+ description: "Subagents that one parent subagent may keep for follow-up",
48
48
  defaultValue: 8,
49
49
  minimum: 1,
50
50
  },
51
51
  {
52
52
  field: "maxDepth",
53
- label: "Agent tree depth",
54
- description: "Nested child levels below a root agent",
53
+ label: "Nested subagent levels",
54
+ description: "Child levels allowed below a top-level subagent",
55
55
  defaultValue: 3,
56
56
  minimum: 0,
57
57
  },
58
58
  {
59
59
  field: "maxStoredAgents",
60
- label: "Stored agents",
61
- description: "Detached agent records kept per session on disk",
60
+ label: "Stored subagent records",
61
+ description: "Background subagent records kept on disk for each session",
62
62
  defaultValue: 50,
63
63
  minimum: 1,
64
64
  },
@@ -17,6 +17,11 @@ import {
17
17
  THINKING_LEVELS,
18
18
  } from "./agents/types.js";
19
19
  import type { CompletionDeliveryBroker } from "./completion-delivery.js";
20
+ import {
21
+ CompletionRequirementModeSchema,
22
+ completionRequirementsFromBranch,
23
+ reconcileRequiredCompletionContext,
24
+ } from "./completion-requirement.js";
20
25
  import type { ContextMode } from "./context.js";
21
26
  import type { CreateStatefulTransportOptions } from "./create-stateful-transport.js";
22
27
  import { DelegationContractSchema } from "./delegation-contract.js";
@@ -55,7 +60,9 @@ import {
55
60
  validateManageParams,
56
61
  } from "./stateful-tool-params.js";
57
62
  import { MAX_TASK_NAME_LENGTH } from "./task-path.js";
63
+ import { grammarSafeToolObject } from "./tool-schema-compatibility.js";
58
64
  import { MAX_SUBAGENT_TOOL_CALLS, MAX_SUBAGENT_TURNS } from "./turn-budget.js";
65
+ import type { UsageRecordingController } from "./usage-recording.js";
59
66
  import type { WorkspaceManager } from "./workspace.js";
60
67
 
61
68
  type CwdPolicyModule = typeof import("./cwd-policy.js");
@@ -196,6 +203,21 @@ const StatefulTimeoutSchema = Type.Integer({
196
203
  description:
197
204
  "Work deadline in milliseconds selected for the task difficulty. On expiry, Pi aborts the work and makes one separately bounded summary attempt. Retained as the agent default.",
198
205
  });
206
+ const DEFAULT_SUBAGENT_AWAIT_TIMEOUT_MS = 30_000;
207
+ const SubagentAwaitParams = Type.Object({
208
+ agentId: Type.String({
209
+ minLength: 1,
210
+ description: "Retained agent ID or canonical task path.",
211
+ }),
212
+ timeoutMs: Type.Optional(
213
+ Type.Integer({
214
+ minimum: 1,
215
+ maximum: MAX_SUBAGENT_TIMEOUT_MS,
216
+ description:
217
+ "Maximum time to wait in milliseconds. A wait timeout stops only this tool call and does not interrupt or close the subagent. Defaults to 30000.",
218
+ }),
219
+ ),
220
+ });
199
221
  const StatefulTurnLimitFields = {
200
222
  idleTimeoutMs: Type.Optional(
201
223
  Type.Integer({
@@ -209,14 +231,16 @@ const StatefulTurnLimitFields = {
209
231
  Type.Integer({
210
232
  minimum: 1,
211
233
  maximum: MAX_SUBAGENT_TURNS,
212
- description: "Maximum unfinished assistant turns; retained as the agent default.",
234
+ description:
235
+ "Maximum unfinished assistant turns; retained as the agent default. Omit rather than guessing a tight bound.",
213
236
  }),
214
237
  ),
215
238
  maxToolCalls: Type.Optional(
216
239
  Type.Integer({
217
240
  minimum: 1,
218
241
  maximum: MAX_SUBAGENT_TOOL_CALLS,
219
- description: "Maximum tool calls; retained as the agent default.",
242
+ description:
243
+ "Maximum tool calls; retained as the agent default. Omit rather than guessing a tight bound.",
220
244
  }),
221
245
  ),
222
246
  };
@@ -227,6 +251,7 @@ export interface StatefulSubagentDependencies {
227
251
  settings?: SubagentRuntimeSettings;
228
252
  getSettings?: () => SubagentSettings | undefined;
229
253
  loadTransport?: CreateStatefulTransportOptions["loadTransport"];
254
+ usageRecording?: UsageRecordingController;
230
255
  }
231
256
 
232
257
  export interface StatefulSubagentRuntimeStatus {
@@ -362,7 +387,7 @@ export function registerStatefulSubagents(
362
387
  if (!registry) throw new Error("Stateful subagents are not initialized for this session");
363
388
  return registry;
364
389
  };
365
- pi.on("session_start", async (_event, ctx) => {
390
+ pi.on("session_start", async (event, ctx) => {
366
391
  const generation = ++runtimeGeneration;
367
392
  completionBroker?.close();
368
393
  completionBroker = undefined;
@@ -415,9 +440,16 @@ export function registerStatefulSubagents(
415
440
  const reason = error instanceof Error ? error.message : String(error);
416
441
  ctx.ui.notify(`Subagent completion delivery failed: ${reason}`, "warning");
417
442
  },
443
+ onDeliveryAttempt: (completions, input) => {
444
+ if (generation !== runtimeGeneration) return;
445
+ for (const completion of completions) {
446
+ dependencies.usageRecording?.recordCompletionDeliveryAttempt(completion, input);
447
+ }
448
+ },
418
449
  onAcknowledged: (completions, deliveredAt) => {
419
450
  if (generation !== runtimeGeneration) return;
420
451
  for (const completion of completions) {
452
+ dependencies.usageRecording?.recordCompletionVisible(completion);
421
453
  void nextRegistry
422
454
  .markCompletionDelivered(completion.completionId, deliveredAt)
423
455
  .catch((error: unknown) => {
@@ -479,6 +511,7 @@ export function registerStatefulSubagents(
479
511
  if (generation !== runtimeGeneration) return;
480
512
  await sessionPersistence.save(agents);
481
513
  if (generation !== runtimeGeneration) return;
514
+ dependencies.usageRecording?.observeAgents(agents);
482
515
  for (const agent of agents) {
483
516
  for (const message of agent.mailbox) {
484
517
  if (seenMessageIds.has(message.id)) continue;
@@ -506,9 +539,9 @@ export function registerStatefulSubagents(
506
539
  }
507
540
  },
508
541
  onTurnComplete: (completion) => {
509
- if (generation === runtimeGeneration && completion.recipientId === "root") {
510
- sessionBroker.enqueue(completion);
511
- }
542
+ if (generation !== runtimeGeneration) return;
543
+ dependencies.usageRecording?.recordChildCompletion(completion);
544
+ if (completion.recipientId === "root") sessionBroker.enqueue(completion);
512
545
  },
513
546
  });
514
547
  const persisted = sessionPersistence.load();
@@ -550,6 +583,26 @@ export function registerStatefulSubagents(
550
583
  for (const message of agent.mailbox) seenMessageIds.add(message.id);
551
584
  }
552
585
  nextRegistry.restore(restored);
586
+ const branchRequirements = completionRequirementsFromBranch(ctx.sessionManager.getBranch());
587
+ const branchOwnsRequirementState =
588
+ branchRequirements.observedState || event.reason === "fork" || event.reason === "new";
589
+ nextRegistry.reconcileCompletionRequirements(
590
+ branchRequirements.records,
591
+ branchOwnsRequirementState,
592
+ );
593
+ if (branchOwnsRequirementState) {
594
+ try {
595
+ await sessionPersistence.save(nextRegistry.list(true));
596
+ } catch (error) {
597
+ if (generation === runtimeGeneration && ctx.hasUI) {
598
+ const reason = error instanceof Error ? error.message : String(error);
599
+ ctx.ui.notify(
600
+ `Subagent requirement reconciliation could not be persisted yet: ${reason}`,
601
+ "warning",
602
+ );
603
+ }
604
+ }
605
+ }
553
606
  if (generation !== runtimeGeneration) {
554
607
  sessionBroker.close();
555
608
  await sessionPeerBroker.close();
@@ -589,6 +642,9 @@ export function registerStatefulSubagents(
589
642
 
590
643
  pi.on("context", (event) => {
591
644
  completionBroker?.onParentContext(event.messages);
645
+ const agents = registry?.list() ?? [];
646
+ const messages = reconcileRequiredCompletionContext(event.messages, agents);
647
+ if (messages !== event.messages) return { messages };
592
648
  });
593
649
 
594
650
  pi.on("agent_settled", () => {
@@ -639,7 +695,7 @@ export function registerStatefulSubagents(
639
695
  description: appendAgentCatalog(baseSpawnDescription(), agentCatalog),
640
696
  promptSnippet: "Start a reusable detached subagent; completion is delivered asynchronously",
641
697
  promptGuidelines: createSpawnPromptGuidelines(completionDelivery, blockingEnabled),
642
- parameters: Type.Object({
698
+ parameters: grammarSafeToolObject({
643
699
  agent: Type.String({ minLength: 1 }),
644
700
  taskName: Type.Optional(
645
701
  Type.String({
@@ -689,6 +745,7 @@ export function registerStatefulSubagents(
689
745
  "Use text (default), structured-v1, or the evidence-preserving structured-v2 completion contract.",
690
746
  }),
691
747
  ),
748
+ completionRequirement: Type.Optional(CompletionRequirementModeSchema),
692
749
  }),
693
750
  ...createStatefulToolRenderer("spawn"),
694
751
  async execute(_id, params, signal, _update, ctx) {
@@ -780,6 +837,7 @@ export function registerStatefulSubagents(
780
837
  allowConcurrentWrites: params.allowConcurrentWrites ?? false,
781
838
  contract,
782
839
  resultFormat,
840
+ completionRequirement: params.completionRequirement ?? "background",
783
841
  });
784
842
  if (!capturedRegistry) {
785
843
  throw new Error("Stateful subagents are not initialized for this session");
@@ -873,6 +931,7 @@ export function registerStatefulSubagents(
873
931
  spawnRequestHash: params.idempotencyKey ? requestHash : undefined,
874
932
  contract,
875
933
  resultFormat: resultFormat === "text" ? undefined : resultFormat,
934
+ completionRequirement: params.completionRequirement ?? "background",
876
935
  executionPlan,
877
936
  capabilityGrant,
878
937
  semanticSnapshot,
@@ -892,11 +951,15 @@ export function registerStatefulSubagents(
892
951
  resolvePending?.(agent);
893
952
  const deliveryNote =
894
953
  completionDelivery === "auto-resume"
895
- ? "Auto-resume will request synthesis after completion."
954
+ ? "Auto-resume steers completions into active parent work or requests synthesis when idle. Treat this response as progress, not final synthesis, until every final-answer-required completion message is visible. If local work is exhausted while required children remain active, emit at most one brief progress sentence and end the turn; do not repeat waiting updates or use the requested final format, verdict, or conclusion. Do not redo a running child's assigned work."
896
955
  : "The current response must not depend on the result because next-turn delivery will not wake an idle root.";
956
+ const requirementNote =
957
+ params.completionRequirement === "required"
958
+ ? "This exact run is runtime-tracked as required until its completion becomes visible or reaches an explicit terminal state. Current Pi versions still cannot prevent already-streamed premature output."
959
+ : "This run is background work and does not block the parent final answer.";
897
960
  return result(
898
961
  agent,
899
- `Spawned ${agent.agent} as ${agent.taskPath ?? agent.id} (${agent.id}). Continue the identified non-overlapping local work immediately; do not merely announce the spawn or end while useful local work remains. Only an explicit user-requested specialist model, tool-profile, or isolation exception may lack concurrent local work. ${deliveryNote} Do not poll for progress.`,
962
+ `Spawned ${agent.agent} as ${agent.taskPath ?? agent.id} (${agent.id}). ${requirementNote} Continue the identified non-overlapping local work immediately; do not merely announce the spawn or end while useful local work remains. Only an explicit user-requested specialist model, tool-profile, or isolation exception may lack concurrent local work. ${deliveryNote} Do not poll for progress.`,
900
963
  );
901
964
  } catch (error) {
902
965
  rejectPending?.(error);
@@ -937,6 +1000,7 @@ export function registerStatefulSubagents(
937
1000
  }),
938
1001
  ),
939
1002
  ...StatefulTurnLimitFields,
1003
+ completionRequirement: Type.Optional(CompletionRequirementModeSchema),
940
1004
  revalidate: Type.Optional(
941
1005
  Type.Boolean({
942
1006
  description:
@@ -1055,12 +1119,37 @@ export function registerStatefulSubagents(
1055
1119
  idleTimeoutMs: params.idleTimeoutMs,
1056
1120
  maxTurns: params.maxTurns,
1057
1121
  maxToolCalls: params.maxToolCalls,
1122
+ completionRequirement: params.completionRequirement ?? "background",
1058
1123
  });
1059
1124
  assertCurrentSpawn(signal, generation, runtimeGeneration);
1060
1125
  return result(agent, `Started follow-up for ${agent.id}.`);
1061
1126
  },
1062
1127
  });
1063
1128
 
1129
+ if (blockingEnabled) {
1130
+ pi.registerTool({
1131
+ name: "subagent_await",
1132
+ label: "Await Subagent",
1133
+ description:
1134
+ "Wait for one retained subagent's current turn to settle and return its latest bounded output. This blocks Pi from processing queued steering until the wait finishes. A wait timeout or caller cancellation stops only the wait and never interrupts or closes the subagent; use subagent_manage for lifecycle changes. Automatic completion delivery remains active and may later repeat the same at-least-once completion.",
1135
+ promptSnippet:
1136
+ "Intentionally block until one retained subagent settles or the wait times out",
1137
+ promptGuidelines: [
1138
+ "Use subagent_await only when the retained result is required before the next action, useful overlapping main-agent work is complete, and blocking Pi is intentional.",
1139
+ "Do not repeatedly call subagent_await after a timeout; the subagent keeps running and completion delivery remains active. Use subagent_manage only when the user wants to interrupt or close it.",
1140
+ ],
1141
+ parameters: SubagentAwaitParams,
1142
+ ...createStatefulToolRenderer("await"),
1143
+ async execute(_id, params, signal): Promise<StatefulActionToolResult> {
1144
+ const generation = runtimeGeneration;
1145
+ const timeoutMs = params.timeoutMs ?? DEFAULT_SUBAGENT_AWAIT_TIMEOUT_MS;
1146
+ const waited = await requireRegistry().wait(params.agentId, timeoutMs, signal);
1147
+ assertCurrentSpawn(signal, generation, runtimeGeneration);
1148
+ return awaitResult(waited.agent, waited.timedOut, timeoutMs);
1149
+ },
1150
+ });
1151
+ }
1152
+
1064
1153
  pi.registerTool({
1065
1154
  name: "subagent_manage",
1066
1155
  label: "Manage Subagents",
@@ -1225,6 +1314,34 @@ function result(agent: ManagedAgent, text: string) {
1225
1314
  };
1226
1315
  }
1227
1316
 
1317
+ function awaitResult(agent: ManagedAgent, timedOut: boolean, timeoutMs: number) {
1318
+ const latestTurn = agent.history.at(-1);
1319
+ const output = timedOut
1320
+ ? ""
1321
+ : truncateUtf8(latestTurn?.output ?? "", DEFAULT_MAX_CONTEXT_BYTES).text;
1322
+ const error = timedOut ? "" : truncateUtf8(agent.error ?? "", MAX_TOOL_MESSAGE_BYTES).text;
1323
+ const text = timedOut
1324
+ ? `Stopped waiting for ${agent.taskPath ?? agent.id} after ${timeoutMs}ms; it remains ${agent.state}. The wait did not interrupt or close the subagent.`
1325
+ : [`Subagent ${agent.taskPath ?? agent.id} settled as ${agent.state}.`, output || error]
1326
+ .filter(Boolean)
1327
+ .join("\n\n");
1328
+ return {
1329
+ content: [
1330
+ {
1331
+ type: "text" as const,
1332
+ text: truncateUtf8(text, DEFAULT_MAX_CONTEXT_BYTES).text,
1333
+ },
1334
+ ],
1335
+ details: {
1336
+ agent: summarizeStatefulAgent(agent),
1337
+ timedOut,
1338
+ timeoutMs,
1339
+ ...(output ? { output } : {}),
1340
+ ...(error ? { error } : {}),
1341
+ },
1342
+ };
1343
+ }
1344
+
1228
1345
  function normalizeRuntimeThinkingLevel(value: string): ParentRuntimeSnapshot["thinkingLevel"] {
1229
1346
  return isThinkingLevel(value) ? value : "off";
1230
1347
  }
@@ -17,7 +17,7 @@ import {
17
17
  toolHeader,
18
18
  } from "./render-common.js";
19
19
 
20
- export type StatefulRenderTool = "spawn" | "send" | "manage" | "mailbox";
20
+ export type StatefulRenderTool = "spawn" | "send" | "await" | "manage" | "mailbox";
21
21
 
22
22
  export function createStatefulToolRenderer(tool: StatefulRenderTool) {
23
23
  return {
@@ -71,6 +71,11 @@ function renderStatefulCall(tool: StatefulRenderTool, args: Record<string, unkno
71
71
  0,
72
72
  );
73
73
  }
74
+ if (tool === "await") {
75
+ const metadata = ["blocking"];
76
+ if (typeof args.timeoutMs === "number") metadata.push(`timeout:${args.timeoutMs}ms`);
77
+ return new Text(toolHeader(theme, "subagent_await", args.agentId, metadata), 0, 0);
78
+ }
74
79
  if (tool === "manage") {
75
80
  const metadata: string[] = [];
76
81
  if (typeof args.agentId === "string") metadata.push(`id:${safeLine(args.agentId, "", 256)}`);
@@ -98,9 +103,12 @@ function renderStatefulResult(
98
103
  const details = recordValue(result.details);
99
104
  const args = recordValue(context.args) ?? {};
100
105
  if (!details) return renderFallbackResult(result, options, theme, context.isError);
101
- if (tool === "spawn" || tool === "send") {
106
+ if (tool === "spawn" || tool === "send" || tool === "await") {
102
107
  const agent = recordValue(details.agent);
103
108
  if (!agent) return renderFallbackResult(result, options, theme, context.isError);
109
+ if (tool === "await" && details.timedOut === true) {
110
+ return new Text(renderAwaitTimeout(agent, result, options.expanded, theme), 0, 0);
111
+ }
104
112
  return new Text(renderAgentResult(agent, result, options.expanded, theme), 0, 0);
105
113
  }
106
114
  if (tool === "manage") {
@@ -173,6 +181,22 @@ function renderAgentResult(
173
181
  return lines.join("\n");
174
182
  }
175
183
 
184
+ function renderAwaitTimeout(
185
+ agent: Record<string, unknown>,
186
+ result: AgentToolResult<unknown>,
187
+ expanded: boolean,
188
+ theme: Theme,
189
+ ): string {
190
+ const lines = [
191
+ `${statusBadge(theme, "running")} · ${theme.fg("accent", safeLine(agent.id, "agent", 256))} · ${theme.fg("warning", "wait timed out")} · ${theme.fg("muted", safeLine(agent.state, "unknown", 128))}`,
192
+ ];
193
+ if (expanded) {
194
+ const content = safeBlock(textResult(result), "", 8 * 1024).trim();
195
+ if (content) lines.push(theme.fg("toolOutput", content));
196
+ } else lines.push(expansionHint());
197
+ return lines.join("\n");
198
+ }
199
+
176
200
  function renderManageResult(
177
201
  args: Record<string, unknown>,
178
202
  details: Record<string, unknown>,
@@ -276,6 +300,7 @@ function lifecycleStatus(state: string): RenderStatus {
276
300
  return "running";
277
301
  case "idle":
278
302
  return "idle";
303
+ case "partial":
279
304
  case "blocked":
280
305
  case "needs-input":
281
306
  case "abstained":
@@ -0,0 +1,43 @@
1
+ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
2
+ import type { SchedulingDecision } from "./adaptive-scheduler.js";
3
+ import type { AgentScope } from "./agents/types.js";
4
+ import type { OrchestrationMetrics } from "./orchestration-metrics.js";
5
+ import type { PanelSynthesis } from "./panel-contract.js";
6
+ import type { PanelEvidenceArtifact } from "./panel-evidence.js";
7
+ import type { PanelFailure } from "./panel-failure.js";
8
+ import type { PanelPhaseBudgets, PanelPreset } from "./panel-planning.js";
9
+ import type { SingleResult } from "./runner-types.js";
10
+ import type { WorkItemLedgerSnapshot } from "./work-item-ledger.js";
11
+
12
+ export interface PanelDetails {
13
+ id: string;
14
+ preset: PanelPreset;
15
+ sharedTaskPreview: string;
16
+ state: "running" | "completed" | "degraded" | "insufficient-panel" | "failed" | "cancelled";
17
+ reviewerIds: string[];
18
+ validReviewCount: number;
19
+ failedReviewCount: number;
20
+ blockingObjectionCount: number;
21
+ dissentCount: number;
22
+ budgets: PanelPhaseBudgets;
23
+ evidence: PanelEvidenceArtifact[];
24
+ failures: PanelFailure[];
25
+ synthesis?: PanelSynthesis;
26
+ synthesizerResult?: SingleResult;
27
+ cleanupComplete: boolean;
28
+ }
29
+
30
+ export interface SubagentDetails {
31
+ mode: "single" | "parallel" | "chain" | "workflow" | "panel";
32
+ agentScope: AgentScope;
33
+ projectAgentsDir: string | null;
34
+ results: SingleResult[];
35
+ aggregator?: SingleResult;
36
+ workflow?: WorkItemLedgerSnapshot;
37
+ schedulerDecisions?: SchedulingDecision[];
38
+ metrics?: OrchestrationMetrics;
39
+ panel?: PanelDetails;
40
+ isError?: boolean;
41
+ }
42
+
43
+ export type OnUpdateCallback = (partial: AgentToolResult<SubagentDetails>) => void;
@@ -39,7 +39,6 @@ import {
39
39
  import { MAX_BLOCKING_PARALLEL_CONCURRENCY } from "./limits.js";
40
40
  import { SubagentParams } from "./params.js";
41
41
  import { renderSubagentCall, renderSubagentResult } from "./render.js";
42
- import type { SubagentDetails } from "./runner.js";
43
42
  import {
44
43
  consumeSubagentSettingsNotice,
45
44
  DEFAULT_CONSULT_RESOURCE_POLICY,
@@ -50,7 +49,14 @@ import {
50
49
  resolveBlockingMaxParallelTasks,
51
50
  } from "./settings-reader.js";
52
51
  import { registerStatefulSubagents } from "./stateful-registration.js";
52
+ import type { SubagentDetails } from "./subagent-details.js";
53
53
  import type { SubagentTransport } from "./transport.js";
54
+ import {
55
+ registerUsageRecording,
56
+ type UsageRecordingDependencies,
57
+ type UsageSurfaceArm,
58
+ } from "./usage-recording.js";
59
+ import { resolveUsageRecordingEnabled } from "./usage-recording-config.js";
54
60
 
55
61
  type BlockingExecutionModule = Pick<typeof import("./execution.js"), "executeSubagent">;
56
62
 
@@ -60,6 +66,7 @@ export interface SubagentsDependencies {
60
66
  config?: ConfigRegistrationDependencies;
61
67
  consult?: ConsultRegistrationDependencies;
62
68
  inspect?: InspectRegistrationDependencies;
69
+ usageRecording?: Partial<UsageRecordingDependencies>;
63
70
  }
64
71
 
65
72
  export default function (pi: ExtensionAPI, dependencies: SubagentsDependencies = {}) {
@@ -68,6 +75,7 @@ export default function (pi: ExtensionAPI, dependencies: SubagentsDependencies =
68
75
  dependencies.loadBlockingExecution ?? (() => import("./execution.js")),
69
76
  );
70
77
  const configOwner = registerSubagentConfigLifecycle(pi);
78
+ const usageRecording = registerUsageRecording(pi, dependencies.usageRecording);
71
79
  const settings = readSubagentSettings();
72
80
  let currentSettings: SubagentSettings | undefined = settings;
73
81
  let currentCatalog = "";
@@ -78,7 +86,7 @@ export default function (pi: ExtensionAPI, dependencies: SubagentsDependencies =
78
86
  let refreshStatefulCatalog: (catalog: string) => void = () => undefined;
79
87
  let refreshConsultCatalog: (catalog: string) => void = () => undefined;
80
88
 
81
- pi.on("session_start", (_event, ctx) => {
89
+ pi.on("session_start", async (event, ctx) => {
82
90
  // Preserve a one-shot migration notice from extension load while refreshing
83
91
  // validation against settings that may have changed before this session.
84
92
  const loadNotice = consumeSubagentSettingsNotice();
@@ -96,6 +104,14 @@ export default function (pi: ExtensionAPI, dependencies: SubagentsDependencies =
96
104
  refreshBlockingCatalog(currentCatalog);
97
105
  refreshStatefulCatalog(currentCatalog);
98
106
  refreshConsultCatalog(currentCatalog);
107
+ await usageRecording.startSession({
108
+ enabled: resolveUsageRecordingEnabled(currentSettings?.usageRecording),
109
+ surfaceArm: usageSurfaceArm(blockingEnabled, statefulRuntime.getRuntimeStatus().enabled),
110
+ reason: event.reason,
111
+ onWarning: (message) => {
112
+ if (ctx.hasUI) ctx.ui.notify(message, "warning");
113
+ },
114
+ });
99
115
  });
100
116
 
101
117
  const statefulRuntime = registerStatefulSubagents(pi, {
@@ -103,6 +119,7 @@ export default function (pi: ExtensionAPI, dependencies: SubagentsDependencies =
103
119
  settings: settings?.stateful,
104
120
  getSettings: () => currentSettings,
105
121
  loadTransport: dependencies.loadStatefulTransport,
122
+ usageRecording,
106
123
  });
107
124
  refreshStatefulCatalog = statefulRuntime.setAgentCatalog;
108
125
  const getBlockingEnabled = () => blockingEnabled;
@@ -122,6 +139,7 @@ export default function (pi: ExtensionAPI, dependencies: SubagentsDependencies =
122
139
  getConsultResourcePolicy,
123
140
  getConsultationCwdPolicy,
124
141
  getDelegationCwdPolicy,
142
+ getUsageRecordingStatus: () => usageRecording.getStatus(),
125
143
  },
126
144
  dependencies.inspect,
127
145
  );
@@ -141,6 +159,15 @@ export default function (pi: ExtensionAPI, dependencies: SubagentsDependencies =
141
159
  getConsultResourcePolicy,
142
160
  getConsultationCwdPolicy,
143
161
  getDelegationCwdPolicy,
162
+ getUsageRecordingEnabled: () => usageRecording.getStatus().enabled,
163
+ getUsageRecordingStatus: () => usageRecording.getStatus(),
164
+ setUsageRecordingEnabled: async (value: boolean) => {
165
+ await usageRecording.setEnabled(value);
166
+ currentSettings = {
167
+ ...(currentSettings ?? {}),
168
+ usageRecording: { ...(currentSettings?.usageRecording ?? {}), enabled: value },
169
+ };
170
+ },
144
171
  setMaxParallelTasks(value: number) {
145
172
  const previousSettings = currentSettings;
146
173
  currentSettings = {
@@ -188,6 +215,14 @@ export default function (pi: ExtensionAPI, dependencies: SubagentsDependencies =
188
215
  configOwner,
189
216
  dependencies.config,
190
217
  );
218
+ pi.on("session_shutdown", (event) => usageRecording.shutdown(event.reason));
219
+ }
220
+
221
+ function usageSurfaceArm(blockingEnabled: boolean, statefulEnabled: boolean): UsageSurfaceArm {
222
+ if (blockingEnabled && statefulEnabled) return "all";
223
+ if (statefulEnabled) return "async-only";
224
+ if (blockingEnabled) return "blocking-only";
225
+ return "disabled";
191
226
  }
192
227
 
193
228
  function registerBlockingSubagent(
@@ -196,6 +231,7 @@ function registerBlockingSubagent(
196
231
  loadExecution: () => Promise<BlockingExecutionModule>,
197
232
  ): (catalog: string) => void {
198
233
  let catalog = "";
234
+ let deprecationWarningShown = false;
199
235
  const activeControllers = new Set<AbortController>();
200
236
  const activeWork = new Set<Promise<unknown>>();
201
237
  const cancelAndWaitForWork = async (reason: string) => {
@@ -204,10 +240,20 @@ function registerBlockingSubagent(
204
240
  }
205
241
  await Promise.allSettled([...activeWork]);
206
242
  };
207
- pi.on("session_start", () => cancelAndWaitForWork("Blocking subagent session replaced"));
243
+ pi.on("session_start", () => {
244
+ deprecationWarningShown = false;
245
+ return cancelAndWaitForWork("Blocking subagent session replaced");
246
+ });
208
247
  pi.on("session_shutdown", () => cancelAndWaitForWork("Blocking subagent session shut down"));
248
+ const statefulEnabled = () => getSettings()?.stateful?.enabled !== false;
249
+ const deprecationAlternatives = () =>
250
+ statefulEnabled()
251
+ ? "Prefer the main agent for tightly coupled work, subagent_spawn for detached work, subagent_await for an intentional retained-agent join, or subagent_consult for bounded synchronous read-only evidence."
252
+ : "Prefer the main agent for tightly coupled work or subagent_consult for bounded synchronous read-only evidence; enable the background workflow before using detached alternatives.";
209
253
  const baseDescription = () =>
210
254
  [
255
+ "Deprecated compatibility tool: do not choose subagent for new work.",
256
+ deprecationAlternatives(),
211
257
  "Run specialized subagents as a blocking operation with isolated contexts.",
212
258
  "The call blocks the main agent until every worker and optional aggregator finishes, so queued steering waits.",
213
259
  "Modes: single (agent + task), parallel (tasks array), chain (sequential with {previous} placeholder), workflow (named dependency tasks with optional capability routing), or panel (independent reviewers plus evidence-preserving synthesis).",
@@ -218,14 +264,17 @@ function registerBlockingSubagent(
218
264
  `Working-directory target policy: ${getSettings()?.cwdPolicy?.delegation ?? DEFAULT_DELEGATION_CWD_POLICY}. This controls launch targets and protected project resources, not filesystem access or sandboxing.`,
219
265
  ].join(" ");
220
266
  const promptGuidelines = () => [
221
- "Use subagent only when delegation fits; the main agent should decide how many subagents to spawn from task shape instead of waiting for the user to specify a count.",
267
+ statefulEnabled()
268
+ ? "The subagent tool is deprecated for new work; prefer the main agent, subagent_spawn with supported completion delivery, subagent_await for an intentional retained-agent join, or subagent_consult for bounded synchronous read-only evidence."
269
+ : "The subagent tool is deprecated for new work; prefer the main agent or subagent_consult for bounded synchronous read-only evidence, and enable the background workflow before using detached alternatives.",
270
+ "Use deprecated subagent only for an existing caller or an explicit user request whose blocking chain, fan-in, panel, or workflow semantics do not yet have a detached replacement.",
271
+ "When compatibility requires subagent, decide how many subagents to spawn from task shape instead of waiting for the user to specify a count.",
222
272
  "The main agent retains overall planning, immediate critical-path work, integration, final verification, and the final answer.",
223
273
  "Use no subagent for simple answers, quick targeted edits, latency-sensitive one-step work, tasks requiring frequent user back-and-forth, or critical-path work the main agent can perform directly.",
224
274
  "One ordinary implementation worker should not replace work the main agent can perform directly; use a blocking single only when intentional synchronous isolation or a user-requested specialist justifies waiting.",
225
275
  "Keep ordinary planning in the main agent, or use explicit workflow mode when a genuine dependency graph requires caller-authored orchestration.",
226
276
  "Keep ordinary review in the main agent with a review skill and deterministic checks; reserve panel mode or custom verifier agents for consequential independent verification.",
227
- "Use the blocking subagent tool only when delegated outputs are required before the main agent's next action and waiting is intentional; the main agent cannot process queued steering until the call returns.",
228
- "Use a blocking subagent single, parallel, chain, workflow, panel, or fan-in call only when synchronous context or output isolation is worth making the main agent unavailable while it runs.",
277
+ "A compatibility subagent call blocks the main agent from processing queued steering until it returns; use it only when the explicit legacy workflow justifies making Pi unavailable.",
229
278
  `If a blocking parallel subagent call is genuinely required, keep tasks independent, stay within the configured max ${resolveBlockingMaxParallelTasks(getSettings())}, and avoid write-heavy implementation touching the same files or shared state.`,
230
279
  "For parallel subagent calls, omit the aggregator key entirely unless a fan-in step is required; do not send null, empty strings, or an empty object for unused optional fields.",
231
280
  "Use workflow mode for explicit dependencies or capability routing; declare read/write or ownership scopes, require structured-v2 artifacts when downstream tasks consume them, and use retry or hedging only with the required side-effect contract.",
@@ -236,10 +285,10 @@ function registerBlockingSubagent(
236
285
  ];
237
286
  const definition: ToolDefinition<typeof SubagentParams, SubagentDetails> = {
238
287
  name: "subagent",
239
- label: "Blocking Subagent",
288
+ label: "Blocking Subagent · Deprecated",
240
289
  description: appendAgentCatalog(baseDescription(), catalog),
241
290
  promptSnippet:
242
- "Run blocking isolated subagents only when their outputs are required before the main agent can continue.",
291
+ "Deprecated blocking subagent compatibility tool; prefer detached or read-only alternatives.",
243
292
  promptGuidelines: promptGuidelines(),
244
293
  parameters: SubagentParams,
245
294
 
@@ -251,6 +300,15 @@ function registerBlockingSubagent(
251
300
  : lifecycleController.signal;
252
301
  const work = (async () => {
253
302
  throwIfAborted(effectiveSignal, "Blocking subagent execution was cancelled");
303
+ if (!deprecationWarningShown && ctx.hasUI) {
304
+ deprecationWarningShown = true;
305
+ ctx.ui.notify(
306
+ statefulEnabled()
307
+ ? "subagent is deprecated for new work. Prefer the main agent, subagent_spawn with completion delivery, subagent_await for an intentional join, or subagent_consult for synchronous read-only evidence."
308
+ : "subagent is deprecated for new work. Prefer the main agent or subagent_consult; enable the background workflow before using detached alternatives.",
309
+ "warning",
310
+ );
311
+ }
254
312
  let executionModule: BlockingExecutionModule;
255
313
  try {
256
314
  executionModule = await loadExecution();
package/src/subagents.ts CHANGED
@@ -11,6 +11,7 @@ export {
11
11
  inspectDelegationWorkflowSettings,
12
12
  inspectStatefulLimitSettings,
13
13
  inspectSubagentSettings,
14
+ inspectUsageRecordingSettings,
14
15
  normalizeAgentSettings,
15
16
  normalizeSubagentSettings,
16
17
  readSubagentSettings,
@@ -27,6 +28,7 @@ export {
27
28
  updateCwdPolicySetting,
28
29
  updateDelegationWorkflowSetting,
29
30
  updateStatefulLimitSetting,
31
+ updateUsageRecordingSetting,
30
32
  } from "./settings.js";
31
33
  export { default, type SubagentsDependencies } from "./subagents-extension.js";
32
34
  export { formatTokens, formatUsageStats } from "./usage-format.js";
@@ -8,9 +8,10 @@ import {
8
8
  } from "./peer-transport.js";
9
9
  import { resolvePiPromptResources } from "./prompt-resources.js";
10
10
  import type { ManagedAgent, TurnOutcome } from "./registry.js";
11
- import { getResultFinalOutput, runSingleAgent, type SubagentDetails } from "./runner.js";
11
+ import { getResultFinalOutput, runSingleAgent } from "./runner.js";
12
12
  import { readSubagentSettings, resolveSubagentThinkingLevel } from "./settings.js";
13
13
  import { buildStatefulTurnPrompt, resolveStatefulTurnTimeout } from "./stateful-prompt.js";
14
+ import type { SubagentDetails } from "./subagent-details.js";
14
15
  import type { SubagentTransport } from "./transport.js";
15
16
  import type { TransportProgressCallback, TransportTelemetry } from "./transport-types.js";
16
17
 
@@ -1,4 +1,5 @@
1
- import { isResultError, type SingleResult } from "./runner.js";
1
+ import { isResultError } from "./runner-outcome.js";
2
+ import type { SingleResult } from "./runner-types.js";
2
3
 
3
4
  const HEDGE_LOSER_GRACE_MS = 5_000;
4
5