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
@@ -11,10 +11,32 @@
11
11
  import type { AgentManager } from "../../agent/agent-manager.js";
12
12
  import type { EventStore } from "../../store/event-store.js";
13
13
  import type { TaskManager } from "../../task/task-manager.js";
14
- import type { AgentId } from "../../store/types/index.js";
14
+ import type { AgentId, TaskId } from "../../store/types/index.js";
15
+ import type { AgentConfig } from "../../agent/types.js";
16
+ import type { TaskFilter } from "../../task/types.js";
15
17
  import { SessionMapper } from "../../acp/session-mapper.js";
16
18
  import type { ACPSessionId } from "../../acp/types.js";
17
19
 
20
+ // ─────────────────────────────────────────────────────────────────
21
+ // Helpers
22
+ // ─────────────────────────────────────────────────────────────────
23
+
24
+ /** Extract a plain-text output string from `rawOutput` (string | ContentBlock[] | undefined). */
25
+ function extractToolOutput(rawOutput: unknown): string | undefined {
26
+ if (typeof rawOutput === "string") return rawOutput;
27
+ if (Array.isArray(rawOutput)) {
28
+ return (
29
+ rawOutput
30
+ .filter(
31
+ (item: any) => item.type === "text" && typeof item.text === "string",
32
+ )
33
+ .map((item: any) => item.text as string)
34
+ .join("\n") || undefined
35
+ );
36
+ }
37
+ return undefined;
38
+ }
39
+
18
40
  // ─────────────────────────────────────────────────────────────────
19
41
  // Types
20
42
  // ─────────────────────────────────────────────────────────────────
@@ -40,6 +62,18 @@ export interface ACPOverMAPConfig {
40
62
  eventStore: EventStore;
41
63
  taskManager: TaskManager;
42
64
  defaultCwd?: string;
65
+
66
+ /**
67
+ * Callback when a new agent is created via session/new or session/loadSession.
68
+ * Used by MAPAdapter to emit agent.registered events to all subscribers.
69
+ */
70
+ onAgentRegistered?: (agent: {
71
+ id: string;
72
+ name?: string;
73
+ role?: string;
74
+ parent?: string;
75
+ metadata?: Record<string, unknown>;
76
+ }) => void;
43
77
  }
44
78
 
45
79
  /**
@@ -54,6 +88,8 @@ interface StreamState {
54
88
  sessionId?: string;
55
89
  agentId?: AgentId;
56
90
  abortController: AbortController;
91
+ /** Permission mode from initialization _meta */
92
+ permissionMode?: "auto-approve" | "auto-deny" | "callback" | "interactive";
57
93
  }
58
94
 
59
95
  // ─────────────────────────────────────────────────────────────────
@@ -65,18 +101,61 @@ export class ACPOverMAPHandler {
65
101
  private eventStore: EventStore;
66
102
  private taskManager: TaskManager;
67
103
  private defaultCwd: string;
104
+ private onAgentRegistered?: ACPOverMAPConfig["onAgentRegistered"];
68
105
 
69
106
  /** Stream states by streamId */
70
107
  private streams: Map<string, StreamState> = new Map();
71
108
 
72
109
  /** Session mapper for ACP session -> Agent mapping */
73
- private sessionMapper: SessionMapper = new SessionMapper();
110
+ private sessionMapper: SessionMapper;
74
111
 
75
112
  constructor(config: ACPOverMAPConfig) {
76
113
  this.agentManager = config.agentManager;
77
114
  this.eventStore = config.eventStore;
78
115
  this.taskManager = config.taskManager;
79
116
  this.defaultCwd = config.defaultCwd ?? process.cwd();
117
+ this.onAgentRegistered = config.onAgentRegistered;
118
+
119
+ // Initialize session mapper with EventStore for persistence and recovery
120
+ this.sessionMapper = new SessionMapper(this.eventStore);
121
+ const recovered = this.sessionMapper.recoverFromStore();
122
+ if (recovered > 0) {
123
+ console.error(
124
+ `[ACP-over-MAP] Recovered ${recovered} session(s) from store`,
125
+ );
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Notify subscribers that a new agent was registered.
131
+ * Looks up agent details from EventStore and calls the onAgentRegistered callback.
132
+ */
133
+ private notifyAgentRegistered(agentId: string): void {
134
+ if (!this.onAgentRegistered) return;
135
+ const agent = this.eventStore.getAgent(agentId as AgentId);
136
+ this.onAgentRegistered({
137
+ id: agentId,
138
+ name: agent?.name,
139
+ role: agent?.role,
140
+ parent: agent?.parent ?? undefined,
141
+ metadata: agent?.metadata,
142
+ });
143
+ }
144
+
145
+ /**
146
+ * Abort all active streams targeting a specific agent.
147
+ * Called when an agent is stopped via MAP protocol to interrupt
148
+ * any in-progress ACP prompt streaming.
149
+ */
150
+ abortStreamsForAgent(agentId: AgentId): void {
151
+ for (const [streamId, streamState] of this.streams) {
152
+ if (streamState.agentId === agentId) {
153
+ console.error(
154
+ `[ACP-over-MAP] Aborting stream ${streamId} for stopped agent ${agentId}`,
155
+ );
156
+ streamState.abortController.abort();
157
+ }
158
+ }
80
159
  }
81
160
 
82
161
  /**
@@ -94,7 +173,9 @@ export class ACPOverMAPHandler {
94
173
  const { streamId, sessionId } = acpContext;
95
174
  const method = acp.method;
96
175
 
97
- console.error(`[ACP-over-MAP] Processing - streamId=${streamId} method=${method}`);
176
+ console.error(
177
+ `[ACP-over-MAP] Processing - streamId=${streamId} method=${method}`,
178
+ );
98
179
 
99
180
  // Get or create stream state
100
181
  let streamState = this.streams.get(streamId);
@@ -117,11 +198,19 @@ export class ACPOverMAPHandler {
117
198
  break;
118
199
 
119
200
  case "session/new":
120
- result = await this.handleNewSession(streamState, acp.params, emitNotification);
201
+ result = await this.handleNewSession(
202
+ streamState,
203
+ acp.params,
204
+ emitNotification,
205
+ );
121
206
  break;
122
207
 
123
208
  case "session/load":
124
- result = await this.handleLoadSession(streamState, acp.params, emitNotification);
209
+ result = await this.handleLoadSession(
210
+ streamState,
211
+ acp.params,
212
+ emitNotification,
213
+ );
125
214
  break;
126
215
 
127
216
  case "authenticate":
@@ -129,7 +218,12 @@ export class ACPOverMAPHandler {
129
218
  break;
130
219
 
131
220
  case "session/prompt":
132
- result = await this.handlePrompt(streamState, acp.params, sessionId, emitNotification);
221
+ result = await this.handlePrompt(
222
+ streamState,
223
+ acp.params,
224
+ sessionId,
225
+ emitNotification,
226
+ );
133
227
  break;
134
228
 
135
229
  case "session/cancel":
@@ -139,7 +233,11 @@ export class ACPOverMAPHandler {
139
233
  default:
140
234
  // Check for extension methods
141
235
  if (method?.startsWith("_")) {
142
- result = await this.handleExtension(streamState, method, acp.params);
236
+ result = await this.handleExtension(
237
+ streamState,
238
+ method,
239
+ acp.params,
240
+ );
143
241
  } else {
144
242
  throw new Error(`Unknown ACP method: ${method}`);
145
243
  }
@@ -181,6 +279,21 @@ export class ACPOverMAPHandler {
181
279
 
182
280
  streamState.initialized = true;
183
281
 
282
+ // Extract permission mode from _meta.macroConfig if provided
283
+ const meta = (params as Record<string, unknown> | undefined)?._meta as
284
+ | Record<string, unknown>
285
+ | undefined;
286
+ const macroConfig = meta?.macroConfig as
287
+ | Record<string, unknown>
288
+ | undefined;
289
+ const defaultSubAgentConfig = macroConfig?.defaultSubAgentConfig as
290
+ | Record<string, unknown>
291
+ | undefined;
292
+ if (defaultSubAgentConfig?.permissionMode) {
293
+ streamState.permissionMode =
294
+ defaultSubAgentConfig.permissionMode as StreamState["permissionMode"];
295
+ }
296
+
184
297
  return {
185
298
  protocolVersion: 1,
186
299
  agentCapabilities: {
@@ -207,13 +320,15 @@ export class ACPOverMAPHandler {
207
320
  throw new Error("Must call initialize before newSession");
208
321
  }
209
322
 
210
- const { cwd, mcpServers } = (params as { cwd?: string; mcpServers?: unknown[] }) ?? {};
323
+ const { cwd, mcpServers } =
324
+ (params as { cwd?: string; mcpServers?: unknown[] }) ?? {};
211
325
  const workingDir = cwd ?? this.defaultCwd;
212
326
 
213
327
  // Spawn a new head manager for this session
214
328
  const spawned = await this.agentManager.getOrCreateHeadManager({
215
329
  cwd: workingDir,
216
330
  forceNew: true,
331
+ permissionMode: streamState.permissionMode,
217
332
  });
218
333
 
219
334
  const sessionId = spawned.session_id;
@@ -223,7 +338,12 @@ export class ACPOverMAPHandler {
223
338
  // Create session mapping
224
339
  this.sessionMapper.createMapping(sessionId as ACPSessionId, spawned.id);
225
340
 
226
- console.error(`[ACP-over-MAP] Created session ${sessionId} -> agent ${spawned.id}`);
341
+ console.error(
342
+ `[ACP-over-MAP] Created session ${sessionId} -> agent ${spawned.id}`,
343
+ );
344
+
345
+ // Notify subscribers that a new agent was registered
346
+ this.notifyAgentRegistered(spawned.id);
227
347
 
228
348
  // Emit session_info_update so client has title/timestamps
229
349
  this.emitSessionInfo(streamState, sessionId, emitNotification);
@@ -240,7 +360,11 @@ export class ACPOverMAPHandler {
240
360
  throw new Error("Must call initialize before loadSession");
241
361
  }
242
362
 
243
- const { sessionId: rawSessionId, cwd, _meta } = (params as {
363
+ const {
364
+ sessionId: rawSessionId,
365
+ cwd,
366
+ _meta,
367
+ } = (params as {
244
368
  sessionId: string;
245
369
  cwd?: string;
246
370
  _meta?: Record<string, unknown>;
@@ -259,8 +383,46 @@ export class ACPOverMAPHandler {
259
383
  }
260
384
  sessionId = agent.session_id;
261
385
  console.error(
262
- `[ACP-over-MAP] loadSession: Resolved agentId ${metaAgentId} to session ${sessionId}`
386
+ `[ACP-over-MAP] loadSession: Resolved agentId ${metaAgentId} to session ${sessionId}`,
263
387
  );
388
+
389
+ // Handle the agent directly — works for both head managers and sub-agents.
390
+ // The previous code only searched listHeadManagers(), so sub-agents
391
+ // were never found and a new head manager was created instead.
392
+ if (this.agentManager.hasActiveSession(metaAgentId as AgentId)) {
393
+ console.error(
394
+ `[ACP-over-MAP] loadSession: Reusing active session for agent ${metaAgentId}`,
395
+ );
396
+ } else {
397
+ // Agent exists but no active session — resume it
398
+ console.error(
399
+ `[ACP-over-MAP] loadSession: Resuming agent ${metaAgentId}`,
400
+ );
401
+ try {
402
+ await this.agentManager.resume(
403
+ metaAgentId as AgentId,
404
+ streamState.permissionMode,
405
+ );
406
+ } catch (resumeErr) {
407
+ // ALREADY_RUNNING can happen in a race — safe to ignore
408
+ const code = (resumeErr as { code?: string }).code;
409
+ if (code !== "ALREADY_RUNNING") {
410
+ throw resumeErr;
411
+ }
412
+ console.error(
413
+ `[ACP-over-MAP] loadSession: Agent ${metaAgentId} already running (race), continuing`,
414
+ );
415
+ }
416
+ }
417
+
418
+ streamState.sessionId = sessionId;
419
+ streamState.agentId = metaAgentId as AgentId;
420
+ this.sessionMapper.createMapping(
421
+ sessionId as ACPSessionId,
422
+ metaAgentId as AgentId,
423
+ );
424
+ this.emitSessionInfo(streamState, sessionId, emitNotification);
425
+ return { sessionId };
264
426
  }
265
427
 
266
428
  const workingDir = cwd ?? this.defaultCwd;
@@ -272,17 +434,27 @@ export class ACPOverMAPHandler {
272
434
  if (existing) {
273
435
  // Check if the agent already has an active session
274
436
  if (this.agentManager.hasActiveSession(existing.id)) {
275
- console.error(`[ACP-over-MAP] loadSession: Reusing existing session for agent ${existing.id}`);
437
+ console.error(
438
+ `[ACP-over-MAP] loadSession: Reusing existing session for agent ${existing.id}`,
439
+ );
276
440
  streamState.sessionId = sessionId;
277
441
  streamState.agentId = existing.id;
278
- this.sessionMapper.createMapping(sessionId as ACPSessionId, existing.id);
442
+ this.sessionMapper.createMapping(
443
+ sessionId as ACPSessionId,
444
+ existing.id,
445
+ );
279
446
  this.emitSessionInfo(streamState, sessionId, emitNotification);
280
447
  return { sessionId };
281
448
  }
282
449
 
283
450
  // Agent exists but no active session - resume it
284
- console.error(`[ACP-over-MAP] loadSession: Resuming stopped agent ${existing.id}`);
285
- const spawned = await this.agentManager.resume(existing.id);
451
+ console.error(
452
+ `[ACP-over-MAP] loadSession: Resuming stopped agent ${existing.id}`,
453
+ );
454
+ const spawned = await this.agentManager.resume(
455
+ existing.id,
456
+ streamState.permissionMode,
457
+ );
286
458
  streamState.sessionId = sessionId;
287
459
  streamState.agentId = spawned.id;
288
460
  this.sessionMapper.createMapping(sessionId as ACPSessionId, spawned.id);
@@ -291,15 +463,22 @@ export class ACPOverMAPHandler {
291
463
  }
292
464
 
293
465
  // No existing agent found - create new with the specified session ID
294
- console.error(`[ACP-over-MAP] loadSession: Creating new agent for session ${sessionId}`);
466
+ console.error(
467
+ `[ACP-over-MAP] loadSession: Creating new agent for session ${sessionId}`,
468
+ );
295
469
  const spawned = await this.agentManager.getOrCreateHeadManager({
296
470
  cwd: workingDir,
297
471
  sessionId,
472
+ permissionMode: streamState.permissionMode,
298
473
  });
299
474
 
300
475
  streamState.sessionId = sessionId;
301
476
  streamState.agentId = spawned.id;
302
477
  this.sessionMapper.createMapping(sessionId as ACPSessionId, spawned.id);
478
+
479
+ // Notify subscribers that a new agent was registered
480
+ this.notifyAgentRegistered(spawned.id);
481
+
303
482
  this.emitSessionInfo(streamState, sessionId, emitNotification);
304
483
 
305
484
  return { sessionId };
@@ -316,15 +495,17 @@ export class ACPOverMAPHandler {
316
495
  sessionIdFromContext?: string,
317
496
  emitNotification?: ACPNotificationEmitter,
318
497
  ): Promise<unknown> {
319
- const { prompt, sessionId: paramSessionId } = (params as {
320
- prompt?: Array<{ type: string; text?: string }>;
321
- sessionId?: string;
322
- messages?: Array<{ role: string; content: string }>;
323
- }) ?? {};
498
+ const { prompt, sessionId: paramSessionId } =
499
+ (params as {
500
+ prompt?: Array<{ type: string; text?: string }>;
501
+ sessionId?: string;
502
+ messages?: Array<{ role: string; content: string }>;
503
+ }) ?? {};
324
504
 
325
505
  // Prefer the server's resolved session ID (set during loadSession) over the
326
506
  // client's acpContext.sessionId which may be stale (e.g., "_resolve_" sentinel)
327
- const sessionId = streamState.sessionId ?? paramSessionId ?? sessionIdFromContext;
507
+ const sessionId =
508
+ streamState.sessionId ?? paramSessionId ?? sessionIdFromContext;
328
509
  if (!sessionId) {
329
510
  throw new Error("No session - call newSession or loadSession first");
330
511
  }
@@ -347,15 +528,21 @@ export class ACPOverMAPHandler {
347
528
  .filter((block) => block.type === "text" && block.text)
348
529
  .map((block) => block.text)
349
530
  .join("\n");
350
- } else if ((params as { messages?: Array<{ content: string }> })?.messages) {
531
+ } else if (
532
+ (params as { messages?: Array<{ content: string }> })?.messages
533
+ ) {
351
534
  // Handle messages format (role/content array)
352
- const messages = (params as { messages: Array<{ role: string; content: string }> }).messages;
535
+ const messages = (
536
+ params as { messages: Array<{ role: string; content: string }> }
537
+ ).messages;
353
538
  messageContent = messages.map((m) => m.content).join("\n");
354
539
  } else {
355
540
  messageContent = JSON.stringify(params);
356
541
  }
357
542
 
358
- console.error(`[ACP-over-MAP] Prompting agent ${agentId} with: ${messageContent.slice(0, 100)}...`);
543
+ console.error(
544
+ `[ACP-over-MAP] Prompting agent ${agentId} with: ${messageContent.slice(0, 100)}...`,
545
+ );
359
546
 
360
547
  // Mark session as processing
361
548
  this.sessionMapper.setProcessing(sessionId as ACPSessionId, true);
@@ -386,37 +573,106 @@ export class ACPOverMAPHandler {
386
573
  this.ensureConversation(sessionId as ACPSessionId, agentId);
387
574
 
388
575
  // Accumulate response content for history recording
389
- const buffer: { textChunks: string[]; toolCalls: Record<string, unknown>[] } = {
390
- textChunks: [],
391
- toolCalls: [],
576
+ const buffer: {
577
+ parts: Array<
578
+ | { type: "text"; text: string }
579
+ | ({ type: "tool" } & Record<string, unknown>)
580
+ >;
581
+ } = {
582
+ parts: [],
392
583
  };
393
584
 
585
+ // Track latest plan for persistence
586
+ let latestPlan: Array<{
587
+ content: string;
588
+ priority: string;
589
+ status: string;
590
+ }> | null = null;
591
+
592
+ // Track tool info from initial tool_call events (title, name, input)
593
+ // so we can merge them when tool_call_update arrives with status "completed"
594
+ const toolInfoCache = new Map<
595
+ string,
596
+ { title?: string; name?: string; input?: unknown }
597
+ >();
598
+
394
599
  try {
395
600
  // Stream responses from the agent
396
601
  let updateCount = 0;
397
- for await (const update of this.agentManager.prompt(agentId, messageContent)) {
602
+ for await (const update of this.agentManager.prompt(
603
+ agentId,
604
+ messageContent,
605
+ )) {
398
606
  // Check for cancellation
399
607
  if (streamState.abortController.signal.aborted) {
400
608
  return { stopReason: "cancelled" };
401
609
  }
402
610
 
403
- // Accumulate content for history persistence
611
+ // Accumulate content for history persistence (preserving text/tool interleaving order)
404
612
  const u = update as Record<string, unknown>;
405
- const updateType = u.sessionUpdate as string ?? u.type as string;
613
+ const updateType = (u.sessionUpdate as string) ?? (u.type as string);
614
+
615
+ // Annotate permission_request updates with agentId so clients can respond
616
+ if (updateType === "permission_request") {
617
+ u._agentId = agentId;
618
+ }
406
619
  if (updateType === "agent_message_chunk") {
407
- const content = u.content as { type?: string; text?: string } | undefined;
620
+ const content = u.content as
621
+ | { type?: string; text?: string }
622
+ | undefined;
408
623
  if (content?.text) {
409
- buffer.textChunks.push(content.text);
624
+ const last = buffer.parts[buffer.parts.length - 1];
625
+ if (last && last.type === "text") {
626
+ last.text += content.text;
627
+ } else {
628
+ buffer.parts.push({ type: "text", text: content.text });
629
+ }
630
+ }
631
+ } else if (updateType === "plan") {
632
+ const entries = (
633
+ u as {
634
+ entries?: Array<{
635
+ content: string;
636
+ priority: string;
637
+ status: string;
638
+ }>;
639
+ }
640
+ ).entries;
641
+ if (entries) {
642
+ latestPlan = entries;
410
643
  }
411
- } else if (updateType === "tool_call" || updateType === "tool_call_update") {
644
+ } else if (
645
+ updateType === "tool_call" ||
646
+ updateType === "tool_call_update"
647
+ ) {
648
+ const toolCallId = u.toolCallId as string | undefined;
412
649
  const status = u.status as string | undefined;
413
- if (status === "completed" || (updateType === "tool_call" && status !== "running")) {
414
- buffer.toolCalls.push({
415
- toolCallId: u.toolCallId,
416
- title: u.title,
417
- status: u.status,
650
+ const meta = u._meta as
651
+ | { claudeCode?: { toolName?: string } }
652
+ | undefined;
653
+
654
+ // Cache tool info from initial tool_call events
655
+ if (updateType === "tool_call" && toolCallId) {
656
+ toolInfoCache.set(toolCallId, {
657
+ title: u.title as string | undefined,
658
+ name: meta?.claudeCode?.toolName,
418
659
  input: u.rawInput,
419
- output: u.output,
660
+ });
661
+ }
662
+
663
+ if (status === "completed" || status === "failed") {
664
+ // Merge cached info for tool_call_update events that lack title/input
665
+ const cached = toolCallId
666
+ ? toolInfoCache.get(toolCallId)
667
+ : undefined;
668
+ buffer.parts.push({
669
+ type: "tool",
670
+ toolCallId,
671
+ title: u.title ?? cached?.title,
672
+ name: meta?.claudeCode?.toolName ?? cached?.name,
673
+ status: u.status,
674
+ input: u.rawInput ?? cached?.input,
675
+ output: extractToolOutput(u.rawOutput),
420
676
  });
421
677
  }
422
678
  }
@@ -426,20 +682,58 @@ export class ACPOverMAPHandler {
426
682
  updateCount++;
427
683
  }
428
684
 
429
- console.error(`[ACP-over-MAP] Prompt completed for agent ${agentId}, ${updateCount} updates`);
685
+ // Check if the loop ended because of cancellation
686
+ const wasCancelled = streamState.abortController.signal.aborted;
687
+ const stopReason = wasCancelled ? "cancelled" : "end_turn";
688
+
689
+ console.error(
690
+ `[ACP-over-MAP] Prompt completed for agent ${agentId}, ${updateCount} updates, stopReason=${stopReason}`,
691
+ );
430
692
 
431
693
  // Persist conversation turns for history
432
- this.recordPromptTurns(sessionId as ACPSessionId, agentId, messageContent, buffer);
694
+ this.recordPromptTurns(
695
+ sessionId as ACPSessionId,
696
+ agentId,
697
+ messageContent,
698
+ buffer,
699
+ );
700
+
701
+ // Persist latest plan in EventStore for history loading across restarts
702
+ if (latestPlan) {
703
+ this.eventStore.updateAgentPlan(agentId as AgentId, latestPlan);
704
+ }
433
705
 
434
706
  // Emit updated session info after prompt completes
435
707
  this.emitSessionInfo(streamState, sessionId, emitNotification);
436
708
 
437
- return { stopReason: "end_turn" };
709
+ return { stopReason };
438
710
  } catch (error) {
439
- console.error(`[ACP-over-MAP] Prompt error:`, error);
711
+ // Extract a meaningful error message — errors from the ACP SDK may be
712
+ // plain objects ({code, message}) rather than Error instances.
713
+ let errorMessage: string;
714
+ if (error instanceof Error) {
715
+ errorMessage = error.message;
716
+ } else if (
717
+ typeof error === "object" &&
718
+ error !== null &&
719
+ "message" in error &&
720
+ typeof (error as { message: unknown }).message === "string"
721
+ ) {
722
+ errorMessage = (error as { message: string }).message;
723
+ } else {
724
+ try {
725
+ errorMessage = JSON.stringify(error);
726
+ } catch {
727
+ errorMessage = String(error);
728
+ }
729
+ }
730
+ console.error(
731
+ `[ACP-over-MAP] Prompt error for agent ${agentId}:`,
732
+ errorMessage,
733
+ );
440
734
  return {
441
735
  stopReason: "end_turn",
442
- error: error instanceof Error ? error.message : String(error),
736
+ error: errorMessage,
443
737
  };
444
738
  } finally {
445
739
  this.sessionMapper.setProcessing(sessionId as ACPSessionId, false);
@@ -451,33 +745,44 @@ export class ACPOverMAPHandler {
451
745
  params: unknown,
452
746
  sessionIdFromContext?: string,
453
747
  ): Promise<unknown> {
454
- const { sessionId: paramSessionId } = (params as { sessionId?: string }) ?? {};
748
+ const { sessionId: paramSessionId } =
749
+ (params as { sessionId?: string }) ?? {};
455
750
  // Prefer server's resolved session ID over client's potentially stale one
456
- const sessionId = streamState.sessionId ?? paramSessionId ?? sessionIdFromContext;
751
+ const sessionId =
752
+ streamState.sessionId ?? paramSessionId ?? sessionIdFromContext;
457
753
 
458
- // Signal cancellation
459
- streamState.abortController.abort();
754
+ const agentId = sessionId
755
+ ? this.sessionMapper.getAgentId(sessionId as ACPSessionId)
756
+ : streamState.agentId;
460
757
 
461
- if (!sessionId) {
462
- return { cancelled: true };
463
- }
758
+ console.error(
759
+ `[ACP-over-MAP] Cancel - streamId=${streamState.streamId} sessionId=${sessionId} agentId=${agentId}`,
760
+ );
464
761
 
465
- // Get the mapped agent
466
- const agentId = this.sessionMapper.getAgentId(sessionId as ACPSessionId);
467
- if (!agentId) {
468
- return { cancelled: true };
469
- }
762
+ // 1. Abort the for-await loop in handlePrompt so it stops yielding updates
763
+ streamState.abortController.abort();
470
764
 
471
- // Terminate the agent
472
- try {
473
- await this.agentManager.terminate(agentId, "cancelled");
474
- } catch (error) {
475
- console.warn(`[ACP-over-MAP] Error terminating agent ${agentId}:`, error);
765
+ // 2. Cancel the agent's active session (sends session/cancel to the subprocess)
766
+ // This does NOT terminate the agent — it only interrupts the current prompt.
767
+ // The subprocess stays alive and can accept new prompts.
768
+ // Use map/agents/stop for full agent termination.
769
+ if (agentId) {
770
+ const session = this.agentManager.getSession(agentId);
771
+ if (session) {
772
+ try {
773
+ await session.cancel();
774
+ console.error(
775
+ `[ACP-over-MAP] Session cancelled for agent ${agentId}`,
776
+ );
777
+ } catch (error) {
778
+ console.warn(
779
+ `[ACP-over-MAP] session.cancel() failed for ${agentId}:`,
780
+ error,
781
+ );
782
+ }
783
+ }
476
784
  }
477
785
 
478
- // Clean up
479
- this.sessionMapper.removeMapping(sessionId as ACPSessionId);
480
-
481
786
  return { cancelled: true };
482
787
  }
483
788
 
@@ -490,7 +795,7 @@ export class ACPOverMAPHandler {
490
795
  method: string,
491
796
  params: unknown,
492
797
  ): Promise<unknown> {
493
- const methodParams = params as Record<string, unknown> ?? {};
798
+ const methodParams = (params as Record<string, unknown>) ?? {};
494
799
 
495
800
  switch (method) {
496
801
  case "_macro/spawnAgent": {
@@ -498,7 +803,7 @@ export class ACPOverMAPHandler {
498
803
  task: string;
499
804
  cwd?: string;
500
805
  topics?: string[];
501
- config?: Record<string, unknown>;
806
+ config?: AgentConfig;
502
807
  parentId?: string;
503
808
  };
504
809
 
@@ -522,8 +827,13 @@ export class ACPOverMAPHandler {
522
827
  task,
523
828
  cwd: cwd ?? this.defaultCwd,
524
829
  role: "worker",
830
+ topics,
831
+ config,
525
832
  });
526
833
 
834
+ // Notify subscribers that a new agent was registered
835
+ this.notifyAgentRegistered(spawned.id);
836
+
527
837
  return {
528
838
  agentId: spawned.id,
529
839
  sessionId: spawned.session_id,
@@ -560,10 +870,19 @@ export class ACPOverMAPHandler {
560
870
  throw new Error(`Agent not found: ${agentId}`);
561
871
  }
562
872
 
873
+ // If the agent is already running, return its current state
874
+ // instead of throwing. The caller likely just wants to ensure
875
+ // the agent is active, which it already is.
563
876
  if (agent.state !== "stopped" && agent.state !== "failed") {
564
- throw new Error(
565
- `Agent ${agentId} is ${agent.state} only stopped or failed agents can be resumed`
877
+ console.error(
878
+ `[ACP-over-MAP] _macro/resume: Agent ${agentId} is already ${agent.state}, returning current state`,
566
879
  );
880
+ return {
881
+ success: true,
882
+ agentId: agent.id,
883
+ sessionId: agent.session_id,
884
+ alreadyRunning: true,
885
+ };
567
886
  }
568
887
 
569
888
  const spawned = await this.agentManager.resume(agentId as AgentId);
@@ -575,7 +894,11 @@ export class ACPOverMAPHandler {
575
894
  }
576
895
 
577
896
  case "_macro/getHistory": {
578
- const { sessionId, agentId: historyAgentId, limit } = methodParams as {
897
+ const {
898
+ sessionId,
899
+ agentId: historyAgentId,
900
+ limit,
901
+ } = methodParams as {
579
902
  sessionId?: string;
580
903
  agentId?: string;
581
904
  limit?: number;
@@ -586,12 +909,12 @@ export class ACPOverMAPHandler {
586
909
  // explicit sessionId. This allows history to survive across server
587
910
  // restarts even when the ACP session ID changes (e.g., resume()
588
911
  // fails → TUI creates new session with different ID).
912
+ const agent = historyAgentId
913
+ ? this.eventStore.getAgent(historyAgentId as AgentId)
914
+ : undefined;
589
915
  let conversationId: string | undefined;
590
- if (historyAgentId) {
591
- const agent = this.eventStore.getAgent(historyAgentId as AgentId);
592
- if (agent) {
593
- conversationId = agent.session_id;
594
- }
916
+ if (agent) {
917
+ conversationId = agent.session_id;
595
918
  }
596
919
  if (!conversationId) {
597
920
  conversationId = sessionId;
@@ -600,11 +923,43 @@ export class ACPOverMAPHandler {
600
923
  return { turns: [] };
601
924
  }
602
925
 
603
- const turns = this.eventStore.listTurns({
604
- conversationId,
605
- order: "asc",
606
- limit: limit ?? 200,
607
- });
926
+ // For forked agents, include the source agent's conversation history
927
+ // (pre-fork turns) followed by this agent's own turns.
928
+ // Only include source turns from before the fork to avoid leaking
929
+ // turns that the source recorded after the fork point.
930
+ const sourceAgentId = agent?.metadata?.fork_of as string | undefined;
931
+ let turns;
932
+ if (sourceAgentId) {
933
+ const sourceAgent = this.eventStore.getAgent(
934
+ sourceAgentId as AgentId,
935
+ );
936
+ const sourceConversationId = sourceAgent?.session_id;
937
+ const forkTimestamp = agent!.created_at;
938
+ const sourceTurns = sourceConversationId
939
+ ? this.eventStore
940
+ .listTurns({
941
+ conversationId: sourceConversationId,
942
+ order: "asc",
943
+ limit: limit ?? 200,
944
+ })
945
+ .filter((t) => t.timestamp <= forkTimestamp)
946
+ : [];
947
+ const ownTurns = this.eventStore.listTurns({
948
+ conversationId,
949
+ order: "asc",
950
+ limit: limit ?? 200,
951
+ });
952
+ turns = [...sourceTurns, ...ownTurns];
953
+ } else {
954
+ turns = this.eventStore.listTurns({
955
+ conversationId,
956
+ order: "asc",
957
+ limit: limit ?? 200,
958
+ });
959
+ }
960
+
961
+ const plan = agent?.plan ?? [];
962
+
608
963
  return {
609
964
  turns: turns.map((turn) => ({
610
965
  role:
@@ -614,7 +969,341 @@ export class ACPOverMAPHandler {
614
969
  timestamp: turn.timestamp,
615
970
  content: turn.content,
616
971
  })),
972
+ plan,
973
+ cwd: agent?.cwd ?? null,
974
+ };
975
+ }
976
+
977
+ case "_macro/respondToPermission": {
978
+ const {
979
+ agentId: targetAgentId,
980
+ requestId,
981
+ optionId,
982
+ } = methodParams as {
983
+ agentId: string;
984
+ requestId: string;
985
+ optionId: string;
986
+ };
987
+ if (!targetAgentId || !requestId || !optionId) {
988
+ throw new Error("agentId, requestId, and optionId are required");
989
+ }
990
+ const success = this.agentManager.respondToPermission(
991
+ targetAgentId as AgentId,
992
+ requestId,
993
+ optionId,
994
+ );
995
+ return { success };
996
+ }
997
+
998
+ case "_macro/cancelPermission": {
999
+ const { agentId: targetAgentId, requestId } = methodParams as {
1000
+ agentId: string;
1001
+ requestId: string;
617
1002
  };
1003
+ if (!targetAgentId || !requestId) {
1004
+ throw new Error("agentId and requestId are required");
1005
+ }
1006
+ const success = this.agentManager.cancelPermission(
1007
+ targetAgentId as AgentId,
1008
+ requestId,
1009
+ );
1010
+ return { success };
1011
+ }
1012
+
1013
+ case "_macro/setPermissionMode": {
1014
+ const { agentId: targetAgentId, permissionMode } = methodParams as {
1015
+ agentId: string;
1016
+ permissionMode: string;
1017
+ };
1018
+ if (!targetAgentId || !permissionMode) {
1019
+ throw new Error("agentId and permissionMode are required");
1020
+ }
1021
+ const previousMode = this.agentManager.getPermissionMode(
1022
+ targetAgentId as AgentId,
1023
+ );
1024
+ const success = this.agentManager.setPermissionMode(
1025
+ targetAgentId as AgentId,
1026
+ permissionMode as
1027
+ | "auto-approve"
1028
+ | "auto-deny"
1029
+ | "callback"
1030
+ | "interactive",
1031
+ );
1032
+ if (success) {
1033
+ return { success: true, previousMode: previousMode ?? undefined };
1034
+ }
1035
+ return {
1036
+ success: false,
1037
+ error: `No active session found for agent ${targetAgentId}`,
1038
+ };
1039
+ }
1040
+
1041
+ case "_macro/forkAgent": {
1042
+ const { agentId, name, prompt, cwd } = methodParams as {
1043
+ agentId: string;
1044
+ name?: string;
1045
+ prompt?: string;
1046
+ cwd?: string;
1047
+ };
1048
+ if (!agentId) {
1049
+ throw new Error("agentId is required");
1050
+ }
1051
+
1052
+ const sourceAgent = this.eventStore.getAgent(agentId as AgentId);
1053
+ if (!sourceAgent) {
1054
+ throw new Error(`Agent not found: ${agentId}`);
1055
+ }
1056
+
1057
+ const forked = await this.agentManager.forkAgent(agentId as AgentId, {
1058
+ name,
1059
+ prompt,
1060
+ cwd: cwd ?? sourceAgent.cwd ?? this.defaultCwd,
1061
+ });
1062
+
1063
+ // Fire-and-forget initial prompt if provided
1064
+ if (prompt) {
1065
+ (async () => {
1066
+ try {
1067
+ for await (const _update of this.agentManager.prompt(
1068
+ forked.id,
1069
+ prompt,
1070
+ )) {
1071
+ // drain iterator
1072
+ }
1073
+ } catch {
1074
+ // best-effort
1075
+ }
1076
+ })();
1077
+ }
1078
+
1079
+ return {
1080
+ newAgentId: forked.id,
1081
+ newSessionId: forked.session_id,
1082
+ originalAgentId: agentId,
1083
+ providerSessionId: forked.session?.id,
1084
+ };
1085
+ }
1086
+
1087
+ case "_macro/agents/update": {
1088
+ const { agentId, name, plan, metadata } = methodParams as {
1089
+ agentId?: string;
1090
+ name?: string;
1091
+ plan?: Array<{ content: string; priority: string; status: string }>;
1092
+ metadata?: Record<string, unknown>;
1093
+ };
1094
+ if (!agentId) {
1095
+ throw new Error("agentId is required");
1096
+ }
1097
+ if (
1098
+ name === undefined &&
1099
+ plan === undefined &&
1100
+ metadata === undefined
1101
+ ) {
1102
+ throw new Error(
1103
+ "At least one field to update is required (name, plan, or metadata)",
1104
+ );
1105
+ }
1106
+ if (name !== undefined && !name.trim()) {
1107
+ throw new Error("name must not be empty");
1108
+ }
1109
+
1110
+ const agent = this.eventStore.getAgent(agentId as AgentId);
1111
+ if (!agent) {
1112
+ throw new Error(`Agent not found: ${agentId}`);
1113
+ }
1114
+
1115
+ const updates: Record<string, unknown> = {};
1116
+ const updatedFields: string[] = [];
1117
+
1118
+ if (name !== undefined) {
1119
+ updates.name = name.trim();
1120
+ updatedFields.push("name");
1121
+ }
1122
+ if (plan !== undefined) {
1123
+ updates.plan = plan;
1124
+ updatedFields.push("plan");
1125
+ }
1126
+ if (metadata !== undefined) {
1127
+ updates.metadata = metadata;
1128
+ updatedFields.push("metadata");
1129
+ }
1130
+
1131
+ this.eventStore.updateAgentMetadata(agentId as AgentId, updates);
1132
+
1133
+ return {
1134
+ success: true,
1135
+ agentId,
1136
+ updated: updatedFields,
1137
+ };
1138
+ }
1139
+
1140
+ case "_macro/getModels": {
1141
+ const { sessionId } = methodParams as { sessionId: string };
1142
+ const agentId = this.sessionMapper.getAgentId(
1143
+ sessionId as ACPSessionId,
1144
+ );
1145
+ if (!agentId) {
1146
+ return { currentModelId: null, availableModels: [] };
1147
+ }
1148
+ const session = this.agentManager.getSession(agentId);
1149
+ if (!session) {
1150
+ return { currentModelId: null, availableModels: [] };
1151
+ }
1152
+ // Try clientHandler's model info store first (from _model_state_update notification)
1153
+ const clientHandler = (
1154
+ session as unknown as {
1155
+ clientHandler?: {
1156
+ getSessionModelInfo?: (id: string) => {
1157
+ currentModelId: string | null;
1158
+ availableModels: Array<{ modelId: string; name: string }>;
1159
+ } | null;
1160
+ };
1161
+ }
1162
+ ).clientHandler;
1163
+ const modelInfo = clientHandler?.getSessionModelInfo?.(session.id);
1164
+ if (modelInfo && modelInfo.availableModels.length > 0) {
1165
+ return modelInfo;
1166
+ }
1167
+ // Fall back to Session.models (from initial session response — just IDs)
1168
+ if (session.models && session.models.length > 0) {
1169
+ return {
1170
+ currentModelId: session.models[0],
1171
+ availableModels: session.models.map((id: string) => ({
1172
+ modelId: id,
1173
+ name: id,
1174
+ })),
1175
+ };
1176
+ }
1177
+ return { currentModelId: null, availableModels: [] };
1178
+ }
1179
+
1180
+ case "_session/setCompaction": {
1181
+ // Compaction is handled internally by the agent process.
1182
+ // Accept the request as a no-op so the client doesn't get an error.
1183
+ // TODO: Make sure this overrides if needed.
1184
+ return { success: true };
1185
+ }
1186
+
1187
+ // ── Task Management Extensions ────────────────────────────────
1188
+ // These mirror the registered adapter extensions (_macro/task/*)
1189
+ // so they're accessible via ACP-over-MAP streams.
1190
+
1191
+ case "_macro/task/list": {
1192
+ const { filter } = (methodParams ?? {}) as {
1193
+ filter?: {
1194
+ status?: string;
1195
+ assignedAgent?: string;
1196
+ parentTask?: string;
1197
+ createdBy?: string;
1198
+ rootTasksOnly?: boolean;
1199
+ };
1200
+ };
1201
+
1202
+ const taskFilter: TaskFilter | undefined = filter
1203
+ ? {
1204
+ status: filter.status as TaskFilter["status"],
1205
+ assigned_agent: filter.assignedAgent as AgentId | undefined,
1206
+ parent_task: filter.parentTask as TaskId | undefined,
1207
+ created_by: filter.createdBy as AgentId | undefined,
1208
+ rootTasksOnly: filter.rootTasksOnly,
1209
+ }
1210
+ : undefined;
1211
+
1212
+ const tasks = this.taskManager.list(taskFilter);
1213
+ return {
1214
+ tasks: tasks.map((t) => ({
1215
+ id: t.id,
1216
+ description: t.description,
1217
+ status: t.status,
1218
+ assignedAgent: t.assigned_agent,
1219
+ createdBy: t.created_by,
1220
+ createdAt: t.created_at,
1221
+ parentTask: t.parent_task,
1222
+ isBlocked: (t as any).isBlocked,
1223
+ externalId: (t as any).external_id,
1224
+ })),
1225
+ };
1226
+ }
1227
+
1228
+ case "_macro/task/get": {
1229
+ const { taskId } = methodParams as { taskId: string };
1230
+ if (!taskId) throw new Error("taskId is required");
1231
+ const task = this.taskManager.get(taskId as TaskId);
1232
+ if (!task) throw new Error(`Task not found: ${taskId}`);
1233
+ return {
1234
+ task: {
1235
+ id: task.id,
1236
+ description: task.description,
1237
+ status: task.status,
1238
+ assignedAgent: task.assigned_agent,
1239
+ createdBy: task.created_by,
1240
+ createdAt: task.created_at,
1241
+ parentTask: task.parent_task,
1242
+ isBlocked: (task as any).isBlocked,
1243
+ externalId: (task as any).external_id,
1244
+ },
1245
+ };
1246
+ }
1247
+
1248
+ case "_macro/task/create": {
1249
+ const { description, parentTask, externalId } = methodParams as {
1250
+ description: string;
1251
+ parentTask?: string;
1252
+ externalId?: string;
1253
+ };
1254
+ if (!description) throw new Error("description is required");
1255
+
1256
+ // Determine who is creating the task — use the stream's agent or a default
1257
+ const createdBy = (streamState.agentId ?? "tui") as AgentId;
1258
+
1259
+ const task = this.taskManager.create({
1260
+ description,
1261
+ created_by: createdBy,
1262
+ parent_task: parentTask as TaskId | undefined,
1263
+ });
1264
+
1265
+ return {
1266
+ task: {
1267
+ id: task.id,
1268
+ description: task.description,
1269
+ status: task.status,
1270
+ assignedAgent: task.assigned_agent,
1271
+ createdBy: task.created_by,
1272
+ createdAt: task.created_at,
1273
+ parentTask: task.parent_task,
1274
+ },
1275
+ };
1276
+ }
1277
+
1278
+ case "_macro/task/assign": {
1279
+ const { taskId, agentId, role } = methodParams as {
1280
+ taskId: string;
1281
+ agentId: string;
1282
+ role?: string;
1283
+ };
1284
+ if (!taskId) throw new Error("taskId is required");
1285
+ if (!agentId) throw new Error("agentId is required");
1286
+
1287
+ this.taskManager.assign(taskId as TaskId, agentId as AgentId, role);
1288
+ return { success: true };
1289
+ }
1290
+
1291
+ case "_macro/task/complete": {
1292
+ const { taskId, outputs } = methodParams as {
1293
+ taskId: string;
1294
+ outputs?: { summary?: string; data?: unknown };
1295
+ };
1296
+ if (!taskId) throw new Error("taskId is required");
1297
+
1298
+ // Update status to completed
1299
+ this.taskManager.updateStatus(taskId as TaskId, "completed");
1300
+
1301
+ // If outputs provided, update task metadata
1302
+ if (outputs) {
1303
+ this.taskManager.update(taskId as TaskId, { outputs });
1304
+ }
1305
+
1306
+ return { success: true };
618
1307
  }
619
1308
 
620
1309
  default:
@@ -668,7 +1357,12 @@ export class ACPOverMAPHandler {
668
1357
  acpSessionId: ACPSessionId,
669
1358
  agentId: AgentId,
670
1359
  userMessage: string,
671
- buffer: { textChunks: string[]; toolCalls: Record<string, unknown>[] },
1360
+ buffer: {
1361
+ parts: Array<
1362
+ | { type: "text"; text: string }
1363
+ | ({ type: "tool" } & Record<string, unknown>)
1364
+ >;
1365
+ },
672
1366
  ): void {
673
1367
  const now = Date.now();
674
1368
 
@@ -691,17 +1385,8 @@ export class ACPOverMAPHandler {
691
1385
  });
692
1386
  }
693
1387
 
694
- // Record assistant turn with accumulated content
695
- const assistantText = buffer.textChunks.join("");
696
- const parts: unknown[] = [];
697
-
698
- if (assistantText) {
699
- parts.push({ type: "text", text: assistantText });
700
- }
701
-
702
- for (const tool of buffer.toolCalls) {
703
- parts.push({ type: "tool", ...tool });
704
- }
1388
+ // Record assistant turn with accumulated content (parts already in order)
1389
+ const parts = buffer.parts;
705
1390
 
706
1391
  if (parts.length > 0) {
707
1392
  this.eventStore.emit({