experimental-a2 0.16.0 → 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 (334) hide show
  1. package/CHANGELOG.md +360 -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-CrLYwNEx.js → ai-UrWfKeuv.js} +966 -541
  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 +466 -265
  32. package/dist/ai-server.js.map +1 -1
  33. package/dist/{ai-HJ9fHfYI.d.ts → ai-vpGtcaCB.d.ts} +115 -101
  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 +680 -580
  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 +662 -195
  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 +39 -2
  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 +94 -55
  256. package/src/ai-control.ts +2 -0
  257. package/src/ai-coordinator.ts +23 -22
  258. package/src/ai-message-base.ts +209 -0
  259. package/src/ai-message-projection.ts +382 -58
  260. package/src/ai-progress-batches.ts +26 -0
  261. package/src/ai-projector.ts +95 -14
  262. package/src/ai-react.ts +146 -0
  263. package/src/ai-server.browser.ts +2 -2
  264. package/src/ai-server.ts +20 -1951
  265. package/src/ai-stored-state.ts +48 -163
  266. package/src/ai.ts +343 -276
  267. package/src/bootstrap-stream.ts +72 -0
  268. package/src/bounded-cache.ts +55 -0
  269. package/src/client-render-session.ts +49 -0
  270. package/src/client.ts +688 -151
  271. package/src/contract.ts +92 -42
  272. package/src/deterministic-id.ts +37 -0
  273. package/src/equal-json.ts +32 -0
  274. package/src/errors.ts +13 -1
  275. package/src/index.ts +5 -1
  276. package/src/initial-snapshot.ts +39 -0
  277. package/src/internal.ts +45 -0
  278. package/src/postgres-notification-scope.ts +14 -3
  279. package/src/react.ts +171 -206
  280. package/src/redis-http-reply.ts +256 -0
  281. package/src/redis-http-stream.ts +73 -0
  282. package/src/redis-http-subscriptions.ts +13 -5
  283. package/src/reducer-runtime.ts +52 -0
  284. package/src/reducer.ts +92 -10
  285. package/src/scheduler-qstash.ts +49 -20
  286. package/src/scheduler-task.ts +34 -2
  287. package/src/scheduler-vercel.ts +32 -84
  288. package/src/server-fetch.ts +139 -35
  289. package/src/server.ts +957 -237
  290. package/src/session-clear.ts +23 -0
  291. package/src/session-socket.ts +154 -49
  292. package/src/sse.ts +71 -4
  293. package/src/store-memory.ts +220 -78
  294. package/src/store-polling.ts +18 -4
  295. package/src/store-postgres.ts +461 -114
  296. package/src/store-redis-core.ts +1260 -434
  297. package/src/store-redis-http.ts +18 -2
  298. package/src/store-redis-notify.ts +260 -85
  299. package/src/store-redis.ts +19 -9
  300. package/src/store-sqlite.ts +259 -45
  301. package/src/store.ts +46 -1
  302. package/src/stream-buffer.ts +43 -0
  303. package/src/telemetry.ts +13 -1
  304. package/src/wire.ts +241 -13
  305. package/dist/actor-ohPC-81x.d.ts.map +0 -1
  306. package/dist/actor-shared-B5tJfzt-.js.map +0 -1
  307. package/dist/ai-CrLYwNEx.js.map +0 -1
  308. package/dist/ai-HJ9fHfYI.d.ts.map +0 -1
  309. package/dist/client-BYzHjkwU.js.map +0 -1
  310. package/dist/client-CzyacQpJ.d.ts.map +0 -1
  311. package/dist/contract-CKRg_E4q.js.map +0 -1
  312. package/dist/errors-DCk6ch5n.js.map +0 -1
  313. package/dist/errors-DvhSXnxk.d.ts.map +0 -1
  314. package/dist/internal-Dq2qYxou.js.map +0 -1
  315. package/dist/react.d.ts.map +0 -1
  316. package/dist/reducer-DEMjEY_O.js +0 -29
  317. package/dist/reducer-DEMjEY_O.js.map +0 -1
  318. package/dist/reducer-otzHjuJj.d.ts.map +0 -1
  319. package/dist/scheduler-task-BpzhPnRS.js.map +0 -1
  320. package/dist/server-BD5ckxZb.js.map +0 -1
  321. package/dist/server-DbD7IJVK.d.ts.map +0 -1
  322. package/dist/store-DyZM6fS5.d.ts.map +0 -1
  323. package/dist/store-polling-CmxUbV93.js.map +0 -1
  324. package/dist/store-redis-notify-BVLUfI3j.js +0 -1583
  325. package/dist/store-redis-notify-BVLUfI3j.js.map +0 -1
  326. package/dist/telemetry-B5jzpy6y.d.ts.map +0 -1
  327. package/dist/validate-XKT4FSNn.js +0 -30
  328. package/dist/validate-XKT4FSNn.js.map +0 -1
  329. package/dist/wire-BO5wWCb1.js.map +0 -1
  330. package/examples/playground/app/agent/[agentId]/compaction/route.ts +0 -14
  331. package/examples/playground/app/agent/compaction-settings.test.ts +0 -144
  332. package/examples/playground/app/agent/compaction-settings.ts +0 -49
  333. package/examples/playground/app/agent/compaction-timeline.test.ts +0 -377
  334. package/examples/playground/app/agent/compaction-timeline.ts +0 -208
@@ -0,0 +1,3 @@
1
+ import { crashEvents } from '../server'
2
+
3
+ export { crashEvents as GET, crashEvents as POST }
@@ -0,0 +1,33 @@
1
+ import type { ContractEvent } from 'experimental-a2'
2
+ import { expect, it } from 'vitest'
3
+ import { crashLog, crashLogId, isCrashLogId } from './crashes'
4
+
5
+ type CrashEvents = typeof crashLog.events
6
+
7
+ const pending = (...types: (keyof CrashEvents & string)[]): boolean =>
8
+ types.reduce(
9
+ (state, type, index) =>
10
+ crashLog.reducer.fold(state, {
11
+ id: `event-${index}`,
12
+ type,
13
+ payload: {},
14
+ index: index + 1,
15
+ sessionId: 'log-1',
16
+ createdAt: new Date(index * 1000),
17
+ } as ContractEvent<CrashEvents>),
18
+ crashLog.reducer.initialState,
19
+ )
20
+
21
+ it('holds a crash from its request until it happens', () => {
22
+ expect(pending()).toBe(false)
23
+ expect(pending('crash.requested')).toBe(true)
24
+ expect(pending('crash.requested', 'crashed')).toBe(false)
25
+ expect(pending('crash.requested', 'crashed', 'crash.requested')).toBe(true)
26
+ })
27
+
28
+ it("names one crash log per app, and only an app's", () => {
29
+ const appId = '6f1c1c3e-2f7a-4f0e-9d4e-5b8a9f1d2c3b'
30
+ expect(isCrashLogId(crashLogId(appId))).toBe(true)
31
+ expect(isCrashLogId(appId)).toBe(false)
32
+ expect(isCrashLogId('not-a-uuid:crashes')).toBe(false)
33
+ })
@@ -0,0 +1,28 @@
1
+ import { contract } from 'experimental-a2'
2
+ import { z } from 'zod'
3
+ import { isAppId } from './apps'
4
+
5
+ /**
6
+ * The dev tools' crashes for one app, in a log of their own so the app's
7
+ * log stays its conversation: a request, then the crash that carried it
8
+ * out. The state is whether one is still pending.
9
+ */
10
+ export const crashLog = contract({
11
+ name: 'a0-crashes',
12
+ events: {
13
+ 'crash.requested': z.object({}),
14
+ crashed: z.object({}),
15
+ },
16
+ reducer: {
17
+ name: 'a0-crashes.v1',
18
+ initialState: false as boolean,
19
+ fold: (_pending, event) => event.type === 'crash.requested',
20
+ },
21
+ })
22
+
23
+ const suffix = ':crashes'
24
+
25
+ export const crashLogId = (appId: string): string => `${appId}${suffix}`
26
+
27
+ export const isCrashLogId = (id: string): boolean =>
28
+ id.endsWith(suffix) && isAppId(id.slice(0, -suffix.length))
@@ -0,0 +1,164 @@
1
+ 'use client'
2
+ import type { ReactNode } from 'react'
3
+ import { memo, useEffect, useState } from 'react'
4
+ import { useSearchParams } from 'next/navigation'
5
+ import { X, Zap } from 'lucide-react'
6
+ import { crashLogId } from './crashes'
7
+ import { setFlag, toggleDevtools } from './flags'
8
+ import { logPage } from './model'
9
+ import { useCrashLog, useSession } from './session'
10
+
11
+ type LogEvent = ReturnType<typeof useSession>['events'][number]
12
+
13
+ const gap = (ms: number): string =>
14
+ ms < 1000 ? `+${Math.round(ms)}ms` : `+${(ms / 1000).toFixed(2)}s`
15
+
16
+ /**
17
+ * The app's raw A2 log, and ways to break the function running its agent:
18
+ * a crash, or a 5-second time limit, so the recovery is there to watch.
19
+ */
20
+ export function DevTools({
21
+ appId,
22
+ deployed,
23
+ crashesAllowed,
24
+ }: {
25
+ appId: string
26
+ /** Function limits and crashes only mean something on Vercel. */
27
+ deployed: boolean
28
+ crashesAllowed: boolean
29
+ }): ReactNode {
30
+ const { state, events, index, history, hydrated, loadHistory } = useSession()
31
+ const short = useSearchParams().get('limit') === '5s'
32
+ const crashes = useCrashLog({ sessionId: crashLogId(appId) })
33
+ const [error, setError] = useState<string | null>(null)
34
+
35
+ const load = (options: { before?: number }): void => {
36
+ loadHistory({ ...options, limit: logPage }).catch((cause: unknown) =>
37
+ setError(cause instanceof Error ? cause.message : String(cause)),
38
+ )
39
+ }
40
+ // Opening reads the latest page. The client skips what it already holds,
41
+ // such as the server's seed, so this reads nothing when there's one.
42
+ useEffect(() => {
43
+ if (hydrated && !history.complete) load({ before: index + 1 })
44
+ }, [hydrated])
45
+
46
+ const crash = (): void => {
47
+ setError(null)
48
+ crashes
49
+ .push({ type: 'crash.requested', payload: {} })
50
+ .catch((cause: unknown) =>
51
+ setError(cause instanceof Error ? cause.message : String(cause)),
52
+ )
53
+ }
54
+
55
+ const localOnly = deployed ? undefined : "next dev doesn't limit functions"
56
+ return (
57
+ <section aria-label="Dev tools" className="a0-devtools">
58
+ <header className="a0-devtools-bar">
59
+ <span className="a0-devtools-title">
60
+ Log <span>{events.length} events</span>
61
+ </span>
62
+ {error && (
63
+ <span role="alert" className="a0-error">
64
+ {error}
65
+ </span>
66
+ )}
67
+ <label className="a0-devtools-switch" title={localOnly}>
68
+ <input
69
+ type="checkbox"
70
+ checked={short}
71
+ disabled={!deployed}
72
+ onChange={(event) =>
73
+ setFlag({
74
+ name: 'limit',
75
+ value: event.currentTarget.checked ? '5s' : null,
76
+ })
77
+ }
78
+ />
79
+ 5-second functions
80
+ </label>
81
+ {/* A crash only means something while the agent works. */}
82
+ {state.active && (
83
+ <button
84
+ type="button"
85
+ className="a0-ghost a0-devtools-crash"
86
+ disabled={!crashesAllowed || crashes.state}
87
+ title={
88
+ crashesAllowed
89
+ ? 'Ends the process running the agent, right now'
90
+ : 'Crashes need a preview deployment, or A0_CRASHES=on'
91
+ }
92
+ onClick={crash}
93
+ >
94
+ <Zap size={13} aria-hidden />
95
+ {crashes.state ? 'Crashing…' : 'Crash'}
96
+ </button>
97
+ )}
98
+ <button
99
+ type="button"
100
+ className="a0-icon-button"
101
+ aria-label="Close dev tools"
102
+ title="Close (⌘J)"
103
+ onClick={toggleDevtools}
104
+ >
105
+ <X size={14} />
106
+ </button>
107
+ </header>
108
+ {/* Reversed, so the view stays pinned to the newest event. */}
109
+ <div className="a0-log-scroll">
110
+ <ol className="a0-log">
111
+ {!history.complete && (
112
+ <li>
113
+ <button
114
+ type="button"
115
+ className="a0-ghost a0-log-older"
116
+ disabled={history.loading}
117
+ onClick={() => load({})}
118
+ >
119
+ {history.loading ? 'Loading…' : 'Load older'}
120
+ </button>
121
+ </li>
122
+ )}
123
+ {events.map((event, position) => (
124
+ <LogRow
125
+ key={event.id}
126
+ event={event}
127
+ previous={events[position - 1]}
128
+ />
129
+ ))}
130
+ </ol>
131
+ </div>
132
+ </section>
133
+ )
134
+ }
135
+
136
+ // Rows keep their events' identity, so a new event renders only its own row.
137
+ const LogRow = memo(function LogRow({
138
+ event,
139
+ previous,
140
+ }: {
141
+ event: LogEvent
142
+ previous: LogEvent | undefined
143
+ }): ReactNode {
144
+ const payload = JSON.stringify(event.payload)
145
+ return (
146
+ <li>
147
+ <details className="a0-log-row">
148
+ <summary title={event.createdAt.toISOString()}>
149
+ <span className="a0-log-index">{event.index}</span>
150
+ <span className="a0-log-gap">
151
+ {previous
152
+ ? gap(event.createdAt.getTime() - previous.createdAt.getTime())
153
+ : ''}
154
+ </span>
155
+ <span className="a0-log-type">{event.type}</span>
156
+ <code>
157
+ {payload.length > 240 ? `${payload.slice(0, 240)}…` : payload}
158
+ </code>
159
+ </summary>
160
+ <pre>{JSON.stringify(event.payload, null, 2)}</pre>
161
+ </details>
162
+ </li>
163
+ )
164
+ })
@@ -0,0 +1,6 @@
1
+ import { appEvents } from '../server'
2
+
3
+ export { appEvents as GET, appEvents as POST }
4
+
5
+ // Agent work started by a push runs after the response, up to this long.
6
+ export const maxDuration = 300
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The dev tools' state lives in the query: `?devtools` while they're open,
3
+ * and `limit=5s` for the 5-second switch. Setting a flag is browser-only.
4
+ */
5
+
6
+ /** A query string for a URL, with bare flags as `?devtools`, not `?devtools=`. */
7
+ export function queryString(params: URLSearchParams): string {
8
+ const query = params.toString().replace(/=(?=&|$)/g, '')
9
+ return query && `?${query}`
10
+ }
11
+
12
+ /** Sets or clears one flag, in place, without a navigation. */
13
+ export function setFlag({
14
+ name,
15
+ value,
16
+ }: {
17
+ name: 'devtools' | 'limit'
18
+ value: string | null
19
+ }): void {
20
+ const params = new URLSearchParams(location.search)
21
+ if (value === null) params.delete(name)
22
+ else params.set(name, value)
23
+ history.replaceState(null, '', `${location.pathname}${queryString(params)}`)
24
+ }
25
+
26
+ export const toggleDevtools = (): void =>
27
+ setFlag({
28
+ name: 'devtools',
29
+ value: new URLSearchParams(location.search).has('devtools') ? null : '',
30
+ })
@@ -0,0 +1,38 @@
1
+ import type { Metadata } from 'next'
2
+ import type { ReactNode } from 'react'
3
+ import { cookies } from 'next/headers'
4
+ import { appList, isAppId } from './apps'
5
+ import { listCookie, parseWidths, widthsCookie } from './cookies'
6
+ import { listServer } from './server'
7
+ import { A0Shell } from './shell'
8
+ import './a0.css'
9
+
10
+ export const metadata: Metadata = { title: 'A0 · a2 example' }
11
+
12
+ /** The apps list and the columns stay put while the app beside them changes. */
13
+ export default async function A0Layout({
14
+ children,
15
+ }: {
16
+ children: ReactNode
17
+ }): Promise<ReactNode> {
18
+ const jar = await cookies()
19
+ const saved = jar.get(listCookie)?.value
20
+ const listId = saved && isAppId(saved) ? saved : null
21
+ const list = listId
22
+ ? { id: listId, initial: await listServer.session(listId).state() }
23
+ : {
24
+ // Becomes this browser's list with its first write.
25
+ id: crypto.randomUUID(),
26
+ initial: { state: appList.reducer.initialState, index: 0 },
27
+ }
28
+ return (
29
+ <A0Shell
30
+ list={list}
31
+ widths={parseWidths(jar.get(widthsCookie)?.value)}
32
+ renderedAt={Date.now()}
33
+ newAppId={crypto.randomUUID()}
34
+ >
35
+ {children}
36
+ </A0Shell>
37
+ )
38
+ }
@@ -0,0 +1,10 @@
1
+ import { isAppId } from '../apps'
2
+ import { listServer } from '../server'
3
+
4
+ function list(request: Request): Promise<Response> {
5
+ return listServer.fetch(request, {
6
+ authorize: (operation) => isAppId(operation.sessionId),
7
+ })
8
+ }
9
+
10
+ export { list as GET, list as POST }
@@ -0,0 +1,56 @@
1
+ import type { ContractEvent } from 'experimental-a2'
2
+ import { expect, it } from 'vitest'
3
+ import { appAgent, defaultModelChoice } from './model'
4
+
5
+ const chosen = (payload: unknown): ContractEvent<typeof appAgent.events> =>
6
+ ({
7
+ id: 'choice',
8
+ index: 1,
9
+ sessionId: 'app',
10
+ createdAt: new Date(0),
11
+ type: 'model.chosen',
12
+ payload,
13
+ }) as ContractEvent<typeof appAgent.events>
14
+
15
+ const validate = (payload: unknown) =>
16
+ appAgent.events['model.chosen']['~standard'].validate(payload)
17
+
18
+ it('defaults to Opus 5.5 in fast mode', () => {
19
+ expect(appAgent.reducer.initialState.modelChoice).toEqual({
20
+ model: 'anthropic/claude-opus-5.5',
21
+ reasoning: 'provider-default',
22
+ fast: true,
23
+ })
24
+ expect(defaultModelChoice).toEqual(appAgent.reducer.initialState.modelChoice)
25
+ })
26
+
27
+ it("keeps the latest choice in the app's state", () => {
28
+ const choice = {
29
+ model: 'openai/gpt-5.6-luna',
30
+ reasoning: 'high',
31
+ fast: false,
32
+ }
33
+ const state = appAgent.reducer.fold(
34
+ appAgent.reducer.initialState,
35
+ chosen(choice),
36
+ )
37
+ expect(state.modelChoice).toEqual(choice)
38
+ expect(state.messages).toEqual([])
39
+ })
40
+
41
+ it('fills in defaults and rejects choices a browser could write badly', () => {
42
+ expect(validate({ model: ' anthropic/claude-sonnet-5 ' })).toEqual({
43
+ value: {
44
+ model: 'anthropic/claude-sonnet-5',
45
+ reasoning: 'provider-default',
46
+ fast: false,
47
+ },
48
+ })
49
+ for (const payload of [
50
+ {},
51
+ { model: ' ' },
52
+ { model: 'openai/gpt-5.6-luna', reasoning: 'maximum' },
53
+ { model: 'openai/gpt-5.6-luna', fast: 'yes' },
54
+ ])
55
+ expect(validate(payload)).toHaveProperty('issues')
56
+ })
@@ -0,0 +1,58 @@
1
+ import type { UIMessage } from 'ai'
2
+ import { agentContract } from 'experimental-a2/ai'
3
+ import { z } from 'zod'
4
+
5
+ /** Log events the dev tools read at a time. */
6
+ export const logPage = 200
7
+
8
+ /** The port the agent serves its Next.js app on, and the preview shows. */
9
+ export const appPort = 3000
10
+
11
+ export const reasoningSchema = z.enum([
12
+ 'provider-default',
13
+ 'none',
14
+ 'minimal',
15
+ 'low',
16
+ 'medium',
17
+ 'high',
18
+ 'xhigh',
19
+ ])
20
+
21
+ /** What the app's next request runs with. The browser picks it and the log keeps it. */
22
+ const modelChoiceSchema = z.object({
23
+ model: z.string().trim().min(1),
24
+ reasoning: reasoningSchema.default('provider-default'),
25
+ fast: z.boolean().default(false),
26
+ })
27
+
28
+ export type ModelChoice = z.output<typeof modelChoiceSchema>
29
+
30
+ export const defaultModelChoice: ModelChoice = {
31
+ model: 'anthropic/claude-opus-5.5',
32
+ reasoning: 'provider-default',
33
+ fast: true,
34
+ }
35
+
36
+ export type AppMessage = UIMessage
37
+
38
+ /** The coding agent's contract, shared by the server and the browser. */
39
+ export const appAgent = agentContract({
40
+ name: 'a0',
41
+ events: { 'model.chosen': modelChoiceSchema },
42
+ reducer: {
43
+ name: 'a0.model-choice.v1',
44
+ initialState: { modelChoice: defaultModelChoice },
45
+ fold: (state, event) =>
46
+ event.type === 'model.chosen' ? { modelChoice: event.payload } : state,
47
+ },
48
+ })
49
+
50
+ export const toolLabels: Record<string, string> = {
51
+ bash: 'Running a command',
52
+ show_preview: 'Showing the app',
53
+ }
54
+
55
+ /** While the model is still writing a call's input. */
56
+ export const writingLabels: Record<string, string> = {
57
+ bash: 'Writing a command',
58
+ }
@@ -0,0 +1,14 @@
1
+ import { redirect } from 'next/navigation'
2
+ import { queryString } from './flags'
3
+
4
+ /** A new app is just an id with no log yet; the query keeps the dev tools. */
5
+ export default async function NewApp({
6
+ searchParams,
7
+ }: {
8
+ searchParams: Promise<Record<string, string | string[] | undefined>>
9
+ }): Promise<never> {
10
+ const params = new URLSearchParams()
11
+ for (const [name, value] of Object.entries(await searchParams))
12
+ for (const one of [value ?? []].flat()) params.append(name, one)
13
+ redirect(`/a0/${crypto.randomUUID()}${queryString(params)}`)
14
+ }
@@ -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
+ }