@cursor/july 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (623) hide show
  1. package/AGENTS.md +290 -0
  2. package/LICENSE.md +3 -0
  3. package/README.md +1233 -0
  4. package/dist/ab.d.ts +290 -0
  5. package/dist/ab.d.ts.map +1 -0
  6. package/dist/ab.js +386 -0
  7. package/dist/bin/agent-serve.d.ts +15 -0
  8. package/dist/bin/agent-serve.d.ts.map +1 -0
  9. package/dist/bin/agent-serve.js +754 -0
  10. package/dist/channels/github/api.d.ts +101 -0
  11. package/dist/channels/github/api.d.ts.map +1 -0
  12. package/dist/channels/github/api.js +229 -0
  13. package/dist/channels/github/auth.d.ts +67 -0
  14. package/dist/channels/github/auth.d.ts.map +1 -0
  15. package/dist/channels/github/auth.js +239 -0
  16. package/dist/channels/github/binding.d.ts +73 -0
  17. package/dist/channels/github/binding.d.ts.map +1 -0
  18. package/dist/channels/github/binding.js +114 -0
  19. package/dist/channels/github/defaults.d.ts +24 -0
  20. package/dist/channels/github/defaults.d.ts.map +1 -0
  21. package/dist/channels/github/defaults.js +100 -0
  22. package/dist/channels/github/events.d.ts +18 -0
  23. package/dist/channels/github/events.d.ts.map +1 -0
  24. package/dist/channels/github/events.js +59 -0
  25. package/dist/channels/github/forward.d.ts +137 -0
  26. package/dist/channels/github/forward.d.ts.map +1 -0
  27. package/dist/channels/github/forward.js +271 -0
  28. package/dist/channels/github/github-channel.d.ts +127 -0
  29. package/dist/channels/github/github-channel.d.ts.map +1 -0
  30. package/dist/channels/github/github-channel.js +1688 -0
  31. package/dist/channels/github/index.d.ts +52 -0
  32. package/dist/channels/github/index.d.ts.map +1 -0
  33. package/dist/channels/github/index.js +51 -0
  34. package/dist/channels/github/replay.d.ts +115 -0
  35. package/dist/channels/github/replay.d.ts.map +1 -0
  36. package/dist/channels/github/replay.js +282 -0
  37. package/dist/channels/github/state.d.ts +26 -0
  38. package/dist/channels/github/state.d.ts.map +1 -0
  39. package/dist/channels/github/state.js +50 -0
  40. package/dist/channels/github/types.d.ts +385 -0
  41. package/dist/channels/github/types.d.ts.map +1 -0
  42. package/dist/channels/github/types.js +1 -0
  43. package/dist/channels/slack/api.d.ts +117 -0
  44. package/dist/channels/slack/api.d.ts.map +1 -0
  45. package/dist/channels/slack/api.js +437 -0
  46. package/dist/channels/slack/approvals.d.ts +32 -0
  47. package/dist/channels/slack/approvals.d.ts.map +1 -0
  48. package/dist/channels/slack/approvals.js +157 -0
  49. package/dist/channels/slack/assistant.d.ts +23 -0
  50. package/dist/channels/slack/assistant.d.ts.map +1 -0
  51. package/dist/channels/slack/assistant.js +67 -0
  52. package/dist/channels/slack/auth.d.ts +40 -0
  53. package/dist/channels/slack/auth.d.ts.map +1 -0
  54. package/dist/channels/slack/auth.js +71 -0
  55. package/dist/channels/slack/channel-watch.d.ts +46 -0
  56. package/dist/channels/slack/channel-watch.d.ts.map +1 -0
  57. package/dist/channels/slack/channel-watch.js +180 -0
  58. package/dist/channels/slack/constants.d.ts +17 -0
  59. package/dist/channels/slack/constants.d.ts.map +1 -0
  60. package/dist/channels/slack/constants.js +27 -0
  61. package/dist/channels/slack/credentials.d.ts +35 -0
  62. package/dist/channels/slack/credentials.d.ts.map +1 -0
  63. package/dist/channels/slack/credentials.js +88 -0
  64. package/dist/channels/slack/cursor-account.d.ts +76 -0
  65. package/dist/channels/slack/cursor-account.d.ts.map +1 -0
  66. package/dist/channels/slack/cursor-account.js +88 -0
  67. package/dist/channels/slack/defaults.d.ts +32 -0
  68. package/dist/channels/slack/defaults.d.ts.map +1 -0
  69. package/dist/channels/slack/defaults.js +380 -0
  70. package/dist/channels/slack/dispatch.d.ts +43 -0
  71. package/dist/channels/slack/dispatch.d.ts.map +1 -0
  72. package/dist/channels/slack/dispatch.js +185 -0
  73. package/dist/channels/slack/doctor.d.ts +24 -0
  74. package/dist/channels/slack/doctor.d.ts.map +1 -0
  75. package/dist/channels/slack/doctor.js +115 -0
  76. package/dist/channels/slack/event-id-dedupe.d.ts +10 -0
  77. package/dist/channels/slack/event-id-dedupe.d.ts.map +1 -0
  78. package/dist/channels/slack/event-id-dedupe.js +23 -0
  79. package/dist/channels/slack/inbound.d.ts +41 -0
  80. package/dist/channels/slack/inbound.d.ts.map +1 -0
  81. package/dist/channels/slack/inbound.js +166 -0
  82. package/dist/channels/slack/index.d.ts +45 -0
  83. package/dist/channels/slack/index.d.ts.map +1 -0
  84. package/dist/channels/slack/index.js +44 -0
  85. package/dist/channels/slack/init.d.ts +32 -0
  86. package/dist/channels/slack/init.d.ts.map +1 -0
  87. package/dist/channels/slack/init.js +82 -0
  88. package/dist/channels/slack/interactive.d.ts +19 -0
  89. package/dist/channels/slack/interactive.d.ts.map +1 -0
  90. package/dist/channels/slack/interactive.js +142 -0
  91. package/dist/channels/slack/links.d.ts +10 -0
  92. package/dist/channels/slack/links.d.ts.map +1 -0
  93. package/dist/channels/slack/links.js +19 -0
  94. package/dist/channels/slack/live-delivery.d.ts +75 -0
  95. package/dist/channels/slack/live-delivery.d.ts.map +1 -0
  96. package/dist/channels/slack/live-delivery.js +143 -0
  97. package/dist/channels/slack/log.d.ts +25 -0
  98. package/dist/channels/slack/log.d.ts.map +1 -0
  99. package/dist/channels/slack/log.js +47 -0
  100. package/dist/channels/slack/manifest.d.ts +57 -0
  101. package/dist/channels/slack/manifest.d.ts.map +1 -0
  102. package/dist/channels/slack/manifest.js +110 -0
  103. package/dist/channels/slack/pr-ref.d.ts +18 -0
  104. package/dist/channels/slack/pr-ref.d.ts.map +1 -0
  105. package/dist/channels/slack/pr-ref.js +36 -0
  106. package/dist/channels/slack/setup.d.ts +14 -0
  107. package/dist/channels/slack/setup.d.ts.map +1 -0
  108. package/dist/channels/slack/setup.js +86 -0
  109. package/dist/channels/slack/slack-channel.d.ts +15 -0
  110. package/dist/channels/slack/slack-channel.d.ts.map +1 -0
  111. package/dist/channels/slack/slack-channel.js +281 -0
  112. package/dist/channels/slack/socket-mode.d.ts +26 -0
  113. package/dist/channels/slack/socket-mode.d.ts.map +1 -0
  114. package/dist/channels/slack/socket-mode.js +116 -0
  115. package/dist/channels/slack/stream.d.ts +26 -0
  116. package/dist/channels/slack/stream.d.ts.map +1 -0
  117. package/dist/channels/slack/stream.js +39 -0
  118. package/dist/channels/slack/tool-approval-events.d.ts +33 -0
  119. package/dist/channels/slack/tool-approval-events.d.ts.map +1 -0
  120. package/dist/channels/slack/tool-approval-events.js +129 -0
  121. package/dist/channels/slack/types.d.ts +326 -0
  122. package/dist/channels/slack/types.d.ts.map +1 -0
  123. package/dist/channels/slack/types.js +4 -0
  124. package/dist/channels.d.ts +116 -0
  125. package/dist/channels.d.ts.map +1 -0
  126. package/dist/channels.js +294 -0
  127. package/dist/connections.d.ts +75 -0
  128. package/dist/connections.d.ts.map +1 -0
  129. package/dist/connections.js +75 -0
  130. package/dist/docs/404.html +22 -0
  131. package/dist/docs/ab.html +87 -0
  132. package/dist/docs/assets/ab.md.COdXkces.js +63 -0
  133. package/dist/docs/assets/ab.md.COdXkces.lean.js +1 -0
  134. package/dist/docs/assets/app.DqfFEmJd.js +1 -0
  135. package/dist/docs/assets/building-with-agents.md.CJCtZCyi.js +13 -0
  136. package/dist/docs/assets/building-with-agents.md.CJCtZCyi.lean.js +1 -0
  137. package/dist/docs/assets/chunks/@localSearchIndexroot.CcVk1uKq.js +1 -0
  138. package/dist/docs/assets/chunks/VPLocalSearchBox.BaLEdS15.js +9 -0
  139. package/dist/docs/assets/chunks/framework.CAZyNGu9.js +19 -0
  140. package/dist/docs/assets/chunks/theme.CZRvu_0q.js +2 -0
  141. package/dist/docs/assets/concepts.md.Cfb9b-k1.js +4 -0
  142. package/dist/docs/assets/concepts.md.Cfb9b-k1.lean.js +1 -0
  143. package/dist/docs/assets/deployment.md.Dx1TYNk5.js +14 -0
  144. package/dist/docs/assets/deployment.md.Dx1TYNk5.lean.js +1 -0
  145. package/dist/docs/assets/evals.md.DPZ_MAnI.js +85 -0
  146. package/dist/docs/assets/evals.md.DPZ_MAnI.lean.js +1 -0
  147. package/dist/docs/assets/guides_agent-to-agent.md.CrtrsySy.js +10 -0
  148. package/dist/docs/assets/guides_agent-to-agent.md.CrtrsySy.lean.js +1 -0
  149. package/dist/docs/assets/guides_cloud-runtime.md.CYlNMTNp.js +9 -0
  150. package/dist/docs/assets/guides_cloud-runtime.md.CYlNMTNp.lean.js +1 -0
  151. package/dist/docs/assets/guides_github.md.DwbKhCeS.js +20 -0
  152. package/dist/docs/assets/guides_github.md.DwbKhCeS.lean.js +1 -0
  153. package/dist/docs/assets/guides_human-in-the-loop.md.Dvuctx7s.js +28 -0
  154. package/dist/docs/assets/guides_human-in-the-loop.md.Dvuctx7s.lean.js +1 -0
  155. package/dist/docs/assets/guides_slack.md.bv41fHfW.js +37 -0
  156. package/dist/docs/assets/guides_slack.md.bv41fHfW.lean.js +1 -0
  157. package/dist/docs/assets/guides_webhooks.md.hFTik3lf.js +50 -0
  158. package/dist/docs/assets/guides_webhooks.md.hFTik3lf.lean.js +1 -0
  159. package/dist/docs/assets/hillclimbing.md.DHNast08.js +4 -0
  160. package/dist/docs/assets/hillclimbing.md.DHNast08.lean.js +1 -0
  161. package/dist/docs/assets/index.md.BPKcj5AI.js +20 -0
  162. package/dist/docs/assets/index.md.BPKcj5AI.lean.js +1 -0
  163. package/dist/docs/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 +0 -0
  164. package/dist/docs/assets/inter-italic-cyrillic.By2_1cv3.woff2 +0 -0
  165. package/dist/docs/assets/inter-italic-greek-ext.1u6EdAuj.woff2 +0 -0
  166. package/dist/docs/assets/inter-italic-greek.DJ8dCoTZ.woff2 +0 -0
  167. package/dist/docs/assets/inter-italic-latin-ext.CN1xVJS-.woff2 +0 -0
  168. package/dist/docs/assets/inter-italic-latin.C2AdPX0b.woff2 +0 -0
  169. package/dist/docs/assets/inter-italic-vietnamese.BSbpV94h.woff2 +0 -0
  170. package/dist/docs/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 +0 -0
  171. package/dist/docs/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 +0 -0
  172. package/dist/docs/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 +0 -0
  173. package/dist/docs/assets/inter-roman-greek.BBVDIX6e.woff2 +0 -0
  174. package/dist/docs/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 +0 -0
  175. package/dist/docs/assets/inter-roman-latin.Di8DUHzh.woff2 +0 -0
  176. package/dist/docs/assets/inter-roman-vietnamese.BjW4sHH5.woff2 +0 -0
  177. package/dist/docs/assets/quickstart.md.tVPiGK_L.js +35 -0
  178. package/dist/docs/assets/quickstart.md.tVPiGK_L.lean.js +1 -0
  179. package/dist/docs/assets/reference_agent-config.md.Bpd7HQwf.js +34 -0
  180. package/dist/docs/assets/reference_agent-config.md.Bpd7HQwf.lean.js +1 -0
  181. package/dist/docs/assets/reference_channels.md.D7JTR03W.js +53 -0
  182. package/dist/docs/assets/reference_channels.md.D7JTR03W.lean.js +1 -0
  183. package/dist/docs/assets/reference_cli.md.DA730zCu.js +33 -0
  184. package/dist/docs/assets/reference_cli.md.DA730zCu.lean.js +1 -0
  185. package/dist/docs/assets/reference_connections.md.C3vNH_DE.js +26 -0
  186. package/dist/docs/assets/reference_connections.md.C3vNH_DE.lean.js +1 -0
  187. package/dist/docs/assets/reference_hooks.md.BCEc3MyM.js +12 -0
  188. package/dist/docs/assets/reference_hooks.md.BCEc3MyM.lean.js +1 -0
  189. package/dist/docs/assets/reference_http-api.md.DBAahtdz.js +11 -0
  190. package/dist/docs/assets/reference_http-api.md.DBAahtdz.lean.js +1 -0
  191. package/dist/docs/assets/reference_instructions.md.BC05LEQ8.js +12 -0
  192. package/dist/docs/assets/reference_instructions.md.BC05LEQ8.lean.js +1 -0
  193. package/dist/docs/assets/reference_playground.md.CPZhfYaO.js +1 -0
  194. package/dist/docs/assets/reference_playground.md.CPZhfYaO.lean.js +1 -0
  195. package/dist/docs/assets/reference_project-layout.md.Bd_CKtNS.js +31 -0
  196. package/dist/docs/assets/reference_project-layout.md.Bd_CKtNS.lean.js +1 -0
  197. package/dist/docs/assets/reference_schedules.md.D7qijxLk.js +82 -0
  198. package/dist/docs/assets/reference_schedules.md.D7qijxLk.lean.js +1 -0
  199. package/dist/docs/assets/reference_sessions.md.DdjWabnN.js +8 -0
  200. package/dist/docs/assets/reference_sessions.md.DdjWabnN.lean.js +1 -0
  201. package/dist/docs/assets/reference_skills.md.VQnlBT3Q.js +15 -0
  202. package/dist/docs/assets/reference_skills.md.VQnlBT3Q.lean.js +1 -0
  203. package/dist/docs/assets/reference_subagents.md.CIRAVcPK.js +10 -0
  204. package/dist/docs/assets/reference_subagents.md.CIRAVcPK.lean.js +1 -0
  205. package/dist/docs/assets/reference_tools.md.DF5kwlt0.js +46 -0
  206. package/dist/docs/assets/reference_tools.md.DF5kwlt0.lean.js +1 -0
  207. package/dist/docs/assets/scaffolding-agents.md.CyYfWGdc.js +1 -0
  208. package/dist/docs/assets/scaffolding-agents.md.CyYfWGdc.lean.js +1 -0
  209. package/dist/docs/assets/style.Bj-IXa5W.css +1 -0
  210. package/dist/docs/assets/troubleshooting.md.DkovjS7h.js +1 -0
  211. package/dist/docs/assets/troubleshooting.md.DkovjS7h.lean.js +1 -0
  212. package/dist/docs/building-with-agents.html +37 -0
  213. package/dist/docs/concepts.html +28 -0
  214. package/dist/docs/deployment.html +38 -0
  215. package/dist/docs/evals.html +109 -0
  216. package/dist/docs/guides/agent-to-agent.html +34 -0
  217. package/dist/docs/guides/cloud-runtime.html +33 -0
  218. package/dist/docs/guides/github.html +44 -0
  219. package/dist/docs/guides/human-in-the-loop.html +52 -0
  220. package/dist/docs/guides/slack.html +61 -0
  221. package/dist/docs/guides/webhooks.html +74 -0
  222. package/dist/docs/hashmap.json +1 -0
  223. package/dist/docs/hillclimbing.html +28 -0
  224. package/dist/docs/index.html +44 -0
  225. package/dist/docs/quickstart.html +59 -0
  226. package/dist/docs/reference/agent-config.html +58 -0
  227. package/dist/docs/reference/channels.html +77 -0
  228. package/dist/docs/reference/cli.html +57 -0
  229. package/dist/docs/reference/connections.html +50 -0
  230. package/dist/docs/reference/hooks.html +36 -0
  231. package/dist/docs/reference/http-api.html +35 -0
  232. package/dist/docs/reference/instructions.html +36 -0
  233. package/dist/docs/reference/playground.html +25 -0
  234. package/dist/docs/reference/project-layout.html +55 -0
  235. package/dist/docs/reference/schedules.html +106 -0
  236. package/dist/docs/reference/sessions.html +32 -0
  237. package/dist/docs/reference/skills.html +39 -0
  238. package/dist/docs/reference/subagents.html +34 -0
  239. package/dist/docs/reference/tools.html +70 -0
  240. package/dist/docs/scaffolding-agents.html +25 -0
  241. package/dist/docs/troubleshooting.html +25 -0
  242. package/dist/docs/vp-icons.css +0 -0
  243. package/dist/evals.d.ts +312 -0
  244. package/dist/evals.d.ts.map +1 -0
  245. package/dist/evals.js +254 -0
  246. package/dist/hooks.d.ts +23 -0
  247. package/dist/hooks.d.ts.map +1 -0
  248. package/dist/hooks.js +23 -0
  249. package/dist/index.d.ts +71 -0
  250. package/dist/index.d.ts.map +1 -0
  251. package/dist/index.js +101 -0
  252. package/dist/internal/ab-collector.d.ts +42 -0
  253. package/dist/internal/ab-collector.d.ts.map +1 -0
  254. package/dist/internal/ab-collector.js +153 -0
  255. package/dist/internal/ab-fold.d.ts +36 -0
  256. package/dist/internal/ab-fold.d.ts.map +1 -0
  257. package/dist/internal/ab-fold.js +171 -0
  258. package/dist/internal/ab-snapshot.d.ts +70 -0
  259. package/dist/internal/ab-snapshot.d.ts.map +1 -0
  260. package/dist/internal/ab-snapshot.js +208 -0
  261. package/dist/internal/approval-gate.d.ts +34 -0
  262. package/dist/internal/approval-gate.d.ts.map +1 -0
  263. package/dist/internal/approval-gate.js +85 -0
  264. package/dist/internal/auth.d.ts +8 -0
  265. package/dist/internal/auth.d.ts.map +1 -0
  266. package/dist/internal/auth.js +27 -0
  267. package/dist/internal/authored-alias-hooks.d.ts +31 -0
  268. package/dist/internal/authored-alias-hooks.d.ts.map +1 -0
  269. package/dist/internal/authored-alias-hooks.js +43 -0
  270. package/dist/internal/authored-loaders.d.ts +25 -0
  271. package/dist/internal/authored-loaders.d.ts.map +1 -0
  272. package/dist/internal/authored-loaders.js +129 -0
  273. package/dist/internal/bounded-int.d.ts +11 -0
  274. package/dist/internal/bounded-int.d.ts.map +1 -0
  275. package/dist/internal/bounded-int.js +13 -0
  276. package/dist/internal/brand.d.ts +6 -0
  277. package/dist/internal/brand.d.ts.map +1 -0
  278. package/dist/internal/brand.js +17 -0
  279. package/dist/internal/chat-client.d.ts +19 -0
  280. package/dist/internal/chat-client.d.ts.map +1 -0
  281. package/dist/internal/chat-client.js +327 -0
  282. package/dist/internal/cli-ax.d.ts +58 -0
  283. package/dist/internal/cli-ax.d.ts.map +1 -0
  284. package/dist/internal/cli-ax.js +345 -0
  285. package/dist/internal/cli-cursor.d.ts +20 -0
  286. package/dist/internal/cli-cursor.d.ts.map +1 -0
  287. package/dist/internal/cli-cursor.js +227 -0
  288. package/dist/internal/cli-github.d.ts +47 -0
  289. package/dist/internal/cli-github.d.ts.map +1 -0
  290. package/dist/internal/cli-github.js +644 -0
  291. package/dist/internal/cli-slack.d.ts +18 -0
  292. package/dist/internal/cli-slack.d.ts.map +1 -0
  293. package/dist/internal/cli-slack.js +144 -0
  294. package/dist/internal/cloud-merge.d.ts +23 -0
  295. package/dist/internal/cloud-merge.d.ts.map +1 -0
  296. package/dist/internal/cloud-merge.js +28 -0
  297. package/dist/internal/cron.d.ts +17 -0
  298. package/dist/internal/cron.d.ts.map +1 -0
  299. package/dist/internal/cron.js +60 -0
  300. package/dist/internal/cursor/account-mcp.d.ts +95 -0
  301. package/dist/internal/cursor/account-mcp.d.ts.map +1 -0
  302. package/dist/internal/cursor/account-mcp.js +342 -0
  303. package/dist/internal/cursor/backend-client.d.ts +118 -0
  304. package/dist/internal/cursor/backend-client.d.ts.map +1 -0
  305. package/dist/internal/cursor/backend-client.js +477 -0
  306. package/dist/internal/cursor/credentials.d.ts +112 -0
  307. package/dist/internal/cursor/credentials.d.ts.map +1 -0
  308. package/dist/internal/cursor/credentials.js +329 -0
  309. package/dist/internal/cursor-event-relay.d.ts +111 -0
  310. package/dist/internal/cursor-event-relay.d.ts.map +1 -0
  311. package/dist/internal/cursor-event-relay.js +181 -0
  312. package/dist/internal/cursor-relay-core.d.ts +113 -0
  313. package/dist/internal/cursor-relay-core.d.ts.map +1 -0
  314. package/dist/internal/cursor-relay-core.js +364 -0
  315. package/dist/internal/cursor-slack-relay.d.ts +94 -0
  316. package/dist/internal/cursor-slack-relay.d.ts.map +1 -0
  317. package/dist/internal/cursor-slack-relay.js +171 -0
  318. package/dist/internal/describe-error.d.ts +3 -0
  319. package/dist/internal/describe-error.d.ts.map +1 -0
  320. package/dist/internal/describe-error.js +4 -0
  321. package/dist/internal/dev-mode.d.ts +13 -0
  322. package/dist/internal/dev-mode.d.ts.map +1 -0
  323. package/dist/internal/dev-mode.js +17 -0
  324. package/dist/internal/discovery.d.ts +55 -0
  325. package/dist/internal/discovery.d.ts.map +1 -0
  326. package/dist/internal/discovery.js +1427 -0
  327. package/dist/internal/distribution.d.ts +47 -0
  328. package/dist/internal/distribution.d.ts.map +1 -0
  329. package/dist/internal/distribution.js +82 -0
  330. package/dist/internal/docs-site.d.ts +28 -0
  331. package/dist/internal/docs-site.d.ts.map +1 -0
  332. package/dist/internal/docs-site.js +169 -0
  333. package/dist/internal/duration.d.ts +8 -0
  334. package/dist/internal/duration.d.ts.map +1 -0
  335. package/dist/internal/duration.js +35 -0
  336. package/dist/internal/eval-run-store.d.ts +81 -0
  337. package/dist/internal/eval-run-store.d.ts.map +1 -0
  338. package/dist/internal/eval-run-store.js +396 -0
  339. package/dist/internal/eval-runner.d.ts +76 -0
  340. package/dist/internal/eval-runner.d.ts.map +1 -0
  341. package/dist/internal/eval-runner.js +336 -0
  342. package/dist/internal/event-log.d.ts +51 -0
  343. package/dist/internal/event-log.d.ts.map +1 -0
  344. package/dist/internal/event-log.js +167 -0
  345. package/dist/internal/event-mapper.d.ts +46 -0
  346. package/dist/internal/event-mapper.d.ts.map +1 -0
  347. package/dist/internal/event-mapper.js +226 -0
  348. package/dist/internal/frontmatter.d.ts +12 -0
  349. package/dist/internal/frontmatter.d.ts.map +1 -0
  350. package/dist/internal/frontmatter.js +61 -0
  351. package/dist/internal/github-fanout.d.ts +32 -0
  352. package/dist/internal/github-fanout.d.ts.map +1 -0
  353. package/dist/internal/github-fanout.js +122 -0
  354. package/dist/internal/handleAgentServeTrigger.d.ts +59 -0
  355. package/dist/internal/handleAgentServeTrigger.d.ts.map +1 -0
  356. package/dist/internal/handleAgentServeTrigger.js +115 -0
  357. package/dist/internal/host-platforms.d.ts +40 -0
  358. package/dist/internal/host-platforms.d.ts.map +1 -0
  359. package/dist/internal/host-platforms.js +113 -0
  360. package/dist/internal/http-channel.d.ts +54 -0
  361. package/dist/internal/http-channel.d.ts.map +1 -0
  362. package/dist/internal/http-channel.js +382 -0
  363. package/dist/internal/ids.d.ts +19 -0
  364. package/dist/internal/ids.d.ts.map +1 -0
  365. package/dist/internal/ids.js +34 -0
  366. package/dist/internal/init-project.d.ts +22 -0
  367. package/dist/internal/init-project.d.ts.map +1 -0
  368. package/dist/internal/init-project.js +125 -0
  369. package/dist/internal/json-dir-store.d.ts +32 -0
  370. package/dist/internal/json-dir-store.d.ts.map +1 -0
  371. package/dist/internal/json-dir-store.js +100 -0
  372. package/dist/internal/json-schema.d.ts +18 -0
  373. package/dist/internal/json-schema.d.ts.map +1 -0
  374. package/dist/internal/json-schema.js +53 -0
  375. package/dist/internal/log-ring.d.ts +23 -0
  376. package/dist/internal/log-ring.d.ts.map +1 -0
  377. package/dist/internal/log-ring.js +42 -0
  378. package/dist/internal/map-pool.d.ts +6 -0
  379. package/dist/internal/map-pool.d.ts.map +1 -0
  380. package/dist/internal/map-pool.js +41 -0
  381. package/dist/internal/mcp-endpoint.d.ts +59 -0
  382. package/dist/internal/mcp-endpoint.d.ts.map +1 -0
  383. package/dist/internal/mcp-endpoint.js +355 -0
  384. package/dist/internal/mcp-host.d.ts +28 -0
  385. package/dist/internal/mcp-host.d.ts.map +1 -0
  386. package/dist/internal/mcp-host.js +162 -0
  387. package/dist/internal/net.d.ts +5 -0
  388. package/dist/internal/net.d.ts.map +1 -0
  389. package/dist/internal/net.js +30 -0
  390. package/dist/internal/peer-connections.d.ts +44 -0
  391. package/dist/internal/peer-connections.d.ts.map +1 -0
  392. package/dist/internal/peer-connections.js +67 -0
  393. package/dist/internal/playground/static.d.ts +35 -0
  394. package/dist/internal/playground/static.d.ts.map +1 -0
  395. package/dist/internal/playground/static.js +162 -0
  396. package/dist/internal/playground/toolchain.d.ts +57 -0
  397. package/dist/internal/playground/toolchain.d.ts.map +1 -0
  398. package/dist/internal/playground/toolchain.js +226 -0
  399. package/dist/internal/reminder-runner.d.ts +62 -0
  400. package/dist/internal/reminder-runner.d.ts.map +1 -0
  401. package/dist/internal/reminder-runner.js +582 -0
  402. package/dist/internal/reminder-schedule.d.ts +22 -0
  403. package/dist/internal/reminder-schedule.d.ts.map +1 -0
  404. package/dist/internal/reminder-schedule.js +82 -0
  405. package/dist/internal/reminder-store.d.ts +58 -0
  406. package/dist/internal/reminder-store.d.ts.map +1 -0
  407. package/dist/internal/reminder-store.js +106 -0
  408. package/dist/internal/resolved-connections.d.ts +41 -0
  409. package/dist/internal/resolved-connections.d.ts.map +1 -0
  410. package/dist/internal/resolved-connections.js +73 -0
  411. package/dist/internal/review-comments.d.ts +50 -0
  412. package/dist/internal/review-comments.d.ts.map +1 -0
  413. package/dist/internal/review-comments.js +194 -0
  414. package/dist/internal/router.d.ts +33 -0
  415. package/dist/internal/router.d.ts.map +1 -0
  416. package/dist/internal/router.js +93 -0
  417. package/dist/internal/run-client.d.ts +49 -0
  418. package/dist/internal/run-client.d.ts.map +1 -0
  419. package/dist/internal/run-client.js +208 -0
  420. package/dist/internal/schedule-runner.d.ts +31 -0
  421. package/dist/internal/schedule-runner.d.ts.map +1 -0
  422. package/dist/internal/schedule-runner.js +126 -0
  423. package/dist/internal/sdk-runner.d.ts +41 -0
  424. package/dist/internal/sdk-runner.d.ts.map +1 -0
  425. package/dist/internal/sdk-runner.js +221 -0
  426. package/dist/internal/server.d.ts +25 -0
  427. package/dist/internal/server.d.ts.map +1 -0
  428. package/dist/internal/server.js +1374 -0
  429. package/dist/internal/session-engine.d.ts +287 -0
  430. package/dist/internal/session-engine.d.ts.map +1 -0
  431. package/dist/internal/session-engine.js +1377 -0
  432. package/dist/internal/session-store.d.ts +29 -0
  433. package/dist/internal/session-store.d.ts.map +1 -0
  434. package/dist/internal/session-store.js +140 -0
  435. package/dist/internal/session-urls.d.ts +10 -0
  436. package/dist/internal/session-urls.d.ts.map +1 -0
  437. package/dist/internal/session-urls.js +25 -0
  438. package/dist/internal/stream-progress.d.ts +14 -0
  439. package/dist/internal/stream-progress.d.ts.map +1 -0
  440. package/dist/internal/stream-progress.js +195 -0
  441. package/dist/internal/tool-result.d.ts +20 -0
  442. package/dist/internal/tool-result.d.ts.map +1 -0
  443. package/dist/internal/tool-result.js +45 -0
  444. package/dist/internal/trajectory.d.ts +56 -0
  445. package/dist/internal/trajectory.d.ts.map +1 -0
  446. package/dist/internal/trajectory.js +248 -0
  447. package/dist/internal/workspace.d.ts +42 -0
  448. package/dist/internal/workspace.d.ts.map +1 -0
  449. package/dist/internal/workspace.js +199 -0
  450. package/dist/playground/assets/cursor-icons-outline-oY2V_mvK.woff2 +0 -0
  451. package/dist/playground/assets/index-1K-hG-7p.css +1 -0
  452. package/dist/playground/assets/index-FlWjhg3x.js +79 -0
  453. package/dist/playground/index.html +17 -0
  454. package/dist/reminders.d.ts +23 -0
  455. package/dist/reminders.d.ts.map +1 -0
  456. package/dist/reminders.js +22 -0
  457. package/dist/schedules.d.ts +23 -0
  458. package/dist/schedules.d.ts.map +1 -0
  459. package/dist/schedules.js +26 -0
  460. package/dist/skills.d.ts +22 -0
  461. package/dist/skills.d.ts.map +1 -0
  462. package/dist/skills.js +22 -0
  463. package/dist/tools.d.ts +92 -0
  464. package/dist/tools.d.ts.map +1 -0
  465. package/dist/tools.js +68 -0
  466. package/dist/types.d.ts +1609 -0
  467. package/dist/types.d.ts.map +1 -0
  468. package/dist/types.js +80 -0
  469. package/docs/README.md +174 -0
  470. package/docs/ab.md +355 -0
  471. package/docs/building-with-agents.md +129 -0
  472. package/docs/concepts.md +177 -0
  473. package/docs/deployment.md +218 -0
  474. package/docs/evals.md +383 -0
  475. package/docs/guides/agent-to-agent.md +118 -0
  476. package/docs/guides/cloud-runtime.md +122 -0
  477. package/docs/guides/github.md +172 -0
  478. package/docs/guides/human-in-the-loop.md +131 -0
  479. package/docs/guides/slack.md +245 -0
  480. package/docs/guides/webhooks.md +201 -0
  481. package/docs/hillclimbing.md +93 -0
  482. package/docs/quickstart.md +183 -0
  483. package/docs/reference/agent-config.md +154 -0
  484. package/docs/reference/channels.md +206 -0
  485. package/docs/reference/cli.md +257 -0
  486. package/docs/reference/connections.md +137 -0
  487. package/docs/reference/hooks.md +74 -0
  488. package/docs/reference/http-api.md +219 -0
  489. package/docs/reference/instructions.md +77 -0
  490. package/docs/reference/playground.md +67 -0
  491. package/docs/reference/project-layout.md +119 -0
  492. package/docs/reference/schedules.md +220 -0
  493. package/docs/reference/sessions.md +165 -0
  494. package/docs/reference/skills.md +79 -0
  495. package/docs/reference/subagents.md +74 -0
  496. package/docs/reference/tools.md +203 -0
  497. package/docs/scaffolding-agents.md +113 -0
  498. package/docs/troubleshooting.md +95 -0
  499. package/package.json +185 -0
  500. package/skills/ab/SKILL.md +139 -0
  501. package/skills/create-agent/SKILL.md +219 -0
  502. package/skills/debug/SKILL.md +71 -0
  503. package/skills/evals/SKILL.md +217 -0
  504. package/skills/framework-map/SKILL.md +142 -0
  505. package/skills/github/SKILL.md +144 -0
  506. package/skills/hillclimb/SKILL.md +162 -0
  507. package/skills/setup-slack/SKILL.md +180 -0
  508. package/src/ab.ts +675 -0
  509. package/src/bin/agent-serve.ts +949 -0
  510. package/src/channels/github/api.ts +374 -0
  511. package/src/channels/github/auth.ts +275 -0
  512. package/src/channels/github/binding.ts +223 -0
  513. package/src/channels/github/defaults.ts +111 -0
  514. package/src/channels/github/events.ts +72 -0
  515. package/src/channels/github/forward.ts +381 -0
  516. package/src/channels/github/github-channel.ts +2270 -0
  517. package/src/channels/github/index.ts +52 -0
  518. package/src/channels/github/replay.ts +387 -0
  519. package/src/channels/github/state.ts +95 -0
  520. package/src/channels/github/types.ts +490 -0
  521. package/src/channels/slack/api.ts +580 -0
  522. package/src/channels/slack/approvals.ts +198 -0
  523. package/src/channels/slack/assistant.ts +76 -0
  524. package/src/channels/slack/auth.ts +112 -0
  525. package/src/channels/slack/channel-watch.ts +219 -0
  526. package/src/channels/slack/constants.ts +30 -0
  527. package/src/channels/slack/credentials.ts +90 -0
  528. package/src/channels/slack/cursor-account.ts +166 -0
  529. package/src/channels/slack/defaults.ts +450 -0
  530. package/src/channels/slack/dispatch.ts +293 -0
  531. package/src/channels/slack/doctor.ts +135 -0
  532. package/src/channels/slack/event-id-dedupe.ts +30 -0
  533. package/src/channels/slack/inbound.ts +219 -0
  534. package/src/channels/slack/index.ts +45 -0
  535. package/src/channels/slack/init.ts +133 -0
  536. package/src/channels/slack/interactive.ts +194 -0
  537. package/src/channels/slack/links.ts +23 -0
  538. package/src/channels/slack/live-delivery.ts +201 -0
  539. package/src/channels/slack/log.ts +83 -0
  540. package/src/channels/slack/manifest.ts +179 -0
  541. package/src/channels/slack/pr-ref.ts +50 -0
  542. package/src/channels/slack/setup.ts +97 -0
  543. package/src/channels/slack/slack-channel.ts +374 -0
  544. package/src/channels/slack/socket-mode.ts +154 -0
  545. package/src/channels/slack/stream.ts +63 -0
  546. package/src/channels/slack/tool-approval-events.ts +136 -0
  547. package/src/channels/slack/types.ts +349 -0
  548. package/src/channels.ts +385 -0
  549. package/src/connections.ts +89 -0
  550. package/src/evals.ts +501 -0
  551. package/src/hooks.ts +33 -0
  552. package/src/index.ts +184 -0
  553. package/src/internal/ab-collector.ts +210 -0
  554. package/src/internal/ab-fold.ts +228 -0
  555. package/src/internal/ab-snapshot.ts +335 -0
  556. package/src/internal/approval-gate.ts +115 -0
  557. package/src/internal/auth.ts +39 -0
  558. package/src/internal/authored-alias-hooks.ts +69 -0
  559. package/src/internal/authored-loaders.ts +138 -0
  560. package/src/internal/bounded-int.ts +27 -0
  561. package/src/internal/brand.ts +29 -0
  562. package/src/internal/chat-client.ts +407 -0
  563. package/src/internal/cli-ax.ts +474 -0
  564. package/src/internal/cli-cursor.ts +275 -0
  565. package/src/internal/cli-github.ts +849 -0
  566. package/src/internal/cli-slack.ts +175 -0
  567. package/src/internal/cloud-merge.ts +41 -0
  568. package/src/internal/cron.ts +78 -0
  569. package/src/internal/cursor/account-mcp.ts +472 -0
  570. package/src/internal/cursor/backend-client.ts +614 -0
  571. package/src/internal/cursor/credentials.ts +401 -0
  572. package/src/internal/cursor-event-relay.ts +281 -0
  573. package/src/internal/cursor-relay-core.ts +444 -0
  574. package/src/internal/cursor-slack-relay.ts +242 -0
  575. package/src/internal/describe-error.ts +4 -0
  576. package/src/internal/dev-mode.ts +20 -0
  577. package/src/internal/discovery.ts +1709 -0
  578. package/src/internal/distribution.ts +97 -0
  579. package/src/internal/docs-site.ts +191 -0
  580. package/src/internal/duration.ts +39 -0
  581. package/src/internal/eval-run-store.ts +519 -0
  582. package/src/internal/eval-runner.ts +522 -0
  583. package/src/internal/event-log.ts +175 -0
  584. package/src/internal/event-mapper.ts +272 -0
  585. package/src/internal/frontmatter.ts +77 -0
  586. package/src/internal/github-fanout.ts +159 -0
  587. package/src/internal/handleAgentServeTrigger.ts +156 -0
  588. package/src/internal/host-platforms.ts +144 -0
  589. package/src/internal/http-channel.ts +546 -0
  590. package/src/internal/ids.ts +44 -0
  591. package/src/internal/init-project.ts +165 -0
  592. package/src/internal/json-dir-store.ts +109 -0
  593. package/src/internal/json-schema.ts +69 -0
  594. package/src/internal/log-ring.ts +59 -0
  595. package/src/internal/map-pool.ts +32 -0
  596. package/src/internal/mcp-endpoint.ts +518 -0
  597. package/src/internal/mcp-host.ts +211 -0
  598. package/src/internal/net.ts +31 -0
  599. package/src/internal/peer-connections.ts +100 -0
  600. package/src/internal/playground/static.ts +189 -0
  601. package/src/internal/playground/toolchain.ts +277 -0
  602. package/src/internal/reminder-runner.ts +756 -0
  603. package/src/internal/reminder-schedule.ts +106 -0
  604. package/src/internal/reminder-store.ts +145 -0
  605. package/src/internal/resolved-connections.ts +145 -0
  606. package/src/internal/review-comments.ts +268 -0
  607. package/src/internal/router.ts +145 -0
  608. package/src/internal/run-client.ts +320 -0
  609. package/src/internal/schedule-runner.ts +161 -0
  610. package/src/internal/sdk-runner.ts +315 -0
  611. package/src/internal/server.ts +1737 -0
  612. package/src/internal/session-engine.ts +1810 -0
  613. package/src/internal/session-store.ts +147 -0
  614. package/src/internal/session-urls.ts +40 -0
  615. package/src/internal/stream-progress.ts +254 -0
  616. package/src/internal/tool-result.ts +54 -0
  617. package/src/internal/trajectory.ts +306 -0
  618. package/src/internal/workspace.ts +223 -0
  619. package/src/reminders.ts +35 -0
  620. package/src/schedules.ts +37 -0
  621. package/src/skills.ts +27 -0
  622. package/src/tools.ts +127 -0
  623. package/src/types.ts +1848 -0
@@ -0,0 +1,1810 @@
1
+ /**
2
+ * The session engine owns durable sessions and turn execution. A session is
3
+ * one Cursor agent conversation plus a materialized workspace, an event log,
4
+ * and channel bookkeeping (continuation keys, per-session channel state).
5
+ * Turns are executed by the Cursor SDK runner and their streaming updates
6
+ * are mapped onto the channel event vocabulary and dispatched to channel
7
+ * event handlers and hooks.
8
+ */
9
+
10
+ import { mkdir, rm } from "node:fs/promises";
11
+ import { join } from "node:path";
12
+ import type { SDKCustomTool } from "@cursor/sdk";
13
+ import {
14
+ type ABDefinition,
15
+ mergeABInstructions,
16
+ resolveABMaxPlaygroundSessions,
17
+ resolveABSplit,
18
+ } from "../ab.js";
19
+ import { slackThreadPermalink } from "../channels/slack/links.js";
20
+ import {
21
+ type AgentProject,
22
+ type ApprovalDecision,
23
+ type AuthContext,
24
+ type CallToolOptions,
25
+ type ChannelDefinition,
26
+ type ChannelEventHandler,
27
+ type ChannelSession,
28
+ type ConnectionTransport,
29
+ DIRECT_TOOL_CALL_CHANNEL_ID,
30
+ type HookHandler,
31
+ type HostContext,
32
+ type HttpChannelDefinition,
33
+ isSymbolicConnectionTransport,
34
+ type JsonValue,
35
+ type PendingApproval,
36
+ type ReceiveInput,
37
+ type ResolvedAgent,
38
+ type RunnerTurnRequest,
39
+ type SendMessageFn,
40
+ type SendMessageOptions,
41
+ type SessionEvent,
42
+ type SessionEventPayload,
43
+ type SessionInfo,
44
+ type SessionRecord,
45
+ type SessionSummary,
46
+ type ToolCallOutcome,
47
+ type ToolContext,
48
+ type ToolNeedsApproval,
49
+ type TurnOutcome,
50
+ } from "../types.js";
51
+ import { ABCollector } from "./ab-collector.js";
52
+ import { foldABStream } from "./ab-fold.js";
53
+ import { type ABSnapshot, buildABSnapshot } from "./ab-snapshot.js";
54
+ import { ApprovalGate } from "./approval-gate.js";
55
+ import { samePrincipal } from "./auth.js";
56
+ import { mergeCloudOptions, resolveSessionRuntime } from "./cloud-merge.js";
57
+ import { EventLogStore } from "./event-log.js";
58
+ import { TurnEventMapper } from "./event-mapper.js";
59
+ import { createHostPlatforms, type HostPlatforms } from "./host-platforms.js";
60
+ import {
61
+ formatContinuationToken,
62
+ newContinuationKey,
63
+ newSessionId,
64
+ newToolCallId,
65
+ } from "./ids.js";
66
+ import { validateToolInput } from "./json-schema.js";
67
+ import { McpHost } from "./mcp-host.js";
68
+ import { assertCloudCanHonorAccountServersFilters } from "./resolved-connections.js";
69
+ import type { AgentRunner } from "./sdk-runner.js";
70
+ import { SessionStore } from "./session-store.js";
71
+ import { normalizeToolResult, toolCallErrorMessage } from "./tool-result.js";
72
+ import {
73
+ buildAgentToolsCatalog,
74
+ materializeWorkspace,
75
+ writeWorkspaceFiles,
76
+ } from "./workspace.js";
77
+
78
+ export const HTTP_CHANNEL_ID = "http";
79
+ export const SCHEDULE_CHANNEL_PREFIX = "schedule:";
80
+
81
+ export class SessionBusyError extends Error {
82
+ constructor(sessionId: string) {
83
+ super(
84
+ `Session ${sessionId} already has a running turn; wait for session.waiting before sending a follow-up`
85
+ );
86
+ this.name = "SessionBusyError";
87
+ }
88
+ }
89
+
90
+ export class SessionAuthError extends Error {
91
+ constructor(sessionId: string) {
92
+ super(
93
+ `Session ${sessionId} belongs to a different principal; pass the original caller auth on follow-up`
94
+ );
95
+ this.name = "SessionAuthError";
96
+ }
97
+ }
98
+
99
+ export class UnknownChannelError extends Error {
100
+ constructor(channelId: string) {
101
+ super(`Unknown channel "${channelId}"`);
102
+ this.name = "UnknownChannelError";
103
+ }
104
+ }
105
+
106
+ export class UnknownToolError extends Error {
107
+ constructor(
108
+ toolName: string,
109
+ readonly availableToolNames: string[]
110
+ ) {
111
+ super(`Unknown tool "${toolName}"`);
112
+ this.name = "UnknownToolError";
113
+ }
114
+ }
115
+
116
+ export class ToolNotDirectlyCallableError extends Error {
117
+ constructor(toolName: string) {
118
+ super(
119
+ `Tool "${toolName}" runs in the agent environment (execution: "agent") and cannot be called directly on the host`
120
+ );
121
+ this.name = "ToolNotDirectlyCallableError";
122
+ }
123
+ }
124
+
125
+ export class ToolInputError extends Error {
126
+ constructor(args: { toolName: string; detail: string }) {
127
+ super(`Invalid input for tool "${args.toolName}": ${args.detail}`);
128
+ this.name = "ToolInputError";
129
+ }
130
+ }
131
+
132
+ export class UnknownSessionError extends Error {
133
+ constructor(sessionId: string) {
134
+ super(`Unknown session ${sessionId}`);
135
+ this.name = "UnknownSessionError";
136
+ }
137
+ }
138
+
139
+ export class ApprovalNotFoundError extends Error {
140
+ constructor(sessionId: string, callId: string) {
141
+ super(`No pending approval for session ${sessionId} call ${callId}`);
142
+ this.name = "ApprovalNotFoundError";
143
+ }
144
+ }
145
+
146
+ export class ApprovalInterruptedError extends Error {
147
+ constructor(sessionId: string, callId: string) {
148
+ super(
149
+ `Approval for session ${sessionId} call ${callId} is no longer live (host may have restarted while the tool was parked)`
150
+ );
151
+ this.name = "ApprovalInterruptedError";
152
+ }
153
+ }
154
+
155
+ export interface SessionEngineOptions {
156
+ project: AgentProject;
157
+ stateRoot: string;
158
+ runner: AgentRunner;
159
+ logger?: (line: string) => void;
160
+ }
161
+
162
+ /**
163
+ * A connection the serve host resolved once it was listening — a **peer**
164
+ * (`defineConnection({ agent })` mapped to the target agent's MCP endpoint)
165
+ * or the **Cursor account bridge** (`defineConnection({ cursorAccount })`
166
+ * mapped to the loopback bridge route). Local turns (and host-side MCP
167
+ * calls) use {@link localUrl}; cloud turns use {@link cloudUrl} when the
168
+ * serve host configured a public URL (peers and the account bridge).
169
+ */
170
+ export interface EngineResolvedConnection {
171
+ /** MCP server name the model sees (the connection filename). */
172
+ name: string;
173
+ /** Loopback MCP URL, e.g. `http://127.0.0.1:3000/<slug>/v1/mcp`. */
174
+ localUrl: string;
175
+ /** Publicly reachable MCP URL for cloud turns, when configured. */
176
+ cloudUrl?: string;
177
+ /** Auth headers calls must present (host bearer / bridge secret). */
178
+ headers?: Record<string, string>;
179
+ /**
180
+ * When set, cloud turns must reach the account bridge via {@link cloudUrl}
181
+ * so the filter is enforced — omitting the bridge must not fail open to
182
+ * the account's broader native connector set.
183
+ */
184
+ accountServersFilter?: readonly string[];
185
+ }
186
+
187
+ export interface EngineSendOptions extends SendMessageOptions {
188
+ mode?: "chat" | "task";
189
+ }
190
+
191
+ export class SessionEngine {
192
+ readonly project: AgentProject;
193
+ readonly stateRoot: string;
194
+ readonly sessions: SessionStore;
195
+
196
+ private readonly runner: AgentRunner;
197
+ private readonly logger: (line: string) => void;
198
+ private readonly logs: EventLogStore;
199
+ private readonly channelsById = new Map<string, ChannelDefinition>();
200
+ /** Sessions with a turn being admitted (before the run promise exists). */
201
+ private readonly pendingTurns = new Set<string>();
202
+ private readonly activeTurns = new Map<string, Promise<TurnOutcome>>();
203
+ /** Aborts the in-flight turn per session so a follow-up can preempt it. */
204
+ private readonly turnAborts = new Map<string, AbortController>();
205
+ /** Serializes turn admission per session (latest follow-up wins). */
206
+ private readonly admissionChains = new Map<string, Promise<unknown>>();
207
+ private readonly lastOutcomes = new Map<string, TurnOutcome>();
208
+ private readonly updateChains = new Map<string, Promise<unknown>>();
209
+ private readonly dispatchChains = new Map<string, Promise<void>>();
210
+ private readonly backgroundWork = new Set<Promise<unknown>>();
211
+ private readonly mcpHost: McpHost;
212
+ private readonly platforms: HostPlatforms;
213
+ private readonly approvals = new ApprovalGate();
214
+ private readonly abCollector: ABCollector;
215
+ private reminderApi: HostContext["reminders"];
216
+ /** Serve-time resolved connections (see {@link setResolvedConnections}). */
217
+ private resolvedConnections: EngineResolvedConnection[] = [];
218
+
219
+ constructor(options: SessionEngineOptions) {
220
+ this.project = options.project;
221
+ this.stateRoot = options.stateRoot;
222
+ this.runner = options.runner;
223
+ this.logger = options.logger ?? (() => {});
224
+ this.sessions = new SessionStore(join(options.stateRoot, "sessions"));
225
+ this.logs = new EventLogStore(
226
+ (sessionId) => this.sessions.eventFilePath(sessionId),
227
+ (sessionId, error) =>
228
+ this.logger(
229
+ `[agent-serve] failed to persist events for ${sessionId}: ${describeError(error)}`
230
+ )
231
+ );
232
+ this.abCollector = new ABCollector(
233
+ options.project.abs,
234
+ this.logger,
235
+ async (sessionId) => (await this.logs.get(sessionId)).snapshot(0),
236
+ {
237
+ projectRoot: options.project.rootDir,
238
+ ...(options.project.abConfig?.persistSamples === undefined
239
+ ? {}
240
+ : { persistSamples: options.project.abConfig.persistSamples }),
241
+ }
242
+ );
243
+ // Peer and Cursor-account transports are symbolic until the serve host
244
+ // resolves them; the host-side MCP registry starts with the concrete
245
+ // connections and gains the rest via setResolvedConnections.
246
+ this.mcpHost = new McpHost(
247
+ options.project.agent.connections.filter(
248
+ (connection) => !isSymbolicConnectionTransport(connection.transport)
249
+ )
250
+ );
251
+ this.platforms = createHostPlatforms();
252
+ for (const channel of options.project.channels) {
253
+ this.channelsById.set(channel.id, channel.definition);
254
+ }
255
+ }
256
+
257
+ /**
258
+ * Attach serve-time resolved connections (peers, Cursor account bridge).
259
+ * They join the per-turn SDK `mcpServers` map (loopback URL for local
260
+ * turns, `cloudUrl` for cloud turns) and the host-side registry
261
+ * (`ctx.host.mcp`, always loopback).
262
+ */
263
+ setResolvedConnections(connections: EngineResolvedConnection[]): void {
264
+ this.resolvedConnections = connections;
265
+ for (const connection of connections) {
266
+ this.mcpHost.addConnection({
267
+ name: connection.name,
268
+ transport: {
269
+ url: connection.localUrl,
270
+ ...(connection.headers === undefined
271
+ ? {}
272
+ : { headers: connection.headers }),
273
+ },
274
+ });
275
+ }
276
+ }
277
+
278
+ /** Shared host services (MCP / GitHub / Slack / reminders). */
279
+ get host(): HostContext {
280
+ return {
281
+ mcp: this.mcpHost,
282
+ github: this.platforms.github,
283
+ slack: this.platforms.slack,
284
+ reminders: this.reminderApi,
285
+ };
286
+ }
287
+
288
+ /** Attach the serve-host reminder API (called from startServer). */
289
+ setReminders(api: HostContext["reminders"]): void {
290
+ this.reminderApi = api;
291
+ }
292
+
293
+ // ==========================================================================
294
+ // Sending messages
295
+ // ==========================================================================
296
+
297
+ /**
298
+ * Start or resume a session on a channel. When `continuationToken`
299
+ * addresses an existing session this is a follow-up turn; otherwise a new
300
+ * session is created (keyed by the provided token, or a fresh one).
301
+ */
302
+ async send(
303
+ channelId: string,
304
+ message: string,
305
+ options: EngineSendOptions = {}
306
+ ): Promise<ChannelSession> {
307
+ const auth = options.auth ?? null;
308
+ const existing =
309
+ options.continuationToken === undefined
310
+ ? undefined
311
+ : await this.sessions.findByContinuation(
312
+ channelId,
313
+ options.continuationToken
314
+ );
315
+
316
+ if (existing !== undefined) {
317
+ if (!samePrincipal(existing.auth, auth)) {
318
+ throw new SessionAuthError(existing.sessionId);
319
+ }
320
+ // Refresh harness cwd when the caller supplies a newer worktree (e.g. PR
321
+ // synchronize). Leave the durable continuation key unchanged.
322
+ let record = existing;
323
+ if (
324
+ options.workspaceDir !== undefined &&
325
+ options.workspaceDir !== existing.workspaceOverride
326
+ ) {
327
+ record = await this.updateRecord(existing.sessionId, (current) => ({
328
+ ...current,
329
+ workspaceOverride: options.workspaceDir,
330
+ pendingWorkspaceFiles:
331
+ options.workspaceFiles ?? current.pendingWorkspaceFiles,
332
+ }));
333
+ } else if (options.workspaceFiles !== undefined) {
334
+ record = await this.updateRecord(existing.sessionId, (current) => ({
335
+ ...current,
336
+ pendingWorkspaceFiles: options.workspaceFiles,
337
+ }));
338
+ }
339
+ await this.startTurn(record.sessionId, message);
340
+ return this.channelSession(record.sessionId, record, false);
341
+ }
342
+
343
+ const now = new Date().toISOString();
344
+ const channelState = this.initialChannelState(channelId, options.state);
345
+ const purpose = options.purpose === "eval" ? "eval" : "live";
346
+ let record: SessionRecord = {
347
+ sessionId: newSessionId(),
348
+ channelId,
349
+ mode: options.mode ?? "chat",
350
+ purpose,
351
+ continuationKey: options.continuationToken ?? newContinuationKey(),
352
+ auth,
353
+ title: options.title,
354
+ channelState,
355
+ workspaceOverride: options.workspaceDir,
356
+ pendingWorkspaceFiles: options.workspaceFiles,
357
+ cloudOverride: options.cloud,
358
+ sdkAgentId: options.sdkAgentId,
359
+ turnCount: 0,
360
+ createdAt: now,
361
+ updatedAt: now,
362
+ };
363
+ await this.sessions.save(record);
364
+ await this.appendEvent(record.sessionId, {
365
+ type: "session.started",
366
+ data: { channelId },
367
+ });
368
+ record = await this.enrollABs(record);
369
+ await this.startTurn(record.sessionId, message);
370
+ return this.channelSession(record.sessionId, record, true);
371
+ }
372
+
373
+ /**
374
+ * Sticky A/B enrollment at session creation — before the first turn, so
375
+ * variants are in the durable stream (and on {@link SessionRecord.abs})
376
+ * ahead of model/instructions work. Failures persist as `variant: null`
377
+ * (sticky skip). Eval harness sessions (`purpose: "eval"`) are never
378
+ * enrolled.
379
+ */
380
+ private async enrollABs(record: SessionRecord): Promise<SessionRecord> {
381
+ if (!this.abCollector.enabled || record.purpose === "eval") {
382
+ return record;
383
+ }
384
+ const sessionInfo = this.sessionInfo(record);
385
+ const abs: Record<string, string | null> = {};
386
+ for (const experiment of this.project.abs) {
387
+ const def = experiment.definition as ABDefinition;
388
+ const variantKeys = Object.keys(def.variants);
389
+ let variant: string | null = null;
390
+ try {
391
+ variant = await resolveABSplit(def.split, {
392
+ agent: { name: this.project.name },
393
+ channel: { id: record.channelId },
394
+ session: sessionInfo,
395
+ experiment: experiment.name,
396
+ variants: variantKeys,
397
+ });
398
+ if (variant !== null && !(variant in def.variants)) {
399
+ throw new Error(
400
+ `split resolved to ${JSON.stringify(variant)}; expected one of ${variantKeys.join(", ")} or null to skip`
401
+ );
402
+ }
403
+ } catch (error) {
404
+ this.logger(
405
+ `[agent-serve] ab "${experiment.name}" enrollment failed (sticky skip): ${describeError(error)}`
406
+ );
407
+ variant = null;
408
+ }
409
+ abs[experiment.name] = variant;
410
+ await this.appendEvent(record.sessionId, {
411
+ type: "ab.assigned",
412
+ data: { experiment: experiment.name, variant },
413
+ });
414
+ }
415
+ return this.updateRecord(record.sessionId, (current) => ({
416
+ ...current,
417
+ abs,
418
+ }));
419
+ }
420
+
421
+ /**
422
+ * Ensure {@link SessionRecord.abs} is populated (hydrate from the durable
423
+ * `ab.assigned` stream when missing — e.g. older sessions after upgrade).
424
+ */
425
+ private async ensureAbs(record: SessionRecord): Promise<SessionRecord> {
426
+ if (record.abs !== undefined || record.purpose === "eval") {
427
+ return record;
428
+ }
429
+ if (!this.abCollector.enabled) {
430
+ return record;
431
+ }
432
+ const events = await this.listEvents(record.sessionId, 0);
433
+ const folded = foldABStream(events);
434
+ if (folded.arms.size === 0) {
435
+ return record;
436
+ }
437
+ const abs = Object.fromEntries(folded.arms);
438
+ return this.updateRecord(record.sessionId, (current) => ({
439
+ ...current,
440
+ abs,
441
+ }));
442
+ }
443
+
444
+ /**
445
+ * Agent config for this session: base project agent plus any per-variant
446
+ * `instructions` overlays from enrolled A/B arms.
447
+ */
448
+ private agentForSession(record: SessionRecord): ResolvedAgent {
449
+ const base = this.project.agent;
450
+ const instructions = mergeABInstructions(
451
+ base.instructions,
452
+ record.abs,
453
+ this.project.abs.map((experiment) => ({
454
+ name: experiment.name,
455
+ definition: experiment.definition as ABDefinition,
456
+ }))
457
+ );
458
+ if (instructions === base.instructions) {
459
+ return base;
460
+ }
461
+ return { ...base, instructions };
462
+ }
463
+
464
+ /** Look up a session as a channel-scoped handle. */
465
+ async getChannelSession(
466
+ channelId: string,
467
+ sessionId: string
468
+ ): Promise<ChannelSession | null> {
469
+ const record = await this.sessions.get(sessionId);
470
+ if (record === undefined || record.channelId !== channelId) {
471
+ return null;
472
+ }
473
+ return this.channelSession(sessionId, record, false);
474
+ }
475
+
476
+ /** Cross-channel hand-off (schedules and channel route handlers). */
477
+ async receive(
478
+ channel: ChannelDefinition | HttpChannelDefinition | string,
479
+ input: ReceiveInput
480
+ ): Promise<ChannelSession> {
481
+ const channelId = this.resolveChannelId(channel);
482
+ const definition = this.channelsById.get(channelId);
483
+
484
+ if (definition?.receive !== undefined) {
485
+ let lastCreated: ChannelSession | undefined;
486
+ const send: SendMessageFn = async (message, options = {}) => {
487
+ const session = await this.send(channelId, message, options);
488
+ lastCreated = session;
489
+ return session;
490
+ };
491
+ const returned = await definition.receive(input, { send });
492
+ const session = returned ?? lastCreated;
493
+ if (session === undefined) {
494
+ throw new Error(
495
+ `Channel "${channelId}" receive hook did not start a session`
496
+ );
497
+ }
498
+ return session;
499
+ }
500
+
501
+ return this.send(channelId, input.message, {
502
+ auth: input.auth,
503
+ });
504
+ }
505
+
506
+ /** Fire-and-forget task session (schedules' markdown form). */
507
+ async runTask(args: {
508
+ channelId: string;
509
+ prompt: string;
510
+ auth: AuthContext | null;
511
+ title?: string;
512
+ }): Promise<{ sessionId: string; done: Promise<TurnOutcome> }> {
513
+ const session = await this.send(args.channelId, args.prompt, {
514
+ auth: args.auth,
515
+ mode: "task",
516
+ ...(args.title === undefined ? {} : { title: args.title }),
517
+ });
518
+ return { sessionId: session.id, done: this.waitForCompletion(session.id) };
519
+ }
520
+
521
+ // ==========================================================================
522
+ // Turn execution
523
+ // ==========================================================================
524
+
525
+ /**
526
+ * Admit a turn for `sessionId`. A follow-up that arrives while a turn is in
527
+ * flight preempts it: the running turn is cancelled and its SDK run marked
528
+ * terminal before the new turn starts. Admissions are serialized per session
529
+ * so a burst of follow-ups can't race two runs onto the same SDK agent — the
530
+ * last message wins. Resolves once the new turn has started (not finished).
531
+ */
532
+ private startTurn(sessionId: string, message: string): Promise<void> {
533
+ const prior = this.admissionChains.get(sessionId) ?? Promise.resolve();
534
+ const admitted = prior
535
+ .catch(() => {})
536
+ .then(() => this.admitTurn(sessionId, message));
537
+ this.admissionChains.set(
538
+ sessionId,
539
+ admitted.catch(() => {})
540
+ );
541
+ return admitted;
542
+ }
543
+
544
+ private async admitTurn(sessionId: string, message: string): Promise<void> {
545
+ // Interrupt any turn in flight and wait for it to settle so the SDK run is
546
+ // terminal before this turn's send (the follow-up also force-expires as a
547
+ // belt-and-suspenders — see buildTurnRequest).
548
+ await this.interruptActiveTurn(sessionId);
549
+ this.pendingTurns.add(sessionId);
550
+
551
+ try {
552
+ let record = await this.updateRecord(sessionId, (current) => ({
553
+ ...current,
554
+ turnCount: current.turnCount + 1,
555
+ }));
556
+ // Arms must be on the record before tools/instructions see SessionInfo.
557
+ record = await this.ensureAbs(record);
558
+ const turnId = `turn_${record.turnCount}`;
559
+ const isFirstTurn = record.turnCount === 1;
560
+
561
+ await this.appendEvent(sessionId, {
562
+ type: "message.received",
563
+ turnId,
564
+ data: { text: message },
565
+ });
566
+ await this.appendEvent(sessionId, {
567
+ type: "turn.started",
568
+ turnId,
569
+ data: {},
570
+ });
571
+
572
+ const abort = new AbortController();
573
+ this.turnAborts.set(sessionId, abort);
574
+ const turnPromise = this.executeTurn(
575
+ record,
576
+ turnId,
577
+ message,
578
+ isFirstTurn,
579
+ abort.signal
580
+ );
581
+ this.activeTurns.set(sessionId, turnPromise);
582
+ void turnPromise
583
+ .then((outcome) => {
584
+ this.lastOutcomes.set(sessionId, outcome);
585
+ })
586
+ .catch(() => {})
587
+ .finally(() => {
588
+ // Guard against clobbering a newer turn that already replaced us.
589
+ if (this.activeTurns.get(sessionId) === turnPromise) {
590
+ this.activeTurns.delete(sessionId);
591
+ }
592
+ if (this.turnAborts.get(sessionId) === abort) {
593
+ this.turnAborts.delete(sessionId);
594
+ }
595
+ });
596
+ } finally {
597
+ this.pendingTurns.delete(sessionId);
598
+ }
599
+ }
600
+
601
+ /**
602
+ * Cancel the turn in flight for `sessionId` (if any) and wait for it to
603
+ * settle, so its SDK run is terminal before the next turn's send. Safe to
604
+ * call when nothing is running.
605
+ */
606
+ private async interruptActiveTurn(sessionId: string): Promise<void> {
607
+ this.turnAborts.get(sessionId)?.abort();
608
+ const active = this.activeTurns.get(sessionId);
609
+ if (active !== undefined) {
610
+ await active.catch(() => {});
611
+ }
612
+ }
613
+
614
+ /**
615
+ * Abort the in-flight turn without starting a follow-up. Returns whether
616
+ * a turn was active (or pending abort) when called.
617
+ */
618
+ async interrupt(sessionId: string): Promise<boolean> {
619
+ const hadWork =
620
+ this.turnAborts.has(sessionId) || this.activeTurns.has(sessionId);
621
+ await this.interruptActiveTurn(sessionId);
622
+ return hadWork;
623
+ }
624
+
625
+ /**
626
+ * Local harness cwd for a session: per-send override, else
627
+ * `<agent.local.cwd>/<sessionId>`, else the durable session workspace.
628
+ */
629
+ harnessCwd(record: SessionRecord): string {
630
+ if (record.workspaceOverride !== undefined) {
631
+ return record.workspaceOverride;
632
+ }
633
+ const agentCwd = this.project.agent.local?.cwd;
634
+ if (agentCwd !== undefined) {
635
+ return join(agentCwd, record.sessionId);
636
+ }
637
+ return this.sessions.workspaceDir(record.sessionId);
638
+ }
639
+
640
+ /**
641
+ * Directory that receives AGENTS.md / skills / agent tools. When the
642
+ * harness cwd is a one-off override (e.g. PR worktree), scaffold stays in
643
+ * the durable session workspace; otherwise scaffold lands in the harness.
644
+ */
645
+ private scaffoldDir(record: SessionRecord): string {
646
+ if (record.workspaceOverride !== undefined) {
647
+ return this.sessions.workspaceDir(record.sessionId);
648
+ }
649
+ return this.harnessCwd(record);
650
+ }
651
+
652
+ private async executeTurn(
653
+ record: SessionRecord,
654
+ turnId: string,
655
+ message: string,
656
+ isFirstTurn: boolean,
657
+ signal: AbortSignal
658
+ ): Promise<TurnOutcome> {
659
+ const sessionId = record.sessionId;
660
+ const workspaceDir = this.scaffoldDir(record);
661
+ const harnessCwd = this.harnessCwd(record);
662
+ // Hold the open log so tool events append synchronously. Fire-and-forget
663
+ // `appendEvent` races parallel tool completions and delays the live stream
664
+ // until the next await tick — often after *all* parallel tools finish.
665
+ const log = await this.logs.get(sessionId);
666
+ const emit = (payload: SessionEventPayload): void => {
667
+ try {
668
+ const event = log.append({ ...payload, turnId });
669
+ this.queueDispatch(sessionId, event);
670
+ } catch (error) {
671
+ this.logger(
672
+ `[agent-serve] failed to record event for ${sessionId}: ${describeError(error)}`
673
+ );
674
+ }
675
+ };
676
+ const mapper = new TurnEventMapper(emit);
677
+
678
+ let outcome: TurnOutcome;
679
+ try {
680
+ // Scaffold agent files into the session workspace even when the harness
681
+ // cwd is overridden (e.g. a PR worktree). A/B variant instruction
682
+ // overlays are applied via agentForSession.
683
+ const agent = this.agentForSession(record);
684
+ await materializeWorkspace({
685
+ agent,
686
+ workspaceDir,
687
+ seed: isFirstTurn,
688
+ });
689
+ const pendingFiles = record.pendingWorkspaceFiles;
690
+ if (pendingFiles !== undefined && Object.keys(pendingFiles).length > 0) {
691
+ await writeWorkspaceFiles(harnessCwd, pendingFiles);
692
+ await this.updateRecord(sessionId, (current) => {
693
+ const { pendingWorkspaceFiles: _cleared, ...rest } = current;
694
+ return rest;
695
+ });
696
+ }
697
+ const request = this.buildTurnRequest(
698
+ record,
699
+ agent,
700
+ turnId,
701
+ message,
702
+ isFirstTurn,
703
+ emit,
704
+ async (update) => {
705
+ mapper.handleUpdate(update);
706
+ // Yield so the HTTP NDJSON writer can flush tool start/result
707
+ // events while sibling parallel tools are still executing.
708
+ if (
709
+ update.type === "tool-call-started" ||
710
+ update.type === "tool-call-completed"
711
+ ) {
712
+ await yieldEventLoop();
713
+ }
714
+ },
715
+ signal
716
+ );
717
+ outcome = await this.runner.runTurn(request);
718
+ } catch (error) {
719
+ this.approvals.cancelSession(sessionId, "deny");
720
+ outcome = { status: "error", errorMessage: describeError(error) };
721
+ } finally {
722
+ this.approvals.cancelSession(sessionId, "deny");
723
+ await this.clearPendingApprovals(sessionId);
724
+ }
725
+
726
+ mapper.finish();
727
+
728
+ if (mapper.title !== undefined) {
729
+ const title = mapper.title;
730
+ await this.updateRecord(sessionId, (current) => ({ ...current, title }));
731
+ }
732
+
733
+ const finalResult = outcome.result ?? mapper.finalText;
734
+ const finalUsage = outcome.usage ?? mapper.usage;
735
+
736
+ if (outcome.status === "finished") {
737
+ await this.appendEvent(sessionId, {
738
+ type: "turn.completed",
739
+ turnId,
740
+ data: {
741
+ ...(finalResult === undefined ? {} : { result: finalResult }),
742
+ ...(finalUsage === undefined ? {} : { usage: finalUsage }),
743
+ },
744
+ });
745
+ } else {
746
+ const interrupted = signal.aborted && outcome.status === "cancelled";
747
+ await this.appendEvent(sessionId, {
748
+ type: "turn.failed",
749
+ turnId,
750
+ data: {
751
+ message: interrupted
752
+ ? "turn interrupted"
753
+ : (outcome.errorMessage ??
754
+ (outcome.status === "cancelled"
755
+ ? "turn cancelled"
756
+ : "turn failed")),
757
+ },
758
+ });
759
+ }
760
+
761
+ const current = await this.sessions.get(sessionId);
762
+ const mode = current?.mode ?? record.mode;
763
+ if (mode === "task") {
764
+ await this.appendEvent(
765
+ sessionId,
766
+ outcome.status === "finished"
767
+ ? { type: "session.completed", turnId, data: {} }
768
+ : {
769
+ type: "session.failed",
770
+ turnId,
771
+ data: { message: outcome.errorMessage ?? "task failed" },
772
+ }
773
+ );
774
+ } else {
775
+ await this.appendEvent(sessionId, {
776
+ type: "session.waiting",
777
+ turnId,
778
+ data: {},
779
+ });
780
+ }
781
+
782
+ return {
783
+ ...outcome,
784
+ ...(finalResult === undefined ? {} : { result: finalResult }),
785
+ ...(finalUsage === undefined ? {} : { usage: finalUsage }),
786
+ };
787
+ }
788
+
789
+ private buildTurnRequest(
790
+ record: SessionRecord,
791
+ agent: ResolvedAgent,
792
+ turnId: string,
793
+ prompt: string,
794
+ isFirstTurn: boolean,
795
+ emit: (payload: SessionEventPayload) => void,
796
+ onUpdate: RunnerTurnRequest["onUpdate"],
797
+ signal: AbortSignal
798
+ ): RunnerTurnRequest {
799
+ // Per-session cloud attach (channel/tool handoff) forces cloud runtime
800
+ // even when the agent defaults to local for chat server tools.
801
+ const runtime = resolveSessionRuntime({
802
+ agentRuntime: agent.runtime,
803
+ cloudOverride: record.cloudOverride,
804
+ });
805
+ const mergedCloud =
806
+ runtime === "cloud"
807
+ ? mergeCloudOptions({
808
+ base: agent.cloud,
809
+ override: record.cloudOverride,
810
+ })
811
+ : undefined;
812
+ let turnPrompt = prompt;
813
+ if (runtime === "cloud" && isFirstTurn) {
814
+ const preamble = [
815
+ agent.instructions?.trim() ?? "",
816
+ buildAgentToolsCatalog(agent.tools, { includeScripts: true }),
817
+ ]
818
+ .filter((part) => part !== "")
819
+ .join("\n\n");
820
+ turnPrompt = preamble === "" ? prompt : `${preamble}\n\n${prompt}`;
821
+ }
822
+ return {
823
+ sessionId: record.sessionId,
824
+ ...(record.sdkAgentId === undefined
825
+ ? {}
826
+ : { sdkAgentId: record.sdkAgentId }),
827
+ workspaceDir: this.harnessCwd(record),
828
+ runnerStateDir: join(this.stateRoot, "runner"),
829
+ prompt: turnPrompt,
830
+ isFirstTurn,
831
+ ...(agent.model === undefined ? {} : { model: agent.model }),
832
+ runtime,
833
+ ...(mergedCloud === undefined ? {} : { cloud: mergedCloud }),
834
+ customTools:
835
+ runtime === "local" ? this.buildCustomTools(record, turnId, emit) : {},
836
+ mcpServers: this.buildMcpServers(runtime),
837
+ subagents: agent.subagents.map((subagent) => ({
838
+ name: subagent.name,
839
+ description: subagent.description ?? subagent.name,
840
+ prompt: subagentPrompt(subagent),
841
+ ...(subagent.model === undefined ? {} : { model: subagent.model }),
842
+ })),
843
+ onUpdate,
844
+ signal,
845
+ // Local follow-ups expire any lingering active run so a preempted turn
846
+ // (or one orphaned by a crashed process) never blocks this send.
847
+ forceExpireActiveRun: runtime === "local" && !isFirstTurn,
848
+ onSdkAgentId: async (sdkAgentId) => {
849
+ if (record.sdkAgentId === sdkAgentId) {
850
+ return;
851
+ }
852
+ await this.updateRecord(record.sessionId, (current) => ({
853
+ ...current,
854
+ sdkAgentId,
855
+ }));
856
+ record.sdkAgentId = sdkAgentId;
857
+ await this.appendEvent(record.sessionId, {
858
+ type: "agent.bound",
859
+ turnId,
860
+ data: {
861
+ sdkAgentId,
862
+ ...(sdkAgentId.startsWith("bc-")
863
+ ? { url: `https://cursor.com/agents/${sdkAgentId}` }
864
+ : {}),
865
+ },
866
+ });
867
+ },
868
+ };
869
+ }
870
+
871
+ /**
872
+ * MCP servers for a turn: authored concrete connections plus resolved
873
+ * serve-time resolved connections (peers, Cursor account bridge).
874
+ * Resolved connections pick their URL by runtime — loopback for local
875
+ * turns, `cloudUrl` for cloud turns — and are omitted from cloud turns
876
+ * when no cloud URL exists unless a `servers` filter requires the bridge
877
+ * (see {@link assertCloudCanHonorAccountServersFilters}).
878
+ */
879
+ private buildMcpServers(
880
+ runtime: "local" | "cloud"
881
+ ): Record<string, ConnectionTransport> {
882
+ assertCloudCanHonorAccountServersFilters({
883
+ runtime,
884
+ resolvedConnections: this.resolvedConnections,
885
+ });
886
+ const servers: Record<string, ConnectionTransport> = {};
887
+ for (const connection of this.project.agent.connections) {
888
+ if (isSymbolicConnectionTransport(connection.transport)) {
889
+ continue;
890
+ }
891
+ servers[connection.name] = connection.transport;
892
+ }
893
+ for (const resolved of this.resolvedConnections) {
894
+ const url = runtime === "cloud" ? resolved.cloudUrl : resolved.localUrl;
895
+ if (url === undefined) {
896
+ continue;
897
+ }
898
+ servers[resolved.name] = {
899
+ url,
900
+ ...(resolved.headers === undefined
901
+ ? {}
902
+ : { headers: resolved.headers }),
903
+ };
904
+ }
905
+ return servers;
906
+ }
907
+
908
+ private buildCustomTools(
909
+ record: SessionRecord,
910
+ turnId: string,
911
+ emit: (payload: SessionEventPayload) => void
912
+ ): Record<string, SDKCustomTool> {
913
+ const tools: Record<string, SDKCustomTool> = {};
914
+ for (const tool of this.project.agent.tools) {
915
+ if (tool.execution !== "server") {
916
+ continue;
917
+ }
918
+ const execute = tool.definition.execute;
919
+ if (typeof execute !== "function") {
920
+ continue;
921
+ }
922
+ tools[tool.name] = {
923
+ description: tool.description,
924
+ inputSchema: tool.inputSchema as SDKCustomTool["inputSchema"],
925
+ execute: async (args, context) => {
926
+ const validation = validateToolInput(
927
+ tool.definition.inputSchema,
928
+ args
929
+ );
930
+ if (!validation.ok) {
931
+ return {
932
+ content: [
933
+ {
934
+ type: "text",
935
+ text: `Invalid input for tool "${tool.name}": ${validation.message}`,
936
+ },
937
+ ],
938
+ isError: true,
939
+ };
940
+ }
941
+ const toolCallId = context.toolCallId ?? newToolCallId(tool.name);
942
+ const toolCtx: ToolContext = {
943
+ toolCallId,
944
+ session: this.sessionInfo(record),
945
+ workspaceDir: this.harnessCwd(record),
946
+ host: this.host,
947
+ send: (channelId, message, sendOptions = {}) =>
948
+ this.send(channelId, message, {
949
+ ...sendOptions,
950
+ auth:
951
+ sendOptions.auth !== undefined
952
+ ? sendOptions.auth
953
+ : record.auth,
954
+ }),
955
+ getSession: (channelId, sessionId) =>
956
+ this.getChannelSession(channelId, sessionId),
957
+ };
958
+ try {
959
+ const needsGate = await evaluateNeedsApproval(
960
+ tool.definition.needsApproval,
961
+ validation.value
962
+ );
963
+ if (needsGate) {
964
+ const decision = await this.parkForApproval({
965
+ sessionId: record.sessionId,
966
+ turnId,
967
+ callId: toolCallId,
968
+ toolName: tool.name,
969
+ args: validation.value,
970
+ emit,
971
+ });
972
+ if (decision === "deny") {
973
+ return {
974
+ content: [
975
+ {
976
+ type: "text",
977
+ text: `Tool "${tool.name}" was denied by a human approver.`,
978
+ },
979
+ ],
980
+ isError: true,
981
+ };
982
+ }
983
+ }
984
+ const started = Date.now();
985
+ this.logger(`[session] tool ${tool.name} start`);
986
+ const result = await execute(validation.value as never, toolCtx);
987
+ this.logger(
988
+ `[session] tool ${tool.name} done (${Date.now() - started}ms)`
989
+ );
990
+ return normalizeToolResult(result);
991
+ } catch (error) {
992
+ this.logger(
993
+ `[session] tool ${tool.name} failed: ${describeError(error)}`
994
+ );
995
+ return {
996
+ content: [
997
+ {
998
+ type: "text",
999
+ text: `Tool "${tool.name}" failed: ${describeError(error)}`,
1000
+ },
1001
+ ],
1002
+ isError: true,
1003
+ };
1004
+ }
1005
+ },
1006
+ };
1007
+ }
1008
+ return tools;
1009
+ }
1010
+
1011
+ /**
1012
+ * Park a server-tool call until a human approves or denies. Emits
1013
+ * `action.approval_requested` / `action.approval_resolved` on the
1014
+ * held turn log so the playground and stream see the gate.
1015
+ */
1016
+ private async parkForApproval(args: {
1017
+ sessionId: string;
1018
+ turnId: string;
1019
+ callId: string;
1020
+ toolName: string;
1021
+ args: unknown;
1022
+ emit: (payload: SessionEventPayload) => void;
1023
+ }): Promise<ApprovalDecision> {
1024
+ const requestedAt = new Date().toISOString();
1025
+ const pendingRecord: PendingApproval = {
1026
+ callId: args.callId,
1027
+ turnId: args.turnId,
1028
+ toolName: args.toolName,
1029
+ args: args.args,
1030
+ requestedAt,
1031
+ };
1032
+ const resolutionPromise = this.approvals.wait({
1033
+ sessionId: args.sessionId,
1034
+ ...pendingRecord,
1035
+ });
1036
+ await this.updateRecord(args.sessionId, (current) => ({
1037
+ ...current,
1038
+ pendingApprovals: [
1039
+ ...(current.pendingApprovals ?? []).filter(
1040
+ (item) => item.callId !== args.callId
1041
+ ),
1042
+ pendingRecord,
1043
+ ],
1044
+ }));
1045
+ args.emit({
1046
+ type: "action.approval_requested",
1047
+ data: {
1048
+ callId: args.callId,
1049
+ toolName: args.toolName,
1050
+ args: args.args,
1051
+ },
1052
+ });
1053
+ this.logger(
1054
+ `[session] tool ${args.toolName} awaiting approval (${args.callId})`
1055
+ );
1056
+ const { decision, by } = await resolutionPromise;
1057
+ await this.updateRecord(args.sessionId, (current) => ({
1058
+ ...current,
1059
+ pendingApprovals: (current.pendingApprovals ?? []).filter(
1060
+ (item) => item.callId !== args.callId
1061
+ ),
1062
+ }));
1063
+ args.emit({
1064
+ type: "action.approval_resolved",
1065
+ data: {
1066
+ callId: args.callId,
1067
+ toolName: args.toolName,
1068
+ decision,
1069
+ ...(by === undefined ? {} : { by }),
1070
+ },
1071
+ });
1072
+ this.logger(
1073
+ `[session] tool ${args.toolName} approval ${decision} (${args.callId})`
1074
+ );
1075
+ return decision;
1076
+ }
1077
+
1078
+ private async clearPendingApprovals(sessionId: string): Promise<void> {
1079
+ const current = await this.sessions.get(sessionId);
1080
+ if (
1081
+ current === undefined ||
1082
+ current.pendingApprovals === undefined ||
1083
+ current.pendingApprovals.length === 0
1084
+ ) {
1085
+ return;
1086
+ }
1087
+ await this.updateRecord(sessionId, (record) => ({
1088
+ ...record,
1089
+ pendingApprovals: [],
1090
+ }));
1091
+ }
1092
+
1093
+ /** Pending tool approvals for a session (live waiters ∪ persisted). */
1094
+ async listPendingApprovals(sessionId: string): Promise<PendingApproval[]> {
1095
+ const record = await this.sessions.get(sessionId);
1096
+ if (record === undefined) {
1097
+ return [];
1098
+ }
1099
+ const live = this.approvals.list(sessionId);
1100
+ if (live.length > 0) {
1101
+ return live;
1102
+ }
1103
+ return record.pendingApprovals ?? [];
1104
+ }
1105
+
1106
+ /**
1107
+ * Approve or deny a parked server-tool call. Unblocks the in-flight
1108
+ * SDK custom-tool callback for the same `callId`.
1109
+ *
1110
+ * Pass `asSessionOwner: true` only from an explicit host bridge (e.g. `--dev`
1111
+ * playground resolving a Slack-owned parked tool). Ownership is checked
1112
+ * against the stored session principal; `by` still records the clicker.
1113
+ */
1114
+ async resolveApproval(
1115
+ sessionId: string,
1116
+ callId: string,
1117
+ decision: ApprovalDecision,
1118
+ auth: AuthContext | null,
1119
+ options?: { asSessionOwner?: boolean }
1120
+ ): Promise<PendingApproval> {
1121
+ const record = await this.sessions.get(sessionId);
1122
+ if (record === undefined) {
1123
+ throw new ApprovalNotFoundError(sessionId, callId);
1124
+ }
1125
+ const ownershipAuth = options?.asSessionOwner === true ? record.auth : auth;
1126
+ if (!samePrincipal(record.auth, ownershipAuth)) {
1127
+ throw new SessionAuthError(sessionId);
1128
+ }
1129
+ const pending =
1130
+ (record.pendingApprovals ?? []).find((item) => item.callId === callId) ??
1131
+ this.approvals.list(sessionId).find((item) => item.callId === callId);
1132
+ if (pending === undefined) {
1133
+ throw new ApprovalNotFoundError(sessionId, callId);
1134
+ }
1135
+ const resolvedBy = approvalResolverLabel(auth);
1136
+ if (!this.approvals.resolve(sessionId, callId, decision, resolvedBy)) {
1137
+ await this.removePendingApproval(sessionId, callId);
1138
+ throw new ApprovalInterruptedError(sessionId, callId);
1139
+ }
1140
+ return pending;
1141
+ }
1142
+
1143
+ private async removePendingApproval(
1144
+ sessionId: string,
1145
+ callId: string
1146
+ ): Promise<void> {
1147
+ await this.updateRecord(sessionId, (current) => ({
1148
+ ...current,
1149
+ pendingApprovals: (current.pendingApprovals ?? []).filter(
1150
+ (item) => item.callId !== callId
1151
+ ),
1152
+ }));
1153
+ }
1154
+
1155
+ // ==========================================================================
1156
+ // Direct tool calls (deterministic invocation, no model turn)
1157
+ // ==========================================================================
1158
+
1159
+ /**
1160
+ * Call an authored server tool deterministically: validate `input`
1161
+ * against the tool's schema (zod schemas validate and transform; plain
1162
+ * JSON Schema inputs pass through unvalidated, matching the model path),
1163
+ * run `execute` in-process, and return the normalized result exactly as
1164
+ * the model would receive it.
1165
+ *
1166
+ * With `options.sessionId` the tool runs inside that session (its info
1167
+ * and materialized workspace) and the call is recorded on the session's
1168
+ * event stream as `actions.requested` / `action.result` under a
1169
+ * per-call `turnId` (the call id), so it shows up in trajectories.
1170
+ * Rejected with {@link SessionBusyError} while a model turn is running.
1171
+ * Without a session, the tool runs against an **ephemeral** scratch
1172
+ * workspace under `<stateRoot>/tool-calls/<callId>` (materialized with
1173
+ * seed files, removed after the call returns).
1174
+ *
1175
+ * Throws {@link UnknownToolError}, {@link ToolNotDirectlyCallableError},
1176
+ * {@link ToolInputError}, or {@link UnknownSessionError} before the tool
1177
+ * body runs; a tool body that throws is reported as an `isError` outcome
1178
+ * with the same `{ content, isError }` envelope the model would see, not
1179
+ * an exception.
1180
+ */
1181
+ async callTool(
1182
+ toolName: string,
1183
+ input: unknown = {},
1184
+ options: CallToolOptions = {}
1185
+ ): Promise<ToolCallOutcome> {
1186
+ const tool = this.project.agent.tools.find((t) => t.name === toolName);
1187
+ if (tool === undefined) {
1188
+ throw new UnknownToolError(
1189
+ toolName,
1190
+ this.project.agent.tools
1191
+ .filter((t) => t.execution === "server")
1192
+ .map((t) => t.name)
1193
+ );
1194
+ }
1195
+ const execute = tool.definition.execute;
1196
+ if (tool.execution !== "server" || typeof execute !== "function") {
1197
+ throw new ToolNotDirectlyCallableError(toolName);
1198
+ }
1199
+ const validation = validateToolInput(tool.definition.inputSchema, input);
1200
+ if (!validation.ok) {
1201
+ throw new ToolInputError({ toolName, detail: validation.message });
1202
+ }
1203
+
1204
+ const callId = newToolCallId(toolName);
1205
+ const auth = options.auth ?? null;
1206
+ let record: SessionRecord | undefined;
1207
+ let session: SessionInfo;
1208
+ let workspaceDir: string;
1209
+ let scratchDir: string | undefined;
1210
+ if (options.sessionId !== undefined) {
1211
+ record = await this.sessions.get(options.sessionId);
1212
+ if (record === undefined) {
1213
+ throw new UnknownSessionError(options.sessionId);
1214
+ }
1215
+ // Direct calls respect the same serialization as follow-up sends: a
1216
+ // running model turn owns the session workspace and event stream.
1217
+ if (
1218
+ this.activeTurns.has(record.sessionId) ||
1219
+ this.pendingTurns.has(record.sessionId)
1220
+ ) {
1221
+ throw new SessionBusyError(record.sessionId);
1222
+ }
1223
+ session = this.sessionInfo(record);
1224
+ workspaceDir = this.harnessCwd(record);
1225
+ } else {
1226
+ session = {
1227
+ id: callId,
1228
+ channelId: DIRECT_TOOL_CALL_CHANNEL_ID,
1229
+ mode: "task",
1230
+ auth,
1231
+ };
1232
+ scratchDir = join(this.stateRoot, "tool-calls", callId);
1233
+ workspaceDir = scratchDir;
1234
+ }
1235
+ await mkdir(workspaceDir, { recursive: true });
1236
+ // Match what a model turn would see: session workspaces get the same
1237
+ // scaffold as executeTurn (seeds only before the first turn); scratch
1238
+ // workspaces are materialized fresh.
1239
+ await materializeWorkspace({
1240
+ agent: this.project.agent,
1241
+ workspaceDir:
1242
+ record === undefined ? workspaceDir : this.scaffoldDir(record),
1243
+ seed: record === undefined || record.turnCount === 0,
1244
+ });
1245
+
1246
+ // A per-call turnId keeps these events visible in trajectories (which
1247
+ // drop turn-less action events) without attributing them to a model turn.
1248
+ if (record !== undefined) {
1249
+ await this.appendEvent(record.sessionId, {
1250
+ type: "actions.requested",
1251
+ turnId: callId,
1252
+ data: { calls: [{ callId, toolName, args: validation.value }] },
1253
+ });
1254
+ }
1255
+
1256
+ const started = Date.now();
1257
+ this.logger(`[session] tool ${toolName} start (direct ${callId})`);
1258
+ let result: unknown;
1259
+ let isError: boolean;
1260
+ try {
1261
+ result = normalizeToolResult(
1262
+ await execute(validation.value as never, {
1263
+ toolCallId: callId,
1264
+ session,
1265
+ workspaceDir,
1266
+ host: this.host,
1267
+ send: (channelId, message, sendOptions = {}) =>
1268
+ this.send(channelId, message, {
1269
+ ...sendOptions,
1270
+ auth:
1271
+ sendOptions.auth !== undefined
1272
+ ? sendOptions.auth
1273
+ : session.auth,
1274
+ }),
1275
+ getSession: (channelId, sessionId) =>
1276
+ this.getChannelSession(channelId, sessionId),
1277
+ })
1278
+ );
1279
+ isError = isErrorEnvelope(result);
1280
+ } catch (error) {
1281
+ this.logger(
1282
+ `[session] tool ${toolName} failed (direct ${callId}): ${describeError(error)}`
1283
+ );
1284
+ // Same envelope the model path returns for a throwing tool body.
1285
+ result = {
1286
+ content: [
1287
+ {
1288
+ type: "text",
1289
+ text: `Tool "${toolName}" failed: ${describeError(error)}`,
1290
+ },
1291
+ ],
1292
+ isError: true,
1293
+ };
1294
+ isError = true;
1295
+ } finally {
1296
+ if (scratchDir !== undefined) {
1297
+ await rm(scratchDir, { recursive: true, force: true }).catch(() => {});
1298
+ }
1299
+ }
1300
+ const durationMs = Date.now() - started;
1301
+ this.logger(
1302
+ `[session] tool ${toolName} done (direct ${callId}, ${durationMs}ms)`
1303
+ );
1304
+
1305
+ if (record !== undefined) {
1306
+ await this.appendEvent(record.sessionId, {
1307
+ type: "action.result",
1308
+ turnId: callId,
1309
+ data: { callId, toolName, output: result, isError },
1310
+ });
1311
+ }
1312
+
1313
+ const errorMessage = isError ? toolCallErrorMessage(result) : undefined;
1314
+ return {
1315
+ toolName,
1316
+ callId,
1317
+ isError,
1318
+ result,
1319
+ durationMs,
1320
+ ...(errorMessage === undefined ? {} : { errorMessage }),
1321
+ ...(record === undefined ? {} : { sessionId: record.sessionId }),
1322
+ };
1323
+ }
1324
+
1325
+ // ==========================================================================
1326
+ // Events: append, dispatch to channel handlers + hooks
1327
+ // ==========================================================================
1328
+
1329
+ private async appendEvent(
1330
+ sessionId: string,
1331
+ payload: SessionEventPayload & { turnId?: string }
1332
+ ): Promise<SessionEvent> {
1333
+ const log = await this.logs.get(sessionId);
1334
+ const event = log.append(payload);
1335
+ this.queueDispatch(sessionId, event);
1336
+ return event;
1337
+ }
1338
+
1339
+ private queueDispatch(sessionId: string, event: SessionEvent): void {
1340
+ const chain = this.dispatchChains.get(sessionId) ?? Promise.resolve();
1341
+ const next = chain
1342
+ .then(() => this.dispatchEvent(sessionId, event))
1343
+ .catch((error) => {
1344
+ this.logger(
1345
+ `[agent-serve] event dispatch failed for ${sessionId}: ${describeError(error)}`
1346
+ );
1347
+ });
1348
+ this.dispatchChains.set(sessionId, next);
1349
+ }
1350
+
1351
+ private async dispatchEvent(
1352
+ sessionId: string,
1353
+ event: SessionEvent
1354
+ ): Promise<void> {
1355
+ const record = await this.sessions.get(sessionId);
1356
+ if (record === undefined) {
1357
+ return;
1358
+ }
1359
+
1360
+ const sessionCtx = { session: this.sessionInfo(record) };
1361
+ const channelEvents = this.channelEventHandlersFor(record.channelId);
1362
+ if (channelEvents !== undefined) {
1363
+ // Handlers are declared against the narrowed event for their key; the
1364
+ // dispatch key always matches, so widening here is sound.
1365
+ const handlers: Array<ChannelEventHandler | undefined> = [
1366
+ channelEvents[event.type] as ChannelEventHandler | undefined,
1367
+ channelEvents["*"],
1368
+ ];
1369
+ const channelCtx = {
1370
+ channelId: record.channelId,
1371
+ state: (record.channelState ?? null) as JsonValue,
1372
+ continuationToken: record.continuationKey,
1373
+ setContinuationToken: (token: string | null) =>
1374
+ this.setContinuationToken(sessionId, token),
1375
+ };
1376
+ for (const handler of handlers) {
1377
+ if (handler === undefined) {
1378
+ continue;
1379
+ }
1380
+ try {
1381
+ await handler(event, channelCtx, sessionCtx);
1382
+ } catch (error) {
1383
+ this.logger(
1384
+ `[agent-serve] channel "${record.channelId}" handler for ${event.type} threw: ${describeError(error)}`
1385
+ );
1386
+ }
1387
+ }
1388
+ // Handlers mutate channel state in place; persist after each batch.
1389
+ await this.updateRecord(sessionId, (current) => ({ ...current }));
1390
+ }
1391
+
1392
+ for (const hook of this.project.hooks) {
1393
+ const handlers: Array<HookHandler | undefined> = [
1394
+ hook.definition.events[event.type] as HookHandler | undefined,
1395
+ hook.definition.events["*"],
1396
+ ];
1397
+ for (const handler of handlers) {
1398
+ if (handler === undefined) {
1399
+ continue;
1400
+ }
1401
+ try {
1402
+ await handler(event, {
1403
+ agent: { name: this.project.name },
1404
+ channel: {
1405
+ id: record.channelId,
1406
+ continuationToken: record.continuationKey,
1407
+ },
1408
+ session: this.sessionInfo(record),
1409
+ });
1410
+ } catch (error) {
1411
+ this.logger(
1412
+ `[agent-serve] hook "${hook.name}" handler for ${event.type} threw: ${describeError(error)}`
1413
+ );
1414
+ }
1415
+ }
1416
+ }
1417
+
1418
+ if (this.abCollector.enabled) {
1419
+ await this.abCollector.onEvent(
1420
+ event,
1421
+ this.sessionInfo(record),
1422
+ this.project.name
1423
+ );
1424
+ }
1425
+ }
1426
+
1427
+ private channelEventHandlersFor(
1428
+ channelId: string
1429
+ ): ChannelDefinition["events"] | undefined {
1430
+ if (channelId === HTTP_CHANNEL_ID) {
1431
+ return this.project.httpChannel.events;
1432
+ }
1433
+ return this.channelsById.get(channelId)?.events;
1434
+ }
1435
+
1436
+ // ==========================================================================
1437
+ // Session handles
1438
+ // ==========================================================================
1439
+
1440
+ async waitForCompletion(sessionId: string): Promise<TurnOutcome> {
1441
+ // Drain admission first so pendingTurns → activeTurns is visible before we
1442
+ // decide the session is idle (reminders use this for queueBehindTurn).
1443
+ const chain = this.admissionChains.get(sessionId);
1444
+ if (chain !== undefined) {
1445
+ await chain.catch(() => {});
1446
+ }
1447
+ const active = this.activeTurns.get(sessionId);
1448
+ if (active !== undefined) {
1449
+ return active;
1450
+ }
1451
+ return this.lastOutcomes.get(sessionId) ?? { status: "finished" };
1452
+ }
1453
+
1454
+ /**
1455
+ * Sessions visible to `caller`, newest first (`GET /v1/sessions`).
1456
+ * Callers only see sessions whose stored principal matches — except when
1457
+ * `includeAll` is set (dev mode), where the listing becomes a full
1458
+ * observability surface across every principal (channel-driven runs,
1459
+ * schedule/task sessions, other callers). The route auth chain still
1460
+ * gates who may call this at all.
1461
+ */
1462
+ async listSessions(
1463
+ caller: AuthContext | null,
1464
+ options?: { includeAll?: boolean }
1465
+ ): Promise<SessionSummary[]> {
1466
+ const includeAll = options?.includeAll ?? false;
1467
+ const records = await this.sessions.list();
1468
+ return records
1469
+ .filter((record) => includeAll || samePrincipal(record.auth, caller))
1470
+ .map((record) => {
1471
+ const externalUrl = externalUrlForSession(record);
1472
+ return {
1473
+ sessionId: record.sessionId,
1474
+ channelId: record.channelId,
1475
+ mode: record.mode,
1476
+ ...(record.title === undefined ? {} : { title: record.title }),
1477
+ turnCount: record.turnCount,
1478
+ running:
1479
+ this.activeTurns.has(record.sessionId) ||
1480
+ this.pendingTurns.has(record.sessionId),
1481
+ createdAt: record.createdAt,
1482
+ updatedAt: record.updatedAt,
1483
+ continuationToken:
1484
+ record.continuationKey === null
1485
+ ? null
1486
+ : formatContinuationToken(
1487
+ record.channelId,
1488
+ record.continuationKey
1489
+ ),
1490
+ ...(externalUrl === undefined ? {} : { externalUrl }),
1491
+ };
1492
+ })
1493
+ .reverse();
1494
+ }
1495
+
1496
+ /**
1497
+ * Fold durable session streams into per-session A/B assignments and
1498
+ * aggregate arm metrics (`GET /v1/abs`). Collector-independent: always
1499
+ * reads `events.ndjson`.
1500
+ */
1501
+ async abSnapshot(
1502
+ caller: AuthContext | null,
1503
+ options?: { includeAll?: boolean }
1504
+ ): Promise<ABSnapshot> {
1505
+ const sessions = await this.listSessions(caller, options);
1506
+ const abConfig = this.project.abConfig;
1507
+ const maxPlaygroundSessions = resolveABMaxPlaygroundSessions(
1508
+ abConfig?.maxPlaygroundSessions
1509
+ );
1510
+ const foldedSessions = sessions.slice(0, maxPlaygroundSessions);
1511
+ const snapshot = await buildABSnapshot({
1512
+ experiments: this.project.abs,
1513
+ agentName: this.project.name,
1514
+ sessions: foldedSessions,
1515
+ loadRecord: (sessionId) => this.sessions.get(sessionId),
1516
+ loadEvents: (sessionId) => this.listEvents(sessionId, 0),
1517
+ sessionInfo: (record) => this.sessionInfo(record),
1518
+ logger: this.logger,
1519
+ config: {
1520
+ maxPlaygroundSessions,
1521
+ durableSamples: abConfig?.persistSamples !== undefined,
1522
+ durableSnapshots: abConfig?.persistSnapshots !== undefined,
1523
+ },
1524
+ });
1525
+ // Side effect on the read path: playground polls GET /v1/abs ~every 4s.
1526
+ // Authors who set persistSnapshots accept that write cadence.
1527
+ const persistSnapshots = abConfig?.persistSnapshots;
1528
+ if (persistSnapshots !== undefined) {
1529
+ try {
1530
+ await persistSnapshots.save(snapshot, {
1531
+ projectRoot: this.project.rootDir,
1532
+ });
1533
+ } catch (error) {
1534
+ this.logger(
1535
+ `[agent-serve] ab persistSnapshots.save threw: ${describeError(error)}`
1536
+ );
1537
+ }
1538
+ }
1539
+ return snapshot;
1540
+ }
1541
+
1542
+ async eventStream(
1543
+ sessionId: string,
1544
+ startIndex = 0
1545
+ ): Promise<ReadableStream<Uint8Array>> {
1546
+ const log = await this.logs.get(sessionId);
1547
+ return log.stream(startIndex);
1548
+ }
1549
+
1550
+ /**
1551
+ * Append an assistant message to a session transcript without a model turn
1552
+ * and without a user `message.received` bubble. Used by host-driven channel
1553
+ * tasks (e.g. webhook stages) to publish a final summary.
1554
+ */
1555
+ async emitAssistantMessage(sessionId: string, text: string): Promise<void> {
1556
+ if (this.isTurnRunning(sessionId)) {
1557
+ throw new SessionBusyError(sessionId);
1558
+ }
1559
+ const existing = await this.sessions.get(sessionId);
1560
+ if (existing === undefined) {
1561
+ throw new Error(`Unknown session: ${sessionId}`);
1562
+ }
1563
+ const record = await this.updateRecord(sessionId, (current) => ({
1564
+ ...current,
1565
+ turnCount: current.turnCount + 1,
1566
+ }));
1567
+ const turnId = `turn_${record.turnCount}`;
1568
+ await this.appendEvent(sessionId, {
1569
+ type: "turn.started",
1570
+ turnId,
1571
+ data: {},
1572
+ });
1573
+ await this.appendEvent(sessionId, {
1574
+ type: "message.completed",
1575
+ turnId,
1576
+ data: { text, finishReason: "stop" },
1577
+ });
1578
+ await this.appendEvent(sessionId, {
1579
+ type: "turn.completed",
1580
+ turnId,
1581
+ data: { result: text },
1582
+ });
1583
+ await this.appendEvent(sessionId, {
1584
+ type: "session.waiting",
1585
+ data: {},
1586
+ });
1587
+ this.lastOutcomes.set(sessionId, {
1588
+ status: "finished",
1589
+ result: text,
1590
+ });
1591
+ }
1592
+
1593
+ /** Snapshot of recorded events (for download / offline trace). */
1594
+ async listEvents(sessionId: string, startIndex = 0): Promise<SessionEvent[]> {
1595
+ const log = await this.logs.get(sessionId);
1596
+ return log.snapshot(startIndex);
1597
+ }
1598
+
1599
+ async setContinuationToken(
1600
+ sessionId: string,
1601
+ token: string | null
1602
+ ): Promise<void> {
1603
+ await this.updateRecord(sessionId, (current) => ({
1604
+ ...current,
1605
+ continuationKey: token,
1606
+ }));
1607
+ }
1608
+
1609
+ isTurnRunning(sessionId: string): boolean {
1610
+ // Include the admit window (pendingTurns) so busy policies do not treat a
1611
+ // turn that has been accepted but not yet marked active as idle.
1612
+ return this.activeTurns.has(sessionId) || this.pendingTurns.has(sessionId);
1613
+ }
1614
+
1615
+ sessionInfo(record: SessionRecord): SessionInfo {
1616
+ return {
1617
+ id: record.sessionId,
1618
+ channelId: record.channelId,
1619
+ mode: record.mode,
1620
+ purpose: record.purpose === "eval" ? "eval" : "live",
1621
+ auth: record.auth,
1622
+ continuationKey: record.continuationKey,
1623
+ ...(record.abs === undefined ? {} : { abs: record.abs }),
1624
+ ...(record.title === undefined ? {} : { title: record.title }),
1625
+ ...(record.sdkAgentId === undefined
1626
+ ? {}
1627
+ : { sdkAgentId: record.sdkAgentId }),
1628
+ };
1629
+ }
1630
+
1631
+ private channelSession(
1632
+ sessionId: string,
1633
+ record: SessionRecord,
1634
+ isNew: boolean
1635
+ ): ChannelSession {
1636
+ return {
1637
+ id: sessionId,
1638
+ continuationToken: record.continuationKey,
1639
+ isNew,
1640
+ info: this.sessionInfo(record),
1641
+ getEventStream: (options?: { startIndex?: number }) =>
1642
+ this.eventStream(sessionId, options?.startIndex ?? 0),
1643
+ waitForCompletion: () => this.waitForCompletion(sessionId),
1644
+ setContinuationToken: (token: string | null) =>
1645
+ this.setContinuationToken(sessionId, token),
1646
+ };
1647
+ }
1648
+
1649
+ private resolveChannelId(
1650
+ channel: ChannelDefinition | HttpChannelDefinition | string
1651
+ ): string {
1652
+ if (typeof channel === "string") {
1653
+ if (
1654
+ channel !== HTTP_CHANNEL_ID &&
1655
+ !this.channelsById.has(channel) &&
1656
+ !channel.startsWith(SCHEDULE_CHANNEL_PREFIX)
1657
+ ) {
1658
+ throw new UnknownChannelError(channel);
1659
+ }
1660
+ return channel;
1661
+ }
1662
+ if (channel.__agentServe === "http-channel") {
1663
+ return HTTP_CHANNEL_ID;
1664
+ }
1665
+ for (const [id, definition] of this.channelsById) {
1666
+ if (definition === channel) {
1667
+ return id;
1668
+ }
1669
+ }
1670
+ throw new UnknownChannelError("<unregistered channel definition>");
1671
+ }
1672
+
1673
+ private initialChannelState(
1674
+ channelId: string,
1675
+ override: JsonValue | undefined
1676
+ ): JsonValue | undefined {
1677
+ if (override !== undefined) {
1678
+ return structuredClone(override);
1679
+ }
1680
+ const definition = this.channelsById.get(channelId);
1681
+ if (definition?.state === undefined) {
1682
+ return undefined;
1683
+ }
1684
+ return structuredClone(definition.state);
1685
+ }
1686
+
1687
+ /** Serialized, immutable record updates (latest-wins per session). */
1688
+ private updateRecord(
1689
+ sessionId: string,
1690
+ updater: (current: SessionRecord) => SessionRecord
1691
+ ): Promise<SessionRecord> {
1692
+ const chain = this.updateChains.get(sessionId) ?? Promise.resolve();
1693
+ const next = chain.then(async () => {
1694
+ const current = await this.sessions.get(sessionId);
1695
+ if (current === undefined) {
1696
+ throw new Error(`Session ${sessionId} not found`);
1697
+ }
1698
+ const updated = {
1699
+ ...updater(current),
1700
+ updatedAt: new Date().toISOString(),
1701
+ };
1702
+ await this.sessions.save(updated);
1703
+ return updated;
1704
+ });
1705
+ this.updateChains.set(
1706
+ sessionId,
1707
+ next.catch(() => {})
1708
+ );
1709
+ return next;
1710
+ }
1711
+
1712
+ // ==========================================================================
1713
+ // Lifecycle
1714
+ // ==========================================================================
1715
+
1716
+ trackBackground(promise: Promise<unknown>): void {
1717
+ this.backgroundWork.add(promise);
1718
+ void promise
1719
+ .catch(() => {})
1720
+ .finally(() => {
1721
+ this.backgroundWork.delete(promise);
1722
+ });
1723
+ }
1724
+
1725
+ /** Drain running turns, dispatches, and pending writes, then dispose. */
1726
+ async close(): Promise<void> {
1727
+ this.approvals.cancelAll("deny");
1728
+ await Promise.allSettled([...this.activeTurns.values()]);
1729
+ await Promise.allSettled([...this.backgroundWork]);
1730
+ await Promise.allSettled([...this.dispatchChains.values()]);
1731
+ await Promise.allSettled([...this.updateChains.values()]);
1732
+ await this.logs.flushAll();
1733
+ await this.mcpHost.close();
1734
+ await this.runner.dispose?.();
1735
+ }
1736
+ }
1737
+
1738
+ function subagentPrompt(subagent: ResolvedAgent): string {
1739
+ if (subagent.instructions !== undefined && subagent.instructions !== "") {
1740
+ return subagent.instructions;
1741
+ }
1742
+ return subagent.description ?? subagent.name;
1743
+ }
1744
+
1745
+ function describeError(error: unknown): string {
1746
+ return error instanceof Error ? error.message : String(error);
1747
+ }
1748
+
1749
+ /** Detects the `{ content, isError: true }` rich-result error envelope. */
1750
+ function isErrorEnvelope(result: unknown): boolean {
1751
+ return (
1752
+ typeof result === "object" &&
1753
+ result !== null &&
1754
+ !Array.isArray(result) &&
1755
+ Array.isArray((result as { content?: unknown }).content) &&
1756
+ (result as { isError?: unknown }).isError === true
1757
+ );
1758
+ }
1759
+
1760
+ async function evaluateNeedsApproval(
1761
+ needsApproval: ToolNeedsApproval<never> | undefined,
1762
+ input: unknown
1763
+ ): Promise<boolean> {
1764
+ if (needsApproval === undefined || needsApproval === false) {
1765
+ return false;
1766
+ }
1767
+ if (needsApproval === true) {
1768
+ return true;
1769
+ }
1770
+ return Boolean(await needsApproval(input as never));
1771
+ }
1772
+
1773
+ function approvalResolverLabel(auth: AuthContext | null): string | undefined {
1774
+ if (auth === null) {
1775
+ return undefined;
1776
+ }
1777
+ const slackUser = auth.attributes?.user_id;
1778
+ if (
1779
+ auth.authenticator === "slack" &&
1780
+ slackUser !== undefined &&
1781
+ slackUser !== ""
1782
+ ) {
1783
+ return slackUser;
1784
+ }
1785
+ return auth.principalId;
1786
+ }
1787
+
1788
+ /** Deep link to the external conversation (Slack thread today). */
1789
+ function externalUrlForSession(record: SessionRecord): string | undefined {
1790
+ const auth = record.auth;
1791
+ if (auth === null || auth.authenticator !== "slack") {
1792
+ return undefined;
1793
+ }
1794
+ const channelId = auth.attributes?.channel_id ?? "";
1795
+ const threadTs = auth.attributes?.thread_ts ?? "";
1796
+ const teamId = auth.attributes?.team_id;
1797
+ const url = slackThreadPermalink({
1798
+ channelId,
1799
+ threadTs,
1800
+ ...(teamId === undefined ? {} : { teamId }),
1801
+ });
1802
+ return url ?? undefined;
1803
+ }
1804
+
1805
+ /** Let the HTTP NDJSON writer flush before the next SDK update. */
1806
+ function yieldEventLoop(): Promise<void> {
1807
+ return new Promise((resolve) => {
1808
+ setImmediate(resolve);
1809
+ });
1810
+ }