macro-agent 0.0.17 → 0.1.1

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 (338) hide show
  1. package/.claude/settings.local.json +3 -1
  2. package/.sudocode/specs.jsonl +4 -0
  3. package/CLAUDE.md +16 -14
  4. package/README.md +11 -29
  5. package/dist/acp/macro-agent.d.ts +17 -0
  6. package/dist/acp/macro-agent.d.ts.map +1 -1
  7. package/dist/acp/macro-agent.js +183 -55
  8. package/dist/acp/macro-agent.js.map +1 -1
  9. package/dist/acp/types.d.ts +32 -1
  10. package/dist/acp/types.d.ts.map +1 -1
  11. package/dist/acp/types.js.map +1 -1
  12. package/dist/agent/agent-manager.d.ts +65 -1
  13. package/dist/agent/agent-manager.d.ts.map +1 -1
  14. package/dist/agent/agent-manager.js +464 -183
  15. package/dist/agent/agent-manager.js.map +1 -1
  16. package/dist/agent/types.d.ts +1 -1
  17. package/dist/agent/types.d.ts.map +1 -1
  18. package/dist/api/server.d.ts +3 -0
  19. package/dist/api/server.d.ts.map +1 -1
  20. package/dist/api/server.js +37 -6
  21. package/dist/api/server.js.map +1 -1
  22. package/dist/auth/index.d.ts +2 -0
  23. package/dist/auth/index.d.ts.map +1 -0
  24. package/dist/auth/index.js +2 -0
  25. package/dist/auth/index.js.map +1 -0
  26. package/dist/auth/token.d.ts +41 -0
  27. package/dist/auth/token.d.ts.map +1 -0
  28. package/dist/auth/token.js +73 -0
  29. package/dist/auth/token.js.map +1 -0
  30. package/dist/cli/acp.d.ts +2 -23
  31. package/dist/cli/acp.d.ts.map +1 -1
  32. package/dist/cli/acp.js +127 -61
  33. package/dist/cli/acp.js.map +1 -1
  34. package/dist/cli/index.js +147 -15
  35. package/dist/cli/index.js.map +1 -1
  36. package/dist/cli/mcp.d.ts +6 -0
  37. package/dist/cli/mcp.d.ts.map +1 -1
  38. package/dist/cli/mcp.js +268 -181
  39. package/dist/cli/mcp.js.map +1 -1
  40. package/dist/cli/parse-args.d.ts +20 -0
  41. package/dist/cli/parse-args.d.ts.map +1 -0
  42. package/dist/cli/parse-args.js +43 -0
  43. package/dist/cli/parse-args.js.map +1 -0
  44. package/dist/cli/stable-instance-id.d.ts +8 -0
  45. package/dist/cli/stable-instance-id.d.ts.map +1 -0
  46. package/dist/cli/stable-instance-id.js +14 -0
  47. package/dist/cli/stable-instance-id.js.map +1 -0
  48. package/dist/config/project-config.d.ts +74 -7
  49. package/dist/config/project-config.d.ts.map +1 -1
  50. package/dist/config/project-config.js +123 -20
  51. package/dist/config/project-config.js.map +1 -1
  52. package/dist/map/adapter/acp-over-map.d.ts +23 -0
  53. package/dist/map/adapter/acp-over-map.d.ts.map +1 -1
  54. package/dist/map/adapter/acp-over-map.js +482 -55
  55. package/dist/map/adapter/acp-over-map.js.map +1 -1
  56. package/dist/map/adapter/connection-manager.d.ts.map +1 -1
  57. package/dist/map/adapter/connection-manager.js +3 -0
  58. package/dist/map/adapter/connection-manager.js.map +1 -1
  59. package/dist/map/adapter/event-log.d.ts +87 -0
  60. package/dist/map/adapter/event-log.d.ts.map +1 -0
  61. package/dist/map/adapter/event-log.js +122 -0
  62. package/dist/map/adapter/event-log.js.map +1 -0
  63. package/dist/map/adapter/event-translator.js +6 -6
  64. package/dist/map/adapter/event-translator.js.map +1 -1
  65. package/dist/map/adapter/extensions/agent-lifecycle.d.ts +82 -0
  66. package/dist/map/adapter/extensions/agent-lifecycle.d.ts.map +1 -0
  67. package/dist/map/adapter/extensions/agent-lifecycle.js +164 -0
  68. package/dist/map/adapter/extensions/agent-lifecycle.js.map +1 -0
  69. package/dist/map/adapter/extensions/index.d.ts +10 -1
  70. package/dist/map/adapter/extensions/index.d.ts.map +1 -1
  71. package/dist/map/adapter/extensions/index.js +34 -0
  72. package/dist/map/adapter/extensions/index.js.map +1 -1
  73. package/dist/map/adapter/extensions/mcp-bridge.d.ts +57 -0
  74. package/dist/map/adapter/extensions/mcp-bridge.d.ts.map +1 -0
  75. package/dist/map/adapter/extensions/mcp-bridge.js +745 -0
  76. package/dist/map/adapter/extensions/mcp-bridge.js.map +1 -0
  77. package/dist/map/adapter/extensions/rename.d.ts +29 -0
  78. package/dist/map/adapter/extensions/rename.d.ts.map +1 -0
  79. package/dist/map/adapter/extensions/rename.js +49 -0
  80. package/dist/map/adapter/extensions/rename.js.map +1 -0
  81. package/dist/map/adapter/extensions/task.d.ts.map +1 -1
  82. package/dist/map/adapter/extensions/task.js +10 -0
  83. package/dist/map/adapter/extensions/task.js.map +1 -1
  84. package/dist/map/adapter/extensions/update-metadata.d.ts +29 -0
  85. package/dist/map/adapter/extensions/update-metadata.d.ts.map +1 -0
  86. package/dist/map/adapter/extensions/update-metadata.js +67 -0
  87. package/dist/map/adapter/extensions/update-metadata.js.map +1 -0
  88. package/dist/map/adapter/index.d.ts +2 -1
  89. package/dist/map/adapter/index.d.ts.map +1 -1
  90. package/dist/map/adapter/index.js +8 -2
  91. package/dist/map/adapter/index.js.map +1 -1
  92. package/dist/map/adapter/interface.d.ts +2 -0
  93. package/dist/map/adapter/interface.d.ts.map +1 -1
  94. package/dist/map/adapter/map-adapter.d.ts +4 -0
  95. package/dist/map/adapter/map-adapter.d.ts.map +1 -1
  96. package/dist/map/adapter/map-adapter.js +302 -30
  97. package/dist/map/adapter/map-adapter.js.map +1 -1
  98. package/dist/map/adapter/subscription-manager.d.ts.map +1 -1
  99. package/dist/map/adapter/subscription-manager.js +5 -1
  100. package/dist/map/adapter/subscription-manager.js.map +1 -1
  101. package/dist/map/adapter/types.d.ts +2 -0
  102. package/dist/map/adapter/types.d.ts.map +1 -1
  103. package/dist/mcp/map-client.d.ts +39 -0
  104. package/dist/mcp/map-client.d.ts.map +1 -0
  105. package/dist/mcp/map-client.js +129 -0
  106. package/dist/mcp/map-client.js.map +1 -0
  107. package/dist/mcp/mcp-server.d.ts +14 -0
  108. package/dist/mcp/mcp-server.d.ts.map +1 -1
  109. package/dist/mcp/mcp-server.js +113 -85
  110. package/dist/mcp/mcp-server.js.map +1 -1
  111. package/dist/mcp/types.d.ts +9 -1
  112. package/dist/mcp/types.d.ts.map +1 -1
  113. package/dist/mcp/types.js.map +1 -1
  114. package/dist/metrics/metrics.js +1 -1
  115. package/dist/metrics/metrics.js.map +1 -1
  116. package/dist/roles/capabilities.d.ts +3 -1
  117. package/dist/roles/capabilities.d.ts.map +1 -1
  118. package/dist/roles/capabilities.js +17 -7
  119. package/dist/roles/capabilities.js.map +1 -1
  120. package/dist/roles/config-loader.d.ts +6 -6
  121. package/dist/roles/config-loader.d.ts.map +1 -1
  122. package/dist/roles/config-loader.js +6 -6
  123. package/dist/roles/config-loader.js.map +1 -1
  124. package/dist/roles/registry.d.ts +2 -2
  125. package/dist/roles/registry.js +2 -2
  126. package/dist/server/combined-server.d.ts +20 -0
  127. package/dist/server/combined-server.d.ts.map +1 -1
  128. package/dist/server/combined-server.js +107 -8
  129. package/dist/server/combined-server.js.map +1 -1
  130. package/dist/store/event-store.d.ts +7 -1
  131. package/dist/store/event-store.d.ts.map +1 -1
  132. package/dist/store/event-store.js +91 -8
  133. package/dist/store/event-store.js.map +1 -1
  134. package/dist/store/types/agents.d.ts +23 -0
  135. package/dist/store/types/agents.d.ts.map +1 -1
  136. package/dist/store/types/events.d.ts +1 -1
  137. package/dist/store/types/events.d.ts.map +1 -1
  138. package/dist/task/backend/index.d.ts +47 -29
  139. package/dist/task/backend/index.d.ts.map +1 -1
  140. package/dist/task/backend/index.js +109 -71
  141. package/dist/task/backend/index.js.map +1 -1
  142. package/dist/task/backend/memory.d.ts +1 -0
  143. package/dist/task/backend/memory.d.ts.map +1 -1
  144. package/dist/task/backend/memory.js +3 -0
  145. package/dist/task/backend/memory.js.map +1 -1
  146. package/dist/task/backend/opentasks/backend.d.ts +140 -0
  147. package/dist/task/backend/opentasks/backend.d.ts.map +1 -0
  148. package/dist/task/backend/opentasks/backend.js +1023 -0
  149. package/dist/task/backend/opentasks/backend.js.map +1 -0
  150. package/dist/task/backend/opentasks/client.d.ts +337 -0
  151. package/dist/task/backend/opentasks/client.d.ts.map +1 -0
  152. package/dist/task/backend/opentasks/client.js +225 -0
  153. package/dist/task/backend/opentasks/client.js.map +1 -0
  154. package/dist/task/backend/opentasks/daemon-manager.d.ts +89 -0
  155. package/dist/task/backend/opentasks/daemon-manager.d.ts.map +1 -0
  156. package/dist/task/backend/opentasks/daemon-manager.js +195 -0
  157. package/dist/task/backend/opentasks/daemon-manager.js.map +1 -0
  158. package/dist/task/backend/opentasks/index.d.ts +21 -0
  159. package/dist/task/backend/opentasks/index.d.ts.map +1 -0
  160. package/dist/task/backend/opentasks/index.js +21 -0
  161. package/dist/task/backend/opentasks/index.js.map +1 -0
  162. package/dist/task/backend/opentasks/mapping.d.ts +48 -0
  163. package/dist/task/backend/opentasks/mapping.d.ts.map +1 -0
  164. package/dist/task/backend/opentasks/mapping.js +77 -0
  165. package/dist/task/backend/opentasks/mapping.js.map +1 -0
  166. package/dist/task/backend/types.d.ts +33 -53
  167. package/dist/task/backend/types.d.ts.map +1 -1
  168. package/dist/task/backend/types.js +7 -11
  169. package/dist/task/backend/types.js.map +1 -1
  170. package/dist/task/backend/unified-tool-provider.d.ts +57 -0
  171. package/dist/task/backend/unified-tool-provider.d.ts.map +1 -0
  172. package/dist/task/backend/unified-tool-provider.js +623 -0
  173. package/dist/task/backend/unified-tool-provider.js.map +1 -0
  174. package/dist/teams/team-loader.d.ts +2 -2
  175. package/dist/teams/team-loader.js +3 -3
  176. package/dist/teams/team-loader.js.map +1 -1
  177. package/dist/teams/team-runtime.d.ts.map +1 -1
  178. package/dist/teams/team-runtime.js +2 -0
  179. package/dist/teams/team-runtime.js.map +1 -1
  180. package/docs/architecture.md +7 -6
  181. package/docs/configuration.md +26 -62
  182. package/docs/implementation-details.md +5 -5
  183. package/docs/implementation-summary.md +17 -17
  184. package/docs/plan-self-driving-support.md +4 -4
  185. package/docs/spec-self-driving-support.md +10 -10
  186. package/docs/team-templates.md +2 -2
  187. package/docs/teams.md +3 -3
  188. package/docs/troubleshooting.md +10 -11
  189. package/package.json +6 -4
  190. package/src/__tests__/e2e/agent-spawn-visibility.e2e.test.ts +761 -0
  191. package/src/__tests__/e2e/full-agent-conflict-resolution.e2e.test.ts +2 -2
  192. package/src/__tests__/e2e/mcp-thin-client-bridge.e2e.test.ts +304 -0
  193. package/src/__tests__/e2e/mcp-tools-available.e2e.test.ts +324 -0
  194. package/src/__tests__/e2e/multi-agent.e2e.test.ts +5 -5
  195. package/src/__tests__/e2e/spawn-session-streaming.e2e.test.ts +563 -0
  196. package/src/acp/__tests__/history.test.ts +8 -4
  197. package/src/acp/__tests__/integration.test.ts +56 -31
  198. package/src/acp/__tests__/macro-agent.test.ts +16 -7
  199. package/src/acp/macro-agent.ts +230 -62
  200. package/src/acp/types.ts +46 -1
  201. package/src/agent/__tests__/agent-manager.test.ts +228 -2
  202. package/src/agent/agent-manager.ts +714 -261
  203. package/src/agent/types.ts +3 -1
  204. package/src/api/server.ts +41 -7
  205. package/src/auth/__tests__/token.test.ts +100 -0
  206. package/src/auth/index.ts +1 -0
  207. package/src/auth/token.ts +82 -0
  208. package/src/cli/__tests__/acp.test.ts +1 -1
  209. package/src/cli/__tests__/stable-instance-id.test.ts +1 -1
  210. package/src/cli/acp.ts +130 -72
  211. package/src/cli/index.ts +120 -14
  212. package/src/cli/mcp.ts +311 -207
  213. package/src/cli/parse-args.ts +54 -0
  214. package/src/cli/stable-instance-id.ts +14 -0
  215. package/src/config/project-config.ts +190 -27
  216. package/src/lifecycle/__tests__/cascade-termination.test.ts +1 -1
  217. package/src/map/adapter/__tests__/acp-over-map-cancel.test.ts +820 -0
  218. package/src/map/adapter/__tests__/acp-over-map-getmodels.test.ts +355 -0
  219. package/src/map/adapter/__tests__/acp-over-map-history.test.ts +724 -2
  220. package/src/map/adapter/__tests__/acp-over-map-persistence.e2e.test.ts +1 -1
  221. package/src/map/adapter/__tests__/event-broadcast.test.ts +420 -0
  222. package/src/map/adapter/__tests__/event-log.test.ts +527 -0
  223. package/src/map/adapter/__tests__/event-translator.test.ts +3 -3
  224. package/src/map/adapter/__tests__/extensions.test.ts +408 -0
  225. package/src/map/adapter/__tests__/map-adapter.test.ts +99 -0
  226. package/src/map/adapter/__tests__/mcp-bridge.test.ts +1187 -0
  227. package/src/map/adapter/__tests__/multi-client-broadcast.test.ts +711 -0
  228. package/src/map/adapter/__tests__/websocket-integration.test.ts +218 -0
  229. package/src/map/adapter/acp-over-map.ts +777 -92
  230. package/src/map/adapter/connection-manager.ts +3 -0
  231. package/src/map/adapter/event-log.ts +208 -0
  232. package/src/map/adapter/event-translator.ts +6 -6
  233. package/src/map/adapter/extensions/agent-lifecycle.ts +267 -0
  234. package/src/map/adapter/extensions/index.ts +60 -0
  235. package/src/map/adapter/extensions/mcp-bridge.ts +995 -0
  236. package/src/map/adapter/extensions/task.ts +11 -0
  237. package/src/map/adapter/extensions/update-metadata.ts +126 -0
  238. package/src/map/adapter/index.ts +28 -0
  239. package/src/map/adapter/interface.ts +2 -0
  240. package/src/map/adapter/map-adapter.ts +373 -38
  241. package/src/map/adapter/subscription-manager.ts +5 -1
  242. package/src/map/adapter/types.ts +2 -0
  243. package/src/mcp/__tests__/map-client.test.ts +386 -0
  244. package/src/mcp/__tests__/mcp-server-thin-client.test.ts +368 -0
  245. package/src/mcp/__tests__/mcp-server.test.ts +100 -1
  246. package/src/mcp/map-client.ts +177 -0
  247. package/src/mcp/mcp-server.ts +191 -100
  248. package/src/mcp/types.ts +6 -1
  249. package/src/metrics/metrics.ts +1 -1
  250. package/src/monitor/__tests__/stale-agent-flow.integration.test.ts +1 -1
  251. package/src/roles/__tests__/config-loader.test.ts +7 -7
  252. package/src/roles/capabilities.ts +17 -7
  253. package/src/roles/config-loader.ts +6 -6
  254. package/src/roles/registry.ts +2 -2
  255. package/src/server/__tests__/combined-server.test.ts +94 -21
  256. package/src/server/combined-server.ts +189 -33
  257. package/src/steering/__tests__/steering-integration.test.ts +1 -1
  258. package/src/store/__tests__/event-store.test.ts +236 -1
  259. package/src/store/__tests__/instance.test.ts +3 -3
  260. package/src/store/event-store.ts +109 -8
  261. package/src/store/types/agents.ts +16 -0
  262. package/src/store/types/events.ts +1 -1
  263. package/src/task/backend/__tests__/create-task-backend.test.ts +225 -0
  264. package/src/task/backend/__tests__/e2e/unified-tool-provider-opentasks.e2e.test.ts +524 -0
  265. package/src/task/backend/__tests__/unified-tool-provider.test.ts +579 -0
  266. package/src/task/backend/index.ts +156 -106
  267. package/src/task/backend/memory.ts +4 -0
  268. package/src/task/backend/opentasks/__tests__/backend.test.ts +968 -0
  269. package/src/task/backend/opentasks/__tests__/daemon-manager.test.ts +406 -0
  270. package/src/task/backend/opentasks/__tests__/mapping.test.ts +84 -0
  271. package/src/task/backend/opentasks/__tests__/opentasks-backend.e2e.test.ts +1338 -0
  272. package/src/task/backend/opentasks/backend.ts +1323 -0
  273. package/src/task/backend/opentasks/client.ts +652 -0
  274. package/src/task/backend/opentasks/daemon-manager.ts +253 -0
  275. package/src/task/backend/opentasks/index.ts +69 -0
  276. package/src/task/backend/opentasks/mapping.ts +94 -0
  277. package/src/task/backend/types.ts +42 -66
  278. package/src/task/backend/unified-tool-provider.ts +779 -0
  279. package/src/teams/__tests__/cross-subsystem.integration.test.ts +1 -1
  280. package/src/teams/team-loader.ts +3 -3
  281. package/src/teams/team-runtime.ts +2 -0
  282. package/test_fixtures/README.md +2 -3
  283. package/test_fixtures/fixtures/index.ts +0 -3
  284. package/test_fixtures/fixtures/projects/project-with-specs.ts +7 -149
  285. package/test_fixtures/fixtures/repos/index.ts +1 -3
  286. package/test_fixtures/fixtures/repos/temp-repo-factory.ts +0 -116
  287. package/test_fixtures/fixtures/repos/types.ts +0 -11
  288. package/test_fixtures/harness/__tests__/fixtures.test.ts +10 -102
  289. package/test_fixtures/harness/__tests__/temp-repo-and-simulator.test.ts +0 -33
  290. package/test_fixtures/harness/simulator/agent-simulator.ts +4 -4
  291. package/vitest.config.ts +1 -1
  292. package/vitest.e2e.config.ts +1 -1
  293. package/vitest.setup.ts +1 -30
  294. package/.macro-agent/teams/self-driving/prompts/grinder.md +0 -27
  295. package/.macro-agent/teams/self-driving/prompts/judge.md +0 -27
  296. package/.macro-agent/teams/self-driving/prompts/planner.md +0 -33
  297. package/.macro-agent/teams/self-driving/roles/grinder.yaml +0 -17
  298. package/.macro-agent/teams/self-driving/roles/judge.yaml +0 -24
  299. package/.macro-agent/teams/self-driving/roles/planner.yaml +0 -18
  300. package/.macro-agent/teams/self-driving/team.yaml +0 -103
  301. package/.macro-agent/teams/structured/prompts/developer.md +0 -26
  302. package/.macro-agent/teams/structured/prompts/lead.md +0 -25
  303. package/.macro-agent/teams/structured/prompts/reviewer.md +0 -24
  304. package/.macro-agent/teams/structured/roles/developer.yaml +0 -12
  305. package/.macro-agent/teams/structured/roles/lead.yaml +0 -11
  306. package/.macro-agent/teams/structured/roles/reviewer.yaml +0 -19
  307. package/.macro-agent/teams/structured/team.yaml +0 -89
  308. package/docs/sudocode-integration.md +0 -383
  309. package/src/task/backend/__tests__/backend-parity.test.ts +0 -451
  310. package/src/task/backend/__tests__/tool-provider-edge-cases.test.ts +0 -430
  311. package/src/task/backend/__tests__/tool-provider.test.ts +0 -983
  312. package/src/task/backend/sudocode/__tests__/backend-edge-cases.test.ts +0 -575
  313. package/src/task/backend/sudocode/__tests__/backend.test.ts +0 -1194
  314. package/src/task/backend/sudocode/__tests__/client-integration.test.ts +0 -418
  315. package/src/task/backend/sudocode/__tests__/client.test.ts +0 -345
  316. package/src/task/backend/sudocode/__tests__/e2e/backend.e2e.test.ts +0 -753
  317. package/src/task/backend/sudocode/__tests__/e2e/server-client.e2e.test.ts +0 -680
  318. package/src/task/backend/sudocode/__tests__/e2e-workflow.test.ts +0 -666
  319. package/src/task/backend/sudocode/__tests__/integration/standalone-client.integration.test.ts +0 -396
  320. package/src/task/backend/sudocode/__tests__/integration/sudocode-cli.integration.test.ts +0 -328
  321. package/src/task/backend/sudocode/__tests__/integration/test-utils.ts +0 -175
  322. package/src/task/backend/sudocode/__tests__/mapping-edge-cases.test.ts +0 -265
  323. package/src/task/backend/sudocode/__tests__/server-client.test.ts +0 -675
  324. package/src/task/backend/sudocode/__tests__/sync-policy-edge-cases.test.ts +0 -521
  325. package/src/task/backend/sudocode/__tests__/sync-policy.test.ts +0 -519
  326. package/src/task/backend/sudocode/__tests__/tools.test.ts +0 -471
  327. package/src/task/backend/sudocode/backend.ts +0 -1237
  328. package/src/task/backend/sudocode/client.ts +0 -515
  329. package/src/task/backend/sudocode/index.ts +0 -120
  330. package/src/task/backend/sudocode/mapping.ts +0 -93
  331. package/src/task/backend/sudocode/server-client.ts +0 -522
  332. package/src/task/backend/sudocode/standalone-client.ts +0 -623
  333. package/src/task/backend/sudocode/sync-policy.ts +0 -387
  334. package/src/task/backend/sudocode/tools.ts +0 -896
  335. package/src/task/backend/tool-provider.ts +0 -506
  336. package/test_fixtures/fixtures/sudocode/index.ts +0 -29
  337. package/test_fixtures/fixtures/sudocode/issues.ts +0 -185
  338. package/test_fixtures/fixtures/sudocode/specs.ts +0 -159
@@ -0,0 +1,563 @@
1
+ /**
2
+ * E2E test: Spawned Agent Session Streaming
3
+ *
4
+ * Verifies that when an agent is spawned via the MCP bridge (`_macro/mcp/spawn_agent`),
5
+ * the fire-and-forget prompt emits MAP events that TUI clients can subscribe to:
6
+ * - session_user_message (before prompt starts)
7
+ * - session_update (for each streaming update during the prompt)
8
+ * - session_prompt_done (after the prompt completes)
9
+ *
10
+ * Also verifies that turns are recorded in EventStore for history persistence.
11
+ *
12
+ * REQUIRES: RUN_FULL_AGENT_TESTS=true environment variable (and authenticated Claude Code)
13
+ *
14
+ * Run with:
15
+ * RUN_FULL_AGENT_TESTS=true npx vitest run src/__tests__/e2e/spawn-session-streaming.e2e.test.ts
16
+ */
17
+
18
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
19
+ import { WebSocket } from "ws";
20
+ import * as fs from "fs";
21
+ import * as path from "path";
22
+ import * as os from "os";
23
+ import { createEventStore, type EventStore } from "../../store/event-store.js";
24
+ import {
25
+ createAgentManager,
26
+ type AgentManager,
27
+ } from "../../agent/agent-manager.js";
28
+ import { createTaskManager, type TaskManager } from "../../task/task-manager.js";
29
+ import {
30
+ createMessageRouter,
31
+ type MessageRouter,
32
+ } from "../../router/message-router.js";
33
+ import {
34
+ createCombinedServer,
35
+ type CombinedServer,
36
+ type CombinedServerServices,
37
+ } from "../../server/combined-server.js";
38
+ import { mapCall } from "../../mcp/map-client.js";
39
+
40
+ // =============================================================================
41
+ // Test Configuration
42
+ // =============================================================================
43
+
44
+ const RUN_FULL_AGENT = !!process.env.RUN_FULL_AGENT_TESTS;
45
+ const testFn = RUN_FULL_AGENT ? it : it.skip;
46
+
47
+ const TIMEOUT = {
48
+ SPAWN_AND_PROMPT: 120_000, // Agent spawn + prompt completion
49
+ EVENT_WAIT: 60_000, // Waiting for individual MAP events
50
+ };
51
+
52
+ const log = (msg: string) => {
53
+ if (RUN_FULL_AGENT) {
54
+ console.log(`[SpawnStreamE2E] ${msg}`);
55
+ }
56
+ };
57
+
58
+ // No event type filter needed — the TUI worker subscribes to ALL events
59
+ // by calling client.subscribe() without a filter. The e2e test mirrors this.
60
+
61
+ // =============================================================================
62
+ // Helpers
63
+ // =============================================================================
64
+
65
+ function getRandomPort(): number {
66
+ return 10000 + Math.floor(Math.random() * 50000);
67
+ }
68
+
69
+ interface JsonRpcMessage {
70
+ jsonrpc: "2.0";
71
+ id?: number;
72
+ method?: string;
73
+ params?: unknown;
74
+ result?: unknown;
75
+ error?: { code: number; message: string; data?: unknown };
76
+ }
77
+
78
+ /**
79
+ * WebSocket MAP client for subscribing to events.
80
+ */
81
+ class MAPTestClient {
82
+ private ws!: WebSocket;
83
+ private waiters: Map<
84
+ number,
85
+ { resolve: (r: JsonRpcMessage) => void; reject: (e: Error) => void }
86
+ > = new Map();
87
+ private nextId = 1;
88
+ private url: string;
89
+
90
+ readonly notifications: JsonRpcMessage[] = [];
91
+
92
+ private notificationWaiters: Array<{
93
+ check: (msg: JsonRpcMessage) => boolean;
94
+ resolve: (msg: JsonRpcMessage) => void;
95
+ reject: (e: Error) => void;
96
+ timeout: ReturnType<typeof setTimeout>;
97
+ }> = [];
98
+
99
+ constructor(url: string) {
100
+ this.url = url;
101
+ }
102
+
103
+ async connect(): Promise<void> {
104
+ this.ws = new WebSocket(this.url);
105
+ return new Promise((resolve, reject) => {
106
+ const timeout = setTimeout(
107
+ () => reject(new Error("Connection timeout")),
108
+ 5000,
109
+ );
110
+ this.ws.on("open", () => {
111
+ clearTimeout(timeout);
112
+ resolve();
113
+ });
114
+ this.ws.on("error", (err) => {
115
+ clearTimeout(timeout);
116
+ reject(err);
117
+ });
118
+ this.ws.on("message", (data: Buffer) => {
119
+ try {
120
+ const msg = JSON.parse(data.toString()) as JsonRpcMessage;
121
+ if (msg.id != null) {
122
+ const waiter = this.waiters.get(msg.id);
123
+ if (waiter) {
124
+ this.waiters.delete(msg.id);
125
+ waiter.resolve(msg);
126
+ }
127
+ } else if (msg.method) {
128
+ this.notifications.push(msg);
129
+ for (let i = this.notificationWaiters.length - 1; i >= 0; i--) {
130
+ const w = this.notificationWaiters[i];
131
+ if (w.check(msg)) {
132
+ clearTimeout(w.timeout);
133
+ this.notificationWaiters.splice(i, 1);
134
+ w.resolve(msg);
135
+ }
136
+ }
137
+ }
138
+ } catch {
139
+ // ignore
140
+ }
141
+ });
142
+ });
143
+ }
144
+
145
+ async request(method: string, params?: unknown): Promise<JsonRpcMessage> {
146
+ const id = this.nextId++;
147
+ return new Promise((resolve, reject) => {
148
+ const timeout = setTimeout(() => {
149
+ this.waiters.delete(id);
150
+ reject(new Error(`Request timeout: ${method}`));
151
+ }, 30000);
152
+ this.waiters.set(id, {
153
+ resolve: (r) => {
154
+ clearTimeout(timeout);
155
+ resolve(r);
156
+ },
157
+ reject: (e) => {
158
+ clearTimeout(timeout);
159
+ reject(e);
160
+ },
161
+ });
162
+ this.ws.send(JSON.stringify({ jsonrpc: "2.0", method, params, id }));
163
+ });
164
+ }
165
+
166
+ waitForNotification(
167
+ check: (msg: JsonRpcMessage) => boolean,
168
+ timeoutMs = 15000,
169
+ ): Promise<JsonRpcMessage> {
170
+ const existing = this.notifications.find(check);
171
+ if (existing) return Promise.resolve(existing);
172
+
173
+ return new Promise((resolve, reject) => {
174
+ const timeout = setTimeout(() => {
175
+ const idx = this.notificationWaiters.findIndex(
176
+ (w) => w.resolve === resolve,
177
+ );
178
+ if (idx >= 0) this.notificationWaiters.splice(idx, 1);
179
+ reject(
180
+ new Error(
181
+ `Timeout waiting for notification. Received ${this.notifications.length} notifications:\n` +
182
+ this.notifications
183
+ .map((n) => {
184
+ const p = n.params as Record<string, unknown> | undefined;
185
+ const evt = p?.event as Record<string, unknown> | undefined;
186
+ return ` ${n.method} → type=${evt?.type}, agentId=${(evt?.data as Record<string, unknown> | undefined)?.agentId}`;
187
+ })
188
+ .join("\n"),
189
+ ),
190
+ );
191
+ }, timeoutMs);
192
+ this.notificationWaiters.push({ check, resolve, reject, timeout });
193
+ });
194
+ }
195
+
196
+ /**
197
+ * Collect all notifications matching a predicate that have arrived so far.
198
+ */
199
+ collectNotifications(check: (msg: JsonRpcMessage) => boolean): JsonRpcMessage[] {
200
+ return this.notifications.filter(check);
201
+ }
202
+
203
+ close(): void {
204
+ for (const w of this.notificationWaiters) {
205
+ clearTimeout(w.timeout);
206
+ }
207
+ this.notificationWaiters.length = 0;
208
+ try {
209
+ this.ws?.close();
210
+ } catch {
211
+ // ignore
212
+ }
213
+ }
214
+ }
215
+
216
+ function isEventOfType(type: string) {
217
+ return (msg: JsonRpcMessage) => {
218
+ if (msg.method !== "map/event") return false;
219
+ const params = msg.params as Record<string, unknown> | undefined;
220
+ const event = params?.event as Record<string, unknown> | undefined;
221
+ return event?.type === type;
222
+ };
223
+ }
224
+
225
+ function isEventForAgent(type: string, agentId: string) {
226
+ return (msg: JsonRpcMessage) => {
227
+ if (msg.method !== "map/event") return false;
228
+ const params = msg.params as Record<string, unknown> | undefined;
229
+ const event = params?.event as Record<string, unknown> | undefined;
230
+ if (event?.type !== type) return false;
231
+ const data = event?.data as Record<string, unknown> | undefined;
232
+ return data?.agentId === agentId;
233
+ };
234
+ }
235
+
236
+ function getEventData(msg: JsonRpcMessage): Record<string, unknown> {
237
+ const params = msg.params as Record<string, unknown>;
238
+ const event = params.event as Record<string, unknown>;
239
+ return event.data as Record<string, unknown>;
240
+ }
241
+
242
+ // =============================================================================
243
+ // Tests
244
+ // =============================================================================
245
+
246
+ describe("Spawned Agent Session Streaming E2E", () => {
247
+ let eventStore: EventStore;
248
+ let agentManager: AgentManager;
249
+ let taskManager: TaskManager;
250
+ let messageRouter: MessageRouter;
251
+ let server: CombinedServer;
252
+ let port: number;
253
+ let serverUrl: string;
254
+ let tmpDir: string;
255
+ const clients: MAPTestClient[] = [];
256
+
257
+ beforeEach(async () => {
258
+ if (!RUN_FULL_AGENT) return;
259
+
260
+ port = getRandomPort();
261
+ serverUrl = `http://localhost:${port}`;
262
+
263
+ // File-based EventStore so the Claude Code subprocess can access the same DB
264
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "spawn-stream-e2e-"));
265
+ const instanceId = `test-spawn-stream-${Date.now()}`;
266
+ log(`EventStore baseDir: ${tmpDir}, instanceId: ${instanceId}`);
267
+
268
+ eventStore = await createEventStore({ instanceId, baseDir: tmpDir });
269
+ messageRouter = createMessageRouter(eventStore);
270
+ agentManager = createAgentManager(eventStore, messageRouter, {
271
+ defaultPermissionMode: "auto-approve",
272
+ defaultCwd: process.cwd(),
273
+ });
274
+ taskManager = createTaskManager(eventStore);
275
+
276
+ const services: CombinedServerServices = {
277
+ eventStore,
278
+ agentManager,
279
+ taskManager,
280
+ messageRouter,
281
+ };
282
+
283
+ server = createCombinedServer(services, { port, host: "localhost" });
284
+ await server.start();
285
+ log(`Server started on port ${port}`);
286
+ });
287
+
288
+ afterEach(async () => {
289
+ if (!RUN_FULL_AGENT) return;
290
+
291
+ for (const c of clients) {
292
+ c.close();
293
+ }
294
+ clients.length = 0;
295
+
296
+ // Terminate any running agents
297
+ try {
298
+ for (const agent of agentManager.list()) {
299
+ if (agent.state === "running") {
300
+ try {
301
+ await agentManager.terminate(agent.id, "test_cleanup");
302
+ } catch { /* ignore */ }
303
+ }
304
+ }
305
+ } catch { /* ignore */ }
306
+
307
+ await server.stop().catch(() => {});
308
+ await agentManager.close();
309
+ await eventStore.close();
310
+
311
+ // Clean up temp dir
312
+ try {
313
+ fs.rmSync(tmpDir, { recursive: true, force: true });
314
+ } catch { /* ignore */ }
315
+
316
+ log("Cleanup complete");
317
+ });
318
+
319
+ function createClient(): MAPTestClient {
320
+ const client = new MAPTestClient(`ws://localhost:${port}/map`);
321
+ clients.push(client);
322
+ return client;
323
+ }
324
+
325
+ // ─────────────────────────────────────────────────────────────────
326
+ // Core: MAP event streaming during spawned agent prompt
327
+ // ─────────────────────────────────────────────────────────────────
328
+
329
+ testFn(
330
+ "subscriber receives session_user_message, session_update, and session_prompt_done when agent is spawned",
331
+ { timeout: TIMEOUT.SPAWN_AND_PROMPT },
332
+ async () => {
333
+ // Step 1: Subscribe to ALL events (no filter) — same as the real TUI worker
334
+ const subscriber = createClient();
335
+ await subscriber.connect();
336
+ const subRes = await subscriber.request("map/subscribe", {});
337
+ expect(subRes.error).toBeUndefined();
338
+ log("Subscriber connected and subscribed (no filter)");
339
+
340
+ // Step 2: Seed a root agent (the "caller" that spawns children)
341
+ eventStore.emit({
342
+ type: "spawn",
343
+ source: { agent_id: "system" },
344
+ payload: {
345
+ agent_id: "agent_caller",
346
+ session_id: "sess_caller",
347
+ task: "Root caller agent",
348
+ task_id: "task_caller",
349
+ parent: null,
350
+ config: {},
351
+ cwd: process.cwd(),
352
+ },
353
+ });
354
+ log("Root agent seeded");
355
+
356
+ // Step 3: Spawn a real agent via the MCP bridge
357
+ // Give it a simple task so it completes quickly
358
+ let spawnResult: { agent_id: string; session_id: string; task_id: string };
359
+ try {
360
+ spawnResult = await mapCall<{
361
+ agent_id: string;
362
+ session_id: string;
363
+ task_id: string;
364
+ }>(serverUrl, "_macro/mcp/spawn_agent", {
365
+ context: {
366
+ agent_id: "agent_caller",
367
+ session_id: "sess_caller",
368
+ task_id: "task_caller",
369
+ lineage: [],
370
+ cwd: process.cwd(),
371
+ },
372
+ task: "Say exactly: 'Hello from spawned agent'. Do not use any tools. Just respond with that text.",
373
+ });
374
+ log(`Agent spawned: ${spawnResult.agent_id} (session: ${spawnResult.session_id})`);
375
+ } catch (err) {
376
+ throw new Error(`spawn_agent failed: ${(err as Error).message}`);
377
+ }
378
+
379
+ const agentId = spawnResult.agent_id;
380
+
381
+ // Step 4: Wait for session_user_message
382
+ log("Waiting for session_user_message...");
383
+ const userMsg = await subscriber.waitForNotification(
384
+ isEventForAgent("session_user_message", agentId),
385
+ TIMEOUT.EVENT_WAIT,
386
+ );
387
+ const userMsgData = getEventData(userMsg);
388
+ log(`Got session_user_message: content=${JSON.stringify(userMsgData.content).slice(0, 80)}...`);
389
+
390
+ expect(userMsgData.agentId).toBe(agentId);
391
+ expect(userMsgData.sessionId).toBe(spawnResult.session_id);
392
+ expect(userMsgData.content).toContain("Hello from spawned agent");
393
+
394
+ // Step 5: Wait for session_prompt_done (agent should finish quickly)
395
+ log("Waiting for session_prompt_done...");
396
+ const promptDone = await subscriber.waitForNotification(
397
+ isEventForAgent("session_prompt_done", agentId),
398
+ TIMEOUT.EVENT_WAIT,
399
+ );
400
+ const promptDoneData = getEventData(promptDone);
401
+ log(`Got session_prompt_done: stopReason=${promptDoneData.stopReason}`);
402
+
403
+ expect(promptDoneData.agentId).toBe(agentId);
404
+ expect(promptDoneData.stopReason).toBe("end_turn");
405
+
406
+ // Step 6: Verify session_update events were received (at least one)
407
+ const sessionUpdates = subscriber.collectNotifications(
408
+ isEventForAgent("session_update", agentId),
409
+ );
410
+ log(`Got ${sessionUpdates.length} session_update events`);
411
+ expect(sessionUpdates.length).toBeGreaterThan(0);
412
+
413
+ // Verify at least one update contains an agent_message_chunk
414
+ const hasTextChunk = sessionUpdates.some((msg) => {
415
+ const data = getEventData(msg);
416
+ const update = data.update as Record<string, unknown> | undefined;
417
+ return update?.sessionUpdate === "agent_message_chunk";
418
+ });
419
+ log(`Has text chunk in updates: ${hasTextChunk}`);
420
+ expect(hasTextChunk).toBe(true);
421
+
422
+ // Step 7: Verify event ordering: user_message → updates → prompt_done
423
+ const allSessionEvents = subscriber.collectNotifications((msg) => {
424
+ if (msg.method !== "map/event") return false;
425
+ const params = msg.params as Record<string, unknown> | undefined;
426
+ const event = params?.event as Record<string, unknown> | undefined;
427
+ const data = event?.data as Record<string, unknown> | undefined;
428
+ if (data?.agentId !== agentId) return false;
429
+ const type = event?.type as string;
430
+ return type === "session_user_message" || type === "session_update" || type === "session_prompt_done";
431
+ });
432
+
433
+ const eventTypes = allSessionEvents.map((msg) => {
434
+ const params = msg.params as Record<string, unknown>;
435
+ const event = params.event as Record<string, unknown>;
436
+ return event.type;
437
+ });
438
+ log(`Event order: ${eventTypes.join(" → ")}`);
439
+
440
+ // First event should be user_message, last should be prompt_done
441
+ expect(eventTypes[0]).toBe("session_user_message");
442
+ expect(eventTypes[eventTypes.length - 1]).toBe("session_prompt_done");
443
+
444
+ // Step 8: Verify turns were recorded in EventStore for history persistence
445
+ // Wait a moment for the async turn recording to complete
446
+ await new Promise((r) => setTimeout(r, 2000));
447
+
448
+ const turnEvents = eventStore.query({
449
+ type: "turn",
450
+ source_agent_id: agentId,
451
+ });
452
+ log(`Turn events recorded: ${turnEvents.length}`);
453
+
454
+ // Should have at least a user turn and an assistant turn
455
+ const userTurns = turnEvents.filter(
456
+ (e) => (e.payload as Record<string, unknown>).participant === "user",
457
+ );
458
+ const assistantTurns = turnEvents.filter(
459
+ (e) => (e.payload as Record<string, unknown>).participant === agentId,
460
+ );
461
+
462
+ log(`User turns: ${userTurns.length}, Assistant turns: ${assistantTurns.length}`);
463
+ expect(userTurns.length).toBeGreaterThanOrEqual(1);
464
+ expect(assistantTurns.length).toBeGreaterThanOrEqual(1);
465
+
466
+ // Verify the user turn contains the task prompt
467
+ const userTurnPayload = userTurns[0].payload as Record<string, unknown>;
468
+ expect(userTurnPayload.content).toContain("Hello from spawned agent");
469
+ expect(userTurnPayload.conversation_id).toBe(spawnResult.session_id);
470
+
471
+ // Verify the assistant turn has content
472
+ const assistantPayload = assistantTurns[0].payload as Record<string, unknown>;
473
+ expect(assistantPayload.content_type).toBe("assistant_response");
474
+ const content = assistantPayload.content as { parts: Array<Record<string, unknown>> };
475
+ expect(content.parts.length).toBeGreaterThan(0);
476
+ expect(content.parts[0].type).toBe("text");
477
+ log(`Assistant response: ${JSON.stringify(content.parts[0].text).slice(0, 100)}...`);
478
+
479
+ log("All assertions passed!");
480
+ },
481
+ );
482
+
483
+ testFn(
484
+ "multiple subscribers each receive spawned agent session events",
485
+ { timeout: TIMEOUT.SPAWN_AND_PROMPT },
486
+ async () => {
487
+ // Connect two subscribers
488
+ const sub1 = createClient();
489
+ const sub2 = createClient();
490
+ await sub1.connect();
491
+ await sub2.connect();
492
+
493
+ await sub1.request("map/subscribe", {});
494
+ await sub2.request("map/subscribe", {});
495
+ log("Two subscribers connected");
496
+
497
+ // Seed root agent
498
+ eventStore.emit({
499
+ type: "spawn",
500
+ source: { agent_id: "system" },
501
+ payload: {
502
+ agent_id: "agent_multi_caller",
503
+ session_id: "sess_multi_caller",
504
+ task: "Multi-subscriber test caller",
505
+ task_id: "task_multi_caller",
506
+ parent: null,
507
+ config: {},
508
+ cwd: process.cwd(),
509
+ },
510
+ });
511
+
512
+ // Spawn agent
513
+ const spawnResult = await mapCall<{
514
+ agent_id: string;
515
+ session_id: string;
516
+ task_id: string;
517
+ }>(serverUrl, "_macro/mcp/spawn_agent", {
518
+ context: {
519
+ agent_id: "agent_multi_caller",
520
+ session_id: "sess_multi_caller",
521
+ task_id: "task_multi_caller",
522
+ lineage: [],
523
+ cwd: process.cwd(),
524
+ },
525
+ task: "Say exactly: 'test'. Do not use any tools.",
526
+ });
527
+ log(`Agent spawned: ${spawnResult.agent_id}`);
528
+
529
+ const agentId = spawnResult.agent_id;
530
+
531
+ // Both subscribers should receive session_prompt_done
532
+ const [done1, done2] = await Promise.all([
533
+ sub1.waitForNotification(
534
+ isEventForAgent("session_prompt_done", agentId),
535
+ TIMEOUT.EVENT_WAIT,
536
+ ),
537
+ sub2.waitForNotification(
538
+ isEventForAgent("session_prompt_done", agentId),
539
+ TIMEOUT.EVENT_WAIT,
540
+ ),
541
+ ]);
542
+
543
+ expect(getEventData(done1).stopReason).toBe("end_turn");
544
+ expect(getEventData(done2).stopReason).toBe("end_turn");
545
+
546
+ // Both should have received session_user_message
547
+ const userMsg1 = sub1.collectNotifications(isEventForAgent("session_user_message", agentId));
548
+ const userMsg2 = sub2.collectNotifications(isEventForAgent("session_user_message", agentId));
549
+ expect(userMsg1.length).toBe(1);
550
+ expect(userMsg2.length).toBe(1);
551
+
552
+ // Both should have received session_update events
553
+ const updates1 = sub1.collectNotifications(isEventForAgent("session_update", agentId));
554
+ const updates2 = sub2.collectNotifications(isEventForAgent("session_update", agentId));
555
+ expect(updates1.length).toBeGreaterThan(0);
556
+ expect(updates2.length).toBeGreaterThan(0);
557
+ expect(updates1.length).toBe(updates2.length);
558
+
559
+ log(`Both subscribers received ${updates1.length} session_update events`);
560
+ log("Multi-subscriber test passed!");
561
+ },
562
+ );
563
+ });
@@ -228,7 +228,7 @@ describe("_macro/getHistory", () => {
228
228
  title: "Read file",
229
229
  status: "completed",
230
230
  rawInput: { path: "/test.txt" },
231
- output: "file contents here",
231
+ rawOutput: "file contents here",
232
232
  },
233
233
  {
234
234
  sessionUpdate: "agent_message_chunk",
@@ -258,10 +258,10 @@ describe("_macro/getHistory", () => {
258
258
  const assistantContent = turns[1].content as {
259
259
  parts: { type: string; text?: string; toolCallId?: string; title?: string; output?: unknown }[];
260
260
  };
261
- expect(assistantContent.parts).toHaveLength(2);
261
+ expect(assistantContent.parts).toHaveLength(3);
262
262
  expect(assistantContent.parts[0]).toEqual({
263
263
  type: "text",
264
- text: "Let me check that. Done!",
264
+ text: "Let me check that.",
265
265
  });
266
266
  expect(assistantContent.parts[1]).toMatchObject({
267
267
  type: "tool",
@@ -270,6 +270,10 @@ describe("_macro/getHistory", () => {
270
270
  status: "completed",
271
271
  output: "file contents here",
272
272
  });
273
+ expect(assistantContent.parts[2]).toEqual({
274
+ type: "text",
275
+ text: " Done!",
276
+ });
273
277
  });
274
278
 
275
279
  it("should accumulate history across multiple prompts", async () => {
@@ -406,7 +410,7 @@ describe("_macro/getHistory", () => {
406
410
  title: "Done tool",
407
411
  status: "completed",
408
412
  rawInput: { x: 1 },
409
- output: "result",
413
+ rawOutput: "result",
410
414
  },
411
415
  ]);
412
416