macro-agent 0.0.10 → 0.0.12

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 (518) hide show
  1. package/.macro-agent/teams/self-driving/prompts/grinder.md +27 -0
  2. package/.macro-agent/teams/self-driving/prompts/judge.md +27 -0
  3. package/.macro-agent/teams/self-driving/prompts/planner.md +33 -0
  4. package/.macro-agent/teams/self-driving/roles/grinder.yaml +17 -0
  5. package/.macro-agent/teams/self-driving/roles/judge.yaml +24 -0
  6. package/.macro-agent/teams/self-driving/roles/planner.yaml +18 -0
  7. package/.macro-agent/teams/self-driving/team.yaml +103 -0
  8. package/.macro-agent/teams/structured/prompts/developer.md +26 -0
  9. package/.macro-agent/teams/structured/prompts/lead.md +25 -0
  10. package/.macro-agent/teams/structured/prompts/reviewer.md +24 -0
  11. package/.macro-agent/teams/structured/roles/developer.yaml +12 -0
  12. package/.macro-agent/teams/structured/roles/lead.yaml +11 -0
  13. package/.macro-agent/teams/structured/roles/reviewer.yaml +19 -0
  14. package/.macro-agent/teams/structured/team.yaml +89 -0
  15. package/.sudocode/issues.jsonl +56 -51
  16. package/.sudocode/specs.jsonl +8 -1
  17. package/CLAUDE.md +121 -30
  18. package/README.md +60 -3
  19. package/dist/acp/macro-agent.d.ts +4 -0
  20. package/dist/acp/macro-agent.d.ts.map +1 -1
  21. package/dist/acp/macro-agent.js +50 -4
  22. package/dist/acp/macro-agent.js.map +1 -1
  23. package/dist/acp/session-mapper.d.ts +20 -1
  24. package/dist/acp/session-mapper.d.ts.map +1 -1
  25. package/dist/acp/session-mapper.js +90 -1
  26. package/dist/acp/session-mapper.js.map +1 -1
  27. package/dist/acp/types.d.ts +24 -1
  28. package/dist/acp/types.d.ts.map +1 -1
  29. package/dist/acp/types.js.map +1 -1
  30. package/dist/agent/agent-manager.d.ts +40 -1
  31. package/dist/agent/agent-manager.d.ts.map +1 -1
  32. package/dist/agent/agent-manager.js +172 -8
  33. package/dist/agent/agent-manager.js.map +1 -1
  34. package/dist/agent/types.d.ts +22 -0
  35. package/dist/agent/types.d.ts.map +1 -1
  36. package/dist/agent/types.js.map +1 -1
  37. package/dist/agent/wake.d.ts +15 -0
  38. package/dist/agent/wake.d.ts.map +1 -1
  39. package/dist/agent/wake.js +15 -0
  40. package/dist/agent/wake.js.map +1 -1
  41. package/dist/agent-detection/command-builder.d.ts +30 -0
  42. package/dist/agent-detection/command-builder.d.ts.map +1 -0
  43. package/dist/agent-detection/command-builder.js +71 -0
  44. package/dist/agent-detection/command-builder.js.map +1 -0
  45. package/dist/agent-detection/detector.d.ts +84 -0
  46. package/dist/agent-detection/detector.d.ts.map +1 -0
  47. package/dist/agent-detection/detector.js +240 -0
  48. package/dist/agent-detection/detector.js.map +1 -0
  49. package/dist/agent-detection/index.d.ts +12 -0
  50. package/dist/agent-detection/index.d.ts.map +1 -0
  51. package/dist/agent-detection/index.js +14 -0
  52. package/dist/agent-detection/index.js.map +1 -0
  53. package/dist/agent-detection/registry.d.ts +53 -0
  54. package/dist/agent-detection/registry.d.ts.map +1 -0
  55. package/dist/agent-detection/registry.js +177 -0
  56. package/dist/agent-detection/registry.js.map +1 -0
  57. package/dist/agent-detection/types.d.ts +121 -0
  58. package/dist/agent-detection/types.d.ts.map +1 -0
  59. package/dist/agent-detection/types.js +20 -0
  60. package/dist/agent-detection/types.js.map +1 -0
  61. package/dist/api/server.d.ts +5 -1
  62. package/dist/api/server.d.ts.map +1 -1
  63. package/dist/api/server.js +362 -0
  64. package/dist/api/server.js.map +1 -1
  65. package/dist/api/types.d.ts +50 -1
  66. package/dist/api/types.d.ts.map +1 -1
  67. package/dist/cli/acp.d.ts +2 -0
  68. package/dist/cli/acp.d.ts.map +1 -1
  69. package/dist/cli/acp.js +8 -1
  70. package/dist/cli/acp.js.map +1 -1
  71. package/dist/cli/index.js +29 -0
  72. package/dist/cli/index.js.map +1 -1
  73. package/dist/cli/mcp.js +38 -0
  74. package/dist/cli/mcp.js.map +1 -1
  75. package/dist/config/index.d.ts +2 -0
  76. package/dist/config/index.d.ts.map +1 -0
  77. package/dist/config/index.js +2 -0
  78. package/dist/config/index.js.map +1 -0
  79. package/dist/config/project-config.d.ts +46 -0
  80. package/dist/config/project-config.d.ts.map +1 -0
  81. package/dist/config/project-config.js +68 -0
  82. package/dist/config/project-config.js.map +1 -0
  83. package/dist/lifecycle/cascade.d.ts +1 -1
  84. package/dist/lifecycle/cascade.d.ts.map +1 -1
  85. package/dist/lifecycle/handlers/index.d.ts +4 -0
  86. package/dist/lifecycle/handlers/index.d.ts.map +1 -1
  87. package/dist/lifecycle/handlers/index.js +2 -0
  88. package/dist/lifecycle/handlers/index.js.map +1 -1
  89. package/dist/lifecycle/handlers/worker.d.ts +4 -0
  90. package/dist/lifecycle/handlers/worker.d.ts.map +1 -1
  91. package/dist/lifecycle/handlers/worker.js +35 -3
  92. package/dist/lifecycle/handlers/worker.js.map +1 -1
  93. package/dist/mail/conversation-map.d.ts +33 -0
  94. package/dist/mail/conversation-map.d.ts.map +1 -0
  95. package/dist/mail/conversation-map.js +61 -0
  96. package/dist/mail/conversation-map.js.map +1 -0
  97. package/dist/mail/index.d.ts +11 -0
  98. package/dist/mail/index.d.ts.map +1 -0
  99. package/dist/mail/index.js +11 -0
  100. package/dist/mail/index.js.map +1 -0
  101. package/dist/mail/mail-service.d.ts +85 -0
  102. package/dist/mail/mail-service.d.ts.map +1 -0
  103. package/dist/mail/mail-service.js +121 -0
  104. package/dist/mail/mail-service.js.map +1 -0
  105. package/dist/mail/stores/eventstore-conversation-store.d.ts +40 -0
  106. package/dist/mail/stores/eventstore-conversation-store.d.ts.map +1 -0
  107. package/dist/mail/stores/eventstore-conversation-store.js +131 -0
  108. package/dist/mail/stores/eventstore-conversation-store.js.map +1 -0
  109. package/dist/mail/stores/eventstore-participant-store.d.ts +43 -0
  110. package/dist/mail/stores/eventstore-participant-store.d.ts.map +1 -0
  111. package/dist/mail/stores/eventstore-participant-store.js +145 -0
  112. package/dist/mail/stores/eventstore-participant-store.js.map +1 -0
  113. package/dist/mail/stores/eventstore-thread-store.d.ts +46 -0
  114. package/dist/mail/stores/eventstore-thread-store.d.ts.map +1 -0
  115. package/dist/mail/stores/eventstore-thread-store.js +118 -0
  116. package/dist/mail/stores/eventstore-thread-store.js.map +1 -0
  117. package/dist/mail/stores/eventstore-turn-store.d.ts +47 -0
  118. package/dist/mail/stores/eventstore-turn-store.d.ts.map +1 -0
  119. package/dist/mail/stores/eventstore-turn-store.js +153 -0
  120. package/dist/mail/stores/eventstore-turn-store.js.map +1 -0
  121. package/dist/mail/stores/index.d.ts +12 -0
  122. package/dist/mail/stores/index.d.ts.map +1 -0
  123. package/dist/mail/stores/index.js +12 -0
  124. package/dist/mail/stores/index.js.map +1 -0
  125. package/dist/mail/stores/types.d.ts +146 -0
  126. package/dist/mail/stores/types.d.ts.map +1 -0
  127. package/dist/mail/stores/types.js +13 -0
  128. package/dist/mail/stores/types.js.map +1 -0
  129. package/dist/mail/turn-recorder.d.ts +30 -0
  130. package/dist/mail/turn-recorder.d.ts.map +1 -0
  131. package/dist/mail/turn-recorder.js +98 -0
  132. package/dist/mail/turn-recorder.js.map +1 -0
  133. package/dist/map/adapter/acp-over-map.d.ts.map +1 -1
  134. package/dist/map/adapter/acp-over-map.js +32 -2
  135. package/dist/map/adapter/acp-over-map.js.map +1 -1
  136. package/dist/map/adapter/event-translator.d.ts.map +1 -1
  137. package/dist/map/adapter/event-translator.js +4 -0
  138. package/dist/map/adapter/event-translator.js.map +1 -1
  139. package/dist/map/adapter/extensions/agent-detection.d.ts +49 -0
  140. package/dist/map/adapter/extensions/agent-detection.d.ts.map +1 -0
  141. package/dist/map/adapter/extensions/agent-detection.js +91 -0
  142. package/dist/map/adapter/extensions/agent-detection.js.map +1 -0
  143. package/dist/map/adapter/extensions/index.d.ts +10 -1
  144. package/dist/map/adapter/extensions/index.d.ts.map +1 -1
  145. package/dist/map/adapter/extensions/index.js +39 -0
  146. package/dist/map/adapter/extensions/index.js.map +1 -1
  147. package/dist/map/adapter/extensions/resume.d.ts +47 -0
  148. package/dist/map/adapter/extensions/resume.d.ts.map +1 -0
  149. package/dist/map/adapter/extensions/resume.js +59 -0
  150. package/dist/map/adapter/extensions/resume.js.map +1 -0
  151. package/dist/map/adapter/extensions/workspace-files.d.ts +42 -0
  152. package/dist/map/adapter/extensions/workspace-files.d.ts.map +1 -0
  153. package/dist/map/adapter/extensions/workspace-files.js +338 -0
  154. package/dist/map/adapter/extensions/workspace-files.js.map +1 -0
  155. package/dist/map/adapter/mail-handler-adapter.d.ts +27 -0
  156. package/dist/map/adapter/mail-handler-adapter.d.ts.map +1 -0
  157. package/dist/map/adapter/mail-handler-adapter.js +292 -0
  158. package/dist/map/adapter/mail-handler-adapter.js.map +1 -0
  159. package/dist/map/adapter/map-adapter.d.ts +34 -10
  160. package/dist/map/adapter/map-adapter.d.ts.map +1 -1
  161. package/dist/map/adapter/map-adapter.js +110 -14
  162. package/dist/map/adapter/map-adapter.js.map +1 -1
  163. package/dist/map/adapter/rpc-handler.d.ts +4 -1
  164. package/dist/map/adapter/rpc-handler.d.ts.map +1 -1
  165. package/dist/map/adapter/rpc-handler.js +6 -0
  166. package/dist/map/adapter/rpc-handler.js.map +1 -1
  167. package/dist/map/index.d.ts +1 -0
  168. package/dist/map/index.d.ts.map +1 -1
  169. package/dist/map/index.js +2 -0
  170. package/dist/map/index.js.map +1 -1
  171. package/dist/map/types.d.ts +3 -1
  172. package/dist/map/types.d.ts.map +1 -1
  173. package/dist/map/types.js.map +1 -1
  174. package/dist/mcp/mcp-server.d.ts +6 -0
  175. package/dist/mcp/mcp-server.d.ts.map +1 -1
  176. package/dist/mcp/mcp-server.js +45 -0
  177. package/dist/mcp/mcp-server.js.map +1 -1
  178. package/dist/mcp/tools/claim_task.d.ts +35 -0
  179. package/dist/mcp/tools/claim_task.d.ts.map +1 -0
  180. package/dist/mcp/tools/claim_task.js +58 -0
  181. package/dist/mcp/tools/claim_task.js.map +1 -0
  182. package/dist/mcp/tools/done.d.ts +15 -2
  183. package/dist/mcp/tools/done.d.ts.map +1 -1
  184. package/dist/mcp/tools/done.js +45 -10
  185. package/dist/mcp/tools/done.js.map +1 -1
  186. package/dist/mcp/tools/list_claimable_tasks.d.ts +38 -0
  187. package/dist/mcp/tools/list_claimable_tasks.d.ts.map +1 -0
  188. package/dist/mcp/tools/list_claimable_tasks.js +63 -0
  189. package/dist/mcp/tools/list_claimable_tasks.js.map +1 -0
  190. package/dist/mcp/tools/unclaim_task.d.ts +31 -0
  191. package/dist/mcp/tools/unclaim_task.d.ts.map +1 -0
  192. package/dist/mcp/tools/unclaim_task.js +47 -0
  193. package/dist/mcp/tools/unclaim_task.js.map +1 -0
  194. package/dist/metrics/index.d.ts +2 -0
  195. package/dist/metrics/index.d.ts.map +1 -0
  196. package/dist/metrics/index.js +2 -0
  197. package/dist/metrics/index.js.map +1 -0
  198. package/dist/metrics/metrics.d.ts +79 -0
  199. package/dist/metrics/metrics.d.ts.map +1 -0
  200. package/dist/metrics/metrics.js +166 -0
  201. package/dist/metrics/metrics.js.map +1 -0
  202. package/dist/roles/capabilities.d.ts +1 -0
  203. package/dist/roles/capabilities.d.ts.map +1 -1
  204. package/dist/roles/capabilities.js +3 -0
  205. package/dist/roles/capabilities.js.map +1 -1
  206. package/dist/roles/types.d.ts +1 -1
  207. package/dist/roles/types.d.ts.map +1 -1
  208. package/dist/router/channels.d.ts +2 -4
  209. package/dist/router/channels.d.ts.map +1 -1
  210. package/dist/router/channels.js.map +1 -1
  211. package/dist/router/message-router.d.ts +85 -9
  212. package/dist/router/message-router.d.ts.map +1 -1
  213. package/dist/router/message-router.js +203 -14
  214. package/dist/router/message-router.js.map +1 -1
  215. package/dist/router/role-resolver.d.ts +10 -1
  216. package/dist/router/role-resolver.d.ts.map +1 -1
  217. package/dist/router/role-resolver.js +15 -1
  218. package/dist/router/role-resolver.js.map +1 -1
  219. package/dist/router/types.d.ts +30 -1
  220. package/dist/router/types.d.ts.map +1 -1
  221. package/dist/router/types.js.map +1 -1
  222. package/dist/server/combined-server.d.ts +6 -0
  223. package/dist/server/combined-server.d.ts.map +1 -1
  224. package/dist/server/combined-server.js +24 -2
  225. package/dist/server/combined-server.js.map +1 -1
  226. package/dist/store/event-store.d.ts +14 -1
  227. package/dist/store/event-store.d.ts.map +1 -1
  228. package/dist/store/event-store.js +456 -4
  229. package/dist/store/event-store.js.map +1 -1
  230. package/dist/store/types/agents.d.ts +1 -1
  231. package/dist/store/types/agents.d.ts.map +1 -1
  232. package/dist/store/types/conversations.d.ts +91 -0
  233. package/dist/store/types/conversations.d.ts.map +1 -0
  234. package/dist/store/types/conversations.js +8 -0
  235. package/dist/store/types/conversations.js.map +1 -0
  236. package/dist/store/types/events.d.ts +1 -1
  237. package/dist/store/types/events.d.ts.map +1 -1
  238. package/dist/store/types/events.js.map +1 -1
  239. package/dist/store/types/index.d.ts +2 -0
  240. package/dist/store/types/index.d.ts.map +1 -1
  241. package/dist/store/types/index.js +2 -0
  242. package/dist/store/types/index.js.map +1 -1
  243. package/dist/store/types/sessions.d.ts +44 -0
  244. package/dist/store/types/sessions.d.ts.map +1 -0
  245. package/dist/store/types/sessions.js +9 -0
  246. package/dist/store/types/sessions.js.map +1 -0
  247. package/dist/store/types/tasks.d.ts +2 -0
  248. package/dist/store/types/tasks.d.ts.map +1 -1
  249. package/dist/task/backend/memory.d.ts +4 -1
  250. package/dist/task/backend/memory.d.ts.map +1 -1
  251. package/dist/task/backend/memory.js +81 -0
  252. package/dist/task/backend/memory.js.map +1 -1
  253. package/dist/task/backend/types.d.ts +30 -0
  254. package/dist/task/backend/types.d.ts.map +1 -1
  255. package/dist/task/backend/types.js.map +1 -1
  256. package/dist/teams/index.d.ts +4 -0
  257. package/dist/teams/index.d.ts.map +1 -0
  258. package/dist/teams/index.js +4 -0
  259. package/dist/teams/index.js.map +1 -0
  260. package/dist/teams/team-loader.d.ts +20 -0
  261. package/dist/teams/team-loader.d.ts.map +1 -0
  262. package/dist/teams/team-loader.js +293 -0
  263. package/dist/teams/team-loader.js.map +1 -0
  264. package/dist/teams/team-runtime.d.ts +139 -0
  265. package/dist/teams/team-runtime.d.ts.map +1 -0
  266. package/dist/teams/team-runtime.js +613 -0
  267. package/dist/teams/team-runtime.js.map +1 -0
  268. package/dist/teams/types.d.ts +266 -0
  269. package/dist/teams/types.d.ts.map +1 -0
  270. package/dist/teams/types.js +20 -0
  271. package/dist/teams/types.js.map +1 -0
  272. package/dist/trigger/router/trigger-router.d.ts +30 -3
  273. package/dist/trigger/router/trigger-router.d.ts.map +1 -1
  274. package/dist/trigger/router/trigger-router.js +30 -3
  275. package/dist/trigger/router/trigger-router.js.map +1 -1
  276. package/dist/trigger/wake/types.d.ts +31 -5
  277. package/dist/trigger/wake/types.d.ts.map +1 -1
  278. package/dist/trigger/wake/types.js +19 -0
  279. package/dist/trigger/wake/types.js.map +1 -1
  280. package/dist/workspace/dataplane-adapter.d.ts +1 -1
  281. package/dist/workspace/dataplane-adapter.d.ts.map +1 -1
  282. package/dist/workspace/dataplane-adapter.js +1 -1
  283. package/dist/workspace/dataplane-adapter.js.map +1 -1
  284. package/dist/workspace/index.d.ts +1 -1
  285. package/dist/workspace/index.d.ts.map +1 -1
  286. package/dist/workspace/strategies/index.d.ts +6 -0
  287. package/dist/workspace/strategies/index.d.ts.map +1 -0
  288. package/dist/workspace/strategies/index.js +5 -0
  289. package/dist/workspace/strategies/index.js.map +1 -0
  290. package/dist/workspace/strategies/optimistic.d.ts +26 -0
  291. package/dist/workspace/strategies/optimistic.d.ts.map +1 -0
  292. package/dist/workspace/strategies/optimistic.js +121 -0
  293. package/dist/workspace/strategies/optimistic.js.map +1 -0
  294. package/dist/workspace/strategies/queue.d.ts +26 -0
  295. package/dist/workspace/strategies/queue.d.ts.map +1 -0
  296. package/dist/workspace/strategies/queue.js +67 -0
  297. package/dist/workspace/strategies/queue.js.map +1 -0
  298. package/dist/workspace/strategies/registry.d.ts +37 -0
  299. package/dist/workspace/strategies/registry.d.ts.map +1 -0
  300. package/dist/workspace/strategies/registry.js +63 -0
  301. package/dist/workspace/strategies/registry.js.map +1 -0
  302. package/dist/workspace/strategies/trunk.d.ts +20 -0
  303. package/dist/workspace/strategies/trunk.d.ts.map +1 -0
  304. package/dist/workspace/strategies/trunk.js +108 -0
  305. package/dist/workspace/strategies/trunk.js.map +1 -0
  306. package/dist/workspace/strategies/types.d.ts +104 -0
  307. package/dist/workspace/strategies/types.d.ts.map +1 -0
  308. package/dist/workspace/strategies/types.js +11 -0
  309. package/dist/workspace/strategies/types.js.map +1 -0
  310. package/dist/workspace/types.d.ts +1 -1
  311. package/dist/workspace/types.d.ts.map +1 -1
  312. package/dist/workspace/workspace-manager.d.ts +1 -1
  313. package/dist/workspace/workspace-manager.d.ts.map +1 -1
  314. package/docs/implementation-details.md +1127 -0
  315. package/docs/implementation-summary.md +448 -0
  316. package/docs/mail-integration.md +608 -0
  317. package/docs/plan-self-driving-support.md +433 -0
  318. package/docs/spec-self-driving-support.md +462 -0
  319. package/docs/team-templates.md +860 -0
  320. package/docs/teams.md +233 -0
  321. package/package.json +5 -3
  322. package/src/acp/__tests__/integration.test.ts +161 -1
  323. package/src/acp/__tests__/macro-agent.test.ts +95 -0
  324. package/src/acp/__tests__/session-persistence.test.ts +276 -0
  325. package/src/acp/macro-agent.ts +79 -7
  326. package/src/acp/session-mapper.ts +108 -1
  327. package/src/acp/types.ts +33 -1
  328. package/src/agent/agent-manager.ts +278 -6
  329. package/src/agent/types.ts +27 -0
  330. package/src/agent/wake.ts +15 -0
  331. package/src/agent-detection/__tests__/command-builder.test.ts +336 -0
  332. package/src/agent-detection/__tests__/detector.test.ts +768 -0
  333. package/src/agent-detection/__tests__/registry.test.ts +254 -0
  334. package/src/agent-detection/command-builder.ts +90 -0
  335. package/src/agent-detection/detector.ts +307 -0
  336. package/src/agent-detection/index.ts +36 -0
  337. package/src/agent-detection/registry.ts +200 -0
  338. package/src/agent-detection/types.ts +184 -0
  339. package/src/api/__tests__/conversation-api.test.ts +468 -0
  340. package/src/api/server.ts +425 -1
  341. package/src/api/types.ts +64 -1
  342. package/src/cli/acp.ts +9 -1
  343. package/src/cli/index.ts +44 -0
  344. package/src/cli/mcp.ts +47 -0
  345. package/src/config/index.ts +9 -0
  346. package/src/config/project-config.ts +107 -0
  347. package/src/lifecycle/cascade.ts +1 -1
  348. package/src/lifecycle/handlers/index.ts +8 -0
  349. package/src/lifecycle/handlers/worker.ts +48 -3
  350. package/src/mail/__tests__/conversation-lifecycle.test.ts +409 -0
  351. package/src/mail/__tests__/eventstore-stores.test.ts +1073 -0
  352. package/src/mail/__tests__/mail-full-agent.e2e.test.ts +575 -0
  353. package/src/mail/__tests__/mail-integration.test.ts +759 -0
  354. package/src/mail/__tests__/mail-map-protocol.e2e.test.ts +1068 -0
  355. package/src/mail/__tests__/mail-service.test.ts +506 -0
  356. package/src/mail/__tests__/turn-recorder.test.ts +328 -0
  357. package/src/mail/conversation-map.ts +107 -0
  358. package/src/mail/index.ts +25 -0
  359. package/src/mail/mail-service.ts +257 -0
  360. package/src/mail/stores/eventstore-conversation-store.ts +146 -0
  361. package/src/mail/stores/eventstore-participant-store.ts +172 -0
  362. package/src/mail/stores/eventstore-thread-store.ts +129 -0
  363. package/src/mail/stores/eventstore-turn-store.ts +173 -0
  364. package/src/mail/stores/index.ts +12 -0
  365. package/src/mail/stores/types.ts +160 -0
  366. package/src/mail/turn-recorder.ts +124 -0
  367. package/src/map/README.md +79 -0
  368. package/src/map/adapter/__tests__/extensions.test.ts +359 -0
  369. package/src/map/adapter/__tests__/map-adapter.test.ts +90 -0
  370. package/src/map/adapter/__tests__/workspace-files.test.ts +673 -0
  371. package/src/map/adapter/acp-over-map.ts +45 -2
  372. package/src/map/adapter/event-translator.ts +4 -0
  373. package/src/map/adapter/extensions/agent-detection.ts +201 -0
  374. package/src/map/adapter/extensions/index.ts +63 -0
  375. package/src/map/adapter/extensions/resume.ts +114 -0
  376. package/src/map/adapter/extensions/workspace-files.ts +449 -0
  377. package/src/map/adapter/mail-handler-adapter.ts +429 -0
  378. package/src/map/adapter/map-adapter.ts +173 -27
  379. package/src/map/adapter/rpc-handler.ts +8 -1
  380. package/src/map/index.ts +3 -0
  381. package/src/map/types.ts +3 -1
  382. package/src/mcp/mcp-server.ts +67 -0
  383. package/src/mcp/tools/claim_task.ts +86 -0
  384. package/src/mcp/tools/done.ts +59 -10
  385. package/src/mcp/tools/list_claimable_tasks.ts +93 -0
  386. package/src/mcp/tools/unclaim_task.ts +71 -0
  387. package/src/metrics/index.ts +9 -0
  388. package/src/metrics/metrics.ts +280 -0
  389. package/src/roles/capabilities.ts +3 -0
  390. package/src/roles/types.ts +2 -1
  391. package/src/router/README.md +120 -0
  392. package/src/router/__tests__/message-router.test.ts +561 -0
  393. package/src/router/channels.ts +3 -4
  394. package/src/router/message-router.ts +308 -22
  395. package/src/router/role-resolver.ts +22 -1
  396. package/src/router/types.ts +36 -1
  397. package/src/server/combined-server.ts +36 -2
  398. package/src/store/README.md +134 -0
  399. package/src/store/event-store.ts +546 -3
  400. package/src/store/types/agents.ts +1 -1
  401. package/src/store/types/conversations.ts +129 -0
  402. package/src/store/types/events.ts +5 -1
  403. package/src/store/types/index.ts +2 -0
  404. package/src/store/types/sessions.ts +53 -0
  405. package/src/store/types/tasks.ts +3 -0
  406. package/src/task/backend/memory.ts +116 -0
  407. package/src/task/backend/types.ts +43 -0
  408. package/src/teams/__tests__/cross-subsystem.integration.test.ts +983 -0
  409. package/src/teams/__tests__/e2e/team-runtime.e2e.test.ts +553 -0
  410. package/src/teams/__tests__/team-system.test.ts +1280 -0
  411. package/src/teams/index.ts +13 -0
  412. package/src/teams/team-loader.ts +434 -0
  413. package/src/teams/team-runtime.ts +727 -0
  414. package/src/teams/types.ts +377 -0
  415. package/src/trigger/router/trigger-router.ts +30 -3
  416. package/src/trigger/wake/types.ts +32 -5
  417. package/src/trigger/wake/wake-manager.ts +2 -2
  418. package/src/workspace/dataplane-adapter.ts +1 -1
  419. package/src/workspace/index.ts +1 -1
  420. package/src/workspace/strategies/index.ts +18 -0
  421. package/src/workspace/strategies/optimistic.ts +136 -0
  422. package/src/workspace/strategies/queue.ts +81 -0
  423. package/src/workspace/strategies/registry.ts +89 -0
  424. package/src/workspace/strategies/trunk.ts +123 -0
  425. package/src/workspace/strategies/types.ts +145 -0
  426. package/src/workspace/types.ts +1 -1
  427. package/src/workspace/workspace-manager.ts +1 -1
  428. package/.claude/settings.local.json +0 -59
  429. package/dist/map/utils/address-translation.d.ts +0 -99
  430. package/dist/map/utils/address-translation.d.ts.map +0 -1
  431. package/dist/map/utils/address-translation.js +0 -285
  432. package/dist/map/utils/address-translation.js.map +0 -1
  433. package/dist/map/utils/index.d.ts +0 -7
  434. package/dist/map/utils/index.d.ts.map +0 -1
  435. package/dist/map/utils/index.js +0 -7
  436. package/dist/map/utils/index.js.map +0 -1
  437. package/openspec/AGENTS.md +0 -456
  438. package/openspec/changes/archive/2025-12-21-add-mvp-foundation/design.md +0 -128
  439. package/openspec/changes/archive/2025-12-21-add-mvp-foundation/proposal.md +0 -49
  440. package/openspec/changes/archive/2025-12-21-add-mvp-foundation/specs/agent-manager/spec.md +0 -150
  441. package/openspec/changes/archive/2025-12-21-add-mvp-foundation/specs/cli-api/spec.md +0 -258
  442. package/openspec/changes/archive/2025-12-21-add-mvp-foundation/specs/event-store/spec.md +0 -160
  443. package/openspec/changes/archive/2025-12-21-add-mvp-foundation/specs/mcp-tools/spec.md +0 -224
  444. package/openspec/changes/archive/2025-12-21-add-mvp-foundation/specs/message-router/spec.md +0 -153
  445. package/openspec/changes/archive/2025-12-21-add-mvp-foundation/specs/task-manager/spec.md +0 -136
  446. package/openspec/changes/archive/2025-12-21-add-mvp-foundation/tasks.md +0 -147
  447. package/openspec/project.md +0 -31
  448. package/openspec/specs/agent-manager/spec.md +0 -154
  449. package/openspec/specs/cli-api/spec.md +0 -262
  450. package/openspec/specs/event-store/spec.md +0 -164
  451. package/openspec/specs/mcp-tools/spec.md +0 -228
  452. package/openspec/specs/message-router/spec.md +0 -157
  453. package/openspec/specs/task-manager/spec.md +0 -140
  454. package/references/acp-factory-ref/CHANGELOG.md +0 -33
  455. package/references/acp-factory-ref/LICENSE +0 -21
  456. package/references/acp-factory-ref/README.md +0 -341
  457. package/references/acp-factory-ref/package-lock.json +0 -3102
  458. package/references/acp-factory-ref/package.json +0 -96
  459. package/references/acp-factory-ref/python/CHANGELOG.md +0 -33
  460. package/references/acp-factory-ref/python/LICENSE +0 -21
  461. package/references/acp-factory-ref/python/Makefile +0 -57
  462. package/references/acp-factory-ref/python/README.md +0 -253
  463. package/references/acp-factory-ref/python/pyproject.toml +0 -73
  464. package/references/acp-factory-ref/python/tests/__init__.py +0 -0
  465. package/references/acp-factory-ref/python/tests/e2e/__init__.py +0 -1
  466. package/references/acp-factory-ref/python/tests/e2e/test_codex_e2e.py +0 -349
  467. package/references/acp-factory-ref/python/tests/e2e/test_gemini_e2e.py +0 -165
  468. package/references/acp-factory-ref/python/tests/e2e/test_opencode_e2e.py +0 -296
  469. package/references/acp-factory-ref/python/tests/test_client_handler.py +0 -543
  470. package/references/acp-factory-ref/python/tests/test_pushable.py +0 -199
  471. package/references/claude-code-acp/.github/workflows/ci.yml +0 -45
  472. package/references/claude-code-acp/.github/workflows/publish.yml +0 -34
  473. package/references/claude-code-acp/.prettierrc.json +0 -4
  474. package/references/claude-code-acp/CHANGELOG.md +0 -249
  475. package/references/claude-code-acp/LICENSE +0 -222
  476. package/references/claude-code-acp/README.md +0 -53
  477. package/references/claude-code-acp/docs/RELEASES.md +0 -24
  478. package/references/claude-code-acp/eslint.config.js +0 -48
  479. package/references/claude-code-acp/package-lock.json +0 -4570
  480. package/references/claude-code-acp/package.json +0 -88
  481. package/references/claude-code-acp/scripts/release.sh +0 -119
  482. package/references/claude-code-acp/src/acp-agent.ts +0 -2065
  483. package/references/claude-code-acp/src/index.ts +0 -26
  484. package/references/claude-code-acp/src/lib.ts +0 -38
  485. package/references/claude-code-acp/src/mcp-server.ts +0 -911
  486. package/references/claude-code-acp/src/settings.ts +0 -522
  487. package/references/claude-code-acp/src/tests/.claude/commands/quick-math.md +0 -5
  488. package/references/claude-code-acp/src/tests/.claude/commands/say-hello.md +0 -6
  489. package/references/claude-code-acp/src/tests/acp-agent-fork.test.ts +0 -479
  490. package/references/claude-code-acp/src/tests/acp-agent.test.ts +0 -1502
  491. package/references/claude-code-acp/src/tests/extract-lines.test.ts +0 -103
  492. package/references/claude-code-acp/src/tests/fork-session.test.ts +0 -335
  493. package/references/claude-code-acp/src/tests/replace-and-calculate-location.test.ts +0 -334
  494. package/references/claude-code-acp/src/tests/settings.test.ts +0 -617
  495. package/references/claude-code-acp/src/tests/skills-options.test.ts +0 -187
  496. package/references/claude-code-acp/src/tests/tools.test.ts +0 -318
  497. package/references/claude-code-acp/src/tests/typescript-declarations.test.ts +0 -558
  498. package/references/claude-code-acp/src/tools.ts +0 -819
  499. package/references/claude-code-acp/src/utils.ts +0 -171
  500. package/references/claude-code-acp/tsconfig.json +0 -18
  501. package/references/claude-code-acp/vitest.config.ts +0 -19
  502. package/references/multi-agent-protocol/.sudocode/issues.jsonl +0 -82
  503. package/references/multi-agent-protocol/.sudocode/specs.jsonl +0 -9
  504. package/references/multi-agent-protocol/LICENSE +0 -21
  505. package/references/multi-agent-protocol/README.md +0 -113
  506. package/references/multi-agent-protocol/docs/00-design-specification.md +0 -460
  507. package/references/multi-agent-protocol/docs/01-open-questions.md +0 -1050
  508. package/references/multi-agent-protocol/docs/02-wire-protocol.md +0 -296
  509. package/references/multi-agent-protocol/docs/03-streaming-semantics.md +0 -252
  510. package/references/multi-agent-protocol/docs/04-error-handling.md +0 -231
  511. package/references/multi-agent-protocol/docs/05-connection-model.md +0 -244
  512. package/references/multi-agent-protocol/docs/06-visibility-permissions.md +0 -243
  513. package/references/multi-agent-protocol/docs/07-federation.md +0 -259
  514. package/references/multi-agent-protocol/docs/08-macro-agent-migration.md +0 -253
  515. package/references/multi-agent-protocol/package-lock.json +0 -3239
  516. package/references/multi-agent-protocol/package.json +0 -56
  517. package/references/multi-agent-protocol/schema/meta.json +0 -337
  518. package/references/multi-agent-protocol/schema/schema.json +0 -1828
@@ -1,9 +0,0 @@
1
- {"id":"s-4cts","uuid":"d1212471-4731-4b05-8ab3-cc79b931bede","title":"P1 Implementation: Streaming, Permissions, Federation","file_path":"specs/s-4cts_p1_implementation_streaming_permissions_federation.md","content":"# P1 Implementation: Streaming, Permissions, Federation\n\nThis spec captures all design decisions, requirements, and implementation details for P1 gaps in the MAP SDK.\n\n**Status**: Approved for Implementation\n**Design Doc**: `ts-sdk/docs/p1-implementation-plan.md`\n\n---\n\n## Table of Contents\n\n1. [Overview](#overview)\n2. [Streaming: Pause/Resume, Overflow, Backpressure](#streaming)\n3. [Permissions: Agent-Level Permissions](#agent-permissions)\n4. [Permissions: Dynamic Updates](#dynamic-permissions)\n5. [Federation: Envelope & Routing](#federation-envelope)\n6. [Federation: Reconnection & Recovery](#federation-reconnection)\n7. [Implementation Phases](#implementation-phases)\n8. [Breaking Changes](#breaking-changes)\n\n---\n\n## Overview\n\n### Gaps Addressed\n\n| ID | Gap | Area |\n|----|-----|------|\n| STREAM-004 | Pause/resume subscriptions | Streaming |\n| STREAM-005 | Overflow notifications | Streaming |\n| STREAM-006 | Backpressure acknowledgment | Streaming |\n| PERM-004 | Agent-level permissions | Permissions |\n| PERM-005 | Dynamic permission updates | Permissions |\n| FED-001 | Federation envelope | Federation |\n| FED-003 | Message queuing during outages | Federation |\n| FED-004 | Federation auto-reconnect | Federation |\n\n### Key Design Decisions\n\n1. **Backpressure**: Optionally implemented on both client and server\n2. **Agent Permissions**: Hybrid approach (role defaults + per-agent overrides)\n3. **Permission Updates**: Flow 1 (system→client) + Flow 4 (owner→agent)\n4. **Federation Envelope**: Full routing metadata, signing deferred\n5. **Federation Recovery**: Outage buffer + event store replay by timestamp\n6. **Error Handling**: `PERMISSION_DENIED` for direct messages, silent drop for broadcasts\n\n---\n\n## Streaming\n\n### Requirements\n\n| ID | Requirement | Priority |\n|----|-------------|----------|\n| S1 | Client can explicitly pause/resume event consumption | Must |\n| S2 | Client receives overflow notifications when events are dropped | Must |\n| S3 | Server can optionally implement flow control via acknowledgments | Should |\n| S4 | Client can optionally send acknowledgments if server supports it | Should |\n| S5 | Backpressure is opt-in for both client and server | Must |\n\n### Type Definitions\n\n```typescript\n// Server capability advertisement\ninterface StreamingCapabilities {\n supportsAck?: boolean;\n supportsFlowControl?: boolean;\n supportsPause?: boolean;\n}\n\n// Add to ParticipantCapabilities\ninterface ParticipantCapabilities {\n // ... existing ...\n streaming?: StreamingCapabilities;\n}\n\n// Subscription state\ntype SubscriptionState = 'active' | 'paused' | 'closed';\n\n// Overflow information\ninterface OverflowInfo {\n eventsDropped: number;\n oldestDroppedId?: string;\n newestDroppedId?: string;\n timestamp: number;\n totalDropped: number;\n}\n\ntype OverflowHandler = (info: OverflowInfo) => void;\n\n// Acknowledgment (optional)\ninterface SubscriptionAckParams {\n subscriptionId: SubscriptionId;\n upToSequence: number;\n}\n```\n\n### Updated Subscription API\n\n```typescript\nclass Subscription implements AsyncIterable<Event> {\n // Existing\n readonly id: SubscriptionId;\n readonly isClosed: boolean;\n readonly bufferedCount: number;\n\n // New properties\n readonly state: SubscriptionState;\n readonly isPaused: boolean;\n readonly totalDropped: number;\n\n // New methods\n pause(): void;\n resume(): void;\n on(type: 'overflow', handler: OverflowHandler): this;\n off(type: 'overflow', handler: OverflowHandler): this;\n ack(upToSequence?: number): void;\n}\n```\n\n### Implementation Tasks\n\n**Phase 1**: Type definitions\n- Add `StreamingCapabilities` to types\n- Add `SubscriptionState`, `OverflowInfo` types\n- Add `SubscriptionAckParams` type\n\n**Phase 2**: Core logic\n- Add `#state`, `#totalDropped` fields to Subscription\n- Implement `pause()`, `resume()` methods\n- Modify async iterator to check `isPaused`\n- Track dropped event IDs for overflow info\n- Add overflow handler support\n\n**Phase 3**: Integration\n- Server advertises capabilities in connect response\n- Add `ack()` method to Subscription\n- TestServer receives and tracks acks\n\n**Phase 4**: Testing\n- Pause/resume tests\n- Overflow handler tests\n- Ack flow tests\n\n---\n\n## Agent Permissions\n\n### Requirements\n\n| ID | Requirement | Priority |\n|----|-------------|----------|\n| P1 | Agents can have visibility rules (who can see them) | Must |\n| P2 | Agents can have messaging rules (who they can message) | Must |\n| P3 | Agents can have acceptance rules (who they accept messages from) | Must |\n| P4 | Role-based default permissions | Must |\n| P5 | Per-agent permission overrides | Must |\n| P6 | System-wide default permissions | Should |\n\n### Design Decision: Hybrid Approach\n\nResolution order:\n1. Start with system default permissions\n2. If agent has a role, deep merge role permissions\n3. Deep merge agent's `permissionOverrides`\n\nDeep merge at field level within `canSee`, `canMessage`, `acceptsFrom`.\n\n### Type Definitions\n\n```typescript\n// Permission rule types\ntype AgentVisibilityRule =\n | 'all' | 'hierarchy' | 'scoped' | 'direct'\n | { include: AgentId[] };\n\ntype ScopeVisibilityRule =\n | 'all' | 'member'\n | { include: ScopeId[] };\n\ntype StructureVisibilityRule = 'full' | 'local' | 'none';\n\ntype AgentMessagingRule =\n | 'all' | 'hierarchy' | 'scoped'\n | { include: AgentId[] };\n\ntype ScopeMessagingRule =\n | 'all' | 'member'\n | { include: ScopeId[] };\n\ntype AgentAcceptanceRule =\n | 'all' | 'hierarchy' | 'scoped'\n | { include: AgentId[] };\n\ntype ClientAcceptanceRule =\n | 'all' | 'none'\n | { include: ParticipantId[] };\n\ntype SystemAcceptanceRule =\n | 'all' | 'none'\n | { include: string[] };\n\n// Main permissions interface\ninterface AgentPermissions {\n canSee?: {\n agents?: AgentVisibilityRule;\n scopes?: ScopeVisibilityRule;\n structure?: StructureVisibilityRule;\n };\n canMessage?: {\n agents?: AgentMessagingRule;\n scopes?: ScopeMessagingRule;\n };\n acceptsFrom?: {\n agents?: AgentAcceptanceRule;\n clients?: ClientAcceptanceRule;\n systems?: SystemAcceptanceRule;\n };\n}\n\n// Agent interface update\ninterface Agent {\n // ... existing fields ...\n permissionOverrides?: Partial<AgentPermissions>;\n}\n\n// System configuration\ninterface AgentPermissionConfig {\n defaultPermissions: AgentPermissions;\n rolePermissions: { [role: string]: AgentPermissions };\n}\n```\n\n### Permission Resolution Function\n\n```typescript\nfunction resolveAgentPermissions(\n agent: Agent,\n config: AgentPermissionConfig\n): AgentPermissions {\n let permissions = deepClone(config.defaultPermissions);\n\n if (agent.role && config.rolePermissions[agent.role]) {\n permissions = deepMerge(permissions, config.rolePermissions[agent.role]);\n }\n\n if (agent.permissionOverrides) {\n permissions = deepMerge(permissions, agent.permissionOverrides);\n }\n\n // Backwards compat: map visibility to canSee.agents\n if (agent.visibility && !agent.permissionOverrides?.canSee?.agents) {\n permissions.canSee = permissions.canSee ?? {};\n permissions.canSee.agents = mapVisibilityToRule(agent.visibility);\n }\n\n return permissions;\n}\n```\n\n### Implementation Tasks\n\n**Phase 1**: Type definitions\n- Add permission rule types\n- Add `AgentPermissions` interface\n- Add `permissionOverrides` to Agent\n- Add `AgentPermissionConfig`\n\n**Phase 2**: Core logic\n- Implement `resolveAgentPermissions()`\n- Implement `deepMerge()` utility\n- Add `canAgentAcceptMessage()` function\n- Update existing permission check functions\n\n**Phase 3**: Integration\n- Add config to `MAPRouterConfig`\n- TestServer resolves permissions\n- Check `acceptsFrom` before message delivery\n- Update `filterVisibleAgents()`\n\n**Phase 4**: Testing\n- Role-based permission tests\n- Override merge tests\n- Acceptance rule tests\n- Backwards compatibility tests\n\n---\n\n## Dynamic Permissions\n\n### Requirements\n\n| ID | Requirement | Priority |\n|----|-------------|----------|\n| D1 | System can update client permissions at runtime | Must |\n| D2 | Agent owner can update agent permissions at runtime | Must |\n| D3 | Permission changes emit events | Should |\n| D4 | Permission updates are immediate | Must |\n\n### Supported Flows\n\n**Flow 1: System updates client**\n```\nSystem ── map/permissions/update ──► Router ── event ──► Client\n```\n\n**Flow 4: Owner updates agent**\n```\nClient ── map/agents/update ──► Router ── event ──► Subscribers\n```\n\n### Type Definitions\n\n```typescript\n// Client permission update\ninterface PermissionsUpdateRequestParams {\n clientId?: ParticipantId;\n permissions: Partial<ParticipantCapabilities>;\n _meta?: Meta;\n}\n\ninterface PermissionsUpdateResponseResult {\n success: boolean;\n effectivePermissions: ParticipantCapabilities;\n _meta?: Meta;\n}\n\n// Permission events\nconst PERMISSION_EVENT_TYPES = {\n PERMISSIONS_CLIENT_UPDATED: 'permissions_client_updated',\n PERMISSIONS_AGENT_UPDATED: 'permissions_agent_updated',\n} as const;\n\ninterface PermissionsClientUpdatedEventData {\n clientId: ParticipantId;\n changes: Partial<ParticipantCapabilities>;\n effectivePermissions: ParticipantCapabilities;\n updatedBy: ParticipantId;\n}\n\ninterface PermissionsAgentUpdatedEventData {\n agentId: AgentId;\n changes: Partial<AgentPermissions>;\n effectivePermissions: AgentPermissions;\n updatedBy: ParticipantId;\n}\n```\n\n### Authorization Rules\n\n- Only system can update client permissions\n- Owner can update their own agents\n- Owner of parent can update child agents\n\n### Implementation Tasks\n\n**Phase 1**: Type definitions\n- Add request/response types\n- Add event types\n- Register `map/permissions/update` method\n\n**Phase 2**: Core logic\n- Authorization check functions\n\n**Phase 3**: Integration\n- Add `permissionOverrides` to `UpdateAgentRequestParams`\n- TestServer handler for `map/permissions/update`\n- Emit permission events\n\n**Phase 4**: Testing\n- Authorization tests\n- Event emission tests\n\n---\n\n## Federation Envelope\n\n### Requirements\n\n| ID | Requirement | Priority |\n|----|-------------|----------|\n| F1 | Messages include source system identifier | Must |\n| F2 | Messages include target system identifier | Must |\n| F3 | Hop count tracking for loop prevention | Must |\n| F4 | Routing path tracking for debugging | Should |\n| F5 | Correlation ID for cross-system tracing | Should |\n| F6 | Message signing for integrity | Deferred |\n\n### Type Definitions\n\n```typescript\ninterface FederationMetadata {\n sourceSystem: string;\n targetSystem: string;\n hopCount: number;\n maxHops?: number;\n path?: string[];\n originTimestamp: Timestamp;\n correlationId?: string;\n signature?: string; // TODO: Define signing later\n}\n\ninterface FederationEnvelope<T = unknown> {\n payload: T;\n federation: FederationMetadata;\n}\n\ninterface FederationRoutingConfig {\n systemId: string;\n maxHops?: number;\n trackPath?: boolean;\n allowedTargets?: string[];\n allowedSources?: string[];\n}\n\n// Updated request params\ninterface FederationRouteRequestParams {\n systemId: string;\n envelope: FederationEnvelope<Message>;\n /** @deprecated Use envelope instead */\n message?: Message;\n _meta?: Meta;\n}\n\n// New error codes\nFEDERATION_LOOP_DETECTED: 5010,\nFEDERATION_MAX_HOPS_EXCEEDED: 5011,\n```\n\n### Routing Logic\n\n```typescript\nfunction processFederationEnvelope<T>(\n envelope: FederationEnvelope<T>,\n config: FederationRoutingConfig\n): FederationEnvelope<T> | null {\n const { federation } = envelope;\n const maxHops = federation.maxHops ?? config.maxHops ?? 10;\n\n // Check hop count\n if (federation.hopCount >= maxHops) return null;\n\n // Check for loops\n if (federation.path?.includes(config.systemId)) return null;\n\n // Check source allowlist\n if (config.allowedSources && !config.allowedSources.includes(federation.sourceSystem)) {\n return null;\n }\n\n // Update for forwarding\n return {\n payload: envelope.payload,\n federation: {\n ...federation,\n hopCount: federation.hopCount + 1,\n path: config.trackPath\n ? [...(federation.path ?? []), config.systemId]\n : undefined,\n },\n };\n}\n\nfunction createFederationEnvelope<T>(\n payload: T,\n sourceSystem: string,\n targetSystem: string,\n options?: { correlationId?: string; maxHops?: number; trackPath?: boolean }\n): FederationEnvelope<T> {\n return {\n payload,\n federation: {\n sourceSystem,\n targetSystem,\n hopCount: 0,\n maxHops: options?.maxHops,\n path: options?.trackPath ? [sourceSystem] : undefined,\n originTimestamp: Date.now(),\n correlationId: options?.correlationId,\n },\n };\n}\n```\n\n### Implementation Tasks\n\n**Phase 1**: Type definitions\n- Add `FederationMetadata`, `FederationEnvelope<T>`\n- Add `FederationRoutingConfig`\n- Update `FederationRouteRequestParams`\n- Add error codes\n\n**Phase 2**: Core logic\n- Create `ts-sdk/src/federation/envelope.ts`\n- Implement `createFederationEnvelope()`\n- Implement `processFederationEnvelope()`\n- Loop detection logic\n\n**Phase 3**: Integration\n- Add routing config to `GatewayConnectionOptions`\n- Update `routeToSystem()` to wrap in envelope\n- Add `routeEnvelope()` method\n- TestServer handles envelope format\n\n**Phase 4**: Testing\n- Envelope creation tests\n- Hop count tests\n- Loop detection tests\n- Backwards compatibility tests\n\n---\n\n## Federation Reconnection\n\n### Requirements\n\n| ID | Requirement | Priority |\n|----|-------------|----------|\n| R1 | GatewayConnection supports auto-reconnect | Must |\n| R2 | Consistent with ClientConnection pattern | Must |\n| R3 | Buffer outbound messages during brief outages | Should |\n| R4 | Replay missed events on reconnect | Should |\n| R5 | Configurable buffer size and TTL | Should |\n| R6 | Replay by timestamp | Must |\n\n### Design: Hybrid Recovery\n\n1. **Outage Buffer** (in-memory, short-term): Messages that failed during brief outage\n2. **Event Store Replay** (persisted, longer-term): State-changing events\n\nBuffer drained first, then events replayed.\n\n### Type Definitions\n\n```typescript\ninterface GatewayReconnectionOptions {\n enabled: boolean;\n maxRetries?: number;\n baseDelayMs?: number;\n maxDelayMs?: number;\n jitter?: boolean;\n}\n\ninterface FederationBufferConfig {\n enabled?: boolean;\n maxQueueSize?: number; // default: 1000\n maxQueueDuration?: number; // default: 60000ms\n overflowPolicy?: 'drop-oldest' | 'drop-newest' | 'reject-new';\n}\n\ninterface FederationReplayConfig {\n enabled?: boolean;\n eventTypes?: EventType[];\n maxEvents?: number; // default: 1000\n maxAgeMs?: number; // default: 300000ms (5 min)\n}\n\ninterface GatewayConnectionOptions extends BaseConnectionOptions {\n name?: string;\n capabilities?: ParticipantCapabilities;\n routing?: FederationRoutingConfig;\n reconnection?: GatewayReconnectionOptions;\n buffer?: FederationBufferConfig;\n replay?: FederationReplayConfig;\n createStream?: () => Promise<Stream>;\n}\n\ntype GatewayReconnectionEventType =\n | 'disconnected'\n | 'reconnecting'\n | 'reconnected'\n | 'reconnectFailed'\n | 'bufferOverflow'\n | 'replayStarted'\n | 'replayCompleted';\n\ninterface GatewayReconnectionEvent {\n type: GatewayReconnectionEventType;\n timestamp: number;\n attempt?: number;\n peerId?: string;\n eventsReplayed?: number;\n messagesBuffered?: number;\n error?: Error;\n}\n```\n\n### Outage Buffer\n\n```typescript\nclass FederationOutageBuffer {\n enqueue(peerId: string, envelope: FederationEnvelope<Message>): boolean;\n drain(peerId: string): FederationEnvelope<Message>[];\n stats(): Map<string, { count: number; oldestAge: number }>;\n}\n```\n\n### Implementation Tasks\n\n**Phase 1**: Type definitions\n- Add all config interfaces\n- Update `GatewayConnectionOptions`\n- Add event types\n\n**Phase 2**: Core logic\n- Create `ts-sdk/src/federation/buffer.ts`\n- Implement `FederationOutageBuffer`\n\n**Phase 3**: Integration\n- Add reconnection to `GatewayConnection`\n- Integrate buffer\n- Track `lastSyncTimestamp` per peer\n- Replay events on reconnect\n\n**Phase 4**: Testing\n- Reconnection tests\n- Buffer overflow tests\n- Replay tests\n- Combined recovery flow tests\n\n---\n\n## Implementation Phases\n\n### Phase 1: Type Definitions (Week 1)\n\n| Issue | Title |\n|-------|-------|\n| [[i-3f48]] | Add streaming backpressure type definitions |\n| [[i-2fao]] | Add agent permission type definitions |\n| [[i-1ha9]] | Add dynamic permission update type definitions |\n| [[i-6yme]] | Add federation envelope type definitions |\n| [[i-9xsr]] | Add federation reconnection type definitions |\n\n**Parallelizable**: i-3f48, i-2fao, i-6yme\n**Dependencies**: i-1ha9 → i-2fao, i-9xsr → i-6yme\n\n### Phase 2: Core Logic (Week 2)\n\n- Subscription pause/resume/overflow implementation\n- `resolveAgentPermissions()` and acceptance checks\n- Federation envelope utilities and routing logic\n- `FederationOutageBuffer` class\n\n### Phase 3: Integration (Week 3)\n\n- Server capability advertisement\n- Subscription ack support\n- Permission update handlers and events\n- Gateway reconnection with buffer/replay\n\n### Phase 4: Testing & Polish (Week 4)\n\n- Comprehensive test coverage for all features\n- Documentation updates\n- Migration guide for breaking changes\n\n---\n\n## Breaking Changes\n\n### Streaming\n- None (additive only)\n\n### Permissions\n- `Agent.visibility` deprecated in favor of `permissionOverrides.canSee.agents`\n- Still supported for backwards compatibility\n\n### Federation\n- `FederationRouteRequestParams.message` deprecated in favor of `envelope`\n- Old format supported temporarily for migration\n\n---\n\n## Open Items / TODOs\n\n1. **Signing**: Define algorithm and key management for `FederationMetadata.signature`\n2. **Policy Entities**: Consider adding policy CRUD for P2 if hybrid approach insufficient\n3. **Permission Request Flow**: Design approval workflow for capability upgrade requests\n4. **Server Flow Control**: Implement send pausing based on ack lag (optional enhancement)\n","priority":1,"archived":0,"archived_at":null,"created_at":"2026-01-29 04:25:28","updated_at":"2026-01-29 04:31:11","parent_id":null,"parent_uuid":null,"relationships":[],"tags":["approved","federation","p1","permissions","streaming"]}
2
- {"id":"s-10j2","uuid":"c6344426-5210-44e6-93ce-1f6731307522","title":"MAP Server SDK - Building Blocks Architecture","file_path":"specs/s-10j2_map_server_sdk_building_blocks_architecture.md","content":"# MAP Server SDK - Building Blocks Architecture\n\n## Overview\n\nThis spec defines the server-side SDK components for the Multi-Agent Protocol (MAP). The SDK provides composable building blocks that allow server implementers to build MAP-compliant routers with varying levels of customization.\n\n## Design Philosophy\n\n### Goals\n1. **Composable** - Building blocks work independently and together\n2. **Pluggable** - Storage, permissions, and federation are swappable\n3. **Optional Persistence** - Works in-memory by default, persistence opt-in\n4. **Handler-First** - Handlers pattern as primitive, interface pattern built on top\n5. **Federation as Decorator** - Core blocks stay simple, federation wraps them\n\n### Non-Goals\n- Providing a single \"MAP Server\" class that does everything\n- Forcing a specific storage backend\n- Requiring federation support\n\n## Architecture\n\n```\n┌─────────────────────────────────────────────────────────────┐\n│ RouterConnection │\n│ (JSON-RPC routing, validation, middleware) │\n└─────────────────────────────────────────────────────────────┘\n │\n ┌────────────────────┼────────────────────┐\n ▼ ▼ ▼\n┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐\n│ AgentRegistry │ │ ScopeManager │ │ SessionManager │\n│ │◄─┤ │ │ │\n│ - agents CRUD │ │ - scope CRUD │ │ - connections │\n│ - state machine │ │ - membership │ │ - cleanup │\n└────────┬────────┘ └────────┬────────┘ └────────┬────────┘\n │ │ │\n └────────────────────┼────────────────────┘\n ▼\n ┌─────────────────┐\n │ EventBus │\n │ │\n │ - emit/subscribe│\n │ - history │\n └────────┬────────┘\n │\n ▼\n ┌───────────────────────┐\n │ SubscriptionManager │\n │ │\n │ - client subscriptions│\n │ - filtering & replay │\n │ - causal ordering │\n └───────────────────────┘\n```\n\n## Design Decisions\n\n### 1. Handler Pattern as Primitive\n\nThe SDK uses a handlers pattern (like Express/Hono) as the core abstraction, with an optional interface adapter for those who prefer OOP.\n\n**Rationale:**\n- MAP servers vary wildly (in-memory test server vs distributed production)\n- Federation is optional - shouldn't force implementing it\n- Middleware is genuinely useful for auth, logging, metrics\n- Easier to provide composable \"starter\" handler sets\n- Interface pattern is trivially built on top\n\n### 2. Constructor Injection for Dependencies\n\nBuilding blocks take dependencies explicitly in their constructor.\n\n**Rationale:**\n- Explicit dependencies - clear what each component needs\n- No magic - easy to understand and debug\n- Tree-shakeable - only import what you use\n- Easy to test - pass mocks directly\n- TypeScript catches missing dependencies at compile time\n\n### 3. Per-Building-Block Storage Interfaces\n\nEach building block that needs persistence defines its own storage interface.\n\n**Rationale:**\n- Query patterns differ significantly (events need time-range, agents need state filter)\n- Allows specialized implementations per data type\n- Users only implement stores for what they need to persist\n- Can mix backends (agents in Postgres, events in Redis)\n\n### 4. Layered Permissions\n\nMiddleware handles coarse-grained (method-level) permissions, injected checker handles fine-grained (resource-level).\n\n**Rationale:**\n- Centralized policy for method access is easy to audit\n- Fine-grained checks (can agent A message agent B?) need handler context\n- Handlers stay clean - permission logic is separated\n- Flexible enough for simple and complex use cases\n\n### 5. Federation as Decorator\n\nFederation wraps core building blocks rather than being integrated into them.\n\n**Rationale:**\n- Core building blocks stay simple and testable\n- Federation is truly optional - don't pay for what you don't use\n- Users can choose what to federate (agents yes, scopes no)\n- Clear separation of concerns\n- Easy to test each layer independently\n\n### 6. Optional OOP Interface\n\nProvide a `MAPRouter` interface with `routerToHandlers()` adapter for users who prefer class-based implementations.\n\n**Rationale:**\n- Some teams prefer OOP patterns\n- TypeScript enforces all methods are implemented\n- Easy to migrate between patterns\n- Interface is trivially built on handler primitives\n\n### 7. Connection Resume Support\n\n`RouterConnection` supports resuming sessions after disconnect/reconnect.\n\n**Rationale:**\n- Network interruptions are common\n- Agents shouldn't lose state on brief disconnects\n- Subscriptions can resume from last event ID\n- Better UX for long-running agents\n\n### 8. Causal Event Ordering\n\n`SubscriptionManager` delivers events respecting causal dependencies by default.\n\n**Rationale:**\n- Events often have dependencies (agent.registered before agent.state.changed)\n- Out-of-order delivery causes bugs in subscribers\n- Can be disabled for performance-critical cases\n- Uses `causedBy` field in events\n\n### 9. Nested Scopes\n\nScopes support parent-child hierarchy.\n\n**Rationale:**\n- Natural for organizational structures (org > team > project)\n- Permissions can cascade down\n- Membership queries can include descendants\n- Optional - flat scopes still work\n\n### 10. Message Queuing\n\n`MessageRouter` queues messages for temporarily offline agents.\n\n**Rationale:**\n- Brief disconnects shouldn't lose messages\n- Configurable queue size and TTL\n- Fail-fast option still available\n- Essential for reliable delivery\n\n### 11. Flexible Cleanup Component\n\n`ResourceCleaner` handles stale resources with pluggable strategies.\n\n**Rationale:**\n- Sessions can disconnect without cleanup\n- Orphaned agents/subscriptions waste resources\n- Different deployments need different strategies\n- Configurable thresholds and behaviors\n\n---\n\n## Core Interfaces\n\n### Events\n\n```typescript\n/**\n * A MAP event that can be emitted, stored, and delivered to subscribers.\n */\ninterface MAPEvent {\n /** ULID for ordering and deduplication */\n id: string;\n /** Event type (e.g., 'agent.registered', 'scope.agent.joined') */\n type: string;\n /** Unix timestamp in milliseconds */\n timestamp: number;\n /** Event-specific payload */\n data: unknown;\n /** Source of the event */\n source?: {\n agentId?: string;\n scopeId?: string;\n sessionId?: string;\n };\n /** Parent event ID for causal ordering */\n causedBy?: string;\n}\n\n/**\n * Filter criteria for querying events.\n */\ninterface EventFilter {\n /** Filter by event types */\n types?: string[];\n /** Return events after this event ID */\n since?: string;\n /** Return events before this event ID */\n until?: string;\n /** Maximum number of events to return */\n limit?: number;\n}\n```\n\n### EventStore (Pluggable Backend)\n\n```typescript\n/**\n * Storage backend for events. Implement this interface to provide\n * custom persistence (Redis, Postgres, etc.).\n */\ninterface EventStore {\n /** Append an event to storage */\n append(event: MAPEvent): void;\n \n /** Query events matching filter criteria */\n query(filter: EventFilter): MAPEvent[];\n \n /** Get a specific event by ID */\n getById(id: string): MAPEvent | undefined;\n \n /** Clear all events (useful for testing) */\n clear(): void;\n}\n```\n\n### EventBus\n\n```typescript\n/**\n * Central event dispatcher. All building blocks emit events through this.\n */\ninterface EventBus {\n /**\n * Emit an event. Assigns ID and timestamp automatically.\n * @returns The complete event with ID and timestamp\n */\n emit(event: Omit<MAPEvent, 'id' | 'timestamp'>): MAPEvent;\n \n /**\n * Subscribe to events by type.\n * @param types Event type(s) to listen for, or '*' for all\n * @param handler Callback invoked for each matching event\n * @returns Unsubscribe function\n */\n on(types: string | string[], handler: (event: MAPEvent) => void): () => void;\n \n /**\n * Query historical events.\n */\n getEvents(filter: EventFilter): MAPEvent[];\n \n /** The underlying storage backend */\n readonly store: EventStore;\n}\n\ninterface EventBusOptions {\n /** Custom event store (defaults to InMemoryEventStore) */\n store?: EventStore;\n}\n```\n\n### Agents\n\n```typescript\n/** Valid agent states */\ntype AgentState = 'idle' | 'busy' | 'suspended' | 'stopped';\n\n/**\n * A registered agent in the system.\n */\ninterface RegisteredAgent {\n /** Unique agent identifier */\n id: string;\n /** Human-readable name */\n name: string;\n /** Optional role identifier */\n role?: string;\n /** Current agent state */\n state: AgentState;\n /** Arbitrary metadata */\n metadata: Record<string, unknown>;\n /** Session that owns this agent */\n sessionId: string;\n /** Registration timestamp */\n registeredAt: number;\n /** Last state change timestamp */\n lastStateChange: number;\n}\n\n/**\n * Filter criteria for listing agents.\n */\ninterface AgentFilter {\n /** Filter by state */\n state?: AgentState;\n /** Filter by role */\n role?: string;\n /** Filter by session */\n sessionId?: string;\n /** Filter by scope membership */\n scopeId?: string;\n}\n```\n\n### AgentStore (Pluggable Backend)\n\n```typescript\n/**\n * Storage backend for agents. Implement for persistence.\n */\ninterface AgentStore {\n save(agent: RegisteredAgent): void;\n get(id: string): RegisteredAgent | undefined;\n list(filter?: AgentFilter): RegisteredAgent[];\n delete(id: string): boolean;\n clear(): void;\n}\n```\n\n### AgentRegistry\n\n```typescript\n/**\n * Manages agent lifecycle and state.\n * \n * Events emitted:\n * - agent.registered\n * - agent.unregistered\n * - agent.state.changed\n * - agent.metadata.changed\n */\ninterface AgentRegistry {\n /**\n * Register a new agent.\n */\n register(params: {\n name: string;\n role?: string;\n metadata?: Record<string, unknown>;\n sessionId: string;\n }): RegisteredAgent;\n \n /** Get agent by ID */\n get(id: string): RegisteredAgent | undefined;\n \n /** List agents matching filter */\n list(filter?: AgentFilter): RegisteredAgent[];\n \n /** Unregister an agent */\n unregister(id: string): boolean;\n \n /** Update agent state */\n updateState(id: string, state: AgentState): RegisteredAgent;\n \n /** Update agent metadata (merges with existing) */\n updateMetadata(id: string, metadata: Record<string, unknown>): RegisteredAgent;\n \n /** Unregister all agents for a session (cleanup on disconnect) */\n unregisterBySession(sessionId: string): string[];\n}\n\ninterface AgentRegistryOptions {\n eventBus: EventBus;\n store?: AgentStore;\n}\n```\n\n### Scopes (with Hierarchy)\n\n```typescript\n/**\n * A scope for grouping agents. Supports parent-child hierarchy.\n */\ninterface Scope {\n /** Unique scope identifier */\n id: string;\n /** Human-readable name */\n name: string;\n /** Arbitrary metadata */\n metadata: Record<string, unknown>;\n /** Creation timestamp */\n createdAt: number;\n /** Creator (agent or session ID) */\n createdBy?: string;\n /** Parent scope ID for hierarchy (null for root scopes) */\n parentId?: string;\n}\n\n/**\n * Filter criteria for listing scopes.\n */\ninterface ScopeFilter {\n /** Filter by parent (null for root scopes only) */\n parentId?: string | null;\n /** Include all descendants of this scope */\n ancestorId?: string;\n}\n```\n\n### ScopeStore (Pluggable Backend)\n\n```typescript\n/**\n * Storage backend for scopes. Implement for persistence.\n */\ninterface ScopeStore {\n saveScope(scope: Scope): void;\n getScope(id: string): Scope | undefined;\n listScopes(filter?: ScopeFilter): Scope[];\n deleteScope(id: string): boolean;\n \n /** Get all ancestor scope IDs (parent chain) */\n getAncestors(scopeId: string): string[];\n \n /** Get all descendant scope IDs */\n getDescendants(scopeId: string): string[];\n \n // Membership\n addMember(scopeId: string, agentId: string): void;\n removeMember(scopeId: string, agentId: string): void;\n getMembers(scopeId: string, includeDescendants?: boolean): string[];\n getScopesForAgent(agentId: string): string[];\n \n clear(): void;\n}\n```\n\n### ScopeManager\n\n```typescript\n/**\n * Manages scopes and membership with hierarchy support.\n * \n * Events emitted:\n * - scope.created\n * - scope.deleted\n * - scope.agent.joined\n * - scope.agent.left\n */\ninterface ScopeManager {\n /** Create a new scope */\n create(params: {\n name: string;\n metadata?: Record<string, unknown>;\n createdBy?: string;\n parentId?: string;\n }): Scope;\n \n /** Get scope by ID */\n get(id: string): Scope | undefined;\n \n /** List scopes with optional filter */\n list(filter?: ScopeFilter): Scope[];\n \n /** Delete a scope (and optionally its descendants) */\n delete(id: string, opts?: { deleteDescendants?: boolean }): boolean;\n \n /** Get parent scope */\n getParent(scopeId: string): Scope | undefined;\n \n /** Get child scopes */\n getChildren(scopeId: string): Scope[];\n \n /** Get all ancestor scopes (parent chain to root) */\n getAncestors(scopeId: string): Scope[];\n \n /** Get all descendant scopes */\n getDescendants(scopeId: string): Scope[];\n \n /** Add agent to scope */\n join(scopeId: string, agentId: string): void;\n \n /** Remove agent from scope */\n leave(scopeId: string, agentId: string): void;\n \n /** Get all agents in a scope (optionally including descendants) */\n getMembers(scopeId: string, opts?: { includeDescendants?: boolean }): string[];\n \n /** Get all scopes an agent belongs to */\n getScopesForAgent(agentId: string): string[];\n \n /** Check if agent is in scope (optionally checking ancestors) */\n isMember(scopeId: string, agentId: string, opts?: { checkAncestors?: boolean }): boolean;\n \n /** Remove agent from all scopes (cleanup) */\n leaveAll(agentId: string): string[];\n}\n\ninterface ScopeManagerOptions {\n eventBus: EventBus;\n store?: ScopeStore;\n}\n```\n\n### Sessions (with Resume Support)\n\n```typescript\n/** Connection role */\ntype SessionRole = 'client' | 'agent' | 'gateway';\n\n/** Session status */\ntype SessionStatus = 'connected' | 'disconnected' | 'expired';\n\n/**\n * A connected session (client, agent, or gateway).\n * Supports disconnect/resume lifecycle.\n */\ninterface Session {\n /** Unique session identifier */\n id: string;\n /** Connection role */\n role: SessionRole;\n /** Human-readable name */\n name?: string;\n /** Current status */\n status: SessionStatus;\n /** Initial connection timestamp */\n connectedAt: number;\n /** Last activity timestamp */\n lastActivity: number;\n /** Last disconnect timestamp (if disconnected) */\n disconnectedAt?: number;\n /** Arbitrary metadata */\n metadata: Record<string, unknown>;\n /** Agents registered via this session */\n agentIds: string[];\n /** Active subscriptions */\n subscriptionIds: string[];\n /** Resume token for reconnection */\n resumeToken?: string;\n}\n\n/**\n * Resume result when reconnecting.\n */\ninterface ResumeResult {\n /** Whether resume succeeded */\n success: boolean;\n /** The resumed session (if successful) */\n session?: Session;\n /** Reason for failure (if unsuccessful) */\n reason?: 'expired' | 'invalid_token' | 'not_found';\n}\n```\n\n### SessionStore (Pluggable Backend)\n\n```typescript\n/**\n * Storage backend for sessions. Implement for persistence.\n */\ninterface SessionStore {\n save(session: Session): void;\n get(id: string): Session | undefined;\n getByResumeToken(token: string): Session | undefined;\n list(filter?: { role?: SessionRole; status?: SessionStatus }): Session[];\n delete(id: string): boolean;\n clear(): void;\n}\n```\n\n### SessionManager\n\n```typescript\n/**\n * Tracks connections and their resources with resume support.\n * \n * Events emitted:\n * - session.connected\n * - session.disconnected\n * - session.resumed\n * - session.expired\n */\ninterface SessionManager {\n /** Create a new session */\n create(params: {\n role: SessionRole;\n name?: string;\n metadata?: Record<string, unknown>;\n }): Session;\n \n /** Get session by ID */\n get(id: string): Session | undefined;\n \n /** List sessions */\n list(filter?: { role?: SessionRole; status?: SessionStatus }): Session[];\n \n /**\n * Mark session as disconnected (but resumable).\n * @returns Resume token for reconnection\n */\n disconnect(id: string): string | undefined;\n \n /**\n * Resume a disconnected session.\n */\n resume(resumeToken: string): ResumeResult;\n \n /**\n * Permanently close a session (not resumable).\n * @returns Session for cleanup\n */\n close(id: string): Session | undefined;\n \n /**\n * Expire sessions that have been disconnected too long.\n * @returns Expired session IDs\n */\n expireStale(maxDisconnectMs: number): string[];\n \n /** Track agent registration */\n addAgent(sessionId: string, agentId: string): void;\n removeAgent(sessionId: string, agentId: string): void;\n \n /** Track subscriptions */\n addSubscription(sessionId: string, subscriptionId: string): void;\n removeSubscription(sessionId: string, subscriptionId: string): void;\n \n /** Update last activity timestamp */\n touch(id: string): void;\n}\n\ninterface SessionManagerOptions {\n eventBus: EventBus;\n store?: SessionStore;\n /** How long disconnected sessions remain resumable (default: 5 minutes) */\n resumeWindowMs?: number;\n}\n```\n\n### Subscriptions (with Causal Ordering)\n\n```typescript\n/**\n * Filter for subscription events.\n */\ninterface SubscriptionFilter {\n /** Event types to receive */\n eventTypes?: string[];\n /** Agents to watch */\n agents?: string[];\n /** Scopes to watch (includes nested scopes by default) */\n scopes?: string[];\n}\n\n/**\n * An active subscription.\n */\ninterface Subscription {\n /** Unique subscription identifier */\n id: string;\n /** Owning session */\n sessionId: string;\n /** Filter criteria */\n filter: SubscriptionFilter;\n /** Creation timestamp */\n createdAt: number;\n /** Last delivered event ID (for replay/resume) */\n lastEventId?: string;\n /** Whether delivery is paused */\n paused: boolean;\n}\n\n/**\n * Causal ordering configuration.\n */\ninterface CausalOrderingOptions {\n /** Enable causal ordering (default: true) */\n enabled?: boolean;\n /** Max time to wait for dependencies (default: 5000ms) */\n maxWaitMs?: number;\n /** Max events to buffer while waiting (default: 1000) */\n maxBufferSize?: number;\n}\n```\n\n### SubscriptionStore (Pluggable Backend)\n\n```typescript\n/**\n * Storage backend for subscriptions. Implement for persistence.\n */\ninterface SubscriptionStore {\n save(subscription: Subscription): void;\n get(id: string): Subscription | undefined;\n list(filter?: { sessionId?: string }): Subscription[];\n delete(id: string): boolean;\n clear(): void;\n}\n```\n\n### SubscriptionManager\n\n```typescript\n/**\n * Manages client subscriptions to events with causal ordering.\n */\ninterface SubscriptionManager {\n /** Create a new subscription */\n create(params: {\n sessionId: string;\n filter: SubscriptionFilter;\n startAfter?: string;\n }): Subscription;\n \n /** Get subscription by ID */\n get(id: string): Subscription | undefined;\n \n /** Cancel a subscription */\n cancel(id: string): boolean;\n \n /** Cancel all subscriptions for a session */\n cancelBySession(sessionId: string): string[];\n \n /** Pause event delivery */\n pause(id: string): void;\n \n /** Resume event delivery */\n resume(id: string): void;\n \n /** Update last delivered event ID */\n acknowledge(id: string, eventId: string): void;\n \n /**\n * Find subscriptions that should receive an event.\n * @returns Subscription IDs that match\n */\n match(event: MAPEvent): string[];\n \n /**\n * Get ordered event stream for a subscription.\n * Handles causal ordering internally.\n */\n getEventStream(id: string): AsyncIterable<MAPEvent>;\n}\n\ninterface SubscriptionManagerOptions {\n eventBus: EventBus;\n store?: SubscriptionStore;\n scopes?: ScopeManager;\n causalOrdering?: CausalOrderingOptions;\n}\n```\n\n### Messages (with Queuing)\n\n```typescript\n/**\n * A message between agents.\n */\ninterface Message {\n /** Unique message identifier */\n id: string;\n /** Sender (agent or session ID) */\n from: string;\n /** Recipient(s) - agent ID, scope ID, or array */\n to: string | string[];\n /** Message content */\n payload: unknown;\n /** Send timestamp */\n timestamp: number;\n /** For request/response patterns */\n replyTo?: string;\n /** Message priority (lower = higher priority) */\n priority?: number;\n /** TTL in milliseconds (for queued messages) */\n ttlMs?: number;\n}\n\n/**\n * Queued message awaiting delivery.\n */\ninterface QueuedMessage {\n message: Message;\n targetAgentId: string;\n queuedAt: number;\n expiresAt: number;\n attempts: number;\n}\n\n/**\n * Callback for message delivery.\n */\ntype DeliveryHandler = (agentId: string, message: Message) => void;\n\n/**\n * Message queue configuration.\n */\ninterface MessageQueueOptions {\n /** Enable queuing for offline agents (default: true) */\n enabled?: boolean;\n /** Max messages per agent (default: 100) */\n maxPerAgent?: number;\n /** Default TTL in milliseconds (default: 60000) */\n defaultTtlMs?: number;\n /** Max total queued messages (default: 10000) */\n maxTotal?: number;\n}\n```\n\n### MessageStore (Pluggable Backend)\n\n```typescript\n/**\n * Storage backend for message queue. Implement for persistence.\n */\ninterface MessageQueueStore {\n enqueue(msg: QueuedMessage): void;\n dequeue(agentId: string, limit?: number): QueuedMessage[];\n peek(agentId: string, limit?: number): QueuedMessage[];\n remove(messageId: string): boolean;\n getQueueSize(agentId: string): number;\n getTotalSize(): number;\n expireOld(): number;\n clear(): void;\n}\n```\n\n### MessageRouter\n\n```typescript\n/**\n * Routes messages between agents with queuing support.\n * \n * Events emitted:\n * - message.sent\n * - message.delivered\n * - message.queued\n * - message.expired\n */\ninterface MessageRouter {\n /** Send to a specific agent */\n sendToAgent(params: {\n from: string;\n to: string;\n payload: unknown;\n replyTo?: string;\n priority?: number;\n ttlMs?: number;\n }): Message;\n \n /** Broadcast to all agents in a scope */\n sendToScope(params: {\n from: string;\n scopeId: string;\n payload: unknown;\n excludeSender?: boolean;\n includeDescendants?: boolean;\n }): Message;\n \n /** Set delivery callback */\n onDeliver(handler: DeliveryHandler): void;\n \n /**\n * Flush queued messages for an agent (call when agent reconnects).\n * @returns Number of messages delivered\n */\n flushQueue(agentId: string): number;\n \n /** Get queue stats */\n getQueueStats(): {\n total: number;\n byAgent: Record<string, number>;\n };\n}\n\ninterface MessageRouterOptions {\n eventBus: EventBus;\n agents: AgentRegistry;\n scopes: ScopeManager;\n queue?: MessageQueueOptions;\n queueStore?: MessageQueueStore;\n}\n```\n\n---\n\n## Router Connection\n\n### Handler Types\n\n```typescript\n/**\n * Context passed to handlers.\n */\ninterface HandlerContext {\n /** Current session */\n session: Session;\n /** Request metadata */\n requestId: string;\n /** Abort signal for cancellation */\n signal: AbortSignal;\n}\n\n/**\n * A request handler function.\n */\ntype Handler<TParams = unknown, TResult = unknown> = (\n params: TParams,\n ctx: HandlerContext\n) => Promise<TResult>;\n\n/**\n * Registry of method handlers.\n */\ntype HandlerRegistry = Record<string, Handler>;\n\n/**\n * Middleware function.\n */\ntype Middleware = (\n method: string,\n params: unknown,\n ctx: HandlerContext,\n next: () => Promise<unknown>\n) => Promise<unknown>;\n```\n\n### RouterConnection\n\n```typescript\n/**\n * JSON-RPC router that dispatches to handlers.\n * Supports session resume on reconnect.\n */\ninterface RouterConnection {\n /** Start processing messages */\n start(): Promise<void>;\n \n /** Stop processing and close */\n close(): Promise<void>;\n \n /** Promise that resolves when connection closes */\n readonly closed: Promise<void>;\n \n /** Current session */\n readonly session: Session;\n \n /** Send notification to connected peer */\n notify(method: string, params: unknown): Promise<void>;\n}\n\ninterface RouterConnectionOptions {\n /** Bidirectional stream */\n stream: Stream;\n \n /** Method handlers */\n handlers: HandlerRegistry;\n \n /** Middleware chain (executed in order) */\n middleware?: Middleware[];\n \n /** Session manager for connection tracking */\n sessions: SessionManager;\n \n /** Resume token for reconnection (optional) */\n resumeToken?: string;\n \n /** Role for new sessions */\n role: SessionRole;\n \n /** Name for new sessions */\n name?: string;\n}\n```\n\n### Handler Factories\n\n```typescript\n/**\n * Create handlers for agent-related methods.\n */\nfunction createAgentHandlers(opts: {\n agents: AgentRegistry;\n sessions: SessionManager;\n permissions?: PermissionChecker;\n}): HandlerRegistry;\n\n/**\n * Create handlers for scope-related methods.\n */\nfunction createScopeHandlers(opts: {\n scopes: ScopeManager;\n agents: AgentRegistry;\n permissions?: PermissionChecker;\n}): HandlerRegistry;\n\n/**\n * Create handlers for subscription-related methods.\n */\nfunction createSubscriptionHandlers(opts: {\n subscriptions: SubscriptionManager;\n eventBus: EventBus;\n permissions?: PermissionChecker;\n}): HandlerRegistry;\n\n/**\n * Create handlers for message-related methods.\n */\nfunction createMessageHandlers(opts: {\n messages: MessageRouter;\n permissions?: PermissionChecker;\n}): HandlerRegistry;\n\n/**\n * Create handlers for connection lifecycle.\n */\nfunction createConnectionHandlers(opts: {\n sessions: SessionManager;\n}): HandlerRegistry;\n```\n\n---\n\n## Optional OOP Interface\n\n### MAPRouter Interface\n\n```typescript\n/**\n * OOP interface for implementing a MAP router.\n * All methods correspond to protocol methods.\n */\ninterface MAPRouter {\n // Connection\n connect(params: ConnectRequest, ctx: HandlerContext): Promise<ConnectResponse>;\n disconnect(params: DisconnectRequest, ctx: HandlerContext): Promise<void>;\n resume(params: ResumeRequest, ctx: HandlerContext): Promise<ResumeResponse>;\n \n // Agents\n registerAgent(params: RegisterAgentRequest, ctx: HandlerContext): Promise<RegisterAgentResponse>;\n unregisterAgent(params: UnregisterAgentRequest, ctx: HandlerContext): Promise<void>;\n listAgents(params: ListAgentsRequest, ctx: HandlerContext): Promise<Agent[]>;\n getAgent(params: GetAgentRequest, ctx: HandlerContext): Promise<Agent>;\n updateAgentState(params: UpdateAgentStateRequest, ctx: HandlerContext): Promise<Agent>;\n updateAgentMetadata(params: UpdateAgentMetadataRequest, ctx: HandlerContext): Promise<Agent>;\n \n // Scopes\n createScope(params: CreateScopeRequest, ctx: HandlerContext): Promise<Scope>;\n deleteScope(params: DeleteScopeRequest, ctx: HandlerContext): Promise<void>;\n listScopes(params: ListScopesRequest, ctx: HandlerContext): Promise<Scope[]>;\n getScope(params: GetScopeRequest, ctx: HandlerContext): Promise<Scope>;\n joinScope(params: JoinScopeRequest, ctx: HandlerContext): Promise<void>;\n leaveScope(params: LeaveScopeRequest, ctx: HandlerContext): Promise<void>;\n \n // Messages\n send(params: SendRequest, ctx: HandlerContext): Promise<SendResponse>;\n \n // Subscriptions\n subscribe(params: SubscribeRequest, ctx: HandlerContext): Promise<SubscribeResponse>;\n unsubscribe(params: UnsubscribeRequest, ctx: HandlerContext): Promise<void>;\n replay(params: ReplayRequest, ctx: HandlerContext): Promise<ReplayResponse>;\n}\n```\n\n### Router to Handlers Adapter\n\n```typescript\n/**\n * Convert a MAPRouter instance to a HandlerRegistry.\n */\nfunction routerToHandlers(router: MAPRouter): HandlerRegistry;\n\n/**\n * Example usage:\n * \n * class MyRouter implements MAPRouter {\n * // ... implement all methods\n * }\n * \n * const router = new MyRouter();\n * const connection = new RouterConnection(stream, {\n * handlers: routerToHandlers(router),\n * sessions,\n * });\n */\n```\n\n### Abstract Base Router\n\n```typescript\n/**\n * Abstract base class with default implementations using building blocks.\n * Extend and override specific methods as needed.\n */\nabstract class BaseMAPRouter implements MAPRouter {\n constructor(protected opts: {\n agents: AgentRegistry;\n scopes: ScopeManager;\n sessions: SessionManager;\n subscriptions: SubscriptionManager;\n messages: MessageRouter;\n eventBus: EventBus;\n });\n \n // Default implementations that delegate to building blocks\n async registerAgent(params, ctx) {\n return this.opts.agents.register({\n ...params,\n sessionId: ctx.session.id,\n });\n }\n \n // ... other defaults\n \n // Override in subclass for custom behavior\n}\n```\n\n---\n\n## Permissions\n\n### PermissionChecker\n\n```typescript\n/**\n * Permission check result.\n */\ninterface PermissionResult {\n allowed: boolean;\n reason?: string;\n}\n\n/**\n * Checks permissions at various levels.\n */\ninterface PermissionChecker {\n /** Check if method is allowed for session role */\n canCallMethod(session: Session, method: string): PermissionResult;\n \n /** Check if session can access a scope */\n canAccessScope(session: Session, scopeId: string, action: string): PermissionResult;\n \n /** Check if agent can perform action on target */\n canAgentPerform(agentId: string, action: string, targetId: string): PermissionResult;\n}\n\n/**\n * Permission rule definition.\n */\ninterface PermissionRule {\n /** Method pattern (glob supported) */\n method?: string;\n /** Required session role(s) */\n roles?: SessionRole[];\n /** Custom check function */\n check?: (session: Session, params: unknown) => PermissionResult;\n}\n\ninterface PermissionCheckerOptions {\n rules: PermissionRule[];\n /** Default behavior when no rule matches */\n defaultAllow?: boolean;\n}\n```\n\n### Permission Middleware\n\n```typescript\n/**\n * Middleware that checks method-level permissions.\n */\nfunction permissionMiddleware(checker: PermissionChecker): Middleware;\n```\n\n---\n\n## Resource Cleanup\n\n### ResourceCleaner\n\n```typescript\n/**\n * Strategy for handling cleanup.\n */\ninterface CleanupStrategy {\n /** Called for each stale session */\n onStaleSession?(session: Session): void;\n \n /** Called for each orphaned agent */\n onOrphanedAgent?(agent: RegisteredAgent): void;\n \n /** Called for each orphaned subscription */\n onOrphanedSubscription?(subscription: Subscription): void;\n \n /** Called for expired queued messages */\n onExpiredMessages?(count: number): void;\n}\n\n/**\n * Cleanup thresholds configuration.\n */\ninterface CleanupThresholds {\n /** Max time a session can be disconnected before expiring */\n sessionDisconnectMs?: number;\n /** Max time since last activity before session is stale */\n sessionInactiveMs?: number;\n /** Run cleanup every N milliseconds (0 = manual only) */\n intervalMs?: number;\n}\n\n/**\n * Handles cleanup of stale resources.\n */\ninterface ResourceCleaner {\n /**\n * Run cleanup cycle.\n * @returns Stats about what was cleaned\n */\n run(): Promise<CleanupStats>;\n \n /** Start automatic cleanup interval */\n start(): void;\n \n /** Stop automatic cleanup */\n stop(): void;\n \n /** Check if automatic cleanup is running */\n readonly running: boolean;\n}\n\ninterface CleanupStats {\n sessionsExpired: number;\n agentsUnregistered: number;\n subscriptionsCancelled: number;\n messagesExpired: number;\n}\n\ninterface ResourceCleanerOptions {\n sessions: SessionManager;\n agents: AgentRegistry;\n subscriptions: SubscriptionManager;\n messages: MessageRouter;\n thresholds?: CleanupThresholds;\n strategy?: CleanupStrategy;\n}\n```\n\n---\n\n## Federation (Decorator Pattern)\n\n### FederationGateway\n\n```typescript\n/**\n * Peer system connection.\n */\ninterface PeerConnection {\n systemId: string;\n endpoint: string;\n status: 'connected' | 'disconnected' | 'connecting';\n connectedAt?: number;\n lastActivity?: number;\n}\n\n/**\n * Federation envelope for cross-system messages.\n */\ninterface FederationEnvelope {\n /** Original message */\n payload: unknown;\n /** Routing metadata */\n routing: {\n from: string;\n to: string;\n hops: string[];\n maxHops: number;\n };\n /** Timestamp */\n timestamp: number;\n}\n\n/**\n * Gateway for federation between MAP systems.\n */\ninterface FederationGateway {\n /** Connect to a peer system */\n connectPeer(params: {\n systemId: string;\n endpoint: string;\n credentials?: unknown;\n }): Promise<PeerConnection>;\n \n /** Route message to peer */\n routeToPeer(systemId: string, envelope: FederationEnvelope): Promise<void>;\n \n /** Handle incoming messages from peers */\n onPeerMessage(handler: (from: string, envelope: FederationEnvelope) => void): void;\n \n /** List connected peers */\n listPeers(): PeerConnection[];\n \n /** Disconnect from peer */\n disconnectPeer(systemId: string): void;\n \n /** Outage buffer for resilience */\n readonly buffer: OutageBuffer;\n}\n\n/**\n * Buffers messages during peer outages.\n */\ninterface OutageBuffer {\n /** Add message to buffer */\n add(systemId: string, envelope: FederationEnvelope): void;\n \n /** Flush buffered messages (on reconnect) */\n flush(systemId: string): FederationEnvelope[];\n \n /** Get buffer stats */\n stats(): { total: number; bySystem: Record<string, number> };\n}\n```\n\n### Federated Decorators\n\n```typescript\n/**\n * Wraps AgentRegistry to sync agents across systems.\n */\nclass FederatedAgentRegistry implements AgentRegistry {\n constructor(opts: {\n local: AgentRegistry;\n gateway: FederationGateway;\n sync?: {\n /** Sync local registrations to peers */\n onRegister?: boolean;\n /** Sync state changes to peers */\n onStateChange?: boolean;\n /** Include remote agents in list() */\n includeRemote?: boolean;\n };\n });\n}\n\n/**\n * Wraps ScopeManager to sync scopes across systems.\n */\nclass FederatedScopeManager implements ScopeManager {\n constructor(opts: {\n local: ScopeManager;\n gateway: FederationGateway;\n sync?: {\n onCreateScope?: boolean;\n onMembershipChange?: boolean;\n includeRemote?: boolean;\n };\n });\n}\n\n/**\n * Wraps MessageRouter to route messages across systems.\n */\nclass FederatedMessageRouter implements MessageRouter {\n constructor(opts: {\n local: MessageRouter;\n gateway: FederationGateway;\n agents: AgentRegistry;\n });\n}\n```\n\n---\n\n## Built-in Implementations\n\n### In-Memory Stores (Default)\n\n```typescript\nclass InMemoryEventStore implements EventStore { /* ... */ }\nclass InMemoryAgentStore implements AgentStore { /* ... */ }\nclass InMemoryScopeStore implements ScopeStore { /* ... */ }\nclass InMemorySessionStore implements SessionStore { /* ... */ }\nclass InMemorySubscriptionStore implements SubscriptionStore { /* ... */ }\nclass InMemoryMessageQueueStore implements MessageQueueStore { /* ... */ }\n```\n\n### Core Building Blocks\n\n```typescript\nclass EventBusImpl implements EventBus { /* ... */ }\nclass AgentRegistryImpl implements AgentRegistry { /* ... */ }\nclass ScopeManagerImpl implements ScopeManager { /* ... */ }\nclass SessionManagerImpl implements SessionManager { /* ... */ }\nclass SubscriptionManagerImpl implements SubscriptionManager { /* ... */ }\nclass MessageRouterImpl implements MessageRouter { /* ... */ }\nclass ResourceCleanerImpl implements ResourceCleaner { /* ... */ }\n```\n\n---\n\n## Usage Examples\n\n### Minimal Server (In-Memory)\n\n```typescript\nimport {\n EventBus,\n AgentRegistry,\n ScopeManager,\n SessionManager,\n SubscriptionManager,\n MessageRouter,\n RouterConnection,\n createAgentHandlers,\n createScopeHandlers,\n createSubscriptionHandlers,\n createMessageHandlers,\n createConnectionHandlers,\n} from '@anthropic/multi-agent-protocol/server';\n\n// Create building blocks with defaults\nconst eventBus = new EventBus();\nconst sessions = new SessionManager({ eventBus });\nconst agents = new AgentRegistry({ eventBus });\nconst scopes = new ScopeManager({ eventBus });\nconst subscriptions = new SubscriptionManager({ eventBus, scopes });\nconst messages = new MessageRouter({ eventBus, agents, scopes });\n\n// Wire up handlers\nconst handlers = {\n ...createConnectionHandlers({ sessions }),\n ...createAgentHandlers({ agents, sessions }),\n ...createScopeHandlers({ scopes, agents }),\n ...createSubscriptionHandlers({ subscriptions, eventBus }),\n ...createMessageHandlers({ messages }),\n};\n\n// Create router for each connection\nfunction handleConnection(stream: Stream) {\n const router = new RouterConnection(stream, {\n handlers,\n sessions,\n role: 'agent',\n });\n router.start();\n}\n```\n\n### With OOP Interface\n\n```typescript\nimport { BaseMAPRouter, routerToHandlers } from '@anthropic/multi-agent-protocol/server';\n\nclass MyRouter extends BaseMAPRouter {\n // Override specific methods\n async registerAgent(params, ctx) {\n // Custom logic before registration\n console.log(`Registering agent: ${params.name}`);\n \n // Call default implementation\n const agent = await super.registerAgent(params, ctx);\n \n // Custom logic after registration\n await this.notifyAdmins(agent);\n \n return agent;\n }\n}\n\nconst router = new MyRouter({ agents, scopes, sessions, subscriptions, messages, eventBus });\nconst connection = new RouterConnection(stream, {\n handlers: routerToHandlers(router),\n sessions,\n role: 'agent',\n});\n```\n\n### With Session Resume\n\n```typescript\nfunction handleConnection(stream: Stream, resumeToken?: string) {\n const router = new RouterConnection(stream, {\n handlers,\n sessions,\n role: 'agent',\n resumeToken, // Pass token from client's reconnect request\n });\n \n router.start().then(() => {\n if (resumeToken) {\n // Flush any queued messages\n const agentIds = router.session.agentIds;\n for (const agentId of agentIds) {\n messages.flushQueue(agentId);\n }\n }\n });\n}\n```\n\n### With Cleanup\n\n```typescript\nimport { ResourceCleaner } from '@anthropic/multi-agent-protocol/server';\n\nconst cleaner = new ResourceCleaner({\n sessions,\n agents,\n subscriptions,\n messages,\n thresholds: {\n sessionDisconnectMs: 5 * 60 * 1000, // 5 minutes\n sessionInactiveMs: 30 * 60 * 1000, // 30 minutes\n intervalMs: 60 * 1000, // Run every minute\n },\n strategy: {\n onStaleSession(session) {\n console.log(`Cleaning up stale session: ${session.id}`);\n },\n },\n});\n\n// Start automatic cleanup\ncleaner.start();\n\n// Or run manually\nconst stats = await cleaner.run();\nconsole.log(`Cleaned: ${stats.sessionsExpired} sessions, ${stats.agentsUnregistered} agents`);\n```\n\n### With Custom Storage\n\n```typescript\nimport { RedisEventStore, PostgresAgentStore } from './my-stores';\n\nconst eventBus = new EventBus({\n store: new RedisEventStore({ url: process.env.REDIS_URL }),\n});\n\nconst agents = new AgentRegistry({\n eventBus,\n store: new PostgresAgentStore({ connectionString: process.env.DATABASE_URL }),\n});\n```\n\n### With Permissions\n\n```typescript\nimport { PermissionChecker, permissionMiddleware } from '@anthropic/multi-agent-protocol/server';\n\nconst permissions = new PermissionChecker({\n rules: [\n { method: 'map/agents/register', roles: ['agent'] },\n { method: 'map/agents/list', roles: ['client', 'agent'] },\n { method: 'map/send', roles: ['agent', 'client'] },\n { method: 'map/scopes/create', roles: ['agent'] },\n ],\n defaultAllow: false,\n});\n\nconst router = new RouterConnection(stream, {\n handlers,\n sessions,\n role: 'agent',\n middleware: [\n permissionMiddleware(permissions),\n ],\n});\n```\n\n### With Federation\n\n```typescript\nimport {\n FederationGateway,\n FederatedAgentRegistry,\n FederatedMessageRouter,\n} from '@anthropic/multi-agent-protocol/server/federation';\n\nconst gateway = new FederationGateway({\n systemId: 'system-a',\n buffer: { maxMessages: 10000 },\n});\n\n// Connect to peer\nawait gateway.connectPeer({\n systemId: 'system-b',\n endpoint: 'wss://system-b.example.com/federation',\n});\n\n// Wrap local building blocks\nconst federatedAgents = new FederatedAgentRegistry({\n local: agents,\n gateway,\n sync: { onRegister: true, includeRemote: true },\n});\n\nconst federatedMessages = new FederatedMessageRouter({\n local: messages,\n gateway,\n agents: federatedAgents,\n});\n\n// Use federated versions in handlers\nconst handlers = {\n ...createAgentHandlers({ agents: federatedAgents, sessions }),\n ...createMessageHandlers({ messages: federatedMessages }),\n};\n```\n\n---\n\n## File Structure\n\n```\nts-sdk/src/\n├── server/\n│ ├── index.ts # Main exports\n│ ├── types.ts # All interfaces and types\n│ │\n│ ├── events/\n│ │ ├── index.ts\n│ │ ├── event-bus.ts # EventBus implementation\n│ │ └── stores/\n│ │ └── in-memory.ts # InMemoryEventStore\n│ │\n│ ├── agents/\n│ │ ├── index.ts\n│ │ ├── registry.ts # AgentRegistry implementation\n│ │ ├── handlers.ts # createAgentHandlers\n│ │ └── stores/\n│ │ └── in-memory.ts # InMemoryAgentStore\n│ │\n│ ├── scopes/\n│ │ ├── index.ts\n│ │ ├── manager.ts # ScopeManager implementation\n│ │ ├── handlers.ts # createScopeHandlers\n│ │ └── stores/\n│ │ └── in-memory.ts # InMemoryScopeStore\n│ │\n│ ├── sessions/\n│ │ ├── index.ts\n│ │ ├── manager.ts # SessionManager implementation\n│ │ └── stores/\n│ │ └── in-memory.ts # InMemorySessionStore\n│ │\n│ ├── subscriptions/\n│ │ ├── index.ts\n│ │ ├── manager.ts # SubscriptionManager implementation\n│ │ ├── handlers.ts # createSubscriptionHandlers\n│ │ ├── causal-buffer.ts # CausalEventBuffer for ordering\n│ │ └── stores/\n│ │ └── in-memory.ts # InMemorySubscriptionStore\n│ │\n│ ├── messages/\n│ │ ├── index.ts\n│ │ ├── router.ts # MessageRouter implementation\n│ │ ├── handlers.ts # createMessageHandlers\n│ │ └── stores/\n│ │ └── in-memory.ts # InMemoryMessageQueueStore\n│ │\n│ ├── router/\n│ │ ├── index.ts\n│ │ ├── connection.ts # RouterConnection implementation\n│ │ ├── middleware.ts # Built-in middleware\n│ │ ├── interface.ts # MAPRouter interface\n│ │ ├── base-router.ts # BaseMAPRouter abstract class\n│ │ └── adapter.ts # routerToHandlers\n│ │\n│ ├── permissions/\n│ │ ├── index.ts\n│ │ ├── checker.ts # PermissionChecker implementation\n│ │ └── middleware.ts # permissionMiddleware\n│ │\n│ ├── cleanup/\n│ │ ├── index.ts\n│ │ └── cleaner.ts # ResourceCleaner implementation\n│ │\n│ └── federation/\n│ ├── index.ts\n│ ├── gateway.ts # FederationGateway\n│ ├── buffer.ts # OutageBuffer\n│ ├── decorators/\n│ │ ├── agents.ts # FederatedAgentRegistry\n│ │ ├── scopes.ts # FederatedScopeManager\n│ │ └── messages.ts # FederatedMessageRouter\n│ └── handlers.ts # createFederationHandlers\n```\n","priority":1,"archived":0,"archived_at":null,"created_at":"2026-01-29 19:27:32","updated_at":"2026-01-29 19:38:46","parent_id":null,"parent_uuid":null,"relationships":[],"tags":["architecture","building-blocks","map","server-sdk"]}
3
- {"id":"s-1x3s","uuid":"fc0cd465-2f0f-4ac9-af94-2ee1a7979a4f","title":"MAP Protocol Improvements - Server SDK Implementation Feedback","file_path":"specs/s-1x3s_map_protocol_improvements_server_sdk_implementatio.md","content":"# MAP Protocol Improvements\n\nFriction points and improvement suggestions discovered during the Server SDK implementation.\n\n## Overview\n\nThis spec captures protocol-level issues that create friction for implementers or break interoperability between MAP systems. Items are prioritized by impact on the ecosystem.\n\n---\n\n## High Priority\n\n### 1. Federation Identity Format\n\n**Problem:** Remote agent/scope IDs have no standardized format. Each implementation invents its own.\n\n**Current State:**\n```typescript\n// Our implementation\n\"remote:system-west:agent-123\"\n\n// Other implementations might use\n\"system-west/agent-123\"\n\"system-west::agent-123\"\n{ system: \"system-west\", id: \"agent-123\" }\n```\n\n**Impact:** Federation between different MAP implementations will fail due to ID parsing incompatibility.\n\n**Proposed Solution:**\n```typescript\n// Standardize on URI-like format\n\"map://{systemId}/{entityType}/{entityId}\"\n\n// Examples\n\"map://system-west/agent/agent-123\"\n\"map://system-east/scope/room-456\"\n\n// Or simpler colon-delimited format\n\"{systemId}:{entityType}:{entityId}\"\n\"system-west:agent:agent-123\"\n```\n\n**Spec Change:** Add \"Federated Identity\" section defining canonical format.\n\n---\n\n### 2. Session Resume Semantics\n\n**Problem:** Protocol doesn't specify what state is preserved on resume.\n\n**Current State:** Each implementation decides independently:\n- Subscriptions preserved? (usually yes)\n- Agent registrations preserved? (usually yes)\n- Agent states preserved? (varies)\n- Queued messages delivered? (varies)\n- Scope memberships preserved? (varies)\n\n**Impact:** Clients can't rely on consistent behavior across MAP servers.\n\n**Proposed Solution:**\n```typescript\ninterface ResumeGuarantees {\n // MUST be preserved\n sessionId: true;\n agentRegistrations: true;\n subscriptions: true;\n scopeMemberships: true;\n \n // SHOULD be preserved (with limits)\n queuedMessages: {\n preserved: true;\n maxAge: \"5 minutes\";\n maxCount: 1000;\n };\n \n // MAY be preserved\n agentMetadata: \"implementation-defined\";\n}\n```\n\n**Spec Change:** Add \"Session Resume\" section with explicit guarantees.\n\n---\n\n### 3. Baseline Permission Model\n\n**Problem:** Protocol defines roles but no permission semantics.\n\n**Current State:**\n- Roles exist: `client`, `agent`, `gateway`\n- No standard for what each role can do\n- No resource-level permissions defined\n\n**Impact:** Security model varies wildly between implementations. Can't build secure multi-tenant systems reliably.\n\n**Proposed Solution:**\n```typescript\n// Baseline permissions (implementations MAY be more restrictive)\nconst baselinePermissions = {\n client: {\n \"map/agents/list\": true,\n \"map/agents/get\": true,\n \"map/scopes/list\": true,\n \"map/scopes/get\": true,\n \"map/subscribe\": true,\n \"map/send\": \"to-joined-scopes-only\",\n },\n agent: {\n \"map/agents/*\": true,\n \"map/scopes/*\": true,\n \"map/send\": true,\n \"map/subscribe\": true,\n },\n gateway: {\n \"*\": true, // Full access for federation\n },\n};\n\n// Resource-level rules\nconst resourcePermissions = {\n \"map/send\": {\n // Agents can message:\n // - Any agent in a shared scope\n // - Any agent that messaged them first\n rule: \"shared-scope OR prior-contact\"\n },\n \"map/scopes/delete\": {\n // Only creator or gateway can delete\n rule: \"creator OR gateway\"\n },\n};\n```\n\n**Spec Change:** Add \"Permission Model\" section with baseline and extension points.\n\n---\n\n## Medium Priority\n\n### 4. Message Recipient Disambiguation\n\n**Problem:** `to` field can be agent ID or scope ID with no way to distinguish.\n\n**Current State:**\n```typescript\n// Implementation must check both registries\nsend({ to: \"abc123\", payload: {} })\n// Is \"abc123\" an agent or scope?\n```\n\n**Proposed Solutions:**\n\nOption A - Prefix convention:\n```typescript\nsend({ to: \"agent:abc123\", payload: {} })\nsend({ to: \"scope:xyz789\", payload: {} })\n```\n\nOption B - Explicit field:\n```typescript\nsend({ toAgent: \"abc123\", payload: {} })\n// or\nsend({ toScope: \"xyz789\", payload: {} })\n```\n\nOption C - Type field:\n```typescript\nsend({ to: \"abc123\", toType: \"agent\", payload: {} })\n```\n\n**Recommendation:** Option A (prefix) - backward compatible, self-describing.\n\n---\n\n### 5. Agent State Machine Extensibility\n\n**Problem:** Fixed states (`idle`, `busy`, `suspended`, `stopped`) don't cover all use cases.\n\n**Current State:**\n```typescript\ntype AgentState = \"idle\" | \"busy\" | \"suspended\" | \"stopped\";\n// No room for: \"initializing\", \"error\", \"thinking\", \"waiting_for_human\"\n```\n\n**Proposed Solution:**\n```typescript\n// Standard states remain\ntype StandardState = \"idle\" | \"busy\" | \"suspended\" | \"stopped\";\n\n// Allow custom states with prefix\ntype CustomState = `custom:${string}`;\n\ntype AgentState = StandardState | CustomState;\n\n// Examples\nagent.state = \"custom:thinking\";\nagent.state = \"custom:awaiting_approval\";\n```\n\n**Spec Change:** Allow `custom:*` states, document standard state semantics.\n\n---\n\n### 6. Subscription Filter Expressiveness\n\n**Problem:** Filters only support implicit AND, no OR or complex conditions.\n\n**Current State:**\n```typescript\n// All criteria are AND'd together\nfilter: {\n eventTypes: [\"a\", \"b\"], // type is \"a\" OR \"b\" (within field)\n scopes: [\"s1\"], // AND scope is \"s1\"\n agents: [\"a1\"], // AND agent is \"a1\"\n}\n// Can't express: \"events in scope-1 OR events about agent-123\"\n```\n\n**Proposed Solution:**\n```typescript\n// Add explicit operators\nfilter: {\n $or: [\n { scopes: [\"scope-1\"] },\n { agents: [\"agent-123\"] },\n ]\n}\n\n// Or keep simple with \"any\" modifier\nfilter: {\n eventTypes: [\"agent.registered\"],\n match: \"any\", // instead of default \"all\"\n scopes: [\"s1\", \"s2\"],\n agents: [\"a1\"],\n}\n```\n\n**Recommendation:** Start with `match: \"any\" | \"all\"` for simplicity.\n\n---\n\n### 7. Scope Hierarchy Semantics\n\n**Problem:** Edge cases for hierarchy operations aren't specified.\n\n**Questions:**\n1. Delete parent with children - error, orphan, or cascade?\n2. Does membership inherit down the hierarchy?\n3. Do permissions cascade?\n4. Can a scope be reparented?\n\n**Proposed Solution:**\n```typescript\n// Explicit options on operations\nscopes.delete(id, {\n onChildren: \"error\" | \"orphan\" | \"cascade\", // default: \"error\"\n});\n\nscopes.getMembers(id, {\n includeDescendants: boolean, // default: false\n});\n\nscopes.isMember(scopeId, agentId, {\n checkAncestors: boolean, // default: false\n});\n\n// Reparenting\nscopes.update(id, {\n parentId: newParentId | null, // null = make root\n});\n```\n\n**Spec Change:** Document default behaviors and available options.\n\n---\n\n## Low Priority\n\n### 8. Multi-Cause Event Dependencies\n\n**Problem:** `causedBy` only allows one parent event.\n\n**Current State:**\n```typescript\n// Can only reference one cause\nevent.causedBy = \"event-123\";\n\n// But scope.agent.joined is caused by BOTH:\n// - agent.registered\n// - scope.created\n```\n\n**Proposed Solution:**\n```typescript\n// Allow array\ncausedBy?: string | string[];\n\n// Example\n{\n type: \"scope.agent.joined\",\n causedBy: [\"agent-registered-event\", \"scope-created-event\"],\n}\n```\n\n**Consideration:** Complicates causal ordering algorithm. May be better to keep single-cause and document as \"primary cause\".\n\n---\n\n### 9. Message Type Field\n\n**Problem:** No way to filter/route messages by type without inspecting payload.\n\n**Current State:**\n```typescript\n// Type buried in payload\n{ from: \"a\", to: \"b\", payload: { type: \"chat\", text: \"hi\" } }\n```\n\n**Proposed Solution:**\n```typescript\n// Top-level optional field\ninterface Message {\n id: string;\n from: string;\n to: string;\n messageType?: string; // NEW\n payload: unknown;\n}\n\n// Enables filtering\nsubscriptions.create({\n filter: { messageTypes: [\"chat\", \"command\"] }\n});\n```\n\n---\n\n### 10. Capability Discovery\n\n**Problem:** No way to discover agent capabilities without external knowledge.\n\n**Current State:**\n```typescript\n// Must know agent IDs in advance\n// No way to ask \"who can translate French?\"\n```\n\n**Proposed Solution:**\n```typescript\n// Add capabilities to agent registration\nagents.register({\n name: \"Translator\",\n capabilities: [\"translate:fr\", \"translate:de\"],\n});\n\n// Query by capability\nagents.list({ capability: \"translate:fr\" });\n\n// Or dedicated discovery method\nagents.discover({ capability: \"translate:*\" });\n```\n\n---\n\n## Summary\n\n| # | Issue | Priority | Breaking Change |\n|---|-------|----------|-----------------|\n| 1 | Federation ID format | High | Yes (federation) |\n| 2 | Resume semantics | High | No (documentation) |\n| 3 | Permission model | High | No (additive) |\n| 4 | Recipient disambiguation | Medium | Yes (wire format) |\n| 5 | Agent state extensibility | Medium | No (additive) |\n| 6 | Filter expressiveness | Medium | No (additive) |\n| 7 | Hierarchy semantics | Medium | No (documentation) |\n| 8 | Multi-cause events | Low | No (additive) |\n| 9 | Message type field | Low | No (additive) |\n| 10 | Capability discovery | Low | No (additive) |\n\n## Implementation Path\n\n**Phase 1 - Documentation (No Breaking Changes):**\n- Resume semantics (#2)\n- Permission model baseline (#3)\n- Hierarchy semantics (#7)\n\n**Phase 2 - Additive Features:**\n- Agent state extensibility (#5)\n- Filter expressiveness (#6)\n- Message type field (#9)\n- Multi-cause events (#8)\n- Capability discovery (#10)\n\n**Phase 3 - Breaking Changes (Major Version):**\n- Federation ID format (#1)\n- Recipient disambiguation (#4)\n","priority":1,"archived":0,"archived_at":null,"created_at":"2026-01-29 21:20:42","updated_at":"2026-01-29 21:20:42","parent_id":null,"parent_uuid":null,"relationships":[{"from":"s-1x3s","from_type":"spec","to":"s-10j2","to_type":"spec","type":"discovered-from"}],"tags":["breaking-changes","feedback","improvements","protocol","server-sdk"]}
4
- {"id":"s-2uns","uuid":"8e6f784a-b340-4215-b9e3-38a1ffbbb24a","title":"Client-Server SDK Integration Tests","file_path":"specs/s-2uns_client_server_sdk_integration_tests.md","content":"# Client-Server SDK Integration Tests\n\n## Overview\n\nCreate integration tests that verify the client SDK and server SDK work together correctly. Currently, client SDK tests use a separate `TestServer` mock implementation, while server SDK tests exercise building blocks in isolation. This spec defines tests that wire both SDKs together to validate end-to-end protocol compliance.\n\n## Motivation\n\n### Current State\n\n```\n┌─────────────────────────────────────────────────────────────┐\n│ Client SDK Tests (integration.test.ts) │\n│ │\n│ ClientConnection ──────► TestServer (ad-hoc mock) │\n│ AgentConnection ──────► TestServer │\n└─────────────────────────────────────────────────────────────┘\n\n┌─────────────────────────────────────────────────────────────┐\n│ Server SDK Tests (server-sdk-integration.test.ts) │\n│ │\n│ Direct API calls ──────► Building blocks │\n│ (No JSON-RPC layer) │\n└─────────────────────────────────────────────────────────────┘\n```\n\n### Problems\n\n1. **Protocol Drift**: Client and server could implement protocol differently\n2. **Missing Coverage**: JSON-RPC serialization/deserialization between SDKs untested\n3. **Type Mismatches**: Request/response types could diverge\n4. **Edge Cases**: Error handling across the wire untested\n5. **Duplicate Code**: TestServer reimplements what server SDK already provides\n\n### Desired State\n\n```\n┌─────────────────────────────────────────────────────────────┐\n│ Client-Server Integration Tests │\n│ │\n│ ClientConnection ──┐ │\n│ AgentConnection ──┼──► StreamPair ──► RouterConnectionImpl│\n│ GatewayConnection ─┘ │ │ │\n│ │ ▼ │\n│ │ Server SDK Building │\n│ │ Blocks (real impl) │\n└─────────────────────────────────────────────────────────────┘\n```\n\n## Goals\n\n1. **Validate Protocol Compliance**: Both SDKs speak the same protocol\n2. **Test Real Integration**: No mocks between client and server\n3. **Cover All Protocol Methods**: Every MAP method tested end-to-end\n4. **Test Event Flow**: Subscriptions receive events from server actions\n5. **Test Error Propagation**: Server errors reach client correctly\n6. **Test Session Resume**: Reconnection works across SDKs\n\n## Non-Goals\n\n- Testing client SDK in isolation (existing tests cover this)\n- Testing server SDK in isolation (existing tests cover this)\n- Performance/load testing\n- Testing with real network (streams are sufficient)\n\n## Test Architecture\n\n### Test Harness\n\nCreate a reusable test harness that wires client and server SDKs:\n\n```typescript\ninterface IntegrationTestHarness {\n // Server-side building blocks\n eventBus: EventBusImpl;\n agents: AgentRegistryImpl;\n scopes: ScopeManagerImpl;\n sessions: SessionManagerImpl;\n subscriptions: SubscriptionManagerImpl;\n messages: MessageRouterImpl;\n handlers: HandlerRegistry;\n\n // Create a connected client\n createClient(name?: string): Promise<{\n connection: ClientConnection;\n disconnect: () => Promise<void>;\n }>;\n\n // Create a connected agent\n createAgent(name: string): Promise<{\n connection: AgentConnection;\n agent: RegisteredAgent;\n disconnect: () => Promise<void>;\n }>;\n\n // Cleanup all connections\n cleanup(): Promise<void>;\n}\n\nfunction createIntegrationHarness(): IntegrationTestHarness;\n```\n\n### Stream Pair Connection\n\n```typescript\nfunction connectClientToServer(\n harness: IntegrationTestHarness,\n clientConnection: BaseConnection\n): void {\n const [clientStream, serverStream] = createStreamPair();\n \n // Client uses one end\n clientConnection.setStream(clientStream);\n \n // Server uses the other end\n const router = new RouterConnectionImpl({\n stream: serverStream,\n handlers: harness.handlers,\n sessions: harness.sessions,\n role: \"agent\", // or \"client\"\n });\n router.start();\n}\n```\n\n## Test Categories\n\n### 1. Connection Lifecycle\n\n| Test | Description |\n|------|-------------|\n| Client connect | ClientConnection.connect() creates server session |\n| Agent connect | AgentConnection.connect() creates server session |\n| Client disconnect | Clean disconnect removes session |\n| Disconnect policy | `sessionPolicy: \"end\"` vs `\"pause\"` |\n| Session resume | Reconnect with resumeToken restores session |\n| Resume with agents | Resumed session retains registered agents |\n\n### 2. Agent Operations\n\n| Test | Description |\n|------|-------------|\n| Register agent | AgentConnection.register() creates agent on server |\n| List agents | ClientConnection.listAgents() returns registered agents |\n| Get agent | ClientConnection.getAgent(id) returns specific agent |\n| Update state | AgentConnection.updateState() changes server state |\n| Update metadata | AgentConnection.updateMetadata() merges metadata |\n| Unregister | AgentConnection.unregister() removes from server |\n| Multiple agents | Same session can register multiple agents |\n\n### 3. Scope Operations\n\n| Test | Description |\n|------|-------------|\n| Create scope | AgentConnection.createScope() creates on server |\n| List scopes | ClientConnection.listScopes() returns scopes |\n| Join scope | AgentConnection.joinScope() adds membership |\n| Leave scope | AgentConnection.leaveScope() removes membership |\n| Scope hierarchy | Parent/child scopes work correctly |\n| Scope members | getScope() includes member list |\n\n### 4. Messaging\n\n| Test | Description |\n|------|-------------|\n| Agent to agent | sendMessage() delivers to target agent |\n| Agent to scope | sendToScope() broadcasts to members |\n| Client to agent | Client can send messages to agents |\n| Message delivery | onMessage handler receives sent messages |\n| Message fields | id, from, to, timestamp preserved |\n| Exclude sender | excludeSender option works |\n\n### 5. Subscriptions & Events\n\n| Test | Description |\n|------|-------------|\n| Subscribe to events | subscribe() returns subscription |\n| Receive agent events | agent.registered triggers event |\n| Receive scope events | scope.created triggers event |\n| Filter by type | eventTypes filter works |\n| Filter by scope | scopes filter works |\n| Unsubscribe | unsubscribe() stops events |\n| Event ordering | Events arrive in causal order |\n\n### 6. Error Handling\n\n| Test | Description |\n|------|-------------|\n| Invalid method | Unknown method returns error |\n| Invalid params | Bad parameters return validation error |\n| Not found | Getting nonexistent agent returns error |\n| Permission denied | Unauthorized action returns error |\n| Error codes | JSON-RPC error codes are correct |\n\n### 7. Session Resume\n\n| Test | Description |\n|------|-------------|\n| Get resume token | Disconnect returns resumeToken |\n| Resume session | Connect with token restores session |\n| Agent preservation | Agents survive disconnect/resume |\n| Subscription restoration | Subscriptions survive resume |\n| Message queue flush | Queued messages delivered on resume |\n| Expired token | Old token fails gracefully |\n\n### 8. Concurrent Operations\n\n| Test | Description |\n|------|-------------|\n| Multiple clients | Several clients connect simultaneously |\n| Multiple agents | Several agents in same session |\n| Cross-agent messaging | Agent A messages Agent B |\n| Broadcast to scope | Message reaches all scope members |\n| Event fanout | Event reaches all subscribers |\n\n## Implementation Strategy\n\n### Phase 1: Test Harness\n\n1. Create `createIntegrationHarness()` function\n2. Wire all server SDK building blocks\n3. Implement `createClient()` and `createAgent()` helpers\n4. Add cleanup and teardown logic\n\n### Phase 2: Core Tests\n\n1. Connection lifecycle tests\n2. Agent registration tests\n3. Basic messaging tests\n4. Basic subscription tests\n\n### Phase 3: Advanced Tests\n\n1. Scope hierarchy tests\n2. Session resume tests\n3. Error handling tests\n4. Concurrent operation tests\n\n### Phase 4: Edge Cases\n\n1. Large message payloads\n2. Rapid connect/disconnect\n3. Many simultaneous subscriptions\n4. Deep scope hierarchies\n\n## File Structure\n\n```\nsrc/__tests__/\n├── client-server-integration.test.ts # Main integration tests\n└── helpers/\n └── integration-harness.ts # Test harness utilities\n```\n\n## Success Criteria\n\n- [ ] All protocol methods tested end-to-end\n- [ ] Event delivery verified for all event types\n- [ ] Session resume works across disconnect\n- [ ] Error codes match between client and server\n- [ ] No type mismatches discovered\n- [ ] Tests run in < 10 seconds\n\n## Dependencies\n\n- Server SDK building blocks (complete)\n- Client SDK connections (complete)\n- Stream pair utilities (complete)\n\n## Risks\n\n1. **Stream timing**: Async streams may need careful synchronization\n2. **Event ordering**: Causal ordering adds complexity to assertions\n3. **Cleanup**: Must ensure all connections close to avoid test hangs\n\n## Open Questions\n\n1. Should we deprecate `TestServer` after this? Or keep both?\n2. Should integration harness be exported for external use?\n3. How to handle federation integration tests (needs two server instances)?\n","priority":1,"archived":0,"archived_at":null,"created_at":"2026-01-29 21:21:32","updated_at":"2026-01-29 21:21:32","parent_id":null,"parent_uuid":null,"relationships":[],"tags":["client-sdk","integration","server-sdk","testing"]}
5
- {"id":"s-7hnb","uuid":"abc49215-6278-4cf2-bec7-6fc09e8d5f32","title":"MAP Server SDK - MAPServer Convenience Layer","file_path":"specs/s-7hnb_map_server_sdk_mapserver_convenience_layer.md","content":"# MAP Server SDK - MAPServer Convenience Layer\n\n## Overview\n\nThis spec defines a convenience layer for the MAP Server SDK that reduces boilerplate while preserving full access to the underlying building blocks. The goal is **progressive disclosure**: simple things simple, complex things possible.\n\n**Parent spec:** [[s-10j2]] (MAP Server SDK - Building Blocks Architecture)\n\n## Problem Statement\n\nThe current building blocks architecture is powerful and composable, but requires significant boilerplate for common use cases:\n\n1. **~50 lines to create a basic server** - Must instantiate 6+ building blocks, combine 5+ handler factories, and wire event delivery manually\n2. **Event delivery is manual** - Users must wire `eventBus.on(\"*\")` → `subscriptions.match()` → `router.notify()` themselves\n3. **No \"just works\" option** - Users must understand the full dependency graph before writing any code\n4. **Connection tracking is DIY** - Users must track active connections for cleanup and event delivery\n\nThe integration test harness (`integration-harness.ts`) is ~400 lines, demonstrating the complexity.\n\n## Design Philosophy\n\n### Core Principles\n\n1. **Thin wrapper, not replacement** - `MAPServer` wires building blocks together; all internals remain accessible\n2. **Progressive disclosure** - Start with defaults, customize as needed\n3. **Escape hatches everywhere** - Replace any component or hook into any lifecycle point\n4. **No magic** - Behavior is explicit and predictable\n\n### What MAPServer Is NOT\n\n- NOT a replacement for building blocks (they're still the primitive)\n- NOT required (advanced users can still wire manually)\n- NOT opinionated about transport (accepts any Stream)\n\n## API Design\n\n### MAPServerOptions\n\n```typescript\ninterface MAPServerOptions {\n // === Basic Config ===\n /** Server name for connect responses */\n name?: string;\n /** Server version for connect responses */\n version?: string;\n /** Server capabilities advertised to clients */\n capabilities?: ParticipantCapabilities;\n \n // === Building Block Overrides ===\n /** \n * Replace EventBus entirely.\n * NOTE: If provided, stores.events is ignored.\n */\n eventBus?: EventBus;\n /** \n * Replace AgentRegistry entirely.\n * NOTE: If provided, stores.agents is ignored.\n */\n agents?: AgentRegistry;\n /** \n * Replace ScopeManager entirely.\n * NOTE: If provided, stores.scopes is ignored.\n */\n scopes?: ScopeManager;\n /** \n * Replace SessionManager entirely.\n * NOTE: If provided, stores.sessions is ignored.\n */\n sessions?: SessionManager;\n /** \n * Replace SubscriptionManager entirely.\n * NOTE: If provided, stores.subscriptions is ignored.\n */\n subscriptions?: SubscriptionManager;\n /** \n * Replace MessageRouter entirely.\n * NOTE: If provided, stores.messages is ignored.\n */\n messages?: MessageRouter;\n \n // === Storage Overrides ===\n /** \n * Custom stores (uses default impl with your store).\n * NOTE: Only applies to building blocks NOT explicitly provided above.\n */\n stores?: {\n events?: EventStore;\n agents?: AgentStore;\n sessions?: SessionStore;\n scopes?: ScopeStore;\n subscriptions?: SubscriptionStore;\n messages?: MessageQueueStore;\n };\n \n // === Handler Customization ===\n /** Replace all handlers entirely */\n handlers?: HandlerRegistry;\n /** Add handlers to defaults (merged after built-in handlers) */\n additionalHandlers?: HandlerRegistry;\n /** Middleware chain (applied to all requests) */\n middleware?: Middleware[];\n \n // === Event Delivery ===\n /** Event delivery configuration */\n eventDelivery?: {\n /** Enable automatic event delivery (default: true) */\n enabled?: boolean;\n /** Custom filter for event delivery */\n filter?: (event: MAPEvent, subscription: ServerSubscription) => boolean;\n };\n \n // === Session/Cleanup Config ===\n /** Session resume window in ms (default: 300000 = 5 minutes) */\n resumeWindowMs?: number;\n}\n```\n\n### MAPServer Class\n\n```typescript\nclass MAPServer {\n // === Building Blocks (readonly access) ===\n readonly eventBus: EventBus;\n readonly agents: AgentRegistry;\n readonly scopes: ScopeManager;\n readonly sessions: SessionManager;\n readonly subscriptions: SubscriptionManager;\n readonly messages: MessageRouter;\n readonly handlers: HandlerRegistry;\n \n // === Connection Tracking ===\n /** \n * Active connections keyed by session ID.\n * Connections are added on accept() and removed when closed.\n */\n readonly connections: ReadonlyMap<string, RouterConnection>;\n \n // === Constructor ===\n constructor(options?: MAPServerOptions);\n \n // === Connection Lifecycle ===\n /**\n * Accept a new connection.\n * Creates RouterConnection, tracks it, and wires up cleanup.\n * \n * NOTE: Caller must call router.start() to begin processing.\n * This allows pre-start configuration if needed.\n */\n accept(stream: Stream, options?: AcceptOptions): RouterConnection;\n \n /**\n * Close the server.\n * Gracefully disconnects all clients with optional timeout.\n * \n * NOTE: Queued messages for offline agents are lost on shutdown.\n * NOTE: Disconnected-but-resumable sessions are force-expired.\n */\n close(options?: CloseOptions): Promise<void>;\n \n // === Convenience Methods (delegate to building blocks) ===\n /** Subscribe to events (delegates to eventBus.on) */\n on(type: string | string[], handler: (event: MAPEvent) => void): () => void;\n \n /** Emit an event (delegates to eventBus.emit) */\n emit(event: Omit<MAPEvent, 'id' | 'timestamp'>): MAPEvent;\n}\n\ninterface AcceptOptions {\n /** \n * Connection role (default: 'agent').\n * - 'client': Observer that queries and subscribes\n * - 'agent': Active participant that registers and sends messages\n * - 'gateway': Federation peer\n */\n role?: 'client' | 'agent' | 'gateway';\n /** Name for the session */\n name?: string;\n /** Resume token for reconnection */\n resumeToken?: string;\n}\n\ninterface CloseOptions {\n /** Timeout for graceful shutdown in ms (default: 5000) */\n timeout?: number;\n /** Force close without waiting for graceful disconnect */\n force?: boolean;\n}\n```\n\n## Behavior\n\n### Construction\n\nWhen `new MAPServer(options)` is called:\n\n1. **Create building blocks** in dependency order, respecting user overrides:\n ```typescript\n // User-provided blocks take precedence over stores\n // Auto-created blocks use user-provided blocks as dependencies\n \n const eventBus = options.eventBus ?? new EventBusImpl({\n store: options.stores?.events // Only used if eventBus not provided\n });\n \n const sessions = options.sessions ?? new SessionManagerImpl({ \n eventBus, // Uses user's eventBus if provided\n store: options.stores?.sessions,\n resumeWindowMs: options.resumeWindowMs\n });\n \n const agents = options.agents ?? new AgentRegistryImpl({ \n eventBus, // Uses user's eventBus if provided\n store: options.stores?.agents \n });\n \n const scopes = options.scopes ?? new ScopeManagerImpl({ \n eventBus,\n store: options.stores?.scopes \n });\n \n const subscriptions = options.subscriptions ?? new SubscriptionManagerImpl({ \n eventBus, \n scopes, // Uses user's scopes if provided\n store: options.stores?.subscriptions \n });\n \n const messages = options.messages ?? new MessageRouterImpl({ \n eventBus, \n agents, // Uses user's agents if provided\n scopes,\n queueStore: options.stores?.messages \n });\n ```\n\n2. **Compose handlers**:\n ```typescript\n const handlers = options.handlers ?? combineHandlers(\n createConnectionHandlers({ \n sessions, \n serverName: options.name ?? 'MAPServer',\n serverVersion: options.version ?? '1.0.0',\n capabilities: options.capabilities\n }),\n createAgentHandlers({ agents }),\n createScopeHandlers({ scopes }),\n createMessageHandlers({ messages, scopes }),\n createSubscriptionHandlers({ subscriptions, eventBus }),\n options.additionalHandlers ?? {}\n );\n ```\n\n3. **Wire event delivery** (if `eventDelivery.enabled !== false`):\n ```typescript\n // Track sequence numbers per subscription\n const subscriptionSequences = new Map<string, number>();\n \n eventBus.on('*', (event) => {\n const matchingSubs = subscriptions.match(event);\n \n for (const subId of matchingSubs) {\n const sub = subscriptions.get(subId);\n if (!sub || sub.paused) continue;\n \n // Apply custom filter if provided\n if (options.eventDelivery?.filter) {\n if (!options.eventDelivery.filter(event, sub)) continue;\n }\n \n const router = this.#findRouterForSession(sub.sessionId);\n if (!router) continue;\n \n // Increment sequence number for this subscription\n const seq = (subscriptionSequences.get(subId) ?? 0) + 1;\n subscriptionSequences.set(subId, seq);\n \n // Deliver event, handle errors gracefully\n router.notify(NOTIFICATION_METHODS.EVENT, {\n subscriptionId: subId,\n sequenceNumber: seq,\n eventId: event.id,\n timestamp: event.timestamp,\n event,\n }).catch((error) => {\n // Connection likely closed - remove from tracking\n console.warn(`Event delivery failed for session ${sub.sessionId}:`, error.message);\n this.#removeConnection(sub.sessionId);\n });\n }\n });\n ```\n\n### Connection Acceptance\n\nWhen `server.accept(stream, options)` is called:\n\n1. Create `RouterConnectionImpl`:\n ```typescript\n const router = new RouterConnectionImpl({\n stream,\n handlers: this.handlers,\n sessions: this.sessions,\n middleware: this.#middleware,\n role: options?.role ?? 'agent', // Default to 'agent'\n name: options?.name,\n resumeToken: options?.resumeToken,\n });\n ```\n\n2. Track connection by session ID:\n ```typescript\n // Session is created when router.start() processes map/connect\n // We track the router, then update the key once session is established\n router.closed.then(() => {\n if (router.session) {\n this.#connections.delete(router.session.id);\n }\n });\n ```\n\n3. Return router (caller calls `start()`):\n ```typescript\n // Not auto-starting allows:\n // - Pre-start configuration\n // - Synchronous setup before async processing begins\n // - Consistent with RouterConnectionImpl behavior\n return router;\n ```\n\n**Why not auto-start?**\n- Consistency with underlying `RouterConnectionImpl` API\n- Allows attaching event handlers before processing starts\n- Explicit is better than implicit for connection lifecycle\n\n### Server Shutdown\n\nWhen `server.close(options)` is called:\n\n1. **Force-expire disconnected sessions** (within resume window):\n ```typescript\n // Clean up sessions that are disconnected but resumable\n this.sessions.expireStale(0); // Expire all disconnected immediately\n ```\n\n2. **Close active connections**:\n ```typescript\n const { timeout = 5000, force = false } = options ?? {};\n \n if (force) {\n // Immediately close all\n await Promise.all([...this.#connections.values()].map(r => r.close()));\n return;\n }\n \n // Graceful: attempt close with timeout\n const closePromises = [...this.#connections.values()].map(async (router) => {\n try {\n await Promise.race([\n router.close(),\n new Promise((_, reject) => \n setTimeout(() => reject(new Error('Timeout')), timeout)\n )\n ]);\n } catch {\n // Force close on timeout\n await router.close();\n }\n });\n \n await Promise.all(closePromises);\n ```\n\n3. **Note:** Queued messages (in `MessageRouter`) are lost on shutdown. This is intentional - a clean shutdown shouldn't leave state that can't be recovered.\n\n## Usage Examples\n\n### Simplest Case (3 lines)\n\n```typescript\nimport { MAPServer } from '@multi-agent-protocol/sdk/server';\n\nconst server = new MAPServer({ name: 'MyServer' });\n\n// Accept connections (e.g., from WebSocket)\nwss.on('connection', (ws) => {\n const stream = websocketToStream(ws);\n server.accept(stream).start();\n});\n```\n\n### With Custom Storage\n\n```typescript\nconst server = new MAPServer({\n name: 'PersistentServer',\n stores: {\n agents: new PostgresAgentStore(db),\n events: new RedisEventStore(redis),\n }\n});\n```\n\n### With Custom Handler\n\n```typescript\nconst server = new MAPServer({\n name: 'ExtendedServer',\n additionalHandlers: {\n 'myapp/custom/method': async (params, ctx) => {\n return { result: 'ok' };\n }\n }\n});\n```\n\n### With Middleware\n\n```typescript\nimport { permissionMiddleware, secureDefaults } from '@multi-agent-protocol/sdk/server';\n\nconst server = new MAPServer({\n name: 'SecureServer',\n middleware: [\n loggingMiddleware(),\n secureDefaults(),\n ]\n});\n```\n\n### Partial Building Block Override\n\n```typescript\n// Custom agent registry with special validation\nconst myAgents = new MyAgentRegistry({ eventBus: new EventBusImpl() });\n\nconst server = new MAPServer({\n agents: myAgents,\n // MessageRouter will be auto-created AND will use myAgents\n // This is the expected behavior - partial overrides compose correctly\n});\n\n// Verify: server.messages uses server.agents internally\n```\n\n### Full Control (escape hatch)\n\n```typescript\n// Create all custom building blocks\nconst eventBus = new EventBusImpl();\nconst agents = new MyCustomAgentRegistry({ eventBus });\n\n// Pass to MAPServer - other blocks use these as dependencies\nconst server = new MAPServer({\n eventBus,\n agents,\n // scopes, sessions, subscriptions, messages created automatically\n // and will use the provided eventBus and agents\n});\n\n// Direct access still works\nserver.agents.register({ name: 'SystemAgent', sessionId: 'system' });\nserver.eventBus.on('agent.registered', console.log);\n```\n\n### Disable Auto Event Delivery\n\n```typescript\nconst server = new MAPServer({\n name: 'CustomDelivery',\n eventDelivery: { enabled: false }\n});\n\n// Wire your own delivery logic\nserver.eventBus.on('*', (event) => {\n // Custom routing with custom sequence tracking...\n});\n```\n\n### Graceful Shutdown\n\n```typescript\n// On SIGTERM\nprocess.on('SIGTERM', async () => {\n console.log('Shutting down gracefully...');\n await server.close({ timeout: 10000 });\n console.log('All connections closed');\n process.exit(0);\n});\n\n// Emergency shutdown\nawait server.close({ force: true });\n```\n\n## Design Decisions\n\n### 1. Track Connections by Session ID\n\n**Decision:** MAPServer tracks active `RouterConnection`s in a Map keyed by session ID.\n\n**Rationale:**\n- Event delivery needs to find routers by session ID (subscriptions reference sessions)\n- `server.close()` needs to know what to disconnect\n- Users don't have to maintain this map themselves\n- Auto-cleanup on connection close prevents memory leaks\n- Session ID is the natural key (already unique, already tracked)\n\n### 2. Building Block Override Precedence\n\n**Decision:** If user provides a building block directly (e.g., `agents`), the corresponding store option (e.g., `stores.agents`) is ignored.\n\n**Rationale:**\n- Clear precedence avoids confusion\n- User-provided blocks are fully configured already\n- Stores are only for customizing default implementations\n- Documented in JSDoc comments\n\n### 3. Partial Overrides Use Correct Dependencies\n\n**Decision:** When user provides some blocks but not others, auto-created blocks use the user-provided blocks as dependencies.\n\n**Rationale:**\n- Expected behavior for dependency injection\n- Enables incremental customization\n- Example: custom `AgentRegistry` should be used by auto-created `MessageRouter`\n\n### 4. Default Role is 'agent'\n\n**Decision:** `accept()` defaults to `role: 'agent'` if not specified.\n\n**Rationale:**\n- Most connections are agents (active participants)\n- Clients (observers) are less common and can specify explicitly\n- Gateways (federation) definitely need explicit configuration\n- Matches the common use case\n\n### 5. Manual Start (Not Auto-Start)\n\n**Decision:** `accept()` returns a `RouterConnection` that the caller must `start()`.\n\n**Rationale:**\n- Consistency with underlying `RouterConnectionImpl` API\n- Allows attaching event handlers before processing\n- Explicit lifecycle is easier to reason about\n- No hidden async behavior in a synchronous-looking method\n\n### 6. Graceful Shutdown with Timeout\n\n**Decision:** `server.close()` attempts graceful disconnect with configurable timeout, then force closes.\n\n**Rationale:**\n- Expected behavior for \"close\" is clean shutdown\n- Timeout prevents hanging on stuck connections\n- `force: true` option for emergency shutdown\n- Default 5s timeout is reasonable for most cases\n\n### 7. Force-Expire Disconnected Sessions on Shutdown\n\n**Decision:** `close()` immediately expires all disconnected-but-resumable sessions.\n\n**Rationale:**\n- Clean shutdown should leave no dangling state\n- Resumable sessions won't be resumed after server stops\n- Prevents confusion about state after restart\n\n### 8. Queued Messages Lost on Shutdown\n\n**Decision:** Messages queued for offline agents are lost when `close()` is called.\n\n**Rationale:**\n- Clean shutdown semantics\n- No durable queue (that's a different feature)\n- Documented clearly so users can drain queues before shutdown if needed\n\n### 9. Event Delivery Error Handling\n\n**Decision:** If `router.notify()` fails during event delivery, log a warning and remove the connection from tracking.\n\n**Rationale:**\n- Failed notify usually means connection is dead\n- Prevents repeated failures to same dead connection\n- Warning helps debugging without crashing the server\n- Graceful degradation over hard failure\n\n### 10. Federation as Add-on\n\n**Decision:** Federation is NOT included in `MAPServer` by default.\n\n**Rationale:**\n- Federation has different lifecycle (peer connections, not client connections)\n- Most servers don't need federation\n- Keeps core simple\n- Can be enabled via wrapper or separate helper\n\nExample federation add-on:\n```typescript\nimport { enableFederation } from '@multi-agent-protocol/sdk/server/federation';\n\nconst federatedServer = enableFederation(server, {\n systemId: 'system-a',\n peers: [{ systemId: 'system-b', endpoint: 'ws://...' }]\n});\n```\n\n### 11. Permissions as Opt-in Middleware\n\n**Decision:** Permissions middleware is NOT auto-included.\n\n**Rationale:**\n- Default permissions that are too restrictive frustrate users\n- Default permissions that are too permissive aren't \"secure by default\"\n- Explicit opt-in is clearer than magic defaults\n- Provide presets for common patterns\n\nExample:\n```typescript\nimport { restrictedPermissions } from '@multi-agent-protocol/sdk/server';\n\nconst server = new MAPServer({\n middleware: [restrictedPermissions()]\n});\n```\n\n### 12. Transport Agnostic\n\n**Decision:** `MAPServer` accepts `Stream`, doesn't include transport.\n\n**Rationale:**\n- Transports vary (WebSocket, HTTP/SSE, stdio, in-memory)\n- Users often integrate with existing HTTP servers\n- Keep core focused on protocol, not I/O\n- Transport helpers can be separate exports\n\n## File Structure\n\n```\nts-sdk/src/server/\n├── index.ts # Add MAPServer export\n├── server.ts # NEW: MAPServer implementation\n├── types.ts # Add MAPServerOptions, AcceptOptions, CloseOptions\n└── ...existing files...\n```\n\n## Testing Strategy\n\n1. **Unit tests** for MAPServer:\n - Constructor creates all building blocks with defaults\n - Custom stores are passed to building blocks (when block not provided)\n - Custom building blocks are used when provided\n - Custom building blocks are used as dependencies for auto-created blocks\n - Handler composition works correctly\n - Event delivery wiring works with sequence numbers\n - Event delivery errors remove dead connections\n\n2. **Integration tests**:\n - Full client → MAPServer → response flow\n - Multiple concurrent connections\n - Graceful shutdown with active connections\n - Force shutdown\n - Reconnection with resume token\n - Partial building block override (verify dependencies work)\n\n3. **Comparison test**:\n - Verify MAPServer produces identical behavior to manual wiring\n - Side-by-side test with integration-harness.ts\n\n## Success Criteria\n\n- [ ] `MAPServer` can be instantiated with zero options\n- [ ] All building blocks accessible via readonly properties\n- [ ] Custom stores work correctly (only when block not provided)\n- [ ] Custom building blocks are used when provided\n- [ ] Partial overrides use correct dependencies (custom block used by auto-created blocks)\n- [ ] Additional handlers are merged with defaults\n- [ ] Middleware is applied to all requests\n- [ ] Event delivery works automatically with sequence numbers\n- [ ] Event delivery errors are handled gracefully (log + remove connection)\n- [ ] Event delivery can be disabled\n- [ ] Connection tracking works correctly (keyed by session ID)\n- [ ] Default role is 'agent'\n- [ ] Graceful shutdown closes all connections with timeout\n- [ ] Graceful shutdown force-expires disconnected sessions\n- [ ] Force shutdown closes immediately\n- [ ] Server capabilities can be configured\n- [ ] Existing building blocks tests still pass\n- [ ] Integration harness can be simplified to use MAPServer\n\n## Non-Goals (Deferred)\n\n- Transport helpers (WebSocket adapter, HTTP/SSE handler) - separate spec\n- Client SDK convenience layer (e.g., `ClientConnection.connectWebSocket()`) - separate spec\n- Permission presets (`secureDefaults`, `restrictedPermissions`) - separate spec\n- Federation helper (`enableFederation()`) - separate spec\n- Automatic ResourceCleaner integration - users can create their own if needed\n","priority":1,"archived":0,"archived_at":null,"created_at":"2026-01-29 23:24:56","updated_at":"2026-01-29 23:50:11","parent_id":null,"parent_uuid":null,"relationships":[{"from":"s-7hnb","from_type":"spec","to":"s-10j2","to_type":"spec","type":"depends-on"}],"tags":["convenience-layer","developer-experience","map","server-sdk"]}
6
- {"id":"s-243e","uuid":"4b9066b5-f4f7-4d0e-81eb-fd0fd867f52d","title":"MAP Client SDK - Transport Convenience Layer","file_path":"specs/s-243e_map_client_sdk_transport_convenience_layer.md","content":"# MAP Client SDK - Transport Convenience Layer\n\n## Overview\n\nThis spec defines convenience methods for the MAP Client SDK that simplify transport setup while preserving access to the low-level Stream-based API. The goal is to reduce boilerplate for the common case (WebSocket connections) without sacrificing flexibility.\n\n## Problem Statement\n\nThe current client SDK has a good API surface, but connecting to a MAP server requires verbose transport setup:\n\n```typescript\n// Current: 15+ lines just to connect with reconnection\nconst ws = new WebSocket('ws://localhost:8080');\nconst stream = websocketStream(ws);\nconst client = new ClientConnection(stream, {\n name: 'MyClient',\n createStream: async () => {\n const newWs = new WebSocket('ws://localhost:8080');\n return new Promise((resolve, reject) => {\n newWs.onopen = () => resolve(websocketStream(newWs));\n newWs.onerror = () => reject(new Error('Failed'));\n });\n },\n reconnection: { enabled: true }\n});\nawait client.connect();\n```\n\nThis is error-prone because:\n1. Users must manually wire WebSocket → Stream conversion\n2. Reconnection requires implementing a `createStream` factory that recreates the WebSocket\n3. The pattern is repeated for every client/agent instantiation\n4. Easy to forget error handling on WebSocket connection\n\n## Design Philosophy\n\n1. **Additive, not replacement** - Add static factory methods; don't change existing constructor\n2. **URL-first** - Most users just want to connect to a URL\n3. **Reconnection by default** - Production apps need reconnection; make it easy\n4. **Escape hatch preserved** - Custom streams still work via constructor\n\n## API Design\n\n### ClientConnection Static Methods\n\n```typescript\nclass ClientConnection {\n // === Existing (unchanged) ===\n constructor(stream: Stream, options?: ClientConnectionOptions);\n \n // === NEW: Static factory for URL-based connection ===\n /**\n * Connect to a MAP server via WebSocket URL.\n * \n * Handles:\n * - WebSocket creation and connection\n * - Stream wrapping\n * - Auto-configuration of createStream for reconnection\n * - Initial MAP protocol connect handshake\n * \n * @example\n * ```typescript\n * const client = await ClientConnection.connect('ws://localhost:8080', {\n * name: 'MyClient',\n * reconnection: true\n * });\n * \n * // Already connected, ready to use\n * const agents = await client.listAgents();\n * ```\n */\n static async connect(\n url: string,\n options?: ClientConnectOptions\n ): Promise<ClientConnection>;\n}\n```\n\n### AgentConnection Static Methods\n\n```typescript\nclass AgentConnection {\n // === Existing (unchanged) ===\n constructor(stream: Stream, options?: AgentConnectionOptions);\n \n // === NEW: Static factory for URL-based connection ===\n /**\n * Connect and register an agent via WebSocket URL.\n * \n * Handles:\n * - WebSocket creation and connection\n * - Stream wrapping \n * - Auto-configuration of createStream for reconnection\n * - Initial MAP protocol connect handshake\n * - Agent registration\n * \n * @example\n * ```typescript\n * const agent = await AgentConnection.connect('ws://localhost:8080', {\n * name: 'Worker',\n * role: 'processor',\n * reconnection: true\n * });\n * \n * // Already registered, ready to work\n * agent.onMessage(handleMessage);\n * await agent.busy();\n * ```\n */\n static async connect(\n url: string,\n options?: AgentConnectOptions\n ): Promise<AgentConnection>;\n}\n```\n\n### Options Types\n\n```typescript\n/**\n * Options for ClientConnection.connect()\n */\ninterface ClientConnectOptions {\n /** Client name for identification */\n name?: string;\n /** Client capabilities to advertise */\n capabilities?: ParticipantCapabilities;\n /** Authentication credentials */\n auth?: {\n method: 'bearer' | 'api-key' | 'mtls' | 'none';\n token?: string;\n };\n /** \n * Reconnection configuration.\n * - `true` = enable with defaults\n * - `false` or omitted = disabled\n * - `ReconnectionOptions` = enable with custom settings\n */\n reconnection?: boolean | ReconnectionOptions;\n /** Connection timeout in ms (default: 10000) */\n connectTimeout?: number;\n}\n\n/**\n * Options for AgentConnection.connect()\n */\ninterface AgentConnectOptions extends ClientConnectOptions {\n /** Agent role */\n role?: string;\n /** Agent visibility settings */\n visibility?: AgentVisibility;\n /** Parent agent ID (for child agents) */\n parent?: AgentId;\n /** Initial scopes to join */\n scopes?: ScopeId[];\n /** Initial metadata */\n metadata?: Record<string, unknown>;\n}\n```\n\n## Behavior\n\n### `ClientConnection.connect(url, options)`\n\n1. **Parse URL** and validate protocol (must be `ws:` or `wss:`)\n2. **Create WebSocket** to the URL\n3. **Wait for connection** with timeout (default 10s)\n4. **Wrap in Stream** using existing `websocketStream()` helper\n5. **Configure reconnection** if enabled:\n - Create `createStream` factory that recreates WebSocket to same URL\n - Normalize `reconnection: true` to `{ enabled: true }`\n6. **Create ClientConnection** with configured options\n7. **Call `connect()`** to perform MAP protocol handshake\n8. **Return** the connected client\n\n```typescript\nstatic async connect(url: string, options?: ClientConnectOptions): Promise<ClientConnection> {\n const parsedUrl = new URL(url);\n if (!['ws:', 'wss:'].includes(parsedUrl.protocol)) {\n throw new Error(`Unsupported protocol: ${parsedUrl.protocol}. Use ws: or wss:`);\n }\n \n const timeout = options?.connectTimeout ?? 10000;\n \n // Create and connect WebSocket\n const ws = new WebSocket(url);\n await waitForOpen(ws, timeout);\n const stream = websocketStream(ws);\n \n // Configure createStream for reconnection\n const createStream = async () => {\n const newWs = new WebSocket(url);\n await waitForOpen(newWs, timeout);\n return websocketStream(newWs);\n };\n \n // Normalize reconnection option\n const reconnection = options?.reconnection === true\n ? { enabled: true }\n : typeof options?.reconnection === 'object'\n ? options.reconnection\n : undefined;\n \n // Create connection\n const client = new ClientConnection(stream, {\n name: options?.name,\n capabilities: options?.capabilities,\n createStream,\n reconnection,\n });\n \n // Perform MAP handshake\n await client.connect({ auth: options?.auth });\n \n return client;\n}\n```\n\n### `AgentConnection.connect(url, options)`\n\nSame as ClientConnection, plus:\n- Passes `role`, `visibility`, `parent`, `scopes`, `metadata` to AgentConnectionOptions\n- Returns after both connection AND registration complete\n\n### Error Handling\n\n```typescript\n// Connection timeout\nawait ClientConnection.connect('ws://localhost:8080', { connectTimeout: 5000 });\n// Throws: Error('WebSocket connection timeout after 5000ms')\n\n// Invalid URL\nawait ClientConnection.connect('http://localhost:8080');\n// Throws: Error('Unsupported protocol: http:. Use ws: or wss:')\n\n// Server unreachable\nawait ClientConnection.connect('ws://localhost:9999');\n// Throws: Error('WebSocket connection failed')\n```\n\n## Helper Function\n\nAdd a utility for waiting on WebSocket open:\n\n```typescript\n// In src/stream/index.ts\n\n/**\n * Wait for a WebSocket to open with timeout.\n */\nexport function waitForOpen(ws: WebSocket, timeoutMs = 10000): Promise<void> {\n return new Promise((resolve, reject) => {\n if (ws.readyState === WebSocket.OPEN) {\n resolve();\n return;\n }\n \n const timeout = setTimeout(() => {\n ws.close();\n reject(new Error(`WebSocket connection timeout after ${timeoutMs}ms`));\n }, timeoutMs);\n \n const onOpen = () => {\n clearTimeout(timeout);\n ws.removeEventListener('error', onError);\n resolve();\n };\n \n const onError = (event: Event) => {\n clearTimeout(timeout);\n ws.removeEventListener('open', onOpen);\n reject(new Error('WebSocket connection failed'));\n };\n \n ws.addEventListener('open', onOpen, { once: true });\n ws.addEventListener('error', onError, { once: true });\n });\n}\n```\n\n## Usage Examples\n\n### Simple Client\n\n```typescript\nimport { ClientConnection } from '@multi-agent-protocol/sdk';\n\n// Connect with reconnection\nconst client = await ClientConnection.connect('ws://localhost:8080', {\n name: 'Dashboard',\n reconnection: true\n});\n\n// Subscribe to events\nconst sub = await client.subscribe({ eventTypes: ['agent.registered'] });\nfor await (const event of sub) {\n console.log('Agent registered:', event.data);\n}\n```\n\n### Agent with Custom Reconnection\n\n```typescript\nimport { AgentConnection } from '@multi-agent-protocol/sdk';\n\nconst agent = await AgentConnection.connect('wss://prod.example.com/map', {\n name: 'DataProcessor',\n role: 'etl',\n reconnection: {\n enabled: true,\n maxRetries: 20,\n maxDelayMs: 60000,\n restoreScopeMemberships: true\n }\n});\n\nagent.onMessage(async (msg) => {\n await agent.busy();\n // Process message...\n await agent.idle();\n});\n```\n\n### Using Low-Level API (unchanged)\n\n```typescript\nimport { ClientConnection, ndJsonStream } from '@multi-agent-protocol/sdk';\n\n// Custom transport (e.g., stdio)\nconst stream = ndJsonStream(process.stdin, process.stdout);\nconst client = new ClientConnection(stream, { name: 'CLI' });\nawait client.connect();\n```\n\n## Comparison: Before vs After\n\n### Connecting a Client\n\n| Aspect | Before | After |\n|--------|--------|-------|\n| Lines of code | ~15 | 3-5 |\n| WebSocket handling | Manual | Automatic |\n| Reconnection setup | Manual factory | `reconnection: true` |\n| Error handling | Manual | Built-in timeout |\n\n### Code Reduction\n\n**Before:**\n```typescript\nconst ws = new WebSocket('ws://localhost:8080');\nconst stream = websocketStream(ws);\nconst client = new ClientConnection(stream, {\n name: 'MyClient',\n createStream: async () => {\n const newWs = new WebSocket('ws://localhost:8080');\n return new Promise((resolve, reject) => {\n newWs.onopen = () => resolve(websocketStream(newWs));\n newWs.onerror = () => reject(new Error('Failed'));\n });\n },\n reconnection: { enabled: true }\n});\nawait client.connect();\n```\n\n**After:**\n```typescript\nconst client = await ClientConnection.connect('ws://localhost:8080', {\n name: 'MyClient',\n reconnection: true\n});\n```\n\n## File Changes\n\n```\nts-sdk/src/\n├── connection/\n│ ├── client.ts # Add static connect() method\n│ └── agent.ts # Add static connect() method\n├── stream/\n│ └── index.ts # Add waitForOpen() helper\n└── index.ts # Export new types\n```\n\n## Success Criteria\n\n- [ ] `ClientConnection.connect(url)` works with ws: and wss: URLs\n- [ ] `AgentConnection.connect(url)` works and auto-registers\n- [ ] `reconnection: true` enables reconnection with sensible defaults\n- [ ] Custom `ReconnectionOptions` are respected\n- [ ] Connection timeout is configurable\n- [ ] Invalid URLs throw clear error messages\n- [ ] Existing constructor API still works unchanged\n- [ ] All existing tests pass\n\n## Non-Goals\n\n- HTTP/SSE transport (separate spec if needed)\n- Connection pooling\n- Load balancing across multiple URLs\n- Custom WebSocket implementations (use constructor for that)\n","priority":2,"archived":0,"archived_at":null,"created_at":"2026-01-29 23:47:43","updated_at":"2026-01-29 23:47:43","parent_id":null,"parent_uuid":null,"relationships":[{"from":"s-243e","from_type":"spec","to":"s-7hnb","to_type":"spec","type":"related"}],"tags":["client-sdk","convenience-layer","developer-experience","transport"]}
7
- {"id":"s-9kpn","uuid":"f64172e0-8eed-4f14-a17e-4398b6582294","title":"ACP-over-MAP Tunneling","file_path":"specs/s-9kpn_acp_over_map_tunneling.md","content":"# ACP-over-MAP Tunneling\n\n## Overview\n\nThis spec defines how the Agent Client Protocol (ACP) can be tunneled through the Multi-Agent Protocol (MAP), enabling clients to interact with ACP-compatible agents within a MAP system while preserving all ACP semantics and features.\n\n## Goals\n\n1. **Full ACP Compatibility**: Preserve 100% of ACP features so existing client logic patterns work\n2. **Multiplexed Streams**: Support multiple concurrent ACP sessions over a single MAP connection\n3. **Observability**: Gain MAP's visibility into ACP interactions (events, message tracing)\n4. **Multi-Agent Routing**: Route ACP requests to any ACP-compatible agent in the MAP system\n5. **Federation Ready**: ACP messages can traverse federated MAP systems\n6. **Session Continuity**: ACP sessions can survive agent restarts via MAP's event replay\n\n## Non-Goals\n\n- Modifying the ACP specification\n- Supporting legacy ACP clients without code changes (use Gateway pattern for that)\n- Replacing direct ACP connections (this is an optional capability)\n\n---\n\n## Architecture\n\n### Multiplexed ACP Streams over MAP\n\n```\n┌───────────────────────────────────────────────────────────────────────────┐\n│ MAP Client Connection │\n│ │\n│ ┌─────────────────────────────────────────────────────────────────────┐ │\n│ │ Standard MAP Interface │ │\n│ │ send(), subscribe(), listAgents(), structureGraph(), etc. │ │\n│ └─────────────────────────────────────────────────────────────────────┘ │\n│ │ │\n│ ┌─────────────────────────────────┼─────────────────────────────────┐ │\n│ │ ACP Stream Multiplexer │ │\n│ │ │ │\n│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │\n│ │ │ ACPStream #1 │ │ ACPStream #2 │ │ ACPStream #3 │ │ │\n│ │ │ → Agent A │ │ → Agent B │ │ → Agent A │ │ │\n│ │ │ session: s1 │ │ session: s2 │ │ session: s3 │ │ │\n│ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │\n│ │ │ │ │ │ │\n│ └─────────┼──────────────────┼──────────────────┼───────────────────┘ │\n│ │ │ │ │\n└────────────┼──────────────────┼──────────────────┼─────────────────────────┘\n │ │ │\n ▼ ▼ ▼\n ┌────────────┐ ┌────────────┐ ┌────────────┐\n │ Agent A │ │ Agent B │ │ Agent A │\n │ (ACP-cap) │ │ (ACP-cap) │ │ (2nd sess) │\n └────────────┘ └────────────┘ └────────────┘\n```\n\n### Key Design Decisions\n\n1. **Single MAP connection, multiple ACP streams** - Network efficient, simple debugging\n2. **Virtual ACP connection per target agent** - Each stream manages its own ACP session\n3. **Client-side session management** - The ACPStreamConnection owns the session ID\n4. **Agent-side ACP compatibility required** - Target agents must understand ACP envelope format\n5. **Standard MAP messaging underneath** - ACP messages wrapped in MAP message payloads\n\n---\n\n## Design Decisions\n\nThis section documents key design decisions and their rationale.\n\n### 1. Request/Response Correlation\n\n**Decision**: ACP stream manages its own request/response correlation using ACP's existing request IDs.\n\nMAP's `send()` is fire-and-forget from the transport perspective. The `ACPStreamConnection` maintains a pending requests map and correlates responses received via subscription.\n\n```typescript\nclass ACPStreamConnection {\n #pendingRequests: Map<string, { resolve, reject, timeout }> = new Map();\n \n async #sendACPRequest(method: string, params: unknown): Promise<unknown> {\n const correlationId = generateId();\n \n // Send via MAP (fire-and-forget)\n await this.#mapClient.send(\n { agent: this.#targetAgent },\n { acp: { id: correlationId, method, params }, acpContext: {...} },\n { protocol: 'acp', correlationId }\n );\n \n // Wait for response via subscription\n return new Promise((resolve, reject) => {\n this.#pendingRequests.set(correlationId, { \n resolve, \n reject, \n timeout: setTimeout(() => reject(new Error('Timeout')), 30000) \n });\n });\n }\n}\n```\n\n**Rationale**: Keeps MAP simple, reuses ACP's existing correlation mechanism, matches how the ACP SDK works internally.\n\n---\n\n### 2. Message Delivery: Subscriptions vs Direct\n\n**Decision**: Hybrid approach - notifications via subscriptions, requests via direct messaging.\n\n| Message Type | Delivery | Rationale |\n|-------------|----------|-----------|\n| `session/update` (notification) | Subscription | High volume, one-way, benefits from filtering |\n| Agent→Client requests | Direct message | Needs response, lower volume, simpler correlation |\n\n```typescript\n// Agent sends notification (one-way, high volume)\nawait this.#mapAgent.send(\n { participant: clientId },\n { acp: { method: 'session/update', params: update }, acpContext },\n { protocol: 'acp' }\n);\n\n// Agent sends request (needs response)\nawait this.#mapAgent.send(\n { participant: clientId },\n { acp: { id: reqId, method: 'request_permission', params }, acpContext },\n { protocol: 'acp', expectsResponse: true }\n);\n```\n\n**Rationale**: Separates high-volume streaming from interactive requests. Client can prioritize permission requests over buffered updates.\n\n---\n\n### 3. Session Relationship (MAP vs ACP)\n\n**Decision**: MAP sessions and ACP sessions are independent concepts.\n\n| Concept | Scope | Lifecycle |\n|---------|-------|-----------|\n| MAP Session | Connection-level | Created on `map/connect`, survives reconnection |\n| ACP Session | Application-level | Created on `session/new`, tied to conversation |\n| ACP Stream | Bridge-level | Created on `createACPStream()`, ties client to agent |\n\n**Rationale**: Simplest mental model, no coupling between protocols. MAP handles transport, ACP handles conversation state.\n\n---\n\n### 4. Agent Discovery for ACP\n\n**Decision**: Agents advertise ACP capability via metadata.\n\n```typescript\n// Agent registers with ACP capability\nawait mapAgent.connect({\n name: 'CodingAgent',\n capabilities: {\n protocols: ['acp'],\n acp: {\n version: '2024-10-07',\n features: ['loadSession', 'modes', 'terminals']\n }\n }\n});\n\n// Client discovers ACP-capable agents\nconst { agents } = await mapClient.listAgents();\nconst acpAgents = agents.filter(a => a.capabilities?.protocols?.includes('acp'));\n```\n\n**Rationale**: Explicit capability advertisement, filterable via existing `listAgents`, includes version and feature support.\n\n---\n\n### 5. Terminal/FS Resource Handling\n\n**Decision**: Implementation-defined - agent decides whether to use client resources or handle server-side.\n\nThe ACP tunneling layer passes through terminal/FS requests unchanged. The agent implementation decides:\n\n- **Forward to client**: Agent calls `terminal/create` which routes to user's IDE\n- **Handle server-side**: Agent uses its own execution environment\n\n```typescript\nclass MyACPAgent {\n async handleToolCall(tool: string, params: unknown, ctx: ACPContext) {\n if (tool === 'bash') {\n // Option A: Use client's terminal (user visibility)\n const terminal = await this.acpAdapter.createTerminal(ctx.streamId, {\n command: params.command\n });\n \n // Option B: Server-side execution (headless)\n const result = await this.serverSideExec(params.command);\n }\n }\n}\n```\n\n**Rationale**: Flexibility for different deployment scenarios. IDE-connected agents may want user terminals; headless agents handle everything server-side.\n\n---\n\n### 6. Reconnection & Session Resume\n\n**Decision**: Auto-restore ACP streams using MAP's subscription restoration, verify ACP session validity.\n\n```typescript\nclass ACPStreamConnection {\n #lastEventId: string | null = null;\n \n async #onReconnect(): Promise<void> {\n // Restore subscription with replay from last known event\n this.#subscription = await this.#mapClient.subscribe({\n fromAgents: [this.#targetAgent],\n filters: { 'metadata.protocol': 'acp', 'acpContext.streamId': this.#streamId },\n resumeFrom: this.#lastEventId\n });\n \n // Verify ACP session is still valid on agent\n if (this.#sessionId) {\n try {\n await this.#sendACPRequest('session/status', { sessionId: this.#sessionId });\n } catch {\n this.emit('sessionLost', { sessionId: this.#sessionId });\n }\n }\n }\n}\n```\n\n**Rationale**: Leverages MAP's built-in reconnection. Transparent for short disconnects, emits event for session loss so client can use `session/load`.\n\n---\n\n### 7. Backpressure Handling\n\n**Decision**: Use MAP's existing backpressure mechanisms (pause/resume, overflow callbacks).\n\n```typescript\nconst subscription = await mapClient.subscribe({...});\n\nsubscription.on('overflow', (info) => {\n console.warn(`Dropped ${info.eventsDropped} session updates`);\n});\n\n// Manual flow control\nsubscription.pause();\n// ... process backlog\nsubscription.resume();\n```\n\n**Rationale**: MAP already has robust backpressure. `session/update` is incremental (missing some is recoverable). Keep simple, add ACP-specific flow control only if needed.\n\n---\n\n### 8. Error Code Handling\n\n**Decision**: Pass through ACP errors unchanged.\n\n```typescript\n// ACP error propagates directly\nclass ACPError extends Error {\n constructor(public code: number, message: string, public data?: unknown) {\n super(message);\n }\n}\n\n// Client catches ACP errors as-is\ntry {\n await acp.newSession({...});\n} catch (e) {\n if (e instanceof ACPError && e.code === -32000) {\n // Handle ACP auth_required\n }\n}\n```\n\n**Rationale**: Preserves ACP semantics exactly, no lossy translation. MAP transport errors are separate (connection failures).\n\n---\n\n### 9. Federation Support\n\n**Decision**: Defer federation support, but design for compatibility.\n\nCurrent design doesn't preclude federation - the ACP envelope can travel through federation routing. However:\n- Federation adds latency (bad for streaming prompts)\n- Focus on single-system first\n- Document as future extension\n\n---\n\n### 10. Unstable ACP Methods\n\n**Decision**: Start with stable ACP methods only.\n\n**Stable (implement first):**\n- `initialize`, `authenticate`\n- `session/new`, `session/load`, `session/set_mode`\n- `session/prompt`, `session/cancel`\n- `session/update`, `session/request_permission`\n- All `fs/*` and `terminal/*` methods\n\n**Unstable (defer):**\n- `session/list`, `session/fork`, `session/resume`\n- `session/set_model`, `session/set_config_option`\n\n```typescript\ninterface ACPStreamConnection {\n // Stable methods\n initialize(...): Promise<...>;\n newSession(...): Promise<...>;\n prompt(...): Promise<...>;\n \n // Unstable - optional, prefixed\n unstable_listSessions?(...): Promise<...>;\n unstable_forkSession?(...): Promise<...>;\n}\n```\n\n**Rationale**: Stable methods cover core use cases. Unstable methods may change in ACP spec. Add incrementally.\n\n---\n\n### 11. Testing Strategy\n\n**Decision**: Provide both `TestACPAgent` mock and TestServer integration.\n\n```typescript\n// Unit testing - lightweight mock\nimport { TestACPAgent } from '@anthropic/map-sdk/testing';\n\nconst mockAgent = new TestACPAgent({\n onPrompt: async (params) => {\n await mockAgent.sendUpdate({ type: 'message_start' });\n return { stopReason: 'end_turn' };\n }\n});\n\n// Integration testing - full server\nconst server = new TestServer();\nserver.registerACPAgent('test-agent', mockAgent);\nconst client = await ClientConnection.connect(server.url);\n```\n\n**Rationale**: Unit tests need lightweight mocks, integration tests need full behavior. TestServer already exists.\n\n---\n\n### 12. Type Packaging\n\n**Decision**: Bundle ACP types directly in MAP SDK.\n\n```typescript\n// All from @anthropic/map-sdk\nimport { \n ClientConnection,\n ACPStreamConnection,\n // ACP types bundled\n type ACPInitializeRequest,\n type ACPPromptRequest,\n type ACPSessionNotification,\n} from '@anthropic/map-sdk';\n\n// Or namespaced\nimport { ClientConnection, ACP } from '@anthropic/map-sdk';\nconst req: ACP.InitializeRequest = {...};\n```\n\n**Rationale**: Simpler dependency management, single package to install. Types are lightweight (no runtime cost). Can split later if needed.\n\n---\n\n## Protocol Translation\n\n### ACP Message Envelope in MAP\n\nAll ACP messages are wrapped in a standard envelope carried as MAP message payload:\n\n```typescript\ninterface ACPEnvelope {\n // The original ACP JSON-RPC message\n acp: {\n jsonrpc: '2.0';\n id?: string | number; // Present for requests\n method?: string; // Present for requests/notifications\n params?: unknown; // Method parameters\n result?: unknown; // Present for responses\n error?: ACPError; // Present for error responses\n };\n \n // ACP-specific routing context\n acpContext: {\n streamId: string; // Identifies the virtual ACP stream\n sessionId: string | null; // ACP session (null before session/new)\n direction: 'client-to-agent' | 'agent-to-client';\n \n // For agent→client requests (permissions, fs, terminal)\n pendingClientRequest?: {\n requestId: string | number;\n method: string;\n timeout?: number;\n };\n };\n}\n```\n\n### MAP Message Structure\n\n```typescript\n// Client→Agent ACP request via MAP\nconst mapMessage: SendRequestParams = {\n to: { agent: targetAgentId },\n payload: ACPEnvelope,\n meta: {\n protocol: 'acp',\n expectsResponse: true, // For ACP requests\n correlationId: string, // Links request/response\n }\n};\n```\n\n### MAP Metadata Conventions\n\n| Field | Purpose |\n|-------|---------|\n| `protocol: 'acp'` | Identifies this as an ACP-tunneled message |\n| `expectsResponse` | True for ACP requests, false for notifications |\n| `correlationId` | Links ACP request/response pairs |\n\n---\n\n## Client SDK Interface\n\n### Creating ACP Streams\n\n```typescript\n// Extend ClientConnection\nclass ClientConnection {\n /**\n * Create a virtual ACP stream connection to a specific agent.\n * Multiple ACP streams can coexist over a single MAP connection.\n */\n createACPStream(options: ACPStreamOptions): ACPStreamConnection;\n}\n\ninterface ACPStreamOptions {\n /** Target agent that will handle ACP requests */\n targetAgent: AgentId;\n \n /** Client-side handlers for agent→client requests */\n client: ACPClientHandlers;\n \n /** Optional: receive MAP events alongside ACP (for observability) */\n exposeMapEvents?: boolean;\n}\n\ninterface ACPClientHandlers {\n // Required\n requestPermission(params: ACP.RequestPermissionRequest): Promise<ACP.RequestPermissionResponse>;\n sessionUpdate(params: ACP.SessionNotification): Promise<void>;\n \n // Optional (based on advertised capabilities)\n readTextFile?(params: ACP.ReadTextFileRequest): Promise<ACP.ReadTextFileResponse>;\n writeTextFile?(params: ACP.WriteTextFileRequest): Promise<ACP.WriteTextFileResponse>;\n createTerminal?(params: ACP.CreateTerminalRequest): Promise<ACP.CreateTerminalResponse>;\n terminalOutput?(params: ACP.TerminalOutputRequest): Promise<ACP.TerminalOutputResponse>;\n releaseTerminal?(params: ACP.ReleaseTerminalRequest): Promise<ACP.ReleaseTerminalResponse>;\n waitForTerminalExit?(params: ACP.WaitForTerminalExitRequest): Promise<ACP.WaitForTerminalExitResponse>;\n killTerminal?(params: ACP.KillTerminalCommandRequest): Promise<ACP.KillTerminalCommandResponse>;\n}\n```\n\n### ACPStreamConnection Interface\n\n```typescript\n/**\n * Virtual ACP connection over MAP.\n * Implements the full ACP Agent interface.\n */\nclass ACPStreamConnection {\n /** Unique identifier for this stream */\n readonly streamId: string;\n \n /** Target agent this stream connects to */\n readonly targetAgent: AgentId;\n \n /** Current ACP session ID (null until newSession called) */\n readonly sessionId: string | null;\n \n /** Whether initialize() has been called */\n readonly initialized: boolean;\n \n /** Agent capabilities from initialize response */\n readonly capabilities: ACP.AgentCapabilities | null;\n\n // ===== ACP Lifecycle Methods =====\n \n initialize(params: ACP.InitializeRequest): Promise<ACP.InitializeResponse>;\n authenticate(params: ACP.AuthenticateRequest): Promise<ACP.AuthenticateResponse>;\n \n // ===== ACP Session Methods =====\n \n newSession(params: ACP.NewSessionRequest): Promise<ACP.NewSessionResponse>;\n loadSession(params: ACP.LoadSessionRequest): Promise<ACP.LoadSessionResponse>;\n setSessionMode(params: ACP.SetSessionModeRequest): Promise<ACP.SetSessionModeResponse>;\n \n // ===== ACP Prompt Methods =====\n \n prompt(params: ACP.PromptRequest): Promise<ACP.PromptResponse>;\n cancel(params: ACP.CancelNotification): Promise<void>;\n \n // ===== MAP Observability (optional) =====\n \n /** Subscribe to MAP events from the target agent (if exposeMapEvents enabled) */\n onMapEvent?(handler: (event: MAPEvent) => void): () => void;\n \n // ===== Lifecycle =====\n \n /** Close this ACP stream and clean up resources */\n close(): Promise<void>;\n \n // ===== Events =====\n \n /** Emitted when ACP session is lost after reconnection */\n on(event: 'sessionLost', handler: (info: { sessionId: string }) => void): void;\n}\n```\n\n### Usage Example\n\n```typescript\n// Connect to MAP system\nconst mapClient = await ClientConnection.connect('ws://localhost:8080', {\n name: 'MyIDE'\n});\n\n// Discover ACP-capable agents\nconst { agents } = await mapClient.listAgents();\nconst acpAgents = agents.filter(a => a.capabilities?.protocols?.includes('acp'));\n\n// Create virtual ACP stream to a specific agent\nconst acp = mapClient.createACPStream({\n targetAgent: acpAgents[0].id,\n client: {\n requestPermission: async (req) => {\n const allowed = await showPermissionDialog(req.toolCall);\n return { outcome: allowed ? 'allowed' : 'denied' };\n },\n sessionUpdate: async (update) => {\n renderUpdate(update);\n },\n readTextFile: async (req) => {\n const content = await fs.readFile(req.path, 'utf-8');\n return { content };\n },\n createTerminal: async (req) => {\n const terminal = await ide.createTerminal(req.command);\n return { terminalId: terminal.id };\n }\n }\n});\n\n// Handle session loss on reconnection\nacp.on('sessionLost', async ({ sessionId }) => {\n console.log('Session lost, attempting reload...');\n await acp.loadSession({ sessionId });\n});\n\n// Standard ACP workflow\nconst initResult = await acp.initialize({\n protocolVersion: '2024-10-07',\n clientInfo: { name: 'MyIDE', version: '1.0.0' },\n capabilities: {\n fs: { readTextFile: true, writeTextFile: true },\n terminal: true\n }\n});\n\nconst { sessionId } = await acp.newSession({\n workingDirectory: '/path/to/project'\n});\n\n// Prompt with streaming via sessionUpdate handler\nconst result = await acp.prompt({\n sessionId,\n messages: [{\n role: 'user',\n content: [{ type: 'text', text: 'Fix the authentication bug' }]\n }]\n});\n\nconsole.log('Stop reason:', result.stopReason);\n\n// Can create multiple streams to different agents\nconst acp2 = mapClient.createACPStream({\n targetAgent: 'research-agent-1',\n client: { /* handlers */ }\n});\n\n// Clean up\nawait acp.close();\nawait acp2.close();\nawait mapClient.disconnect();\n```\n\n---\n\n## Agent SDK Interface\n\n### ACP-Compatible Agent\n\nAgents that want to receive ACP-tunneled messages must:\n1. Advertise ACP capability during registration\n2. Handle the ACP envelope format\n\n```typescript\n// Register with ACP capability\nconst mapAgent = await AgentConnection.connect('ws://localhost:8080', {\n name: 'CodingAgent',\n capabilities: {\n protocols: ['acp'],\n acp: { version: '2024-10-07', features: ['loadSession', 'modes'] }\n }\n});\n\n// Handle incoming messages\nmapAgent.onMessage(async (message) => {\n if (message.metadata?.protocol === 'acp') {\n await handleACPMessage(message);\n }\n});\n```\n\n### Helper: ACPAgentAdapter\n\nTo simplify agent implementation, use the adapter:\n\n```typescript\nimport { ACPAgentAdapter } from '@anthropic/map-sdk';\n\nconst adapter = new ACPAgentAdapter(mapAgent, {\n initialize: async (params, ctx) => {\n return {\n protocolVersion: '2024-10-07',\n agentInfo: { name: 'CodingAgent', version: '1.0.0' },\n capabilities: { loadSession: true }\n };\n },\n \n newSession: async (params, ctx) => {\n const sessionId = generateSessionId();\n return { sessionId };\n },\n \n prompt: async (params, ctx) => {\n // Stream updates to client\n await adapter.sendSessionUpdate(ctx.streamId, {\n sessionId: params.sessionId,\n update: { type: 'message_start', message: {...} }\n });\n \n // Request permission if needed\n const permission = await adapter.requestPermission(ctx.streamId, {\n sessionId: params.sessionId,\n toolCall: { id: '1', name: 'bash', input: { command: 'npm test' } }\n });\n \n if (permission.outcome === 'allowed') {\n // Option A: Use client's terminal\n const terminal = await adapter.createTerminal(ctx.streamId, {\n sessionId: params.sessionId,\n command: 'npm test'\n });\n \n // Option B: Execute server-side (implementation choice)\n // const result = await exec('npm test');\n }\n \n return { stopReason: 'end_turn' };\n },\n \n cancel: async (params, ctx) => {\n // Abort any in-progress work\n }\n});\n```\n\n### ACPAgentAdapter Interface\n\n```typescript\nclass ACPAgentAdapter {\n constructor(mapAgent: AgentConnection, handler: ACPAgentHandler);\n \n /** Send session update notification to client */\n sendSessionUpdate(streamId: string, update: ACP.SessionNotification): Promise<void>;\n \n /** Request permission from client (blocks until user responds) */\n requestPermission(streamId: string, request: ACP.RequestPermissionRequest): Promise<ACP.RequestPermissionResponse>;\n \n /** Read file from client filesystem */\n readTextFile(streamId: string, request: ACP.ReadTextFileRequest): Promise<ACP.ReadTextFileResponse>;\n \n /** Write file to client filesystem */\n writeTextFile(streamId: string, request: ACP.WriteTextFileRequest): Promise<ACP.WriteTextFileResponse>;\n \n /** Create terminal on client */\n createTerminal(streamId: string, request: ACP.CreateTerminalRequest): Promise<ACP.CreateTerminalResponse>;\n \n /** Get terminal output from client */\n terminalOutput(streamId: string, request: ACP.TerminalOutputRequest): Promise<ACP.TerminalOutputResponse>;\n \n /** Release terminal on client */\n releaseTerminal(streamId: string, request: ACP.ReleaseTerminalRequest): Promise<ACP.ReleaseTerminalResponse>;\n \n /** Wait for terminal exit on client */\n waitForTerminalExit(streamId: string, request: ACP.WaitForTerminalExitRequest): Promise<ACP.WaitForTerminalExitResponse>;\n \n /** Kill terminal command on client */\n killTerminal(streamId: string, request: ACP.KillTerminalCommandRequest): Promise<ACP.KillTerminalCommandResponse>;\n}\n\ninterface ACPAgentHandler {\n initialize(params: ACP.InitializeRequest, ctx: ACPContext): Promise<ACP.InitializeResponse>;\n authenticate?(params: ACP.AuthenticateRequest, ctx: ACPContext): Promise<ACP.AuthenticateResponse>;\n newSession(params: ACP.NewSessionRequest, ctx: ACPContext): Promise<ACP.NewSessionResponse>;\n loadSession?(params: ACP.LoadSessionRequest, ctx: ACPContext): Promise<ACP.LoadSessionResponse>;\n setSessionMode?(params: ACP.SetSessionModeRequest, ctx: ACPContext): Promise<ACP.SetSessionModeResponse>;\n prompt(params: ACP.PromptRequest, ctx: ACPContext): Promise<ACP.PromptResponse>;\n cancel(params: ACP.CancelNotification, ctx: ACPContext): Promise<void>;\n}\n\ninterface ACPContext {\n streamId: string;\n sessionId: string | null;\n clientParticipantId: string;\n}\n```\n\n---\n\n## Method Mapping\n\n### Client→Agent Methods\n\n| ACP Method | Handling |\n|------------|----------|\n| `initialize` | Forward to agent, agent returns capabilities |\n| `authenticate` | Forward to agent |\n| `session/new` | Forward to agent, client stores returned sessionId |\n| `session/load` | Forward to agent, may trigger event replay |\n| `session/set_mode` | Forward to agent |\n| `session/prompt` | Forward to agent, streaming via session/update events |\n| `session/cancel` | Forward immediately as notification |\n\n### Agent→Client Methods\n\n| ACP Method | Handling |\n|------------|----------|\n| `session/update` | Agent sends MAP notification, client routes to sessionUpdate handler |\n| `session/request_permission` | Agent sends MAP request, client shows UI, returns decision |\n| `fs/read_text_file` | Agent sends MAP request, client reads local file |\n| `fs/write_text_file` | Agent sends MAP request, client writes local file |\n| `terminal/create` | Agent sends MAP request, client spawns terminal |\n| `terminal/output` | Agent sends MAP request, client returns terminal output |\n| `terminal/release` | Agent sends MAP request, client releases terminal |\n| `terminal/wait_for_exit` | Agent sends MAP request, client awaits terminal exit |\n| `terminal/kill` | Agent sends MAP request, client kills terminal process |\n\n---\n\n## Event Flow\n\n### Prompt Turn Sequence\n\n```\nClient MAP Agent\n │ │ │\n │ createACPStream() │ │\n │───────────────────────►│ │\n │ │ subscribe(fromAgent) │\n │ │───────────────────────►│\n │ │ │\n │ acp.initialize() │ │\n │───────────────────────►│ MAP send (ACP envelope)│\n │ │───────────────────────►│\n │ │◄───────────────────────│\n │◄───────────────────────│ MAP response │\n │ InitializeResponse │ │\n │ │ │\n │ acp.newSession() │ │\n │───────────────────────►│───────────────────────►│\n │◄───────────────────────│◄───────────────────────│\n │ { sessionId } │ │\n │ │ │\n │ acp.prompt() │ │\n │───────────────────────►│───────────────────────►│\n │ │ │\n │ │ session/update │\n │ sessionUpdate() ◄───│◄───────────────────────│\n │ │ session/update │\n │ sessionUpdate() ◄───│◄───────────────────────│\n │ │ │\n │ │ request_permission │\n │ requestPermission()◄──│◄───────────────────────│\n │ (show UI) │ │\n │───────────────────────►│───────────────────────►│\n │ { allowed } │ │\n │ │ │\n │ │ session/update │\n │ sessionUpdate() ◄───│◄───────────────────────│\n │ │ │\n │◄───────────────────────│◄───────────────────────│\n │ PromptResponse │ (prompt complete) │\n │ │ │\n```\n\n---\n\n## Error Handling\n\n### ACP Errors Through MAP\n\nACP errors are passed through unchanged:\n\n```typescript\n// ACP error in MAP response payload\n{\n payload: {\n acp: {\n jsonrpc: '2.0',\n id: originalRequestId,\n error: {\n code: -32000, // ACP error code preserved\n message: 'Authentication required',\n data: { ... }\n }\n },\n acpContext: { streamId, sessionId, direction: 'agent-to-client' }\n }\n}\n\n// Client receives as ACPError\nclass ACPError extends Error {\n constructor(public code: number, message: string, public data?: unknown);\n}\n\ntry {\n await acp.newSession({...});\n} catch (e) {\n if (e instanceof ACPError && e.code === -32000) {\n // Handle ACP auth_required\n }\n}\n```\n\n### Error Scenarios\n\n| Scenario | Handling |\n|----------|----------|\n| Target agent unavailable | ACPStreamConnection throws with agent unavailable error |\n| MAP message timeout | ACPStreamConnection throws with timeout error |\n| Agent returns ACP error | ACPError propagated to caller unchanged |\n| Client disconnects during agent request | Agent receives disconnect, should abort |\n| Stream closed during operation | Pending operations reject with stream closed error |\n| Session lost after reconnection | `sessionLost` event emitted, client can use `loadSession` |\n\n---\n\n## Capability Negotiation\n\n### Client Capabilities\n\n```typescript\nawait acp.initialize({\n protocolVersion: '2024-10-07',\n clientInfo: { name: 'MyIDE', version: '1.0.0' },\n capabilities: {\n fs: {\n readTextFile: !!clientHandlers.readTextFile,\n writeTextFile: !!clientHandlers.writeTextFile\n },\n terminal: !!clientHandlers.createTerminal,\n _meta: {\n map: { observability: options.exposeMapEvents ?? false }\n }\n }\n});\n```\n\n### Agent Capabilities (Registration)\n\n```typescript\nawait mapAgent.connect({\n name: 'CodingAgent',\n capabilities: {\n protocols: ['acp'],\n acp: {\n version: '2024-10-07',\n features: ['loadSession', 'modes', 'terminals']\n }\n }\n});\n```\n\n### Agent Capabilities (ACP Initialize Response)\n\n```typescript\n{\n protocolVersion: '2024-10-07',\n agentInfo: { name: 'CodingAgent', version: '1.0.0' },\n capabilities: {\n loadSession: true,\n sessionCapabilities: { modes: true },\n _meta: {\n map: {\n multiSession: true,\n federation: true\n }\n }\n }\n}\n```\n\n---\n\n## Comparison with Alternatives\n\n| Aspect | Multiplexed ACP Streams | Gateway Agent | Native ACP |\n|--------|------------------------|---------------|------------|\n| **Client SDK** | MAP SDK (bundled ACP types) | ACP SDK (unchanged) | ACP SDK |\n| **Network** | Single MAP connection | Two connections | Direct connection |\n| **Multiple agents** | ✅ Multiple streams | ❌ Single gateway | ❌ One connection |\n| **Observability** | ✅ Full MAP events | ❌ Limited | ❌ None |\n| **Legacy clients** | ❌ Need migration | ✅ Works unchanged | ✅ Native |\n| **Deployment** | Simple | Gateway process needed | Simple |\n| **Debugging** | Single connection trace | Two connection traces | Single trace |\n| **Federation** | ✅ Via MAP (future) | ✅ Via MAP | ❌ No |\n\n---\n\n## Future Extensions\n\n### 1. Gateway Agent (for legacy ACP clients)\n\nFor clients that cannot migrate to the MAP SDK:\n\n```\nACP Client (unchanged) → ACP Gateway Agent → MAP System → Target Agents\n```\n\nUses the same envelope format internally.\n\n### 2. Multi-Agent Sessions\n\nAllow an ACP session to span multiple agents:\n\n```typescript\nconst acp = mapClient.createACPStream({\n targetScope: 'project-scope',\n routingStrategy: 'round-robin' | 'broadcast' | 'leader',\n client: {...}\n});\n```\n\n### 3. Session Migration\n\nMove an ACP session from one agent to another:\n\n```typescript\nawait acp.migrateSession({\n newTargetAgent: 'agent-2',\n preserveHistory: true\n});\n```\n\n### 4. Federation\n\nRoute ACP streams to agents in federated MAP systems:\n\n```typescript\nconst acp = mapClient.createACPStream({\n targetAgent: 'agent-1',\n targetSystem: 'remote-system',\n client: {...}\n});\n```\n\n---\n\n## Testing Strategy\n\n### Unit Testing\n\n```typescript\nimport { TestACPAgent } from '@anthropic/map-sdk/testing';\n\nconst mockAgent = new TestACPAgent({\n capabilities: { loadSession: true },\n onPrompt: async (params) => {\n await mockAgent.sendUpdate({ type: 'message_start', ... });\n await mockAgent.sendUpdate({ type: 'content_delta', ... });\n return { stopReason: 'end_turn' };\n }\n});\n\n// Test client code against mock\nconst acp = createTestACPStream(mockAgent);\nawait acp.initialize({...});\nconst result = await acp.prompt({...});\nexpect(result.stopReason).toBe('end_turn');\n```\n\n### Integration Testing\n\n```typescript\nimport { TestServer } from '@anthropic/map-sdk/testing';\n\nconst server = new TestServer();\nserver.registerACPAgent('test-agent', mockAgent);\n\nconst client = await ClientConnection.connect(server.url);\nconst acp = client.createACPStream({\n targetAgent: 'test-agent',\n client: {...}\n});\n\n// Full round-trip test\nawait acp.initialize({...});\nconst { sessionId } = await acp.newSession({...});\nconst result = await acp.prompt({ sessionId, messages: [...] });\n```\n\n### Test Coverage\n\n1. **Unit tests**: Message encoding/decoding, correlation logic\n2. **Integration tests**: Full round-trip through TestServer\n3. **Multi-stream tests**: Multiple concurrent ACP streams\n4. **Error handling tests**: Disconnection, timeout, agent unavailable\n5. **Backpressure tests**: Slow client handling rapid session/update\n6. **Reconnection tests**: Session restoration after disconnect\n\n---\n\n## References\n\n- [ACP Specification](https://agentclientprotocol.com/)\n- [ACP TypeScript SDK](../references/typescript-sdk)\n- [MAP Protocol Specification](../docs/00-design-specification.md)\n","priority":1,"archived":0,"archived_at":null,"created_at":"2026-01-30 10:18:54","updated_at":"2026-01-30 18:52:27","parent_id":null,"parent_uuid":null,"relationships":[],"tags":["acp","architecture","integration","protocol"]}
8
- {"id":"s-65aw","uuid":"4a6b6165-7c8a-4184-893f-3fe3f047e5b4","title":"MAP Extension System","file_path":"specs/s-65aw_map_extension_system.md","content":"# MAP Extension System\n\n## Overview\n\nThe MAP Extension System provides a standardized way to add optional functionality to the Multi-Agent Protocol without breaking backward compatibility. Extensions are self-contained feature sets that can be negotiated at connection time, allowing servers and clients to progressively adopt new capabilities.\n\n## Design Principles\n\n### 1. Opt-In Adoption\nExtensions are optional. A minimal MAP implementation only needs to support core methods (`map/connect`, `map/send`, `map/subscribe`, etc.). Extensions can be added incrementally without breaking existing clients.\n\n### 2. Graceful Degradation\nWhen an extension-aware client talks to a non-extension server, or vice versa, the system should degrade gracefully. Extension metadata in messages should be ignorable by non-aware participants.\n\n### 3. Capability Discovery\nClients and servers discover each other's extension support during the `map/connect` handshake. This allows runtime decisions about what features to use.\n\n### 4. Namespace Isolation\nEach extension owns a method namespace (e.g., `conv/*` for conversations). This prevents collisions between extensions and with core MAP methods.\n\n### 5. Backward-Compatible Data\nExtension data piggybacks on existing structures via the `meta` field using `x-{extension}` prefixed keys. Non-aware participants can safely ignore this data.\n\n---\n\n## Extension Manifest\n\nEvery extension is described by a manifest that declares its capabilities:\n\n```typescript\ninterface ExtensionManifest {\n // Unique identifier for this extension\n id: string; // e.g., 'conversations', 'streaming', 'transactions'\n \n // Semantic version\n version: string; // e.g., '1.0.0'\n \n // Method namespace prefix (without trailing /)\n namespace: string; // e.g., 'conv' → methods are 'conv/create', 'conv/join'\n \n // Human-readable description\n description?: string;\n \n // Requirements from MAP core\n requires: {\n // Minimum MAP protocol version\n mapVersion: string; // semver range, e.g., '>=1.0.0'\n \n // Core capabilities needed for this extension to function\n capabilities?: string[]; // e.g., ['messaging.canSend', 'observation.canObserve']\n };\n \n // What this extension provides\n provides: {\n // Methods added by this extension\n methods: MethodDefinition[];\n \n // Event types emitted by this extension\n events: EventDefinition[];\n \n // New capabilities this extension adds (for capability negotiation)\n capabilities?: CapabilityDefinition[];\n };\n \n // Other extensions this depends on (optional)\n dependencies?: ExtensionDependency[];\n}\n\ninterface MethodDefinition {\n // Full method name including namespace\n name: string; // e.g., 'conv/create'\n \n // Human-readable description\n description: string;\n \n // Whether this is a request (has response) or notification\n type: 'request' | 'notification';\n \n // Capability required to call this method (from provides.capabilities or core)\n requiredCapability?: string;\n}\n\ninterface EventDefinition {\n // Event type string\n type: string; // e.g., 'conversation.created'\n \n // Human-readable description\n description: string;\n}\n\ninterface CapabilityDefinition {\n // Capability path\n path: string; // e.g., 'conversations.canCreate'\n \n // Human-readable description\n description: string;\n}\n\ninterface ExtensionDependency {\n // Extension ID\n id: string;\n \n // Version range required\n version: string; // semver range\n}\n```\n\n---\n\n## Capability Negotiation\n\nExtension support is negotiated during the `map/connect` handshake.\n\n### Connect Request (Client → Server)\n\n```typescript\ninterface ConnectRequestParams {\n protocolVersion: ProtocolVersion;\n participantType: ParticipantType;\n // ... existing fields ...\n \n capabilities: {\n // ... existing capability groups ...\n \n // Extension support advertisement\n extensions?: {\n // Extensions this participant supports (with versions)\n supported?: ExtensionSupport[];\n \n // Extensions this participant requires (connection fails without these)\n required?: string[]; // Extension IDs\n };\n };\n}\n\ninterface ExtensionSupport {\n id: string;\n version: string; // Version this client supports\n capabilities?: string[]; // Specific capabilities within the extension\n}\n```\n\n### Connect Response (Server → Client)\n\n```typescript\ninterface ConnectResponseResult {\n protocolVersion: ProtocolVersion;\n sessionId: SessionId;\n // ... existing fields ...\n \n capabilities: {\n // ... existing capability groups ...\n \n extensions?: {\n // What extensions are available on this server\n available: ExtensionAvailability[];\n };\n };\n}\n\ninterface ExtensionAvailability {\n id: string;\n version: string; // Server's version of this extension\n enabled: boolean; // Whether it's enabled for THIS participant\n capabilities?: string[]; // Specific capabilities granted\n \n // If not enabled, why?\n disabledReason?: 'not-supported' | 'not-authorized' | 'disabled-by-config';\n}\n```\n\n### Negotiation Algorithm\n\n1. **Client sends** list of supported extensions with versions\n2. **Server checks** each against its available extensions\n3. **For each extension**:\n - If server doesn't support it: `enabled: false, disabledReason: 'not-supported'`\n - If versions are incompatible: `enabled: false, disabledReason: 'not-supported'`\n - If participant lacks required capabilities: `enabled: false, disabledReason: 'not-authorized'`\n - Otherwise: `enabled: true` with effective capabilities\n4. **If client required an extension** that's not enabled: **connection fails** with error\n5. **Client receives** list of what's actually available for this session\n\n---\n\n## Method Namespacing\n\nExtensions use their own method namespace to avoid collisions.\n\n### Namespace Rules\n\n1. Extension namespace is a short identifier (2-10 lowercase chars)\n2. Methods are `{namespace}/{method-path}`\n3. Namespaces must not conflict with `map` (reserved for core)\n4. Nested paths are allowed: `conv/turns/list`, `conv/thread/create`\n\n### Examples\n\n```\nCore MAP:\n map/connect\n map/send\n map/agents/list\n map/subscribe\n\nConversation Extension (namespace: 'conv'):\n conv/create\n conv/join\n conv/leave\n conv/turn\n conv/turns/list\n conv/turns/subscribe\n conv/thread/create\n\nStreaming Extension (namespace: 'stream'):\n stream/start\n stream/chunk\n stream/end\n stream/cancel\n```\n\n### Method Not Found\n\nCalling an extension method when the extension is not enabled returns:\n\n```json\n{\n \"jsonrpc\": \"2.0\",\n \"id\": 1,\n \"error\": {\n \"code\": -32601,\n \"message\": \"Method not found: conv/create\",\n \"data\": {\n \"category\": \"protocol\",\n \"details\": {\n \"extension\": \"conversations\",\n \"reason\": \"extension-not-enabled\"\n }\n }\n }\n}\n```\n\n---\n\n## Event Namespacing\n\nExtension events use dotted prefixes derived from the extension ID.\n\n### Event Naming Convention\n\n- Core events: `{entity}_{action}` (e.g., `agent_registered`, `message_sent`)\n- Extension events: `{extension}.{entity}.{action}` (e.g., `conversation.created`, `conversation.turn.added`)\n\n### Subscribing to Extension Events\n\nExtension events work with the standard `map/subscribe` mechanism:\n\n```typescript\n// Subscribe to conversation events\n{\n method: 'map/subscribe',\n params: {\n filter: {\n eventTypes: [\n 'agent_registered', // Core event\n 'conversation.created', // Extension event\n 'conversation.turn.added', // Extension event\n 'conversation.participant.joined'\n ]\n }\n }\n}\n```\n\n### Event Delivery\n\nExtension events are delivered through the standard `map/event` notification:\n\n```typescript\n{\n method: 'map/event',\n params: {\n subscriptionId: 'sub-123',\n sequenceNumber: 42,\n eventId: 'evt-789',\n event: {\n id: 'evt-789',\n type: 'conversation.turn.added', // Extension event type\n timestamp: 1706640000000,\n source: 'agent-1',\n data: {\n conversationId: 'conv-abc',\n turn: { ... }\n }\n }\n }\n}\n```\n\n---\n\n## Extension Data in Core Messages\n\nExtensions can attach metadata to core MAP messages for interoperability.\n\n### Meta Field Convention\n\nExtension data uses `x-{extension-id}` keys in the `meta` field:\n\n```typescript\ninterface Message {\n id: MessageId;\n from: ParticipantId;\n to: Address;\n payload?: unknown;\n meta?: {\n // Core MAP fields\n correlationId?: string;\n priority?: MessagePriority;\n \n // Extension metadata (namespaced)\n 'x-conv'?: {\n conversationId: string;\n turnType: string;\n threadId?: string;\n inReplyTo?: string;\n };\n \n 'x-stream'?: {\n streamId: string;\n sequence: number;\n final: boolean;\n };\n };\n}\n```\n\n### Interoperability Semantics\n\n1. **Non-aware participants** MUST ignore unrecognized `x-*` keys\n2. **Extension-aware participants** SHOULD process relevant `x-*` keys\n3. **Forwarding** SHOULD preserve `x-*` keys (pass-through)\n4. **Core fields** (like `correlationId`) can be used for extension purposes when semantically compatible\n\n### Example: Conversation + Non-Aware Agent\n\n```\nAgent A (conv-aware) ──map/send──▶ Agent B (not conv-aware)\n\nMessage:\n{\n from: 'agent-a',\n to: { agent: 'agent-b' },\n payload: { text: 'Hello!' },\n meta: {\n correlationId: 'conv-123',\n 'x-conv': { turnType: 'a2a-message', inReplyTo: 'turn-456' }\n }\n}\n\nAgent B sees:\n- A normal message with payload { text: 'Hello!' }\n- correlationId 'conv-123' (can use for reply correlation)\n- Ignores 'x-conv' (unknown extension)\n\nAgent B replies:\n{\n from: 'agent-b',\n to: { agent: 'agent-a' },\n payload: { text: 'Hi!' },\n meta: { correlationId: 'conv-123' } // Preserves correlation\n}\n\nAgent A receives:\n- Matches correlationId to conversation conv-123\n- No 'x-conv' metadata, but can infer it's a response\n- Records as a turn in the conversation\n```\n\n---\n\n## Extension Registration (Server-Side)\n\nServers register extensions at startup or runtime.\n\n```typescript\ninterface ExtensionRegistry {\n // Register an extension with the server\n register(manifest: ExtensionManifest, handler: ExtensionHandler): void;\n \n // Check if an extension is available\n isAvailable(extensionId: string): boolean;\n \n // Get extension manifest\n getManifest(extensionId: string): ExtensionManifest | undefined;\n \n // List all registered extensions\n listExtensions(): ExtensionManifest[];\n \n // Check if a method belongs to an extension\n getExtensionForMethod(method: string): string | undefined;\n}\n\ninterface ExtensionHandler {\n // Handle extension method calls\n handleRequest(method: string, params: unknown, context: RequestContext): Promise<unknown>;\n \n // Handle extension notifications (optional)\n handleNotification?(method: string, params: unknown, context: RequestContext): void;\n \n // Check if participant can use this extension\n checkAccess?(participant: Participant): ExtensionAccessResult;\n}\n\ninterface ExtensionAccessResult {\n allowed: boolean;\n capabilities?: string[]; // Granted capabilities within extension\n reason?: string; // If not allowed, why\n}\n```\n\n---\n\n## SDK Patterns for Extensions\n\n### Extension Client Pattern\n\nEach extension provides a typed client that wraps the base connection:\n\n```typescript\n// Example: Conversation extension client\nclass ConversationClient {\n private connection: ClientConnection;\n \n constructor(connection: ClientConnection) {\n // Verify extension is available\n const ext = connection.getExtension('conversations');\n if (!ext?.enabled) {\n throw new Error('Conversation extension not available');\n }\n this.connection = connection;\n }\n \n async create(params: ConvCreateParams): Promise<Conversation> {\n const result = await this.connection.request('conv/create', params);\n return result.conversation;\n }\n \n async join(conversationId: string): Promise<JoinResult> {\n return this.connection.request('conv/join', { conversationId });\n }\n \n // ... other methods\n}\n\n// Usage\nconst client = new ClientConnection(stream);\nawait client.connect({ capabilities: { extensions: { supported: [{ id: 'conversations', version: '1.0' }] } } });\n\nif (client.hasExtension('conversations')) {\n const conv = new ConversationClient(client);\n const conversation = await conv.create({ type: 'multi-agent' });\n}\n```\n\n### Extension Server Handler Pattern\n\n```typescript\n// Server-side extension handler\nconst conversationHandler: ExtensionHandler = {\n async handleRequest(method, params, context) {\n switch (method) {\n case 'conv/create':\n return createConversation(params, context);\n case 'conv/join':\n return joinConversation(params, context);\n case 'conv/turn':\n return addTurn(params, context);\n // ...\n }\n },\n \n checkAccess(participant) {\n // Check if participant has required capabilities\n if (!participant.capabilities?.messaging?.canSend) {\n return { allowed: false, reason: 'Requires messaging.canSend' };\n }\n return { allowed: true, capabilities: ['conversations.canCreate', 'conversations.canJoin'] };\n }\n};\n\n// Register with server\nserver.extensions.register(CONVERSATION_MANIFEST, conversationHandler);\n```\n\n---\n\n## Versioning\n\n### Semantic Versioning\n\nExtensions follow semver:\n- **Major**: Breaking changes to method signatures or behavior\n- **Minor**: New methods or events, backward-compatible\n- **Patch**: Bug fixes, documentation\n\n### Version Negotiation\n\nWhen client and server have different versions:\n\n1. **Client 1.0, Server 1.2**: Compatible, use 1.0 features\n2. **Client 1.2, Server 1.0**: Compatible, use 1.0 features\n3. **Client 2.0, Server 1.0**: Incompatible if client requires 2.x features\n4. **Client 1.x, Server 2.0**: Server may support 1.x compatibility mode\n\n### Effective Version\n\nThe connection uses the **minimum compatible version**:\n\n```typescript\ninterface ExtensionAvailability {\n id: string;\n version: string; // Server's version\n effectiveVersion: string; // Negotiated version to use\n enabled: boolean;\n}\n```\n\n---\n\n## Error Handling\n\n### Extension-Specific Error Codes\n\nExtensions can define their own error codes in a reserved range:\n\n| Range | Purpose |\n|-------|---------|\n| -32700 to -32600 | JSON-RPC standard errors |\n| 1000-4999 | MAP core errors |\n| 5000-5999 | Federation errors |\n| 10000-19999 | Reserved for extensions |\n\nEach extension claims a sub-range:\n- Conversations: 10000-10099\n- Streaming: 10100-10199\n- Transactions: 10200-10299\n\n### Extension Error Example\n\n```json\n{\n \"jsonrpc\": \"2.0\",\n \"id\": 1,\n \"error\": {\n \"code\": 10001,\n \"message\": \"Conversation not found\",\n \"data\": {\n \"category\": \"extension\",\n \"extension\": \"conversations\",\n \"retryable\": false,\n \"details\": {\n \"conversationId\": \"conv-xyz\"\n }\n }\n }\n}\n```\n\n---\n\n## Security Considerations\n\n### Capability Gating\n\nExtensions should define granular capabilities:\n\n```typescript\ncapabilities: [\n { path: 'conversations.canCreate', description: 'Create new conversations' },\n { path: 'conversations.canJoin', description: 'Join existing conversations' },\n { path: 'conversations.canInvite', description: 'Invite others to conversations' },\n { path: 'conversations.canViewHistory', description: 'View conversation history before joining' },\n]\n```\n\nServers grant capabilities based on participant identity and role.\n\n### Data Isolation\n\nExtension data should respect MAP's visibility model:\n- Conversation turns visible only to participants\n- Extension metadata in messages follows message visibility rules\n- Events filtered by subscriber permissions\n\n### Input Validation\n\nExtensions MUST validate all inputs:\n- Conversation IDs must exist and be accessible\n- Turn content must match expected types\n- Thread references must be valid\n\n---\n\n## Open Questions\n\n1. **Extension discovery endpoint**: Should there be a `map/extensions/list` method to query available extensions without connecting?\n\n2. **Dynamic extension loading**: Can extensions be enabled/disabled mid-session, or only at connect time?\n\n3. **Extension configuration**: How do extensions expose configuration options to participants?\n\n4. **Cross-extension dependencies**: How do extensions declare and verify dependencies on other extensions?\n\n5. **Extension deprecation**: How are deprecated extensions phased out?\n","priority":2,"archived":0,"archived_at":null,"created_at":"2026-01-31 05:08:06","updated_at":"2026-01-31 05:08:06","parent_id":null,"parent_uuid":null,"relationships":[],"tags":["architecture","extensions","p0","protocol"]}
9
- {"id":"s-1bob","uuid":"96dd7cfe-bf89-4aa1-9aeb-5576e54a471a","title":"MAP Conversation Extension","file_path":"specs/s-1bob_map_conversation_extension.md","content":"# MAP Conversation Extension\n\n## Overview\n\nThe Conversation Extension (`conversations`) provides a unified model for tracking all forms of interaction in a multi-agent system:\n\n- **User ↔ Agent sessions**: A user chatting with one or more agents\n- **Agent trajectories**: An agent's internal reasoning, tool calls, and results\n- **Agent ↔ Agent messaging**: Explicit coordination between agents\n- **Mixed conversations**: Any combination of the above\n\nThis extension treats the **conversation as the universal container** for all interactions, making them observable, joinable, and replayable.\n\n## Extension Manifest\n\n```typescript\nconst CONVERSATION_EXTENSION: ExtensionManifest = {\n id: 'conversations',\n version: '1.0.0',\n namespace: 'conv',\n description: 'Unified conversation tracking for all agent interactions',\n \n requires: {\n mapVersion: '>=1.0.0',\n capabilities: ['messaging.canSend', 'observation.canObserve']\n },\n \n provides: {\n methods: [\n // Conversation lifecycle\n { name: 'conv/create', type: 'request', description: 'Create a new conversation' },\n { name: 'conv/get', type: 'request', description: 'Get conversation details' },\n { name: 'conv/list', type: 'request', description: 'List conversations' },\n { name: 'conv/close', type: 'request', description: 'Close a conversation' },\n \n // Participation\n { name: 'conv/join', type: 'request', description: 'Join an existing conversation' },\n { name: 'conv/leave', type: 'request', description: 'Leave a conversation' },\n { name: 'conv/invite', type: 'request', description: 'Invite a participant' },\n \n // Turns\n { name: 'conv/turn', type: 'request', description: 'Add a turn to conversation' },\n { name: 'conv/turns/list', type: 'request', description: 'List turns in conversation' },\n { name: 'conv/turns/subscribe', type: 'request', description: 'Subscribe to new turns' },\n \n // Threading\n { name: 'conv/thread/create', type: 'request', description: 'Create a thread within conversation' },\n { name: 'conv/thread/list', type: 'request', description: 'List threads in conversation' },\n \n // History & catch-up\n { name: 'conv/summary', type: 'request', description: 'Get or generate conversation summary' },\n { name: 'conv/replay', type: 'request', description: 'Replay turns from a point' },\n ],\n \n events: [\n { type: 'conversation.created', description: 'New conversation created' },\n { type: 'conversation.closed', description: 'Conversation closed' },\n { type: 'conversation.participant.joined', description: 'Participant joined conversation' },\n { type: 'conversation.participant.left', description: 'Participant left conversation' },\n { type: 'conversation.turn.added', description: 'New turn added to conversation' },\n { type: 'conversation.turn.updated', description: 'Turn was updated (e.g., tool completed)' },\n { type: 'conversation.thread.created', description: 'New thread created' },\n { type: 'conversation.summary.generated', description: 'Summary was generated' },\n ],\n \n capabilities: [\n { path: 'conversations.canCreate', description: 'Create new conversations' },\n { path: 'conversations.canJoin', description: 'Join existing conversations' },\n { path: 'conversations.canInvite', description: 'Invite participants to conversations' },\n { path: 'conversations.canViewHistory', description: 'View full conversation history' },\n { path: 'conversations.canCreateThreads', description: 'Create threads within conversations' },\n ]\n }\n};\n```\n\n---\n\n## Core Types\n\n### Identifiers\n\n```typescript\n// Branded ID types for type safety\ntype ConversationId = string; // Format: 'conv-{ulid}'\ntype TurnId = string; // Format: 'turn-{ulid}'\ntype ThreadId = string; // Format: 'thread-{ulid}'\n```\n\n### Conversation\n\n```typescript\ntype ConversationType = \n | 'user-session' // User interacting with agent(s)\n | 'agent-task' // Agent working autonomously (trajectory)\n | 'multi-agent' // Agent-to-agent coordination\n | 'mixed'; // Any combination\n\ntype ConversationStatus = \n | 'active' // Ongoing conversation\n | 'paused' // Temporarily suspended\n | 'completed' // Successfully finished\n | 'failed' // Ended due to error\n | 'archived'; // Closed and archived\n\ninterface Conversation {\n id: ConversationId;\n type: ConversationType;\n status: ConversationStatus;\n \n // Human-readable subject/title\n subject?: string;\n \n // Current participants (use conv/get with include.participants for full list)\n participantCount: number;\n \n // Nesting - conversations can spawn child conversations\n parentConversationId?: ConversationId;\n parentTurnId?: TurnId; // Which turn spawned this conversation\n \n // Timestamps\n createdAt: Timestamp;\n updatedAt: Timestamp;\n closedAt?: Timestamp;\n \n // Who started this conversation\n createdBy: ParticipantId;\n \n // Extension metadata\n metadata?: Record<string, unknown>;\n}\n```\n\n### Conversation Participant\n\n```typescript\ntype ParticipantRole = \n | 'initiator' // Started the conversation\n | 'assistant' // Agent helping the initiator\n | 'worker' // Agent performing sub-tasks\n | 'observer' // Can see but not participate\n | 'moderator'; // Can manage participants\n\ninterface ConversationParticipant {\n id: ParticipantId;\n type: 'user' | 'agent' | 'system';\n \n // Role in this conversation\n role: ParticipantRole;\n \n // Participation timeline\n joinedAt: Timestamp;\n leftAt?: Timestamp;\n \n // What this participant can do\n permissions: ConversationPermissions;\n \n // Agent-specific info (if type === 'agent')\n agentInfo?: {\n agentId: AgentId;\n name?: string;\n role?: string; // MAP agent role\n };\n}\n\ninterface ConversationPermissions {\n // Can add turns to the conversation\n canSend: boolean;\n \n // Can see turns (but not necessarily participate)\n canObserve: boolean;\n \n // Can invite new participants\n canInvite: boolean;\n \n // Can remove participants\n canRemove: boolean;\n \n // Can create threads\n canCreateThreads: boolean;\n \n // How much history can this participant see\n historyAccess: 'none' | 'from-join' | 'full';\n \n // Can see internal turns (agent thoughts, etc.)\n canSeeInternal: boolean;\n}\n```\n\n### Thread\n\n```typescript\ninterface Thread {\n id: ThreadId;\n conversationId: ConversationId;\n \n // Threads can nest\n parentThreadId?: ThreadId;\n \n // What this thread is about\n subject?: string;\n \n // The turn that started this thread\n rootTurnId: TurnId;\n \n // Stats\n turnCount: number;\n participantCount: number;\n \n // Timestamps\n createdAt: Timestamp;\n updatedAt: Timestamp;\n \n createdBy: ParticipantId;\n}\n```\n\n### Turn\n\nA turn is the atomic unit of conversation - any interaction from any participant.\n\n```typescript\ninterface Turn {\n id: TurnId;\n conversationId: ConversationId;\n \n // Who added this turn\n participant: ParticipantId;\n participantType: 'user' | 'agent' | 'system';\n \n // When\n timestamp: Timestamp;\n \n // Threading\n threadId?: ThreadId; // null = main thread\n inReplyTo?: TurnId; // Direct response to\n references?: TurnId[]; // Broader context (email-style)\n \n // The actual content\n content: TurnContent;\n \n // Who can see this turn\n visibility: TurnVisibility;\n \n // For mutable turns (e.g., streaming, tool calls)\n status?: 'pending' | 'streaming' | 'complete' | 'failed';\n updatedAt?: Timestamp;\n \n // Extension metadata\n metadata?: Record<string, unknown>;\n}\n```\n\n### Turn Content Types\n\nTurn content is a discriminated union covering all interaction types:\n\n```typescript\ntype TurnContent =\n | UserMessageContent\n | AgentMessageContent\n | AgentThoughtContent\n | ToolCallContent\n | ToolResultContent\n | AgentToAgentContent\n | HandoffContent\n | SystemEventContent\n | SummaryContent;\n\n// User sends a message\ninterface UserMessageContent {\n type: 'user-message';\n text: string;\n attachments?: Attachment[];\n}\n\n// Agent sends a message (to user or generally)\ninterface AgentMessageContent {\n type: 'agent-message';\n text: string;\n \n // Is this meant for the user to see? (vs internal coordination)\n toUser: boolean;\n \n // For streaming responses\n streaming?: boolean;\n complete?: boolean;\n}\n\n// Agent's internal reasoning (trajectory)\ninterface AgentThoughtContent {\n type: 'agent-thought';\n reasoning: string;\n \n // Optional structured thinking\n steps?: string[];\n conclusion?: string;\n confidence?: number; // 0-1\n}\n\n// Agent calls a tool\ninterface ToolCallContent {\n type: 'tool-call';\n tool: string; // Tool name/identifier\n input: unknown; // Tool input parameters\n \n // Execution state\n status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';\n \n // Populated when complete\n resultTurnId?: TurnId; // Links to the tool-result turn\n}\n\n// Tool returns a result\ninterface ToolResultContent {\n type: 'tool-result';\n toolCallTurnId: TurnId; // Links back to the tool-call turn\n \n output: unknown; // Tool output\n error?: {\n code: string;\n message: string;\n details?: unknown;\n };\n \n // Execution metadata\n durationMs?: number;\n}\n\n// Agent sends message to another agent\ninterface AgentToAgentContent {\n type: 'a2a-message';\n toAgent: ParticipantId;\n \n payload: unknown; // The message content\n \n // Request/response pattern\n expectsResponse: boolean;\n responseTimeout?: number; // ms\n responseTurnId?: TurnId; // Populated when response received\n}\n\n// Control transferred to another participant\ninterface HandoffContent {\n type: 'handoff';\n fromParticipant: ParticipantId;\n toParticipant: ParticipantId;\n \n // Why the handoff\n reason?: string;\n \n // Context for the recipient\n context?: string;\n \n // Where to resume from\n resumeFromTurn?: TurnId;\n \n // Was it accepted?\n accepted?: boolean;\n acceptedAt?: Timestamp;\n}\n\n// System-generated event turn\ninterface SystemEventContent {\n type: 'system-event';\n event: string; // e.g., 'participant.joined', 'timeout', 'error'\n data?: Record<string, unknown>;\n}\n\n// Summarization of previous turns\ninterface SummaryContent {\n type: 'summary';\n \n // What turns this summary covers\n coversTurns: {\n from: TurnId;\n to: TurnId;\n count: number;\n };\n \n // The summary text\n summary: string;\n \n // Extracted key points\n keyPoints?: string[];\n keyDecisions?: string[];\n openQuestions?: string[];\n \n // How was this generated\n generatedBy: 'system' | 'agent' | 'user';\n generatedAt: Timestamp;\n}\n\ninterface Attachment {\n type: 'file' | 'image' | 'link' | 'code';\n name?: string;\n mimeType?: string;\n url?: string; // For links/hosted files\n content?: string; // For inline content (base64 or text)\n size?: number; // bytes\n}\n```\n\n### Turn Visibility\n\n```typescript\ntype TurnVisibility =\n | { type: 'all' } // All conversation participants\n | { type: 'participants'; ids: ParticipantId[] } // Specific participants only\n | { type: 'role'; roles: ParticipantRole[] } // By conversation role\n | { type: 'private' }; // Only the author\n\n// Visibility modifiers\ninterface TurnVisibilityOptions {\n visibility: TurnVisibility;\n \n // Hide from history after this duration\n ephemeral?: boolean;\n ephemeralTtlMs?: number;\n \n // Redact sensitive fields in history\n redactFields?: string[];\n}\n```\n\n---\n\n## Methods\n\n### conv/create\n\nCreate a new conversation.\n\n**Request:**\n```typescript\ninterface ConvCreateRequest {\n // Conversation type\n type?: ConversationType; // Default: 'mixed'\n \n // Human-readable subject\n subject?: string;\n \n // Nest under another conversation\n parentConversationId?: ConversationId;\n parentTurnId?: TurnId;\n \n // Initial participants to invite\n initialParticipants?: Array<{\n id: ParticipantId;\n role?: ParticipantRole;\n permissions?: Partial<ConversationPermissions>;\n }>;\n \n // Initial turn (optional)\n initialTurn?: {\n content: TurnContent;\n visibility?: TurnVisibility;\n };\n \n metadata?: Record<string, unknown>;\n}\n```\n\n**Response:**\n```typescript\ninterface ConvCreateResponse {\n conversation: Conversation;\n participant: ConversationParticipant; // Creator's participation record\n initialTurn?: Turn; // If initialTurn was provided\n}\n```\n\n### conv/get\n\nGet conversation details.\n\n**Request:**\n```typescript\ninterface ConvGetRequest {\n conversationId: ConversationId;\n \n include?: {\n participants?: boolean; // Include participant list\n threads?: boolean; // Include thread list\n recentTurns?: number; // Include N most recent turns\n stats?: boolean; // Include turn counts, etc.\n };\n}\n```\n\n**Response:**\n```typescript\ninterface ConvGetResponse {\n conversation: Conversation;\n \n // If requested\n participants?: ConversationParticipant[];\n threads?: Thread[];\n recentTurns?: Turn[];\n stats?: {\n totalTurns: number;\n turnsByType: Record<TurnContent['type'], number>;\n activeParticipants: number;\n threadCount: number;\n };\n}\n```\n\n### conv/list\n\nList conversations the participant has access to.\n\n**Request:**\n```typescript\ninterface ConvListRequest {\n filter?: {\n type?: ConversationType[];\n status?: ConversationStatus[];\n participantId?: ParticipantId; // Conversations involving this participant\n createdAfter?: Timestamp;\n createdBefore?: Timestamp;\n parentConversationId?: ConversationId;\n };\n \n limit?: number; // Default: 50\n cursor?: string;\n}\n```\n\n**Response:**\n```typescript\ninterface ConvListResponse {\n conversations: Conversation[];\n nextCursor?: string;\n hasMore: boolean;\n}\n```\n\n### conv/join\n\nJoin an existing conversation.\n\n**Request:**\n```typescript\ninterface ConvJoinRequest {\n conversationId: ConversationId;\n \n // Requested role (may be denied/modified)\n role?: ParticipantRole;\n \n // How to catch up on history\n catchUp?: {\n from: 'beginning' | 'recent' | TurnId | Timestamp;\n limit?: number; // Max turns to receive\n includeSummary?: boolean; // Get a summary instead of all turns\n };\n}\n```\n\n**Response:**\n```typescript\ninterface ConvJoinResponse {\n conversation: Conversation;\n participant: ConversationParticipant; // Your participation record\n \n // Based on catchUp settings and permissions\n history?: Turn[];\n historyCursor?: string;\n summary?: SummaryContent;\n}\n```\n\n### conv/leave\n\nLeave a conversation.\n\n**Request:**\n```typescript\ninterface ConvLeaveRequest {\n conversationId: ConversationId;\n reason?: string;\n}\n```\n\n**Response:**\n```typescript\ninterface ConvLeaveResponse {\n success: boolean;\n leftAt: Timestamp;\n}\n```\n\n### conv/invite\n\nInvite a participant to a conversation.\n\n**Request:**\n```typescript\ninterface ConvInviteRequest {\n conversationId: ConversationId;\n participant: {\n id: ParticipantId;\n role?: ParticipantRole;\n permissions?: Partial<ConversationPermissions>;\n };\n \n // Optional message to include with invitation\n message?: string;\n}\n```\n\n**Response:**\n```typescript\ninterface ConvInviteResponse {\n invited: boolean;\n participant?: ConversationParticipant;\n \n // If invitation requires acceptance\n invitationId?: string;\n pending?: boolean;\n}\n```\n\n### conv/turn\n\nAdd a turn to a conversation.\n\n**Request:**\n```typescript\ninterface ConvTurnRequest {\n conversationId: ConversationId;\n \n content: TurnContent;\n \n // Threading\n threadId?: ThreadId;\n inReplyTo?: TurnId;\n references?: TurnId[];\n \n // Visibility\n visibility?: TurnVisibility;\n \n // For streaming/mutable turns\n streaming?: boolean;\n \n metadata?: Record<string, unknown>;\n}\n```\n\n**Response:**\n```typescript\ninterface ConvTurnResponse {\n turn: Turn;\n}\n```\n\n### conv/turns/list\n\nList turns in a conversation.\n\n**Request:**\n```typescript\ninterface ConvTurnsListRequest {\n conversationId: ConversationId;\n \n filter?: {\n threadId?: ThreadId; // null for main thread only\n includeAllThreads?: boolean; // Include all threads\n contentTypes?: TurnContent['type'][];\n participantId?: ParticipantId;\n afterTurnId?: TurnId;\n beforeTurnId?: TurnId;\n afterTimestamp?: Timestamp;\n beforeTimestamp?: Timestamp;\n };\n \n limit?: number; // Default: 100\n order?: 'asc' | 'desc'; // Default: 'asc'\n}\n```\n\n**Response:**\n```typescript\ninterface ConvTurnsListResponse {\n turns: Turn[];\n hasMore: boolean;\n nextCursor?: string;\n}\n```\n\n### conv/turns/subscribe\n\nSubscribe to new turns in a conversation.\n\n**Request:**\n```typescript\ninterface ConvTurnsSubscribeRequest {\n conversationId: ConversationId;\n \n filter?: {\n threadId?: ThreadId;\n contentTypes?: TurnContent['type'][];\n excludeOwn?: boolean; // Don't receive own turns\n };\n \n // Also receive turn updates (streaming chunks, tool completions)\n includeUpdates?: boolean;\n}\n```\n\n**Response:**\n```typescript\ninterface ConvTurnsSubscribeResponse {\n subscriptionId: SubscriptionId;\n}\n\n// Turns delivered via standard map/event notifications:\n// event.type = 'conversation.turn.added' or 'conversation.turn.updated'\n```\n\n### conv/thread/create\n\nCreate a thread within a conversation.\n\n**Request:**\n```typescript\ninterface ConvThreadCreateRequest {\n conversationId: ConversationId;\n \n // The turn that starts this thread\n rootTurnId: TurnId;\n \n // Optional subject\n subject?: string;\n \n // Nest under another thread\n parentThreadId?: ThreadId;\n}\n```\n\n**Response:**\n```typescript\ninterface ConvThreadCreateResponse {\n thread: Thread;\n}\n```\n\n### conv/summary\n\nGet or generate a conversation summary.\n\n**Request:**\n```typescript\ninterface ConvSummaryRequest {\n conversationId: ConversationId;\n \n // What to summarize\n scope?: {\n fromTurnId?: TurnId;\n toTurnId?: TurnId;\n threadId?: ThreadId;\n };\n \n // Force regeneration even if cached summary exists\n regenerate?: boolean;\n \n // What to include in summary\n include?: {\n keyPoints?: boolean;\n keyDecisions?: boolean;\n openQuestions?: boolean;\n participants?: boolean;\n };\n}\n```\n\n**Response:**\n```typescript\ninterface ConvSummaryResponse {\n summary: SummaryContent;\n \n // If this was generated now vs cached\n generated: boolean;\n cachedAt?: Timestamp;\n}\n```\n\n### conv/close\n\nClose a conversation.\n\n**Request:**\n```typescript\ninterface ConvCloseRequest {\n conversationId: ConversationId;\n \n status?: 'completed' | 'failed' | 'archived';\n reason?: string;\n \n // Final summary\n generateSummary?: boolean;\n}\n```\n\n**Response:**\n```typescript\ninterface ConvCloseResponse {\n conversation: Conversation;\n summary?: SummaryContent;\n}\n```\n\n---\n\n## Events\n\n### conversation.created\n\nEmitted when a new conversation is created.\n\n```typescript\ninterface ConversationCreatedEvent {\n type: 'conversation.created';\n data: {\n conversation: Conversation;\n createdBy: ParticipantId;\n initialParticipants: ParticipantId[];\n };\n}\n```\n\n### conversation.closed\n\nEmitted when a conversation is closed.\n\n```typescript\ninterface ConversationClosedEvent {\n type: 'conversation.closed';\n data: {\n conversationId: ConversationId;\n status: ConversationStatus;\n closedBy: ParticipantId;\n reason?: string;\n };\n}\n```\n\n### conversation.participant.joined\n\nEmitted when someone joins a conversation.\n\n```typescript\ninterface ParticipantJoinedEvent {\n type: 'conversation.participant.joined';\n data: {\n conversationId: ConversationId;\n participant: ConversationParticipant;\n invitedBy?: ParticipantId;\n };\n}\n```\n\n### conversation.participant.left\n\nEmitted when someone leaves a conversation.\n\n```typescript\ninterface ParticipantLeftEvent {\n type: 'conversation.participant.left';\n data: {\n conversationId: ConversationId;\n participantId: ParticipantId;\n reason?: string;\n leftAt: Timestamp;\n };\n}\n```\n\n### conversation.turn.added\n\nEmitted when a new turn is added.\n\n```typescript\ninterface TurnAddedEvent {\n type: 'conversation.turn.added';\n data: {\n conversationId: ConversationId;\n turn: Turn;\n \n // For privacy, content may be omitted based on subscriber permissions\n contentIncluded: boolean;\n };\n}\n```\n\n### conversation.turn.updated\n\nEmitted when a turn is updated (streaming, tool completion).\n\n```typescript\ninterface TurnUpdatedEvent {\n type: 'conversation.turn.updated';\n data: {\n conversationId: ConversationId;\n turnId: TurnId;\n \n // What changed\n updates: {\n status?: Turn['status'];\n content?: Partial<TurnContent>;\n };\n \n updatedAt: Timestamp;\n };\n}\n```\n\n### conversation.thread.created\n\nEmitted when a new thread is created.\n\n```typescript\ninterface ThreadCreatedEvent {\n type: 'conversation.thread.created';\n data: {\n conversationId: ConversationId;\n thread: Thread;\n createdBy: ParticipantId;\n };\n}\n```\n\n---\n\n## Interop with MAP Core\n\n### Message → Turn Mapping\n\nWhen `map/send` is called with conversation metadata, it's recorded as a turn:\n\n```typescript\n// Agent sends a MAP message that's also a conversation turn\n{\n method: 'map/send',\n params: {\n to: { agent: 'agent-b' },\n payload: { text: 'Let me help with that' },\n meta: {\n correlationId: 'conv-123', // Maps to conversationId\n 'x-conv': {\n turnType: 'a2a-message',\n threadId: 'thread-456',\n inReplyTo: 'turn-789',\n visibility: { type: 'all' }\n }\n }\n }\n}\n```\n\nThe server (if conversation extension is enabled):\n1. Routes the message via normal MAP routing\n2. Records a turn in conversation `conv-123`\n3. Emits `conversation.turn.added` event\n\n### Scope → Conversation Mapping\n\nA MAP scope can be associated with a conversation:\n\n```typescript\n// Create a scope that's backed by a conversation\n{\n method: 'map/scopes/create',\n params: {\n scopeId: 'planning-room',\n metadata: {\n 'x-conv': {\n conversationId: 'conv-planning-123',\n recordScopeMessages: true // All scope messages become turns\n }\n }\n }\n}\n```\n\n### Agent Registration → Participant\n\nWhen an agent registers with conversation context:\n\n```typescript\n{\n method: 'map/agents/register',\n params: {\n agentId: 'worker-1',\n metadata: {\n 'x-conv': {\n autoJoinConversations: ['conv-123'],\n role: 'worker'\n }\n }\n }\n}\n```\n\n---\n\n## Error Codes\n\n| Code | Name | Description |\n|------|------|-------------|\n| 10000 | CONVERSATION_NOT_FOUND | Conversation ID doesn't exist |\n| 10001 | CONVERSATION_CLOSED | Cannot modify a closed conversation |\n| 10002 | NOT_A_PARTICIPANT | Caller is not a participant |\n| 10003 | PERMISSION_DENIED | Lacks required permission for operation |\n| 10004 | TURN_NOT_FOUND | Turn ID doesn't exist |\n| 10005 | THREAD_NOT_FOUND | Thread ID doesn't exist |\n| 10006 | INVALID_TURN_CONTENT | Turn content validation failed |\n| 10007 | PARTICIPANT_ALREADY_JOINED | Already a participant |\n| 10008 | INVITATION_REQUIRED | Cannot join without invitation |\n| 10009 | HISTORY_ACCESS_DENIED | Cannot access requested history |\n| 10010 | PARENT_CONVERSATION_NOT_FOUND | Parent conversation doesn't exist |\n\n---\n\n## Implementation Considerations\n\n### Storage\n\nConversations and turns need persistent storage:\n- **Conversations table**: id, type, status, subject, parent, timestamps, metadata\n- **Participants table**: conversation_id, participant_id, role, permissions, joined_at, left_at\n- **Turns table**: id, conversation_id, thread_id, participant_id, content (JSONB), visibility, timestamps\n- **Threads table**: id, conversation_id, parent_thread_id, root_turn_id, subject\n\nIndexes needed for:\n- Turns by conversation + timestamp (primary query)\n- Turns by thread\n- Conversations by participant\n- Active conversations\n\n### Scalability\n\nFor high-volume conversations:\n- **Turn pagination**: Always paginate turn queries\n- **Streaming subscriptions**: Use backpressure for high-frequency turns\n- **Summary caching**: Cache summaries, invalidate on new turns\n- **Archival**: Move old conversations to cold storage\n\n### Privacy\n\n- **Turn visibility**: Enforce at query time, not just delivery\n- **History access**: Check permissions before returning history on join\n- **Internal turns**: Agent thoughts may be hidden from users\n- **Redaction**: Support redacting sensitive content from history\n\n---\n\n## Open Questions\n\n1. **Turn immutability**: Should turns be immutable after creation? Or allow edits with version history?\n\n2. **Conversation forking**: Can a conversation be forked/branched like git? Useful for \"what-if\" scenarios.\n\n3. **Cross-conversation references**: How do turns reference turns in other conversations?\n\n4. **Real-time presence**: Should there be \"typing\" indicators or presence in conversations?\n\n5. **Conversation templates**: Pre-defined conversation structures for common patterns?\n\n6. **Retention policies**: How long are conversations/turns retained? Per-conversation settings?\n\n7. **Search**: Full-text search across conversation content? Separate extension?\n\n8. **Export/Import**: Standard format for exporting conversations? (e.g., for training data)\n","priority":2,"archived":0,"archived_at":null,"created_at":"2026-01-31 05:09:59","updated_at":"2026-01-31 05:09:59","parent_id":null,"parent_uuid":null,"relationships":[{"from":"s-1bob","from_type":"spec","to":"s-65aw","to_type":"spec","type":"depends-on"}],"tags":["conversations","extension","p0","protocol"]}
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2025
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.