@narumitw/pi-subagents 2.1.3 → 3.0.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 (314) hide show
  1. package/README.md +181 -1250
  2. package/dist/child-communication-bridge.ts +136 -0
  3. package/dist/child-communication-bridge.ts.map +7 -0
  4. package/dist/chunks/chunk-UKTWUQRM.js +667 -0
  5. package/dist/chunks/chunk-UKTWUQRM.js.map +7 -0
  6. package/dist/index.ts +1448 -1598
  7. package/dist/index.ts.map +4 -4
  8. package/docs/design-principles.md +31 -0
  9. package/docs/ipc-transport-idea.md +103 -0
  10. package/docs/tools.md +109 -0
  11. package/package.json +11 -19
  12. package/src/broker-credentials.ts +71 -0
  13. package/src/child-communication-bridge.ts +153 -0
  14. package/src/child-communication-tools.ts +147 -0
  15. package/src/completion-renderer.ts +60 -0
  16. package/src/index.ts +1 -1
  17. package/src/message-broker.ts +628 -0
  18. package/src/model-output.ts +42 -0
  19. package/src/process.ts +688 -0
  20. package/src/runtime.ts +538 -0
  21. package/src/subagents.ts +33 -34
  22. package/src/tools.ts +416 -0
  23. package/src/types.ts +85 -0
  24. package/src/widget.ts +130 -0
  25. package/dist/chunks/auto-transport-FUUKFDIG.ts +0 -109
  26. package/dist/chunks/auto-transport-FUUKFDIG.ts.map +0 -7
  27. package/dist/chunks/capability-grant-PR72SWWS.ts +0 -19
  28. package/dist/chunks/capability-grant-PR72SWWS.ts.map +0 -7
  29. package/dist/chunks/child-peer-bridge.ts +0 -108
  30. package/dist/chunks/child-peer-bridge.ts.map +0 -7
  31. package/dist/chunks/chunk-2LMJU25E.ts +0 -1641
  32. package/dist/chunks/chunk-2LMJU25E.ts.map +0 -7
  33. package/dist/chunks/chunk-47SK2QYC.ts +0 -56
  34. package/dist/chunks/chunk-47SK2QYC.ts.map +0 -7
  35. package/dist/chunks/chunk-4AQSF7AS.ts +0 -29
  36. package/dist/chunks/chunk-4AQSF7AS.ts.map +0 -7
  37. package/dist/chunks/chunk-5DGRMKNV.ts +0 -126
  38. package/dist/chunks/chunk-5DGRMKNV.ts.map +0 -7
  39. package/dist/chunks/chunk-6H6TBBED.ts +0 -108
  40. package/dist/chunks/chunk-6H6TBBED.ts.map +0 -7
  41. package/dist/chunks/chunk-6KJ34M6S.ts +0 -159
  42. package/dist/chunks/chunk-6KJ34M6S.ts.map +0 -7
  43. package/dist/chunks/chunk-6NSJVPXX.ts +0 -480
  44. package/dist/chunks/chunk-6NSJVPXX.ts.map +0 -7
  45. package/dist/chunks/chunk-7AAJEUSL.ts +0 -238
  46. package/dist/chunks/chunk-7AAJEUSL.ts.map +0 -7
  47. package/dist/chunks/chunk-BFK2Z4ZF.ts +0 -189
  48. package/dist/chunks/chunk-BFK2Z4ZF.ts.map +0 -7
  49. package/dist/chunks/chunk-BOAXY55Y.ts +0 -153
  50. package/dist/chunks/chunk-BOAXY55Y.ts.map +0 -7
  51. package/dist/chunks/chunk-D4CR7T73.ts +0 -353
  52. package/dist/chunks/chunk-D4CR7T73.ts.map +0 -7
  53. package/dist/chunks/chunk-FEVPWRMU.ts +0 -300
  54. package/dist/chunks/chunk-FEVPWRMU.ts.map +0 -7
  55. package/dist/chunks/chunk-G6VZTCFH.ts +0 -70
  56. package/dist/chunks/chunk-G6VZTCFH.ts.map +0 -7
  57. package/dist/chunks/chunk-H3FP6DLR.ts +0 -160
  58. package/dist/chunks/chunk-H3FP6DLR.ts.map +0 -7
  59. package/dist/chunks/chunk-HA36LPCO.ts +0 -130
  60. package/dist/chunks/chunk-HA36LPCO.ts.map +0 -7
  61. package/dist/chunks/chunk-HOP5FXDT.ts +0 -27
  62. package/dist/chunks/chunk-HOP5FXDT.ts.map +0 -7
  63. package/dist/chunks/chunk-IGAMVWFN.ts +0 -139
  64. package/dist/chunks/chunk-IGAMVWFN.ts.map +0 -7
  65. package/dist/chunks/chunk-ILEQ27AL.ts +0 -45
  66. package/dist/chunks/chunk-ILEQ27AL.ts.map +0 -7
  67. package/dist/chunks/chunk-ITVWPNU4.ts +0 -819
  68. package/dist/chunks/chunk-ITVWPNU4.ts.map +0 -7
  69. package/dist/chunks/chunk-IWC32VPY.ts +0 -172
  70. package/dist/chunks/chunk-IWC32VPY.ts.map +0 -7
  71. package/dist/chunks/chunk-JSZIP73U.ts +0 -362
  72. package/dist/chunks/chunk-JSZIP73U.ts.map +0 -7
  73. package/dist/chunks/chunk-JU6LUNLP.ts +0 -380
  74. package/dist/chunks/chunk-JU6LUNLP.ts.map +0 -7
  75. package/dist/chunks/chunk-KMGKCEO4.ts +0 -38
  76. package/dist/chunks/chunk-KMGKCEO4.ts.map +0 -7
  77. package/dist/chunks/chunk-LASD73CM.ts +0 -339
  78. package/dist/chunks/chunk-LASD73CM.ts.map +0 -7
  79. package/dist/chunks/chunk-LEOYDZI3.ts +0 -22
  80. package/dist/chunks/chunk-LEOYDZI3.ts.map +0 -7
  81. package/dist/chunks/chunk-LL4LP2T7.ts +0 -737
  82. package/dist/chunks/chunk-LL4LP2T7.ts.map +0 -7
  83. package/dist/chunks/chunk-N2T5IN4X.ts +0 -18
  84. package/dist/chunks/chunk-N2T5IN4X.ts.map +0 -7
  85. package/dist/chunks/chunk-N7BLVXKK.ts +0 -73
  86. package/dist/chunks/chunk-N7BLVXKK.ts.map +0 -7
  87. package/dist/chunks/chunk-NLT67IZS.ts +0 -322
  88. package/dist/chunks/chunk-NLT67IZS.ts.map +0 -7
  89. package/dist/chunks/chunk-NTRPLF46.ts +0 -63
  90. package/dist/chunks/chunk-NTRPLF46.ts.map +0 -7
  91. package/dist/chunks/chunk-ONDTY4EL.ts +0 -91
  92. package/dist/chunks/chunk-ONDTY4EL.ts.map +0 -7
  93. package/dist/chunks/chunk-OVPHFGKO.ts +0 -1769
  94. package/dist/chunks/chunk-OVPHFGKO.ts.map +0 -7
  95. package/dist/chunks/chunk-P7OH4XMF.ts +0 -60
  96. package/dist/chunks/chunk-P7OH4XMF.ts.map +0 -7
  97. package/dist/chunks/chunk-PBZMBTNJ.ts +0 -422
  98. package/dist/chunks/chunk-PBZMBTNJ.ts.map +0 -7
  99. package/dist/chunks/chunk-PGLSFLYW.ts +0 -52
  100. package/dist/chunks/chunk-PGLSFLYW.ts.map +0 -7
  101. package/dist/chunks/chunk-RRR66UWR.ts +0 -28
  102. package/dist/chunks/chunk-RRR66UWR.ts.map +0 -7
  103. package/dist/chunks/chunk-RSUXZD6S.ts +0 -275
  104. package/dist/chunks/chunk-RSUXZD6S.ts.map +0 -7
  105. package/dist/chunks/chunk-RTCVYIZA.ts +0 -221
  106. package/dist/chunks/chunk-RTCVYIZA.ts.map +0 -7
  107. package/dist/chunks/chunk-RY2AEGMZ.ts +0 -34
  108. package/dist/chunks/chunk-RY2AEGMZ.ts.map +0 -7
  109. package/dist/chunks/chunk-SWGQLFSD.ts +0 -60
  110. package/dist/chunks/chunk-SWGQLFSD.ts.map +0 -7
  111. package/dist/chunks/chunk-TM2R67J3.ts +0 -52
  112. package/dist/chunks/chunk-TM2R67J3.ts.map +0 -7
  113. package/dist/chunks/chunk-TMZRHIIK.ts +0 -497
  114. package/dist/chunks/chunk-TMZRHIIK.ts.map +0 -7
  115. package/dist/chunks/chunk-TZ34IQ3M.ts +0 -59
  116. package/dist/chunks/chunk-TZ34IQ3M.ts.map +0 -7
  117. package/dist/chunks/chunk-UMBPMVJW.ts +0 -22
  118. package/dist/chunks/chunk-UMBPMVJW.ts.map +0 -7
  119. package/dist/chunks/chunk-VDG7LTYE.ts +0 -80
  120. package/dist/chunks/chunk-VDG7LTYE.ts.map +0 -7
  121. package/dist/chunks/chunk-X4NMONPE.ts +0 -73
  122. package/dist/chunks/chunk-X4NMONPE.ts.map +0 -7
  123. package/dist/chunks/chunk-YPJEN6NU.ts +0 -65
  124. package/dist/chunks/chunk-YPJEN6NU.ts.map +0 -7
  125. package/dist/chunks/chunk-YU53SHA7.ts +0 -328
  126. package/dist/chunks/chunk-YU53SHA7.ts.map +0 -7
  127. package/dist/chunks/completion-delivery-RSJU6BXL.ts +0 -17
  128. package/dist/chunks/completion-delivery-RSJU6BXL.ts.map +0 -7
  129. package/dist/chunks/config-status-FKGDECZ3.ts +0 -35
  130. package/dist/chunks/config-status-FKGDECZ3.ts.map +0 -7
  131. package/dist/chunks/config-ui-ABHYNGQ7.ts +0 -1212
  132. package/dist/chunks/config-ui-ABHYNGQ7.ts.map +0 -7
  133. package/dist/chunks/consult-LJU3IQY5.ts +0 -519
  134. package/dist/chunks/consult-LJU3IQY5.ts.map +0 -7
  135. package/dist/chunks/context-QDKLVQXE.ts +0 -11
  136. package/dist/chunks/context-QDKLVQXE.ts.map +0 -7
  137. package/dist/chunks/create-stateful-transport-JWC2EFYL.ts +0 -130
  138. package/dist/chunks/create-stateful-transport-JWC2EFYL.ts.map +0 -7
  139. package/dist/chunks/cwd-policy-NB6XZ5GF.ts +0 -17
  140. package/dist/chunks/cwd-policy-NB6XZ5GF.ts.map +0 -7
  141. package/dist/chunks/delegation-contract-LA56I5DT.ts +0 -24
  142. package/dist/chunks/delegation-contract-LA56I5DT.ts.map +0 -7
  143. package/dist/chunks/discovery-MIFB2U4Y.ts +0 -12
  144. package/dist/chunks/discovery-MIFB2U4Y.ts.map +0 -7
  145. package/dist/chunks/execution-Q2JZLKJA.ts +0 -4037
  146. package/dist/chunks/execution-Q2JZLKJA.ts.map +0 -7
  147. package/dist/chunks/in-process-transport-HJ6TZXC3.ts +0 -39
  148. package/dist/chunks/in-process-transport-HJ6TZXC3.ts.map +0 -7
  149. package/dist/chunks/inspect-UH2TKH6E.ts +0 -634
  150. package/dist/chunks/inspect-UH2TKH6E.ts.map +0 -7
  151. package/dist/chunks/peer-communication-KL36Z6CO.ts +0 -294
  152. package/dist/chunks/peer-communication-KL36Z6CO.ts.map +0 -7
  153. package/dist/chunks/persistence-UY3PY6E5.ts +0 -348
  154. package/dist/chunks/persistence-UY3PY6E5.ts.map +0 -7
  155. package/dist/chunks/registry-BT54L6CY.ts +0 -1445
  156. package/dist/chunks/registry-BT54L6CY.ts.map +0 -7
  157. package/dist/chunks/retained-semantic-state-GM75FZGE.ts +0 -81
  158. package/dist/chunks/retained-semantic-state-GM75FZGE.ts.map +0 -7
  159. package/dist/chunks/rpc-transport-7R7DVCEB.ts +0 -1222
  160. package/dist/chunks/rpc-transport-7R7DVCEB.ts.map +0 -7
  161. package/dist/chunks/runtime-policy-5YELCOVC.ts +0 -15
  162. package/dist/chunks/runtime-policy-5YELCOVC.ts.map +0 -7
  163. package/dist/chunks/semantic-snapshot-WPJ3UAFW.ts +0 -19
  164. package/dist/chunks/semantic-snapshot-WPJ3UAFW.ts.map +0 -7
  165. package/dist/chunks/spawn-idempotency-BNHOSMZW.ts +0 -13
  166. package/dist/chunks/spawn-idempotency-BNHOSMZW.ts.map +0 -7
  167. package/dist/chunks/stateful-lifecycle-JAR6K55H.ts +0 -15
  168. package/dist/chunks/stateful-lifecycle-JAR6K55H.ts.map +0 -7
  169. package/dist/chunks/subprocess-transport-VHZJRBTW.ts +0 -160
  170. package/dist/chunks/subprocess-transport-VHZJRBTW.ts.map +0 -7
  171. package/dist/chunks/usage-recording-store-CW3EH2SZ.ts +0 -164
  172. package/dist/chunks/usage-recording-store-CW3EH2SZ.ts.map +0 -7
  173. package/dist/chunks/workspace-WDKLN72J.ts +0 -11
  174. package/dist/chunks/workspace-WDKLN72J.ts.map +0 -7
  175. package/docs/async-runtime-protocol.md +0 -84
  176. package/docs/implementation-notes/pi-subagents-capability-matrix.md +0 -62
  177. package/docs/implementation-notes/pi-subagents-current-direction.md +0 -99
  178. package/docs/implementation-notes/pi-subagents-rpc-v1.md +0 -153
  179. package/docs/pi-subagents-diagrams.md +0 -183
  180. package/src/adaptive-scheduler.ts +0 -224
  181. package/src/admission-benchmark.ts +0 -95
  182. package/src/admission-policy.ts +0 -78
  183. package/src/agent-projection.ts +0 -53
  184. package/src/agents/built-ins.ts +0 -71
  185. package/src/agents/catalog.ts +0 -241
  186. package/src/agents/discovery.ts +0 -265
  187. package/src/agents/types.ts +0 -100
  188. package/src/agents.ts +0 -51
  189. package/src/async-subagent-benchmark.ts +0 -532
  190. package/src/auto-transport.ts +0 -121
  191. package/src/blocking-status.ts +0 -63
  192. package/src/cached-module-loader.ts +0 -18
  193. package/src/capabilities.ts +0 -145
  194. package/src/capability-grant.ts +0 -115
  195. package/src/capability-router.ts +0 -107
  196. package/src/child-peer-bridge.ts +0 -124
  197. package/src/child-peer-tools.ts +0 -132
  198. package/src/completion-delivery.ts +0 -409
  199. package/src/completion-render.ts +0 -190
  200. package/src/completion-requirement.ts +0 -479
  201. package/src/completion-routing.ts +0 -24
  202. package/src/config-registration.ts +0 -114
  203. package/src/config-status.ts +0 -217
  204. package/src/config-ui.ts +0 -983
  205. package/src/consult-policy.ts +0 -15
  206. package/src/consult-registration.ts +0 -125
  207. package/src/consult-render.ts +0 -194
  208. package/src/consult-resources.ts +0 -51
  209. package/src/consult-tool.ts +0 -95
  210. package/src/consult.ts +0 -566
  211. package/src/context.ts +0 -126
  212. package/src/create-stateful-transport.ts +0 -157
  213. package/src/cwd-policy.ts +0 -183
  214. package/src/delegation-contract.ts +0 -463
  215. package/src/execution/budget.ts +0 -56
  216. package/src/execution/runtime-policy.ts +0 -19
  217. package/src/execution-plan.ts +0 -322
  218. package/src/execution-ui.ts +0 -259
  219. package/src/execution.ts +0 -1679
  220. package/src/in-process-transport.ts +0 -941
  221. package/src/inspect-registration.ts +0 -64
  222. package/src/inspect-render.ts +0 -334
  223. package/src/inspect-tool.ts +0 -45
  224. package/src/inspect.ts +0 -769
  225. package/src/integration-controller.ts +0 -98
  226. package/src/limits.ts +0 -75
  227. package/src/orchestration-metrics.ts +0 -116
  228. package/src/outcome.ts +0 -61
  229. package/src/panel-child-group.ts +0 -35
  230. package/src/panel-contract.ts +0 -343
  231. package/src/panel-evidence.ts +0 -59
  232. package/src/panel-execution.ts +0 -769
  233. package/src/panel-failure.ts +0 -56
  234. package/src/panel-planning.ts +0 -175
  235. package/src/panel-presets.ts +0 -3
  236. package/src/panel-prompts.ts +0 -132
  237. package/src/panel-reconciliation.ts +0 -57
  238. package/src/panel-render.ts +0 -103
  239. package/src/parallel-limit-ui.ts +0 -113
  240. package/src/params.ts +0 -278
  241. package/src/peer-communication.ts +0 -352
  242. package/src/peer-transport.ts +0 -49
  243. package/src/persistence.ts +0 -522
  244. package/src/pi-args.ts +0 -43
  245. package/src/pi-invocation.ts +0 -168
  246. package/src/process-control.ts +0 -43
  247. package/src/prompt-resources.ts +0 -38
  248. package/src/prompt-source-safety.ts +0 -48
  249. package/src/protocol.ts +0 -76
  250. package/src/registry-types.ts +0 -204
  251. package/src/registry.ts +0 -1698
  252. package/src/render-common.ts +0 -252
  253. package/src/render.ts +0 -704
  254. package/src/result-contract.ts +0 -431
  255. package/src/retained-semantic-state.ts +0 -100
  256. package/src/rpc-timeout-finalization.ts +0 -207
  257. package/src/rpc-transport-metadata.ts +0 -65
  258. package/src/rpc-transport.ts +0 -1035
  259. package/src/rpc-turn-capture.ts +0 -217
  260. package/src/runner-outcome.ts +0 -31
  261. package/src/runner-result.ts +0 -55
  262. package/src/runner-types.ts +0 -102
  263. package/src/runner-usage.ts +0 -48
  264. package/src/runner.ts +0 -866
  265. package/src/safe-text.ts +0 -67
  266. package/src/semantic-snapshot.ts +0 -214
  267. package/src/session-guidance-contract.ts +0 -399
  268. package/src/settings/inspection.ts +0 -302
  269. package/src/settings/schema.ts +0 -195
  270. package/src/settings-reader.ts +0 -215
  271. package/src/settings.ts +0 -452
  272. package/src/spawn-idempotency.ts +0 -68
  273. package/src/stateful-agent-view.ts +0 -85
  274. package/src/stateful-config.ts +0 -13
  275. package/src/stateful-guidance.ts +0 -29
  276. package/src/stateful-lifecycle.ts +0 -74
  277. package/src/stateful-limit-ui.ts +0 -249
  278. package/src/stateful-limits.ts +0 -96
  279. package/src/stateful-prompt.ts +0 -49
  280. package/src/stateful-registration.ts +0 -1320
  281. package/src/stateful-render.ts +0 -318
  282. package/src/stateful-safety.ts +0 -47
  283. package/src/stateful-tool-params.ts +0 -215
  284. package/src/stateful.ts +0 -12
  285. package/src/subagent-details.ts +0 -43
  286. package/src/subagents-extension.ts +0 -355
  287. package/src/subprocess-transport.ts +0 -145
  288. package/src/supervision.ts +0 -104
  289. package/src/task-path.ts +0 -65
  290. package/src/timeout-checkpoint.ts +0 -305
  291. package/src/timeout-finalization.ts +0 -75
  292. package/src/tool-schema-compatibility.ts +0 -73
  293. package/src/transport-types.ts +0 -74
  294. package/src/transport-ui.ts +0 -135
  295. package/src/transport.ts +0 -43
  296. package/src/turn-budget.ts +0 -109
  297. package/src/usage-format.ts +0 -42
  298. package/src/usage-recording-config.ts +0 -13
  299. package/src/usage-recording-store.ts +0 -183
  300. package/src/usage-recording.ts +0 -478
  301. package/src/verification-harness.ts +0 -516
  302. package/src/verification-policy.ts +0 -67
  303. package/src/verification-receipt.ts +0 -275
  304. package/src/verified-execution-benchmark.ts +0 -86
  305. package/src/verified-execution-contract.ts +0 -191
  306. package/src/verified-execution-schema.ts +0 -32
  307. package/src/work-item-ledger.ts +0 -1404
  308. package/src/work-item-persistence.ts +0 -254
  309. package/src/workflow-completion-controller.ts +0 -397
  310. package/src/workflow-planning.ts +0 -172
  311. package/src/workflow-tree-identity.ts +0 -289
  312. package/src/workflow-ui.ts +0 -69
  313. package/src/workflow-verification.ts +0 -296
  314. package/src/workspace.ts +0 -174
package/src/registry.ts DELETED
@@ -1,1698 +0,0 @@
1
- /**
2
- * AgentRegistry intentionally owns its full state machine in one module so queue, tree,
3
- * mailbox, generation, grant, transport, persistence, and completion transitions are atomic.
4
- */
5
- import { randomUUID } from "node:crypto";
6
- import { projectAgentRecords } from "./agent-projection.js";
7
- import type { SubagentThinkingLevel } from "./agents/types.js";
8
- import {
9
- type CapabilityGrant,
10
- isCapabilityGrantActive,
11
- revokeCapabilityGrant,
12
- } from "./capability-grant.js";
13
- import {
14
- beginCompletionRequirement,
15
- type CompletionRequirementMode,
16
- cancelPendingCompletionRequirements,
17
- completionRequirementKey,
18
- MAX_UNRESOLVED_REQUIRED_COMPLETIONS,
19
- makeCompletionAvailable,
20
- makeCompletionVisible,
21
- pendingRequiredCompletionCount,
22
- } from "./completion-requirement.js";
23
- import { resolveCompletionRecipient } from "./completion-routing.js";
24
- import type { TargetPolicyAudit } from "./cwd-policy.js";
25
- import type { DelegationContract } from "./delegation-contract.js";
26
- import {
27
- copyExecutionPlan,
28
- type ExecutionPlan,
29
- rotateExecutionPlanGeneration,
30
- } from "./execution-plan.js";
31
- import {
32
- DEFAULT_MAX_CONTEXT_BYTES,
33
- DEFAULT_MAX_OUTPUT_BYTES,
34
- MAX_SUBAGENT_TIMEOUT_MS,
35
- MAX_TOOL_MESSAGE_BYTES,
36
- truncateUtf8,
37
- } from "./limits.js";
38
- import { classifyStructuredOutcome } from "./outcome.js";
39
- import type {
40
- AgentInspectionCounts,
41
- AgentLifecycleState,
42
- AgentMailboxMessage,
43
- AgentRegistryOptions,
44
- AgentRunInspectionDetail,
45
- AgentRunInspectionSummary,
46
- AgentTurnCompletion,
47
- ManagedAgent,
48
- } from "./registry-types.js";
49
- import {
50
- type AnyStructuredSubagentResult,
51
- parseAnyStructuredSubagentResult,
52
- type SubagentResultFormat,
53
- } from "./result-contract.js";
54
- import { safeTerminalLine } from "./safe-text.js";
55
- import type { SemanticCompatibility, SemanticSnapshot } from "./semantic-snapshot.js";
56
- import { resolveStatefulLimits } from "./stateful-limits.js";
57
- import {
58
- deriveTaskName,
59
- joinTaskPath,
60
- ROOT_TASK_PATH,
61
- resolveTaskPath,
62
- validateTaskName,
63
- validateTaskPath,
64
- } from "./task-path.js";
65
- import { copyTurnTerminationReport } from "./timeout-checkpoint.js";
66
- import { type AgentTurnRunner, normalizeTransport, type SubagentTransport } from "./transport.js";
67
- import type { TransportTelemetry } from "./transport-types.js";
68
- import { type TurnLimits, validateTurnLimits } from "./turn-budget.js";
69
-
70
- const DEFAULT_STATEFUL_LIMITS = resolveStatefulLimits();
71
- const MAX_PENDING_COMPLETIONS_PER_AGENT = 20;
72
- const INITIAL_PERSISTENCE_RETRY_DELAY_MS = 25;
73
- const MAX_PERSISTENCE_RETRY_DELAY_MS = 1_000;
74
-
75
- export type * from "./registry-types.js";
76
-
77
- function positiveInteger(value: number, label: string): number {
78
- if (!Number.isSafeInteger(value) || value < 1) {
79
- throw new Error(`${label} must be a positive safe integer`);
80
- }
81
- return value;
82
- }
83
-
84
- function nonNegativeInteger(value: number, label: string): number {
85
- if (!Number.isSafeInteger(value) || value < 0) {
86
- throw new Error(`${label} must be a non-negative safe integer`);
87
- }
88
- return value;
89
- }
90
-
91
- function validateTurnTimeout(value: number): number {
92
- if (!Number.isSafeInteger(value) || value < 1 || value > MAX_SUBAGENT_TIMEOUT_MS) {
93
- throw new Error(`Subagent timeoutMs must be between 1 and ${MAX_SUBAGENT_TIMEOUT_MS}`);
94
- }
95
- return value;
96
- }
97
-
98
- function clearCurrentTurn(agent: ManagedAgent): void {
99
- agent.currentTask = undefined;
100
- agent.currentRunId = undefined;
101
- agent.currentTurnGeneration = undefined;
102
- agent.currentTimeoutMs = undefined;
103
- agent.currentIdleTimeoutMs = undefined;
104
- agent.currentMaxTurns = undefined;
105
- agent.currentMaxToolCalls = undefined;
106
- agent.currentMailboxMessageIds = undefined;
107
- }
108
-
109
- function waitAbortError(): Error {
110
- const error = new Error("Subagent wait was aborted");
111
- error.name = "AbortError";
112
- return error;
113
- }
114
-
115
- function waitForPersistenceRetry(milliseconds: number, signal: AbortSignal): Promise<void> {
116
- if (signal.aborted) return Promise.reject(signal.reason);
117
- return new Promise((resolve, reject) => {
118
- const onAbort = () => {
119
- clearTimeout(timer);
120
- reject(signal.reason);
121
- };
122
- const timer = setTimeout(() => {
123
- signal.removeEventListener("abort", onAbort);
124
- resolve();
125
- }, milliseconds);
126
- signal.addEventListener("abort", onAbort, { once: true });
127
- });
128
- }
129
-
130
- export class AgentRegistry {
131
- private readonly agents = new Map<string, ManagedAgent>();
132
- private readonly controllers = new Map<string, AbortController>();
133
- private readonly running = new Map<string, Promise<ManagedAgent>>();
134
- private readonly queue: Array<{
135
- agent: ManagedAgent;
136
- task: string;
137
- resolve: (agent: ManagedAgent) => void;
138
- }> = [];
139
- private changeQueue: Promise<void> = Promise.resolve();
140
- private readonly shutdownController = new AbortController();
141
- private readonly maxAgents: number;
142
- private readonly maxActiveTurns: number;
143
- private readonly maxHistoryTurns: number;
144
- private readonly maxDepth: number;
145
- private readonly maxChildrenPerAgent: number;
146
- private readonly maxMailboxMessages: number;
147
- private readonly maxMailboxMessageBytes: number;
148
- private readonly maxTaskBytes: number;
149
- private readonly maxTurnOutputBytes: number;
150
- private readonly idleTtlMs: number;
151
- private readonly transport: SubagentTransport;
152
- private readonly now: () => number;
153
- private lastCompletionAt = 0;
154
-
155
- constructor(
156
- transport: SubagentTransport | AgentTurnRunner,
157
- private readonly options: AgentRegistryOptions = {},
158
- ) {
159
- this.transport = normalizeTransport(transport);
160
- this.maxAgents = positiveInteger(
161
- options.maxAgents ?? DEFAULT_STATEFUL_LIMITS.maxAgents,
162
- "maxAgents",
163
- );
164
- this.maxActiveTurns = positiveInteger(
165
- options.maxActiveTurns ?? DEFAULT_STATEFUL_LIMITS.maxActiveTurns,
166
- "maxActiveTurns",
167
- );
168
- this.maxHistoryTurns = positiveInteger(options.maxHistoryTurns ?? 20, "maxHistoryTurns");
169
- this.maxDepth = nonNegativeInteger(
170
- options.maxDepth ?? DEFAULT_STATEFUL_LIMITS.maxDepth,
171
- "maxDepth",
172
- );
173
- this.maxChildrenPerAgent = positiveInteger(
174
- options.maxChildrenPerAgent ?? DEFAULT_STATEFUL_LIMITS.maxChildrenPerAgent,
175
- "maxChildrenPerAgent",
176
- );
177
- this.maxMailboxMessages = positiveInteger(
178
- options.maxMailboxMessages ?? 100,
179
- "maxMailboxMessages",
180
- );
181
- this.maxMailboxMessageBytes = positiveInteger(
182
- options.maxMailboxMessageBytes ?? 16 * 1024,
183
- "maxMailboxMessageBytes",
184
- );
185
- this.maxTaskBytes = positiveInteger(
186
- options.maxTaskBytes ?? DEFAULT_MAX_CONTEXT_BYTES,
187
- "maxTaskBytes",
188
- );
189
- this.maxTurnOutputBytes = positiveInteger(
190
- options.maxTurnOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES,
191
- "maxTurnOutputBytes",
192
- );
193
- this.idleTtlMs = positiveInteger(options.idleTtlMs ?? 60 * 60 * 1000, "idleTtlMs");
194
- this.now = options.now ?? Date.now;
195
- }
196
-
197
- restore(records: readonly ManagedAgent[]): void {
198
- const candidates = new Map(
199
- projectAgentRecords(
200
- records.filter((record) => record.id && record.state !== "closed"),
201
- { maxAgents: this.maxAgents, maxDepth: this.maxDepth },
202
- ).map((record) => [record.id, record]),
203
- );
204
- const restorable: Array<{ record: ManagedAgent; rootId: string; depth: number }> = [];
205
- for (const record of candidates.values()) {
206
- if (record.parentId && !candidates.has(record.parentId)) continue;
207
- if (record.parentId === record.id) continue;
208
- const seen = new Set([record.id]);
209
- let parentId = record.parentId;
210
- let rootId = record.id;
211
- let cyclic = false;
212
- while (parentId) {
213
- if (seen.has(parentId)) {
214
- cyclic = true;
215
- break;
216
- }
217
- seen.add(parentId);
218
- rootId = parentId;
219
- parentId = candidates.get(parentId)?.parentId;
220
- }
221
- const depth = seen.size - 1;
222
- if (!cyclic && depth <= this.maxDepth) restorable.push({ record, rootId, depth });
223
- }
224
- restorable.sort(
225
- (left, right) =>
226
- left.depth - right.depth ||
227
- left.record.createdAt - right.record.createdAt ||
228
- left.record.id.localeCompare(right.record.id),
229
- );
230
- const reservedPaths = new Set(
231
- [...this.agents.values()].flatMap((agent) =>
232
- agent.state !== "closed" && agent.taskPath ? [agent.taskPath] : [],
233
- ),
234
- );
235
- for (const { record, rootId, depth } of restorable) {
236
- const parentPath = record.parentId
237
- ? this.agents.get(record.parentId)?.taskPath
238
- : ROOT_TASK_PATH;
239
- if (!parentPath) continue;
240
- const identity = this.reserveRestoredIdentity(record, parentPath, reservedPaths);
241
- for (const completion of record.pendingCompletions ?? []) {
242
- this.lastCompletionAt = Math.max(this.lastCompletionAt, completion.createdAt);
243
- }
244
- this.agents.set(record.id, {
245
- ...record,
246
- ...identity,
247
- state:
248
- record.state === "running" || record.state === "starting" ? "interrupted" : record.state,
249
- rootId,
250
- depth,
251
- currentTask: undefined,
252
- turnGeneration: record.turnGeneration ?? 0,
253
- currentRunId: undefined,
254
- currentTurnGeneration: undefined,
255
- pendingCompletions: (record.pendingCompletions ?? []).map((completion) => ({
256
- ...completion,
257
- })),
258
- currentTimeoutMs: undefined,
259
- currentIdleTimeoutMs: undefined,
260
- currentMaxTurns: undefined,
261
- currentMaxToolCalls: undefined,
262
- currentMailboxMessageIds: undefined,
263
- children: [],
264
- contextSourceIds: [...(record.contextSourceIds ?? [])],
265
- capabilityGrant:
266
- record.capabilityGrant?.state === "active"
267
- ? revokeCapabilityGrant(record.capabilityGrant, "restore-boundary", this.now())
268
- : record.capabilityGrant,
269
- mailbox: (record.mailbox ?? [])
270
- .slice(-this.maxMailboxMessages)
271
- .map((message) => ({ ...message, recipientId: record.id })),
272
- completionRequirements: (record.completionRequirements ?? []).map((item) => ({
273
- ...item,
274
- })),
275
- history: record.history.slice(-this.maxHistoryTurns).map((turn) => ({ ...turn })),
276
- });
277
- const restored = this.agents.get(record.id);
278
- if (restored && restored.state !== "running" && restored.state !== "starting") {
279
- restored.completionRequirements = cancelPendingCompletionRequirements(
280
- restored.completionRequirements,
281
- restored.state,
282
- this.now(),
283
- );
284
- }
285
- }
286
- for (const agent of this.agents.values()) {
287
- if (agent.parentId) {
288
- const parent = this.agents.get(agent.parentId);
289
- if (parent && !parent.children.includes(agent.id)) parent.children.push(agent.id);
290
- }
291
- for (const completion of agent.pendingCompletions ?? []) {
292
- if (!completion.recipientId || !completion.recipientPath) {
293
- Object.assign(completion, this.completionRecipient(agent));
294
- }
295
- this.enqueueCompletionMessage(
296
- agent,
297
- completion,
298
- completion.output || completion.error || "(no output)",
299
- );
300
- }
301
- }
302
- }
303
-
304
- async spawn(input: {
305
- agent: string;
306
- taskName?: string;
307
- task: string;
308
- cwd: string;
309
- agentScope?: "user" | "project" | "both";
310
- thinkingLevel?: SubagentThinkingLevel;
311
- timeoutMs?: number;
312
- idleTimeoutMs?: number;
313
- maxTurns?: number;
314
- maxToolCalls?: number;
315
- completionRequirement?: CompletionRequirementMode;
316
- parentId?: string;
317
- context?: string;
318
- contextSourceIds?: string[];
319
- contextTruncated?: boolean;
320
- contextTurns?: number;
321
- contextBytes?: number;
322
- workspaceMode?: "worktree";
323
- spawnIdempotencyKey?: string;
324
- spawnRequestHash?: string;
325
- contract?: DelegationContract;
326
- resultFormat?: SubagentResultFormat;
327
- executionPlan?: ExecutionPlan;
328
- capabilityGrant?: CapabilityGrant;
329
- semanticSnapshot?: SemanticSnapshot;
330
- semanticCompatibility?: SemanticCompatibility;
331
- target?: TargetPolicyAudit;
332
- }): Promise<ManagedAgent> {
333
- if (!input.task.trim()) throw new Error("Subagent tasks cannot be empty");
334
- if (input.timeoutMs !== undefined) validateTurnTimeout(input.timeoutMs);
335
- validateTurnLimits(input);
336
- const existing = this.findBySpawnIdempotencyKey(
337
- input.spawnIdempotencyKey,
338
- input.spawnRequestHash,
339
- );
340
- if (existing) return existing;
341
- this.assertRequiredCompletionCapacity(input.completionRequirement);
342
- const task = truncateUtf8(input.task, this.maxTaskBytes).text;
343
- const expired = this.evictExpired();
344
- let expiryReleaseError: unknown;
345
- try {
346
- await this.releaseAgents(expired);
347
- } catch (error) {
348
- expiryReleaseError = error;
349
- }
350
- if (expired.length > 0) await this.changed();
351
- if (expiryReleaseError) throw expiryReleaseError;
352
- if (this.retainedCount() >= this.maxAgents) {
353
- throw new Error(`Subagent capacity reached (${this.maxAgents})`);
354
- }
355
- const parent = input.parentId ? this.require(input.parentId) : undefined;
356
- if (parent?.state === "closed") throw new Error(`Cannot spawn under closed agent ${parent.id}`);
357
- if (parent && parent.children.length >= this.maxChildrenPerAgent) {
358
- throw new Error(`Agent ${parent.id} child capacity reached (${this.maxChildrenPerAgent})`);
359
- }
360
- const depth = parent ? parent.depth + 1 : 0;
361
- if (depth > this.maxDepth) throw new Error(`Subagent depth limit reached (${this.maxDepth})`);
362
- const now = this.now();
363
- const id = `sa_${randomUUID()}`;
364
- const taskName = input.taskName ? validateTaskName(input.taskName) : deriveTaskName(id);
365
- const taskPath = joinTaskPath(parent?.taskPath ?? ROOT_TASK_PATH, taskName);
366
- if (this.findByTaskPath(taskPath)) {
367
- throw new Error(`Canonical subagent task path is already retained: ${taskPath}`);
368
- }
369
- const record: ManagedAgent = {
370
- id,
371
- taskName,
372
- taskPath,
373
- agent: input.agent,
374
- parentId: parent?.id,
375
- rootId: parent?.rootId ?? id,
376
- depth,
377
- children: [],
378
- state: "starting",
379
- createdAt: now,
380
- updatedAt: now,
381
- cwd: input.cwd,
382
- agentScope: input.agentScope,
383
- thinkingLevel: input.thinkingLevel,
384
- timeoutMs: input.timeoutMs,
385
- currentTimeoutMs: input.timeoutMs,
386
- idleTimeoutMs: input.idleTimeoutMs,
387
- currentIdleTimeoutMs: input.idleTimeoutMs,
388
- maxTurns: input.maxTurns,
389
- currentMaxTurns: input.maxTurns,
390
- maxToolCalls: input.maxToolCalls,
391
- currentMaxToolCalls: input.maxToolCalls,
392
- currentTask: task,
393
- turnGeneration: 0,
394
- pendingCompletions: [],
395
- completionRequirements: [],
396
- history: [],
397
- mailbox: [],
398
- context: input.context,
399
- contextSourceIds: input.contextSourceIds,
400
- contextTruncated: input.contextTruncated,
401
- contextTurns: input.contextTurns,
402
- contextBytes: input.contextBytes,
403
- workspaceMode: input.workspaceMode,
404
- spawnIdempotencyKey: input.spawnIdempotencyKey,
405
- spawnRequestHash: input.spawnRequestHash,
406
- contract: input.contract,
407
- resultFormat: input.resultFormat,
408
- executionPlan: input.executionPlan,
409
- capabilityGrant: input.capabilityGrant,
410
- semanticSnapshot: input.semanticSnapshot,
411
- semanticCompatibility: input.semanticCompatibility,
412
- target: input.target,
413
- };
414
- this.agents.set(record.id, record);
415
- if (parent) {
416
- parent.children.push(record.id);
417
- parent.updatedAt = now;
418
- }
419
- await this.changed();
420
- this.startTurn(record, task, input, input.completionRequirement);
421
- return this.copy(record);
422
- }
423
-
424
- findBySpawnIdempotencyKey(
425
- key: string | undefined,
426
- requestHash: string | undefined,
427
- ): ManagedAgent | undefined {
428
- if (!key) return undefined;
429
- const existing = [...this.agents.values()].find(
430
- (agent) => agent.state !== "closed" && agent.spawnIdempotencyKey === key,
431
- );
432
- if (!existing) return undefined;
433
- if (!requestHash || existing.spawnRequestHash !== requestHash) {
434
- throw new Error(
435
- "The subagent_spawn idempotencyKey was already used with different parameters",
436
- );
437
- }
438
- return this.copy(existing);
439
- }
440
-
441
- async followUp(
442
- id: string,
443
- task: string,
444
- options: TurnLimits & {
445
- timeoutMs?: number;
446
- completionRequirement?: CompletionRequirementMode;
447
- } = {},
448
- ): Promise<ManagedAgent> {
449
- if (!task.trim()) throw new Error("Subagent tasks cannot be empty");
450
- if (options.timeoutMs !== undefined) validateTurnTimeout(options.timeoutMs);
451
- validateTurnLimits(options);
452
- const boundedTask = truncateUtf8(task, this.maxTaskBytes).text;
453
- const agent = this.require(id);
454
- if (
455
- ![
456
- "idle",
457
- "completed",
458
- "partial",
459
- "blocked",
460
- "needs-input",
461
- "abstained",
462
- "stale",
463
- "interrupted",
464
- "failed",
465
- ].includes(agent.state)
466
- ) {
467
- throw new Error(`Agent ${id} cannot accept follow-up while ${agent.state}`);
468
- }
469
- this.assertRequiredCompletionCapacity(options.completionRequirement);
470
- const unread = agent.mailbox.filter((message) => !message.readAt).slice(-20);
471
- agent.currentMailboxMessageIds = unread.map((message) => message.id);
472
- this.startTurn(agent, boundedTask, options, options.completionRequirement);
473
- return this.copy(agent);
474
- }
475
-
476
- async updateSemanticState(
477
- id: string,
478
- executionPlan: ExecutionPlan,
479
- capabilityGrant: CapabilityGrant,
480
- snapshot: SemanticSnapshot,
481
- compatibility: SemanticCompatibility,
482
- ): Promise<ManagedAgent> {
483
- const agent = this.require(id);
484
- if (agent.state === "running" || agent.state === "starting" || agent.state === "closed") {
485
- throw new Error(`Agent ${id} cannot update semantic state while ${agent.state}`);
486
- }
487
- agent.executionPlan = copyExecutionPlan(executionPlan);
488
- agent.capabilityGrant = structuredClone(capabilityGrant);
489
- agent.semanticSnapshot = structuredClone(snapshot);
490
- agent.semanticCompatibility = structuredClone(compatibility);
491
- agent.updatedAt = this.now();
492
- await this.changed();
493
- return this.copy(agent);
494
- }
495
-
496
- async sendMessage(
497
- recipientId: string,
498
- content: string,
499
- senderId = "root",
500
- deduplicationKey?: string,
501
- ): Promise<AgentMailboxMessage> {
502
- if (!content.trim()) throw new Error("Subagent mailbox messages cannot be empty");
503
- if (deduplicationKey && deduplicationKey.length > 256) {
504
- throw new Error("Subagent mailbox deduplication keys cannot exceed 256 characters");
505
- }
506
- const sender = senderId === "root" ? undefined : this.require(senderId);
507
- if (sender?.state === "closed")
508
- throw new Error(`Closed agent ${sender.id} cannot send messages`);
509
- const recipient = this.require(recipientId, sender?.id);
510
- if (recipient.state === "closed")
511
- throw new Error(`Cannot message closed agent ${recipient.id}`);
512
- const message = this.enqueueMessage(recipient, content, sender?.id ?? "root", deduplicationKey);
513
- await this.changed();
514
- return { ...message };
515
- }
516
-
517
- async readMessages(
518
- id: string,
519
- acknowledge = true,
520
- limit = this.maxMailboxMessages,
521
- ): Promise<AgentMailboxMessage[]> {
522
- if (!Number.isSafeInteger(limit) || limit < 1) {
523
- throw new Error("Subagent mailbox read limit must be a positive safe integer");
524
- }
525
- const agent = this.require(id);
526
- const unread = agent.mailbox.filter((message) => !message.readAt).slice(0, limit);
527
- if (acknowledge && unread.length > 0) {
528
- const readAt = this.now();
529
- for (const message of unread) message.readAt = readAt;
530
- await this.changed();
531
- }
532
- return unread.map((message) => ({ ...message }));
533
- }
534
-
535
- async acknowledgeVisibleMessages(
536
- id: string,
537
- messageIds: readonly string[],
538
- completionIds: readonly string[] = [],
539
- visibleAt = this.now(),
540
- ): Promise<void> {
541
- const agent = this.require(id);
542
- const visibleMessageIds = new Set(messageIds);
543
- const visibleCompletionIds = new Set(completionIds);
544
- const changedMessages: AgentMailboxMessage[] = [];
545
- const removedCompletions: Array<{
546
- owner: ManagedAgent;
547
- completion: NonNullable<ManagedAgent["pendingCompletions"]>[number];
548
- }> = [];
549
- for (const message of agent.mailbox) {
550
- if (message.readAt === undefined && visibleMessageIds.has(message.id)) {
551
- message.readAt = visibleAt;
552
- changedMessages.push(message);
553
- }
554
- if (message.completionId && visibleMessageIds.has(message.id)) {
555
- visibleCompletionIds.add(message.completionId);
556
- }
557
- }
558
- for (const owner of this.agents.values()) {
559
- for (const completion of owner.pendingCompletions ?? []) {
560
- if (!visibleCompletionIds.has(completion.completionId)) continue;
561
- if (completion.recipientId !== agent.id && completion.recipientPath !== agent.taskPath) {
562
- continue;
563
- }
564
- removedCompletions.push({ owner, completion });
565
- }
566
- }
567
- if (changedMessages.length === 0 && removedCompletions.length === 0) return;
568
- for (const { owner, completion } of removedCompletions) {
569
- owner.pendingCompletions = (owner.pendingCompletions ?? []).filter(
570
- (candidate) => candidate.completionId !== completion.completionId,
571
- );
572
- }
573
- agent.updatedAt = Math.max(agent.updatedAt, visibleAt);
574
- try {
575
- await this.changed(true);
576
- } catch (error) {
577
- for (const message of changedMessages) message.readAt = undefined;
578
- for (const { owner, completion } of removedCompletions) {
579
- owner.pendingCompletions = [...(owner.pendingCompletions ?? []), completion].sort(
580
- (left, right) => left.createdAt - right.createdAt || left.generation - right.generation,
581
- );
582
- }
583
- throw error;
584
- }
585
- }
586
-
587
- async wait(
588
- id: string,
589
- timeoutMs = 30_000,
590
- signal?: AbortSignal,
591
- ): Promise<{ timedOut: boolean; agent: ManagedAgent }> {
592
- if (!Number.isFinite(timeoutMs) || timeoutMs < 1) {
593
- throw new Error("Subagent wait timeout must be a positive finite number");
594
- }
595
- if (signal?.aborted) throw waitAbortError();
596
- const agent = this.require(id);
597
- const agentId = agent.id;
598
- const running = this.running.get(agentId);
599
- if (!running) return { timedOut: false, agent: this.copy(agent) };
600
- let timer: NodeJS.Timeout | undefined;
601
- let onAbort: (() => void) | undefined;
602
- const timeout = new Promise<"timeout">((resolve) => {
603
- timer = setTimeout(() => resolve("timeout"), Math.max(1, timeoutMs));
604
- });
605
- const aborted = new Promise<"aborted">((resolve) => {
606
- onAbort = () => resolve("aborted");
607
- signal?.addEventListener("abort", onAbort, { once: true });
608
- });
609
- const result = await Promise.race([running, timeout, aborted]);
610
- if (timer) clearTimeout(timer);
611
- if (onAbort) signal?.removeEventListener("abort", onAbort);
612
- if (result === "aborted") throw waitAbortError();
613
- return result === "timeout"
614
- ? { timedOut: true, agent: this.copy(this.require(agentId)) }
615
- : { timedOut: false, agent: this.copy(result) };
616
- }
617
-
618
- async interruptTree(id: string): Promise<ManagedAgent[]> {
619
- const results: ManagedAgent[] = [];
620
- for (const target of this.descendants(id).reverse()) {
621
- const agent = this.require(target);
622
- if (agent.state === "running" || agent.state === "starting") {
623
- results.push(await this.interrupt(target));
624
- }
625
- }
626
- return results;
627
- }
628
-
629
- async interrupt(id: string): Promise<ManagedAgent> {
630
- const agent = this.require(id);
631
- const agentId = agent.id;
632
- if (agent.state !== "running" && agent.state !== "starting")
633
- throw new Error(`Agent ${id} is not running`);
634
- if (agent.capabilityGrant?.state === "active") {
635
- agent.capabilityGrant = revokeCapabilityGrant(
636
- agent.capabilityGrant,
637
- "interrupted",
638
- this.now(),
639
- );
640
- }
641
- if (agent.executionPlan) {
642
- agent.executionPlan = rotateExecutionPlanGeneration(agent.executionPlan);
643
- }
644
- if (agent.state === "starting") {
645
- const index = this.queue.findIndex((entry) => entry.agent.id === agentId);
646
- if (index >= 0) {
647
- const [entry] = this.queue.splice(index, 1);
648
- const recipient = this.completionRecipient(agent);
649
- const persistedCompletion = {
650
- completionId: `completion:${agent.id}:${randomUUID()}`,
651
- ...recipient,
652
- runId: agent.currentRunId ?? `run:${agent.id}:${randomUUID()}`,
653
- generation: agent.currentTurnGeneration ?? agent.turnGeneration ?? 1,
654
- task: truncateUtf8(entry.task, 256).text,
655
- output: "",
656
- error: "Interrupted before execution",
657
- createdAt: this.completionCreatedAt(),
658
- };
659
- agent.state = "interrupted";
660
- agent.completionRequirements = makeCompletionAvailable(agent.completionRequirements, {
661
- runId: persistedCompletion.runId,
662
- generation: persistedCompletion.generation,
663
- completionId: persistedCompletion.completionId,
664
- terminalState: agent.state,
665
- updatedAt: persistedCompletion.createdAt,
666
- });
667
- agent.pendingCompletions = [...(agent.pendingCompletions ?? []), persistedCompletion];
668
- this.enqueueCompletionMessage(agent, persistedCompletion, persistedCompletion.error);
669
- clearCurrentTurn(agent);
670
- agent.updatedAt = this.now();
671
- const persisted = await this.persistTerminalState().then(
672
- () => true,
673
- () => false,
674
- );
675
- const completion: AgentTurnCompletion = {
676
- ...persistedCompletion,
677
- agent: this.copy(agent),
678
- };
679
- entry.resolve(agent);
680
- this.running.delete(agentId);
681
- if (persisted) await this.notifyTurnComplete(completion);
682
- return this.copy(agent);
683
- }
684
- }
685
- this.controllers.get(agentId)?.abort();
686
- await this.running.get(agentId);
687
- return this.copy(this.require(agentId));
688
- }
689
-
690
- async closeTree(id: string): Promise<ManagedAgent[]> {
691
- const results: ManagedAgent[] = [];
692
- const failures: unknown[] = [];
693
- for (const target of this.descendants(id).reverse()) {
694
- const agent = this.require(target);
695
- if (agent.state === "closed") continue;
696
- try {
697
- results.push(await this.close(target));
698
- } catch (error) {
699
- failures.push(error);
700
- const closed = this.get(target);
701
- if (closed?.state === "closed") results.push(closed);
702
- }
703
- }
704
- if (failures.length > 0) {
705
- throw new AggregateError(failures, `Failed to release ${failures.length} subagent(s)`);
706
- }
707
- return results;
708
- }
709
-
710
- async close(id: string): Promise<ManagedAgent> {
711
- const agent = this.require(id);
712
- const agentId = agent.id;
713
- if (agent.state === "closed") throw new Error(`Agent ${id} is already closed`);
714
- if (agent.children.some((childId) => this.agents.get(childId)?.state !== "closed")) {
715
- throw new Error(`Agent ${id} has active descendants; close the subtree instead`);
716
- }
717
- if (agent.state === "starting" || agent.state === "running") {
718
- if (agent.capabilityGrant?.state === "active") {
719
- agent.capabilityGrant = revokeCapabilityGrant(agent.capabilityGrant, "closed", this.now());
720
- }
721
- if (agent.executionPlan) {
722
- agent.executionPlan = rotateExecutionPlanGeneration(agent.executionPlan);
723
- }
724
- }
725
- if (agent.state === "starting") {
726
- const index = this.queue.findIndex((entry) => entry.agent.id === agentId);
727
- if (index >= 0) {
728
- const [entry] = this.queue.splice(index, 1);
729
- entry.resolve(agent);
730
- this.running.delete(agentId);
731
- }
732
- }
733
- this.controllers.get(agentId)?.abort();
734
- await this.running.get(agentId)?.catch(() => undefined);
735
- agent.state = "closed";
736
- agent.updatedAt = this.now();
737
- agent.completionRequirements = cancelPendingCompletionRequirements(
738
- agent.completionRequirements,
739
- agent.state,
740
- agent.updatedAt,
741
- );
742
- if (agent.parentId) {
743
- const parent = this.agents.get(agent.parentId);
744
- if (parent) parent.children = parent.children.filter((childId) => childId !== agentId);
745
- }
746
- clearCurrentTurn(agent);
747
- let releaseError: unknown;
748
- try {
749
- await this.transport.release?.(this.copy(agent));
750
- } catch (error) {
751
- releaseError = error;
752
- }
753
- this.pruneClosedAgents();
754
- await this.changed();
755
- if (releaseError) throw releaseError;
756
- return this.copy(agent);
757
- }
758
-
759
- async closeAll(): Promise<void> {
760
- const roots = [...this.agents.values()]
761
- .filter((agent) => agent.state !== "closed" && !agent.parentId)
762
- .map((agent) => agent.id);
763
- const results = await Promise.allSettled(roots.map((id) => this.closeTree(id)));
764
- const failures = results.flatMap((result) =>
765
- result.status === "rejected" ? [result.reason] : [],
766
- );
767
- if (failures.length > 0) {
768
- throw new AggregateError(failures, `Failed to close ${failures.length} subagent tree(s)`);
769
- }
770
- }
771
-
772
- async shutdown(): Promise<void> {
773
- this.shutdownController.abort(new Error("Subagent registry is shutting down"));
774
- for (const entry of this.queue.splice(0)) {
775
- if (entry.agent.capabilityGrant?.state === "active") {
776
- entry.agent.capabilityGrant = revokeCapabilityGrant(
777
- entry.agent.capabilityGrant,
778
- "shutdown",
779
- this.now(),
780
- );
781
- }
782
- if (entry.agent.executionPlan) {
783
- entry.agent.executionPlan = rotateExecutionPlanGeneration(entry.agent.executionPlan);
784
- }
785
- entry.agent.state = "interrupted";
786
- clearCurrentTurn(entry.agent);
787
- entry.resolve(entry.agent);
788
- this.running.delete(entry.agent.id);
789
- }
790
- for (const id of this.controllers.keys()) {
791
- const agent = this.agents.get(id);
792
- if (agent?.capabilityGrant?.state === "active") {
793
- agent.capabilityGrant = revokeCapabilityGrant(
794
- agent.capabilityGrant,
795
- "shutdown",
796
- this.now(),
797
- );
798
- }
799
- if (agent?.executionPlan) {
800
- agent.executionPlan = rotateExecutionPlanGeneration(agent.executionPlan);
801
- }
802
- }
803
- for (const controller of this.controllers.values()) controller.abort();
804
- await Promise.all([...this.running.values()].map((turn) => turn.catch(() => undefined)));
805
- for (const agent of this.agents.values()) {
806
- if (agent.state !== "closed") {
807
- if (agent.state === "running" || agent.state === "starting") {
808
- agent.state = "interrupted";
809
- }
810
- agent.completionRequirements = cancelPendingCompletionRequirements(
811
- agent.completionRequirements,
812
- agent.state,
813
- this.now(),
814
- );
815
- clearCurrentTurn(agent);
816
- }
817
- }
818
- let shutdownError: unknown;
819
- try {
820
- await this.transport.shutdown?.();
821
- } catch (error) {
822
- shutdownError = error;
823
- }
824
- await this.changed(true);
825
- if (shutdownError) throw shutdownError;
826
- }
827
-
828
- inspectionCounts(): AgentInspectionCounts {
829
- let activeAgents = 0;
830
- let retainedAgents = 0;
831
- for (const agent of this.agents.values()) {
832
- if (agent.state === "starting" || agent.state === "running") activeAgents++;
833
- if (agent.state !== "closed") retainedAgents++;
834
- }
835
- return { activeAgents, retainedAgents };
836
- }
837
-
838
- listInspection(includeClosed = false): AgentRunInspectionSummary[] {
839
- return [...this.agents.values()]
840
- .filter((agent) => includeClosed || agent.state !== "closed")
841
- .sort((left, right) => left.createdAt - right.createdAt)
842
- .map((agent) => this.inspectSummary(agent));
843
- }
844
-
845
- getInspection(id: string): AgentRunInspectionDetail | undefined {
846
- const agent = this.findReference(id);
847
- if (!agent) return undefined;
848
- return {
849
- ...this.inspectSummary(agent),
850
- cwd: agent.cwd,
851
- thinkingLevel: agent.thinkingLevel,
852
- timeoutMs: agent.timeoutMs,
853
- currentTimeoutMs: agent.currentTimeoutMs,
854
- idleTimeoutMs: agent.idleTimeoutMs,
855
- currentIdleTimeoutMs: agent.currentIdleTimeoutMs,
856
- maxTurns: agent.maxTurns,
857
- currentMaxTurns: agent.currentMaxTurns,
858
- maxToolCalls: agent.maxToolCalls,
859
- currentMaxToolCalls: agent.currentMaxToolCalls,
860
- currentTask: agent.currentTask,
861
- currentRunId: agent.currentRunId,
862
- currentTurnGeneration: agent.currentTurnGeneration,
863
- completionRequirements: (agent.completionRequirements ?? []).map((record) => ({
864
- ...record,
865
- })),
866
- error: agent.error,
867
- workspaceMode: agent.workspaceMode,
868
- contextTurns: agent.contextTurns,
869
- contextBytes: agent.contextBytes,
870
- contextSources: agent.contextSourceIds?.length,
871
- contextTruncated: agent.contextTruncated,
872
- contract: agent.contract ? structuredClone(agent.contract) : undefined,
873
- resultFormat: agent.resultFormat,
874
- target: agent.target ? { ...agent.target, trust: { ...agent.target.trust } } : undefined,
875
- policy: agent.policy
876
- ? {
877
- inherited: [...agent.policy.inherited],
878
- overridden: [...agent.policy.overridden],
879
- unsupported: [...agent.policy.unsupported],
880
- }
881
- : undefined,
882
- structuredResult: agent.structuredResult
883
- ? copyStructuredResult(agent.structuredResult)
884
- : undefined,
885
- termination: agent.termination ? copyTurnTerminationReport(agent.termination) : undefined,
886
- outcome: agent.outcome ? structuredClone(agent.outcome) : undefined,
887
- executionPlan: agent.executionPlan ? copyExecutionPlan(agent.executionPlan) : undefined,
888
- capabilityGrant: agent.capabilityGrant ? structuredClone(agent.capabilityGrant) : undefined,
889
- semanticSnapshot: agent.semanticSnapshot
890
- ? structuredClone(agent.semanticSnapshot)
891
- : undefined,
892
- semanticCompatibility: agent.semanticCompatibility
893
- ? structuredClone(agent.semanticCompatibility)
894
- : undefined,
895
- telemetry: agent.telemetry ? copyTelemetry(agent.telemetry) : undefined,
896
- };
897
- }
898
-
899
- list(includeClosed = false, rootId?: string): ManagedAgent[] {
900
- return [...this.agents.values()]
901
- .filter((agent) => !rootId || agent.rootId === rootId)
902
- .filter((agent) => includeClosed || agent.state !== "closed")
903
- .sort((a, b) => a.createdAt - b.createdAt)
904
- .map((agent) => this.copy(agent));
905
- }
906
-
907
- get(reference: string): ManagedAgent | undefined {
908
- return this.resolveAgent(reference);
909
- }
910
-
911
- resolveAgent(reference: string, senderId = "root"): ManagedAgent | undefined {
912
- const agent = this.findReference(reference, senderId);
913
- return agent ? this.copy(agent) : undefined;
914
- }
915
-
916
- reconcileCompletionRequirements(
917
- activeRecords: ReadonlyMap<
918
- string,
919
- import("./completion-requirement.js").CompletionRequirementRecord
920
- >,
921
- observedState: boolean,
922
- ): void {
923
- if (!observedState) return;
924
- for (const agent of this.agents.values()) {
925
- agent.completionRequirements = (agent.completionRequirements ?? []).flatMap((record) => {
926
- const restored = activeRecords.get(completionRequirementKey(record));
927
- if (!restored) return [];
928
- if (
929
- restored.state === "pending" &&
930
- agent.state !== "running" &&
931
- agent.state !== "starting"
932
- ) {
933
- return [
934
- {
935
- ...restored,
936
- state: "cancelled" as const,
937
- terminalState: "interrupted" as const,
938
- updatedAt: Math.max(restored.updatedAt, this.now()),
939
- },
940
- ];
941
- }
942
- return [{ ...restored }];
943
- });
944
- }
945
- }
946
-
947
- listPendingCompletions(): AgentTurnCompletion[] {
948
- return [...this.agents.values()]
949
- .flatMap((agent) =>
950
- (agent.pendingCompletions ?? []).map((completion) => ({
951
- ...completion,
952
- agent: this.copy(agent),
953
- })),
954
- )
955
- .sort(
956
- (left, right) =>
957
- left.createdAt - right.createdAt ||
958
- left.generation - right.generation ||
959
- left.completionId.localeCompare(right.completionId),
960
- );
961
- }
962
-
963
- async markCompletionDelivered(completionId: string, deliveredAt: number): Promise<void> {
964
- const agent = [...this.agents.values()].find((candidate) =>
965
- candidate.pendingCompletions?.some((completion) => completion.completionId === completionId),
966
- );
967
- if (!agent) return;
968
- const acknowledged = (agent.pendingCompletions ?? []).find(
969
- (completion) => completion.completionId === completionId,
970
- );
971
- if (!acknowledged) return;
972
- agent.completionRequirements = makeCompletionVisible(
973
- agent.completionRequirements,
974
- completionId,
975
- deliveredAt,
976
- );
977
- agent.pendingCompletions = (agent.pendingCompletions ?? []).filter(
978
- (completion) => completion.completionId !== completionId,
979
- );
980
- if (agent.telemetry) {
981
- agent.telemetry = {
982
- ...agent.telemetry,
983
- updatedAt: deliveredAt,
984
- timing: { ...agent.telemetry.timing, completionDeliveredAt: deliveredAt },
985
- };
986
- }
987
- agent.updatedAt = Math.max(agent.updatedAt, deliveredAt);
988
- try {
989
- await this.changed(true);
990
- } catch (error) {
991
- agent.completionRequirements = (agent.completionRequirements ?? []).map((record) =>
992
- record.completionId === completionId && record.state === "visible"
993
- ? { ...record, state: "available", updatedAt: acknowledged.createdAt }
994
- : record,
995
- );
996
- if (
997
- !agent.pendingCompletions?.some(
998
- (completion) => completion.completionId === acknowledged.completionId,
999
- )
1000
- ) {
1001
- agent.pendingCompletions = [...(agent.pendingCompletions ?? []), acknowledged].sort(
1002
- (left, right) => left.createdAt - right.createdAt || left.generation - right.generation,
1003
- );
1004
- }
1005
- if (agent.telemetry?.timing.completionDeliveredAt === deliveredAt) {
1006
- const { completionDeliveredAt: _discarded, ...timing } = agent.telemetry.timing;
1007
- agent.telemetry = { ...agent.telemetry, timing };
1008
- }
1009
- throw error;
1010
- }
1011
- }
1012
-
1013
- async sweepExpired(): Promise<number> {
1014
- const removed = this.evictExpired();
1015
- let releaseError: unknown;
1016
- try {
1017
- await this.releaseAgents(removed);
1018
- } catch (error) {
1019
- releaseError = error;
1020
- }
1021
- if (removed.length > 0) await this.changed();
1022
- if (releaseError) throw releaseError;
1023
- return removed.length;
1024
- }
1025
-
1026
- private startTurn(
1027
- agent: ManagedAgent,
1028
- task: string,
1029
- limits: TurnLimits & { timeoutMs?: number } = {},
1030
- completionRequirement: CompletionRequirementMode = "background",
1031
- ): void {
1032
- if ((agent.pendingCompletions?.length ?? 0) >= MAX_PENDING_COMPLETIONS_PER_AGENT) {
1033
- throw new Error(
1034
- `Agent ${agent.id} has ${MAX_PENDING_COMPLETIONS_PER_AGENT} undelivered completions; wait for delivery before another turn`,
1035
- );
1036
- }
1037
- agent.turnGeneration = (agent.turnGeneration ?? 0) + 1;
1038
- agent.currentTurnGeneration = agent.turnGeneration;
1039
- agent.currentRunId = `run:${agent.id}:${randomUUID()}`;
1040
- if (completionRequirement === "required") {
1041
- agent.completionRequirements = beginCompletionRequirement(agent.completionRequirements, {
1042
- runId: agent.currentRunId,
1043
- generation: agent.currentTurnGeneration,
1044
- createdAt: this.now(),
1045
- });
1046
- }
1047
- agent.state = "starting";
1048
- agent.error = undefined;
1049
- agent.currentTask = task;
1050
- agent.currentTimeoutMs = limits.timeoutMs ?? agent.timeoutMs;
1051
- agent.currentIdleTimeoutMs = limits.idleTimeoutMs ?? agent.idleTimeoutMs;
1052
- agent.currentMaxTurns = limits.maxTurns ?? agent.maxTurns;
1053
- agent.currentMaxToolCalls = limits.maxToolCalls ?? agent.maxToolCalls;
1054
- agent.structuredResult = undefined;
1055
- agent.termination = undefined;
1056
- agent.outcome = undefined;
1057
- agent.updatedAt = this.now();
1058
- agent.telemetry = {
1059
- phase: "queued",
1060
- queuePosition: this.queue.length + 1,
1061
- updatedAt: agent.updatedAt,
1062
- timing: { queuedAt: agent.updatedAt },
1063
- budgetSource: {
1064
- timeout: agent.currentTimeoutMs === undefined ? "runtime" : "explicit",
1065
- idleTimeout: agent.currentIdleTimeoutMs === undefined ? "runtime" : "explicit",
1066
- turnLimit: agent.currentMaxTurns === undefined ? "runtime" : "explicit",
1067
- toolCallLimit: agent.currentMaxToolCalls === undefined ? "runtime" : "explicit",
1068
- },
1069
- };
1070
- let resolveQueued!: (agent: ManagedAgent) => void;
1071
- const completion = new Promise<ManagedAgent>((resolve) => {
1072
- resolveQueued = resolve;
1073
- });
1074
- this.running.set(agent.id, completion);
1075
- this.queue.push({ agent, task, resolve: resolveQueued });
1076
- this.updateQueuePositions();
1077
- void this.changed();
1078
- this.pumpQueue();
1079
- }
1080
-
1081
- private pumpQueue(): void {
1082
- while (this.controllers.size < this.maxActiveTurns && this.queue.length > 0) {
1083
- const next = this.queue.shift();
1084
- if (!next) return;
1085
- this.runQueuedTurn(next.agent, next.task, next.resolve);
1086
- this.updateQueuePositions();
1087
- }
1088
- }
1089
-
1090
- private runQueuedTurn(
1091
- agent: ManagedAgent,
1092
- task: string,
1093
- resolveQueued: (agent: ManagedAgent) => void,
1094
- ): void {
1095
- if (
1096
- agent.capabilityGrant &&
1097
- agent.executionPlan &&
1098
- !isCapabilityGrantActive(agent.capabilityGrant, agent.executionPlan, this.now())
1099
- ) {
1100
- agent.state = "failed";
1101
- agent.error = "Capability grant expired or no longer matches the accepted plan";
1102
- agent.outcome = classifyStructuredOutcome("failed", "capability-grant-invalid");
1103
- const recipient = this.completionRecipient(agent);
1104
- const persistedCompletion = {
1105
- completionId: `completion:${agent.id}:${randomUUID()}`,
1106
- ...recipient,
1107
- runId: agent.currentRunId ?? `run:${agent.id}:${randomUUID()}`,
1108
- generation: agent.currentTurnGeneration ?? agent.turnGeneration ?? 1,
1109
- task: truncateUtf8(task, 256).text,
1110
- output: "",
1111
- error: truncateUtf8(agent.error, 512).text,
1112
- createdAt: this.completionCreatedAt(),
1113
- };
1114
- agent.completionRequirements = makeCompletionAvailable(agent.completionRequirements, {
1115
- runId: persistedCompletion.runId,
1116
- generation: persistedCompletion.generation,
1117
- completionId: persistedCompletion.completionId,
1118
- terminalState: agent.state,
1119
- updatedAt: persistedCompletion.createdAt,
1120
- });
1121
- agent.pendingCompletions = [...(agent.pendingCompletions ?? []), persistedCompletion];
1122
- this.enqueueCompletionMessage(agent, persistedCompletion, persistedCompletion.error ?? "");
1123
- clearCurrentTurn(agent);
1124
- agent.updatedAt = this.now();
1125
- void this.persistTerminalState()
1126
- .then(() =>
1127
- this.notifyTurnComplete({
1128
- ...persistedCompletion,
1129
- agent: this.copy(agent),
1130
- }),
1131
- )
1132
- .catch(() => undefined)
1133
- .finally(() => {
1134
- resolveQueued(agent);
1135
- this.running.delete(agent.id);
1136
- });
1137
- return;
1138
- }
1139
- const controller = new AbortController();
1140
- this.controllers.set(agent.id, controller);
1141
- agent.state = "running";
1142
- agent.updatedAt = this.now();
1143
- const startedAt = this.now();
1144
- const runId = agent.currentRunId ?? `run:${agent.id}:${randomUUID()}`;
1145
- const turnGeneration = agent.currentTurnGeneration ?? agent.turnGeneration ?? 1;
1146
- const completionKey = `completion:${agent.id}:${randomUUID()}`;
1147
- const acceptedPlanId = agent.executionPlan?.id;
1148
- let completionContent = "";
1149
- let completionOutput = "";
1150
- let completionError: string | undefined;
1151
- const visibleMailboxMessageIds = [...(agent.currentMailboxMessageIds ?? [])];
1152
- void this.transport
1153
- .runTurn(this.copy(agent), task, controller.signal, (progress) => {
1154
- agent.telemetry = {
1155
- ...progress,
1156
- queuePosition: undefined,
1157
- timing: {
1158
- queuedAt: agent.telemetry?.timing.queuedAt,
1159
- ...progress.timing,
1160
- },
1161
- };
1162
- })
1163
- .then(async (outcome) => {
1164
- if (visibleMailboxMessageIds.length > 0) {
1165
- await this.acknowledgeVisibleMessages(
1166
- agent.id,
1167
- visibleMailboxMessageIds,
1168
- [],
1169
- this.now(),
1170
- ).catch(() => undefined);
1171
- }
1172
- const output = truncateUtf8(outcome.output, this.maxTurnOutputBytes).text;
1173
- const error = outcome.error
1174
- ? truncateUtf8(outcome.error, this.maxTurnOutputBytes).text
1175
- : undefined;
1176
- agent.history.push({
1177
- runId,
1178
- generation: turnGeneration,
1179
- task,
1180
- output,
1181
- startedAt,
1182
- completedAt: this.now(),
1183
- exitCode: outcome.exitCode,
1184
- truncated: outcome.truncated,
1185
- termination: outcome.termination
1186
- ? copyTurnTerminationReport(outcome.termination)
1187
- : undefined,
1188
- });
1189
- agent.history = agent.history.slice(-this.maxHistoryTurns);
1190
- agent.structuredResult =
1191
- outcome.structuredResult ?? parseAnyStructuredSubagentResult(output, agent.resultFormat);
1192
- const malformedStructuredResult =
1193
- agent.resultFormat !== undefined &&
1194
- agent.resultFormat !== "text" &&
1195
- agent.structuredResult === undefined;
1196
- const finalizedPartial =
1197
- outcome.termination?.finalization.status === "completed" && output.trim().length > 0;
1198
- agent.outcome = outcome.aborted
1199
- ? classifyStructuredOutcome("interrupted", "transport-aborted")
1200
- : malformedStructuredResult
1201
- ? classifyStructuredOutcome("contract-invalid", "malformed-structured-result")
1202
- : finalizedPartial
1203
- ? classifyStructuredOutcome("partial", outcome.termination?.reason)
1204
- : (outcome.outcome ??
1205
- (agent.structuredResult?.version === "pi-subagents:result:v2"
1206
- ? classifyStructuredOutcome(
1207
- agent.structuredResult.status,
1208
- agent.structuredResult.reasonCode,
1209
- )
1210
- : undefined));
1211
- const staleGeneration = Boolean(
1212
- acceptedPlanId && agent.executionPlan?.id !== acceptedPlanId,
1213
- );
1214
- if (staleGeneration) {
1215
- agent.outcome = classifyStructuredOutcome("stale", "cancelled-generation");
1216
- }
1217
- agent.state = staleGeneration
1218
- ? "stale"
1219
- : outcome.aborted
1220
- ? "interrupted"
1221
- : outcome.exitCode !== 0 && agent.outcome?.status !== "partial"
1222
- ? "failed"
1223
- : lifecycleStateForOutcome(agent.outcome?.status);
1224
- agent.error = error;
1225
- if (agent.capabilityGrant?.state === "active") {
1226
- agent.capabilityGrant = revokeCapabilityGrant(
1227
- agent.capabilityGrant,
1228
- "turn-settled",
1229
- this.now(),
1230
- );
1231
- }
1232
- agent.policy = outcome.policy;
1233
- agent.telemetry = outcome.telemetry
1234
- ? {
1235
- ...outcome.telemetry,
1236
- timing: {
1237
- queuedAt: agent.telemetry?.timing.queuedAt,
1238
- ...outcome.telemetry.timing,
1239
- },
1240
- budgetSource: outcome.telemetry.budgetSource ?? agent.telemetry?.budgetSource,
1241
- }
1242
- : agent.telemetry;
1243
- agent.termination = outcome.termination
1244
- ? copyTurnTerminationReport(outcome.termination)
1245
- : undefined;
1246
- completionOutput = output;
1247
- completionError = error;
1248
- completionContent = output || error || `${agent.id} ${agent.state}`;
1249
- return agent;
1250
- })
1251
- .catch((error) => {
1252
- const staleGeneration = Boolean(
1253
- acceptedPlanId && agent.executionPlan?.id !== acceptedPlanId,
1254
- );
1255
- agent.state = staleGeneration
1256
- ? "stale"
1257
- : controller.signal.aborted
1258
- ? "interrupted"
1259
- : "failed";
1260
- agent.outcome = classifyStructuredOutcome(
1261
- staleGeneration ? "stale" : controller.signal.aborted ? "interrupted" : "failed",
1262
- staleGeneration
1263
- ? "cancelled-generation"
1264
- : controller.signal.aborted
1265
- ? "transport-aborted"
1266
- : "transport-error",
1267
- );
1268
- if (agent.capabilityGrant?.state === "active") {
1269
- agent.capabilityGrant = revokeCapabilityGrant(
1270
- agent.capabilityGrant,
1271
- "turn-failed",
1272
- this.now(),
1273
- );
1274
- }
1275
- agent.error = truncateUtf8(
1276
- error instanceof Error ? error.message : String(error),
1277
- this.maxTurnOutputBytes,
1278
- ).text;
1279
- agent.history.push({
1280
- runId,
1281
- generation: turnGeneration,
1282
- task,
1283
- output: "",
1284
- startedAt,
1285
- completedAt: this.now(),
1286
- exitCode: controller.signal.aborted ? 130 : 1,
1287
- });
1288
- agent.history = agent.history.slice(-this.maxHistoryTurns);
1289
- agent.telemetry = {
1290
- ...(agent.telemetry ?? {
1291
- phase: "failed",
1292
- updatedAt: this.now(),
1293
- timing: { queuedAt: startedAt },
1294
- }),
1295
- phase: controller.signal.aborted ? "interrupted" : "failed",
1296
- failurePhase: agent.telemetry?.phase ?? "running",
1297
- updatedAt: this.now(),
1298
- };
1299
- completionError = agent.error;
1300
- completionContent = agent.error;
1301
- return agent;
1302
- })
1303
- .finally(async () => {
1304
- const recipient = this.completionRecipient(agent);
1305
- const persistedCompletion = {
1306
- completionId: completionKey,
1307
- ...recipient,
1308
- runId,
1309
- generation: turnGeneration,
1310
- task: truncateUtf8(task, 256).text,
1311
- output: truncateUtf8(completionOutput, MAX_TOOL_MESSAGE_BYTES).text,
1312
- error: completionError ? truncateUtf8(completionError, 512).text : undefined,
1313
- createdAt: this.completionCreatedAt(),
1314
- };
1315
- agent.completionRequirements = makeCompletionAvailable(agent.completionRequirements, {
1316
- runId,
1317
- generation: turnGeneration,
1318
- completionId: persistedCompletion.completionId,
1319
- terminalState: agent.state,
1320
- updatedAt: persistedCompletion.createdAt,
1321
- });
1322
- agent.pendingCompletions = [...(agent.pendingCompletions ?? []), persistedCompletion];
1323
- this.enqueueCompletionMessage(agent, persistedCompletion, completionContent);
1324
- clearCurrentTurn(agent);
1325
- agent.updatedAt = this.now();
1326
- const persisted = await this.persistTerminalState().then(
1327
- () => true,
1328
- () => false,
1329
- );
1330
- this.controllers.delete(agent.id);
1331
- const turnCompletion: AgentTurnCompletion = {
1332
- ...persistedCompletion,
1333
- agent: this.copy(agent),
1334
- };
1335
- this.running.delete(agent.id);
1336
- resolveQueued(agent);
1337
- this.pumpQueue();
1338
- if (persisted) await this.notifyTurnComplete(turnCompletion);
1339
- });
1340
- }
1341
-
1342
- private updateQueuePositions(): void {
1343
- for (const [index, entry] of this.queue.entries()) {
1344
- if (!entry.agent.telemetry) continue;
1345
- entry.agent.telemetry = {
1346
- ...entry.agent.telemetry,
1347
- queuePosition: index + 1,
1348
- updatedAt: this.now(),
1349
- };
1350
- }
1351
- }
1352
-
1353
- private completionRecipient(
1354
- agent: ManagedAgent,
1355
- ): Pick<
1356
- NonNullable<ManagedAgent["pendingCompletions"]>[number],
1357
- "recipientId" | "recipientPath"
1358
- > {
1359
- return resolveCompletionRecipient(agent, (id) => this.agents.get(id));
1360
- }
1361
-
1362
- private enqueueCompletionMessage(
1363
- agent: ManagedAgent,
1364
- completion: NonNullable<ManagedAgent["pendingCompletions"]>[number],
1365
- content: string,
1366
- ): void {
1367
- if (!completion.recipientId || completion.recipientId === "root") return;
1368
- const recipient = this.agents.get(completion.recipientId);
1369
- if (!recipient || recipient.state === "closed") return;
1370
- this.enqueueMessage(
1371
- recipient,
1372
- content || "(no output)",
1373
- agent.id,
1374
- completion.completionId,
1375
- completion.completionId,
1376
- );
1377
- }
1378
-
1379
- private enqueueMessage(
1380
- recipient: ManagedAgent,
1381
- content: string,
1382
- senderId: string,
1383
- deduplicationKey?: string,
1384
- completionId?: string,
1385
- ): AgentMailboxMessage {
1386
- if (deduplicationKey) {
1387
- const existing = recipient.mailbox.find(
1388
- (message) => message.deduplicationKey === deduplicationKey && message.senderId === senderId,
1389
- );
1390
- if (existing) return existing;
1391
- }
1392
- const bounded = truncateUtf8(content, this.maxMailboxMessageBytes);
1393
- const message: AgentMailboxMessage = {
1394
- id: `msg_${randomUUID()}`,
1395
- senderId,
1396
- recipientId: recipient.id,
1397
- content: bounded.text,
1398
- createdAt: this.now(),
1399
- deduplicationKey,
1400
- completionId,
1401
- };
1402
- recipient.mailbox.push(message);
1403
- recipient.mailbox = recipient.mailbox.slice(-this.maxMailboxMessages);
1404
- recipient.updatedAt = this.now();
1405
- return message;
1406
- }
1407
-
1408
- private descendants(id: string): string[] {
1409
- const root = this.require(id);
1410
- const result: string[] = [];
1411
- const visit = (agent: ManagedAgent) => {
1412
- result.push(agent.id);
1413
- for (const childId of agent.children) {
1414
- const child = this.agents.get(childId);
1415
- if (child) visit(child);
1416
- }
1417
- };
1418
- visit(root);
1419
- return result;
1420
- }
1421
-
1422
- private require(reference: string, senderId = "root"): ManagedAgent {
1423
- const agent = this.findReference(reference, senderId);
1424
- if (!agent) throw new Error(`Unknown subagent: ${safeTerminalLine(reference, 256)}`);
1425
- return agent;
1426
- }
1427
-
1428
- private findReference(reference: string, senderId = "root"): ManagedAgent | undefined {
1429
- const direct = this.agents.get(reference);
1430
- if (direct) return direct;
1431
- const sender =
1432
- senderId === "root"
1433
- ? undefined
1434
- : (this.agents.get(senderId) ?? this.findByTaskPath(senderId));
1435
- if (senderId !== "root" && !sender) return undefined;
1436
- let taskPath: string;
1437
- try {
1438
- taskPath = resolveTaskPath(sender?.taskPath ?? ROOT_TASK_PATH, reference);
1439
- } catch {
1440
- return undefined;
1441
- }
1442
- return this.findByTaskPath(taskPath);
1443
- }
1444
-
1445
- private findByTaskPath(taskPath: string): ManagedAgent | undefined {
1446
- return [...this.agents.values()].find(
1447
- (agent) => agent.state !== "closed" && agent.taskPath === taskPath,
1448
- );
1449
- }
1450
-
1451
- private reserveRestoredIdentity(
1452
- record: ManagedAgent,
1453
- parentPath: string,
1454
- reservedPaths: Set<string>,
1455
- ): Pick<ManagedAgent, "taskName" | "taskPath"> {
1456
- let taskName: string | undefined;
1457
- try {
1458
- taskName = record.taskName ? validateTaskName(record.taskName) : undefined;
1459
- } catch {
1460
- taskName = undefined;
1461
- }
1462
- if (!taskName && record.taskPath) {
1463
- try {
1464
- const storedPath = validateTaskPath(record.taskPath);
1465
- const storedName = storedPath.slice(storedPath.lastIndexOf("/") + 1);
1466
- taskName = validateTaskName(storedName);
1467
- } catch {
1468
- taskName = undefined;
1469
- }
1470
- }
1471
- taskName ??= deriveTaskName(record.id);
1472
- let taskPath = joinTaskPath(parentPath, taskName);
1473
- if (reservedPaths.has(taskPath)) {
1474
- taskName = deriveTaskName(record.id);
1475
- taskPath = joinTaskPath(parentPath, taskName);
1476
- }
1477
- if (reservedPaths.has(taskPath)) {
1478
- throw new Error(`Cannot restore duplicate canonical subagent task path: ${taskPath}`);
1479
- }
1480
- reservedPaths.add(taskPath);
1481
- return { taskName, taskPath };
1482
- }
1483
-
1484
- private assertRequiredCompletionCapacity(mode: CompletionRequirementMode | undefined): void {
1485
- if (mode !== "required") return;
1486
- const unresolved = [...this.agents.values()].reduce(
1487
- (sum, agent) => sum + pendingRequiredCompletionCount(agent),
1488
- 0,
1489
- );
1490
- if (unresolved >= MAX_UNRESOLVED_REQUIRED_COMPLETIONS) {
1491
- throw new Error(
1492
- `Required subagent completion capacity reached (${MAX_UNRESOLVED_REQUIRED_COMPLETIONS}); wait for exact completion visibility or terminalize an existing requirement`,
1493
- );
1494
- }
1495
- }
1496
-
1497
- private retainedCount(): number {
1498
- return [...this.agents.values()].filter((agent) => agent.state !== "closed").length;
1499
- }
1500
-
1501
- private completionCreatedAt(): number {
1502
- this.lastCompletionAt = Math.max(this.now(), this.lastCompletionAt + 1);
1503
- return this.lastCompletionAt;
1504
- }
1505
-
1506
- private evictExpired(): ManagedAgent[] {
1507
- const cutoff = this.now() - this.idleTtlMs;
1508
- const protectedIds = new Set<string>();
1509
- for (const agent of this.agents.values()) {
1510
- if (
1511
- agent.state !== "running" &&
1512
- agent.state !== "starting" &&
1513
- (agent.pendingCompletions?.length ?? 0) === 0
1514
- ) {
1515
- continue;
1516
- }
1517
- let current: ManagedAgent | undefined = agent;
1518
- while (current) {
1519
- protectedIds.add(current.id);
1520
- current = current.parentId ? this.agents.get(current.parentId) : undefined;
1521
- }
1522
- }
1523
- const removed: ManagedAgent[] = [];
1524
- const candidates = [...this.agents.values()].sort((left, right) => right.depth - left.depth);
1525
- for (const agent of candidates) {
1526
- if (protectedIds.has(agent.id) || agent.updatedAt >= cutoff) continue;
1527
- if (agent.children.some((childId) => this.agents.get(childId)?.state !== "closed")) continue;
1528
- this.agents.delete(agent.id);
1529
- if (agent.parentId) {
1530
- const parent = this.agents.get(agent.parentId);
1531
- if (parent) parent.children = parent.children.filter((childId) => childId !== agent.id);
1532
- }
1533
- removed.push(this.copy(agent));
1534
- }
1535
- return removed;
1536
- }
1537
-
1538
- private async releaseAgents(agents: readonly ManagedAgent[]): Promise<void> {
1539
- if (!this.transport.release || agents.length === 0) return;
1540
- const results = await Promise.allSettled(
1541
- agents.map((agent) => this.transport.release?.(agent)),
1542
- );
1543
- const failures = results.flatMap((result) =>
1544
- result.status === "rejected" ? [result.reason] : [],
1545
- );
1546
- if (failures.length > 0) {
1547
- throw new AggregateError(
1548
- failures,
1549
- `Failed to release ${failures.length} subagent transport session(s)`,
1550
- );
1551
- }
1552
- }
1553
-
1554
- private pruneClosedAgents(): void {
1555
- const closed = [...this.agents.values()]
1556
- .filter((agent) => agent.state === "closed")
1557
- .sort((left, right) => right.updatedAt - left.updatedAt);
1558
- for (const agent of closed.slice(this.maxAgents)) this.agents.delete(agent.id);
1559
- }
1560
-
1561
- private async persistTerminalState(): Promise<void> {
1562
- let failures = 0;
1563
- for (;;) {
1564
- try {
1565
- await this.changed(true);
1566
- return;
1567
- } catch (error) {
1568
- if (this.shutdownController.signal.aborted) throw error;
1569
- failures++;
1570
- if (failures === 1) continue;
1571
- const delay = Math.min(
1572
- INITIAL_PERSISTENCE_RETRY_DELAY_MS * 2 ** (failures - 2),
1573
- MAX_PERSISTENCE_RETRY_DELAY_MS,
1574
- );
1575
- await waitForPersistenceRetry(delay, this.shutdownController.signal);
1576
- }
1577
- }
1578
- }
1579
-
1580
- private async notifyTurnComplete(completion: AgentTurnCompletion): Promise<void> {
1581
- try {
1582
- await this.options.onTurnComplete?.(completion);
1583
- } catch {
1584
- // Completion notifications are best-effort and must not destabilize agent lifecycle.
1585
- }
1586
- }
1587
-
1588
- private changed(propagateError = false): Promise<void> {
1589
- const snapshot = this.list(true);
1590
- const next = this.changeQueue.then(async () => {
1591
- try {
1592
- await this.options.onChange?.(snapshot);
1593
- } catch (error) {
1594
- if (propagateError) throw error;
1595
- // Non-terminal persistence remains best-effort so lifecycle controls stay usable.
1596
- }
1597
- });
1598
- this.changeQueue = next.catch(() => undefined);
1599
- return next;
1600
- }
1601
-
1602
- private inspectSummary(agent: ManagedAgent): AgentRunInspectionSummary {
1603
- let unreadMessages = 0;
1604
- for (const message of agent.mailbox) {
1605
- if (message.readAt === undefined) unreadMessages++;
1606
- }
1607
- return {
1608
- id: agent.id,
1609
- taskName: agent.taskName,
1610
- taskPath: agent.taskPath,
1611
- agent: agent.agent,
1612
- state: agent.state,
1613
- createdAt: agent.createdAt,
1614
- updatedAt: agent.updatedAt,
1615
- historyCount: agent.history.length,
1616
- unreadMessages,
1617
- turnGeneration: agent.turnGeneration ?? 0,
1618
- pendingCompletionCount: agent.pendingCompletions?.length ?? 0,
1619
- pendingRequiredCompletionCount: pendingRequiredCompletionCount(agent),
1620
- };
1621
- }
1622
-
1623
- private copy(agent: ManagedAgent): ManagedAgent {
1624
- return {
1625
- ...agent,
1626
- children: [...agent.children],
1627
- contextSourceIds: [...(agent.contextSourceIds ?? [])],
1628
- currentMailboxMessageIds: agent.currentMailboxMessageIds
1629
- ? [...agent.currentMailboxMessageIds]
1630
- : undefined,
1631
- pendingCompletions: (agent.pendingCompletions ?? []).map((completion) => ({
1632
- ...completion,
1633
- })),
1634
- completionRequirements: (agent.completionRequirements ?? []).map((record) => ({
1635
- ...record,
1636
- })),
1637
- history: agent.history.map((turn) => ({ ...turn })),
1638
- mailbox: agent.mailbox.map((message) => ({ ...message })),
1639
- contract: agent.contract ? structuredClone(agent.contract) : undefined,
1640
- target: agent.target ? { ...agent.target, trust: { ...agent.target.trust } } : undefined,
1641
- policy: agent.policy
1642
- ? {
1643
- inherited: [...agent.policy.inherited],
1644
- overridden: [...agent.policy.overridden],
1645
- unsupported: [...agent.policy.unsupported],
1646
- }
1647
- : undefined,
1648
- structuredResult: agent.structuredResult
1649
- ? copyStructuredResult(agent.structuredResult)
1650
- : undefined,
1651
- termination: agent.termination ? copyTurnTerminationReport(agent.termination) : undefined,
1652
- outcome: agent.outcome ? structuredClone(agent.outcome) : undefined,
1653
- executionPlan: agent.executionPlan ? copyExecutionPlan(agent.executionPlan) : undefined,
1654
- capabilityGrant: agent.capabilityGrant ? structuredClone(agent.capabilityGrant) : undefined,
1655
- semanticSnapshot: agent.semanticSnapshot
1656
- ? structuredClone(agent.semanticSnapshot)
1657
- : undefined,
1658
- semanticCompatibility: agent.semanticCompatibility
1659
- ? structuredClone(agent.semanticCompatibility)
1660
- : undefined,
1661
- telemetry: agent.telemetry ? copyTelemetry(agent.telemetry) : undefined,
1662
- };
1663
- }
1664
- }
1665
-
1666
- function lifecycleStateForOutcome(
1667
- status: import("./result-contract.js").SubagentOutcomeStatus | undefined,
1668
- ): AgentLifecycleState {
1669
- switch (status) {
1670
- case "partial":
1671
- return "partial";
1672
- case "blocked":
1673
- case "needs-input":
1674
- case "abstained":
1675
- case "stale":
1676
- return status;
1677
- case "failed":
1678
- case "contract-invalid":
1679
- return "failed";
1680
- case "interrupted":
1681
- return "interrupted";
1682
- default:
1683
- return "completed";
1684
- }
1685
- }
1686
-
1687
- function copyStructuredResult(value: AnyStructuredSubagentResult): AnyStructuredSubagentResult {
1688
- return structuredClone(value);
1689
- }
1690
-
1691
- function copyTelemetry(value: TransportTelemetry): TransportTelemetry {
1692
- return {
1693
- ...value,
1694
- timing: { ...value.timing },
1695
- usage: value.usage ? { ...value.usage } : undefined,
1696
- budgetSource: value.budgetSource ? { ...value.budgetSource } : undefined,
1697
- };
1698
- }