@narumitw/pi-subagents 2.1.4 → 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 (312) hide show
  1. package/README.md +187 -1228
  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 -1614
  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 -139
  179. package/docs/pi-subagents-diagrams.md +0 -183
  180. package/src/adaptive-scheduler.ts +0 -224
  181. package/src/admission-policy.ts +0 -78
  182. package/src/agent-projection.ts +0 -53
  183. package/src/agents/built-ins.ts +0 -71
  184. package/src/agents/catalog.ts +0 -241
  185. package/src/agents/discovery.ts +0 -265
  186. package/src/agents/types.ts +0 -100
  187. package/src/agents.ts +0 -51
  188. package/src/auto-transport.ts +0 -121
  189. package/src/blocking-status.ts +0 -63
  190. package/src/cached-module-loader.ts +0 -18
  191. package/src/capabilities.ts +0 -145
  192. package/src/capability-grant.ts +0 -115
  193. package/src/capability-router.ts +0 -107
  194. package/src/child-peer-bridge.ts +0 -124
  195. package/src/child-peer-tools.ts +0 -132
  196. package/src/completion-delivery.ts +0 -409
  197. package/src/completion-render.ts +0 -190
  198. package/src/completion-requirement.ts +0 -479
  199. package/src/completion-routing.ts +0 -24
  200. package/src/config-registration.ts +0 -114
  201. package/src/config-status.ts +0 -217
  202. package/src/config-ui.ts +0 -983
  203. package/src/consult-policy.ts +0 -15
  204. package/src/consult-registration.ts +0 -125
  205. package/src/consult-render.ts +0 -194
  206. package/src/consult-resources.ts +0 -51
  207. package/src/consult-tool.ts +0 -95
  208. package/src/consult.ts +0 -566
  209. package/src/context.ts +0 -126
  210. package/src/create-stateful-transport.ts +0 -157
  211. package/src/cwd-policy.ts +0 -183
  212. package/src/delegation-contract.ts +0 -463
  213. package/src/execution/budget.ts +0 -56
  214. package/src/execution/runtime-policy.ts +0 -19
  215. package/src/execution-plan.ts +0 -322
  216. package/src/execution-ui.ts +0 -259
  217. package/src/execution.ts +0 -1679
  218. package/src/in-process-transport.ts +0 -941
  219. package/src/inspect-registration.ts +0 -64
  220. package/src/inspect-render.ts +0 -334
  221. package/src/inspect-tool.ts +0 -45
  222. package/src/inspect.ts +0 -769
  223. package/src/integration-controller.ts +0 -98
  224. package/src/limits.ts +0 -75
  225. package/src/orchestration-metrics.ts +0 -116
  226. package/src/outcome.ts +0 -61
  227. package/src/panel-child-group.ts +0 -35
  228. package/src/panel-contract.ts +0 -343
  229. package/src/panel-evidence.ts +0 -59
  230. package/src/panel-execution.ts +0 -769
  231. package/src/panel-failure.ts +0 -56
  232. package/src/panel-planning.ts +0 -175
  233. package/src/panel-presets.ts +0 -3
  234. package/src/panel-prompts.ts +0 -132
  235. package/src/panel-reconciliation.ts +0 -57
  236. package/src/panel-render.ts +0 -103
  237. package/src/parallel-limit-ui.ts +0 -113
  238. package/src/params.ts +0 -278
  239. package/src/peer-communication.ts +0 -352
  240. package/src/peer-transport.ts +0 -49
  241. package/src/persistence.ts +0 -522
  242. package/src/pi-args.ts +0 -43
  243. package/src/pi-invocation.ts +0 -168
  244. package/src/process-control.ts +0 -43
  245. package/src/prompt-resources.ts +0 -38
  246. package/src/prompt-source-safety.ts +0 -48
  247. package/src/protocol.ts +0 -76
  248. package/src/registry-types.ts +0 -204
  249. package/src/registry.ts +0 -1698
  250. package/src/render-common.ts +0 -252
  251. package/src/render.ts +0 -704
  252. package/src/result-contract.ts +0 -431
  253. package/src/retained-semantic-state.ts +0 -100
  254. package/src/rpc-timeout-finalization.ts +0 -207
  255. package/src/rpc-transport-metadata.ts +0 -65
  256. package/src/rpc-transport.ts +0 -1035
  257. package/src/rpc-turn-capture.ts +0 -217
  258. package/src/runner-outcome.ts +0 -31
  259. package/src/runner-result.ts +0 -55
  260. package/src/runner-types.ts +0 -102
  261. package/src/runner-usage.ts +0 -48
  262. package/src/runner.ts +0 -866
  263. package/src/safe-text.ts +0 -67
  264. package/src/semantic-snapshot.ts +0 -214
  265. package/src/session-guidance-contract.ts +0 -399
  266. package/src/settings/inspection.ts +0 -302
  267. package/src/settings/schema.ts +0 -195
  268. package/src/settings-reader.ts +0 -215
  269. package/src/settings.ts +0 -452
  270. package/src/spawn-idempotency.ts +0 -68
  271. package/src/stateful-agent-view.ts +0 -85
  272. package/src/stateful-config.ts +0 -13
  273. package/src/stateful-guidance.ts +0 -29
  274. package/src/stateful-lifecycle.ts +0 -74
  275. package/src/stateful-limit-ui.ts +0 -249
  276. package/src/stateful-limits.ts +0 -96
  277. package/src/stateful-prompt.ts +0 -49
  278. package/src/stateful-registration.ts +0 -1320
  279. package/src/stateful-render.ts +0 -318
  280. package/src/stateful-safety.ts +0 -47
  281. package/src/stateful-tool-params.ts +0 -215
  282. package/src/stateful.ts +0 -12
  283. package/src/subagent-details.ts +0 -43
  284. package/src/subagents-extension.ts +0 -372
  285. package/src/subprocess-transport.ts +0 -145
  286. package/src/supervision.ts +0 -104
  287. package/src/task-path.ts +0 -65
  288. package/src/timeout-checkpoint.ts +0 -305
  289. package/src/timeout-finalization.ts +0 -75
  290. package/src/tool-schema-compatibility.ts +0 -73
  291. package/src/transport-types.ts +0 -74
  292. package/src/transport-ui.ts +0 -135
  293. package/src/transport.ts +0 -43
  294. package/src/turn-budget.ts +0 -109
  295. package/src/usage-format.ts +0 -42
  296. package/src/usage-recording-config.ts +0 -13
  297. package/src/usage-recording-store.ts +0 -183
  298. package/src/usage-recording.ts +0 -478
  299. package/src/verification-harness.ts +0 -516
  300. package/src/verification-policy.ts +0 -67
  301. package/src/verification-receipt.ts +0 -275
  302. package/src/verified-execution-benchmark.ts +0 -86
  303. package/src/verified-execution-contract.ts +0 -191
  304. package/src/verified-execution-schema.ts +0 -32
  305. package/src/work-item-ledger.ts +0 -1404
  306. package/src/work-item-persistence.ts +0 -254
  307. package/src/workflow-completion-controller.ts +0 -397
  308. package/src/workflow-planning.ts +0 -172
  309. package/src/workflow-tree-identity.ts +0 -289
  310. package/src/workflow-ui.ts +0 -69
  311. package/src/workflow-verification.ts +0 -296
  312. 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
- }