experimental-a2 0.16.1 → 0.17.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 (333) hide show
  1. package/CHANGELOG.md +352 -0
  2. package/dist/{actor-ohPC-81x.d.ts → actor-DBbe5xeE.d.ts} +21 -11
  3. package/dist/actor-DBbe5xeE.d.ts.map +1 -0
  4. package/dist/actor-client.d.ts +2 -5
  5. package/dist/actor-client.d.ts.map +1 -1
  6. package/dist/actor-client.js +13 -4
  7. package/dist/actor-client.js.map +1 -1
  8. package/dist/actor-react.d.ts +5 -3
  9. package/dist/actor-react.d.ts.map +1 -1
  10. package/dist/actor-react.js +20 -10
  11. package/dist/actor-react.js.map +1 -1
  12. package/dist/{actor-shared-B5tJfzt-.js → actor-shared-t7w8xIKP.js} +12 -7
  13. package/dist/actor-shared-t7w8xIKP.js.map +1 -0
  14. package/dist/actor.d.ts +1 -1
  15. package/dist/actor.js +42 -23
  16. package/dist/actor.js.map +1 -1
  17. package/dist/{ai-DgOBltJ_.js → ai-UrWfKeuv.js} +621 -216
  18. package/dist/ai-UrWfKeuv.js.map +1 -0
  19. package/dist/ai-client.d.ts +27 -0
  20. package/dist/ai-client.d.ts.map +1 -0
  21. package/dist/ai-client.js +29 -0
  22. package/dist/ai-client.js.map +1 -0
  23. package/dist/ai-react.d.ts +96 -0
  24. package/dist/ai-react.d.ts.map +1 -0
  25. package/dist/ai-react.js +142 -0
  26. package/dist/ai-react.js.map +1 -0
  27. package/dist/ai-server.browser.js +2 -2
  28. package/dist/ai-server.browser.js.map +1 -1
  29. package/dist/ai-server.d.ts +80 -36
  30. package/dist/ai-server.d.ts.map +1 -1
  31. package/dist/ai-server.js +432 -258
  32. package/dist/ai-server.js.map +1 -1
  33. package/dist/{ai-CrEf6p_W.d.ts → ai-vpGtcaCB.d.ts} +114 -100
  34. package/dist/ai-vpGtcaCB.d.ts.map +1 -0
  35. package/dist/ai.d.ts +2 -2
  36. package/dist/ai.js +2 -2
  37. package/dist/{client-BYzHjkwU.js → client-COaiaDub.js} +447 -80
  38. package/dist/client-COaiaDub.js.map +1 -0
  39. package/dist/{client-CzyacQpJ.d.ts → client-qRnRUzBS.d.ts} +14 -13
  40. package/dist/client-qRnRUzBS.d.ts.map +1 -0
  41. package/dist/client.d.ts +4 -2
  42. package/dist/client.js +1 -1
  43. package/dist/{contract-CKRg_E4q.js → contract-BYKASlCV.js} +12 -21
  44. package/dist/contract-BYKASlCV.js.map +1 -0
  45. package/dist/{idempotent-replay-DVOlyYbx.js → idempotent-replay-JdgMQRRm.js} +2 -2
  46. package/dist/{idempotent-replay-DVOlyYbx.js.map → idempotent-replay-JdgMQRRm.js.map} +1 -1
  47. package/dist/index.d.ts +6 -5
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +4 -3
  50. package/dist/{errors-DvhSXnxk.d.ts → initial-snapshot-BrLso5x_.d.ts} +7 -3
  51. package/dist/initial-snapshot-BrLso5x_.d.ts.map +1 -0
  52. package/dist/{internal-Dq2qYxou.js → internal-CbVvEwad.js} +23 -2
  53. package/dist/internal-CbVvEwad.js.map +1 -0
  54. package/dist/otel.d.ts +1 -1
  55. package/dist/react-BbJ-g6Uu.d.ts +86 -0
  56. package/dist/react-BbJ-g6Uu.d.ts.map +1 -0
  57. package/dist/react.d.ts +2 -122
  58. package/dist/react.js +107 -100
  59. package/dist/react.js.map +1 -1
  60. package/dist/reducer-BAXc8JyF.js +87 -0
  61. package/dist/reducer-BAXc8JyF.js.map +1 -0
  62. package/dist/{reducer-otzHjuJj.d.ts → reducer-BYu6DT7h.d.ts} +45 -29
  63. package/dist/reducer-BYu6DT7h.d.ts.map +1 -0
  64. package/dist/reducer-runtime-DQwz_J59.js +28 -0
  65. package/dist/reducer-runtime-DQwz_J59.js.map +1 -0
  66. package/dist/scheduler-qstash.d.ts +3 -3
  67. package/dist/scheduler-qstash.d.ts.map +1 -1
  68. package/dist/scheduler-qstash.js +33 -10
  69. package/dist/scheduler-qstash.js.map +1 -1
  70. package/dist/{scheduler-task-BpzhPnRS.js → scheduler-task-X1SToDjP.js} +18 -3
  71. package/dist/scheduler-task-X1SToDjP.js.map +1 -0
  72. package/dist/scheduler-vercel.d.ts +4 -9
  73. package/dist/scheduler-vercel.d.ts.map +1 -1
  74. package/dist/scheduler-vercel.js +25 -44
  75. package/dist/scheduler-vercel.js.map +1 -1
  76. package/dist/{server-DbD7IJVK.d.ts → server-CGM8WzRG.d.ts} +87 -26
  77. package/dist/server-CGM8WzRG.d.ts.map +1 -0
  78. package/dist/{server-BD5ckxZb.js → server-Rj64fNvX.js} +854 -202
  79. package/dist/server-Rj64fNvX.js.map +1 -0
  80. package/dist/server.d.ts +4 -4
  81. package/dist/server.js +2 -2
  82. package/dist/{errors-DCk6ch5n.js → session-clear-DGI4jTQ5.js} +17 -2
  83. package/dist/session-clear-DGI4jTQ5.js.map +1 -0
  84. package/dist/session-clear-Dshpuhff.d.ts +12 -0
  85. package/dist/session-clear-Dshpuhff.d.ts.map +1 -0
  86. package/dist/{store-DyZM6fS5.d.ts → store-Gpkq7WBo.d.ts} +44 -3
  87. package/dist/store-Gpkq7WBo.d.ts.map +1 -0
  88. package/dist/store-N8PXxDAS.js.map +1 -1
  89. package/dist/store-memory.d.ts +1 -1
  90. package/dist/store-memory.d.ts.map +1 -1
  91. package/dist/store-memory.js +182 -69
  92. package/dist/store-memory.js.map +1 -1
  93. package/dist/{store-polling-CmxUbV93.js → store-polling-4_y8lETm.js} +21 -4
  94. package/dist/store-polling-4_y8lETm.js.map +1 -0
  95. package/dist/store-postgres.d.ts +1 -1
  96. package/dist/store-postgres.d.ts.map +1 -1
  97. package/dist/store-postgres.js +399 -108
  98. package/dist/store-postgres.js.map +1 -1
  99. package/dist/{store-presence-polling-C7-XZyW9.js → store-presence-polling-Q1C-k2dd.js} +3 -3
  100. package/dist/{store-presence-polling-C7-XZyW9.js.map → store-presence-polling-Q1C-k2dd.js.map} +1 -1
  101. package/dist/store-redis-http.d.ts +1 -1
  102. package/dist/store-redis-http.d.ts.map +1 -1
  103. package/dist/store-redis-http.js +293 -11
  104. package/dist/store-redis-http.js.map +1 -1
  105. package/dist/store-redis-notify-tqWNnMRu.js +2374 -0
  106. package/dist/store-redis-notify-tqWNnMRu.js.map +1 -0
  107. package/dist/store-redis.d.ts +1 -1
  108. package/dist/store-redis.d.ts.map +1 -1
  109. package/dist/store-redis.js +11 -11
  110. package/dist/store-redis.js.map +1 -1
  111. package/dist/store-sqlite.d.ts +1 -1
  112. package/dist/store-sqlite.d.ts.map +1 -1
  113. package/dist/store-sqlite.js +166 -32
  114. package/dist/store-sqlite.js.map +1 -1
  115. package/dist/stream-buffer-Cx7AAPGq.js +23 -0
  116. package/dist/stream-buffer-Cx7AAPGq.js.map +1 -0
  117. package/dist/{telemetry-B5jzpy6y.d.ts → telemetry-cZH-cibj.d.ts} +2 -2
  118. package/dist/telemetry-cZH-cibj.d.ts.map +1 -0
  119. package/dist/{wire-BO5wWCb1.js → wire-DjfHOvdW.js} +132 -10
  120. package/dist/wire-DjfHOvdW.js.map +1 -0
  121. package/docs/01-quickstart.mdx +2 -2
  122. package/docs/actors/01-introduction.mdx +4 -0
  123. package/docs/actors/04-routes.mdx +14 -1
  124. package/docs/concepts/01-contracts.mdx +95 -0
  125. package/docs/concepts/04-state.mdx +3 -3
  126. package/docs/guides/01-timers.mdx +22 -14
  127. package/docs/guides/02-cancellation.mdx +1 -1
  128. package/docs/guides/03-react.mdx +268 -328
  129. package/docs/guides/04-local-first.mdx +32 -73
  130. package/docs/guides/05-production.mdx +40 -27
  131. package/docs/guides/06-ai-agents.mdx +671 -581
  132. package/docs/guides/09-presence.mdx +3 -7
  133. package/docs/guides/10-transports.mdx +5 -5
  134. package/docs/index.mdx +10 -18
  135. package/docs/reference/01-api.mdx +656 -194
  136. package/docs/reference/02-errors.mdx +2 -1
  137. package/examples/playground/DEPLOY.md +52 -6
  138. package/examples/playground/app/a0/[appId]/loading.tsx +16 -0
  139. package/examples/playground/app/a0/[appId]/page.tsx +48 -0
  140. package/examples/playground/app/a0/a0.css +818 -0
  141. package/examples/playground/app/a0/app-list.tsx +211 -0
  142. package/examples/playground/app/a0/app-view.tsx +55 -0
  143. package/examples/playground/app/a0/apps/[appId]/preview/route.ts +27 -0
  144. package/examples/playground/app/a0/apps/[appId]/route.ts +23 -0
  145. package/examples/playground/app/a0/apps.test.ts +116 -0
  146. package/examples/playground/app/a0/apps.ts +87 -0
  147. package/examples/playground/app/a0/chat.tsx +364 -0
  148. package/examples/playground/app/a0/columns.tsx +109 -0
  149. package/examples/playground/app/a0/context-menu.tsx +116 -0
  150. package/examples/playground/app/a0/cookies.ts +46 -0
  151. package/examples/playground/app/a0/crash-on-request.ts +72 -0
  152. package/examples/playground/app/a0/crashes/route.ts +3 -0
  153. package/examples/playground/app/a0/crashes.test.ts +33 -0
  154. package/examples/playground/app/a0/crashes.ts +28 -0
  155. package/examples/playground/app/a0/devtools.tsx +164 -0
  156. package/examples/playground/app/a0/events/route.ts +6 -0
  157. package/examples/playground/app/a0/flags.ts +30 -0
  158. package/examples/playground/app/a0/layout.tsx +38 -0
  159. package/examples/playground/app/a0/list/route.ts +10 -0
  160. package/examples/playground/app/a0/model.test.ts +56 -0
  161. package/examples/playground/app/a0/model.ts +58 -0
  162. package/examples/playground/app/a0/page.tsx +14 -0
  163. package/examples/playground/app/a0/preview-pane.tsx +145 -0
  164. package/examples/playground/app/a0/preview.test.ts +53 -0
  165. package/examples/playground/app/a0/preview.ts +26 -0
  166. package/examples/playground/app/a0/sandbox-log.ts +69 -0
  167. package/examples/playground/app/a0/sandbox.ts +191 -0
  168. package/examples/playground/app/a0/sandboxes/scheduler/route.ts +6 -0
  169. package/examples/playground/app/a0/scheduler/route.ts +7 -0
  170. package/examples/playground/app/a0/server.ts +158 -0
  171. package/examples/playground/app/a0/session.ts +47 -0
  172. package/examples/playground/app/a0/shell.tsx +150 -0
  173. package/examples/playground/app/a0/short/events/route.ts +9 -0
  174. package/examples/playground/app/a0/splitter.tsx +120 -0
  175. package/examples/playground/app/agent/[agentId]/agent-client.tsx +144 -174
  176. package/examples/playground/app/agent/[agentId]/command-receipt.test.tsx +65 -0
  177. package/examples/playground/app/agent/[agentId]/command-receipt.tsx +27 -0
  178. package/examples/playground/app/agent/[agentId]/compaction-event.tsx +25 -24
  179. package/examples/playground/app/agent/[agentId]/compaction-panel.test.tsx +119 -0
  180. package/examples/playground/app/agent/[agentId]/compaction-panel.tsx +208 -165
  181. package/examples/playground/app/agent/[agentId]/page.tsx +2 -16
  182. package/examples/playground/app/agent/events/route.ts +5 -2
  183. package/examples/playground/app/agent/ingress.test.ts +98 -0
  184. package/examples/playground/app/agent/model-input.test.tsx +58 -0
  185. package/examples/playground/app/agent/model-input.tsx +115 -0
  186. package/examples/playground/app/agent/model.test.ts +73 -0
  187. package/examples/playground/app/agent/model.ts +58 -26
  188. package/examples/playground/app/agent/new-agent-session.tsx +37 -19
  189. package/examples/playground/app/agent/page.tsx +4 -4
  190. package/examples/playground/app/agent/server.ts +15 -14
  191. package/examples/playground/app/agent/session.ts +5 -6
  192. package/examples/playground/app/agent/telemetry.test.ts +168 -0
  193. package/examples/playground/app/agent/telemetry.ts +30 -0
  194. package/examples/playground/app/agent/trace.test.ts +23 -0
  195. package/examples/playground/app/agent/trace.ts +18 -0
  196. package/examples/playground/app/canvas/[canvasId]/canvas-client.tsx +11 -11
  197. package/examples/playground/app/canvas/[canvasId]/page.tsx +1 -2
  198. package/examples/playground/app/canvas/[canvasId]/session.ts +2 -2
  199. package/examples/playground/app/canvas/model.ts +53 -53
  200. package/examples/playground/app/chat/[chatId]/agent-state.test.ts +176 -0
  201. package/examples/playground/app/chat/[chatId]/agent-stream-drawer.tsx +16 -1
  202. package/examples/playground/app/chat/[chatId]/chat-client.tsx +55 -77
  203. package/examples/playground/app/chat/[chatId]/chat-view.ts +37 -3
  204. package/examples/playground/app/chat/[chatId]/page.tsx +7 -15
  205. package/examples/playground/app/chat/[chatId]/session.ts +2 -19
  206. package/examples/playground/app/chat/agent/route.ts +9 -0
  207. package/examples/playground/app/chat/model.test.ts +46 -119
  208. package/examples/playground/app/chat/model.ts +82 -128
  209. package/examples/playground/app/chat/scheduler/route.ts +2 -2
  210. package/examples/playground/app/chat/server.test.ts +108 -0
  211. package/examples/playground/app/chat/server.ts +69 -73
  212. package/examples/playground/app/components/activity-feed.tsx +30 -1
  213. package/examples/playground/app/components/activity-history.test.tsx +35 -0
  214. package/examples/playground/app/components/demo-icons.tsx +89 -0
  215. package/examples/playground/app/components/demos.tsx +33 -0
  216. package/examples/playground/app/components/fast-link.test.tsx +58 -0
  217. package/examples/playground/app/components/fast-link.tsx +55 -0
  218. package/examples/playground/app/components/site-header.tsx +96 -0
  219. package/examples/playground/app/counter/counter-client.tsx +3 -3
  220. package/examples/playground/app/counter/model.test.ts +3 -3
  221. package/examples/playground/app/counter/model.ts +11 -21
  222. package/examples/playground/app/counter/page.tsx +2 -15
  223. package/examples/playground/app/counter/session.ts +2 -2
  224. package/examples/playground/app/documents/[documentId]/code-editor.tsx +12 -1
  225. package/examples/playground/app/documents/[documentId]/document-client.tsx +10 -11
  226. package/examples/playground/app/documents/[documentId]/page.tsx +1 -2
  227. package/examples/playground/app/documents/[documentId]/session.ts +2 -2
  228. package/examples/playground/app/documents/model.ts +27 -27
  229. package/examples/playground/app/globals.css +420 -207
  230. package/examples/playground/app/initial-state.test.tsx +54 -0
  231. package/examples/playground/app/layout.tsx +7 -23
  232. package/examples/playground/app/orders/[orderId]/page.tsx +1 -2
  233. package/examples/playground/app/orders/[orderId]/session.ts +2 -2
  234. package/examples/playground/app/orders/model.ts +49 -49
  235. package/examples/playground/app/page.tsx +13 -102
  236. package/examples/playground/app/recovery/[recoveryId]/page.tsx +1 -2
  237. package/examples/playground/app/recovery/[recoveryId]/session.ts +2 -2
  238. package/examples/playground/app/recovery/model.ts +30 -30
  239. package/examples/playground/instrumentation.ts +5 -0
  240. package/examples/playground/package.json +10 -2
  241. package/examples/playground/scripts/a0-template.ts +77 -0
  242. package/examples/playground/vercel.json +18 -0
  243. package/package.json +3 -1
  244. package/src/actor-client.ts +16 -4
  245. package/src/actor-react.ts +25 -11
  246. package/src/actor-shared.ts +21 -6
  247. package/src/actor.ts +76 -31
  248. package/src/ai-agent-actions.ts +219 -0
  249. package/src/ai-agent.ts +2343 -0
  250. package/src/ai-app-state.ts +91 -0
  251. package/src/ai-client-state.ts +6 -3
  252. package/src/ai-client.ts +62 -0
  253. package/src/ai-context-schema.ts +25 -5
  254. package/src/ai-context.ts +90 -40
  255. package/src/ai-control-server.ts +92 -72
  256. package/src/ai-coordinator.ts +23 -22
  257. package/src/ai-message-base.ts +209 -0
  258. package/src/ai-message-projection.ts +262 -64
  259. package/src/ai-progress-batches.ts +26 -0
  260. package/src/ai-projector.ts +95 -14
  261. package/src/ai-react.ts +146 -0
  262. package/src/ai-server.browser.ts +2 -2
  263. package/src/ai-server.ts +20 -1964
  264. package/src/ai-stored-state.ts +48 -163
  265. package/src/ai.ts +339 -155
  266. package/src/bootstrap-stream.ts +72 -0
  267. package/src/bounded-cache.ts +55 -0
  268. package/src/client-render-session.ts +49 -0
  269. package/src/client.ts +688 -151
  270. package/src/contract.ts +92 -42
  271. package/src/deterministic-id.ts +37 -0
  272. package/src/equal-json.ts +32 -0
  273. package/src/errors.ts +13 -1
  274. package/src/index.ts +5 -1
  275. package/src/initial-snapshot.ts +39 -0
  276. package/src/internal.ts +45 -0
  277. package/src/postgres-notification-scope.ts +14 -3
  278. package/src/react.ts +171 -206
  279. package/src/redis-http-reply.ts +256 -0
  280. package/src/redis-http-stream.ts +73 -0
  281. package/src/redis-http-subscriptions.ts +13 -5
  282. package/src/reducer-runtime.ts +52 -0
  283. package/src/reducer.ts +92 -10
  284. package/src/scheduler-qstash.ts +49 -20
  285. package/src/scheduler-task.ts +34 -2
  286. package/src/scheduler-vercel.ts +32 -84
  287. package/src/server-fetch.ts +139 -35
  288. package/src/server.ts +957 -237
  289. package/src/session-clear.ts +23 -0
  290. package/src/session-socket.ts +154 -49
  291. package/src/sse.ts +71 -4
  292. package/src/store-memory.ts +220 -78
  293. package/src/store-polling.ts +18 -4
  294. package/src/store-postgres.ts +461 -114
  295. package/src/store-redis-core.ts +1260 -434
  296. package/src/store-redis-http.ts +18 -2
  297. package/src/store-redis-notify.ts +260 -85
  298. package/src/store-redis.ts +19 -9
  299. package/src/store-sqlite.ts +259 -45
  300. package/src/store.ts +46 -1
  301. package/src/stream-buffer.ts +43 -0
  302. package/src/telemetry.ts +13 -1
  303. package/src/wire.ts +241 -13
  304. package/dist/actor-ohPC-81x.d.ts.map +0 -1
  305. package/dist/actor-shared-B5tJfzt-.js.map +0 -1
  306. package/dist/ai-CrEf6p_W.d.ts.map +0 -1
  307. package/dist/ai-DgOBltJ_.js.map +0 -1
  308. package/dist/client-BYzHjkwU.js.map +0 -1
  309. package/dist/client-CzyacQpJ.d.ts.map +0 -1
  310. package/dist/contract-CKRg_E4q.js.map +0 -1
  311. package/dist/errors-DCk6ch5n.js.map +0 -1
  312. package/dist/errors-DvhSXnxk.d.ts.map +0 -1
  313. package/dist/internal-Dq2qYxou.js.map +0 -1
  314. package/dist/react.d.ts.map +0 -1
  315. package/dist/reducer-DEMjEY_O.js +0 -29
  316. package/dist/reducer-DEMjEY_O.js.map +0 -1
  317. package/dist/reducer-otzHjuJj.d.ts.map +0 -1
  318. package/dist/scheduler-task-BpzhPnRS.js.map +0 -1
  319. package/dist/server-BD5ckxZb.js.map +0 -1
  320. package/dist/server-DbD7IJVK.d.ts.map +0 -1
  321. package/dist/store-DyZM6fS5.d.ts.map +0 -1
  322. package/dist/store-polling-CmxUbV93.js.map +0 -1
  323. package/dist/store-redis-notify-BVLUfI3j.js +0 -1583
  324. package/dist/store-redis-notify-BVLUfI3j.js.map +0 -1
  325. package/dist/telemetry-B5jzpy6y.d.ts.map +0 -1
  326. package/dist/validate-XKT4FSNn.js +0 -30
  327. package/dist/validate-XKT4FSNn.js.map +0 -1
  328. package/dist/wire-BO5wWCb1.js.map +0 -1
  329. package/examples/playground/app/agent/[agentId]/compaction/route.ts +0 -14
  330. package/examples/playground/app/agent/compaction-settings.test.ts +0 -144
  331. package/examples/playground/app/agent/compaction-settings.ts +0 -49
  332. package/examples/playground/app/agent/compaction-timeline.test.ts +0 -377
  333. package/examples/playground/app/agent/compaction-timeline.ts +0 -208
@@ -0,0 +1,69 @@
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 { agentServer } 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.
42
+ const { state } = await agentServer.session(appId).state()
43
+ if (state.messages.length === 0) await deleteAppSandbox({ appId })
44
+ },
45
+ },
46
+ })
47
+
48
+ /**
49
+ * Schedules the check for a new app's sandbox. It's keyed to the sandbox's
50
+ * creation, so repeating it converges on one check, and a sandbox made
51
+ * again later gets its own. Without a scheduler, as under next dev, the
52
+ * sandbox just waits for its session to time out.
53
+ */
54
+ export async function checkWhenIdle({
55
+ appId,
56
+ createdAt,
57
+ }: {
58
+ appId: string
59
+ createdAt: Date
60
+ }): Promise<void> {
61
+ if (!sandboxScheduler || !isAppId(appId)) return
62
+ await sandboxServer
63
+ .session(sandboxLogId(appId))
64
+ .schedule(
65
+ `idle-check-${createdAt.getTime()}`,
66
+ { at: new Date(createdAt.getTime() + IDLE_GRACE_MS) },
67
+ { type: 'sandbox.idle.checked', payload: { appId } },
68
+ )
69
+ }
@@ -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, appScheduler } from '../server'
2
+
3
+ export const POST =
4
+ appScheduler?.handler(agentServer) ??
5
+ (() => new Response('a0 scheduler is not configured', { status: 503 }))
6
+
7
+ export const maxDuration = 300
@@ -0,0 +1,158 @@
1
+ import 'server-only'
2
+ import { waitUntil } from '@vercel/functions'
3
+ import { tool } from 'ai'
4
+ import { handlerContext } from 'experimental-a2/ai'
5
+ import { agent } from 'experimental-a2/ai/server'
6
+ import { createServer } from 'experimental-a2/server'
7
+ import { vercelQueues } from 'experimental-a2/scheduler-vercel'
8
+ import { z } from 'zod'
9
+ import { store } from '@/lib/store'
10
+ import { appList, isAppId } from './apps'
11
+ import { crashOnRequest } from './crash-on-request'
12
+ import { crashLog, crashLogId, isCrashLogId } from './crashes'
13
+ import { appAgent, appPort } from './model'
14
+ import {
15
+ appDir,
16
+ appSandbox,
17
+ bash,
18
+ previewUrl,
19
+ START_DEV,
20
+ workspace,
21
+ } from './sandbox'
22
+
23
+ const INSTRUCTIONS = `You are a coding agent that builds a Next.js app in your own Linux sandbox, through the bash tool. The user watches the app in a live preview next to this chat.
24
+ - ${appDir} already holds a blank Next.js app (App Router, TypeScript, Tailwind) with its dependencies installed, and next dev serves it on port ${appPort} with hot reload. It may still be starting when you begin: don't wait for it, poll it, or start another one. Build what the user asks for there; don't create another app.
25
+ - Until you change them, app/page.tsx renders an empty <main />, app/layout.tsx only sets the title, and app/globals.css only imports Tailwind. Write them directly; there's no need to look first.
26
+ - Assume your edits work. Don't curl the dev server, sleep, or read the page back to check, and don't run builds, type checks or lint unless the user asks.
27
+ - When the change is ready, call show_preview once, as your last step, so the user sees the result; until then they see no app. After that first time, saved files show up in their preview on their own, so call it again only to show a different path.
28
+ - If the preview stops answering, read /tmp/next.log, then start next dev with \`${START_DEV}\`; it does nothing while one is already running.
29
+ - next.config allows the preview's *.vercel.run host (allowedDevOrigins); keep that when you edit it.
30
+ - Background anything long-running; a bash call stops after its timeout.
31
+ - Keep replies short and in plain text, without Markdown: what you changed.`
32
+
33
+ /** A tool's result, with how long it took; the chat shows it. */
34
+ async function timed<T extends object>(
35
+ run: () => Promise<T>,
36
+ ): Promise<T & { durationMs: number }> {
37
+ const started = performance.now()
38
+ const result = await run()
39
+ return { ...result, durationMs: Math.round(performance.now() - started) }
40
+ }
41
+
42
+ const tools = {
43
+ show_preview: tool({
44
+ description: `Show the user the app, as the dev server on port ${appPort} serves it, next to this chat. It doesn't check or start the server. Returns the public URL.`,
45
+ inputSchema: z.object({
46
+ path: z.string().startsWith('/').default('/'),
47
+ }),
48
+ execute: ({ path }) =>
49
+ timed(async () =>
50
+ previewUrl({
51
+ sandbox: await appSandbox({
52
+ appId: handlerContext(appAgent).session.id,
53
+ }),
54
+ path,
55
+ }),
56
+ ),
57
+ }),
58
+ bash: tool({
59
+ description: `Run a shell command in ${workspace}. Files persist between commands. Background long-running servers.`,
60
+ inputSchema: z.object({
61
+ command: z.string().min(1),
62
+ // Within one function invocation; see maxDuration on the routes.
63
+ timeoutSeconds: z.number().int().min(1).max(240).default(120),
64
+ }),
65
+ execute: ({ command, timeoutSeconds }) => {
66
+ const context = handlerContext(appAgent)
67
+ return timed(async () =>
68
+ bash({
69
+ sandbox: await appSandbox({ appId: context.session.id }),
70
+ command,
71
+ timeoutSeconds,
72
+ signal: context.signal,
73
+ }),
74
+ )
75
+ },
76
+ }),
77
+ }
78
+
79
+ /**
80
+ * Wakes agent work in a fresh invocation when the one that started it ends,
81
+ * and recovers from crashed ones.
82
+ */
83
+ export const appScheduler =
84
+ process.env.VERCEL || process.env.A2_QUEUES
85
+ ? vercelQueues({ topic: 'a2-a0' })
86
+ : undefined
87
+
88
+ // Each step runs with the app's model choice. The browser pushes it, and the
89
+ // contract's schema checks it on the way in.
90
+ export const agentServer = agent({
91
+ ...(store ? { store } : {}),
92
+ ...(appScheduler ? { scheduler: appScheduler } : {}),
93
+ contract: appAgent,
94
+ model: ({ event, state }) => {
95
+ // Every step starts here, in the process that runs it, which is what
96
+ // the dev tools' crash has to end.
97
+ if (crashesAllowed) {
98
+ const appId = event.sessionId
99
+ waitUntil(
100
+ crashOnRequest({
101
+ appId,
102
+ app: agentServer.session(appId),
103
+ log: crashServer.session(crashLogId(appId)),
104
+ }).catch(() => {}),
105
+ )
106
+ }
107
+ const { model, reasoning, fast } = state.modelChoice
108
+ return { model, reasoning, speed: fast ? 'fast' : 'standard' }
109
+ },
110
+ generation: () => ({
111
+ // Claude leaves its thinking's text out unless asked for a summary.
112
+ providerOptions: {
113
+ anthropic: { thinking: { type: 'adaptive', display: 'summarized' } },
114
+ },
115
+ }),
116
+ instructions: INSTRUCTIONS,
117
+ tools,
118
+ maxSteps: 80,
119
+ })
120
+
121
+ /** The app's event stream and pushes; only an app id names a session. */
122
+ export function appEvents(request: Request): Promise<Response> {
123
+ return agentServer.fetch(request, {
124
+ // The agent refuses the AI protocol's server-authored events itself.
125
+ authorize: (operation) => isAppId(operation.sessionId),
126
+ })
127
+ }
128
+
129
+ /** The apps sidebar's lists, which only the browser writes. */
130
+ export const listServer = createServer({
131
+ ...(store ? { store } : {}),
132
+ contract: appList,
133
+ })
134
+
135
+ /** The dev tools' crash logs, one per app. They have no handlers. */
136
+ export const crashServer = createServer({
137
+ ...(store ? { store } : {}),
138
+ contract: crashLog,
139
+ })
140
+
141
+ /**
142
+ * A crash ends the function instance, and anything else it's serving, so
143
+ * only previews allow it, behind Deployment Protection, unless A0_CRASHES
144
+ * is on. Under next dev it would take the dev server down.
145
+ */
146
+ export const crashesAllowed =
147
+ process.env.VERCEL_ENV === 'preview' || process.env.A0_CRASHES === 'on'
148
+
149
+ /** The browser can only ask for a crash; the process it ends records it. */
150
+ export function crashEvents(request: Request): Promise<Response> {
151
+ return crashServer.fetch(request, {
152
+ authorize: (operation) =>
153
+ isCrashLogId(operation.sessionId) &&
154
+ (crashesAllowed || operation.type !== 'push') &&
155
+ (operation.type !== 'push' ||
156
+ operation.events.every((event) => event.type === 'crash.requested')),
157
+ })
158
+ }
@@ -0,0 +1,47 @@
1
+ 'use client'
2
+ import { createClient } from 'experimental-a2/client'
3
+ import { createReact } from 'experimental-a2/react'
4
+ import { appList } from './apps'
5
+ import { crashLog } from './crashes'
6
+ import { appAgent } from './model'
7
+
8
+ /** The dev tools' switch, `?limit=5s`, for the app at this URL, while they're open. */
9
+ export function shortFunctions(appId: string): boolean {
10
+ const params = new URLSearchParams(location.search)
11
+ return (
12
+ location.pathname === `/a0/${appId}` &&
13
+ params.has('devtools') &&
14
+ params.get('limit') === '5s'
15
+ )
16
+ }
17
+
18
+ /**
19
+ * Follows an app's conversation log over the events route. Pushes go to a
20
+ * 5-second function instead while the dev tools' switch is on.
21
+ */
22
+ export const appClient = createClient({
23
+ contract: appAgent,
24
+ api: {
25
+ type: 'http',
26
+ stream: '/a0/events',
27
+ push: (appId) =>
28
+ shortFunctions(appId) ? '/a0/short/events' : '/a0/events',
29
+ },
30
+ })
31
+
32
+ export const { SessionProvider, useSession } = createReact({
33
+ client: appClient,
34
+ })
35
+
36
+ /** Follows this browser's app list. */
37
+ export const listClient = createClient({ contract: appList, api: '/a0/list' })
38
+
39
+ export const { useSession: useList } = createReact({ client: listClient })
40
+
41
+ /** Follows an app's crash log, for the dev tools. */
42
+ export const crashClient = createClient({
43
+ contract: crashLog,
44
+ api: '/a0/crashes',
45
+ })
46
+
47
+ export const { useSession: useCrashLog } = createReact({ client: crashClient })
@@ -0,0 +1,150 @@
1
+ 'use client'
2
+ import type { ReactNode } from 'react'
3
+ import { createContext, useContext, useEffect, useState } from 'react'
4
+ import { usePathname, useRouter, useSearchParams } from 'next/navigation'
5
+ import { SiteNav } from '../components/site-header'
6
+ import { type App, byLatestPrompt } from './apps'
7
+ import { AppList } from './app-list'
8
+ import { ColumnEdge, Columns } from './columns'
9
+ import { listCookie, readCookie, type Widths, writeCookie } from './cookies'
10
+ import { toggleDevtools } from './flags'
11
+ import { useList } from './session'
12
+
13
+ type Apps = {
14
+ listed: (id: string) => App | null
15
+ /** Records a prompt to an app, listing it under `name` the first time. */
16
+ prompted: (app: { id: string; name: string }) => void
17
+ /** Makes a new app on screen the one "+" opens until it's prompted. */
18
+ opened: (appId: string) => void
19
+ }
20
+
21
+ const AppsContext = createContext<Apps | null>(null)
22
+
23
+ export function useApps(): Apps {
24
+ const apps = useContext(AppsContext)
25
+ if (!apps) throw new Error('useApps needs the A0 layout.')
26
+ return apps
27
+ }
28
+
29
+ type ListSeed = NonNullable<Parameters<typeof useList>[0]>['initial']
30
+
31
+ const idFromPath = (pathname: string): string | null =>
32
+ pathname.split('/')[2] || null
33
+
34
+ /** A0's frame: this browser's apps, beside whichever app is open. */
35
+ export function A0Shell({
36
+ list,
37
+ widths,
38
+ renderedAt,
39
+ newAppId,
40
+ children,
41
+ }: {
42
+ list: { id: string; initial: ListSeed }
43
+ widths: Widths
44
+ renderedAt: number
45
+ /** A new app is an id with no log yet. */
46
+ newAppId: string
47
+ children: ReactNode
48
+ }): ReactNode {
49
+ const router = useRouter()
50
+ const current = idFromPath(usePathname())
51
+ const devtools = useSearchParams().has('devtools')
52
+ const { state: apps, push } = useList({
53
+ sessionId: list.id,
54
+ initial: list.initial,
55
+ })
56
+ const [listError, setListError] = useState<string | null>(null)
57
+ // The new app "+" opens: the one last on screen, so its sandbox is likely
58
+ // warm, until it's prompted.
59
+ const [ready, setReady] = useState(newAppId)
60
+
61
+ useEffect(() => {
62
+ const toggle = (event: KeyboardEvent): void => {
63
+ if (event.key.toLowerCase() !== 'j' || !(event.metaKey || event.ctrlKey))
64
+ return
65
+ event.preventDefault()
66
+ toggleDevtools()
67
+ }
68
+ document.addEventListener('keydown', toggle)
69
+ return () => document.removeEventListener('keydown', toggle)
70
+ }, [])
71
+
72
+ const write = (...event: Parameters<typeof push>): void => {
73
+ // The list is this browser's from its first write.
74
+ if (!readCookie(listCookie))
75
+ writeCookie({ name: listCookie, value: list.id })
76
+ setListError(null)
77
+ // A failed write has already been rolled back out of the list.
78
+ push(...event).catch((cause: unknown) =>
79
+ setListError(
80
+ `Couldn't save that: ${cause instanceof Error ? cause.message : String(cause)}`,
81
+ ),
82
+ )
83
+ }
84
+ const listed = (id: string): App | null =>
85
+ apps.find((app) => app.id === id) ?? null
86
+ const context: Apps = {
87
+ listed,
88
+ prompted: ({ id, name }) => {
89
+ if (listed(id)) {
90
+ write({ type: 'app.prompted', payload: { appId: id } })
91
+ return
92
+ }
93
+ write({ type: 'app.created', payload: { appId: id, name } })
94
+ if (id === ready) setReady(crypto.randomUUID())
95
+ },
96
+ opened: setReady,
97
+ }
98
+ const remove = async (target: App): Promise<void> => {
99
+ if (
100
+ !confirm(
101
+ `Delete ${target.name}? This deletes its sandbox, with everything the agent built in it.`,
102
+ )
103
+ )
104
+ return
105
+ const response = await fetch(`/a0/apps/${target.id}`, { method: 'DELETE' })
106
+ if (!response.ok) {
107
+ alert(`Could not delete ${target.name}.`)
108
+ return
109
+ }
110
+ write({ type: 'app.deleted', payload: { appId: target.id } })
111
+ if (target.id === current) router.replace(`/a0/${ready}${location.search}`)
112
+ }
113
+
114
+ return (
115
+ <AppsContext value={context}>
116
+ <Columns saved={widths} panel={devtools}>
117
+ <aside aria-label="A0" className="a0-apps">
118
+ <div className="site-header a0-head">
119
+ <SiteNav />
120
+ </div>
121
+ <AppList
122
+ apps={byLatestPrompt(apps)}
123
+ currentId={current}
124
+ renderedAt={renderedAt}
125
+ newAppId={ready}
126
+ onRename={({ id, name }) =>
127
+ write({ type: 'app.renamed', payload: { appId: id, name } })
128
+ }
129
+ onDelete={(target) => void remove(target)}
130
+ />
131
+ {listError && (
132
+ <p role="alert" className="a0-error a0-list-error">
133
+ {listError}
134
+ </p>
135
+ )}
136
+ <button
137
+ type="button"
138
+ className="a0-devtools-toggle"
139
+ aria-pressed={devtools}
140
+ onClick={toggleDevtools}
141
+ >
142
+ Dev tools <kbd>⌘J</kbd>
143
+ </button>
144
+ <ColumnEdge column="apps" />
145
+ </aside>
146
+ {children}
147
+ </Columns>
148
+ </AppsContext>
149
+ )
150
+ }
@@ -0,0 +1,9 @@
1
+ import { appEvents } from '../../server'
2
+
3
+ /**
4
+ * Pushes from the dev tools' 5-second switch. Agent work they start stops
5
+ * with the function, and the scheduler resumes it in a full-length one.
6
+ */
7
+ export { appEvents as POST }
8
+
9
+ export const maxDuration = 5