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,377 +0,0 @@
1
- import type { UIMessageChunk } from 'ai'
2
- import { reduceAIState } from 'experimental-a2/ai'
3
- import { expect, it } from 'vitest'
4
- import { compactionTimeline, type TimelineEntry } from './compaction-timeline'
5
- import { demoAgent, type AgentEvent } from './model'
6
-
7
- type EventInput = {
8
- [K in AgentEvent['type']]: Pick<
9
- Extract<AgentEvent, { type: K }>,
10
- 'type' | 'payload'
11
- > & { id?: string }
12
- }[AgentEvent['type']]
13
- type Generation = Extract<
14
- AgentEvent,
15
- { type: 'ai.generation.started' }
16
- >['payload']
17
-
18
- function transcript() {
19
- const events: AgentEvent[] = []
20
- const append = (input: EventInput): void => {
21
- events.push({
22
- ...input,
23
- id: input.id ?? `event:${events.length + 1}`,
24
- index: events.length + 1,
25
- sessionId: 'timeline',
26
- createdAt: new Date('2026-09-14T12:00:00Z'),
27
- })
28
- }
29
- const user = (id: string): void =>
30
- append({
31
- type: 'ai.message.created',
32
- payload: {
33
- message: { id, role: 'user', parts: [{ type: 'text', text: id }] },
34
- },
35
- })
36
- const start = ({
37
- requestId = 'request',
38
- reason = 'message',
39
- attempt = 1,
40
- sourceGenerationId,
41
- }: {
42
- requestId?: string
43
- reason?: 'message' | 'tool' | 'retry'
44
- attempt?: number
45
- sourceGenerationId?: string
46
- } = {}): Generation => {
47
- const generation = {
48
- requestId,
49
- messageId: reason === 'tool' ? 'assistant' : 'user',
50
- responseMessageId: 'assistant',
51
- generationId: `generation-${events.length}-${attempt}`,
52
- attempt,
53
- model: 'test/model',
54
- }
55
- if (attempt === 1)
56
- append({
57
- id: requestId,
58
- type: 'ai.generation.requested',
59
- payload: {
60
- messageId: generation.messageId,
61
- responseMessageId: 'assistant',
62
- reason,
63
- ...(sourceGenerationId === undefined ? {} : { sourceGenerationId }),
64
- },
65
- })
66
- append({ type: 'ai.generation.started', payload: generation })
67
- return generation
68
- }
69
- const progress = (generation: Generation, chunks: UIMessageChunk[]): void =>
70
- append({
71
- type: 'ai.generation.progress',
72
- payload: { ...generation, sequence: events.length, chunks },
73
- })
74
- const text = (generation: Generation, value: string, step = true): void =>
75
- progress(generation, [
76
- { type: 'start', messageId: generation.responseMessageId },
77
- ...(step ? [{ type: 'start-step' as const }] : []),
78
- { type: 'text-start', id: generation.generationId },
79
- { type: 'text-delta', id: generation.generationId, delta: value },
80
- { type: 'text-end', id: generation.generationId },
81
- ])
82
- const compact = (generation: Generation, completed = true): void => {
83
- const payload = {
84
- generationId: generation.generationId,
85
- throughMessageId: generation.messageId,
86
- throughIndex: events.length,
87
- }
88
- append({ type: 'ai.compaction.requested', payload })
89
- if (completed)
90
- append({
91
- type: 'ai.compaction.completed',
92
- payload: {
93
- ...payload,
94
- messages: [],
95
- summary: `Summary ${generation.generationId}`,
96
- },
97
- })
98
- }
99
- const state = () =>
100
- events.reduce(
101
- reduceAIState,
102
- demoAgent.reducer.view(demoAgent.reducer.initialState),
103
- )
104
- const timeline = () => compactionTimeline({ state: state(), events })
105
- return {
106
- events,
107
- append,
108
- user,
109
- start,
110
- progress,
111
- text,
112
- compact,
113
- state,
114
- timeline,
115
- }
116
- }
117
-
118
- const labels = (entries: TimelineEntry[]): string[] =>
119
- entries.map((entry) =>
120
- entry.type === 'compaction'
121
- ? entry.id
122
- : entry.message.parts
123
- .flatMap((part) => (part.type === 'text' ? [part.text] : []))
124
- .join(''),
125
- )
126
-
127
- it('keeps a stable marker from running to completed, including after replay', () => {
128
- const log = transcript()
129
- log.user('user')
130
- const generation = log.start()
131
- log.compact(generation, false)
132
- const running = log.timeline().find((entry) => entry.type === 'compaction')
133
- expect(running?.running).toBe(true)
134
- expect(labels(log.timeline())).toEqual([
135
- 'user',
136
- `compaction:${generation.generationId}`,
137
- ])
138
- log.append({
139
- type: 'ai.compaction.completed',
140
- payload: {
141
- generationId: generation.generationId,
142
- throughMessageId: 'user',
143
- messages: [],
144
- summary: 'Summary',
145
- },
146
- })
147
- log.text(generation, 'Answer')
148
- log.append({ type: 'ai.generation.completed', payload: generation })
149
- log.append({
150
- type: 'ai.message.completed',
151
- payload: { messageId: 'assistant' },
152
- })
153
- const completed = log.timeline().find((entry) => entry.type === 'compaction')
154
- expect(completed).toMatchObject({
155
- id: running?.id,
156
- running: false,
157
- event: { type: 'ai.compaction.completed' },
158
- })
159
- expect(labels(log.timeline())).toEqual([
160
- 'user',
161
- `compaction:${generation.generationId}`,
162
- 'Answer',
163
- ])
164
- })
165
-
166
- it('places compaction between tool steps without moving a queued user into the response', () => {
167
- const log = transcript()
168
- log.user('user')
169
- const first = log.start()
170
- log.text(first, 'Before tool')
171
- log.append({
172
- type: 'ai.generation.completed',
173
- payload: { ...first, finishReason: 'tool-calls' },
174
- })
175
- log.user('queued')
176
- const second = log.start({
177
- requestId: 'continuation-request',
178
- sourceGenerationId: first.generationId,
179
- reason: 'tool',
180
- })
181
- log.compact(second, false)
182
- expect(log.state().compaction?.status).toBe('running')
183
- expect(labels(log.timeline())).toEqual([
184
- 'user',
185
- 'Before tool',
186
- `compaction:${second.generationId}`,
187
- 'queued',
188
- ])
189
- log.append({
190
- type: 'ai.compaction.completed',
191
- payload: {
192
- generationId: second.generationId,
193
- throughMessageId: 'assistant',
194
- messages: [],
195
- },
196
- })
197
- log.text(second, 'After tool')
198
- expect(labels(log.timeline())).toEqual([
199
- 'user',
200
- 'Before tool',
201
- `compaction:${second.generationId}`,
202
- 'After tool',
203
- 'queued',
204
- ])
205
- expect(
206
- log
207
- .timeline()
208
- .flatMap((entry) =>
209
- entry.type === 'message' && entry.message.id === 'assistant'
210
- ? entry.message.parts
211
- : [],
212
- ),
213
- ).toEqual(
214
- log.state().messages.find((message) => message.id === 'assistant')?.parts,
215
- )
216
- })
217
-
218
- it('preserves compaction history but drops superseded attempt content', () => {
219
- const log = transcript()
220
- log.user('user')
221
- const first = log.start()
222
- log.compact(first)
223
- log.text(first, 'Superseded')
224
- const second = log.start({ attempt: 2 })
225
- log.compact(second)
226
- log.text(second, 'Replacement')
227
- expect(log.state().terminalGenerations?.[first.generationId]).toBe(
228
- 'superseded',
229
- )
230
- expect(labels(log.timeline())).toEqual([
231
- 'user',
232
- `compaction:${first.generationId}`,
233
- `compaction:${second.generationId}`,
234
- 'Replacement',
235
- ])
236
- expect(
237
- log
238
- .timeline()
239
- .find((entry) => entry.id === `compaction:${first.generationId}`),
240
- ).toMatchObject({ outcome: undefined, running: false })
241
- })
242
-
243
- it('keeps old checkpoints before an explicit retry without restoring discarded parts', () => {
244
- const log = transcript()
245
- log.user('user')
246
- const first = log.start()
247
- log.compact(first)
248
- log.text(first, 'Failed response')
249
- log.append({
250
- type: 'ai.generation.failed',
251
- payload: { ...first, error: 'Test failure' },
252
- })
253
- const retry = log.start({ requestId: 'retry', reason: 'retry' })
254
- log.compact(retry)
255
- log.text(retry, 'Retried response')
256
- expect(labels(log.timeline())).toEqual([
257
- 'user',
258
- `compaction:${first.generationId}`,
259
- `compaction:${retry.generationId}`,
260
- 'Retried response',
261
- ])
262
- expect(
263
- log
264
- .timeline()
265
- .find((entry) => entry.id === `compaction:${first.generationId}`),
266
- ).toMatchObject({ outcome: undefined })
267
- })
268
-
269
- it('does not leave failed compaction spinning', () => {
270
- const log = transcript()
271
- log.user('user')
272
- const generation = log.start()
273
- log.compact(generation, false)
274
- log.append({
275
- type: 'ai.generation.failed',
276
- payload: { ...generation, error: 'Compaction failed' },
277
- })
278
- expect(
279
- log.timeline().find((entry) => entry.type === 'compaction'),
280
- ).toMatchObject({
281
- running: false,
282
- outcome: 'failed',
283
- event: { type: 'ai.compaction.requested' },
284
- })
285
- })
286
-
287
- it('keeps completed steps around committed compactions when retrying a later step', () => {
288
- const log = transcript()
289
- log.user('user')
290
- const first = log.start()
291
- log.compact(first)
292
- log.text(first, 'Completed step')
293
- log.append({
294
- type: 'ai.generation.completed',
295
- payload: { ...first, finishReason: 'tool-calls' },
296
- })
297
- const second = log.start({
298
- requestId: 'second',
299
- reason: 'tool',
300
- sourceGenerationId: first.generationId,
301
- })
302
- log.compact(second)
303
- log.text(second, 'Discarded partial step')
304
- log.append({
305
- type: 'ai.generation.failed',
306
- payload: { ...second, error: 'Failed' },
307
- })
308
- const retry = log.start({ requestId: 'retry', reason: 'retry' })
309
- log.text(retry, 'Retried step')
310
- expect(labels(log.timeline())).toEqual([
311
- 'user',
312
- `compaction:${first.generationId}`,
313
- 'Completed step',
314
- `compaction:${second.generationId}`,
315
- 'Retried step',
316
- ])
317
- })
318
-
319
- it.each([false, true])(
320
- 'preserves complete messages without step boundaries, delta only=%s',
321
- (deltaOnly) => {
322
- const log = transcript()
323
- log.user('user')
324
- const first = log.start()
325
- log.text(first, 'Before')
326
- log.append({
327
- type: 'ai.generation.completed',
328
- payload: { ...first, finishReason: 'tool-calls' },
329
- })
330
- const second = log.start({
331
- requestId: 'continuation-request',
332
- sourceGenerationId: first.generationId,
333
- reason: 'tool',
334
- })
335
- log.compact(second)
336
- if (deltaOnly)
337
- log.progress(second, [
338
- {
339
- type: 'text-delta',
340
- id: 'after',
341
- delta: 'After without a step boundary',
342
- },
343
- ])
344
- else log.text(second, 'After without a step boundary', false)
345
- const messages = log
346
- .timeline()
347
- .flatMap((entry) => (entry.type === 'message' ? [entry.message] : []))
348
- expect(messages).toEqual(log.state().messages)
349
- expect(
350
- log.timeline().filter((entry) => entry.type === 'compaction'),
351
- ).toHaveLength(1)
352
- expect(labels(log.timeline())[1]).toBe(`compaction:${second.generationId}`)
353
- },
354
- )
355
-
356
- it('ignores a rejected later generation when mapping canonical parts', () => {
357
- const log = transcript()
358
- log.user('user')
359
- const first = log.start()
360
- log.compact(first)
361
- log.text(first, 'Kept')
362
- log.append({ type: 'ai.generation.completed', payload: first })
363
- log.append({
364
- type: 'ai.message.completed',
365
- payload: { messageId: 'assistant' },
366
- })
367
- const rejected = log.start({ requestId: 'stale-retry', reason: 'retry' })
368
- log.text(rejected, 'Ignored')
369
- expect(log.state().responseOwners['assistant']?.generationId).toBe(
370
- first.generationId,
371
- )
372
- expect(labels(log.timeline())).toEqual([
373
- 'user',
374
- `compaction:${first.generationId}`,
375
- 'Kept',
376
- ])
377
- })
@@ -1,208 +0,0 @@
1
- import type { UIMessage } from 'ai'
2
- import type { AIState } from 'experimental-a2/ai'
3
- import type { AgentEvent } from './model'
4
-
5
- type Generation = Extract<AgentEvent, { type: 'ai.generation.started' }>
6
- type CompactionEvent = Extract<
7
- AgentEvent,
8
- { type: 'ai.compaction.requested' | 'ai.compaction.completed' }
9
- >
10
-
11
- export type CompactionEntry = {
12
- type: 'compaction'
13
- id: string
14
- event: CompactionEvent
15
- running: boolean
16
- outcome: 'failed' | 'interrupted' | 'superseded' | undefined
17
- }
18
-
19
- export type TimelineEntry =
20
- | { type: 'message'; id: string; message: UIMessage; lastSegment: boolean }
21
- | CompactionEntry
22
-
23
- export function compactionTimeline({
24
- state,
25
- events,
26
- }: {
27
- state: AIState
28
- events: readonly AgentEvent[]
29
- }): TimelineEntry[] {
30
- const generations = new Map<string, Generation>()
31
- const requests = new Map<string, string>()
32
- const stepCounts = new Map<string, number>()
33
- const contentGenerations = new Set<string>()
34
- const completedGenerations = new Set<string>()
35
- const compactions = new Map<string, CompactionEvent>()
36
- const roots = new Map<string, string>()
37
- for (const event of events) {
38
- switch (event.type) {
39
- case 'ai.generation.requested':
40
- requests.set(event.id, event.payload.reason)
41
- break
42
- case 'ai.generation.started': {
43
- const { generationId, messageId, responseMessageId } = event.payload
44
- generations.set(generationId, event)
45
- if (messageId !== responseMessageId)
46
- roots.set(responseMessageId, messageId)
47
- break
48
- }
49
- case 'ai.generation.progress': {
50
- const id = event.payload.generationId
51
- const count = event.payload.chunks.filter(
52
- (chunk) => chunk.type === 'start-step',
53
- ).length
54
- stepCounts.set(id, (stepCounts.get(id) ?? 0) + count)
55
- if (
56
- event.payload.chunks.some(
57
- (chunk) =>
58
- ![
59
- 'start',
60
- 'finish',
61
- 'start-step',
62
- 'finish-step',
63
- 'message-metadata',
64
- 'abort',
65
- 'error',
66
- ].includes(chunk.type),
67
- )
68
- )
69
- contentGenerations.add(id)
70
- break
71
- }
72
- case 'ai.compaction.requested':
73
- case 'ai.compaction.completed':
74
- compactions.set(event.payload.generationId, event)
75
- break
76
- case 'ai.generation.completed':
77
- completedGenerations.add(event.payload.generationId)
78
- break
79
- }
80
- }
81
-
82
- const contributions = new Map<string, Generation[]>()
83
- for (const generation of generations.values()) {
84
- const { generationId, responseMessageId, requestId } = generation.payload
85
- const ownerId = state.responseOwners[responseMessageId]?.generationId
86
- const owner = ownerId === undefined ? undefined : generations.get(ownerId)
87
- if (
88
- owner === undefined ||
89
- generation.index > owner.index ||
90
- state.terminalGenerations?.[generationId] === 'superseded'
91
- )
92
- continue
93
- let steps = contributions.get(responseMessageId) ?? []
94
- if (requests.get(requestId) === 'retry')
95
- steps = steps.filter((step) =>
96
- completedGenerations.has(step.payload.generationId),
97
- )
98
- if (steps.at(-1)?.payload.requestId === requestId) steps.pop()
99
- steps.push(generation)
100
- contributions.set(responseMessageId, steps)
101
- }
102
-
103
- const included = new Set(
104
- [...contributions.values()].flatMap((steps) =>
105
- steps.map((step) => step.payload.generationId),
106
- ),
107
- )
108
- const offsets = new Map<string, number>()
109
- const counts = new Map<string, number>()
110
- for (const { payload } of generations.values()) {
111
- const count = counts.get(payload.responseMessageId) ?? 0
112
- offsets.set(payload.generationId, count)
113
- if (included.has(payload.generationId))
114
- counts.set(
115
- payload.responseMessageId,
116
- count + (stepCounts.get(payload.generationId) ?? 0),
117
- )
118
- }
119
-
120
- const markers = new Map<string, CompactionEntry[]>()
121
- const orphaned: CompactionEntry[] = []
122
- const messageIds = new Set(state.messages.map((message) => message.id))
123
- for (const [generationId, event] of compactions) {
124
- const terminal = state.terminalGenerations?.[generationId]
125
- const entry: CompactionEntry = {
126
- type: 'compaction',
127
- id: `compaction:${generationId}`,
128
- event,
129
- running:
130
- state.compaction?.generationId === generationId &&
131
- state.compaction.status === 'running',
132
- outcome:
133
- event.type === 'ai.compaction.completed' || terminal === 'completed'
134
- ? undefined
135
- : terminal,
136
- }
137
- const responseId = generations.get(generationId)?.payload.responseMessageId
138
- const anchor =
139
- responseId !== undefined && messageIds.has(responseId)
140
- ? responseId
141
- : responseId === undefined
142
- ? undefined
143
- : roots.get(responseId)
144
- if (anchor === undefined || !messageIds.has(anchor)) orphaned.push(entry)
145
- else {
146
- const entries = markers.get(anchor) ?? []
147
- entries.push(entry)
148
- markers.set(anchor, entries)
149
- }
150
- }
151
-
152
- const timeline: TimelineEntry[] = []
153
- for (const message of state.messages) {
154
- const entries = markers.get(message.id) ?? []
155
- if (entries.length === 0) {
156
- timeline.push({
157
- type: 'message',
158
- id: `${message.id}:0`,
159
- message,
160
- lastSegment: true,
161
- })
162
- continue
163
- }
164
- const steps = contributions.get(message.id) ?? []
165
- const boundaries = message.parts.flatMap((part, index) =>
166
- part.type === 'step-start' ? [index] : [],
167
- )
168
- const mapped =
169
- boundaries[0] === 0 &&
170
- boundaries.length === counts.get(message.id) &&
171
- steps.every(
172
- ({ payload }) =>
173
- !contentGenerations.has(payload.generationId) ||
174
- (stepCounts.get(payload.generationId) ?? 0) > 0,
175
- )
176
- let offset = 0
177
- const appendMessage = (end: number): void => {
178
- if (end <= offset) return
179
- timeline.push({
180
- type: 'message',
181
- id: `${message.id}:${offset}`,
182
- message: { ...message, parts: message.parts.slice(offset, end) },
183
- lastSegment: end === message.parts.length,
184
- })
185
- offset = end
186
- }
187
- for (const entry of entries) {
188
- if (message.role === 'user') {
189
- appendMessage(message.parts.length)
190
- } else if (mapped) {
191
- const stepIndex = offsets.get(entry.event.payload.generationId) ?? 0
192
- appendMessage(boundaries[stepIndex] ?? message.parts.length)
193
- }
194
- timeline.push(entry)
195
- }
196
- if (message.parts.length === 0) {
197
- timeline.push({
198
- type: 'message',
199
- id: message.id,
200
- message,
201
- lastSegment: true,
202
- })
203
- } else {
204
- appendMessage(message.parts.length)
205
- }
206
- }
207
- return [...timeline, ...orphaned]
208
- }