@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
@@ -0,0 +1,60 @@
1
+ import { type ExtensionAPI, getMarkdownTheme, keyText } from "@earendil-works/pi-coding-agent";
2
+ import { Box, type Component, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
3
+ import { sanitizeTerminalText } from "./message-broker.js";
4
+
5
+ export const COMPLETION_MESSAGE_TYPE = "pi-subagents-completion";
6
+
7
+ function createCompletionBox(
8
+ children: Component[],
9
+ outputPad: number,
10
+ background: (text: string) => string,
11
+ ): Component {
12
+ return {
13
+ render(width) {
14
+ if (width <= 0) return [];
15
+ const maxPadding = Math.floor((width - 1) / 2);
16
+ const box = new Box(Math.min(Math.max(0, outputPad), maxPadding), 1, background);
17
+ for (const child of children) box.addChild(child);
18
+ return box.render(width);
19
+ },
20
+ invalidate() {
21
+ for (const child of children) child.invalidate();
22
+ },
23
+ };
24
+ }
25
+
26
+ export function registerCompletionRenderer(pi: ExtensionAPI): void {
27
+ pi.registerMessageRenderer(COMPLETION_MESSAGE_TYPE, (message, options, theme) => {
28
+ const children: Component[] = [
29
+ new Text(theme.fg("customMessageLabel", theme.bold(`[${COMPLETION_MESSAGE_TYPE}]`)), 0, 0),
30
+ new Spacer(1),
31
+ ];
32
+ if (!options.expanded) {
33
+ children.push(
34
+ new Text(
35
+ theme.fg("customMessageText", "Subagent job completion (") +
36
+ theme.fg("dim", keyText("app.tools.expand")) +
37
+ theme.fg("customMessageText", " to expand)"),
38
+ 0,
39
+ 0,
40
+ ),
41
+ );
42
+ } else {
43
+ const content =
44
+ typeof message.content === "string"
45
+ ? message.content
46
+ : message.content
47
+ .filter((part) => part.type === "text")
48
+ .map((part) => part.text)
49
+ .join("\n");
50
+ children.push(
51
+ new Markdown(sanitizeTerminalText(content), 0, 0, getMarkdownTheme(), {
52
+ color: (text) => theme.fg("customMessageText", text),
53
+ }),
54
+ );
55
+ }
56
+ return createCompletionBox(children, options.outputPad, (text) =>
57
+ theme.bg("customMessageBg", text),
58
+ );
59
+ });
60
+ }
package/src/index.ts CHANGED
@@ -1 +1 @@
1
- export { default } from "./subagents-extension.js";
1
+ export { default } from "./subagents.js";
@@ -0,0 +1,628 @@
1
+ import { randomBytes, randomUUID } from "node:crypto";
2
+ import net, { type AddressInfo, type Server, type Socket } from "node:net";
3
+ import type { BrokerCredentials } from "./types.js";
4
+
5
+ export const BROKER_HOST = "127.0.0.1" as const;
6
+ export const MAX_MESSAGE_BYTES = 48 * 1024;
7
+ export const MAX_MESSAGE_LINES = 1_992;
8
+ export const MAX_FRAME_BYTES = 384 * 1024;
9
+ export const MAX_ERROR_BYTES = 8 * 1024;
10
+ export const MAX_OUTSTANDING_REQUESTS = 4;
11
+ export const MAX_IDENTIFIER_LENGTH = 128;
12
+
13
+ const MAX_CONNECTIONS = 32;
14
+ const REQUEST_FRAME_TIMEOUT_MS = 2_000;
15
+ const MAX_TIMEOUT_MS = 2_147_483_647;
16
+ const MAX_RETAINED_CONSUMED_REQUESTS = 4;
17
+ const CHILD_WAIT_INTERRUPTED_ERROR =
18
+ "Subagent wait was interrupted by an incoming main-agent request. The original request remains active.";
19
+
20
+ export type BrokerRequestOrigin = "main" | "child";
21
+
22
+ export interface BrokerInboundMessage {
23
+ kind: "request" | "response";
24
+ requestId: string;
25
+ jobId: string;
26
+ message: string;
27
+ }
28
+
29
+ export interface BrokerSendAcknowledgement {
30
+ requestId: string;
31
+ accepted: boolean;
32
+ duplicate: boolean;
33
+ }
34
+
35
+ export interface MessageBrokerOptions {
36
+ onMessage(message: BrokerInboundMessage): void;
37
+ createServer?: (listener: (socket: Socket) => void) => Server;
38
+ now?: () => number;
39
+ requestFrameTimeoutMs?: number;
40
+ }
41
+
42
+ interface JobRecord {
43
+ jobId: string;
44
+ generation: number;
45
+ token: string;
46
+ }
47
+
48
+ interface RequestWaiter {
49
+ socket: Socket;
50
+ timer?: NodeJS.Timeout;
51
+ }
52
+
53
+ interface RequestRecord {
54
+ requestId: string;
55
+ jobId: string;
56
+ generation: number;
57
+ origin: BrokerRequestOrigin;
58
+ expectedResponder: BrokerRequestOrigin;
59
+ message: string;
60
+ createdAt: number;
61
+ response?: string;
62
+ consumedAt?: number;
63
+ deliveryQueued?: boolean;
64
+ waiter?: RequestWaiter;
65
+ }
66
+
67
+ type BrokerRequest =
68
+ | {
69
+ type: "send";
70
+ token: string;
71
+ recipient?: string;
72
+ requestId?: string;
73
+ message: string;
74
+ }
75
+ | { type: "wait"; token: string; requestId: string; timeoutMs?: number };
76
+
77
+ /** Session-scoped authenticated loopback transport for bidirectional requests and responses. */
78
+ export class MessageBroker {
79
+ private server?: Server;
80
+ private starting?: Promise<void>;
81
+ private address?: { host: typeof BROKER_HOST; port: number };
82
+ private failure?: string;
83
+ private generation = 0;
84
+ private readonly sockets = new Set<Socket>();
85
+ private readonly jobsByToken = new Map<string, JobRecord>();
86
+ private readonly tokenByJob = new Map<string, string>();
87
+ private readonly requests = new Map<string, RequestRecord>();
88
+ private readonly inboundMessageListeners = new Set<() => void>();
89
+ private pendingInboundResponse = false;
90
+ private readonly createServer: (listener: (socket: Socket) => void) => Server;
91
+ private readonly now: () => number;
92
+ private readonly requestFrameTimeoutMs: number;
93
+
94
+ constructor(private readonly options: MessageBrokerOptions) {
95
+ this.createServer = options.createServer ?? ((listener) => net.createServer(listener));
96
+ this.now = options.now ?? Date.now;
97
+ this.requestFrameTimeoutMs = options.requestFrameTimeoutMs ?? REQUEST_FRAME_TIMEOUT_MS;
98
+ if (!Number.isFinite(this.requestFrameTimeoutMs) || this.requestFrameTimeoutMs <= 0) {
99
+ throw new Error("Subagent broker frame timeout must be positive.");
100
+ }
101
+ }
102
+
103
+ async start(): Promise<void> {
104
+ if (this.server && this.address && !this.failure) return;
105
+ if (this.starting) return this.starting;
106
+ const ownerGeneration = ++this.generation;
107
+ this.failure = undefined;
108
+ this.starting = new Promise<void>((resolve, reject) => {
109
+ const server = this.createServer((socket) => this.accept(socket));
110
+ server.maxConnections = MAX_CONNECTIONS;
111
+ const onStartError = (error: Error) => {
112
+ server.close(() => undefined);
113
+ reject(error);
114
+ };
115
+ server.once("error", onStartError);
116
+ server.listen({ host: BROKER_HOST, port: 0 }, () => {
117
+ server.off("error", onStartError);
118
+ if (ownerGeneration !== this.generation) {
119
+ server.close(() => undefined);
120
+ reject(new Error("Subagent message broker start was superseded."));
121
+ return;
122
+ }
123
+ const address = server.address();
124
+ if (!isLoopbackAddress(address)) {
125
+ server.close(() => undefined);
126
+ reject(new Error("Subagent message broker did not bind to loopback."));
127
+ return;
128
+ }
129
+ this.server = server;
130
+ this.address = { host: BROKER_HOST, port: address.port };
131
+ server.on("error", (error) => this.fail(error));
132
+ server.unref();
133
+ resolve();
134
+ });
135
+ })
136
+ .catch((error) => {
137
+ const message = truncateError(error instanceof Error ? error.message : String(error));
138
+ this.failure = message;
139
+ throw new Error(`Subagent message broker failed to start: ${message}`);
140
+ })
141
+ .finally(() => {
142
+ this.starting = undefined;
143
+ });
144
+ return this.starting;
145
+ }
146
+
147
+ assertReady(): void {
148
+ if (this.server && this.address && !this.failure) return;
149
+ throw new Error(
150
+ this.failure
151
+ ? `Subagent messaging is unavailable: ${this.failure}`
152
+ : "Subagent messaging is unavailable because the session broker is not ready.",
153
+ );
154
+ }
155
+
156
+ issueCredentials(input: { jobId: string; generation: number }): BrokerCredentials {
157
+ this.assertReady();
158
+ if (this.tokenByJob.has(input.jobId)) {
159
+ throw new Error(`Subagent messaging credentials already exist for ${input.jobId}.`);
160
+ }
161
+ const token = randomBytes(32).toString("hex");
162
+ const record: JobRecord = { ...input, token };
163
+ this.jobsByToken.set(token, record);
164
+ this.tokenByJob.set(input.jobId, token);
165
+ return { host: BROKER_HOST, port: this.address?.port ?? 0, token };
166
+ }
167
+
168
+ createMainRequest(jobId: string, message: string): BrokerSendAcknowledgement {
169
+ this.assertReady();
170
+ const job = this.requireJob(jobId);
171
+ return this.createRequest(job, "main", message);
172
+ }
173
+
174
+ replyFromMain(requestId: string, message: string): BrokerSendAcknowledgement {
175
+ this.assertReady();
176
+ return this.acceptResponse("main", undefined, requestId, message);
177
+ }
178
+
179
+ rollbackMainRequest(requestId: string): void {
180
+ const request = this.requests.get(requestId);
181
+ if (request?.origin === "main" && request.response === undefined) {
182
+ this.clearWaiter(request);
183
+ this.requests.delete(requestId);
184
+ }
185
+ }
186
+
187
+ revokeJob(jobId: string, reason = "Subagent job is no longer active."): void {
188
+ const token = this.tokenByJob.get(jobId);
189
+ if (token) {
190
+ this.tokenByJob.delete(jobId);
191
+ this.jobsByToken.delete(token);
192
+ }
193
+ for (const request of [...this.requests.values()]) {
194
+ if (request.jobId !== jobId) continue;
195
+ if (request.waiter) this.respondError(request.waiter.socket, reason);
196
+ this.clearWaiter(request);
197
+ this.requests.delete(request.requestId);
198
+ }
199
+ }
200
+
201
+ hasPendingMainRequest(): boolean {
202
+ return [...this.requests.values()].some(
203
+ (request) => request.origin === "child" && request.response === undefined,
204
+ );
205
+ }
206
+
207
+ takePendingInboundResponse(): boolean {
208
+ const pending = this.pendingInboundResponse;
209
+ this.pendingInboundResponse = false;
210
+ return pending;
211
+ }
212
+
213
+ markMainRequestQueued(requestId: string): boolean {
214
+ const request = this.requests.get(requestId);
215
+ if (request?.origin !== "main") {
216
+ throw new Error("Unknown or expired main-agent subagent request.");
217
+ }
218
+ request.deliveryQueued = true;
219
+ return request.response === undefined;
220
+ }
221
+
222
+ interruptChildWaits(jobId: string): number {
223
+ let interrupted = 0;
224
+ for (const request of this.requests.values()) {
225
+ if (request.jobId !== jobId || request.origin !== "child" || !request.waiter) continue;
226
+ const socket = request.waiter.socket;
227
+ this.clearWaiter(request);
228
+ this.respondError(socket, CHILD_WAIT_INTERRUPTED_ERROR);
229
+ interrupted++;
230
+ }
231
+ return interrupted;
232
+ }
233
+
234
+ subscribeInboundMessage(listener: () => void): () => void {
235
+ this.inboundMessageListeners.add(listener);
236
+ if (this.hasPendingMainRequest()) listener();
237
+ return () => this.inboundMessageListeners.delete(listener);
238
+ }
239
+
240
+ async shutdown(): Promise<void> {
241
+ ++this.generation;
242
+ const starting = this.starting;
243
+ if (starting) await starting.catch(() => undefined);
244
+ for (const jobId of [...this.tokenByJob.keys()]) {
245
+ this.revokeJob(jobId, "Subagent session shut down.");
246
+ }
247
+ this.requests.clear();
248
+ this.inboundMessageListeners.clear();
249
+ this.pendingInboundResponse = false;
250
+ for (const socket of this.sockets) socket.destroy();
251
+ this.sockets.clear();
252
+ const server = this.server;
253
+ this.server = undefined;
254
+ this.address = undefined;
255
+ this.failure = undefined;
256
+ if (!server?.listening) return;
257
+ await new Promise<void>((resolve) => server.close(() => resolve()));
258
+ }
259
+
260
+ private requireJob(jobId: string): JobRecord {
261
+ const token = this.tokenByJob.get(jobId);
262
+ const job = token ? this.jobsByToken.get(token) : undefined;
263
+ if (!job) throw new Error("Unknown or inactive subagent job.");
264
+ return job;
265
+ }
266
+
267
+ private hasQueuedRequestForChild(jobId: string): boolean {
268
+ return [...this.requests.values()].some(
269
+ (request) =>
270
+ request.jobId === jobId &&
271
+ request.origin === "main" &&
272
+ request.deliveryQueued === true &&
273
+ request.response === undefined,
274
+ );
275
+ }
276
+
277
+ private createRequest(
278
+ job: JobRecord,
279
+ origin: BrokerRequestOrigin,
280
+ message: string,
281
+ ): BrokerSendAcknowledgement {
282
+ validateMessage(message, "Subagent request");
283
+ const outstanding = [...this.requests.values()].filter(
284
+ (request) => request.jobId === job.jobId && request.consumedAt === undefined,
285
+ ).length;
286
+ if (outstanding >= MAX_OUTSTANDING_REQUESTS) {
287
+ throw new Error(
288
+ `Subagent job may have at most ${MAX_OUTSTANDING_REQUESTS} outstanding requests.`,
289
+ );
290
+ }
291
+ const request: RequestRecord = {
292
+ requestId: `req_${randomUUID()}`,
293
+ jobId: job.jobId,
294
+ generation: job.generation,
295
+ origin,
296
+ expectedResponder: origin === "main" ? "child" : "main",
297
+ message,
298
+ createdAt: this.now(),
299
+ };
300
+ this.requests.set(request.requestId, request);
301
+ if (origin === "child") {
302
+ try {
303
+ this.deliverInbound({
304
+ kind: "request",
305
+ requestId: request.requestId,
306
+ jobId: request.jobId,
307
+ message: request.message,
308
+ });
309
+ } catch (error) {
310
+ this.requests.delete(request.requestId);
311
+ throw error;
312
+ }
313
+ }
314
+ return { requestId: request.requestId, accepted: true, duplicate: false };
315
+ }
316
+
317
+ private acceptResponse(
318
+ responder: BrokerRequestOrigin,
319
+ job: JobRecord | undefined,
320
+ requestId: string,
321
+ message: string,
322
+ ): BrokerSendAcknowledgement {
323
+ validateRequestId(requestId);
324
+ validateMessage(message, "Subagent response");
325
+ const request = this.requests.get(requestId);
326
+ if (
327
+ !request ||
328
+ request.expectedResponder !== responder ||
329
+ (job && (request.jobId !== job.jobId || request.generation !== job.generation))
330
+ ) {
331
+ throw new Error("Unknown, expired, or unauthorized subagent request.");
332
+ }
333
+ if (request.response !== undefined) {
334
+ return { requestId, accepted: false, duplicate: true };
335
+ }
336
+ request.response = message;
337
+ if (responder === "child") {
338
+ try {
339
+ this.deliverInbound({
340
+ kind: "response",
341
+ requestId,
342
+ jobId: request.jobId,
343
+ message,
344
+ });
345
+ this.markConsumed(request);
346
+ } catch (error) {
347
+ request.response = undefined;
348
+ throw error;
349
+ }
350
+ } else if (request.waiter) {
351
+ const socket = request.waiter.socket;
352
+ this.clearWaiter(request);
353
+ this.respond(socket, { ok: true, response: message }, () => this.markConsumed(request));
354
+ }
355
+ return { requestId, accepted: true, duplicate: false };
356
+ }
357
+
358
+ private deliverInbound(message: BrokerInboundMessage): void {
359
+ this.options.onMessage(message);
360
+ if (message.kind === "response" && this.inboundMessageListeners.size === 0) {
361
+ this.pendingInboundResponse = true;
362
+ }
363
+ for (const listener of [...this.inboundMessageListeners]) {
364
+ try {
365
+ listener();
366
+ } catch {
367
+ // Runtime wait observers cannot interrupt broker delivery.
368
+ }
369
+ }
370
+ }
371
+
372
+ private accept(socket: Socket): void {
373
+ if (!this.server || this.failure || this.sockets.size >= MAX_CONNECTIONS) {
374
+ socket.destroy();
375
+ return;
376
+ }
377
+ this.sockets.add(socket);
378
+ socket.setNoDelay(true);
379
+ let frame = Buffer.alloc(0);
380
+ let handled = false;
381
+ const frameTimer = setTimeout(() => {
382
+ if (handled) return;
383
+ handled = true;
384
+ this.respondError(socket, "Subagent broker request frame timed out.");
385
+ }, this.requestFrameTimeoutMs);
386
+ frameTimer.unref();
387
+ const cleanup = () => {
388
+ clearTimeout(frameTimer);
389
+ this.sockets.delete(socket);
390
+ for (const request of this.requests.values()) {
391
+ if (request.waiter?.socket === socket) this.clearWaiter(request);
392
+ }
393
+ };
394
+ socket.on("data", (chunk: Buffer) => {
395
+ if (handled) return;
396
+ frame = Buffer.concat([frame, chunk]);
397
+ if (frame.byteLength > MAX_FRAME_BYTES) {
398
+ handled = true;
399
+ clearTimeout(frameTimer);
400
+ this.respondError(socket, "Subagent broker request exceeded its size limit.");
401
+ return;
402
+ }
403
+ const newline = frame.indexOf(0x0a);
404
+ if (newline < 0) return;
405
+ handled = true;
406
+ clearTimeout(frameTimer);
407
+ socket.removeAllListeners("data");
408
+ const trailing = frame
409
+ .subarray(newline + 1)
410
+ .toString("utf8")
411
+ .trim();
412
+ if (trailing) {
413
+ this.respondError(socket, "Subagent broker accepts one request per connection.");
414
+ return;
415
+ }
416
+ this.handleFrame(socket, frame.subarray(0, newline).toString("utf8"));
417
+ });
418
+ socket.once("close", cleanup);
419
+ socket.once("error", cleanup);
420
+ }
421
+
422
+ private handleFrame(socket: Socket, frame: string): void {
423
+ let request: BrokerRequest;
424
+ try {
425
+ const parsed = JSON.parse(frame) as unknown;
426
+ if (!isRecord(parsed)) throw new Error();
427
+ request = parsed as BrokerRequest;
428
+ } catch {
429
+ this.respondError(socket, "Malformed subagent broker request.");
430
+ return;
431
+ }
432
+ const job = typeof request.token === "string" ? this.jobsByToken.get(request.token) : undefined;
433
+ if (!job) {
434
+ this.respondError(socket, "Unauthenticated subagent broker request.");
435
+ return;
436
+ }
437
+ try {
438
+ if (request.type === "send") {
439
+ this.handleSend(socket, job, request);
440
+ return;
441
+ }
442
+ if (request.type === "wait") {
443
+ this.handleWait(socket, job, request);
444
+ return;
445
+ }
446
+ throw new Error("Unsupported subagent broker request.");
447
+ } catch (error) {
448
+ this.respondError(socket, error instanceof Error ? error.message : String(error));
449
+ }
450
+ }
451
+
452
+ private handleSend(
453
+ socket: Socket,
454
+ job: JobRecord,
455
+ request: Extract<BrokerRequest, { type: "send" }>,
456
+ ): void {
457
+ if (typeof request.message !== "string") {
458
+ throw new Error("subagent_send requires a message string.");
459
+ }
460
+ const hasRecipient = typeof request.recipient === "string";
461
+ const hasRequestId = typeof request.requestId === "string";
462
+ if (hasRecipient === hasRequestId) {
463
+ throw new Error("subagent_send requires exactly one of recipient or requestId.");
464
+ }
465
+ const acknowledgement = hasRecipient
466
+ ? request.recipient === "main"
467
+ ? this.createRequest(job, "child", request.message)
468
+ : (() => {
469
+ throw new Error('A subagent may send new requests only to recipient "main".');
470
+ })()
471
+ : this.acceptResponse("child", job, request.requestId ?? "", request.message);
472
+ this.respond(socket, { ok: true, ...acknowledgement });
473
+ }
474
+
475
+ private handleWait(
476
+ socket: Socket,
477
+ job: JobRecord,
478
+ request: Extract<BrokerRequest, { type: "wait" }>,
479
+ ): void {
480
+ if (typeof request.requestId !== "string") {
481
+ throw new Error("Subagent wait requires a request ID.");
482
+ }
483
+ validateRequestId(request.requestId);
484
+ if (request.timeoutMs !== undefined) validateTimeout(request.timeoutMs);
485
+ const record = this.requests.get(request.requestId);
486
+ if (
487
+ record?.origin !== "child" ||
488
+ record.jobId !== job.jobId ||
489
+ record.generation !== job.generation
490
+ ) {
491
+ throw new Error("Unknown or expired subagent request.");
492
+ }
493
+ if (record.consumedAt !== undefined && record.response !== undefined) {
494
+ this.respond(socket, { ok: true, response: record.response });
495
+ return;
496
+ }
497
+ if (record.response !== undefined) {
498
+ this.respond(socket, { ok: true, response: record.response }, () =>
499
+ this.markConsumed(record),
500
+ );
501
+ return;
502
+ }
503
+ if (this.hasQueuedRequestForChild(job.jobId)) {
504
+ this.respondError(socket, CHILD_WAIT_INTERRUPTED_ERROR);
505
+ return;
506
+ }
507
+ if (record.waiter) {
508
+ throw new Error(`A wait is already active for subagent request ${request.requestId}.`);
509
+ }
510
+ const waiter: RequestWaiter = { socket };
511
+ record.waiter = waiter;
512
+ if (request.timeoutMs !== undefined) {
513
+ waiter.timer = setTimeout(() => {
514
+ if (record.waiter !== waiter) return;
515
+ this.clearWaiter(record);
516
+ this.respondError(socket, "Subagent response wait timed out.");
517
+ }, request.timeoutMs);
518
+ waiter.timer.unref();
519
+ }
520
+ }
521
+
522
+ private markConsumed(request: RequestRecord): void {
523
+ if (this.requests.get(request.requestId) !== request || request.response === undefined) return;
524
+ request.consumedAt ??= this.now();
525
+ const consumed = [...this.requests.values()]
526
+ .filter(
527
+ (candidate) => candidate.jobId === request.jobId && candidate.consumedAt !== undefined,
528
+ )
529
+ .sort((left, right) => (left.consumedAt ?? 0) - (right.consumedAt ?? 0));
530
+ for (const expired of consumed.slice(
531
+ 0,
532
+ Math.max(0, consumed.length - MAX_RETAINED_CONSUMED_REQUESTS),
533
+ )) {
534
+ this.requests.delete(expired.requestId);
535
+ }
536
+ }
537
+
538
+ private clearWaiter(request: RequestRecord): void {
539
+ if (request.waiter?.timer) clearTimeout(request.waiter.timer);
540
+ request.waiter = undefined;
541
+ }
542
+
543
+ private respondError(socket: Socket, error: string): void {
544
+ this.respond(socket, { ok: false, error: truncateError(error) });
545
+ }
546
+
547
+ private respond(socket: Socket, value: Record<string, unknown>, onFlushed?: () => void): void {
548
+ if (socket.destroyed) return;
549
+ let content = `${JSON.stringify(value)}\n`;
550
+ if (Buffer.byteLength(content, "utf8") > MAX_FRAME_BYTES) {
551
+ content = `${JSON.stringify({ ok: false, error: "Subagent broker response exceeded its size limit." })}\n`;
552
+ }
553
+ socket.end(content, onFlushed);
554
+ }
555
+
556
+ private fail(error: Error): void {
557
+ this.failure = truncateError(error.message);
558
+ for (const jobId of [...this.tokenByJob.keys()]) {
559
+ this.revokeJob(jobId, "Subagent message broker failed.");
560
+ }
561
+ for (const socket of this.sockets) socket.destroy();
562
+ this.sockets.clear();
563
+ this.address = undefined;
564
+ }
565
+ }
566
+
567
+ export function validateMessage(message: string, label: string): void {
568
+ if (!message.trim()) throw new Error(`${label} is required.`);
569
+ if (message.includes("\0")) throw new Error(`${label} must not contain NUL bytes.`);
570
+ if (Buffer.byteLength(message, "utf8") > MAX_MESSAGE_BYTES) {
571
+ throw new Error(`${label} must be at most ${MAX_MESSAGE_BYTES} UTF-8 bytes.`);
572
+ }
573
+ if (lineCount(message) > MAX_MESSAGE_LINES) {
574
+ throw new Error(`${label} must contain at most ${MAX_MESSAGE_LINES} lines.`);
575
+ }
576
+ }
577
+
578
+ export function sanitizeTerminalText(value: string): string {
579
+ return [...value]
580
+ .filter((character) => {
581
+ const codePoint = character.codePointAt(0) ?? 0;
582
+ if (character === "\n" || character === "\t") return true;
583
+ if (codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f)) return false;
584
+ return !(
585
+ (codePoint >= 0x202a && codePoint <= 0x202e) ||
586
+ (codePoint >= 0x2066 && codePoint <= 0x2069)
587
+ );
588
+ })
589
+ .join("");
590
+ }
591
+
592
+ function validateRequestId(requestId: string): void {
593
+ if (
594
+ requestId.length > MAX_IDENTIFIER_LENGTH ||
595
+ !/^req_[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u.test(requestId)
596
+ ) {
597
+ throw new Error("Invalid subagent request ID.");
598
+ }
599
+ }
600
+
601
+ function validateTimeout(timeoutMs: number): void {
602
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > MAX_TIMEOUT_MS) {
603
+ throw new Error("Subagent wait timeout is outside the supported range.");
604
+ }
605
+ }
606
+
607
+ function lineCount(value: string): number {
608
+ let count = 1;
609
+ for (const character of value) if (character === "\n") count++;
610
+ return count;
611
+ }
612
+
613
+ function truncateError(error: string): string {
614
+ const bytes = Buffer.from(error, "utf8");
615
+ if (bytes.length <= MAX_ERROR_BYTES) return error;
616
+ return `${bytes
617
+ .subarray(0, Math.max(0, MAX_ERROR_BYTES - 18))
618
+ .toString("utf8")
619
+ .replace(/�+$/gu, "")}\n… [truncated]`;
620
+ }
621
+
622
+ function isRecord(value: unknown): value is Record<string, unknown> {
623
+ return typeof value === "object" && value !== null && !Array.isArray(value);
624
+ }
625
+
626
+ function isLoopbackAddress(address: AddressInfo | string | null): address is AddressInfo {
627
+ return Boolean(address && typeof address !== "string" && address.address === BROKER_HOST);
628
+ }