experimental-a2 0.16.1 → 0.18.0

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 (406) hide show
  1. package/CHANGELOG.md +373 -0
  2. package/README.md +1 -1
  3. package/dist/{actor-ohPC-81x.d.ts → actor-Bjce9_5U.d.ts} +21 -11
  4. package/dist/actor-Bjce9_5U.d.ts.map +1 -0
  5. package/dist/actor-client.d.ts +2 -5
  6. package/dist/actor-client.d.ts.map +1 -1
  7. package/dist/actor-client.js +13 -4
  8. package/dist/actor-client.js.map +1 -1
  9. package/dist/actor-react.d.ts +5 -3
  10. package/dist/actor-react.d.ts.map +1 -1
  11. package/dist/actor-react.js +20 -10
  12. package/dist/actor-react.js.map +1 -1
  13. package/dist/{actor-shared-B5tJfzt-.js → actor-shared-t7w8xIKP.js} +12 -7
  14. package/dist/actor-shared-t7w8xIKP.js.map +1 -0
  15. package/dist/actor.d.ts +1 -1
  16. package/dist/actor.js +42 -23
  17. package/dist/actor.js.map +1 -1
  18. package/dist/{ai-DgOBltJ_.js → ai-BpOGm-zB.js} +621 -216
  19. package/dist/ai-BpOGm-zB.js.map +1 -0
  20. package/dist/{ai-CrEf6p_W.d.ts → ai-DrUaMyRt.d.ts} +114 -100
  21. package/dist/ai-DrUaMyRt.d.ts.map +1 -0
  22. package/dist/ai-client.d.ts +27 -0
  23. package/dist/ai-client.d.ts.map +1 -0
  24. package/dist/ai-client.js +29 -0
  25. package/dist/ai-client.js.map +1 -0
  26. package/dist/ai-fx.browser.d.ts +1 -0
  27. package/dist/ai-fx.browser.js +6 -0
  28. package/dist/ai-fx.browser.js.map +1 -0
  29. package/dist/ai-fx.d.ts +10 -0
  30. package/dist/ai-fx.d.ts.map +1 -0
  31. package/dist/ai-fx.js +634 -0
  32. package/dist/ai-fx.js.map +1 -0
  33. package/dist/ai-harness-BuLh_EYi.js +273 -0
  34. package/dist/ai-harness-BuLh_EYi.js.map +1 -0
  35. package/dist/ai-harness-DvsPeJeB.d.ts +26 -0
  36. package/dist/ai-harness-DvsPeJeB.d.ts.map +1 -0
  37. package/dist/ai-pi.browser.d.ts +1 -0
  38. package/dist/ai-pi.browser.js +6 -0
  39. package/dist/ai-pi.browser.js.map +1 -0
  40. package/dist/ai-pi.d.ts +17 -0
  41. package/dist/ai-pi.d.ts.map +1 -0
  42. package/dist/ai-pi.js +904 -0
  43. package/dist/ai-pi.js.map +1 -0
  44. package/dist/ai-react.d.ts +96 -0
  45. package/dist/ai-react.d.ts.map +1 -0
  46. package/dist/ai-react.js +142 -0
  47. package/dist/ai-react.js.map +1 -0
  48. package/dist/ai-server.browser.js +2 -2
  49. package/dist/ai-server.browser.js.map +1 -1
  50. package/dist/ai-server.d.ts +109 -43
  51. package/dist/ai-server.d.ts.map +1 -1
  52. package/dist/ai-server.js +526 -283
  53. package/dist/ai-server.js.map +1 -1
  54. package/dist/ai.d.ts +2 -2
  55. package/dist/ai.js +2 -2
  56. package/dist/bounded-cache-CPHWF-8h.js +41 -0
  57. package/dist/bounded-cache-CPHWF-8h.js.map +1 -0
  58. package/dist/{client-BYzHjkwU.js → client-BReKqwZC.js} +447 -80
  59. package/dist/client-BReKqwZC.js.map +1 -0
  60. package/dist/{client-CzyacQpJ.d.ts → client-h_DKFAjj.d.ts} +14 -13
  61. package/dist/client-h_DKFAjj.d.ts.map +1 -0
  62. package/dist/client.d.ts +4 -2
  63. package/dist/client.js +1 -1
  64. package/dist/{contract-CKRg_E4q.js → contract-BYKASlCV.js} +12 -21
  65. package/dist/contract-BYKASlCV.js.map +1 -0
  66. package/dist/{idempotent-replay-DVOlyYbx.js → idempotent-replay-JdgMQRRm.js} +2 -2
  67. package/dist/{idempotent-replay-DVOlyYbx.js.map → idempotent-replay-JdgMQRRm.js.map} +1 -1
  68. package/dist/index.d.ts +6 -5
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +4 -3
  71. package/dist/{errors-DvhSXnxk.d.ts → initial-snapshot-Bx-c0pDP.d.ts} +7 -3
  72. package/dist/initial-snapshot-Bx-c0pDP.d.ts.map +1 -0
  73. package/dist/{internal-Dq2qYxou.js → internal-B2T3U4Rq.js} +29 -2
  74. package/dist/internal-B2T3U4Rq.js.map +1 -0
  75. package/dist/otel.d.ts +1 -1
  76. package/dist/react-BBXgHWFc.d.ts +86 -0
  77. package/dist/react-BBXgHWFc.d.ts.map +1 -0
  78. package/dist/react.d.ts +2 -122
  79. package/dist/react.js +107 -100
  80. package/dist/react.js.map +1 -1
  81. package/dist/reducer-BAXc8JyF.js +87 -0
  82. package/dist/reducer-BAXc8JyF.js.map +1 -0
  83. package/dist/{reducer-otzHjuJj.d.ts → reducer-CqA59Q6U.d.ts} +46 -30
  84. package/dist/reducer-CqA59Q6U.d.ts.map +1 -0
  85. package/dist/reducer-runtime-DQwz_J59.js +28 -0
  86. package/dist/reducer-runtime-DQwz_J59.js.map +1 -0
  87. package/dist/scheduler-qstash.d.ts +3 -3
  88. package/dist/scheduler-qstash.d.ts.map +1 -1
  89. package/dist/scheduler-qstash.js +33 -10
  90. package/dist/scheduler-qstash.js.map +1 -1
  91. package/dist/{scheduler-task-BpzhPnRS.js → scheduler-task-X1SToDjP.js} +18 -3
  92. package/dist/scheduler-task-X1SToDjP.js.map +1 -0
  93. package/dist/scheduler-vercel.d.ts +4 -9
  94. package/dist/scheduler-vercel.d.ts.map +1 -1
  95. package/dist/scheduler-vercel.js +25 -44
  96. package/dist/scheduler-vercel.js.map +1 -1
  97. package/dist/{server-BD5ckxZb.js → server-Dw_k_7kO.js} +904 -219
  98. package/dist/server-Dw_k_7kO.js.map +1 -0
  99. package/dist/{server-DbD7IJVK.d.ts → server-En7cbi3v.d.ts} +116 -26
  100. package/dist/server-En7cbi3v.d.ts.map +1 -0
  101. package/dist/server.d.ts +4 -4
  102. package/dist/server.js +2 -2
  103. package/dist/session-clear-Bo-XTFEy.d.ts +12 -0
  104. package/dist/session-clear-Bo-XTFEy.d.ts.map +1 -0
  105. package/dist/{errors-DCk6ch5n.js → session-clear-DGI4jTQ5.js} +17 -2
  106. package/dist/session-clear-DGI4jTQ5.js.map +1 -0
  107. package/dist/{store-DyZM6fS5.d.ts → store-Djdv0bnZ.d.ts} +73 -3
  108. package/dist/store-Djdv0bnZ.d.ts.map +1 -0
  109. package/dist/store-N8PXxDAS.js.map +1 -1
  110. package/dist/store-memory.d.ts +1 -1
  111. package/dist/store-memory.d.ts.map +1 -1
  112. package/dist/store-memory.js +205 -66
  113. package/dist/store-memory.js.map +1 -1
  114. package/dist/{store-polling-CmxUbV93.js → store-polling-B5FcwJfo.js} +21 -4
  115. package/dist/store-polling-B5FcwJfo.js.map +1 -0
  116. package/dist/store-postgres.d.ts +1 -1
  117. package/dist/store-postgres.d.ts.map +1 -1
  118. package/dist/store-postgres.js +450 -108
  119. package/dist/store-postgres.js.map +1 -1
  120. package/dist/{store-presence-polling-C7-XZyW9.js → store-presence-polling-BAwS8RWn.js} +3 -3
  121. package/dist/{store-presence-polling-C7-XZyW9.js.map → store-presence-polling-BAwS8RWn.js.map} +1 -1
  122. package/dist/store-redis-http.d.ts +1 -1
  123. package/dist/store-redis-http.d.ts.map +1 -1
  124. package/dist/store-redis-http.js +293 -11
  125. package/dist/store-redis-http.js.map +1 -1
  126. package/dist/store-redis-notify-BRITti_o.js +2493 -0
  127. package/dist/store-redis-notify-BRITti_o.js.map +1 -0
  128. package/dist/store-redis.d.ts +1 -1
  129. package/dist/store-redis.d.ts.map +1 -1
  130. package/dist/store-redis.js +11 -11
  131. package/dist/store-redis.js.map +1 -1
  132. package/dist/store-sqlite.d.ts +1 -1
  133. package/dist/store-sqlite.d.ts.map +1 -1
  134. package/dist/store-sqlite.js +197 -33
  135. package/dist/store-sqlite.js.map +1 -1
  136. package/dist/stream-buffer-Cx7AAPGq.js +23 -0
  137. package/dist/stream-buffer-Cx7AAPGq.js.map +1 -0
  138. package/dist/{telemetry-B5jzpy6y.d.ts → telemetry-Cyfqo6Qt.d.ts} +2 -2
  139. package/dist/telemetry-Cyfqo6Qt.d.ts.map +1 -0
  140. package/dist/{wire-BO5wWCb1.js → wire-rSUglYo8.js} +132 -10
  141. package/dist/wire-rSUglYo8.js.map +1 -0
  142. package/docs/01-quickstart.mdx +50 -84
  143. package/docs/{guides/07-examples.mdx → 02-examples.mdx} +10 -20
  144. package/docs/actors/01-introduction.mdx +48 -82
  145. package/docs/actors/02-concurrency.mdx +43 -70
  146. package/docs/actors/03-timers.mdx +28 -42
  147. package/docs/actors/04-routes.mdx +110 -137
  148. package/docs/actors/meta.ts +1 -1
  149. package/docs/agents/01-introduction.mdx +257 -0
  150. package/docs/agents/02-tools.mdx +247 -0
  151. package/docs/agents/03-conversations.mdx +538 -0
  152. package/docs/agents/04-harnesses.mdx +136 -0
  153. package/docs/agents/meta.ts +1 -0
  154. package/docs/client/01-react.mdx +294 -0
  155. package/docs/client/02-transports.mdx +86 -0
  156. package/docs/client/03-presence.mdx +224 -0
  157. package/docs/client/meta.ts +1 -0
  158. package/docs/contract/01-events.mdx +57 -0
  159. package/docs/contract/02-state.mdx +142 -0
  160. package/docs/contract/03-sessions.mdx +175 -0
  161. package/docs/index.mdx +112 -156
  162. package/docs/reference/01-core.mdx +139 -0
  163. package/docs/reference/02-server.mdx +855 -0
  164. package/docs/reference/03-client.mdx +301 -0
  165. package/docs/reference/04-ai.mdx +722 -0
  166. package/docs/reference/05-adapters.mdx +351 -0
  167. package/docs/reference/06-errors.mdx +138 -0
  168. package/docs/reference/meta.ts +1 -1
  169. package/docs/server/01-handlers.mdx +200 -0
  170. package/docs/server/02-timers.mdx +130 -0
  171. package/docs/server/03-cancellation.mdx +141 -0
  172. package/docs/server/04-routes.mdx +243 -0
  173. package/docs/server/05-production.mdx +322 -0
  174. package/docs/server/06-guarantees.mdx +123 -0
  175. package/examples/playground/DEPLOY.md +53 -7
  176. package/examples/playground/app/a0/[appId]/loading.tsx +16 -0
  177. package/examples/playground/app/a0/[appId]/page.tsx +53 -0
  178. package/examples/playground/app/a0/a0.css +818 -0
  179. package/examples/playground/app/a0/app-list.tsx +218 -0
  180. package/examples/playground/app/a0/app-view.tsx +63 -0
  181. package/examples/playground/app/a0/apps/[appId]/preview/route.ts +27 -0
  182. package/examples/playground/app/a0/apps/[appId]/route.ts +28 -0
  183. package/examples/playground/app/a0/apps.test.ts +128 -0
  184. package/examples/playground/app/a0/apps.ts +102 -0
  185. package/examples/playground/app/a0/chat.tsx +410 -0
  186. package/examples/playground/app/a0/columns.tsx +109 -0
  187. package/examples/playground/app/a0/context-menu.tsx +116 -0
  188. package/examples/playground/app/a0/cookies.ts +46 -0
  189. package/examples/playground/app/a0/crash-on-request.ts +72 -0
  190. package/examples/playground/app/a0/crashes/route.ts +3 -0
  191. package/examples/playground/app/a0/crashes.test.ts +33 -0
  192. package/examples/playground/app/a0/crashes.ts +28 -0
  193. package/examples/playground/app/a0/devtools.tsx +164 -0
  194. package/examples/playground/app/a0/events/route.ts +6 -0
  195. package/examples/playground/app/a0/flags.ts +30 -0
  196. package/examples/playground/app/a0/fx/events/route.ts +9 -0
  197. package/examples/playground/app/a0/fx/scheduler/route.ts +7 -0
  198. package/examples/playground/app/a0/fx/short/events/route.ts +11 -0
  199. package/examples/playground/app/a0/layout.tsx +38 -0
  200. package/examples/playground/app/a0/list/route.ts +10 -0
  201. package/examples/playground/app/a0/model.test.ts +73 -0
  202. package/examples/playground/app/a0/model.ts +95 -0
  203. package/examples/playground/app/a0/no-photon.cjs +4 -0
  204. package/examples/playground/app/a0/no-quickjs.cjs +3 -0
  205. package/examples/playground/app/a0/page.tsx +14 -0
  206. package/examples/playground/app/a0/pi/events/route.ts +9 -0
  207. package/examples/playground/app/a0/pi/scheduler/route.ts +7 -0
  208. package/examples/playground/app/a0/pi/short/events/route.ts +11 -0
  209. package/examples/playground/app/a0/preview-pane.tsx +145 -0
  210. package/examples/playground/app/a0/preview.test.ts +53 -0
  211. package/examples/playground/app/a0/preview.ts +26 -0
  212. package/examples/playground/app/a0/sandbox-log.ts +75 -0
  213. package/examples/playground/app/a0/sandbox-session.ts +152 -0
  214. package/examples/playground/app/a0/sandbox.ts +191 -0
  215. package/examples/playground/app/a0/sandboxes/scheduler/route.ts +6 -0
  216. package/examples/playground/app/a0/scheduler/route.ts +7 -0
  217. package/examples/playground/app/a0/server.ts +213 -0
  218. package/examples/playground/app/a0/session.ts +59 -0
  219. package/examples/playground/app/a0/shell.tsx +158 -0
  220. package/examples/playground/app/a0/short/events/route.ts +9 -0
  221. package/examples/playground/app/a0/splitter.tsx +120 -0
  222. package/examples/playground/app/agent/[agentId]/agent-client.tsx +144 -174
  223. package/examples/playground/app/agent/[agentId]/command-receipt.test.tsx +65 -0
  224. package/examples/playground/app/agent/[agentId]/command-receipt.tsx +27 -0
  225. package/examples/playground/app/agent/[agentId]/compaction-event.tsx +25 -24
  226. package/examples/playground/app/agent/[agentId]/compaction-panel.test.tsx +119 -0
  227. package/examples/playground/app/agent/[agentId]/compaction-panel.tsx +208 -165
  228. package/examples/playground/app/agent/[agentId]/page.tsx +2 -16
  229. package/examples/playground/app/agent/events/route.ts +5 -2
  230. package/examples/playground/app/agent/ingress.test.ts +101 -0
  231. package/examples/playground/app/agent/model-input.test.tsx +58 -0
  232. package/examples/playground/app/agent/model-input.tsx +115 -0
  233. package/examples/playground/app/agent/model.test.ts +73 -0
  234. package/examples/playground/app/agent/model.ts +58 -26
  235. package/examples/playground/app/agent/new-agent-session.tsx +37 -19
  236. package/examples/playground/app/agent/page.tsx +4 -4
  237. package/examples/playground/app/agent/server.ts +19 -15
  238. package/examples/playground/app/agent/session.ts +5 -6
  239. package/examples/playground/app/agent/telemetry.test.ts +189 -0
  240. package/examples/playground/app/agent/telemetry.ts +30 -0
  241. package/examples/playground/app/agent/trace.test.ts +23 -0
  242. package/examples/playground/app/agent/trace.ts +18 -0
  243. package/examples/playground/app/canvas/[canvasId]/canvas-client.tsx +12 -12
  244. package/examples/playground/app/canvas/[canvasId]/page.tsx +1 -2
  245. package/examples/playground/app/canvas/[canvasId]/session.ts +2 -2
  246. package/examples/playground/app/canvas/model.ts +53 -53
  247. package/examples/playground/app/chat/[chatId]/agent-state.test.ts +176 -0
  248. package/examples/playground/app/chat/[chatId]/agent-stream-drawer.tsx +16 -1
  249. package/examples/playground/app/chat/[chatId]/chat-client.tsx +55 -77
  250. package/examples/playground/app/chat/[chatId]/chat-view.ts +37 -3
  251. package/examples/playground/app/chat/[chatId]/page.tsx +7 -15
  252. package/examples/playground/app/chat/[chatId]/session.ts +2 -19
  253. package/examples/playground/app/chat/agent/route.ts +9 -0
  254. package/examples/playground/app/chat/model.test.ts +46 -119
  255. package/examples/playground/app/chat/model.ts +82 -128
  256. package/examples/playground/app/chat/scheduler/route.ts +2 -2
  257. package/examples/playground/app/chat/server.test.ts +112 -0
  258. package/examples/playground/app/chat/server.ts +113 -117
  259. package/examples/playground/app/components/activity-feed.tsx +30 -1
  260. package/examples/playground/app/components/activity-history.test.tsx +35 -0
  261. package/examples/playground/app/components/demo-icons.tsx +89 -0
  262. package/examples/playground/app/components/demos.tsx +33 -0
  263. package/examples/playground/app/components/fast-link.test.tsx +58 -0
  264. package/examples/playground/app/components/fast-link.tsx +55 -0
  265. package/examples/playground/app/components/site-header.tsx +96 -0
  266. package/examples/playground/app/counter/counter-client.tsx +3 -3
  267. package/examples/playground/app/counter/model.test.ts +3 -3
  268. package/examples/playground/app/counter/model.ts +11 -21
  269. package/examples/playground/app/counter/page.tsx +2 -15
  270. package/examples/playground/app/counter/session.ts +2 -2
  271. package/examples/playground/app/documents/[documentId]/code-editor.tsx +12 -1
  272. package/examples/playground/app/documents/[documentId]/document-client.tsx +10 -11
  273. package/examples/playground/app/documents/[documentId]/page.tsx +1 -2
  274. package/examples/playground/app/documents/[documentId]/session.ts +2 -2
  275. package/examples/playground/app/documents/model.ts +27 -27
  276. package/examples/playground/app/globals.css +420 -207
  277. package/examples/playground/app/initial-state.test.tsx +54 -0
  278. package/examples/playground/app/layout.tsx +7 -23
  279. package/examples/playground/app/orders/[orderId]/page.tsx +1 -2
  280. package/examples/playground/app/orders/[orderId]/session.ts +2 -2
  281. package/examples/playground/app/orders/model.ts +49 -49
  282. package/examples/playground/app/page.tsx +13 -102
  283. package/examples/playground/app/recovery/[recoveryId]/page.tsx +1 -2
  284. package/examples/playground/app/recovery/[recoveryId]/session.ts +2 -2
  285. package/examples/playground/app/recovery/model.ts +30 -30
  286. package/examples/playground/instrumentation.ts +5 -0
  287. package/examples/playground/next.config.ts +17 -5
  288. package/examples/playground/package.json +13 -2
  289. package/examples/playground/scripts/a0-template.ts +77 -0
  290. package/examples/playground/vercel.json +36 -0
  291. package/package.json +29 -3
  292. package/src/actor-client.ts +16 -4
  293. package/src/actor-react.ts +25 -11
  294. package/src/actor-shared.ts +21 -6
  295. package/src/actor.ts +76 -31
  296. package/src/ai-agent-actions.ts +219 -0
  297. package/src/ai-agent.ts +2517 -0
  298. package/src/ai-app-state.ts +91 -0
  299. package/src/ai-client-state.ts +6 -3
  300. package/src/ai-client.ts +62 -0
  301. package/src/ai-context-schema.ts +25 -5
  302. package/src/ai-context.ts +90 -40
  303. package/src/ai-control-server.ts +92 -72
  304. package/src/ai-coordinator.ts +23 -22
  305. package/src/ai-fx.browser.ts +5 -0
  306. package/src/ai-fx.ts +833 -0
  307. package/src/ai-harness.ts +518 -0
  308. package/src/ai-message-base.ts +209 -0
  309. package/src/ai-message-projection.ts +262 -64
  310. package/src/ai-model-metadata.ts +11 -6
  311. package/src/ai-pi.browser.ts +5 -0
  312. package/src/ai-pi.ts +1263 -0
  313. package/src/ai-progress-batches.ts +26 -0
  314. package/src/ai-projector.ts +95 -14
  315. package/src/ai-react.ts +146 -0
  316. package/src/ai-sdk-step.ts +2 -2
  317. package/src/ai-server.browser.ts +2 -2
  318. package/src/ai-server.ts +23 -1965
  319. package/src/ai-stored-state.ts +48 -163
  320. package/src/ai.ts +339 -155
  321. package/src/bootstrap-stream.ts +72 -0
  322. package/src/bounded-cache.ts +55 -0
  323. package/src/client-render-session.ts +49 -0
  324. package/src/client.ts +688 -151
  325. package/src/contract.ts +92 -42
  326. package/src/deterministic-id.ts +37 -0
  327. package/src/equal-json.ts +32 -0
  328. package/src/errors.ts +13 -1
  329. package/src/index.ts +5 -1
  330. package/src/initial-snapshot.ts +39 -0
  331. package/src/internal.ts +52 -0
  332. package/src/libfx.d.ts +34 -0
  333. package/src/postgres-notification-scope.ts +14 -3
  334. package/src/react.ts +171 -206
  335. package/src/redis-http-reply.ts +256 -0
  336. package/src/redis-http-stream.ts +73 -0
  337. package/src/redis-http-subscriptions.ts +13 -5
  338. package/src/reducer-runtime.ts +52 -0
  339. package/src/reducer.ts +93 -11
  340. package/src/scheduler-qstash.ts +49 -20
  341. package/src/scheduler-task.ts +34 -2
  342. package/src/scheduler-vercel.ts +32 -84
  343. package/src/server-fetch.ts +139 -35
  344. package/src/server.ts +1133 -255
  345. package/src/session-clear.ts +23 -0
  346. package/src/session-socket.ts +154 -49
  347. package/src/sse.ts +71 -4
  348. package/src/store-memory.ts +252 -75
  349. package/src/store-polling.ts +18 -4
  350. package/src/store-postgres.ts +541 -114
  351. package/src/store-redis-core.ts +1450 -493
  352. package/src/store-redis-http.ts +18 -2
  353. package/src/store-redis-notify.ts +262 -85
  354. package/src/store-redis.ts +19 -9
  355. package/src/store-sqlite.ts +319 -45
  356. package/src/store.ts +79 -1
  357. package/src/stream-buffer.ts +43 -0
  358. package/src/telemetry.ts +14 -1
  359. package/src/wire.ts +241 -13
  360. package/dist/actor-ohPC-81x.d.ts.map +0 -1
  361. package/dist/actor-shared-B5tJfzt-.js.map +0 -1
  362. package/dist/ai-CrEf6p_W.d.ts.map +0 -1
  363. package/dist/ai-DgOBltJ_.js.map +0 -1
  364. package/dist/client-BYzHjkwU.js.map +0 -1
  365. package/dist/client-CzyacQpJ.d.ts.map +0 -1
  366. package/dist/contract-CKRg_E4q.js.map +0 -1
  367. package/dist/errors-DCk6ch5n.js.map +0 -1
  368. package/dist/errors-DvhSXnxk.d.ts.map +0 -1
  369. package/dist/internal-Dq2qYxou.js.map +0 -1
  370. package/dist/react.d.ts.map +0 -1
  371. package/dist/reducer-DEMjEY_O.js +0 -29
  372. package/dist/reducer-DEMjEY_O.js.map +0 -1
  373. package/dist/reducer-otzHjuJj.d.ts.map +0 -1
  374. package/dist/scheduler-task-BpzhPnRS.js.map +0 -1
  375. package/dist/server-BD5ckxZb.js.map +0 -1
  376. package/dist/server-DbD7IJVK.d.ts.map +0 -1
  377. package/dist/store-DyZM6fS5.d.ts.map +0 -1
  378. package/dist/store-polling-CmxUbV93.js.map +0 -1
  379. package/dist/store-redis-notify-BVLUfI3j.js +0 -1583
  380. package/dist/store-redis-notify-BVLUfI3j.js.map +0 -1
  381. package/dist/telemetry-B5jzpy6y.d.ts.map +0 -1
  382. package/dist/validate-XKT4FSNn.js +0 -30
  383. package/dist/validate-XKT4FSNn.js.map +0 -1
  384. package/dist/wire-BO5wWCb1.js.map +0 -1
  385. package/docs/concepts/01-contracts.mdx +0 -142
  386. package/docs/concepts/02-handlers.mdx +0 -284
  387. package/docs/concepts/03-durability.mdx +0 -314
  388. package/docs/concepts/04-state.mdx +0 -209
  389. package/docs/guides/01-timers.mdx +0 -185
  390. package/docs/guides/02-cancellation.mdx +0 -161
  391. package/docs/guides/03-react.mdx +0 -384
  392. package/docs/guides/04-local-first.mdx +0 -88
  393. package/docs/guides/05-production.mdx +0 -521
  394. package/docs/guides/06-ai-agents.mdx +0 -1318
  395. package/docs/guides/08-application-data.mdx +0 -113
  396. package/docs/guides/09-presence.mdx +0 -266
  397. package/docs/guides/10-transports.mdx +0 -221
  398. package/docs/reference/01-api.mdx +0 -1896
  399. package/docs/reference/02-errors.mdx +0 -149
  400. package/examples/playground/app/agent/[agentId]/compaction/route.ts +0 -14
  401. package/examples/playground/app/agent/compaction-settings.test.ts +0 -144
  402. package/examples/playground/app/agent/compaction-settings.ts +0 -49
  403. package/examples/playground/app/agent/compaction-timeline.test.ts +0 -377
  404. package/examples/playground/app/agent/compaction-timeline.ts +0 -208
  405. /package/docs/{concepts → contract}/meta.ts +0 -0
  406. /package/docs/{guides → server}/meta.ts +0 -0
@@ -0,0 +1,145 @@
1
+ 'use client'
2
+ import type { ReactNode } from 'react'
3
+ import { useEffect, useMemo, useState } from 'react'
4
+ import { AppWindow, LoaderCircle } from 'lucide-react'
5
+ import { appPort } from './model'
6
+ import { shownPreview } from './preview'
7
+ import { useSession } from './session'
8
+
9
+ // Checks come quickly until the app answers, then only as often as keeping
10
+ // its sandbox up needs, since each one runs a command in it.
11
+ const CHECK_MS = 3000
12
+ const KEEP_UP_MS = 2 * 60 * 1000
13
+
14
+ /** Has the server make sure the app is up; says whether it answers. */
15
+ export async function checkApp({
16
+ appId,
17
+ newApp = false,
18
+ signal,
19
+ }: {
20
+ appId: string
21
+ /** An app nobody has prompted yet, whose sandbox goes if nobody does. */
22
+ newApp?: boolean
23
+ signal?: AbortSignal
24
+ }): Promise<boolean> {
25
+ const response = await fetch(`/a0/apps/${appId}/preview`, {
26
+ method: 'POST',
27
+ headers: { 'content-type': 'application/json' },
28
+ body: JSON.stringify({ newApp }),
29
+ ...(signal ? { signal } : {}),
30
+ })
31
+ return ((await response.json()) as { up?: boolean }).up === true
32
+ }
33
+
34
+ type Liveness = {
35
+ up: boolean | null
36
+ /** How many times it came back after being down. */
37
+ returns: number
38
+ }
39
+
40
+ /**
41
+ * Whether the app answers, asked while the page is visible, which keeps its
42
+ * sandbox up. One ask at a time.
43
+ */
44
+ function useAppUp({
45
+ appId,
46
+ watching,
47
+ }: {
48
+ appId: string
49
+ watching: boolean
50
+ }): Liveness {
51
+ const [liveness, setLiveness] = useState<Liveness>({
52
+ up: null,
53
+ returns: 0,
54
+ })
55
+ useEffect(() => {
56
+ if (!watching) return
57
+ const controller = new AbortController()
58
+ let timer: ReturnType<typeof setTimeout> | undefined
59
+ const check = async (): Promise<void> => {
60
+ clearTimeout(timer)
61
+ let answering = false
62
+ try {
63
+ answering = await checkApp({ appId, signal: controller.signal })
64
+ } catch {}
65
+ if (controller.signal.aborted) return
66
+ setLiveness(({ up, returns }) => ({
67
+ up: answering,
68
+ returns: answering && up === false ? returns + 1 : returns,
69
+ }))
70
+ if (document.visibilityState === 'visible')
71
+ timer = setTimeout(check, answering ? KEEP_UP_MS : CHECK_MS)
72
+ }
73
+ const resume = (): void => {
74
+ if (document.visibilityState === 'visible') void check()
75
+ }
76
+ void check()
77
+ document.addEventListener('visibilitychange', resume)
78
+ return () => {
79
+ controller.abort()
80
+ clearTimeout(timer)
81
+ document.removeEventListener('visibilitychange', resume)
82
+ }
83
+ }, [appId, watching])
84
+ return liveness
85
+ }
86
+
87
+ /** The app the agent last showed the user, as its sandbox serves it. */
88
+ export function PreviewPane({ appId }: { appId: string }): ReactNode {
89
+ const { state } = useSession()
90
+ const shown = useMemo(() => shownPreview(state.messages), [state.messages])
91
+ // An app with a conversation stays up while it's on screen.
92
+ const { up, returns } = useAppUp({
93
+ appId,
94
+ watching: state.messages.length > 0,
95
+ })
96
+ const live = shown && up ? shown.url : null
97
+
98
+ return (
99
+ <section aria-label="Preview" className="a0-preview">
100
+ {live ? (
101
+ <iframe
102
+ // Showing the app again, or its server coming back, loads it again.
103
+ key={`${live}:${shown?.callId}:${returns}`}
104
+ title="App"
105
+ src={live}
106
+ allow="clipboard-write"
107
+ sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-modals allow-downloads"
108
+ />
109
+ ) : (
110
+ <Placeholder shown={shown !== null} checking={up === null} />
111
+ )}
112
+ </section>
113
+ )
114
+ }
115
+
116
+ function Placeholder({
117
+ shown,
118
+ checking,
119
+ }: {
120
+ shown: boolean
121
+ checking: boolean
122
+ }): ReactNode {
123
+ return (
124
+ <div className="a0-placeholder">
125
+ {shown && checking ? (
126
+ <p>
127
+ <LoaderCircle size={14} className="a0-spin" aria-hidden />
128
+ Connecting to port {appPort}
129
+ </p>
130
+ ) : (
131
+ <div>
132
+ <AppWindow size={24} strokeWidth={1.5} aria-hidden />
133
+ <p className="a0-placeholder-title">
134
+ {shown ? `Nothing is running on port ${appPort}` : 'No app yet'}
135
+ </p>
136
+ <p>
137
+ {shown
138
+ ? 'Ask the agent to start it again.'
139
+ : 'It shows up here when the agent is done.'}
140
+ </p>
141
+ </div>
142
+ )}
143
+ </div>
144
+ )
145
+ }
@@ -0,0 +1,53 @@
1
+ import { expect, it } from 'vitest'
2
+ import type { AppMessage } from './model'
3
+ import { shownPreview } from './preview'
4
+
5
+ type Part = AppMessage['parts'][number]
6
+
7
+ const message = (role: AppMessage['role'], ...parts: Part[]): AppMessage => ({
8
+ id: crypto.randomUUID(),
9
+ role,
10
+ parts,
11
+ })
12
+
13
+ const shown = (callId: string, output: unknown): Part =>
14
+ ({
15
+ type: 'tool-show_preview',
16
+ toolCallId: callId,
17
+ state: 'output-available',
18
+ input: { path: '/' },
19
+ output,
20
+ }) as Part
21
+
22
+ const url = 'https://sb-3000.vercel.run/'
23
+
24
+ it('shows nothing until the agent shows the app', () => {
25
+ expect(shownPreview([])).toBeNull()
26
+ })
27
+
28
+ it('follows the latest preview the agent showed', () => {
29
+ const messages = [
30
+ message('assistant', shown('p1', { url })),
31
+ message('assistant', shown('p2', { url: `${url}about` })),
32
+ message(
33
+ 'assistant',
34
+ shown('p3', { error: 'Nothing is serving port 3000.' }),
35
+ ),
36
+ ]
37
+ expect(shownPreview(messages)).toEqual({ url: `${url}about`, callId: 'p2' })
38
+ })
39
+
40
+ it('ignores previews a user message carries', () => {
41
+ expect(shownPreview([message('user', shown('p1', { url }))])).toBeNull()
42
+ })
43
+
44
+ it('frames only https sandbox hosts, and survives malformed output', () => {
45
+ for (const output of [
46
+ null,
47
+ url,
48
+ { url: 'https://example.com/' },
49
+ { url: 'http://sb-3000.vercel.run/' },
50
+ { url: 'javascript:alert(1)' },
51
+ ])
52
+ expect(shownPreview([message('assistant', shown('p1', output))])).toBeNull()
53
+ })
@@ -0,0 +1,26 @@
1
+ import { z } from 'zod'
2
+ import type { AppMessage } from './model'
3
+
4
+ // Only the agent's own show_preview results, which point at a sandbox's
5
+ // public host. Anything else in the log never reaches the iframe.
6
+ const previewSchema = z.object({
7
+ url: z.url({ protocol: /^https$/, hostname: /\.vercel\.run$/ }),
8
+ })
9
+
10
+ /** The app the agent last showed the user, so a reload shows it again. */
11
+ export function shownPreview(
12
+ messages: readonly AppMessage[],
13
+ ): { url: string; callId: string } | null {
14
+ let shown: { url: string; callId: string } | null = null
15
+ for (const message of messages) {
16
+ if (message.role !== 'assistant') continue
17
+ for (const part of message.parts) {
18
+ if (part.type !== 'tool-show_preview') continue
19
+ if (!('state' in part) || part.state !== 'output-available') continue
20
+ const parsed = previewSchema.safeParse(part.output)
21
+ if (parsed.success)
22
+ shown = { url: parsed.data.url, callId: part.toolCallId }
23
+ }
24
+ }
25
+ return shown
26
+ }
@@ -0,0 +1,75 @@
1
+ import 'server-only'
2
+ import { contract } from 'experimental-a2'
3
+ import { vercelQueues } from 'experimental-a2/scheduler-vercel'
4
+ import { createServer } from 'experimental-a2/server'
5
+ import { z } from 'zod'
6
+ import { store } from '@/lib/store'
7
+ import { isAppId } from './apps'
8
+ import { deleteAppSandbox } from './sandbox'
9
+ import { agentServers } from './server'
10
+
11
+ // How long a new app's sandbox waits for a first prompt.
12
+ const IDLE_GRACE_MS = 10 * 60 * 1000
13
+
14
+ /**
15
+ * An app's sandbox lifecycle, in a log of its own so the app's log stays
16
+ * its conversation. A new app's sandbox opens on sight, before anyone
17
+ * prompts it, so each one schedules a check that deletes it if the app is
18
+ * still empty when the grace period ends.
19
+ */
20
+ const sandboxLog = contract({
21
+ name: 'a0-sandboxes',
22
+ events: {
23
+ 'sandbox.idle.checked': z.object({ appId: z.uuid() }),
24
+ },
25
+ })
26
+
27
+ const sandboxLogId = (appId: string): string => `${appId}:sandbox`
28
+
29
+ export const sandboxScheduler =
30
+ process.env.VERCEL || process.env.A2_QUEUES
31
+ ? vercelQueues({ topic: 'a2-a0-sandboxes' })
32
+ : undefined
33
+
34
+ export const sandboxServer = createServer({
35
+ ...(store ? { store } : {}),
36
+ ...(sandboxScheduler ? { scheduler: sandboxScheduler } : {}),
37
+ contract: sandboxLog,
38
+ handlers: {
39
+ 'sandbox.idle.checked': async ({ event }) => {
40
+ const { appId } = event.payload
41
+ // A prompt since the sandbox opened makes it a real app's, whichever
42
+ // harness's agent got it.
43
+ const reads = await Promise.all(
44
+ Object.values(agentServers).map((server) =>
45
+ server.session(appId).state(),
46
+ ),
47
+ )
48
+ if (reads.every(({ state }) => state.messages.length === 0))
49
+ await deleteAppSandbox({ appId })
50
+ },
51
+ },
52
+ })
53
+
54
+ /**
55
+ * Schedules the check for a new app's sandbox. It's keyed to the sandbox's
56
+ * creation, so repeating it converges on one check, and a sandbox made
57
+ * again later gets its own. Without a scheduler, as under next dev, the
58
+ * sandbox just waits for its session to time out.
59
+ */
60
+ export async function checkWhenIdle({
61
+ appId,
62
+ createdAt,
63
+ }: {
64
+ appId: string
65
+ createdAt: Date
66
+ }): Promise<void> {
67
+ if (!sandboxScheduler || !isAppId(appId)) return
68
+ await sandboxServer
69
+ .session(sandboxLogId(appId))
70
+ .schedule(
71
+ `idle-check-${createdAt.getTime()}`,
72
+ { at: new Date(createdAt.getTime() + IDLE_GRACE_MS) },
73
+ { type: 'sandbox.idle.checked', payload: { appId } },
74
+ )
75
+ }
@@ -0,0 +1,152 @@
1
+ import 'server-only'
2
+ import type { Sandbox } from '@vercel/sandbox'
3
+ import type { Experimental_SandboxSession } from 'ai'
4
+ import { appDir } from './sandbox'
5
+
6
+ // A job the command backgrounds, like `next dev &`, keeps whatever output it
7
+ // started with, and a read of a pipe would wait on it forever. Output goes to
8
+ // a file instead; background jobs live on.
9
+ const CAPTURE = `out=$(mktemp); bash -lc "$0" > "$out" 2>&1 < /dev/null; code=$?; cat "$out"; rm -f "$out"; exit $code`
10
+
11
+ type Run = Parameters<Experimental_SandboxSession['run']>[0]
12
+
13
+ /**
14
+ * The app's Vercel Sandbox as an AI SDK sandbox session, for harnesses whose
15
+ * own tools need one. Commands run in the app's directory.
16
+ */
17
+ export function sandboxSession(sandbox: Sandbox): Experimental_SandboxSession {
18
+ const params = ({ command, workingDirectory, env, abortSignal }: Run) => ({
19
+ cmd: 'bash',
20
+ args: ['-c', CAPTURE, command],
21
+ cwd: workingDirectory ?? appDir,
22
+ ...(env ? { env } : {}),
23
+ ...(abortSignal ? { signal: abortSignal } : {}),
24
+ })
25
+ const readBinaryFile = async ({
26
+ path,
27
+ abortSignal,
28
+ }: {
29
+ path: string
30
+ abortSignal?: AbortSignal
31
+ }): Promise<Uint8Array | null> => {
32
+ const buffer = await sandbox.readFileToBuffer(
33
+ { path, cwd: appDir },
34
+ abortSignal ? { signal: abortSignal } : {},
35
+ )
36
+ return buffer && new Uint8Array(buffer)
37
+ }
38
+ // The sandbox extracts written files at its root, creating their directories.
39
+ const write = (path: string, content: Uint8Array, signal?: AbortSignal) =>
40
+ sandbox.writeFiles(
41
+ [{ path: path.startsWith('/') ? path : `${appDir}/${path}`, content }],
42
+ signal ? { signal } : {},
43
+ )
44
+
45
+ return {
46
+ description: `A Vercel Sandbox. The app is in ${appDir}.`,
47
+ // Streams, so a reader that stops early stops the download.
48
+ readFile: async ({ path, abortSignal }) => {
49
+ const stream = await sandbox.readFile(
50
+ { path, cwd: appDir },
51
+ abortSignal ? { signal: abortSignal } : {},
52
+ )
53
+ if (stream === null) return null
54
+ const chunks = stream[Symbol.asyncIterator]()
55
+ return new ReadableStream<Uint8Array>({
56
+ pull: async (controller) => {
57
+ const { done, value } = await chunks.next()
58
+ if (done) controller.close()
59
+ else
60
+ controller.enqueue(
61
+ typeof value === 'string'
62
+ ? new TextEncoder().encode(value)
63
+ : value,
64
+ )
65
+ },
66
+ cancel: async () => {
67
+ await chunks.return?.()
68
+ },
69
+ })
70
+ },
71
+ readBinaryFile,
72
+ readTextFile: async ({ encoding, startLine, endLine, ...options }) => {
73
+ const bytes = await readBinaryFile(options)
74
+ if (bytes === null) return null
75
+ const text = new TextDecoder(encoding).decode(bytes)
76
+ if (startLine === undefined && endLine === undefined) return text
77
+ return text
78
+ .split('\n')
79
+ .slice((startLine ?? 1) - 1, endLine)
80
+ .join('\n')
81
+ },
82
+ writeFile: async ({ path, content, abortSignal }) =>
83
+ write(
84
+ path,
85
+ new Uint8Array(await new Response(content).arrayBuffer()),
86
+ abortSignal,
87
+ ),
88
+ writeBinaryFile: ({ path, content, abortSignal }) =>
89
+ write(path, content, abortSignal),
90
+ writeTextFile: ({ path, content, abortSignal }) =>
91
+ write(path, new TextEncoder().encode(content), abortSignal),
92
+ run: async (options) => {
93
+ const finished = await sandbox.runCommand(params(options))
94
+ return {
95
+ exitCode: finished.exitCode,
96
+ stdout: await finished.stdout(),
97
+ stderr: await finished.stderr(),
98
+ }
99
+ },
100
+ spawn: async (options) => {
101
+ const { abortSignal } = options
102
+ const command = await sandbox.runCommand({
103
+ ...params(options),
104
+ detached: true,
105
+ })
106
+ // For a detached command the signal only cancels its start; aborting
107
+ // kills it, as the AI SDK's sandbox contract says.
108
+ const kill = () => void command.kill().catch(() => {})
109
+ if (abortSignal?.aborted) kill()
110
+ abortSignal?.addEventListener('abort', kill, { once: true })
111
+ const encoder = new TextEncoder()
112
+ let stdout!: ReadableStreamDefaultController<Uint8Array>
113
+ let stderr!: ReadableStreamDefaultController<Uint8Array>
114
+ const streams = {
115
+ stdout: new ReadableStream<Uint8Array>({
116
+ start: (c) => void (stdout = c),
117
+ }),
118
+ stderr: new ReadableStream<Uint8Array>({
119
+ start: (c) => void (stderr = c),
120
+ }),
121
+ }
122
+ // The command's log ends when it exits, and closes both streams.
123
+ void (async () => {
124
+ try {
125
+ for await (const line of command.logs())
126
+ (line.stream === 'stdout' ? stdout : stderr).enqueue(
127
+ encoder.encode(line.data),
128
+ )
129
+ stdout.close()
130
+ stderr.close()
131
+ } catch (error) {
132
+ stdout.error(error)
133
+ stderr.error(error)
134
+ }
135
+ })()
136
+ return {
137
+ ...streams,
138
+ wait: async () => {
139
+ try {
140
+ const finished = await command.wait(
141
+ abortSignal ? { signal: abortSignal } : {},
142
+ )
143
+ return { exitCode: finished.exitCode }
144
+ } finally {
145
+ abortSignal?.removeEventListener('abort', kill)
146
+ }
147
+ },
148
+ kill: () => command.kill(),
149
+ }
150
+ },
151
+ }
152
+ }
@@ -0,0 +1,191 @@
1
+ import 'server-only'
2
+ import { APIError, Sandbox } from '@vercel/sandbox'
3
+ import { appPort } from './model'
4
+
5
+ // Each app has one persistent Vercel Sandbox, started from a template that
6
+ // already holds a Next.js app with its dependencies (scripts/a0-template.ts).
7
+
8
+ // The default image's home, owned by the user commands run as.
9
+ export const workspace = '/vercel'
10
+ export const appDir = `${workspace}/app`
11
+ // A sandbox's session lasts this long from its start unless kept alive.
12
+ const SESSION_TIMEOUT_MS = 15 * 60 * 1000
13
+ // Each tool call keeps the sandbox up at least this much longer.
14
+ const KEEP_ALIVE_MS = 10 * 60 * 1000
15
+ // Abandoned apps lose their files after this long.
16
+ const SNAPSHOT_EXPIRATION_MS = 14 * 24 * 60 * 60 * 1000
17
+ const OUTPUT_LIMIT = 32 * 1024
18
+
19
+ // Local runs and deployments share the project's sandboxes, so each name
20
+ // says where it came from.
21
+ const scope = process.env.VERCEL_ENV ?? 'local'
22
+ const sandboxName = (appId: string): string => `a2-a0-${scope}-${appId}`
23
+
24
+ const status = (error: unknown): number | undefined =>
25
+ error instanceof APIError ? error.response.status : undefined
26
+ const notFound = (error: unknown): boolean => status(error) === 404
27
+
28
+ // Starts next dev unless one is already running or booting: it holds a lock
29
+ // for as long as it runs, so another start exits at once, and a server that
30
+ // dies frees it. A server started without the lock, by older code or by hand,
31
+ // still owns the port, which a bare TCP connect checks without asking it for a
32
+ // page. A command only finishes once every process in its group exits, so
33
+ // next dev gets its own session (setsid -f), and appends to its log.
34
+ export const START_DEV = `cd ${appDir} && { (: < /dev/tcp/127.0.0.1/${appPort}) 2>/dev/null || setsid -f flock -n /tmp/next.lock npm run dev -- --port ${appPort} >> /tmp/next.log 2>&1 < /dev/null; }`
35
+
36
+ // Starts next dev, then asks it for the home page in the background, so the
37
+ // page is compiled by the time anyone looks. Neither is waited for.
38
+ const WARM_UP = `${START_DEV}; setsid -f timeout 60 bash -c 'until curl -s -o /dev/null localhost:${appPort}; do sleep 0.5; done' > /dev/null 2>&1 < /dev/null`
39
+
40
+ const startDev = async (sandbox: Sandbox): Promise<void> => {
41
+ await sandbox.runCommand({ cmd: 'bash', args: ['-c', WARM_UP] })
42
+ }
43
+
44
+ function templateSnapshot(): string {
45
+ const snapshotId = process.env.A0_TEMPLATE_SNAPSHOT_ID
46
+ if (!snapshotId)
47
+ throw new Error(
48
+ 'A0_TEMPLATE_SNAPSHOT_ID is not set. Build the template with `pnpm --filter playground a0:template` and set the id it prints.',
49
+ )
50
+ return snapshotId
51
+ }
52
+
53
+ async function openApp(appId: string): Promise<Sandbox> {
54
+ const name = sandboxName(appId)
55
+ const existing = () => Sandbox.get({ name, resume: true, onResume: startDev })
56
+ try {
57
+ return await existing()
58
+ } catch (error) {
59
+ if (!notFound(error)) throw error
60
+ }
61
+ let sandbox: Sandbox
62
+ try {
63
+ sandbox = await Sandbox.create({
64
+ name,
65
+ source: { type: 'snapshot', snapshotId: templateSnapshot() },
66
+ ports: [appPort],
67
+ timeout: SESSION_TIMEOUT_MS,
68
+ persistent: true,
69
+ snapshotExpiration: SNAPSHOT_EXPIRATION_MS,
70
+ keepLastSnapshots: { count: 1 },
71
+ })
72
+ } catch (error) {
73
+ // Another instance created it first. The API holds a racing create until
74
+ // the first one finishes, then refuses it: 409, or 400 "already exists".
75
+ if (status(error) !== 409 && status(error) !== 400) throw error
76
+ return existing().catch((missing: unknown) => {
77
+ throw notFound(missing) ? error : missing
78
+ })
79
+ }
80
+ await startDev(sandbox)
81
+ return sandbox
82
+ }
83
+
84
+ // Parallel tool calls on a new app share one create within this instance.
85
+ const opening = new Map<string, Promise<Sandbox>>()
86
+
87
+ /** The app's sandbox, created or resumed, and kept up while it's used. */
88
+ export async function appSandbox({
89
+ appId,
90
+ }: {
91
+ appId: string
92
+ }): Promise<Sandbox> {
93
+ const pending =
94
+ opening.get(appId) ?? openApp(appId).finally(() => opening.delete(appId))
95
+ opening.set(appId, pending)
96
+ const sandbox = await pending
97
+ // Extends only by the shortfall, so repeated calls never stack up time.
98
+ const shortfall =
99
+ Date.now() + KEEP_ALIVE_MS - (sandbox.expiresAt?.getTime() ?? 0)
100
+ // Throws past the plan's session limit; the sandbox still works until then.
101
+ if (shortfall > 0) await sandbox.extendTimeout(shortfall).catch(() => {})
102
+ return sandbox
103
+ }
104
+
105
+ // A server that's booting refuses connections for a moment, and a page
106
+ // that's still compiling answers late; the probe waits out both.
107
+ const ANSWER_WAIT_SECONDS = 30
108
+ const PROBE = `curl -s -o /dev/null -w '%{http_code}' --retry ${ANSWER_WAIT_SECONDS} --retry-delay 1 --retry-connrefused --retry-max-time ${ANSWER_WAIT_SECONDS} --max-time ${ANSWER_WAIT_SECONDS} http://localhost:${appPort}/`
109
+
110
+ /**
111
+ * Makes sure the app is up: opens its sandbox, creating or resuming it and
112
+ * keeping it running, starts next dev if it isn't running, and says whether
113
+ * the app answers. Safe to repeat; it only does what's missing.
114
+ */
115
+ export async function ensureApp({
116
+ appId,
117
+ }: {
118
+ appId: string
119
+ }): Promise<{ up: boolean; createdAt: Date }> {
120
+ const sandbox = await appSandbox({ appId })
121
+ const probe = await sandbox.runCommand({
122
+ cmd: 'bash',
123
+ args: ['-c', `${START_DEV}; ${PROBE}`],
124
+ })
125
+ return {
126
+ up: Number(await probe.stdout()) > 0,
127
+ createdAt: sandbox.createdAt,
128
+ }
129
+ }
130
+
131
+ /**
132
+ * The public URL of a path on the app. It doesn't ask the dev server; the
133
+ * preview pane waits for the page itself.
134
+ */
135
+ export const previewUrl = ({
136
+ sandbox,
137
+ path,
138
+ }: {
139
+ sandbox: Sandbox
140
+ path: string
141
+ }): { url: string } => ({ url: new URL(path, sandbox.domain(appPort)).href })
142
+
143
+ export async function deleteAppSandbox({
144
+ appId,
145
+ }: {
146
+ appId: string
147
+ }): Promise<void> {
148
+ try {
149
+ const sandbox = await Sandbox.get({
150
+ name: sandboxName(appId),
151
+ resume: false,
152
+ })
153
+ await sandbox.delete()
154
+ } catch (error) {
155
+ if (!notFound(error)) throw error
156
+ }
157
+ }
158
+
159
+ // Runs the command with its output in a file rather than the sandbox's pipe:
160
+ // a job it backgrounds, like `a && b && server &`, keeps whatever output it
161
+ // started with, and on the pipe the call would wait for it forever.
162
+ // coreutils' timeout ends it; background jobs live on. Only the first
163
+ // OUTPUT_LIMIT bytes leave the sandbox.
164
+ const RUN = `out=$(mktemp); timeout "$1" bash -lc "$0" > "$out" 2>&1 < /dev/null; code=$?; size=$(wc -c < "$out"); head -c ${OUTPUT_LIMIT} "$out"; if [ "$size" -gt ${OUTPUT_LIMIT} ]; then printf '\\n… %d more bytes' $((size - ${OUTPUT_LIMIT})); fi; rm -f "$out"; exit $code`
165
+
166
+ /** Runs a shell command in the app's workspace, with output capped. */
167
+ export async function bash({
168
+ sandbox,
169
+ command,
170
+ timeoutSeconds,
171
+ signal,
172
+ }: {
173
+ sandbox: Sandbox
174
+ command: string
175
+ timeoutSeconds: number
176
+ signal?: AbortSignal
177
+ }): Promise<{ exitCode: number; timedOut?: string; output: string }> {
178
+ const result = await sandbox.runCommand({
179
+ cmd: 'bash',
180
+ args: ['-c', RUN, command, `${timeoutSeconds}s`],
181
+ cwd: workspace,
182
+ ...(signal ? { signal } : {}),
183
+ })
184
+ return {
185
+ exitCode: result.exitCode,
186
+ ...(result.exitCode === 124 && {
187
+ timedOut: `Stopped after ${timeoutSeconds}s. Background long-running servers.`,
188
+ }),
189
+ output: await result.stdout(),
190
+ }
191
+ }
@@ -0,0 +1,6 @@
1
+ import { sandboxScheduler, sandboxServer } from '../../sandbox-log'
2
+
3
+ export const POST =
4
+ sandboxScheduler?.handler(sandboxServer) ??
5
+ (() =>
6
+ new Response('a0 sandbox scheduler is not configured', { status: 503 }))
@@ -0,0 +1,7 @@
1
+ import { agentServer, appSchedulers } from '../server'
2
+
3
+ export const POST =
4
+ appSchedulers['ai-sdk']?.handler(agentServer) ??
5
+ (() => new Response('a0 scheduler is not configured', { status: 503 }))
6
+
7
+ export const maxDuration = 300