experimental-a2 0.17.0 → 0.18.1

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 (221) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +1 -1
  3. package/dist/{actor-DBbe5xeE.d.ts → actor-C7b-1arn.d.ts} +5 -5
  4. package/dist/{actor-DBbe5xeE.d.ts.map → actor-C7b-1arn.d.ts.map} +1 -1
  5. package/dist/actor-client.d.ts +1 -1
  6. package/dist/actor-react.d.ts +3 -3
  7. package/dist/actor-react.js +1 -1
  8. package/dist/actor.d.ts +1 -1
  9. package/dist/actor.js +2 -2
  10. package/dist/{ai-vpGtcaCB.d.ts → ai-BMLlI2Ff.d.ts} +3 -3
  11. package/dist/{ai-vpGtcaCB.d.ts.map → ai-BMLlI2Ff.d.ts.map} +1 -1
  12. package/dist/{ai-UrWfKeuv.js → ai-D6mGlO8j.js} +2 -2
  13. package/dist/{ai-UrWfKeuv.js.map → ai-D6mGlO8j.js.map} +1 -1
  14. package/dist/ai-client.d.ts +2 -2
  15. package/dist/ai-client.js +2 -2
  16. package/dist/ai-fx.browser.d.ts +1 -0
  17. package/dist/ai-fx.browser.js +6 -0
  18. package/dist/ai-fx.browser.js.map +1 -0
  19. package/dist/ai-fx.d.ts +10 -0
  20. package/dist/ai-fx.d.ts.map +1 -0
  21. package/dist/ai-fx.js +692 -0
  22. package/dist/ai-fx.js.map +1 -0
  23. package/dist/ai-harness-B2suP2Qh.d.ts +26 -0
  24. package/dist/ai-harness-B2suP2Qh.d.ts.map +1 -0
  25. package/dist/ai-harness-BuLh_EYi.js +273 -0
  26. package/dist/ai-harness-BuLh_EYi.js.map +1 -0
  27. package/dist/ai-pi.browser.d.ts +1 -0
  28. package/dist/ai-pi.browser.js +6 -0
  29. package/dist/ai-pi.browser.js.map +1 -0
  30. package/dist/ai-pi.d.ts +17 -0
  31. package/dist/ai-pi.d.ts.map +1 -0
  32. package/dist/ai-pi.js +950 -0
  33. package/dist/ai-pi.js.map +1 -0
  34. package/dist/ai-react.d.ts +5 -5
  35. package/dist/ai-react.js +2 -2
  36. package/dist/ai-server.d.ts +47 -25
  37. package/dist/ai-server.d.ts.map +1 -1
  38. package/dist/ai-server.js +113 -40
  39. package/dist/ai-server.js.map +1 -1
  40. package/dist/ai-tool-input-DmSqMUQi.js +40 -0
  41. package/dist/ai-tool-input-DmSqMUQi.js.map +1 -0
  42. package/dist/ai.d.ts +1 -1
  43. package/dist/ai.js +1 -1
  44. package/dist/bounded-cache-CPHWF-8h.js +41 -0
  45. package/dist/bounded-cache-CPHWF-8h.js.map +1 -0
  46. package/dist/{client-COaiaDub.js → client-BhHLoNKF.js} +3 -3
  47. package/dist/{client-COaiaDub.js.map → client-BhHLoNKF.js.map} +1 -1
  48. package/dist/{client-qRnRUzBS.d.ts → client-h_DKFAjj.d.ts} +3 -3
  49. package/dist/{client-qRnRUzBS.d.ts.map → client-h_DKFAjj.d.ts.map} +1 -1
  50. package/dist/client.d.ts +3 -3
  51. package/dist/client.js +1 -1
  52. package/dist/index.d.ts +5 -5
  53. package/dist/{initial-snapshot-BrLso5x_.d.ts → initial-snapshot-Bx-c0pDP.d.ts} +2 -2
  54. package/dist/{initial-snapshot-BrLso5x_.d.ts.map → initial-snapshot-Bx-c0pDP.d.ts.map} +1 -1
  55. package/dist/{internal-CbVvEwad.js → internal-CscSY7K_.js} +11 -3
  56. package/dist/internal-CscSY7K_.js.map +1 -0
  57. package/dist/otel.d.ts +1 -1
  58. package/dist/{react-BbJ-g6Uu.d.ts → react-BBXgHWFc.d.ts} +4 -4
  59. package/dist/{react-BbJ-g6Uu.d.ts.map → react-BBXgHWFc.d.ts.map} +1 -1
  60. package/dist/react.d.ts +1 -1
  61. package/dist/react.js +1 -1
  62. package/dist/reducer-BAXc8JyF.js.map +1 -1
  63. package/dist/{reducer-BYu6DT7h.d.ts → reducer-CqA59Q6U.d.ts} +2 -2
  64. package/dist/{reducer-BYu6DT7h.d.ts.map → reducer-CqA59Q6U.d.ts.map} +1 -1
  65. package/dist/scheduler-qstash.d.ts +2 -2
  66. package/dist/scheduler-qstash.js +3 -3
  67. package/dist/scheduler-qstash.js.map +1 -1
  68. package/dist/scheduler-vercel.d.ts +7 -7
  69. package/dist/scheduler-vercel.d.ts.map +1 -1
  70. package/dist/scheduler-vercel.js +9 -10
  71. package/dist/scheduler-vercel.js.map +1 -1
  72. package/dist/{server-Rj64fNvX.js → server-D-Y4pqwu.js} +126 -67
  73. package/dist/server-D-Y4pqwu.js.map +1 -0
  74. package/dist/{server-CGM8WzRG.d.ts → server-DYAATFuT.d.ts} +42 -7
  75. package/dist/server-DYAATFuT.d.ts.map +1 -0
  76. package/dist/server.d.ts +4 -4
  77. package/dist/server.js +1 -1
  78. package/dist/{session-clear-Dshpuhff.d.ts → session-clear-C-QwTXV1.d.ts} +2 -2
  79. package/dist/session-clear-C-QwTXV1.d.ts.map +1 -0
  80. package/dist/{store-Gpkq7WBo.d.ts → store-CyLUy86v.d.ts} +53 -5
  81. package/dist/store-CyLUy86v.d.ts.map +1 -0
  82. package/dist/store-N8PXxDAS.js.map +1 -1
  83. package/dist/store-memory.d.ts +1 -1
  84. package/dist/store-memory.js +56 -11
  85. package/dist/store-memory.js.map +1 -1
  86. package/dist/{store-polling-4_y8lETm.js → store-polling-7WJV0YWP.js} +2 -2
  87. package/dist/{store-polling-4_y8lETm.js.map → store-polling-7WJV0YWP.js.map} +1 -1
  88. package/dist/store-postgres.d.ts +1 -1
  89. package/dist/store-postgres.d.ts.map +1 -1
  90. package/dist/store-postgres.js +106 -25
  91. package/dist/store-postgres.js.map +1 -1
  92. package/dist/{store-presence-polling-Q1C-k2dd.js → store-presence-polling-B1ixeqIS.js} +3 -3
  93. package/dist/{store-presence-polling-Q1C-k2dd.js.map → store-presence-polling-B1ixeqIS.js.map} +1 -1
  94. package/dist/store-redis-http.d.ts +1 -1
  95. package/dist/store-redis-http.js +3 -3
  96. package/dist/{store-redis-notify-tqWNnMRu.js → store-redis-notify-CM0AfLY0.js} +271 -101
  97. package/dist/store-redis-notify-CM0AfLY0.js.map +1 -0
  98. package/dist/store-redis.d.ts +1 -1
  99. package/dist/store-redis.js +1 -1
  100. package/dist/store-sqlite.d.ts +1 -1
  101. package/dist/store-sqlite.js +66 -21
  102. package/dist/store-sqlite.js.map +1 -1
  103. package/dist/{telemetry-cZH-cibj.d.ts → telemetry-Cyfqo6Qt.d.ts} +2 -2
  104. package/dist/telemetry-Cyfqo6Qt.d.ts.map +1 -0
  105. package/dist/{wire-DjfHOvdW.js → wire-BKxH1vbP.js} +2 -2
  106. package/dist/{wire-DjfHOvdW.js.map → wire-BKxH1vbP.js.map} +1 -1
  107. package/docs/01-quickstart.mdx +49 -83
  108. package/docs/{guides/07-examples.mdx → 02-examples.mdx} +10 -20
  109. package/docs/actors/01-introduction.mdx +48 -86
  110. package/docs/actors/02-concurrency.mdx +43 -70
  111. package/docs/actors/03-timers.mdx +28 -42
  112. package/docs/actors/04-routes.mdx +110 -150
  113. package/docs/actors/meta.ts +1 -1
  114. package/docs/agents/01-introduction.mdx +257 -0
  115. package/docs/agents/02-tools.mdx +247 -0
  116. package/docs/agents/03-conversations.mdx +538 -0
  117. package/docs/agents/04-harnesses.mdx +136 -0
  118. package/docs/agents/meta.ts +1 -0
  119. package/docs/client/01-react.mdx +294 -0
  120. package/docs/client/02-transports.mdx +86 -0
  121. package/docs/client/03-presence.mdx +224 -0
  122. package/docs/client/meta.ts +1 -0
  123. package/docs/contract/01-events.mdx +57 -0
  124. package/docs/contract/02-state.mdx +142 -0
  125. package/docs/contract/03-sessions.mdx +175 -0
  126. package/docs/index.mdx +109 -145
  127. package/docs/reference/01-core.mdx +139 -0
  128. package/docs/reference/02-server.mdx +860 -0
  129. package/docs/reference/03-client.mdx +301 -0
  130. package/docs/reference/04-ai.mdx +731 -0
  131. package/docs/reference/05-adapters.mdx +351 -0
  132. package/docs/reference/06-errors.mdx +138 -0
  133. package/docs/reference/meta.ts +1 -1
  134. package/docs/server/01-handlers.mdx +200 -0
  135. package/docs/server/02-timers.mdx +130 -0
  136. package/docs/server/03-cancellation.mdx +142 -0
  137. package/docs/server/04-routes.mdx +243 -0
  138. package/docs/server/05-production.mdx +323 -0
  139. package/docs/server/06-guarantees.mdx +132 -0
  140. package/examples/playground/DEPLOY.md +3 -5
  141. package/examples/playground/app/a0/[appId]/page.tsx +12 -13
  142. package/examples/playground/app/a0/app-list.tsx +9 -2
  143. package/examples/playground/app/a0/app-view.tsx +9 -1
  144. package/examples/playground/app/a0/apps/[appId]/route.ts +10 -5
  145. package/examples/playground/app/a0/apps.test.ts +12 -0
  146. package/examples/playground/app/a0/apps.ts +22 -7
  147. package/examples/playground/app/a0/chat.tsx +47 -1
  148. package/examples/playground/app/a0/devtools.tsx +2 -8
  149. package/examples/playground/app/a0/fx/events/route.ts +9 -0
  150. package/examples/playground/app/a0/fx/scheduler/route.ts +7 -0
  151. package/examples/playground/app/a0/fx/short/events/route.ts +11 -0
  152. package/examples/playground/app/a0/model.test.ts +18 -1
  153. package/examples/playground/app/a0/model.ts +47 -10
  154. package/examples/playground/app/a0/no-photon.cjs +4 -0
  155. package/examples/playground/app/a0/no-quickjs.cjs +3 -0
  156. package/examples/playground/app/a0/pi/events/route.ts +9 -0
  157. package/examples/playground/app/a0/pi/scheduler/route.ts +7 -0
  158. package/examples/playground/app/a0/pi/short/events/route.ts +11 -0
  159. package/examples/playground/app/a0/sandbox-log.ts +10 -4
  160. package/examples/playground/app/a0/sandbox-session.ts +152 -0
  161. package/examples/playground/app/a0/scheduler/route.ts +2 -2
  162. package/examples/playground/app/a0/server.ts +98 -54
  163. package/examples/playground/app/a0/session.ts +17 -5
  164. package/examples/playground/app/a0/shell.tsx +11 -3
  165. package/examples/playground/app/agent/ingress.test.ts +6 -3
  166. package/examples/playground/app/agent/server.ts +9 -6
  167. package/examples/playground/app/agent/telemetry.test.ts +71 -50
  168. package/examples/playground/app/canvas/[canvasId]/canvas-client.tsx +1 -1
  169. package/examples/playground/app/chat/server.test.ts +30 -26
  170. package/examples/playground/app/chat/server.ts +46 -46
  171. package/examples/playground/next.config.ts +17 -5
  172. package/examples/playground/package.json +4 -1
  173. package/examples/playground/vercel.json +18 -0
  174. package/package.json +27 -3
  175. package/src/ai-agent.ts +271 -90
  176. package/src/ai-fx.browser.ts +5 -0
  177. package/src/ai-fx.ts +912 -0
  178. package/src/ai-harness.ts +518 -0
  179. package/src/ai-model-metadata.ts +11 -6
  180. package/src/ai-pi.browser.ts +5 -0
  181. package/src/ai-pi.ts +1322 -0
  182. package/src/ai-sdk-step.ts +2 -2
  183. package/src/ai-server.ts +3 -1
  184. package/src/ai-tool-input.ts +45 -0
  185. package/src/internal.ts +13 -0
  186. package/src/libfx.d.ts +34 -0
  187. package/src/reducer.ts +1 -1
  188. package/src/scheduler-qstash.ts +5 -5
  189. package/src/scheduler-vercel.ts +14 -15
  190. package/src/server.ts +233 -29
  191. package/src/store-memory.ts +92 -16
  192. package/src/store-postgres.ts +156 -25
  193. package/src/store-redis-core.ts +322 -110
  194. package/src/store-redis-notify.ts +2 -0
  195. package/src/store-sqlite.ts +121 -28
  196. package/src/store.ts +55 -3
  197. package/src/telemetry.ts +1 -0
  198. package/dist/internal-CbVvEwad.js.map +0 -1
  199. package/dist/server-CGM8WzRG.d.ts.map +0 -1
  200. package/dist/server-Rj64fNvX.js.map +0 -1
  201. package/dist/session-clear-Dshpuhff.d.ts.map +0 -1
  202. package/dist/store-Gpkq7WBo.d.ts.map +0 -1
  203. package/dist/store-redis-notify-tqWNnMRu.js.map +0 -1
  204. package/dist/telemetry-cZH-cibj.d.ts.map +0 -1
  205. package/docs/concepts/01-contracts.mdx +0 -237
  206. package/docs/concepts/02-handlers.mdx +0 -284
  207. package/docs/concepts/03-durability.mdx +0 -314
  208. package/docs/concepts/04-state.mdx +0 -209
  209. package/docs/guides/01-timers.mdx +0 -193
  210. package/docs/guides/02-cancellation.mdx +0 -161
  211. package/docs/guides/03-react.mdx +0 -324
  212. package/docs/guides/04-local-first.mdx +0 -47
  213. package/docs/guides/05-production.mdx +0 -534
  214. package/docs/guides/06-ai-agents.mdx +0 -1408
  215. package/docs/guides/08-application-data.mdx +0 -113
  216. package/docs/guides/09-presence.mdx +0 -262
  217. package/docs/guides/10-transports.mdx +0 -221
  218. package/docs/reference/01-api.mdx +0 -2358
  219. package/docs/reference/02-errors.mdx +0 -150
  220. /package/docs/{concepts → contract}/meta.ts +0 -0
  221. /package/docs/{guides → server}/meta.ts +0 -0
@@ -0,0 +1,351 @@
1
+ ---
2
+ title: Adapters
3
+ description: "Stores, schedulers, and OpenTelemetry."
4
+ ---
5
+
6
+ ## `experimental-a2/otel`
7
+
8
+ ```ts
9
+ otel(options?: { tracer?: Tracer; tracerName?: string }): A2Telemetry
10
+ ```
11
+
12
+ OpenTelemetry instrumentation for a server (`@opentelemetry/api` is a peer
13
+ dependency). Pass it as `telemetry`. Spans nest through the active context, so
14
+ the happy path lands in one trace: request → append → drain → each handler →
15
+ the events those handlers append. `A2Telemetry` is a one-method interface:
16
+ anything with `span(name, attributes, fn)` works, and `otel()` is the adapter
17
+ A2 ships. See [Watch it in traces](/server/production#watch-it-in-traces).
18
+
19
+ ### The span catalogue
20
+
21
+ This catalogue is a contract: renames and additions are breaking for
22
+ dashboards and are called out in the package's `CHANGELOG.md`. All spans carry
23
+ `a2.contract` and `a2.session_id`. Core spans describe the durable runtime;
24
+ `a2.ai.*` spans describe the built-in AI handlers.
25
+
26
+ | Layer | Span | Wraps |
27
+ | ----- | ---- | ----- |
28
+ | Core | `a2.append` | validation + the store write (one batch) |
29
+ | Core | `a2.drain` | one drain pass, including concurrent handlers |
30
+ | Core | `a2.event` | one claimed dispatch, including its handler and completion |
31
+ | Core | `a2.claim` | one `claimAvailable` store operation |
32
+ | Core | `a2.commit` | one `completeAttempt` or `failAttempt` store operation |
33
+ | Core | `a2.state` | one `state()` read: snapshot-plus-tail load + reducer fold |
34
+ | Core | `a2.snapshot` | one background batch of checkpoint writes |
35
+ | Core | `a2.redrive` | one `session.redrive()`: the store operation and starting the session's wakeup |
36
+ | AI | `a2.ai.control` | applying an input/work report/session-close decision and preparing its returned events |
37
+ | AI | `a2.ai.generate` | one generation handler, including Core calls and model/SDK work |
38
+ | AI | `a2.ai.context` | constructing model messages, including tool prompt reconstruction when uncached |
39
+ | AI | `a2.ai.compaction` | automatic summarization or the custom compaction callback |
40
+ | AI | `a2.ai.tool` | one durable tool handler, including application execution |
41
+ | AI | `a2.ai.ack` | waiting for a persisted work report's canonical events and control receipt |
42
+
43
+ Durations include nested work: `a2.event` contains its AI handler and later
44
+ commit, AI handlers call Core, and `a2.state` includes reducer code.
45
+ Acknowledgements overlap control processing, claims can cover several events,
46
+ and snapshots run in the background, so summing spans, or subtracting summed
47
+ children, does not yield exclusive Core or AI overhead. Model and application
48
+ I/O is included; its child spans have the detail.
49
+
50
+ Attributes arrive in two waves: **at start** (passed to `span()`) and
51
+ **mid-span** (via `handle.setAttribute`, for outcomes and counts known only
52
+ after the work ran). A recorder must merge both; the ones you alert on are all
53
+ mid-span.
54
+
55
+ | Attribute | Span | When | Values |
56
+ | --------------------- | ----------- | ----- | ----------------------------------------------------- |
57
+ | `a2.append.source` | `a2.append` | start | `external` \| `handler` |
58
+ | `a2.append.mode` | `a2.append` | start | `inline` \| `dispatch` |
59
+ | `a2.append.types` | `a2.append` | start | comma-joined event types |
60
+ | `a2.append.count` | `a2.append` | start | batch size |
61
+ | `a2.append.armed` | `a2.append` | mid | `false` when the scheduler arm failed and this append degraded to append-driven healing |
62
+ | `a2.drain.outcome` | `a2.drain` | mid | `settled` \| `busy` \| `stalled` |
63
+ | `a2.drain.processed` | `a2.drain` | mid | events processed this pass |
64
+ | `a2.claim.outcome` | `a2.claim` | mid | `claimed` \| `busy` \| `settled` |
65
+ | `a2.claim.count` | `a2.claim` | mid | number of claimed events, or zero |
66
+ | `a2.commit.operation` | `a2.commit` | start | `complete` \| `fail` |
67
+ | `a2.commit.index` | `a2.commit` | start | event log position |
68
+ | `a2.commit.attempt` | `a2.commit` | start | durable 1-based attempt ordinal |
69
+ | `a2.commit.outcome` | `a2.commit` | mid | `completed` \| `failed` \| `dead_lettered` \| `superseded` |
70
+ | `a2.event.type` | `a2.event` | start | the event's type |
71
+ | `a2.event.index` | `a2.event` | start | log position |
72
+ | `a2.event.id` | `a2.event` | start | event id |
73
+ | `a2.event.attempt` | `a2.event` | start | same durable 1-based ordinal as `ctx.attempt` |
74
+ | `a2.event.lane` | `a2.event` | start | stored lane value; absent for concurrent unlaned work |
75
+ | `a2.event.handled` | `a2.event` | start | normally `true`; `false` when a custom-adapter row has no handler |
76
+ | `a2.event.outcome` | `a2.event` | mid | `processed` \| `failed` \| `dead_lettered` \| `superseded` \| `surrendered` |
77
+ | `a2.event.aborted` | `a2.event` | mid | `true` when `abortOn` fired during the run |
78
+ | `a2.state.reducer` | `a2.state` | start | the reducer's name |
79
+ | `a2.state.snapshot` | `a2.state` | mid | `hit` \| `miss` \| `rejected` (schema guard discarded it) \| `memory` (served from the server's in-memory state) |
80
+ | `a2.state.folded` | `a2.state` | mid | events folded past the snapshot |
81
+ | `a2.state.index` | `a2.state` | mid | the frontier the returned state reflects |
82
+ | `a2.snapshot.reducer` | `a2.snapshot` | start | the reducer's name |
83
+ | `a2.snapshot.index` | `a2.snapshot` | start | greatest checkpoint index in the write batch |
84
+ | `a2.snapshot.count` | `a2.snapshot` | start | number of checkpoints in the write batch |
85
+ | `a2.redrive.index` | `a2.redrive` | start | log position of the event being redriven |
86
+ | `a2.redrive.action` | `a2.redrive` | start | `retry` \| `skip` |
87
+ | `a2.redrive.redriven` | `a2.redrive` | mid | `true` when the event was dead-lettered and is now resolved |
88
+ | `a2.redrive.armed` | `a2.redrive` | mid | `false` when the scheduler arm failed and the wakeup degraded to append-driven healing |
89
+ | `a2.ai.control.type` | `a2.ai.control` | start | `ai.control.requested` \| `ai.work.reported` \| `ai.session.closed` |
90
+ | `a2.ai.control.id` | `a2.ai.control` | start | triggering event id |
91
+ | `a2.ai.control.attempt` | `a2.ai.control` | start | durable 1-based attempt ordinal |
92
+ | `a2.ai.generate.request_id` | `a2.ai.generate` | start | generation request event id |
93
+ | `a2.ai.generate.attempt` | `a2.ai.generate` | start | durable 1-based attempt ordinal |
94
+ | `a2.ai.context.request_id` | `a2.ai.context` | start | generation request event id |
95
+ | `a2.ai.context.phase` | `a2.ai.context` | start | `generation` \| `refresh` \| `tool` |
96
+ | `a2.ai.compaction.request_id` | `a2.ai.compaction` | start | generation request event id |
97
+ | `a2.ai.compaction.generation_id` | `a2.ai.compaction` | start | generation attempt id |
98
+ | `a2.ai.compaction.policy` | `a2.ai.compaction` | start | `automatic` \| `custom` |
99
+ | `a2.ai.tool.request_id` | `a2.ai.tool` | start | originating generation request event id |
100
+ | `a2.ai.tool.call_id` | `a2.ai.tool` | start | tool call id |
101
+ | `a2.ai.tool.name` | `a2.ai.tool` | start | tool name |
102
+ | `a2.ai.tool.attempt` | `a2.ai.tool` | start | durable 1-based attempt ordinal |
103
+ | `a2.ai.ack.request_id` | `a2.ai.ack` | start | reporting worker event id |
104
+ | `a2.ai.ack.report_id` | `a2.ai.ack` | start | work report event id, matching the control span's id |
105
+
106
+ - Drain outcomes: `settled` means nothing actionable is left; `busy` means live
107
+ per-event claims or a failed event's retry hold remain; `stalled` means a
108
+ caught failure in this drain needs a later retry.
109
+ - A failing handler marks its `a2.event` span with the exception and error
110
+ status. Alert on `a2.event.outcome = dead_lettered`. A dead-lettered event
111
+ blocks only later events in its lane, until `session.redrive()` resolves it;
112
+ other lanes stay eligible.
113
+ - `superseded` means a stale completion or failure did not change the log.
114
+
115
+ ## `experimental-a2/store-postgres`
116
+
117
+ `postgres({ connectionString })` returns an `A2Store` on Postgres; pass
118
+ `client` to inject your own pool. See [Postgres](/server/production#postgres)
119
+ for setup.
120
+
121
+ - Without `listenConnectionString`, a live stream polls with an adaptive
122
+ cadence: every 25ms while the session is producing events, backing off to
123
+ two seconds when it goes quiet. After a quiet period, an update from another
124
+ client can take up to the idle interval plus query latency to arrive.
125
+ - A client's own `push()` refreshes its idle stream immediately from the
126
+ current cursor, even when the write and stream routes run in different
127
+ invocations. A WebSocket push wakes the attached stream without reopening
128
+ the socket.
129
+ - With `listenConnectionString`, notifications arrive after commit, and a
130
+ reconnect catches up from the cursor. Native event and presence
131
+ subscriptions also reconcile every ten seconds.
132
+
133
+ ## `experimental-a2/store-redis`
134
+
135
+ `redis({ url })` returns an `A2Store` that keeps each session as a Redis
136
+ Stream; pass `client` to inject your own. See
137
+ [Redis](/server/production#redis-recommended) for setup and per-command costs.
138
+
139
+ - Every write publishes a wake-up, so live streams are push-driven, not
140
+ polled.
141
+ - A wake-up is disposable: a safety re-read recovers a lost one within ten
142
+ seconds.
143
+
144
+ ## `experimental-a2/store-redis-http`
145
+
146
+ `redisHttp({ url, token })` returns the same Redis store over `fetch`, for
147
+ providers that expose only a REST API, such as Upstash. See
148
+ [Redis](/server/production#redis-recommended) for setup and the trust warning.
149
+
150
+ - Live events and presence use Upstash's HTTP `SUBSCRIBE` endpoint (SSE), with
151
+ the same URL and token. Each active channel holds one outbound HTTP
152
+ subscription per store instance, shared by its local viewers and closed when
153
+ the last viewer leaves.
154
+ - Writes publish their wake-ups in the same Redis script as the append. A
155
+ rejected notification does not fail the applied write.
156
+ - Idle event streams re-read every ten seconds to recover missed
157
+ notifications. Subscriptions reconnect on their own and catch up from the
158
+ event cursor.
159
+ - After a reconnect, A2 re-reads the current presence fields and clears fields
160
+ deleted during the gap, so presence may repeat its latest values. Updates
161
+ received during that read take precedence.
162
+ - If the subscribe endpoint answers 404, 405, or 501, events and presence use
163
+ adaptive polling instead, from 25ms to two seconds while idle.
164
+
165
+ ## `experimental-a2/scheduler-vercel`
166
+
167
+ `vercelQueues(options?)` returns an `A2Scheduler` backed by Vercel Queues.
168
+
169
+ ```ts server/scheduler-vercel.ts
170
+ import { vercelQueues } from 'experimental-a2/scheduler-vercel'
171
+
172
+ export const scheduler = vercelQueues()
173
+ ```
174
+
175
+ | Option | Type | Default |
176
+ | --- | --- | --- |
177
+ | `topic` | `string` matching `[A-Za-z0-9_-]+` | `'a2'` |
178
+ | `deploymentId` | `string \| null` | `VERCEL_DEPLOYMENT_ID` |
179
+ | `transport` | `QueueTransport` | lazy `@vercel/queue` client |
180
+ | `clock` | `{ now(): Date }` | system clock |
181
+
182
+ - `deploymentId` passes to the Queue SDK unchanged and applies to every task,
183
+ recovery watchdogs included. Omit it for `VERCEL_DEPLOYMENT_ID`, pass an ID
184
+ to target that deployment, or pass `null` for deploymentless delivery.
185
+ Publishing and consuming clients must use the same deployment scope.
186
+ - One configured client handles sends, payload fetches, acknowledgements, and
187
+ visibility changes; callback contents never choose the client.
188
+ - Pinned tasks stay with their target deployment after another is promoted.
189
+ Deploymentless delivery lets Vercel choose an eligible consumer, not the
190
+ latest production deployment; every eligible consumer must use
191
+ `deploymentId: null` and stay compatible with pending tasks and session work.
192
+ - Tasks are retained for seven days. Delays are accepted through six days,
193
+ leaving the final day for delivery and retries.
194
+ - `transport` and `clock` are advanced injection seams for tests and custom
195
+ delivery. An injected transport receives the topic, an exact
196
+ `SchedulerTask`, and send options, and owns its provider configuration;
197
+ `deploymentId` configures only the built-in client.
198
+
199
+ See [Vercel Queues](/server/production#vercel-queues-recommended).
200
+
201
+ ## `experimental-a2/scheduler-qstash`
202
+
203
+ `qstash(options?)` returns an `A2Scheduler` backed by QStash HTTP delivery.
204
+ Vercel Queues remains the recommended default on Vercel; use QStash for a
205
+ scheduler that calls a stable public endpoint.
206
+
207
+ ```ts server/scheduler-qstash.ts
208
+ import { qstash } from 'experimental-a2/scheduler-qstash'
209
+
210
+ export const scheduler = qstash()
211
+ ```
212
+
213
+ | Option | Type | Default |
214
+ | --- | --- | --- |
215
+ | `url` | `string` | conventional local or Vercel callback; required for process-less runtimes, other production, and custom routes |
216
+ | `token` | `string` | `QSTASH_TOKEN` |
217
+ | `currentSigningKey` | `string` | `QSTASH_CURRENT_SIGNING_KEY` |
218
+ | `nextSigningKey` | `string` | `QSTASH_NEXT_SIGNING_KEY` |
219
+ | `headers` | `Readonly<Record<string, string>>` | no additional callback headers |
220
+ | `client` | `QStashPublishingClient` | lazy `@upstash/qstash` client |
221
+ | `retries` | non-negative integer | the QStash provider default |
222
+ | `maxBodyBytes` | positive integer | `1_048_576` (1 MiB) for callbacks and reserved outbound bodies |
223
+ | `transport` | `QStashTransport` | lazy `@upstash/qstash` client and receiver |
224
+ | `clock` | `{ now(): Date }` | system clock |
225
+
226
+ Mount the application handler once and pass every server that shares the
227
+ scheduler:
228
+
229
+ ```ts app/api/a2/scheduler/route.ts
230
+ import { billingServer, ordersServer, scheduler } from '@/server'
231
+
232
+ export const POST = scheduler.handler(ordersServer, billingServer)
233
+ ```
234
+
235
+ **Callback URL.** An explicit `url` always wins. Otherwise:
236
+
237
+ - Vercel production: `https://${VERCEL_PROJECT_PRODUCTION_URL}/api/a2/scheduler`.
238
+ Preview: `https://${VERCEL_BRANCH_URL}/api/a2/scheduler`. Both are moving
239
+ aliases, so pending tasks can reach later deployments on the same URL.
240
+ - A runtime that exposes `process.env`, outside `NODE_ENV=production`:
241
+ `http://localhost:${PORT ?? 3000}/api/a2/scheduler`, with `devMode: true` on
242
+ the SDK client and receiver. `QSTASH_DEV` is not required, and
243
+ `QSTASH_DEV=false` does not disable it. `PORT` must be an integer from 1
244
+ through 65535; `QSTASH_DEV_PORT` configures the local QStash server, not the
245
+ callback.
246
+ - Automatic local startup needs `@upstash/qstash` 2.11.0 or newer in Node.js;
247
+ the lazy transport starts it on the first publish or verification. An Edge
248
+ runtime that exposes `process.env` infers the local URL but must start the
249
+ development server separately.
250
+ - Non-Vercel production, process-less runtimes, custom hosts or paths, ports
251
+ not available through `PORT`, and Docker networking need an explicit
252
+ absolute URL.
253
+ - Outside production, an explicit URL leaves SDK development mode to
254
+ `QSTASH_DEV` or a manual `QSTASH_URL` plus credentials. In production, A2
255
+ passes `devMode: false`.
256
+
257
+ **Options.**
258
+
259
+ - `headers`: static headers on every callback delivery, recovery successors
260
+ included. Validated and snapshotted when `qstash()` runs; A2 asks QStash to
261
+ redact every value from its dashboard and API, but QStash still receives,
262
+ stores, and delivers them. Names are case-insensitive. Reserved:
263
+ provider-owned `Upstash-*`, `Content-Type*`, HTTP framing headers, and A2's
264
+ managed Vercel identity headers.
265
+ - `client`: the structural `publishJSON(...)` part of a configured
266
+ `@upstash/qstash` `Client`, for its API endpoint, publish-request retry
267
+ policy, or telemetry. Not combinable with `token` or `transport`; A2 still
268
+ creates the `Receiver` lazily from the signing keys. Put callback credentials
269
+ in `headers`, since client-global callback headers and redaction are outside
270
+ A2's merge contract. The client receives raw callback credentials, including
271
+ an automatic preview identity, and must honor per-publish redaction.
272
+ - `transport`, `clock`: advanced injection seams for tests and custom delivery.
273
+ A `QStashTransport` implements `publish(request)` and `verify(request)`.
274
+ Publish requests carry the callback `url`, a complete `SchedulerTask` body
275
+ (plus provider-private generation metadata for store-outage successors),
276
+ optional `headers` and `redact.header` names, an optional Unix-seconds
277
+ `notBefore`, and a deterministic `deduplicationId`; preserve the body,
278
+ deliver every header, and apply every redaction. Verify requests carry the
279
+ signature, raw body, exact callback URL, and optional Upstash region.
280
+ - `retries`: omitted, QStash uses its provider default; an explicit value is
281
+ forwarded. Every delivery attempt is billable.
282
+ - `maxBodyBytes`: the adapter reserves room for a task's largest possible
283
+ store-outage lineage before its initial publish, so an accepted task never
284
+ exceeds the limit later. Raise it for a larger paid-plan payload limit. Keep
285
+ one value across every deployment that receives this URL: a lower value can
286
+ return a non-retryable 489 for a task an older value accepted.
287
+
288
+ **The route.**
289
+
290
+ - Must be reachable by the selected QStash server after any platform edge
291
+ authentication; the development server calls localhost directly.
292
+ - Reads the raw body once and verifies `Upstash-Signature`, the exact URL, and
293
+ `Upstash-Region` before parsing or touching a server. Missing or invalid
294
+ signatures return 401.
295
+ - A signed malformed envelope, or a declared or streamed body over
296
+ `maxBodyBytes`, returns 489 with `Upstash-NonRetryable-Error: true`.
297
+ - Returns 200 when settled; when busy or stalled, after a durable successor
298
+ watchdog exists, so each caught failure gets a fresh message and a fresh
299
+ per-message retry budget. Append tasks return 200 after their fixed-id append
300
+ succeeds and any recovery watchdog for pending handler work is durable; clear
301
+ tasks after their conditional clear and its deletion finish.
302
+ - A retryable store outage hands drain, append, and clear tasks to a fresh
303
+ successor before acknowledging: 30 seconds, times four per signed
304
+ generation, capped at one day. The generation keeps increasing past the cap,
305
+ so each handoff gets a new deduplication id, and duplicate deliveries of one
306
+ generation publish the same successor.
307
+ - Unknown contracts, missing scheduler configuration, schema failures, and
308
+ successor publish failures keep the delivery retryable.
309
+
310
+ **Delivery.**
311
+
312
+ - At least once, so handler side effects need idempotency keys. QStash's
313
+ documented 10-minute deduplication window can reduce duplicate watchdogs but
314
+ is not part of A2's correctness model; stable scheduled-event ids keep timer
315
+ redelivery safe after it.
316
+ - During a prolonged outage where QStash cannot reach the callback, nothing
317
+ publishes a successor: monitor the DLQ and redrive.
318
+ - One-shot delays are capped at 7 days on free plans and 1 year on usage-based
319
+ plans, with no fixed maximum on fixed plans. Leave one second of margin at a
320
+ plan maximum: `notBefore` uses whole Unix seconds.
321
+ - Carries both recovery watchdogs and `session.schedule` tasks.
322
+
323
+ **Protected previews.** For an inferred preview callback, each publish
324
+ forwards the invocation's `x-vercel-oidc-token` as
325
+ `x-vercel-trusted-oidc-idp-token`, with redaction requested, when it is a
326
+ parseable JWT that expires at least 60 seconds after the task's first due time,
327
+ so a matching Vercel Trusted Sources rule can admit the first delivery. QStash
328
+ still receives it. This is opportunistic: missing, malformed, and near-expiry
329
+ tokens fall back to an ordinary publish; an explicit `url` opts out; inferred
330
+ production and local callbacks never forward it; and QStash reuses the stored
331
+ header on retries, so delayed tasks and retries can outlive it. When every
332
+ protected delivery must stay authorized, use an explicit automation bypass
333
+ through `headers` (pending messages keep the captured value after rotation) or
334
+ public ingress. Signature verification still applies after the edge-access
335
+ check.
336
+
337
+ See [QStash](/server/production#qstash).
338
+
339
+ ## Scheduler send errors
340
+
341
+ Both built-in adapters preserve the original provider error from a rejected
342
+ send.
343
+
344
+ - Terminal: a recognized definite 4xx status or error, except 408, 409, 425,
345
+ and 429. The Vercel adapter also treats the SDK's name-only
346
+ `BadRequestError`, `UnauthorizedError`, and `ForbiddenError` as terminal.
347
+ - Retryable: 408, 409, 425, 429, 5xx responses, network failures, and errors
348
+ with no recognized status.
349
+ - The Vercel adapter's native duplicate-message error is success.
350
+ - A2's durable AI tool execution uses this private classification. Direct
351
+ `session.schedule()` calls still reject with the original error object.
@@ -0,0 +1,138 @@
1
+ ---
2
+ title: Errors
3
+ description: Every A2 error code, when it's thrown, whether it's retryable, and how it crosses the wire.
4
+ ---
5
+
6
+ ## `A2Error`
7
+
8
+ A2 failures carry an `A2Error` with a stable `code`. The session client
9
+ exposes `A2ClientError`, which extends `A2Error`, for server failures and local
10
+ ones alike, so `instanceof A2Error` matches both.
11
+
12
+ ```ts
13
+ // e.g. in a route handler:
14
+ import { A2Error } from 'experimental-a2'
15
+
16
+ export async function POST(req: Request) {
17
+ const { orderId, payload } = await req.json()
18
+ try {
19
+ const events = await ordersServer
20
+ .session(orderId)
21
+ .append({ type: 'created', payload })
22
+ return Response.json(events)
23
+ } catch (err) {
24
+ if (err instanceof A2Error && err.code === 'INVALID_PAYLOAD') {
25
+ // err.details carries the schema issues, in Standard Schema format
26
+ return Response.json({ issues: err.details }, { status: 400 })
27
+ }
28
+ throw err
29
+ }
30
+ }
31
+ ```
32
+
33
+ ## The codes
34
+
35
+ | Code | Thrown when | Retryable |
36
+ | --- | --- | --- |
37
+ | `INVALID_PAYLOAD` | a payload fails its schema; nothing was written. `details` carries the issues in [Standard Schema](https://standardschema.dev) format | no |
38
+ | `FORBIDDEN` | `server.fetch` authorization denies a parsed stream, history read, push, or presence update | no |
39
+ | `UNKNOWN_EVENT_TYPE` | an event type isn't in the contract's `events` map | no |
40
+ | `UNKNOWN_PRESENCE_FIELD` | a presence field isn't in the contract's `presence` map | no |
41
+ | `PRESENCE_NOT_SUPPORTED` | the contract declares `presence` but the store has no presence capability. Thrown at construction; for environment-resolved stores, at first store use | no |
42
+ | `PARTIAL_DUPLICATE_BATCH` | a batch mixes already-appended and fresh events, contains the same id twice, or uses an id already used in a different session (event ids are globally unique). Always a caller bug; nothing was written | no |
43
+ | `SUPERSEDED_ATTEMPT` | a handler append or clear comes from an attempt that a recovery claim or a session clear replaced; nothing was written. Also the abort reason on that handler's `ctx.signal` | no |
44
+ | `SESSION_CLEARED` | a state read reaches below the latest clear, a handler reads state past a clear of its event, or an actor call is cleared before it receives its answer | no |
45
+ | `CLAIM_EXPIRED` | never thrown; only the `ctx.signal` abort reason when the attempt's lease lapsed without renewal (assumed loss, decided locally, not proof) | no |
46
+ | `STORE_UNAVAILABLE` | the store backend failed; nothing was written. The original error is the `cause` | **yes** |
47
+ | `STORE_NOT_CONFIGURED` | production boots with no `store` configured (during `next build` page collection, deferred to first use) | no |
48
+
49
+ Retrying an *identical* batch is not `PARTIAL_DUPLICATE_BATCH`: it's an
50
+ idempotent success that returns the previously appended events. A failing
51
+ handler never makes `append` throw; handler failures retry and, if persistent,
52
+ [dead-letter the event](/server/guarantees#when-a-handler-keeps-failing). See
53
+ [What append never throws for](/server/guarantees#what-append-never-throws-for).
54
+
55
+ ## `A2ClientError`
56
+
57
+ Import `A2ClientError` from `experimental-a2/client`. Connection, push, and
58
+ history read failures, and local A2 validation failures, use this class. Server
59
+ failures keep their `code`, `message`, and `details`; unexpected thrown values
60
+ are kept as `cause`. Invalid API arguments can still throw a native `TypeError`
61
+ or `RangeError`.
62
+
63
+ | Client code | Meaning |
64
+ | --- | --- |
65
+ | `TIMEOUT` | A stream stall (`phase: 'stream'`, 35 seconds of silence after connection) or an unconfirmed multiplexed subscription (`phase: 'subscribe'`, 35 seconds without confirmation). `timeoutMs` is the expired deadline. |
66
+ | `HTTP_ERROR` | An unsuccessful HTTP response without a recognized A2 error body. `status` carries the HTTP status. |
67
+ | `CONNECTION_FAILED` | The transport could not establish or retain a connection. WebSocket close events keep `closeCode` and `closeReason` when available. |
68
+ | `UNKNOWN` | An unexpected client failure. The original value is the `cause`. |
69
+
70
+ - HTTP failures keep `status` even when they carry a recognized server code.
71
+ - A browser WebSocket handshake rejection has no readable HTTP status or body,
72
+ so it stays `CONNECTION_FAILED`. A2 never infers an authorization failure or
73
+ a timeout from an opaque socket error.
74
+ - A2 sets no initial connection deadline for HTTP or single-session WebSockets.
75
+ - A clean stream end is not an error: the reconnection callback and the
76
+ connection snapshot carry `null`.
77
+ - Pushes retry `STORE_UNAVAILABLE` and client transport failures, up to three
78
+ attempts. No other server code is retried.
79
+ - The [reconnection policy](/client/transports#lifecycle) controls only the
80
+ live stream. Pausing the stream preserves the writer, but a push can still
81
+ fail while its WebSocket is unavailable.
82
+
83
+ ## NonRetriableError
84
+
85
+ Throw it (or a subclass, matched by `instanceof`) from a handler when the
86
+ failure is deterministic: a guard that refuses this attempt refuses every
87
+ retry identically. The event dead-letters immediately instead of consuming the
88
+ ten-failure retry budget. Any other thrown error keeps the
89
+ ordinary retry contract.
90
+
91
+ ```ts
92
+ import { z } from 'zod'
93
+ import * as a2 from 'experimental-a2'
94
+ import { NonRetriableError } from 'experimental-a2'
95
+ import { createServer } from 'experimental-a2/server'
96
+
97
+ const orders = a2.contract({
98
+ name: 'orders',
99
+ events: { created: z.object({ chargeable: z.boolean() }) },
100
+ })
101
+
102
+ export const ordersServer = createServer({
103
+ contract: orders,
104
+ handlers: {
105
+ created: async ({ event }) => {
106
+ if (!event.payload.chargeable) {
107
+ throw new NonRetriableError('this order cannot be charged')
108
+ }
109
+ // transient failures below retry as usual
110
+ },
111
+ },
112
+ })
113
+ ```
114
+
115
+ ## Over the wire
116
+
117
+ `server.fetch` owns HTTP serialization. The push route sends an `A2Error` as:
118
+
119
+ ```json
120
+ { "error": { "code": "INVALID_PAYLOAD", "message": "...", "details": [] } }
121
+ ```
122
+
123
+ with a mapped status:
124
+
125
+ | Status | Codes |
126
+ | --- | --- |
127
+ | 400 | `INVALID_PAYLOAD`, `UNKNOWN_EVENT_TYPE`, `PARTIAL_DUPLICATE_BATCH` |
128
+ | 403 | `FORBIDDEN` |
129
+ | 409 | `SUPERSEDED_ATTEMPT` |
130
+ | 503 | `STORE_UNAVAILABLE` |
131
+
132
+ - Unknown server exceptions become `STORE_UNAVAILABLE`; their private causes
133
+ are not sent to clients.
134
+ - The session client deserializes the body into an `A2ClientError`, so client
135
+ and server code branch on identical server codes.
136
+ - Multiplexed WebSocket subscription failures carry the same envelope in their
137
+ `unsubscribed` frame, alongside the legacy `reason` string. Older reason-only
138
+ frames read as `CONNECTION_FAILED`.
@@ -1 +1 @@
1
- export default { order: 5 }
1
+ export default { order: 7 }