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,820 @@
1
+ /**
2
+ * ACP-over-MAP Cancel & Stop Tests
3
+ *
4
+ * Tests two distinct cancellation mechanisms:
5
+ * 1. ACP cancel (session/cancel) - soft cancel: aborts the streaming loop and
6
+ * calls session.cancel() on the subprocess, but keeps the agent alive.
7
+ * 2. MAP stop (map/agents/stop via handleStopAgent) - hard kill: terminates
8
+ * the agent subprocess, deallocates resources, cascade-terminates children.
9
+ */
10
+
11
+ import { describe, it, expect, afterEach, vi, beforeEach } from "vitest";
12
+ import { ACPOverMAPHandler } from "../acp-over-map.js";
13
+ import type { ACPEnvelope } from "../acp-over-map.js";
14
+ import {
15
+ createMAPAdapter,
16
+ MAPAdapterImpl,
17
+ type MAPAdapterServices,
18
+ } from "../map-adapter.js";
19
+ import type { MAPAdapter } from "../interface.js";
20
+ import type { ParticipantId, EventNotification } from "../types.js";
21
+ import { createEventStore, type EventStore } from "../../../store/event-store.js";
22
+ import type { AgentManager } from "../../../agent/agent-manager.js";
23
+ import type { TaskManager } from "../../../task/task-manager.js";
24
+ import type { Agent, Task } from "../../../store/types/index.js";
25
+ import type { AgentId } from "../../../store/types/index.js";
26
+
27
+ // ─────────────────────────────────────────────────────────────────
28
+ // Helpers
29
+ // ─────────────────────────────────────────────────────────────────
30
+
31
+ function createMockAgent(overrides: Partial<Agent> = {}): Agent {
32
+ return {
33
+ id: "agent-1" as AgentId,
34
+ session_id: "session-1",
35
+ state: "running",
36
+ task: "Test task",
37
+ task_id: "task-1",
38
+ parent: null,
39
+ lineage: [],
40
+ config: {},
41
+ cwd: "/test/cwd",
42
+ created_at: Date.now(),
43
+ started_at: Date.now(),
44
+ ...overrides,
45
+ };
46
+ }
47
+
48
+ function createMockTask(overrides: Partial<Task> = {}): Task {
49
+ return {
50
+ id: "task-1",
51
+ description: "Test task",
52
+ status: "in_progress",
53
+ created_by: "agent-1",
54
+ created_at: Date.now(),
55
+ ...overrides,
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Create a mock AgentManager that yields the given updates from prompt().
61
+ * The mock session object has a cancel() method for testing ACP cancel.
62
+ */
63
+ function createMockAgentManager(
64
+ promptUpdates: unknown[] = [],
65
+ options?: { slowYield?: number },
66
+ ): { agentManager: AgentManager; mockSession: { cancel: ReturnType<typeof vi.fn> } } {
67
+ const mockAgent = createMockAgent();
68
+ const mockSession = {
69
+ cancel: vi.fn().mockResolvedValue(undefined),
70
+ prompt: vi.fn(),
71
+ id: "session-1",
72
+ cwd: "/test/cwd",
73
+ modes: [],
74
+ models: [],
75
+ isProcessing: false,
76
+ };
77
+
78
+ const agentManager = {
79
+ spawn: vi.fn().mockResolvedValue({
80
+ id: "agent-new",
81
+ session_id: "session-new",
82
+ agent: createMockAgent({ id: "agent-new" as AgentId, session_id: "session-new" }),
83
+ session: mockSession,
84
+ }),
85
+ get: vi.fn().mockReturnValue(mockAgent),
86
+ list: vi.fn().mockReturnValue([mockAgent]),
87
+ listHeadManagers: vi.fn().mockReturnValue([mockAgent]),
88
+ getChildren: vi.fn().mockReturnValue([]),
89
+ getHierarchy: vi.fn().mockReturnValue({
90
+ root: { agent: mockAgent, children: [] },
91
+ depth: 1,
92
+ totalAgents: 1,
93
+ }),
94
+ getOrCreateHeadManager: vi.fn().mockResolvedValue({
95
+ id: "agent-1",
96
+ session_id: "session-1",
97
+ agent: mockAgent,
98
+ session: mockSession,
99
+ }),
100
+ hasActiveSession: vi.fn().mockReturnValue(true),
101
+ resume: vi.fn().mockResolvedValue({
102
+ id: "agent-1",
103
+ session_id: "session-1",
104
+ agent: mockAgent,
105
+ session: mockSession,
106
+ }),
107
+ terminate: vi.fn().mockResolvedValue(undefined),
108
+ prompt: vi.fn().mockReturnValue({
109
+ [Symbol.asyncIterator]: async function* () {
110
+ for (const update of promptUpdates) {
111
+ if (options?.slowYield) {
112
+ await new Promise((r) => setTimeout(r, options.slowYield));
113
+ }
114
+ yield update;
115
+ }
116
+ },
117
+ }),
118
+ getSession: vi.fn().mockReturnValue(mockSession),
119
+ onLifecycleEvent: vi.fn().mockReturnValue(() => {}),
120
+ close: vi.fn().mockResolvedValue(undefined),
121
+ respondToPermission: vi.fn().mockReturnValue(true),
122
+ cancelPermission: vi.fn().mockReturnValue(true),
123
+ } as unknown as AgentManager;
124
+
125
+ return { agentManager, mockSession };
126
+ }
127
+
128
+ function createMockTaskManager(): TaskManager {
129
+ return {
130
+ get: vi.fn().mockReturnValue(createMockTask()),
131
+ list: vi.fn().mockReturnValue([createMockTask()]),
132
+ create: vi.fn().mockReturnValue(createMockTask()),
133
+ } as unknown as TaskManager;
134
+ }
135
+
136
+ /** Build an ACP envelope for processRequest */
137
+ function envelope(
138
+ streamId: string,
139
+ method: string,
140
+ params?: unknown,
141
+ sessionId?: string,
142
+ ): ACPEnvelope {
143
+ return {
144
+ acp: {
145
+ jsonrpc: "2.0",
146
+ id: `${streamId}-${method}-${Date.now()}`,
147
+ method,
148
+ params,
149
+ },
150
+ acpContext: {
151
+ streamId,
152
+ sessionId,
153
+ direction: "client-to-agent",
154
+ },
155
+ };
156
+ }
157
+
158
+ // ─────────────────────────────────────────────────────────────────
159
+ // Tests
160
+ // ─────────────────────────────────────────────────────────────────
161
+
162
+ describe("ACP-over-MAP cancel and stop", () => {
163
+ let eventStore: EventStore;
164
+ let handler: ACPOverMAPHandler;
165
+
166
+ afterEach(async () => {
167
+ if (eventStore) {
168
+ await eventStore.close();
169
+ }
170
+ });
171
+
172
+ async function setup(
173
+ promptUpdates: unknown[] = [],
174
+ options?: { slowYield?: number },
175
+ ) {
176
+ eventStore = await createEventStore({ inMemory: true });
177
+ const { agentManager, mockSession } = createMockAgentManager(promptUpdates, options);
178
+ const taskManager = createMockTaskManager();
179
+
180
+ handler = new ACPOverMAPHandler({
181
+ agentManager,
182
+ eventStore,
183
+ taskManager,
184
+ defaultCwd: "/test/cwd",
185
+ });
186
+
187
+ return { agentManager, taskManager, mockSession };
188
+ }
189
+
190
+ /** Register an agent in the EventStore so loadSession can resolve it */
191
+ function registerAgent(agentId: string, sessionId: string): void {
192
+ eventStore.emit({
193
+ type: "spawn",
194
+ source: { agent_id: agentId },
195
+ payload: {
196
+ agent_id: agentId,
197
+ session_id: sessionId,
198
+ task: "Test task",
199
+ task_id: "task-1",
200
+ cwd: "/test/cwd",
201
+ },
202
+ });
203
+ eventStore.emit({
204
+ type: "lifecycle",
205
+ source: { agent_id: agentId },
206
+ payload: {
207
+ agent_id: agentId,
208
+ action: "started",
209
+ },
210
+ });
211
+ }
212
+
213
+ /** Initialize a stream and create a session, returning the sessionId */
214
+ async function initAndCreateSession(
215
+ streamId: string,
216
+ targetAgentId: AgentId = "agent-1" as AgentId,
217
+ ): Promise<string> {
218
+ await handler.processRequest(
219
+ targetAgentId,
220
+ envelope(streamId, "initialize", {
221
+ protocolVersion: 1,
222
+ capabilities: {},
223
+ clientInfo: { name: "test", version: "1.0" },
224
+ }),
225
+ );
226
+
227
+ const sessionResult = await handler.processRequest(
228
+ targetAgentId,
229
+ envelope(streamId, "session/new", {
230
+ cwd: "/test",
231
+ mcpServers: [],
232
+ }),
233
+ );
234
+
235
+ const sessionId = (sessionResult.acp.result as { sessionId?: string })?.sessionId;
236
+ if (!sessionId) throw new Error("session/new did not return sessionId");
237
+
238
+ registerAgent(targetAgentId, sessionId);
239
+
240
+ return sessionId;
241
+ }
242
+
243
+ // ─────────────────────────────────────────────────────────────────
244
+ // ACP Cancel (session/cancel) — soft cancel
245
+ // ─────────────────────────────────────────────────────────────────
246
+
247
+ describe("ACP cancel (session/cancel)", () => {
248
+ it("should return cancelled:true", async () => {
249
+ await setup();
250
+
251
+ const streamId = "cancel-test-1";
252
+ const agentId = "agent-1" as AgentId;
253
+ const sessionId = await initAndCreateSession(streamId, agentId);
254
+
255
+ const result = await handler.processRequest(
256
+ agentId,
257
+ envelope(streamId, "session/cancel", { sessionId }, sessionId),
258
+ );
259
+
260
+ expect(result.acp.result).toEqual({ cancelled: true });
261
+ });
262
+
263
+ it("should call session.cancel() on the agent's active session", async () => {
264
+ const { mockSession } = await setup();
265
+
266
+ const streamId = "cancel-test-2";
267
+ const agentId = "agent-1" as AgentId;
268
+ const sessionId = await initAndCreateSession(streamId, agentId);
269
+
270
+ await handler.processRequest(
271
+ agentId,
272
+ envelope(streamId, "session/cancel", { sessionId }, sessionId),
273
+ );
274
+
275
+ expect(mockSession.cancel).toHaveBeenCalledTimes(1);
276
+ });
277
+
278
+ it("should NOT call agentManager.terminate()", async () => {
279
+ const { agentManager } = await setup();
280
+
281
+ const streamId = "cancel-test-3";
282
+ const agentId = "agent-1" as AgentId;
283
+ const sessionId = await initAndCreateSession(streamId, agentId);
284
+
285
+ await handler.processRequest(
286
+ agentId,
287
+ envelope(streamId, "session/cancel", { sessionId }, sessionId),
288
+ );
289
+
290
+ expect(agentManager.terminate).not.toHaveBeenCalled();
291
+ });
292
+
293
+ it("should abort the stream's abort controller", async () => {
294
+ await setup();
295
+
296
+ const streamId = "cancel-test-4";
297
+ const agentId = "agent-1" as AgentId;
298
+ const sessionId = await initAndCreateSession(streamId, agentId);
299
+
300
+ await handler.processRequest(
301
+ agentId,
302
+ envelope(streamId, "session/cancel", { sessionId }, sessionId),
303
+ );
304
+
305
+ // Sending a prompt after cancel should reset the abort controller and work
306
+ // (the handler resets it on new prompt if aborted)
307
+ const promptResult = await handler.processRequest(
308
+ agentId,
309
+ envelope(streamId, "session/prompt", {
310
+ prompt: [{ type: "text", text: "Hello after cancel" }],
311
+ }, sessionId),
312
+ );
313
+
314
+ expect(promptResult.acp.result).toBeDefined();
315
+ expect(promptResult.acp.error).toBeUndefined();
316
+ });
317
+
318
+ it("should handle cancel gracefully when no session exists", async () => {
319
+ const { agentManager } = await setup();
320
+ // Mock getSession returning null for this case
321
+ (agentManager.getSession as ReturnType<typeof vi.fn>).mockReturnValue(null);
322
+
323
+ const streamId = "cancel-test-5";
324
+ const agentId = "agent-1" as AgentId;
325
+ const sessionId = await initAndCreateSession(streamId, agentId);
326
+
327
+ const result = await handler.processRequest(
328
+ agentId,
329
+ envelope(streamId, "session/cancel", { sessionId }, sessionId),
330
+ );
331
+
332
+ // Should still succeed even without an active session
333
+ expect(result.acp.result).toEqual({ cancelled: true });
334
+ });
335
+
336
+ it("should handle session.cancel() failure gracefully", async () => {
337
+ const { mockSession } = await setup();
338
+ mockSession.cancel.mockRejectedValue(new Error("cancel failed"));
339
+
340
+ const streamId = "cancel-test-6";
341
+ const agentId = "agent-1" as AgentId;
342
+ const sessionId = await initAndCreateSession(streamId, agentId);
343
+
344
+ // Should not throw even if session.cancel() fails
345
+ const result = await handler.processRequest(
346
+ agentId,
347
+ envelope(streamId, "session/cancel", { sessionId }, sessionId),
348
+ );
349
+
350
+ expect(result.acp.result).toEqual({ cancelled: true });
351
+ });
352
+
353
+ it("should NOT remove the session mapping (agent stays alive)", async () => {
354
+ await setup([
355
+ {
356
+ sessionUpdate: "agent_message_chunk",
357
+ content: { type: "text", text: "Response after cancel" },
358
+ },
359
+ ]);
360
+
361
+ const streamId = "cancel-test-7";
362
+ const agentId = "agent-1" as AgentId;
363
+ const sessionId = await initAndCreateSession(streamId, agentId);
364
+
365
+ // Cancel
366
+ await handler.processRequest(
367
+ agentId,
368
+ envelope(streamId, "session/cancel", { sessionId }, sessionId),
369
+ );
370
+
371
+ // Subsequent prompt should still work (session mapping preserved)
372
+ const promptResult = await handler.processRequest(
373
+ agentId,
374
+ envelope(streamId, "session/prompt", {
375
+ prompt: [{ type: "text", text: "Hello again" }],
376
+ }, sessionId),
377
+ );
378
+
379
+ expect(promptResult.acp.error).toBeUndefined();
380
+ expect(promptResult.acp.result).toMatchObject({ stopReason: "end_turn" });
381
+ });
382
+ });
383
+
384
+ // ─────────────────────────────────────────────────────────────────
385
+ // abortStreamsForAgent
386
+ // ─────────────────────────────────────────────────────────────────
387
+
388
+ describe("abortStreamsForAgent", () => {
389
+ it("should abort streams belonging to the given agent", async () => {
390
+ await setup([
391
+ {
392
+ sessionUpdate: "agent_message_chunk",
393
+ content: { type: "text", text: "Hello" },
394
+ },
395
+ ]);
396
+
397
+ const streamId = "abort-test-1";
398
+ const agentId = "agent-1" as AgentId;
399
+ const sessionId = await initAndCreateSession(streamId, agentId);
400
+
401
+ // Do a prompt to associate the stream with the agent
402
+ await handler.processRequest(
403
+ agentId,
404
+ envelope(streamId, "session/prompt", {
405
+ prompt: [{ type: "text", text: "Hi" }],
406
+ }, sessionId),
407
+ );
408
+
409
+ // Abort all streams for this agent
410
+ handler.abortStreamsForAgent(agentId);
411
+
412
+ // Verify by checking that a new prompt resets the abort controller
413
+ // (if it was aborted, handlePrompt will create a new controller)
414
+ const result = await handler.processRequest(
415
+ agentId,
416
+ envelope(streamId, "session/prompt", {
417
+ prompt: [{ type: "text", text: "After abort" }],
418
+ }, sessionId),
419
+ );
420
+
421
+ expect(result.acp.error).toBeUndefined();
422
+ });
423
+
424
+ it("should not affect streams for other agents", async () => {
425
+ await setup([
426
+ {
427
+ sessionUpdate: "agent_message_chunk",
428
+ content: { type: "text", text: "Hello" },
429
+ },
430
+ ]);
431
+
432
+ const streamId = "abort-test-2";
433
+ const agentId = "agent-1" as AgentId;
434
+ const sessionId = await initAndCreateSession(streamId, agentId);
435
+
436
+ // Do a prompt to associate the stream with the agent
437
+ await handler.processRequest(
438
+ agentId,
439
+ envelope(streamId, "session/prompt", {
440
+ prompt: [{ type: "text", text: "Hi" }],
441
+ }, sessionId),
442
+ );
443
+
444
+ // Abort streams for a different agent — should NOT affect our stream
445
+ handler.abortStreamsForAgent("agent-other" as AgentId);
446
+
447
+ // Our stream should still work normally
448
+ const result = await handler.processRequest(
449
+ agentId,
450
+ envelope(streamId, "session/prompt", {
451
+ prompt: [{ type: "text", text: "Still working" }],
452
+ }, sessionId),
453
+ );
454
+
455
+ expect(result.acp.error).toBeUndefined();
456
+ expect(result.acp.result).toMatchObject({ stopReason: "end_turn" });
457
+ });
458
+ });
459
+
460
+ // ─────────────────────────────────────────────────────────────────
461
+ // closeStream
462
+ // ─────────────────────────────────────────────────────────────────
463
+
464
+ describe("closeStream", () => {
465
+ it("should remove the stream and abort its controller", async () => {
466
+ await setup();
467
+
468
+ const streamId = "close-test-1";
469
+ const agentId = "agent-1" as AgentId;
470
+ await initAndCreateSession(streamId, agentId);
471
+
472
+ // Close the stream
473
+ handler.closeStream(streamId);
474
+
475
+ // Using the closed stream should create a new stream state
476
+ // (processRequest creates a new state if none exists)
477
+ const result = await handler.processRequest(
478
+ agentId,
479
+ envelope(streamId, "initialize", {
480
+ protocolVersion: 1,
481
+ capabilities: {},
482
+ clientInfo: { name: "test", version: "1.0" },
483
+ }),
484
+ );
485
+
486
+ // Should succeed (new stream state created)
487
+ expect(result.acp.error).toBeUndefined();
488
+ });
489
+ });
490
+
491
+ // ─────────────────────────────────────────────────────────────────
492
+ // Cancel during active prompt (integration)
493
+ // ─────────────────────────────────────────────────────────────────
494
+
495
+ describe("cancel during active prompt", () => {
496
+ it("should return stopReason:cancelled when abort controller fires during prompt", async () => {
497
+ // Create a prompt that yields updates slowly so we can cancel mid-stream
498
+ const { agentManager } = await setup([], { slowYield: 50 });
499
+
500
+ // Override prompt to yield many slow updates
501
+ let abortSignalRef: AbortSignal | undefined;
502
+ (agentManager.prompt as ReturnType<typeof vi.fn>).mockReturnValue({
503
+ [Symbol.asyncIterator]: async function* () {
504
+ for (let i = 0; i < 100; i++) {
505
+ await new Promise((r) => setTimeout(r, 10));
506
+ yield {
507
+ sessionUpdate: "agent_message_chunk",
508
+ content: { type: "text", text: `chunk-${i}` },
509
+ };
510
+ }
511
+ },
512
+ });
513
+
514
+ const streamId = "cancel-during-prompt";
515
+ const agentId = "agent-1" as AgentId;
516
+ const sessionId = await initAndCreateSession(streamId, agentId);
517
+
518
+ // Start prompt and cancel concurrently
519
+ const promptPromise = handler.processRequest(
520
+ agentId,
521
+ envelope(streamId, "session/prompt", {
522
+ prompt: [{ type: "text", text: "Long running task" }],
523
+ }, sessionId),
524
+ );
525
+
526
+ // Wait a bit then cancel
527
+ await new Promise((r) => setTimeout(r, 50));
528
+ await handler.processRequest(
529
+ agentId,
530
+ envelope(streamId, "session/cancel", { sessionId }, sessionId),
531
+ );
532
+
533
+ const result = await promptPromise;
534
+
535
+ // Should have been cancelled
536
+ expect(result.acp.result).toMatchObject({ stopReason: "cancelled" });
537
+ });
538
+ });
539
+ });
540
+
541
+ // ─────────────────────────────────────────────────────────────────
542
+ // MAP stop (map/agents/stop via handleStopAgent) — hard kill
543
+ // ─────────────────────────────────────────────────────────────────
544
+
545
+ describe("MAPAdapter handleStopAgent (map/agents/stop)", () => {
546
+ let adapter: MAPAdapter;
547
+ let eventStore: EventStore;
548
+ let mockAgentManager: AgentManager;
549
+ let emittedEvents: EventNotification[];
550
+
551
+ async function setupAdapter(overrides?: Partial<AgentManager>) {
552
+ eventStore = await createEventStore({ inMemory: true });
553
+ emittedEvents = [];
554
+
555
+ const mockAgent = createMockAgent();
556
+ const mockSession = {
557
+ cancel: vi.fn().mockResolvedValue(undefined),
558
+ id: "session-1",
559
+ cwd: "/test/cwd",
560
+ modes: [],
561
+ models: [],
562
+ isProcessing: false,
563
+ };
564
+
565
+ mockAgentManager = {
566
+ spawn: vi.fn(),
567
+ get: vi.fn().mockReturnValue(mockAgent),
568
+ list: vi.fn().mockReturnValue([mockAgent]),
569
+ listHeadManagers: vi.fn().mockReturnValue([mockAgent]),
570
+ getChildren: vi.fn().mockReturnValue([]),
571
+ getHierarchy: vi.fn().mockReturnValue({
572
+ root: { agent: mockAgent, children: [] },
573
+ depth: 1,
574
+ totalAgents: 1,
575
+ }),
576
+ getOrCreateHeadManager: vi.fn(),
577
+ hasActiveSession: vi.fn().mockReturnValue(true),
578
+ resume: vi.fn(),
579
+ terminate: vi.fn().mockResolvedValue(undefined),
580
+ prompt: vi.fn().mockReturnValue({
581
+ [Symbol.asyncIterator]: async function* () {},
582
+ }),
583
+ getSession: vi.fn().mockReturnValue(mockSession),
584
+ onLifecycleEvent: vi.fn().mockReturnValue(() => {}),
585
+ close: vi.fn().mockResolvedValue(undefined),
586
+ respondToPermission: vi.fn().mockReturnValue(true),
587
+ cancelPermission: vi.fn().mockReturnValue(true),
588
+ ...overrides,
589
+ } as unknown as AgentManager;
590
+
591
+ const mockTaskManager = createMockTaskManager();
592
+
593
+ const services: MAPAdapterServices = {
594
+ getAgent: vi.fn(),
595
+ listAgents: vi.fn().mockReturnValue([]),
596
+ sendMessage: vi.fn().mockResolvedValue({ delivered: [] }),
597
+ getAncestors: vi.fn().mockReturnValue([]),
598
+ getDescendants: vi.fn().mockReturnValue([]),
599
+ agentManager: mockAgentManager,
600
+ eventStore,
601
+ taskManager: mockTaskManager,
602
+ defaultCwd: "/test/cwd",
603
+ };
604
+
605
+ adapter = createMAPAdapter(
606
+ {
607
+ name: "test-stop",
608
+ version: "1.0.0",
609
+ defaultClientCapabilities: {
610
+ canQuery: true,
611
+ canSubscribe: true,
612
+ canMessage: true,
613
+ canStop: true,
614
+ },
615
+ },
616
+ services,
617
+ );
618
+
619
+ // Listen for emitted events
620
+ adapter.onEvent((e) => {
621
+ if ("eventId" in e) {
622
+ emittedEvents.push(e as unknown as EventNotification);
623
+ }
624
+ });
625
+
626
+ await adapter.start();
627
+ }
628
+
629
+ afterEach(async () => {
630
+ if (adapter?.isRunning()) {
631
+ await adapter.stop();
632
+ }
633
+ if (eventStore) {
634
+ await eventStore.close();
635
+ }
636
+ });
637
+
638
+ it("should call agentManager.terminate() with agentId and reason", async () => {
639
+ await setupAdapter();
640
+
641
+ const impl = adapter as unknown as {
642
+ handleStopAgent: (
643
+ participantId: ParticipantId,
644
+ params: unknown,
645
+ ) => Promise<{ stopping: boolean }>;
646
+ };
647
+
648
+ const result = await impl.handleStopAgent(
649
+ "p-test" as ParticipantId,
650
+ { agentId: "agent-1" as AgentId, reason: "user stopped" },
651
+ );
652
+
653
+ expect(result).toEqual({ stopping: true });
654
+ expect(mockAgentManager.terminate).toHaveBeenCalledWith(
655
+ "agent-1",
656
+ "user stopped",
657
+ );
658
+ });
659
+
660
+ it("should use 'cancelled' as default reason", async () => {
661
+ await setupAdapter();
662
+
663
+ const impl = adapter as unknown as {
664
+ handleStopAgent: (
665
+ participantId: ParticipantId,
666
+ params: unknown,
667
+ ) => Promise<{ stopping: boolean }>;
668
+ };
669
+
670
+ await impl.handleStopAgent(
671
+ "p-test" as ParticipantId,
672
+ { agentId: "agent-1" as AgentId },
673
+ );
674
+
675
+ expect(mockAgentManager.terminate).toHaveBeenCalledWith(
676
+ "agent-1",
677
+ "cancelled",
678
+ );
679
+ });
680
+
681
+ it("should throw invalidParams when agentId is missing", async () => {
682
+ await setupAdapter();
683
+
684
+ const impl = adapter as unknown as {
685
+ handleStopAgent: (
686
+ participantId: ParticipantId,
687
+ params: unknown,
688
+ ) => Promise<{ stopping: boolean }>;
689
+ };
690
+
691
+ await expect(
692
+ impl.handleStopAgent("p-test" as ParticipantId, {}),
693
+ ).rejects.toThrow("agentId required");
694
+ });
695
+
696
+ it("should throw internalError when agentManager is not available", async () => {
697
+ // Create adapter without agentManager in services
698
+ eventStore = await createEventStore({ inMemory: true });
699
+
700
+ const services: MAPAdapterServices = {
701
+ getAgent: vi.fn(),
702
+ listAgents: vi.fn().mockReturnValue([]),
703
+ sendMessage: vi.fn().mockResolvedValue({ delivered: [] }),
704
+ getAncestors: vi.fn().mockReturnValue([]),
705
+ getDescendants: vi.fn().mockReturnValue([]),
706
+ // No agentManager!
707
+ };
708
+
709
+ adapter = createMAPAdapter({ name: "test-no-am" }, services);
710
+ await adapter.start();
711
+
712
+ const impl = adapter as unknown as {
713
+ handleStopAgent: (
714
+ participantId: ParticipantId,
715
+ params: unknown,
716
+ ) => Promise<{ stopping: boolean }>;
717
+ };
718
+
719
+ await expect(
720
+ impl.handleStopAgent("p-test" as ParticipantId, {
721
+ agentId: "agent-1" as AgentId,
722
+ }),
723
+ ).rejects.toThrow("Agent manager not available");
724
+ });
725
+
726
+ it("should throw internalError when terminate fails", async () => {
727
+ await setupAdapter({
728
+ terminate: vi.fn().mockRejectedValue(new Error("Agent not found")),
729
+ } as unknown as Partial<AgentManager>);
730
+
731
+ const impl = adapter as unknown as {
732
+ handleStopAgent: (
733
+ participantId: ParticipantId,
734
+ params: unknown,
735
+ ) => Promise<{ stopping: boolean }>;
736
+ };
737
+
738
+ await expect(
739
+ impl.handleStopAgent("p-test" as ParticipantId, {
740
+ agentId: "agent-1" as AgentId,
741
+ }),
742
+ ).rejects.toThrow("Failed to stop agent: Agent not found");
743
+ });
744
+
745
+ it("should emit agent.state.changed event via lifecycle listener", async () => {
746
+ // Capture lifecycle callback so we can fire it from the mock terminate
747
+ let lifecycleCallback: ((event: unknown) => void) | undefined;
748
+
749
+ await setupAdapter({
750
+ onLifecycleEvent: vi.fn().mockImplementation((cb: (event: unknown) => void) => {
751
+ lifecycleCallback = cb;
752
+ return () => {};
753
+ }),
754
+ terminate: vi.fn().mockImplementation(async () => {
755
+ // Simulate what real agentManager.terminate() does: fire lifecycle
756
+ if (lifecycleCallback) {
757
+ lifecycleCallback({
758
+ type: "stopped",
759
+ agent: { id: "agent-1", name: "test-agent", role: "worker", state: "stopped" },
760
+ reason: "user stopped",
761
+ });
762
+ }
763
+ }),
764
+ } as unknown as Partial<AgentManager>);
765
+
766
+ const impl = adapter as unknown as {
767
+ handleStopAgent: (
768
+ participantId: ParticipantId,
769
+ params: unknown,
770
+ ) => Promise<{ stopping: boolean }>;
771
+ emitEvent: (event: EventNotification) => void;
772
+ };
773
+
774
+ // Spy on emitEvent to capture what was emitted
775
+ const emitSpy = vi.spyOn(impl, "emitEvent");
776
+
777
+ await impl.handleStopAgent(
778
+ "p-test" as ParticipantId,
779
+ { agentId: "agent-1" as AgentId, reason: "user stopped" },
780
+ );
781
+
782
+ // Verify emitEvent was called with the right event shape via lifecycle
783
+ expect(emitSpy).toHaveBeenCalledWith(
784
+ expect.objectContaining({
785
+ type: "agent_state_changed",
786
+ agentId: "agent-1",
787
+ data: expect.objectContaining({
788
+ agentId: "agent-1",
789
+ current: "stopped",
790
+ previous: "running",
791
+ reason: "user stopped",
792
+ }),
793
+ }),
794
+ );
795
+ });
796
+
797
+ it("should abort ACP streams for the agent before terminating", async () => {
798
+ await setupAdapter();
799
+
800
+ // Access the internal ACP handler to spy on abortStreamsForAgent
801
+ const impl = adapter as unknown as {
802
+ acpOverMapHandler: ACPOverMAPHandler | null;
803
+ handleStopAgent: (
804
+ participantId: ParticipantId,
805
+ params: unknown,
806
+ ) => Promise<{ stopping: boolean }>;
807
+ };
808
+
809
+ const abortSpy = vi.spyOn(impl.acpOverMapHandler!, "abortStreamsForAgent");
810
+
811
+ await impl.handleStopAgent(
812
+ "p-test" as ParticipantId,
813
+ { agentId: "agent-1" as AgentId },
814
+ );
815
+
816
+ expect(abortSpy).toHaveBeenCalledWith("agent-1");
817
+ // terminate should be called AFTER abort
818
+ expect(mockAgentManager.terminate).toHaveBeenCalled();
819
+ });
820
+ });