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
@@ -1,7 +1,5 @@
1
1
  import type { ReactNode } from 'react'
2
- import { demoAgent } from '../model'
3
2
  import { agentServer } from '../server'
4
- import { SessionProvider } from '../session'
5
3
  import { AgentClient } from './agent-client'
6
4
 
7
5
  export default async function AgentSessionPage({
@@ -11,19 +9,7 @@ export default async function AgentSessionPage({
11
9
  }): Promise<ReactNode> {
12
10
  const { agentId } = await params
13
11
  const session = agentServer.session(agentId)
14
- const { state, index } = await session.state(demoAgent.reducer)
15
- const events = (await session.history()).filter(
16
- (event) => event.index <= index,
17
- )
12
+ const initial = await session.state()
18
13
 
19
- return (
20
- <SessionProvider
21
- sessionId={agentId}
22
- initialState={state}
23
- initialIndex={index}
24
- initialEvents={events}
25
- >
26
- <AgentClient agentId={agentId} />
27
- </SessionProvider>
28
- )
14
+ return <AgentClient agentId={agentId} initial={initial} />
29
15
  }
@@ -1,4 +1,7 @@
1
1
  import { agentServer } from '../server'
2
2
 
3
- export const GET = agentServer.fetch
4
- export const POST = agentServer.fetch
3
+ function events(request: Request): Promise<Response> {
4
+ return agentServer.fetch(request)
5
+ }
6
+
7
+ export { events as GET, events as POST }
@@ -0,0 +1,98 @@
1
+ import { expect, it, vi } from 'vitest'
2
+ import { agent } from 'experimental-a2/ai/server'
3
+ import { memory } from 'experimental-a2/store-memory'
4
+ import { inputs } from 'experimental-a2/ai'
5
+ import { demoAgent } from './model'
6
+
7
+ const message = { id: 'message', role: 'user' as const, parts: [] }
8
+
9
+ const post = (events: unknown[]): Request =>
10
+ new Request('http://test/events?sessionId=one', {
11
+ method: 'POST',
12
+ headers: { 'content-type': 'application/json' },
13
+ body: JSON.stringify({ sessionId: 'one', events }),
14
+ })
15
+
16
+ const server = () => {
17
+ let generations = 0
18
+ const agentServer = agent({
19
+ contract: demoAgent,
20
+ store: memory(),
21
+ model: 'test/model',
22
+ generate: () => {
23
+ generations += 1
24
+ return new ReadableStream({ start: (controller) => controller.close() })
25
+ },
26
+ })
27
+ return { agentServer, generations: () => generations }
28
+ }
29
+
30
+ it('accepts compaction settings from the browser without starting a turn', async () => {
31
+ const { agentServer, generations } = server()
32
+ const response = await agentServer.fetch(
33
+ post([
34
+ {
35
+ type: 'compaction.settings.updated',
36
+ payload: { mode: 'manual', thresholdTokens: 1 },
37
+ },
38
+ ]),
39
+ )
40
+ expect(response.status).toBe(200)
41
+ await agentServer.drain('one')
42
+ const { state } = await agentServer.session('one').state()
43
+ expect(state.compactionSettings).toEqual({
44
+ mode: 'manual',
45
+ thresholdTokens: 1,
46
+ })
47
+ expect(generations()).toBe(0)
48
+ })
49
+
50
+ it.each([0, -1, 1.5, Number.MAX_SAFE_INTEGER + 1, '1', null])(
51
+ 'rejects invalid threshold %s',
52
+ async (thresholdTokens) => {
53
+ const { agentServer } = server()
54
+ const response = await agentServer.fetch(
55
+ post([
56
+ {
57
+ type: 'compaction.settings.updated',
58
+ payload: { mode: 'manual', thresholdTokens },
59
+ },
60
+ ]),
61
+ )
62
+ expect(response.status).toBe(400)
63
+ expect(await agentServer.session('one').history()).toEqual([])
64
+ },
65
+ )
66
+
67
+ it('keeps lifecycle events server-only beside public controls', async () => {
68
+ const { agentServer } = server()
69
+ const rejected = await agentServer.fetch(
70
+ post([
71
+ ...inputs.message(message),
72
+ { type: 'ai.session.closed', payload: {} },
73
+ ]),
74
+ )
75
+ expect(rejected.status).toBe(400)
76
+ expect(await agentServer.session('one').history()).toEqual([])
77
+ const accepted = await agentServer.fetch(post(inputs.message(message)))
78
+ expect(accepted.status).toBe(200)
79
+ })
80
+
81
+ it('forwards both HTTP and WebSocket entry requests to the agent', async () => {
82
+ const fetch = vi
83
+ .fn<(request: Request) => Promise<Response>>()
84
+ .mockResolvedValue(new Response(null))
85
+ vi.doMock('./server', () => ({ agentServer: { fetch } }))
86
+ try {
87
+ const route = await import('./events/route')
88
+ const read = new Request('http://test/events', {
89
+ headers: { upgrade: 'websocket' },
90
+ })
91
+ const write = new Request('http://test/events', { method: 'POST' })
92
+ await route.GET(read)
93
+ await route.POST(write)
94
+ expect(fetch.mock.calls).toEqual([[read], [write]])
95
+ } finally {
96
+ vi.doUnmock('./server')
97
+ }
98
+ })
@@ -0,0 +1,58 @@
1
+ // @vitest-environment jsdom
2
+ import { cleanup, fireEvent, render, screen } from '@testing-library/react'
3
+ import { afterEach, expect, it, vi } from 'vitest'
4
+ import type { ModelChoice } from './model'
5
+ import { ModelInput } from './model-input'
6
+
7
+ afterEach(cleanup)
8
+
9
+ const renderInput = (value: ModelChoice) => {
10
+ const onChange = vi.fn<(settings: ModelChoice) => void>()
11
+ const view = render(
12
+ <ModelInput value={value} onChange={onChange} disabled={false} />,
13
+ )
14
+ return {
15
+ onChange,
16
+ model: screen.getByRole('textbox') as HTMLInputElement,
17
+ update: (next: ModelChoice) =>
18
+ view.rerender(
19
+ <ModelInput value={next} onChange={onChange} disabled={false} />,
20
+ ),
21
+ }
22
+ }
23
+
24
+ it('commits a typed model on blur or Enter, not per keystroke', () => {
25
+ const { onChange, model, update } = renderInput({ reasoning: 'high' })
26
+ fireEvent.change(model, { target: { value: 'openai/gpt' } })
27
+ fireEvent.change(model, { target: { value: ' provider/model ' } })
28
+ expect(onChange).not.toHaveBeenCalled()
29
+ expect(model.value).toBe(' provider/model ')
30
+ fireEvent.blur(model)
31
+ expect(onChange).toHaveBeenCalledExactlyOnceWith({
32
+ reasoning: 'high',
33
+ model: 'provider/model',
34
+ })
35
+
36
+ update({ reasoning: 'high', model: 'provider/model' })
37
+ expect(model.value).toBe('provider/model')
38
+ fireEvent.change(model, { target: { value: '' } })
39
+ fireEvent.keyDown(model, { key: 'Enter' })
40
+ expect(onChange).toHaveBeenLastCalledWith({ reasoning: 'high' })
41
+ })
42
+
43
+ it('commits reasoning and speed immediately and omits defaults', () => {
44
+ const { onChange } = renderInput({
45
+ model: 'provider/model',
46
+ reasoning: 'low',
47
+ })
48
+ fireEvent.change(screen.getByTitle('Thinking level'), {
49
+ target: { value: 'provider-default' },
50
+ })
51
+ expect(onChange).toHaveBeenLastCalledWith({ model: 'provider/model' })
52
+ fireEvent.click(screen.getByRole('button', { name: 'Prefer fast' }))
53
+ expect(onChange).toHaveBeenLastCalledWith({
54
+ model: 'provider/model',
55
+ reasoning: 'low',
56
+ speed: 'fast',
57
+ })
58
+ })
@@ -0,0 +1,115 @@
1
+ 'use client'
2
+
3
+ import { useState, type ReactNode } from 'react'
4
+ import { reasoningLevels, type ReasoningLevel } from 'experimental-a2/ai'
5
+ import { agentModel, type ModelChoice } from './model'
6
+
7
+ const choose = <K extends keyof ModelChoice>(
8
+ settings: ModelChoice,
9
+ key: K,
10
+ value: ModelChoice[K] | undefined,
11
+ ): ModelChoice => {
12
+ const { [key]: _previous, ...rest } = settings
13
+ return value === undefined ? rest : { ...rest, [key]: value }
14
+ }
15
+
16
+ export function ModelInput({
17
+ value,
18
+ onChange,
19
+ disabled,
20
+ }: {
21
+ value: ModelChoice
22
+ onChange: (settings: ModelChoice) => void
23
+ disabled: boolean
24
+ }): ReactNode {
25
+ // A choice is a durable session event, so a typed model id commits on
26
+ // blur or Enter rather than once per keystroke.
27
+ const [draft, setDraft] = useState<string | null>(null)
28
+ const model = draft ?? value.model ?? ''
29
+ const commit = (): void => {
30
+ if (draft === null) return
31
+ setDraft(null)
32
+ const next = draft.trim() || undefined
33
+ if (next !== value.model) onChange(choose(value, 'model', next))
34
+ }
35
+ return (
36
+ <div className="agent-model-controls">
37
+ <label className="agent-model-choice">
38
+ <span className="sr-only">Model</span>
39
+ <input
40
+ type="text"
41
+ value={model}
42
+ placeholder={agentModel}
43
+ size={Math.max(model.length || agentModel.length, 12)}
44
+ onChange={(event) => setDraft(event.target.value)}
45
+ onBlur={commit}
46
+ onKeyDown={(event) => {
47
+ if (event.key !== 'Enter' || event.nativeEvent.isComposing) return
48
+ event.preventDefault()
49
+ commit()
50
+ }}
51
+ disabled={disabled}
52
+ autoComplete="off"
53
+ autoCorrect="off"
54
+ autoCapitalize="none"
55
+ spellCheck={false}
56
+ />
57
+ </label>
58
+ <label className="agent-thinking-choice">
59
+ <span className="sr-only">Thinking level</span>
60
+ <select
61
+ title="Thinking level"
62
+ value={value.reasoning ?? 'provider-default'}
63
+ onChange={(event) => {
64
+ const level = event.target.value as ReasoningLevel
65
+ onChange(
66
+ choose(
67
+ value,
68
+ 'reasoning',
69
+ level === 'provider-default' ? undefined : level,
70
+ ),
71
+ )
72
+ }}
73
+ disabled={disabled}
74
+ >
75
+ {reasoningLevels.map((level) => (
76
+ <option key={level} value={level}>
77
+ {level === 'provider-default'
78
+ ? 'Default'
79
+ : level === 'xhigh'
80
+ ? 'Extra high'
81
+ : level[0]!.toUpperCase() + level.slice(1)}
82
+ </option>
83
+ ))}
84
+ </select>
85
+ </label>
86
+ <button
87
+ type="button"
88
+ className="agent-fast-toggle"
89
+ aria-label="Prefer fast"
90
+ aria-pressed={value.speed === 'fast'}
91
+ title="Prefer fast: uses the faster tier when available. May cost more; otherwise runs at standard speed."
92
+ onClick={() =>
93
+ onChange(
94
+ choose(value, 'speed', value.speed === 'fast' ? undefined : 'fast'),
95
+ )
96
+ }
97
+ disabled={disabled}
98
+ >
99
+ <svg
100
+ width="18"
101
+ height="18"
102
+ viewBox="0 0 24 24"
103
+ fill={value.speed === 'fast' ? 'currentColor' : 'none'}
104
+ stroke="currentColor"
105
+ strokeWidth="1.7"
106
+ strokeLinecap="round"
107
+ strokeLinejoin="round"
108
+ aria-hidden="true"
109
+ >
110
+ <path d="M13 2 3 14h9l-1 8 10-12h-9l1-8Z" />
111
+ </svg>
112
+ </button>
113
+ </div>
114
+ )
115
+ }
@@ -0,0 +1,73 @@
1
+ import { expect, it } from 'vitest'
2
+ import {
3
+ agentModel,
4
+ compactionOptions,
5
+ demoAgent,
6
+ modelSettings,
7
+ type AgentEvent,
8
+ } from './model'
9
+
10
+ const event = <T extends AgentEvent['type']>(
11
+ index: number,
12
+ type: T,
13
+ payload: Extract<AgentEvent, { type: T }>['payload'],
14
+ ): AgentEvent =>
15
+ ({
16
+ id: `event-${index}`,
17
+ sessionId: 's',
18
+ index,
19
+ createdAt: new Date(0),
20
+ type,
21
+ payload,
22
+ }) as AgentEvent
23
+
24
+ it('folds compaction settings beside the AI state', () => {
25
+ const { reducer } = demoAgent
26
+ let state = reducer.fold(
27
+ reducer.initialState,
28
+ event(1, 'compaction.settings.updated', { mode: 'disabled' }),
29
+ )
30
+ state = reducer.fold(
31
+ state,
32
+ event(2, 'ai.message.created', {
33
+ message: { id: 'user', role: 'user', parts: [] },
34
+ }),
35
+ )
36
+ expect(state.compactionSettings).toEqual({ mode: 'disabled' })
37
+ expect(state.compactionSettingsId).toBe('event-1')
38
+ expect(state.messages.map((message) => message.id)).toEqual(['user'])
39
+ })
40
+
41
+ it.each([
42
+ [{ mode: 'automatic' } as const, {}],
43
+ [{ mode: 'manual', thresholdTokens: 1 } as const, { thresholdTokens: 1 }],
44
+ [{ mode: 'disabled' } as const, false],
45
+ ])('maps %j to compaction options', (settings, options) => {
46
+ const state = demoAgent.reducer.fold(
47
+ demoAgent.reducer.initialState,
48
+ event(1, 'compaction.settings.updated', settings),
49
+ )
50
+ expect(compactionOptions(state.compactionSettings)).toEqual(options)
51
+ })
52
+
53
+ it('folds the model choice and fills in the default model', () => {
54
+ const { reducer } = demoAgent
55
+ const state = reducer.fold(
56
+ reducer.initialState,
57
+ event(1, 'model.choice.updated', { reasoning: 'high', speed: 'fast' }),
58
+ )
59
+ expect(reducer.initialState.modelChoice).toEqual({})
60
+ expect(modelSettings(state.modelChoice)).toEqual({
61
+ model: agentModel,
62
+ reasoning: 'high',
63
+ speed: 'fast',
64
+ })
65
+ expect(
66
+ modelSettings(
67
+ reducer.fold(
68
+ state,
69
+ event(2, 'model.choice.updated', { model: 'openai/gpt-5.6-terra' }),
70
+ ).modelChoice,
71
+ ),
72
+ ).toEqual({ model: 'openai/gpt-5.6-terra' })
73
+ })
@@ -1,6 +1,13 @@
1
- import { agent } from 'experimental-a2/ai'
2
1
  import type { ContractEvent } from 'experimental-a2'
3
- import type { AutomaticCompaction } from 'experimental-a2/ai/server'
2
+ import {
3
+ agentContract,
4
+ reasoningLevels,
5
+ type ReasoningLevel,
6
+ } from 'experimental-a2/ai'
7
+ import type {
8
+ AutomaticCompaction,
9
+ ModelSettings,
10
+ } from 'experimental-a2/ai/server'
4
11
  import { z } from 'zod'
5
12
 
6
13
  export const agentModel = 'openai/gpt-5.6-luna'
@@ -16,36 +23,61 @@ export const compactionSettingsSchema = z.discriminatedUnion('mode', [
16
23
 
17
24
  export type CompactionSettings = z.infer<typeof compactionSettingsSchema>
18
25
 
19
- export const demoAgent = agent({
20
- name: 'agent',
21
- events: { 'compaction.settings.updated': compactionSettingsSchema },
26
+ /** The picker's choice; an omitted field keeps the agent's default. */
27
+ export const modelChoiceSchema = z.strictObject({
28
+ model: z.string().trim().min(1).optional(),
29
+ reasoning: z
30
+ .string()
31
+ .refine((value): value is ReasoningLevel =>
32
+ reasoningLevels.includes(value as ReasoningLevel),
33
+ )
34
+ .optional(),
35
+ speed: z.enum(['fast', 'standard']).optional(),
22
36
  })
23
37
 
24
- export type AgentEvent = ContractEvent<typeof demoAgent.contract.events>
38
+ export type ModelChoice = z.infer<typeof modelChoiceSchema>
25
39
 
26
40
  const automatic: CompactionSettings = { mode: 'automatic' }
27
-
28
- export function latestCompactionSettings(history: readonly AgentEvent[]): {
29
- settings: CompactionSettings
30
- id: string
31
- } {
32
- const event = history.findLast(
33
- (candidate) => candidate.type === 'compaction.settings.updated',
34
- )
35
- return event
36
- ? { settings: event.payload, id: event.id }
37
- : { settings: automatic, id: 'default' }
41
+ const noChoice: ModelChoice = {}
42
+ const settingsState: {
43
+ compactionSettings: CompactionSettings
44
+ compactionSettingsId: string
45
+ modelChoice: ModelChoice
46
+ } = {
47
+ compactionSettings: automatic,
48
+ compactionSettingsId: 'default',
49
+ modelChoice: noChoice,
38
50
  }
39
51
 
40
- export const compactionSettings = demoAgent.contract
41
- .reducer({
42
- name: 'agent.compaction-settings.v1',
43
- initialState: automatic,
44
- stateSchema: compactionSettingsSchema,
45
- })
46
- .fold((state, event): CompactionSettings =>
47
- event.type === 'compaction.settings.updated' ? event.payload : state,
48
- )
52
+ export const demoAgent = agentContract({
53
+ name: 'agent',
54
+ events: {
55
+ 'compaction.settings.updated': compactionSettingsSchema,
56
+ 'model.choice.updated': modelChoiceSchema,
57
+ },
58
+ reducer: {
59
+ name: 'a2.playground.agent.v4',
60
+ initialState: settingsState,
61
+ fold: (state, event) =>
62
+ event.type === 'compaction.settings.updated'
63
+ ? {
64
+ ...state,
65
+ compactionSettings: event.payload,
66
+ compactionSettingsId: event.id,
67
+ }
68
+ : event.type === 'model.choice.updated'
69
+ ? { ...state, modelChoice: event.payload }
70
+ : state,
71
+ },
72
+ })
73
+
74
+ export type AgentEvent = ContractEvent<typeof demoAgent.events>
75
+ export type AgentState = typeof demoAgent.reducer.initialState
76
+
77
+ export const modelSettings = (choice: ModelChoice): ModelSettings => ({
78
+ ...choice,
79
+ model: choice.model ?? agentModel,
80
+ })
49
81
 
50
82
  export function compactionOptions(
51
83
  settings: CompactionSettings,
@@ -4,11 +4,12 @@ import type {
4
4
  KeyboardEvent as ReactKeyboardEvent,
5
5
  ReactNode,
6
6
  } from 'react'
7
- import { useState } from 'react'
7
+ import { useId, useState } from 'react'
8
8
  import { useRouter } from 'next/navigation'
9
- import type { UIMessage } from 'ai'
10
9
  import { inputs } from 'experimental-a2/ai'
10
+ import type { ModelChoice } from './model'
11
11
  import { agentClient } from './session'
12
+ import { ModelInput } from './model-input'
12
13
 
13
14
  const REMINDER_PROMPT =
14
15
  'Set a reminder for yourself in 37 seconds to check checkout-api again.'
@@ -33,6 +34,8 @@ export function NewAgentSession({
33
34
  remindersEnabled: boolean
34
35
  }): ReactNode {
35
36
  const router = useRouter()
37
+ const promptId = useId()
38
+ const [choice, setChoice] = useState<ModelChoice>({})
36
39
  const [prompt, setPrompt] = useState(
37
40
  remindersEnabled ? REMINDER_PROMPT : INCIDENT_PROMPT,
38
41
  )
@@ -46,17 +49,21 @@ export function NewAgentSession({
46
49
  if (!text || submitting) return
47
50
 
48
51
  const agentId = crypto.randomUUID().slice(0, 8)
49
- const message: UIMessage = {
50
- id: crypto.randomUUID(),
51
- role: 'user',
52
- parts: [{ type: 'text', text }],
53
- }
54
52
 
55
53
  setPrompt('')
56
54
  setSubmitting(true)
57
55
  setError(null)
58
56
  try {
59
- await agentClient.session(agentId).push(...inputs.message(message))
57
+ await agentClient.session(agentId).push(
58
+ ...(Object.keys(choice).length === 0
59
+ ? []
60
+ : [{ type: 'model.choice.updated' as const, payload: choice }]),
61
+ ...inputs.message({
62
+ id: crypto.randomUUID(),
63
+ role: 'user',
64
+ parts: [{ type: 'text', text }],
65
+ }),
66
+ )
60
67
  router.push(`/agent/${agentId}`)
61
68
  } catch (err) {
62
69
  setPrompt((current) => (current === '' ? submittedPrompt : current))
@@ -75,17 +82,28 @@ export function NewAgentSession({
75
82
  </p>
76
83
  </div>
77
84
  <form className="agent-form" onSubmit={(event) => void start(event)}>
78
- <label>
79
- Message
80
- <textarea
81
- autoFocus
82
- rows={3}
83
- value={prompt}
84
- onChange={(event) => setPrompt(event.target.value)}
85
- onKeyDown={submitOnEnter}
86
- disabled={submitting}
87
- />
88
- </label>
85
+ <div className="agent-message-field">
86
+ <label htmlFor={promptId}>Message</label>
87
+ <div className="agent-composer">
88
+ <textarea
89
+ id={promptId}
90
+ autoFocus
91
+ rows={3}
92
+ placeholder="Message the agent…"
93
+ value={prompt}
94
+ onChange={(event) => setPrompt(event.target.value)}
95
+ onKeyDown={submitOnEnter}
96
+ disabled={submitting}
97
+ />
98
+ <div className="agent-composer-toolbar">
99
+ <ModelInput
100
+ value={choice}
101
+ onChange={setChoice}
102
+ disabled={submitting}
103
+ />
104
+ </div>
105
+ </div>
106
+ </div>
89
107
  <div className="agent-form-actions">
90
108
  <button type="submit" disabled={submitting || !prompt.trim()}>
91
109
  {submitting ? 'Opening…' : 'Send to agent'}
@@ -15,10 +15,10 @@ export default function AgentPage(): ReactNode {
15
15
  <NewAgentSession remindersEnabled={agentScheduler !== undefined} />
16
16
  <p className="hint">
17
17
  Each durable generation runs one AI SDK <code>streamText</code> step
18
- with <code>openai/gpt-5.6-luna</code> through Vercel AI Gateway. Local
19
- development needs Gateway credentials and a linked Vercel project for
20
- Sandbox; Vercel deployments use their OIDC token. Reminder delivery also
21
- needs the configured scheduler.
18
+ with your chosen model through Vercel AI Gateway. Local development
19
+ needs Gateway credentials and a linked Vercel project for Sandbox;
20
+ Vercel deployments use their OIDC token. Reminder delivery also needs
21
+ the configured scheduler.
22
22
  </p>
23
23
  </article>
24
24
  )
@@ -4,16 +4,13 @@ import { createHash } from 'node:crypto'
4
4
  import { Sandbox } from '@vercel/sandbox'
5
5
  import { tool } from 'ai'
6
6
  import { handlerContext, inputs } from 'experimental-a2/ai'
7
- import { createAgentServer } from 'experimental-a2/ai/server'
7
+ import { agent } from 'experimental-a2/ai/server'
8
8
  import { vercelQueues } from 'experimental-a2/scheduler-vercel'
9
+ import { otel } from 'experimental-a2/otel'
9
10
  import { z } from 'zod'
10
11
  import { store } from '@/lib/store'
11
- import {
12
- agentModel,
13
- compactionOptions,
14
- compactionSettings,
15
- demoAgent,
16
- } from './model'
12
+ import { modelTelemetry, traceMetadata } from './telemetry'
13
+ import { compactionOptions, demoAgent, modelSettings } from './model'
17
14
 
18
15
  const maxReminderDelaySeconds = 6 * 24 * 60 * 60
19
16
  const bashTimeoutMs = 120_000
@@ -132,13 +129,16 @@ export const agentScheduler =
132
129
  ? vercelQueues({ topic: 'a2-agent' })
133
130
  : undefined
134
131
 
135
- export const agentServer = createAgentServer({
132
+ const telemetry = otel()
133
+
134
+ export const agentServer = agent({
135
+ contract: demoAgent,
136
136
  ...(store ? { store } : {}),
137
137
  ...(agentScheduler ? { scheduler: agentScheduler } : {}),
138
- agent: demoAgent,
139
- model: agentModel,
140
- compaction: async ({ session }) =>
141
- compactionOptions((await session.state(compactionSettings)).state),
138
+ telemetry,
139
+ model: ({ state }) => modelSettings(state.modelChoice),
140
+ messageMetadata: traceMetadata,
141
+ compaction: ({ state }) => compactionOptions(state.compactionSettings),
142
142
  tools,
143
143
  instructions: `You are an incident-response agent for the playground.
144
144
  When responding to an incident, inspect checkout-api before taking action. Its
@@ -154,7 +154,8 @@ it again.
154
154
 
155
155
  When the operator asks you to run a shell command, call bash. Make one bash call
156
156
  at a time and wait for its result before calling it again. Be concise.`,
157
- generation: {
157
+ generation: () => ({
158
+ telemetry: modelTelemetry(),
158
159
  toolApproval: { restartService: 'user-approval' },
159
- },
160
+ }),
160
161
  })
@@ -1,13 +1,12 @@
1
1
  'use client'
2
2
  import { createClient } from 'experimental-a2/client'
3
- import { createReact } from 'experimental-a2/react'
4
- import { demoAgent } from './model'
3
+ import type { UseSessionResult } from 'experimental-a2/react'
4
+ import { demoAgent, type AgentState } from './model'
5
5
 
6
+ /** Shared by the first push and the session page, so one live session serves both. */
6
7
  export const agentClient = createClient({
7
- reducer: demoAgent.reducer,
8
+ contract: demoAgent,
8
9
  api: '/agent/events',
9
10
  })
10
11
 
11
- export const { SessionProvider, useSession } = createReact({
12
- client: agentClient,
13
- })
12
+ export type AgentSession = UseSessionResult<typeof demoAgent.events, AgentState>