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
@@ -1,5 +1,6 @@
1
+ import { contract } from 'experimental-a2'
1
2
  import type { FinishReason, LanguageModelUsage, UIMessage } from 'ai'
2
- import { agent, type AIState, type AIStoredState } from 'experimental-a2/ai'
3
+ import { agentContract } from 'experimental-a2/ai'
3
4
  import { z } from 'zod'
4
5
 
5
6
  export type ChatAgentStep = {
@@ -147,28 +148,12 @@ const chatMessageSchema = z.custom<ChatAgentMessage>(
147
148
  'invalid chat UI message',
148
149
  )
149
150
 
150
- const chatEvents = {
151
- 'message.sent': z.object({
152
- author: z.string().min(1).max(40),
153
- text: z.string().min(1).max(2000),
154
- }),
155
- typed: z.object({ author: z.string().min(1).max(40) }),
156
- 'agent.ambient.changed': z.object({
157
- enabled: z.boolean(),
158
- author: z.string().min(1).max(40),
159
- }),
160
- 'agent.message.posted': agentPostEventSchema,
161
- }
162
-
163
- export const chatAgent = agent<ChatAgentMessage, typeof chatEvents>({
164
- name: 'chat',
165
- events: chatEvents,
151
+ /** The agent's own session: the conversation it sees, one per room. */
152
+ export const chatAgent = agentContract<ChatAgentMessage>({
153
+ name: 'chat-agent',
166
154
  messageSchema: chatMessageSchema,
167
- reducerName: 'a2.playground.chat.ai.v3',
168
155
  })
169
156
 
170
- export const chat = chatAgent.contract
171
-
172
157
  export type ChatMessage = {
173
158
  id: string
174
159
  index: number
@@ -187,7 +172,6 @@ export type AgentPost = {
187
172
  }
188
173
 
189
174
  export type ChatState = {
190
- ai: AIStoredState
191
175
  messages: ChatMessage[]
192
176
  posts: AgentPost[]
193
177
  lastTypingAt: Record<string, string>
@@ -199,124 +183,94 @@ export type ChatState = {
199
183
  }
200
184
 
201
185
  const initialChatState: ChatState = {
202
- ai: chatAgent.reducer.initialState,
203
186
  messages: [],
204
187
  posts: [],
205
188
  lastTypingAt: {},
206
189
  ambient: { enabled: false, changedBy: null, changedAt: null },
207
190
  }
208
191
 
209
- export const chatReducer = chat
210
- .reducer({
211
- name: 'a2.playground.chat.v4',
192
+ /**
193
+ * The room: an ordinary A2 session. The agent runs in its own session, and its
194
+ * `post` tool appends here, so the room's log orders its messages among
195
+ * everyone else's.
196
+ */
197
+ export const chat = contract({
198
+ name: 'chat-room',
199
+ events: {
200
+ 'message.sent': z.object({
201
+ author: z.string().min(1).max(40),
202
+ text: z.string().min(1).max(2000),
203
+ }),
204
+ typed: z.object({ author: z.string().min(1).max(40) }),
205
+ 'agent.ambient.changed': z.object({
206
+ enabled: z.boolean(),
207
+ author: z.string().min(1).max(40),
208
+ }),
209
+ 'agent.message.posted': agentPostEventSchema,
210
+ },
211
+ reducer: {
212
+ name: 'a2.playground.chat-room.v1',
212
213
  initialState: initialChatState,
213
- })
214
- .fold((state, event) => {
215
- const previousAI = chatAgent.reducer.view(state.ai)
216
- if (
217
- event.type === 'ai.tool.called' &&
218
- event.payload.toolName === 'post' &&
219
- (previousAI.activeGeneration?.generationId !==
220
- event.payload.generationId ||
221
- previousAI.activeGeneration.responseMessageId !==
222
- event.payload.messageId)
223
- ) {
224
- return state
225
- }
226
-
227
- const ai = chatAgent.reducer.fold(state.ai, event)
228
- const base = ai === state.ai ? state : { ...state, ai }
229
-
230
- switch (event.type) {
231
- case 'message.sent': {
232
- const stillTyping = { ...base.lastTypingAt }
233
- delete stillTyping[event.payload.author]
234
- return {
235
- ...base,
236
- messages: [
237
- ...base.messages,
238
- {
239
- id: event.id,
240
- index: event.index,
241
- author: event.payload.author,
242
- text: event.payload.text,
243
- at: event.createdAt.toISOString(),
244
- },
245
- ],
246
- lastTypingAt: stillTyping,
214
+ fold: (state, event) => {
215
+ switch (event.type) {
216
+ case 'message.sent': {
217
+ const stillTyping = { ...state.lastTypingAt }
218
+ delete stillTyping[event.payload.author]
219
+ return {
220
+ ...state,
221
+ messages: [
222
+ ...state.messages,
223
+ {
224
+ id: event.id,
225
+ index: event.index,
226
+ author: event.payload.author,
227
+ text: event.payload.text,
228
+ at: event.createdAt.toISOString(),
229
+ },
230
+ ],
231
+ lastTypingAt: stillTyping,
232
+ }
247
233
  }
248
- }
249
- case 'typed':
250
- return {
251
- ...base,
252
- lastTypingAt: {
253
- ...base.lastTypingAt,
254
- [event.payload.author]: event.createdAt.toISOString(),
255
- },
256
- }
257
- case 'agent.ambient.changed':
258
- return {
259
- ...base,
260
- ambient: {
261
- enabled: event.payload.enabled,
262
- changedBy: event.payload.author,
263
- changedAt: event.createdAt.toISOString(),
264
- },
265
- }
266
- case 'agent.message.posted': {
267
- const payload = event.payload
268
- const running = previousAI.tools.some(
269
- (activity) =>
270
- activity.generationId === payload.generationId &&
271
- activity.messageId === payload.responseMessageId &&
272
- activity.toolCallId === payload.toolCallId &&
273
- activity.toolName === 'post' &&
274
- activity.status === 'running',
275
- )
276
- if (!running) return base
277
- return {
278
- ...base,
279
- posts: [
280
- ...base.posts,
281
- {
282
- generationId: payload.generationId,
283
- responseMessageId: payload.responseMessageId,
284
- toolCallId: payload.toolCallId,
285
- index: event.index,
286
- text: payload.message,
287
- at: event.createdAt.toISOString(),
234
+ case 'typed':
235
+ return {
236
+ ...state,
237
+ lastTypingAt: {
238
+ ...state.lastTypingAt,
239
+ [event.payload.author]: event.createdAt.toISOString(),
288
240
  },
289
- ],
290
- }
291
- }
292
- case 'ai.message.interrupted': {
293
- const cutoff = event.payload.lastSeenIndex
294
- if (ai === state.ai || cutoff === undefined) return base
295
- return {
296
- ...base,
297
- posts: base.posts.filter(
298
- (post) =>
299
- post.responseMessageId !== event.payload.messageId ||
300
- (event.payload.generationId !== undefined &&
301
- post.generationId !== event.payload.generationId) ||
302
- post.index <= cutoff,
303
- ),
304
- }
241
+ }
242
+ case 'agent.ambient.changed':
243
+ return {
244
+ ...state,
245
+ ambient: {
246
+ enabled: event.payload.enabled,
247
+ changedBy: event.payload.author,
248
+ changedAt: event.createdAt.toISOString(),
249
+ },
250
+ }
251
+ case 'agent.message.posted':
252
+ return {
253
+ ...state,
254
+ posts: [
255
+ ...state.posts,
256
+ {
257
+ generationId: event.payload.generationId,
258
+ responseMessageId: event.payload.responseMessageId,
259
+ toolCallId: event.payload.toolCallId,
260
+ index: event.index,
261
+ text: event.payload.message,
262
+ at: event.createdAt.toISOString(),
263
+ },
264
+ ],
265
+ }
266
+ default:
267
+ return state
305
268
  }
306
- default:
307
- return base
308
- }
309
- })
269
+ },
270
+ },
271
+ })
272
+ export const chatReducer = chat.reducer
310
273
 
311
274
  const agentMention = /(?:^|\s)@agent(?![\w-])/i
312
275
 
313
276
  export const mentionsAgent = (text: string): boolean => agentMention.test(text)
314
-
315
- export type ChatViewState = Omit<ChatState, 'ai'> & {
316
- ai: AIState<ChatAgentMessage>
317
- }
318
-
319
- export const viewChatState = (state: ChatState): ChatViewState => ({
320
- ...state,
321
- ai: chatAgent.reducer.view(state.ai),
322
- })
@@ -1,5 +1,5 @@
1
- import { chatScheduler, chatServer } from '../server'
1
+ import { chatAgentServer, chatScheduler, chatServer } from '../server'
2
2
 
3
3
  export const POST =
4
- chatScheduler?.handler(chatServer) ??
4
+ chatScheduler?.handler(chatServer, chatAgentServer) ??
5
5
  (() => new Response('chat scheduler is not configured', { status: 503 }))
@@ -0,0 +1,108 @@
1
+ import type { UIMessageChunk } from 'ai'
2
+ import { expect, it, vi } from 'vitest'
3
+
4
+ vi.mock('server-only', () => ({}))
5
+ vi.mock('@/lib/store', async () => {
6
+ const { memory } = await import('experimental-a2/store-memory')
7
+ return { store: memory() }
8
+ })
9
+
10
+ // The chat's real agent, with a scripted model: two `post` calls, then done.
11
+ const { prompts, step } = vi.hoisted(() => ({
12
+ prompts: [] as string[],
13
+ step: (chunks: UIMessageChunk[]) =>
14
+ new ReadableStream<UIMessageChunk>({
15
+ start(controller) {
16
+ for (const chunk of chunks) controller.enqueue(chunk)
17
+ controller.close()
18
+ },
19
+ }),
20
+ }))
21
+ vi.mock('experimental-a2/ai/server', async (importActual) => {
22
+ const actual =
23
+ await importActual<typeof import('experimental-a2/ai/server')>()
24
+ return {
25
+ ...actual,
26
+ agent: (options: Parameters<typeof actual.agent>[0]) =>
27
+ actual.agent({
28
+ ...options,
29
+ generate: ({
30
+ event,
31
+ messages,
32
+ }: {
33
+ event: { payload: { reason: string } }
34
+ messages: { role: string; id: string }[]
35
+ }) => {
36
+ prompts.push(
37
+ messages.map((message) => `${message.role}:${message.id}`).join(),
38
+ )
39
+ return step(
40
+ event.payload.reason === 'message'
41
+ ? [
42
+ { type: 'start' },
43
+ ...['Hello', 'there'].map(
44
+ (message, call): UIMessageChunk => ({
45
+ type: 'tool-input-available',
46
+ toolCallId: `call-${call}`,
47
+ toolName: 'post',
48
+ input: { message },
49
+ }),
50
+ ),
51
+ { type: 'finish', finishReason: 'tool-calls' },
52
+ ]
53
+ : [{ type: 'start' }, { type: 'finish', finishReason: 'stop' }],
54
+ )
55
+ },
56
+ } as never),
57
+ }
58
+ })
59
+
60
+ it('forwards room messages to the agent, whose posts land in the room', async () => {
61
+ const { chatAgentServer, chatServer } = await import('./server')
62
+ const room = chatServer.session('room')
63
+ const agent = chatAgentServer.session('room')
64
+ await room.append({
65
+ type: 'message.sent',
66
+ payload: { author: 'alice', text: 'just chatting' },
67
+ })
68
+ await room.append({
69
+ type: 'message.sent',
70
+ payload: { author: 'bob', text: '@agent say hi' },
71
+ })
72
+
73
+ // Without a scheduler, appends run in background drains.
74
+ await vi.waitFor(
75
+ async () => {
76
+ expect((await room.state()).state.posts).toHaveLength(2)
77
+ expect((await agent.state()).state.status).toBe('idle')
78
+ },
79
+ { timeout: 5_000 },
80
+ )
81
+
82
+ const { state } = await room.state()
83
+ expect(state.messages.map((message) => message.author)).toEqual([
84
+ 'alice',
85
+ 'bob',
86
+ ])
87
+ expect(state.posts.map((post) => post.text)).toEqual(['Hello', 'there'])
88
+ expect(state.posts[0]!.index).toBeGreaterThan(state.messages[1]!.index)
89
+ // The passive message reached the agent as context without a reply of its
90
+ // own; only the mention generated, and it saw both.
91
+ expect(prompts).toHaveLength(2)
92
+ expect(
93
+ prompts[0]!.split(',').filter((id) => id.startsWith('user:')),
94
+ ).toHaveLength(2)
95
+ const messages = (await agent.state()).state.messages
96
+ expect(messages.map((message) => message.role)).toEqual([
97
+ 'user',
98
+ 'user',
99
+ 'assistant',
100
+ ])
101
+
102
+ // Replaying the post tool's append converges on the stored event.
103
+ const [post] = (await room.history()).filter(
104
+ (event) => event.type === 'agent.message.posted',
105
+ )
106
+ await room.append({ type: post!.type, id: post!.id, payload: post!.payload })
107
+ expect((await room.state()).state.posts).toHaveLength(2)
108
+ })
@@ -1,11 +1,13 @@
1
1
  import 'server-only'
2
2
  import { tool } from 'ai'
3
+ import { idempotentId } from 'experimental-a2'
3
4
  import { handlerContext, inputs } from 'experimental-a2/ai'
4
- import { createHandlers } from 'experimental-a2/ai/server'
5
+ import { agent } from 'experimental-a2/ai/server'
5
6
  import { vercelQueues } from 'experimental-a2/scheduler-vercel'
6
7
  import { createServer, type A2Operation } from 'experimental-a2/server'
7
8
  import { store } from '@/lib/store'
8
9
  import {
10
+ chat,
9
11
  chatAgent,
10
12
  chatPostInputSchema,
11
13
  mentionsAgent,
@@ -22,12 +24,16 @@ const tools = {
22
24
  execute: async ({ message }, { toolCallId }) => {
23
25
  const context = handlerContext(chatAgent)
24
26
  context.signal.throwIfAborted()
25
- await context.session.append('post', {
27
+ const { generationId, messageId } = context.event.payload.call
28
+ // The agent's session shares the room's id. The explicit id makes a
29
+ // retried tool converge on one post.
30
+ await chatServer.session(context.session.id).append({
26
31
  type: 'agent.message.posted',
32
+ id: await idempotentId('chat.post', generationId, toolCallId),
27
33
  payload: {
28
34
  message,
29
- generationId: context.event.payload.call.generationId,
30
- responseMessageId: context.event.payload.call.messageId,
35
+ generationId,
36
+ responseMessageId: messageId,
31
37
  toolCallId,
32
38
  },
33
39
  })
@@ -36,10 +42,19 @@ const tools = {
36
42
  }),
37
43
  }
38
44
 
39
- const aiHandlers = createHandlers({
40
- agent: chatAgent,
45
+ export const chatScheduler =
46
+ process.env.VERCEL || process.env.A2_QUEUES
47
+ ? vercelQueues({ topic: 'a2-chat' })
48
+ : undefined
49
+
50
+ export const chatAgentServer = agent({
51
+ ...(store ? { store } : {}),
52
+ ...(chatScheduler ? { scheduler: chatScheduler } : {}),
53
+ contract: chatAgent,
41
54
  model: 'openai/gpt-5.4',
42
55
  tools,
56
+ // Posts become chat bubbles, so one agent's posts must land in call order.
57
+ toolLane: 'a2.chat.agent-tools',
43
58
  instructions: `You are "agent", a participant in a multiplayer chat.
44
59
  The latest participant message invited you either by mentioning @agent or by enabling the room's ambient mode. Messages are formatted as "name: message"; earlier passive messages are useful conversation context.
45
60
  Sound like a sharp, relaxed person in the chat, not a customer-support bot. Match the room's language and energy. Keep messages specific and conversational; avoid canned acknowledgements, assistant-y disclaimers, and explanations of your own mechanics.
@@ -56,10 +71,10 @@ Use as many post calls as the moment naturally calls for. Several short messages
56
71
  },
57
72
  messageMetadata: (context) => {
58
73
  const previous = context.messages.find(
59
- (message) => message.id === context.responseMessageId,
74
+ (message) => message.id === context.event.payload.responseMessageId,
60
75
  )?.metadata
61
76
  const trigger = context.messages.find(
62
- (message) => message.id === context.request.payload.messageId,
77
+ (message) => message.id === context.event.payload.messageId,
63
78
  )?.metadata
64
79
  const steps = previous?.kind === 'agent' ? previous.steps : []
65
80
  const mode: ChatAgentMode | undefined =
@@ -99,76 +114,16 @@ Use as many post calls as the moment naturally calls for. Several short messages
99
114
  },
100
115
  })
101
116
 
102
- export const authorizeChatOperation = (
103
- operation: A2Operation<(typeof chatAgent.contract)['events']>,
104
- ): boolean => {
105
- if (operation.type !== 'push') return true
106
- const serverOnly = operation.events.find(
107
- (event) =>
108
- event.type === 'ai.message.created' ||
109
- event.type === 'agent.message.posted',
110
- )
111
- if (serverOnly !== undefined) return false
112
- const rejected = operation.events.find((event) => {
113
- if (
114
- event.type === 'message.sent' ||
115
- event.type === 'typed' ||
116
- event.type === 'agent.ambient.changed'
117
- ) {
118
- return false
119
- }
120
- return !(
121
- event.type === 'ai.control.requested' &&
122
- typeof event.payload === 'object' &&
123
- event.payload !== null &&
124
- 'action' in event.payload &&
125
- typeof event.payload.action === 'string' &&
126
- [
127
- 'approval',
128
- 'input',
129
- 'interrupt',
130
- 'stop',
131
- 'retry',
132
- 'pause',
133
- 'resume',
134
- ].includes(event.payload.action)
135
- )
136
- })
137
- return rejected === undefined
138
- }
139
-
140
- const toolExecutionHandler = aiHandlers['ai.tool.execution.requested']
141
- if (toolExecutionHandler === undefined) {
142
- throw new Error('A2 AI handlers must include ai.tool.execution.requested')
143
- }
144
- const serializedToolExecutionHandler =
145
- typeof toolExecutionHandler === 'function'
146
- ? { handler: toolExecutionHandler, lane: 'a2.chat.agent-tools' }
147
- : { ...toolExecutionHandler, lane: 'a2.chat.agent-tools' }
148
-
149
- export const chatScheduler =
150
- process.env.VERCEL || process.env.A2_QUEUES
151
- ? vercelQueues({ topic: 'a2-chat' })
152
- : undefined
153
-
117
+ /** The room. Each message goes on to the agent's session, marked for whether it should answer. */
154
118
  export const chatServer = createServer({
155
119
  ...(store ? { store } : {}),
156
120
  ...(chatScheduler ? { scheduler: chatScheduler } : {}),
157
- contract: chatAgent.contract,
121
+ contract: chat,
158
122
  handlers: {
159
- ...aiHandlers,
160
- 'ai.tool.execution.requested': serializedToolExecutionHandler,
161
123
  'message.sent': {
162
124
  lane: 'a2.chat.ingress',
163
125
  handler: async (context) => {
164
- let ambient = false
165
- for (const event of await context.session.history()) {
166
- if (event.index > context.event.index) break
167
- if (event.type === 'agent.ambient.changed') {
168
- ambient = event.payload.enabled
169
- }
170
- }
171
-
126
+ const ambient = (await context.session.state()).state.ambient.enabled
172
127
  const { author, text } = context.event.payload
173
128
  const mode: ChatParticipantMode = ambient
174
129
  ? 'ambient'
@@ -176,7 +131,9 @@ export const chatServer = createServer({
176
131
  ? 'mention'
177
132
  : 'passive'
178
133
  const message: ChatAgentMessage = {
179
- id: context.event.id,
134
+ // Event ids are unique across a store's sessions, so the forward
135
+ // derives its own; a retried forward converges on the same id.
136
+ id: await idempotentId('chat.message', context.event.id),
180
137
  role: 'user',
181
138
  metadata: {
182
139
  kind: 'participant',
@@ -187,8 +144,47 @@ export const chatServer = createServer({
187
144
  },
188
145
  parts: [{ type: 'text', text: `${author}: ${text}` }],
189
146
  }
190
- return inputs.message(message, { generate: mode !== 'passive' })
147
+ await chatAgentServer
148
+ .session(context.session.id)
149
+ .append(...inputs.message(message, { generate: mode !== 'passive' }))
191
150
  },
192
151
  },
193
152
  },
194
153
  })
154
+
155
+ /** Room pushes: people's messages, typing, and ambient mode. Posts are the agent's. */
156
+ export const authorizeChatOperation = (
157
+ operation: A2Operation<(typeof chat)['events']>,
158
+ ): boolean =>
159
+ operation.type !== 'push' ||
160
+ operation.events.every(
161
+ (event) =>
162
+ event.type === 'message.sent' ||
163
+ event.type === 'typed' ||
164
+ event.type === 'agent.ambient.changed',
165
+ )
166
+
167
+ const agentControls = new Set([
168
+ 'approval',
169
+ 'input',
170
+ 'interrupt',
171
+ 'stop',
172
+ 'retry',
173
+ 'pause',
174
+ 'resume',
175
+ ])
176
+
177
+ /** Agent pushes: controls only. Messages reach the agent through the room. */
178
+ export const authorizeChatAgentOperation = (
179
+ operation: A2Operation<(typeof chatAgent)['events']>,
180
+ ): boolean =>
181
+ operation.type !== 'push' ||
182
+ operation.events.every(
183
+ (event) =>
184
+ event.type === 'ai.control.requested' &&
185
+ typeof event.payload === 'object' &&
186
+ event.payload !== null &&
187
+ 'action' in event.payload &&
188
+ typeof event.payload.action === 'string' &&
189
+ agentControls.has(event.payload.action),
190
+ )
@@ -1,4 +1,7 @@
1
- import type { ReactNode } from 'react'
1
+ 'use client'
2
+
3
+ import { useState, type ReactNode } from 'react'
4
+ import type { HistoryState } from 'experimental-a2/client'
2
5
 
3
6
  /** The structural shape every machine's events share — enough for a
4
7
  * feed that shows the log itself. */
@@ -18,11 +21,26 @@ export function ActivityFeed({
18
21
  events,
19
22
  index,
20
23
  status,
24
+ history,
25
+ loadHistory,
21
26
  }: {
22
27
  events: readonly FeedEvent[]
23
28
  index: number
24
29
  status?: ReactNode
30
+ history?: HistoryState
31
+ loadHistory?: () => Promise<unknown>
25
32
  }): ReactNode {
33
+ const [error, setError] = useState<string | null>(null)
34
+ const loadEarlier = async (): Promise<void> => {
35
+ setError(null)
36
+ try {
37
+ await loadHistory?.()
38
+ } catch (cause) {
39
+ setError(
40
+ cause instanceof Error ? cause.message : 'Could not load history.',
41
+ )
42
+ }
43
+ }
26
44
  return (
27
45
  <section>
28
46
  <h2>Activity</h2>
@@ -45,6 +63,17 @@ export function ActivityFeed({
45
63
  </ol>
46
64
  )}
47
65
  {status ? <p className="activity-status">{status}</p> : null}
66
+ {history && loadHistory && !history.complete ? (
67
+ <button
68
+ type="button"
69
+ className="secondary"
70
+ disabled={history.loading}
71
+ onClick={() => void loadEarlier()}
72
+ >
73
+ {history.loading ? 'Loading…' : 'Load earlier events'}
74
+ </button>
75
+ ) : null}
76
+ {error ? <p role="alert">{error}</p> : null}
48
77
  <p className="frontier">
49
78
  Stream frontier: <code>{index}</code> — the last server-confirmed log
50
79
  position this browser has folded.
@@ -0,0 +1,35 @@
1
+ // @vitest-environment jsdom
2
+ import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
3
+ import { afterEach, expect, it, vi } from 'vitest'
4
+ import { ActivityFeed } from './activity-feed'
5
+
6
+ afterEach(cleanup)
7
+
8
+ it('loads an earlier page only on request and keeps failures retryable', async () => {
9
+ const loadHistory = vi
10
+ .fn<() => Promise<unknown>>()
11
+ .mockRejectedValueOnce(new Error('Read failed'))
12
+ .mockResolvedValueOnce([])
13
+ render(
14
+ <ActivityFeed
15
+ events={[]}
16
+ index={100}
17
+ history={{ loading: false, complete: false, oldestLoaded: null }}
18
+ loadHistory={loadHistory}
19
+ />,
20
+ )
21
+ expect(loadHistory).not.toHaveBeenCalled()
22
+ await act(async () =>
23
+ fireEvent.click(
24
+ screen.getByRole('button', { name: 'Load earlier events' }),
25
+ ),
26
+ )
27
+ expect(screen.getByRole('alert').textContent).toBe('Read failed')
28
+ await act(async () =>
29
+ fireEvent.click(
30
+ screen.getByRole('button', { name: 'Load earlier events' }),
31
+ ),
32
+ )
33
+ expect(screen.queryByRole('alert')).toBeNull()
34
+ expect(loadHistory).toHaveBeenCalledTimes(2)
35
+ })