experimental-a2 0.16.1 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (333) hide show
  1. package/CHANGELOG.md +352 -0
  2. package/dist/{actor-ohPC-81x.d.ts → actor-DBbe5xeE.d.ts} +21 -11
  3. package/dist/actor-DBbe5xeE.d.ts.map +1 -0
  4. package/dist/actor-client.d.ts +2 -5
  5. package/dist/actor-client.d.ts.map +1 -1
  6. package/dist/actor-client.js +13 -4
  7. package/dist/actor-client.js.map +1 -1
  8. package/dist/actor-react.d.ts +5 -3
  9. package/dist/actor-react.d.ts.map +1 -1
  10. package/dist/actor-react.js +20 -10
  11. package/dist/actor-react.js.map +1 -1
  12. package/dist/{actor-shared-B5tJfzt-.js → actor-shared-t7w8xIKP.js} +12 -7
  13. package/dist/actor-shared-t7w8xIKP.js.map +1 -0
  14. package/dist/actor.d.ts +1 -1
  15. package/dist/actor.js +42 -23
  16. package/dist/actor.js.map +1 -1
  17. package/dist/{ai-DgOBltJ_.js → ai-UrWfKeuv.js} +621 -216
  18. package/dist/ai-UrWfKeuv.js.map +1 -0
  19. package/dist/ai-client.d.ts +27 -0
  20. package/dist/ai-client.d.ts.map +1 -0
  21. package/dist/ai-client.js +29 -0
  22. package/dist/ai-client.js.map +1 -0
  23. package/dist/ai-react.d.ts +96 -0
  24. package/dist/ai-react.d.ts.map +1 -0
  25. package/dist/ai-react.js +142 -0
  26. package/dist/ai-react.js.map +1 -0
  27. package/dist/ai-server.browser.js +2 -2
  28. package/dist/ai-server.browser.js.map +1 -1
  29. package/dist/ai-server.d.ts +80 -36
  30. package/dist/ai-server.d.ts.map +1 -1
  31. package/dist/ai-server.js +432 -258
  32. package/dist/ai-server.js.map +1 -1
  33. package/dist/{ai-CrEf6p_W.d.ts → ai-vpGtcaCB.d.ts} +114 -100
  34. package/dist/ai-vpGtcaCB.d.ts.map +1 -0
  35. package/dist/ai.d.ts +2 -2
  36. package/dist/ai.js +2 -2
  37. package/dist/{client-BYzHjkwU.js → client-COaiaDub.js} +447 -80
  38. package/dist/client-COaiaDub.js.map +1 -0
  39. package/dist/{client-CzyacQpJ.d.ts → client-qRnRUzBS.d.ts} +14 -13
  40. package/dist/client-qRnRUzBS.d.ts.map +1 -0
  41. package/dist/client.d.ts +4 -2
  42. package/dist/client.js +1 -1
  43. package/dist/{contract-CKRg_E4q.js → contract-BYKASlCV.js} +12 -21
  44. package/dist/contract-BYKASlCV.js.map +1 -0
  45. package/dist/{idempotent-replay-DVOlyYbx.js → idempotent-replay-JdgMQRRm.js} +2 -2
  46. package/dist/{idempotent-replay-DVOlyYbx.js.map → idempotent-replay-JdgMQRRm.js.map} +1 -1
  47. package/dist/index.d.ts +6 -5
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +4 -3
  50. package/dist/{errors-DvhSXnxk.d.ts → initial-snapshot-BrLso5x_.d.ts} +7 -3
  51. package/dist/initial-snapshot-BrLso5x_.d.ts.map +1 -0
  52. package/dist/{internal-Dq2qYxou.js → internal-CbVvEwad.js} +23 -2
  53. package/dist/internal-CbVvEwad.js.map +1 -0
  54. package/dist/otel.d.ts +1 -1
  55. package/dist/react-BbJ-g6Uu.d.ts +86 -0
  56. package/dist/react-BbJ-g6Uu.d.ts.map +1 -0
  57. package/dist/react.d.ts +2 -122
  58. package/dist/react.js +107 -100
  59. package/dist/react.js.map +1 -1
  60. package/dist/reducer-BAXc8JyF.js +87 -0
  61. package/dist/reducer-BAXc8JyF.js.map +1 -0
  62. package/dist/{reducer-otzHjuJj.d.ts → reducer-BYu6DT7h.d.ts} +45 -29
  63. package/dist/reducer-BYu6DT7h.d.ts.map +1 -0
  64. package/dist/reducer-runtime-DQwz_J59.js +28 -0
  65. package/dist/reducer-runtime-DQwz_J59.js.map +1 -0
  66. package/dist/scheduler-qstash.d.ts +3 -3
  67. package/dist/scheduler-qstash.d.ts.map +1 -1
  68. package/dist/scheduler-qstash.js +33 -10
  69. package/dist/scheduler-qstash.js.map +1 -1
  70. package/dist/{scheduler-task-BpzhPnRS.js → scheduler-task-X1SToDjP.js} +18 -3
  71. package/dist/scheduler-task-X1SToDjP.js.map +1 -0
  72. package/dist/scheduler-vercel.d.ts +4 -9
  73. package/dist/scheduler-vercel.d.ts.map +1 -1
  74. package/dist/scheduler-vercel.js +25 -44
  75. package/dist/scheduler-vercel.js.map +1 -1
  76. package/dist/{server-DbD7IJVK.d.ts → server-CGM8WzRG.d.ts} +87 -26
  77. package/dist/server-CGM8WzRG.d.ts.map +1 -0
  78. package/dist/{server-BD5ckxZb.js → server-Rj64fNvX.js} +854 -202
  79. package/dist/server-Rj64fNvX.js.map +1 -0
  80. package/dist/server.d.ts +4 -4
  81. package/dist/server.js +2 -2
  82. package/dist/{errors-DCk6ch5n.js → session-clear-DGI4jTQ5.js} +17 -2
  83. package/dist/session-clear-DGI4jTQ5.js.map +1 -0
  84. package/dist/session-clear-Dshpuhff.d.ts +12 -0
  85. package/dist/session-clear-Dshpuhff.d.ts.map +1 -0
  86. package/dist/{store-DyZM6fS5.d.ts → store-Gpkq7WBo.d.ts} +44 -3
  87. package/dist/store-Gpkq7WBo.d.ts.map +1 -0
  88. package/dist/store-N8PXxDAS.js.map +1 -1
  89. package/dist/store-memory.d.ts +1 -1
  90. package/dist/store-memory.d.ts.map +1 -1
  91. package/dist/store-memory.js +182 -69
  92. package/dist/store-memory.js.map +1 -1
  93. package/dist/{store-polling-CmxUbV93.js → store-polling-4_y8lETm.js} +21 -4
  94. package/dist/store-polling-4_y8lETm.js.map +1 -0
  95. package/dist/store-postgres.d.ts +1 -1
  96. package/dist/store-postgres.d.ts.map +1 -1
  97. package/dist/store-postgres.js +399 -108
  98. package/dist/store-postgres.js.map +1 -1
  99. package/dist/{store-presence-polling-C7-XZyW9.js → store-presence-polling-Q1C-k2dd.js} +3 -3
  100. package/dist/{store-presence-polling-C7-XZyW9.js.map → store-presence-polling-Q1C-k2dd.js.map} +1 -1
  101. package/dist/store-redis-http.d.ts +1 -1
  102. package/dist/store-redis-http.d.ts.map +1 -1
  103. package/dist/store-redis-http.js +293 -11
  104. package/dist/store-redis-http.js.map +1 -1
  105. package/dist/store-redis-notify-tqWNnMRu.js +2374 -0
  106. package/dist/store-redis-notify-tqWNnMRu.js.map +1 -0
  107. package/dist/store-redis.d.ts +1 -1
  108. package/dist/store-redis.d.ts.map +1 -1
  109. package/dist/store-redis.js +11 -11
  110. package/dist/store-redis.js.map +1 -1
  111. package/dist/store-sqlite.d.ts +1 -1
  112. package/dist/store-sqlite.d.ts.map +1 -1
  113. package/dist/store-sqlite.js +166 -32
  114. package/dist/store-sqlite.js.map +1 -1
  115. package/dist/stream-buffer-Cx7AAPGq.js +23 -0
  116. package/dist/stream-buffer-Cx7AAPGq.js.map +1 -0
  117. package/dist/{telemetry-B5jzpy6y.d.ts → telemetry-cZH-cibj.d.ts} +2 -2
  118. package/dist/telemetry-cZH-cibj.d.ts.map +1 -0
  119. package/dist/{wire-BO5wWCb1.js → wire-DjfHOvdW.js} +132 -10
  120. package/dist/wire-DjfHOvdW.js.map +1 -0
  121. package/docs/01-quickstart.mdx +2 -2
  122. package/docs/actors/01-introduction.mdx +4 -0
  123. package/docs/actors/04-routes.mdx +14 -1
  124. package/docs/concepts/01-contracts.mdx +95 -0
  125. package/docs/concepts/04-state.mdx +3 -3
  126. package/docs/guides/01-timers.mdx +22 -14
  127. package/docs/guides/02-cancellation.mdx +1 -1
  128. package/docs/guides/03-react.mdx +268 -328
  129. package/docs/guides/04-local-first.mdx +32 -73
  130. package/docs/guides/05-production.mdx +40 -27
  131. package/docs/guides/06-ai-agents.mdx +671 -581
  132. package/docs/guides/09-presence.mdx +3 -7
  133. package/docs/guides/10-transports.mdx +5 -5
  134. package/docs/index.mdx +10 -18
  135. package/docs/reference/01-api.mdx +656 -194
  136. package/docs/reference/02-errors.mdx +2 -1
  137. package/examples/playground/DEPLOY.md +52 -6
  138. package/examples/playground/app/a0/[appId]/loading.tsx +16 -0
  139. package/examples/playground/app/a0/[appId]/page.tsx +48 -0
  140. package/examples/playground/app/a0/a0.css +818 -0
  141. package/examples/playground/app/a0/app-list.tsx +211 -0
  142. package/examples/playground/app/a0/app-view.tsx +55 -0
  143. package/examples/playground/app/a0/apps/[appId]/preview/route.ts +27 -0
  144. package/examples/playground/app/a0/apps/[appId]/route.ts +23 -0
  145. package/examples/playground/app/a0/apps.test.ts +116 -0
  146. package/examples/playground/app/a0/apps.ts +87 -0
  147. package/examples/playground/app/a0/chat.tsx +364 -0
  148. package/examples/playground/app/a0/columns.tsx +109 -0
  149. package/examples/playground/app/a0/context-menu.tsx +116 -0
  150. package/examples/playground/app/a0/cookies.ts +46 -0
  151. package/examples/playground/app/a0/crash-on-request.ts +72 -0
  152. package/examples/playground/app/a0/crashes/route.ts +3 -0
  153. package/examples/playground/app/a0/crashes.test.ts +33 -0
  154. package/examples/playground/app/a0/crashes.ts +28 -0
  155. package/examples/playground/app/a0/devtools.tsx +164 -0
  156. package/examples/playground/app/a0/events/route.ts +6 -0
  157. package/examples/playground/app/a0/flags.ts +30 -0
  158. package/examples/playground/app/a0/layout.tsx +38 -0
  159. package/examples/playground/app/a0/list/route.ts +10 -0
  160. package/examples/playground/app/a0/model.test.ts +56 -0
  161. package/examples/playground/app/a0/model.ts +58 -0
  162. package/examples/playground/app/a0/page.tsx +14 -0
  163. package/examples/playground/app/a0/preview-pane.tsx +145 -0
  164. package/examples/playground/app/a0/preview.test.ts +53 -0
  165. package/examples/playground/app/a0/preview.ts +26 -0
  166. package/examples/playground/app/a0/sandbox-log.ts +69 -0
  167. package/examples/playground/app/a0/sandbox.ts +191 -0
  168. package/examples/playground/app/a0/sandboxes/scheduler/route.ts +6 -0
  169. package/examples/playground/app/a0/scheduler/route.ts +7 -0
  170. package/examples/playground/app/a0/server.ts +158 -0
  171. package/examples/playground/app/a0/session.ts +47 -0
  172. package/examples/playground/app/a0/shell.tsx +150 -0
  173. package/examples/playground/app/a0/short/events/route.ts +9 -0
  174. package/examples/playground/app/a0/splitter.tsx +120 -0
  175. package/examples/playground/app/agent/[agentId]/agent-client.tsx +144 -174
  176. package/examples/playground/app/agent/[agentId]/command-receipt.test.tsx +65 -0
  177. package/examples/playground/app/agent/[agentId]/command-receipt.tsx +27 -0
  178. package/examples/playground/app/agent/[agentId]/compaction-event.tsx +25 -24
  179. package/examples/playground/app/agent/[agentId]/compaction-panel.test.tsx +119 -0
  180. package/examples/playground/app/agent/[agentId]/compaction-panel.tsx +208 -165
  181. package/examples/playground/app/agent/[agentId]/page.tsx +2 -16
  182. package/examples/playground/app/agent/events/route.ts +5 -2
  183. package/examples/playground/app/agent/ingress.test.ts +98 -0
  184. package/examples/playground/app/agent/model-input.test.tsx +58 -0
  185. package/examples/playground/app/agent/model-input.tsx +115 -0
  186. package/examples/playground/app/agent/model.test.ts +73 -0
  187. package/examples/playground/app/agent/model.ts +58 -26
  188. package/examples/playground/app/agent/new-agent-session.tsx +37 -19
  189. package/examples/playground/app/agent/page.tsx +4 -4
  190. package/examples/playground/app/agent/server.ts +15 -14
  191. package/examples/playground/app/agent/session.ts +5 -6
  192. package/examples/playground/app/agent/telemetry.test.ts +168 -0
  193. package/examples/playground/app/agent/telemetry.ts +30 -0
  194. package/examples/playground/app/agent/trace.test.ts +23 -0
  195. package/examples/playground/app/agent/trace.ts +18 -0
  196. package/examples/playground/app/canvas/[canvasId]/canvas-client.tsx +11 -11
  197. package/examples/playground/app/canvas/[canvasId]/page.tsx +1 -2
  198. package/examples/playground/app/canvas/[canvasId]/session.ts +2 -2
  199. package/examples/playground/app/canvas/model.ts +53 -53
  200. package/examples/playground/app/chat/[chatId]/agent-state.test.ts +176 -0
  201. package/examples/playground/app/chat/[chatId]/agent-stream-drawer.tsx +16 -1
  202. package/examples/playground/app/chat/[chatId]/chat-client.tsx +55 -77
  203. package/examples/playground/app/chat/[chatId]/chat-view.ts +37 -3
  204. package/examples/playground/app/chat/[chatId]/page.tsx +7 -15
  205. package/examples/playground/app/chat/[chatId]/session.ts +2 -19
  206. package/examples/playground/app/chat/agent/route.ts +9 -0
  207. package/examples/playground/app/chat/model.test.ts +46 -119
  208. package/examples/playground/app/chat/model.ts +82 -128
  209. package/examples/playground/app/chat/scheduler/route.ts +2 -2
  210. package/examples/playground/app/chat/server.test.ts +108 -0
  211. package/examples/playground/app/chat/server.ts +69 -73
  212. package/examples/playground/app/components/activity-feed.tsx +30 -1
  213. package/examples/playground/app/components/activity-history.test.tsx +35 -0
  214. package/examples/playground/app/components/demo-icons.tsx +89 -0
  215. package/examples/playground/app/components/demos.tsx +33 -0
  216. package/examples/playground/app/components/fast-link.test.tsx +58 -0
  217. package/examples/playground/app/components/fast-link.tsx +55 -0
  218. package/examples/playground/app/components/site-header.tsx +96 -0
  219. package/examples/playground/app/counter/counter-client.tsx +3 -3
  220. package/examples/playground/app/counter/model.test.ts +3 -3
  221. package/examples/playground/app/counter/model.ts +11 -21
  222. package/examples/playground/app/counter/page.tsx +2 -15
  223. package/examples/playground/app/counter/session.ts +2 -2
  224. package/examples/playground/app/documents/[documentId]/code-editor.tsx +12 -1
  225. package/examples/playground/app/documents/[documentId]/document-client.tsx +10 -11
  226. package/examples/playground/app/documents/[documentId]/page.tsx +1 -2
  227. package/examples/playground/app/documents/[documentId]/session.ts +2 -2
  228. package/examples/playground/app/documents/model.ts +27 -27
  229. package/examples/playground/app/globals.css +420 -207
  230. package/examples/playground/app/initial-state.test.tsx +54 -0
  231. package/examples/playground/app/layout.tsx +7 -23
  232. package/examples/playground/app/orders/[orderId]/page.tsx +1 -2
  233. package/examples/playground/app/orders/[orderId]/session.ts +2 -2
  234. package/examples/playground/app/orders/model.ts +49 -49
  235. package/examples/playground/app/page.tsx +13 -102
  236. package/examples/playground/app/recovery/[recoveryId]/page.tsx +1 -2
  237. package/examples/playground/app/recovery/[recoveryId]/session.ts +2 -2
  238. package/examples/playground/app/recovery/model.ts +30 -30
  239. package/examples/playground/instrumentation.ts +5 -0
  240. package/examples/playground/package.json +10 -2
  241. package/examples/playground/scripts/a0-template.ts +77 -0
  242. package/examples/playground/vercel.json +18 -0
  243. package/package.json +3 -1
  244. package/src/actor-client.ts +16 -4
  245. package/src/actor-react.ts +25 -11
  246. package/src/actor-shared.ts +21 -6
  247. package/src/actor.ts +76 -31
  248. package/src/ai-agent-actions.ts +219 -0
  249. package/src/ai-agent.ts +2343 -0
  250. package/src/ai-app-state.ts +91 -0
  251. package/src/ai-client-state.ts +6 -3
  252. package/src/ai-client.ts +62 -0
  253. package/src/ai-context-schema.ts +25 -5
  254. package/src/ai-context.ts +90 -40
  255. package/src/ai-control-server.ts +92 -72
  256. package/src/ai-coordinator.ts +23 -22
  257. package/src/ai-message-base.ts +209 -0
  258. package/src/ai-message-projection.ts +262 -64
  259. package/src/ai-progress-batches.ts +26 -0
  260. package/src/ai-projector.ts +95 -14
  261. package/src/ai-react.ts +146 -0
  262. package/src/ai-server.browser.ts +2 -2
  263. package/src/ai-server.ts +20 -1964
  264. package/src/ai-stored-state.ts +48 -163
  265. package/src/ai.ts +339 -155
  266. package/src/bootstrap-stream.ts +72 -0
  267. package/src/bounded-cache.ts +55 -0
  268. package/src/client-render-session.ts +49 -0
  269. package/src/client.ts +688 -151
  270. package/src/contract.ts +92 -42
  271. package/src/deterministic-id.ts +37 -0
  272. package/src/equal-json.ts +32 -0
  273. package/src/errors.ts +13 -1
  274. package/src/index.ts +5 -1
  275. package/src/initial-snapshot.ts +39 -0
  276. package/src/internal.ts +45 -0
  277. package/src/postgres-notification-scope.ts +14 -3
  278. package/src/react.ts +171 -206
  279. package/src/redis-http-reply.ts +256 -0
  280. package/src/redis-http-stream.ts +73 -0
  281. package/src/redis-http-subscriptions.ts +13 -5
  282. package/src/reducer-runtime.ts +52 -0
  283. package/src/reducer.ts +92 -10
  284. package/src/scheduler-qstash.ts +49 -20
  285. package/src/scheduler-task.ts +34 -2
  286. package/src/scheduler-vercel.ts +32 -84
  287. package/src/server-fetch.ts +139 -35
  288. package/src/server.ts +957 -237
  289. package/src/session-clear.ts +23 -0
  290. package/src/session-socket.ts +154 -49
  291. package/src/sse.ts +71 -4
  292. package/src/store-memory.ts +220 -78
  293. package/src/store-polling.ts +18 -4
  294. package/src/store-postgres.ts +461 -114
  295. package/src/store-redis-core.ts +1260 -434
  296. package/src/store-redis-http.ts +18 -2
  297. package/src/store-redis-notify.ts +260 -85
  298. package/src/store-redis.ts +19 -9
  299. package/src/store-sqlite.ts +259 -45
  300. package/src/store.ts +46 -1
  301. package/src/stream-buffer.ts +43 -0
  302. package/src/telemetry.ts +13 -1
  303. package/src/wire.ts +241 -13
  304. package/dist/actor-ohPC-81x.d.ts.map +0 -1
  305. package/dist/actor-shared-B5tJfzt-.js.map +0 -1
  306. package/dist/ai-CrEf6p_W.d.ts.map +0 -1
  307. package/dist/ai-DgOBltJ_.js.map +0 -1
  308. package/dist/client-BYzHjkwU.js.map +0 -1
  309. package/dist/client-CzyacQpJ.d.ts.map +0 -1
  310. package/dist/contract-CKRg_E4q.js.map +0 -1
  311. package/dist/errors-DCk6ch5n.js.map +0 -1
  312. package/dist/errors-DvhSXnxk.d.ts.map +0 -1
  313. package/dist/internal-Dq2qYxou.js.map +0 -1
  314. package/dist/react.d.ts.map +0 -1
  315. package/dist/reducer-DEMjEY_O.js +0 -29
  316. package/dist/reducer-DEMjEY_O.js.map +0 -1
  317. package/dist/reducer-otzHjuJj.d.ts.map +0 -1
  318. package/dist/scheduler-task-BpzhPnRS.js.map +0 -1
  319. package/dist/server-BD5ckxZb.js.map +0 -1
  320. package/dist/server-DbD7IJVK.d.ts.map +0 -1
  321. package/dist/store-DyZM6fS5.d.ts.map +0 -1
  322. package/dist/store-polling-CmxUbV93.js.map +0 -1
  323. package/dist/store-redis-notify-BVLUfI3j.js +0 -1583
  324. package/dist/store-redis-notify-BVLUfI3j.js.map +0 -1
  325. package/dist/telemetry-B5jzpy6y.d.ts.map +0 -1
  326. package/dist/validate-XKT4FSNn.js +0 -30
  327. package/dist/validate-XKT4FSNn.js.map +0 -1
  328. package/dist/wire-BO5wWCb1.js.map +0 -1
  329. package/examples/playground/app/agent/[agentId]/compaction/route.ts +0 -14
  330. package/examples/playground/app/agent/compaction-settings.test.ts +0 -144
  331. package/examples/playground/app/agent/compaction-settings.ts +0 -49
  332. package/examples/playground/app/agent/compaction-timeline.test.ts +0 -377
  333. package/examples/playground/app/agent/compaction-timeline.ts +0 -208
@@ -0,0 +1,168 @@
1
+ import { context, trace } from '@opentelemetry/api'
2
+ import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'
3
+ import {
4
+ BasicTracerProvider,
5
+ InMemorySpanExporter,
6
+ SimpleSpanProcessor,
7
+ } from '@opentelemetry/sdk-trace-base'
8
+ import { simulateReadableStream } from 'ai'
9
+ import { MockLanguageModelV4 } from 'ai/test'
10
+ import { inputs } from 'experimental-a2/ai'
11
+ import { agent } from 'experimental-a2/ai/server'
12
+ import { memory } from 'experimental-a2/store-memory'
13
+ import { otel } from 'experimental-a2/otel'
14
+ import { afterAll, beforeAll, beforeEach, expect, it, vi } from 'vitest'
15
+ import { demoAgent } from './model'
16
+ import { modelTelemetry, traceMetadata } from './telemetry'
17
+ import { traceUrl } from './trace'
18
+
19
+ const exporter = new InMemorySpanExporter()
20
+ const openSpans = new Set<string>()
21
+ const provider = new BasicTracerProvider({
22
+ spanProcessors: [
23
+ new SimpleSpanProcessor(exporter),
24
+ {
25
+ onStart: (span) => {
26
+ openSpans.add(span.spanContext().spanId)
27
+ },
28
+ onEnd: (span) => {
29
+ openSpans.delete(span.spanContext().spanId)
30
+ },
31
+ forceFlush: async () => {},
32
+ shutdown: async () => {},
33
+ },
34
+ ],
35
+ })
36
+ beforeAll(() => {
37
+ context.setGlobalContextManager(
38
+ new AsyncLocalStorageContextManager().enable(),
39
+ )
40
+ trace.setGlobalTracerProvider(provider)
41
+ })
42
+ beforeEach(() => {
43
+ exporter.reset()
44
+ })
45
+ afterAll(async () => {
46
+ await provider.shutdown()
47
+ trace.disable()
48
+ context.disable()
49
+ })
50
+
51
+ it.each(['complete', 'stop'] as const)(
52
+ 'correlates standard SDK traces and releases spans when generation ends with %s',
53
+ async (outcome) => {
54
+ const server = agent({
55
+ contract: demoAgent,
56
+ store: memory(),
57
+ telemetry: otel(),
58
+ generation: { telemetry: modelTelemetry() },
59
+ messageMetadata: traceMetadata,
60
+ model: new MockLanguageModelV4({
61
+ modelId: 'test-model',
62
+ doStream: async ({ abortSignal }) => ({
63
+ stream:
64
+ outcome === 'complete'
65
+ ? simulateReadableStream({
66
+ chunks: [
67
+ { type: 'stream-start', warnings: [] },
68
+ { type: 'text-start', id: 'text' },
69
+ { type: 'text-delta', id: 'text', delta: 'PRIVATE_OUTPUT' },
70
+ { type: 'text-end', id: 'text' },
71
+ {
72
+ type: 'finish',
73
+ finishReason: { unified: 'stop', raw: 'stop' },
74
+ usage: {
75
+ inputTokens: {
76
+ total: 5,
77
+ noCache: 5,
78
+ cacheRead: 0,
79
+ cacheWrite: 0,
80
+ },
81
+ outputTokens: { total: 2, text: 2, reasoning: 0 },
82
+ },
83
+ providerMetadata: {
84
+ test: { secret: 'PRIVATE_METADATA' },
85
+ },
86
+ },
87
+ ],
88
+ })
89
+ : new ReadableStream({
90
+ start(controller) {
91
+ controller.enqueue({ type: 'stream-start', warnings: [] })
92
+ controller.enqueue({ type: 'text-start', id: 'text' })
93
+ controller.enqueue({
94
+ type: 'text-delta',
95
+ id: 'text',
96
+ delta: 'PRIVATE_PARTIAL',
97
+ })
98
+ abortSignal?.addEventListener(
99
+ 'abort',
100
+ () => controller.close(),
101
+ { once: true },
102
+ )
103
+ },
104
+ }),
105
+ }),
106
+ }),
107
+ })
108
+ const session = server.session(`trace-${outcome}`)
109
+ await session.append(
110
+ ...inputs.message({
111
+ id: 'user',
112
+ role: 'user',
113
+ parts: [{ type: 'text', text: 'PRIVATE_PROMPT' }],
114
+ }),
115
+ )
116
+ if (outcome === 'stop') {
117
+ await vi.waitUntil(async () => {
118
+ const { state } = await session.state(demoAgent.reducer)
119
+ return state.messages
120
+ .at(-1)
121
+ ?.parts.some(
122
+ (part) => part.type === 'text' && part.text === 'PRIVATE_PARTIAL',
123
+ )
124
+ })
125
+ const { state } = await session.state(demoAgent.reducer)
126
+ await session.append(
127
+ ...inputs.stop({
128
+ turnId: state.active!.turnId,
129
+ }),
130
+ )
131
+ }
132
+ await vi.waitFor(async () => {
133
+ const { state } = await session.state(demoAgent.reducer)
134
+ const view = state
135
+ expect(view.status).toBe('idle')
136
+ expect(view.messages.at(-1)?.role).toBe('assistant')
137
+ expect(openSpans.size).toBe(0)
138
+ })
139
+ const { state } = await session.state(demoAgent.reducer)
140
+ const metadata = state.messages.at(-1)?.metadata
141
+ const spans = exporter.getFinishedSpans()
142
+ const generation = spans.find((span) => span.name === 'a2.ai.generate')!
143
+ expect(metadata).toEqual({ traceId: generation.spanContext().traceId })
144
+ expect(traceUrl(metadata)).toContain(generation.spanContext().traceId)
145
+ const models = spans.filter(
146
+ (span) => span.instrumentationScope.name === 'gen_ai',
147
+ )
148
+ expect(models.length).toBeGreaterThan(0)
149
+ expect(
150
+ models.every(
151
+ (span) =>
152
+ span.spanContext().traceId === generation.spanContext().traceId,
153
+ ),
154
+ ).toBe(true)
155
+ expect(spans.some((span) => span.name.startsWith('a2.model'))).toBe(false)
156
+ expect(
157
+ JSON.stringify(
158
+ spans.map(({ attributes, events }) => ({ attributes, events })),
159
+ ),
160
+ ).not.toContain('PRIVATE_')
161
+ },
162
+ )
163
+
164
+ it('clears an earlier trace when no active trace is available', () => {
165
+ expect(
166
+ traceMetadata({ generationId: 'next', part: { type: 'start' } }),
167
+ ).toEqual({ traceId: null })
168
+ })
@@ -0,0 +1,30 @@
1
+ import { OpenTelemetry } from '@ai-sdk/otel'
2
+ import { isSpanContextValid, trace } from '@opentelemetry/api'
3
+ import type { TelemetryOptions, TextStreamPart, ToolSet } from 'ai'
4
+
5
+ export function modelTelemetry(): TelemetryOptions {
6
+ return {
7
+ recordInputs: false,
8
+ recordOutputs: false,
9
+ integrations: [new OpenTelemetry()],
10
+ }
11
+ }
12
+
13
+ export function traceMetadata({
14
+ generationId,
15
+ part,
16
+ }: {
17
+ generationId: string
18
+ part: TextStreamPart<ToolSet>
19
+ }): { traceId: string | null } | undefined {
20
+ if (part.type !== 'start') return
21
+ const span = trace.getActiveSpan()
22
+ span?.setAttribute('a2.generation_id', generationId)
23
+ const spanContext = span?.spanContext()
24
+ return {
25
+ traceId:
26
+ spanContext && isSpanContextValid(spanContext)
27
+ ? spanContext.traceId
28
+ : null,
29
+ }
30
+ }
@@ -0,0 +1,23 @@
1
+ import { expect, it } from 'vitest'
2
+ import { traceUrl } from './trace'
3
+
4
+ const traceId = '1234567890abcdef1234567890abcdef'
5
+
6
+ it('links current and previously recorded trace metadata to Vercel', () => {
7
+ const current = traceUrl({ traceId })!
8
+ expect(traceUrl({ timing: { traceId } })).toBe(current)
9
+ const url = new URL(current)
10
+ expect(url.origin).toBe('https://vercel.com')
11
+ expect(url.searchParams.get('search')).toBe(`traceId:${traceId}`)
12
+ })
13
+
14
+ it('does not reuse an older trace after the latest generation clears it', () => {
15
+ expect(traceUrl({ traceId: null, timing: { traceId } })).toBeUndefined()
16
+ })
17
+
18
+ it.each([undefined, null, {}, { traceId: 'https://example.com' }])(
19
+ 'omits the link for invalid metadata %j',
20
+ (metadata) => {
21
+ expect(traceUrl(metadata)).toBeUndefined()
22
+ },
23
+ )
@@ -0,0 +1,18 @@
1
+ import { z } from 'zod'
2
+
3
+ const traceId = z.string().regex(/^[0-9a-f]{32}$/)
4
+ const metadataSchema = z.object({
5
+ traceId: traceId.nullable().optional(),
6
+ timing: z.object({ traceId: traceId.optional() }).optional(),
7
+ })
8
+
9
+ export function traceUrl(metadata: unknown): string | undefined {
10
+ const parsed = metadataSchema.safeParse(metadata)
11
+ if (!parsed.success) return
12
+ const id =
13
+ parsed.data.traceId === undefined
14
+ ? parsed.data.timing?.traceId
15
+ : parsed.data.traceId
16
+ if (id)
17
+ return `https://vercel.com/vercel-labs/a2-playground/logs?${new URLSearchParams({ search: `traceId:${id}`, timeline: 'past2Weeks' })}`
18
+ }
@@ -1,4 +1,5 @@
1
1
  'use client'
2
+ import type { StateSnapshot } from 'experimental-a2'
2
3
  import type {
3
4
  CSSProperties,
4
5
  PointerEvent as ReactPointerEvent,
@@ -30,11 +31,11 @@ const ADJECTIVES = ['Bright', 'Calm', 'Curious', 'Kind', 'Swift'] as const
30
31
  const ANIMALS = ['Fox', 'Heron', 'Otter', 'Panda', 'Wren'] as const
31
32
  const COLORS = ['teal', 'amber', 'blue', 'violet', 'rose'] as const
32
33
  const COLOR_HEX: Record<PaletteColor, string> = {
33
- teal: '#16836a',
34
- amber: '#bd7917',
35
- blue: '#3874c8',
36
- violet: '#7956b6',
37
- rose: '#bd5269',
34
+ teal: '#3ccf9f',
35
+ amber: '#f2b54b',
36
+ blue: '#6ea8fe',
37
+ violet: '#a98bff',
38
+ rose: '#ff7a93',
38
39
  }
39
40
 
40
41
  /** The tab's identity: an opaque participant id, plus the name and
@@ -191,13 +192,12 @@ function Smoothed({
191
192
 
192
193
  export function CanvasSession({
193
194
  canvasId,
194
- initialState,
195
- initialIndex,
195
+ initial,
196
196
  }: {
197
197
  canvasId: string
198
- initialState: CanvasState
199
- initialIndex: number
198
+ initial: StateSnapshot<CanvasState>
200
199
  }): ReactNode {
200
+ const { state: initialState, index: initialIndex } = initial
201
201
  // The server snapshot is null so hydration matches exactly. The
202
202
  // browser snapshot then mints one stable tab identity before presence
203
203
  // becomes interactive.
@@ -205,8 +205,8 @@ export function CanvasSession({
205
205
  return (
206
206
  <SessionProvider
207
207
  sessionId={canvasId}
208
- initialState={initialState}
209
- initialIndex={initialIndex}
208
+ initial={initial}
209
+
210
210
  {...(identity ? { participant: identity.id } : {})}
211
211
  >
212
212
  <CanvasClient
@@ -15,8 +15,7 @@ export default async function CanvasSessionPage({
15
15
  return (
16
16
  <CanvasSession
17
17
  canvasId={canvasId}
18
- initialState={state}
19
- initialIndex={index}
18
+ initial={{ state: state, index: index }}
20
19
  />
21
20
  )
22
21
  }
@@ -1,14 +1,14 @@
1
1
  'use client'
2
2
  import { createClient } from 'experimental-a2/client'
3
3
  import { createReact } from 'experimental-a2/react'
4
- import { canvasReducer } from '../model'
4
+ import { canvas } from '../model'
5
5
 
6
6
  // One route, two wires: on Vercel (production and previews), pushes
7
7
  // and presence ride one WebSocket. Anywhere else, including `next
8
8
  // dev` (which drops the upgrade handshake before the route runs) and
9
9
  // a local `next start`, the same route serves the SSE + POST pair.
10
10
  export const canvasClient = createClient({
11
- reducer: canvasReducer,
11
+ contract: canvas,
12
12
  api: process.env.NEXT_PUBLIC_VERCEL_ENV
13
13
  ? { type: 'ws', url: '/canvas/events' }
14
14
  : '/canvas/events',
@@ -5,6 +5,24 @@ const paletteSchema = z.enum(['teal', 'amber', 'blue', 'violet', 'rose'])
5
5
 
6
6
  export type PaletteColor = z.output<typeof paletteSchema>
7
7
 
8
+ const canvasShapeSchema = z.object({
9
+ id: z.string(),
10
+ kind: z.enum(['rect', 'ellipse']),
11
+ x: z.number(),
12
+ y: z.number(),
13
+ w: z.number(),
14
+ h: z.number(),
15
+ color: paletteSchema,
16
+ movedAt: z.number(),
17
+ })
18
+
19
+ const canvasState = z.object({
20
+ shapes: z.record(z.string(), canvasShapeSchema),
21
+ })
22
+
23
+ export type CanvasState = z.output<typeof canvasState>
24
+ export type CanvasShape = CanvasState['shapes'][string]
25
+
8
26
  export const canvas = a2.contract({
9
27
  name: 'canvas',
10
28
  events: {
@@ -32,63 +50,45 @@ export const canvas = a2.contract({
32
50
  cursor: z.object({ x: z.number(), y: z.number() }),
33
51
  dragging: z.object({ shapeId: z.string(), x: z.number(), y: z.number() }),
34
52
  },
35
- })
36
53
 
37
- const canvasShapeSchema = z.object({
38
- id: z.string(),
39
- kind: z.enum(['rect', 'ellipse']),
40
- x: z.number(),
41
- y: z.number(),
42
- w: z.number(),
43
- h: z.number(),
44
- color: paletteSchema,
45
- movedAt: z.number(),
46
- })
47
-
48
- const canvasState = z.object({
49
- shapes: z.record(z.string(), canvasShapeSchema),
50
- })
51
-
52
- export type CanvasState = z.output<typeof canvasState>
53
- export type CanvasShape = CanvasState['shapes'][string]
54
-
55
- export const canvasReducer = canvas
56
- .reducer({
54
+ reducer: {
57
55
  name: 'canvas-scene',
58
56
  initialState: { shapes: {} },
59
57
  stateSchema: canvasState,
60
- })
61
- .fold((state, event) => {
62
- switch (event.type) {
63
- case 'shape.created':
64
- return {
65
- shapes: {
66
- ...state.shapes,
67
- [event.payload.id]: { ...event.payload, movedAt: event.index },
68
- },
69
- }
70
- case 'shape.moved': {
71
- const shape = state.shapes[event.payload.id]
72
- if (!shape) return state
73
- return {
74
- shapes: {
75
- ...state.shapes,
76
- [shape.id]: {
77
- ...shape,
78
- x: event.payload.x,
79
- y: event.payload.y,
80
- movedAt: event.index,
58
+ fold: (state, event) => {
59
+ switch (event.type) {
60
+ case 'shape.created':
61
+ return {
62
+ shapes: {
63
+ ...state.shapes,
64
+ [event.payload.id]: { ...event.payload, movedAt: event.index },
81
65
  },
82
- },
66
+ }
67
+ case 'shape.moved': {
68
+ const shape = state.shapes[event.payload.id]
69
+ if (!shape) return state
70
+ return {
71
+ shapes: {
72
+ ...state.shapes,
73
+ [shape.id]: {
74
+ ...shape,
75
+ x: event.payload.x,
76
+ y: event.payload.y,
77
+ movedAt: event.index,
78
+ },
79
+ },
80
+ }
83
81
  }
82
+ case 'shape.deleted': {
83
+ if (!state.shapes[event.payload.id]) return state
84
+ const shapes = { ...state.shapes }
85
+ delete shapes[event.payload.id]
86
+ return { shapes }
87
+ }
88
+ default:
89
+ return state
84
90
  }
85
- case 'shape.deleted': {
86
- if (!state.shapes[event.payload.id]) return state
87
- const shapes = { ...state.shapes }
88
- delete shapes[event.payload.id]
89
- return { shapes }
90
- }
91
- default:
92
- return state
93
- }
94
- })
91
+ },
92
+ },
93
+ })
94
+ export const canvasReducer = canvas.reducer
@@ -0,0 +1,176 @@
1
+ import { expect, it } from 'vitest'
2
+ import { reduceAIState, type AIState } from 'experimental-a2/ai'
3
+ import { chatAgent, type ChatAgentMessage } from '../model'
4
+ import { agentRetry, agentStreamStatus } from './chat-view'
5
+
6
+ const generation = {
7
+ generationId: 'generation',
8
+ requestId: 'request',
9
+ responseMessageId: 'response',
10
+ messageId: 'input',
11
+ model: 'test/model',
12
+ attempt: 1,
13
+ }
14
+
15
+ function running() {
16
+ let index = 0
17
+ let state: AIState<ChatAgentMessage> = {
18
+ ...chatAgent.reducer.initialState,
19
+ active: {
20
+ turnId: 'turn',
21
+ phase: 'generating',
22
+ input: {
23
+ id: 'input',
24
+ generate: true,
25
+ message: { id: 'input', role: 'user', parts: [] },
26
+ },
27
+ },
28
+ }
29
+ const append = (event: { type: string; payload: unknown; id?: string }) => {
30
+ state = reduceAIState(state, { ...event, index: ++index })
31
+ }
32
+ append({
33
+ id: 'request',
34
+ type: 'ai.generation.requested',
35
+ payload: { ...generation, reason: 'message' },
36
+ })
37
+ append({ type: 'ai.generation.started', payload: generation })
38
+ const snapshot = () => state
39
+ return { append, snapshot }
40
+ }
41
+
42
+ it.each([false, true])(
43
+ 'can retry from a restored snapshot before and after the control commit (step limit: %s)',
44
+ (stepLimit) => {
45
+ const log = running()
46
+ if (stepLimit) {
47
+ log.append({
48
+ type: 'ai.generation.completed',
49
+ payload: { ...generation, finishReason: 'tool-calls' },
50
+ })
51
+ log.append({
52
+ id: 'continuation',
53
+ type: 'ai.generation.requested',
54
+ payload: {
55
+ messageId: 'response',
56
+ responseMessageId: 'response',
57
+ reason: 'tool',
58
+ sourceGenerationId: 'generation',
59
+ },
60
+ })
61
+ }
62
+ log.append({
63
+ type: 'ai.generation.failed',
64
+ payload: {
65
+ ...generation,
66
+ error: 'failed',
67
+ ...(stepLimit ? { stepLimit: true } : {}),
68
+ },
69
+ })
70
+ expect(log.snapshot().status).toBe('failed')
71
+ expect(log.snapshot().activeResponseMessageId).toBeNull()
72
+ expect(agentRetry(log.snapshot())).toEqual({
73
+ messageId: 'input',
74
+ responseMessageId: 'response',
75
+ })
76
+ expect(
77
+ agentStreamStatus({
78
+ state: log.snapshot(),
79
+ responseMessageId: 'response',
80
+ streaming: false,
81
+ }),
82
+ ).toBe('failed')
83
+
84
+ log.append({
85
+ type: 'ai.control.committed',
86
+ payload: {
87
+ changes: [],
88
+ view: {
89
+ turnId: 'turn',
90
+ version: 1,
91
+ phase: 'failed',
92
+ paused: false,
93
+ closed: false,
94
+ requestId: null,
95
+ requestReason: null,
96
+ sourceGenerationId: null,
97
+ responseMessageId: 'response',
98
+ },
99
+ },
100
+ })
101
+ expect(log.snapshot().activeResponseMessageId).toBe('response')
102
+ expect(agentRetry(log.snapshot())).toEqual({
103
+ messageId: 'input',
104
+ responseMessageId: 'response',
105
+ })
106
+ },
107
+ )
108
+
109
+ it('uses the current response owner after retry rather than an older failure', () => {
110
+ const log = running()
111
+ log.append({
112
+ type: 'ai.generation.failed',
113
+ payload: { ...generation, error: 'failed' },
114
+ })
115
+ const retryGeneration = {
116
+ ...generation,
117
+ requestId: 'retry-request',
118
+ generationId: 'retry-generation',
119
+ }
120
+ log.append({
121
+ id: retryGeneration.requestId,
122
+ type: 'ai.generation.requested',
123
+ payload: { ...retryGeneration, reason: 'retry' },
124
+ })
125
+ log.append({ type: 'ai.generation.started', payload: retryGeneration })
126
+ expect(agentRetry(log.snapshot())).toBeNull()
127
+ expect(
128
+ agentStreamStatus({
129
+ state: log.snapshot(),
130
+ responseMessageId: 'response',
131
+ streaming: true,
132
+ }),
133
+ ).toBe('streaming')
134
+ log.append({ type: 'ai.generation.completed', payload: retryGeneration })
135
+ log.append({
136
+ type: 'ai.message.completed',
137
+ payload: { messageId: 'response' },
138
+ })
139
+ log.append({
140
+ type: 'ai.generation.failed',
141
+ payload: { ...generation, error: 'stale failure', superseded: true },
142
+ })
143
+ expect(
144
+ agentStreamStatus({
145
+ state: log.snapshot(),
146
+ responseMessageId: 'response',
147
+ streaming: false,
148
+ }),
149
+ ).toBe('completed')
150
+ })
151
+
152
+ it('shows interruption from the snapshot and does not invent missing retry identifiers', () => {
153
+ const log = running()
154
+ log.append({
155
+ type: 'ai.message.interrupted',
156
+ payload: { ...generation, messageId: 'response' },
157
+ })
158
+ expect(
159
+ agentStreamStatus({
160
+ state: log.snapshot(),
161
+ responseMessageId: 'response',
162
+ streaming: false,
163
+ }),
164
+ ).toBe('interrupted')
165
+ expect(
166
+ agentRetry({
167
+ ...log.snapshot(),
168
+ status: 'failed',
169
+ activeProjection: null,
170
+ activeResponseMessageId: null,
171
+ }),
172
+ ).toBeNull()
173
+ expect(
174
+ agentRetry({ ...log.snapshot(), status: 'failed', active: null }),
175
+ ).toBeNull()
176
+ })
@@ -2,6 +2,7 @@
2
2
 
3
3
  import type { ReactNode } from 'react'
4
4
  import { useEffect, useRef, useState } from 'react'
5
+ import { isCompactionPart, type TranscriptMessage } from 'experimental-a2/ai'
5
6
  import type { ChatAgentMessage, ChatAgentStep } from '../model'
6
7
  import type { AgentStreamSelection } from './chat-view'
7
8
 
@@ -36,9 +37,23 @@ function StreamPart({
36
37
  part,
37
38
  position,
38
39
  }: {
39
- part: ChatAgentMessage['parts'][number]
40
+ part: TranscriptMessage<ChatAgentMessage>['parts'][number]
40
41
  position: number
41
42
  }): ReactNode {
43
+ if (isCompactionPart(part)) {
44
+ return (
45
+ <div className="agent-stream-step" role="separator">
46
+ <span>
47
+ {part.data.status === 'completed'
48
+ ? 'Context compacted'
49
+ : part.data.status === 'running'
50
+ ? 'Compacting…'
51
+ : 'Compaction did not complete'}
52
+ </span>
53
+ </div>
54
+ )
55
+ }
56
+
42
57
  if (part.type === 'step-start') {
43
58
  return (
44
59
  <div className="agent-stream-step" role="separator">