experimental-a2 0.17.0 → 0.18.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.
- package/CHANGELOG.md +21 -0
- package/README.md +1 -1
- package/dist/{actor-DBbe5xeE.d.ts → actor-Bjce9_5U.d.ts} +5 -5
- package/dist/{actor-DBbe5xeE.d.ts.map → actor-Bjce9_5U.d.ts.map} +1 -1
- package/dist/actor-client.d.ts +1 -1
- package/dist/actor-react.d.ts +3 -3
- package/dist/actor-react.js +1 -1
- package/dist/actor.d.ts +1 -1
- package/dist/actor.js +2 -2
- package/dist/{ai-UrWfKeuv.js → ai-BpOGm-zB.js} +2 -2
- package/dist/{ai-UrWfKeuv.js.map → ai-BpOGm-zB.js.map} +1 -1
- package/dist/{ai-vpGtcaCB.d.ts → ai-DrUaMyRt.d.ts} +3 -3
- package/dist/{ai-vpGtcaCB.d.ts.map → ai-DrUaMyRt.d.ts.map} +1 -1
- package/dist/ai-client.d.ts +2 -2
- package/dist/ai-client.js +2 -2
- package/dist/ai-fx.browser.d.ts +1 -0
- package/dist/ai-fx.browser.js +6 -0
- package/dist/ai-fx.browser.js.map +1 -0
- package/dist/ai-fx.d.ts +10 -0
- package/dist/ai-fx.d.ts.map +1 -0
- package/dist/ai-fx.js +634 -0
- package/dist/ai-fx.js.map +1 -0
- package/dist/ai-harness-BuLh_EYi.js +273 -0
- package/dist/ai-harness-BuLh_EYi.js.map +1 -0
- package/dist/ai-harness-DvsPeJeB.d.ts +26 -0
- package/dist/ai-harness-DvsPeJeB.d.ts.map +1 -0
- package/dist/ai-pi.browser.d.ts +1 -0
- package/dist/ai-pi.browser.js +6 -0
- package/dist/ai-pi.browser.js.map +1 -0
- package/dist/ai-pi.d.ts +17 -0
- package/dist/ai-pi.d.ts.map +1 -0
- package/dist/ai-pi.js +904 -0
- package/dist/ai-pi.js.map +1 -0
- package/dist/ai-react.d.ts +5 -5
- package/dist/ai-react.js +2 -2
- package/dist/ai-server.d.ts +47 -25
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +106 -37
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +1 -1
- package/dist/ai.js +1 -1
- package/dist/bounded-cache-CPHWF-8h.js +41 -0
- package/dist/bounded-cache-CPHWF-8h.js.map +1 -0
- package/dist/{client-COaiaDub.js → client-BReKqwZC.js} +3 -3
- package/dist/{client-COaiaDub.js.map → client-BReKqwZC.js.map} +1 -1
- package/dist/{client-qRnRUzBS.d.ts → client-h_DKFAjj.d.ts} +3 -3
- package/dist/{client-qRnRUzBS.d.ts.map → client-h_DKFAjj.d.ts.map} +1 -1
- package/dist/client.d.ts +3 -3
- package/dist/client.js +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/{initial-snapshot-BrLso5x_.d.ts → initial-snapshot-Bx-c0pDP.d.ts} +2 -2
- package/dist/{initial-snapshot-BrLso5x_.d.ts.map → initial-snapshot-Bx-c0pDP.d.ts.map} +1 -1
- package/dist/{internal-CbVvEwad.js → internal-B2T3U4Rq.js} +8 -2
- package/dist/internal-B2T3U4Rq.js.map +1 -0
- package/dist/otel.d.ts +1 -1
- package/dist/{react-BbJ-g6Uu.d.ts → react-BBXgHWFc.d.ts} +4 -4
- package/dist/{react-BbJ-g6Uu.d.ts.map → react-BBXgHWFc.d.ts.map} +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/reducer-BAXc8JyF.js.map +1 -1
- package/dist/{reducer-BYu6DT7h.d.ts → reducer-CqA59Q6U.d.ts} +2 -2
- package/dist/{reducer-BYu6DT7h.d.ts.map → reducer-CqA59Q6U.d.ts.map} +1 -1
- package/dist/scheduler-qstash.d.ts +2 -2
- package/dist/scheduler-qstash.js +2 -2
- package/dist/scheduler-vercel.d.ts +2 -2
- package/dist/scheduler-vercel.js +2 -2
- package/dist/{server-Rj64fNvX.js → server-Dw_k_7kO.js} +93 -60
- package/dist/server-Dw_k_7kO.js.map +1 -0
- package/dist/{server-CGM8WzRG.d.ts → server-En7cbi3v.d.ts} +36 -7
- package/dist/server-En7cbi3v.d.ts.map +1 -0
- package/dist/server.d.ts +4 -4
- package/dist/server.js +1 -1
- package/dist/{session-clear-Dshpuhff.d.ts → session-clear-Bo-XTFEy.d.ts} +2 -2
- package/dist/session-clear-Bo-XTFEy.d.ts.map +1 -0
- package/dist/{store-Gpkq7WBo.d.ts → store-Djdv0bnZ.d.ts} +31 -2
- package/dist/store-Djdv0bnZ.d.ts.map +1 -0
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-memory.js +32 -6
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-4_y8lETm.js → store-polling-B5FcwJfo.js} +2 -2
- package/dist/{store-polling-4_y8lETm.js.map → store-polling-B5FcwJfo.js.map} +1 -1
- package/dist/store-postgres.d.ts +1 -1
- package/dist/store-postgres.js +54 -3
- package/dist/store-postgres.js.map +1 -1
- package/dist/{store-presence-polling-Q1C-k2dd.js → store-presence-polling-BAwS8RWn.js} +3 -3
- package/dist/{store-presence-polling-Q1C-k2dd.js.map → store-presence-polling-BAwS8RWn.js.map} +1 -1
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +3 -3
- package/dist/{store-redis-notify-tqWNnMRu.js → store-redis-notify-BRITti_o.js} +187 -68
- package/dist/store-redis-notify-BRITti_o.js.map +1 -0
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.js +1 -1
- package/dist/store-sqlite.d.ts +1 -1
- package/dist/store-sqlite.js +32 -2
- package/dist/store-sqlite.js.map +1 -1
- package/dist/{telemetry-cZH-cibj.d.ts → telemetry-Cyfqo6Qt.d.ts} +2 -2
- package/dist/telemetry-Cyfqo6Qt.d.ts.map +1 -0
- package/dist/{wire-DjfHOvdW.js → wire-rSUglYo8.js} +2 -2
- package/dist/{wire-DjfHOvdW.js.map → wire-rSUglYo8.js.map} +1 -1
- package/docs/01-quickstart.mdx +49 -83
- package/docs/{guides/07-examples.mdx → 02-examples.mdx} +10 -20
- package/docs/actors/01-introduction.mdx +48 -86
- package/docs/actors/02-concurrency.mdx +43 -70
- package/docs/actors/03-timers.mdx +28 -42
- package/docs/actors/04-routes.mdx +110 -150
- package/docs/actors/meta.ts +1 -1
- package/docs/agents/01-introduction.mdx +257 -0
- package/docs/agents/02-tools.mdx +247 -0
- package/docs/agents/03-conversations.mdx +538 -0
- package/docs/agents/04-harnesses.mdx +136 -0
- package/docs/agents/meta.ts +1 -0
- package/docs/client/01-react.mdx +294 -0
- package/docs/client/02-transports.mdx +86 -0
- package/docs/client/03-presence.mdx +224 -0
- package/docs/client/meta.ts +1 -0
- package/docs/contract/01-events.mdx +57 -0
- package/docs/contract/02-state.mdx +142 -0
- package/docs/contract/03-sessions.mdx +175 -0
- package/docs/index.mdx +108 -144
- package/docs/reference/01-core.mdx +139 -0
- package/docs/reference/02-server.mdx +855 -0
- package/docs/reference/03-client.mdx +301 -0
- package/docs/reference/04-ai.mdx +722 -0
- package/docs/reference/05-adapters.mdx +351 -0
- package/docs/reference/06-errors.mdx +138 -0
- package/docs/reference/meta.ts +1 -1
- package/docs/server/01-handlers.mdx +200 -0
- package/docs/server/02-timers.mdx +130 -0
- package/docs/server/03-cancellation.mdx +141 -0
- package/docs/server/04-routes.mdx +243 -0
- package/docs/server/05-production.mdx +322 -0
- package/docs/server/06-guarantees.mdx +123 -0
- package/examples/playground/DEPLOY.md +1 -1
- package/examples/playground/app/a0/[appId]/page.tsx +11 -6
- package/examples/playground/app/a0/app-list.tsx +9 -2
- package/examples/playground/app/a0/app-view.tsx +9 -1
- package/examples/playground/app/a0/apps/[appId]/route.ts +10 -5
- package/examples/playground/app/a0/apps.test.ts +12 -0
- package/examples/playground/app/a0/apps.ts +22 -7
- package/examples/playground/app/a0/chat.tsx +47 -1
- package/examples/playground/app/a0/fx/events/route.ts +9 -0
- package/examples/playground/app/a0/fx/scheduler/route.ts +7 -0
- package/examples/playground/app/a0/fx/short/events/route.ts +11 -0
- package/examples/playground/app/a0/model.test.ts +18 -1
- package/examples/playground/app/a0/model.ts +47 -10
- package/examples/playground/app/a0/no-photon.cjs +4 -0
- package/examples/playground/app/a0/no-quickjs.cjs +3 -0
- package/examples/playground/app/a0/pi/events/route.ts +9 -0
- package/examples/playground/app/a0/pi/scheduler/route.ts +7 -0
- package/examples/playground/app/a0/pi/short/events/route.ts +11 -0
- package/examples/playground/app/a0/sandbox-log.ts +10 -4
- package/examples/playground/app/a0/sandbox-session.ts +152 -0
- package/examples/playground/app/a0/scheduler/route.ts +2 -2
- package/examples/playground/app/a0/server.ts +100 -45
- package/examples/playground/app/a0/session.ts +17 -5
- package/examples/playground/app/a0/shell.tsx +11 -3
- package/examples/playground/app/agent/ingress.test.ts +6 -3
- package/examples/playground/app/agent/server.ts +9 -6
- package/examples/playground/app/agent/telemetry.test.ts +71 -50
- package/examples/playground/app/canvas/[canvasId]/canvas-client.tsx +1 -1
- package/examples/playground/app/chat/server.test.ts +30 -26
- package/examples/playground/app/chat/server.ts +46 -46
- package/examples/playground/next.config.ts +17 -5
- package/examples/playground/package.json +4 -1
- package/examples/playground/vercel.json +18 -0
- package/package.json +27 -3
- package/src/ai-agent.ts +261 -87
- package/src/ai-fx.browser.ts +5 -0
- package/src/ai-fx.ts +833 -0
- package/src/ai-harness.ts +518 -0
- package/src/ai-model-metadata.ts +11 -6
- package/src/ai-pi.browser.ts +5 -0
- package/src/ai-pi.ts +1263 -0
- package/src/ai-sdk-step.ts +2 -2
- package/src/ai-server.ts +3 -1
- package/src/internal.ts +7 -0
- package/src/libfx.d.ts +34 -0
- package/src/reducer.ts +1 -1
- package/src/server.ts +177 -19
- package/src/store-memory.ts +42 -7
- package/src/store-postgres.ts +80 -0
- package/src/store-redis-core.ts +210 -79
- package/src/store-redis-notify.ts +2 -0
- package/src/store-sqlite.ts +60 -0
- package/src/store.ts +33 -0
- package/src/telemetry.ts +1 -0
- package/dist/internal-CbVvEwad.js.map +0 -1
- package/dist/server-CGM8WzRG.d.ts.map +0 -1
- package/dist/server-Rj64fNvX.js.map +0 -1
- package/dist/session-clear-Dshpuhff.d.ts.map +0 -1
- package/dist/store-Gpkq7WBo.d.ts.map +0 -1
- package/dist/store-redis-notify-tqWNnMRu.js.map +0 -1
- package/dist/telemetry-cZH-cibj.d.ts.map +0 -1
- package/docs/concepts/01-contracts.mdx +0 -237
- package/docs/concepts/02-handlers.mdx +0 -284
- package/docs/concepts/03-durability.mdx +0 -314
- package/docs/concepts/04-state.mdx +0 -209
- package/docs/guides/01-timers.mdx +0 -193
- package/docs/guides/02-cancellation.mdx +0 -161
- package/docs/guides/03-react.mdx +0 -324
- package/docs/guides/04-local-first.mdx +0 -47
- package/docs/guides/05-production.mdx +0 -534
- package/docs/guides/06-ai-agents.mdx +0 -1408
- package/docs/guides/08-application-data.mdx +0 -113
- package/docs/guides/09-presence.mdx +0 -262
- package/docs/guides/10-transports.mdx +0 -221
- package/docs/reference/01-api.mdx +0 -2358
- package/docs/reference/02-errors.mdx +0 -150
- /package/docs/{concepts → contract}/meta.ts +0 -0
- /package/docs/{guides → server}/meta.ts +0 -0
|
@@ -1,2358 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: API
|
|
3
|
-
description: "The whole surface, small enough to read in one sitting: contract, server, session, reducer, and the React and HTTP helpers."
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
## `experimental-a2`
|
|
7
|
-
|
|
8
|
-
### `a2.contract(options)`
|
|
9
|
-
|
|
10
|
-
```ts
|
|
11
|
-
a2.contract(options: {
|
|
12
|
-
name: string
|
|
13
|
-
events: Record<string, StandardSchemaV1>
|
|
14
|
-
presence?: Record<string, StandardSchemaV1>
|
|
15
|
-
reducer?: { name: string; initialState: S; stateSchema?: StandardSchemaV1; fold: (state: S, event: Event) => S }
|
|
16
|
-
}): Contract
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
Defines one kind of lifecycle: a name plus the events it understands. The name
|
|
20
|
-
must be unique per app; it identifies the contract in storage and queue
|
|
21
|
-
messages. The name may not contain the control character U+001F, and event
|
|
22
|
-
types may not start with `a2.`, which A2 reserves for its own events, such as
|
|
23
|
-
`a2.cleared`. The result is a plain,
|
|
24
|
-
importable, **isomorphic** value: the server
|
|
25
|
-
implements it, reducers derive from it, the browser types its pushes off it.
|
|
26
|
-
See [Contracts and sessions](/concepts/contracts).
|
|
27
|
-
|
|
28
|
-
`presence` is the optional second vocabulary: each key a field of one
|
|
29
|
-
participant's ephemeral state, each value its schema. Presence fields are
|
|
30
|
-
replicated last-writer-wins and never stored in the log; reducers cannot
|
|
31
|
-
see them. See [Presence](/guides/presence).
|
|
32
|
-
|
|
33
|
-
Schemas are [Standard Schema](https://standardschema.dev): Zod, Valibot,
|
|
34
|
-
ArkType, anything that implements it. Validators must be synchronous
|
|
35
|
-
(async ones are rejected here, at definition time), and the validated
|
|
36
|
-
output is what's stored.
|
|
37
|
-
|
|
38
|
-
### `contract.batch(...events)`
|
|
39
|
-
|
|
40
|
-
Preserves the literal types in a heterogeneous handler-returned event array.
|
|
41
|
-
It returns the same events as a readonly tuple; A2 validates them when the
|
|
42
|
-
handler completes.
|
|
43
|
-
|
|
44
|
-
### `reducer(options)`
|
|
45
|
-
|
|
46
|
-
```ts
|
|
47
|
-
reducer(options: {
|
|
48
|
-
contract: Contract
|
|
49
|
-
name: string // identity; keys cached snapshots
|
|
50
|
-
initialState: S // the seed (schema input, when stateSchema is set)
|
|
51
|
-
stateSchema?: StandardSchemaV1 // declares S, checks initialState, guards cached folds
|
|
52
|
-
}): { fold(fn: (state: S, event: Event) => S): Reducer<S> }
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
Builds a separate reducer for server reads or for the contract's optional
|
|
56
|
-
`reducer` field. The contract publishes only that selected reducer to clients.
|
|
57
|
-
Inline definitions include `fold` directly. The standalone builder uses two steps: the first call
|
|
58
|
-
fixes the state and event types, so `.fold()` receives fully concrete
|
|
59
|
-
ones. Literal unions in your state survive, with zero annotations.
|
|
60
|
-
|
|
61
|
-
The name is the reducer's identity; it keys cached snapshots, so rename it
|
|
62
|
-
when the fold's logic changes. Two reducers never fight over a cache
|
|
63
|
-
entry, and a rename (or a `stateSchema` mismatch, when present)
|
|
64
|
-
invalidates, and the next read refolds from raw events. See
|
|
65
|
-
[Reading state](/concepts/state).
|
|
66
|
-
|
|
67
|
-
### `A2Error`
|
|
68
|
-
|
|
69
|
-
The shared base for A2 failures, discriminated by `code`. The session
|
|
70
|
-
client exposes its `A2ClientError` subclass. See
|
|
71
|
-
[Errors](/reference/errors).
|
|
72
|
-
|
|
73
|
-
### `idempotentId(...parts)`
|
|
74
|
-
|
|
75
|
-
`idempotentId(...parts: string[]): Promise<string>` derives a stable UUIDv8
|
|
76
|
-
from an ordered tuple of strings. It runs on the server and in the browser.
|
|
77
|
-
|
|
78
|
-
```ts shared/request-id.ts
|
|
79
|
-
import { idempotentId } from 'experimental-a2'
|
|
80
|
-
|
|
81
|
-
export const id = await idempotentId('orders', 'shop-7', 'request-42')
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
Pass the result as an event's `id` when retries should converge on the same
|
|
85
|
-
event. The same tuple produces the same ID across runtimes and A2 versions.
|
|
86
|
-
String boundaries, order, and case matter; strings are not normalized.
|
|
87
|
-
Empty strings and an empty tuple are valid inputs.
|
|
88
|
-
|
|
89
|
-
Every argument has the same role. A prefix such as `'orders'` is a namespace
|
|
90
|
-
by convention. Include enough context to distinguish operations across
|
|
91
|
-
contracts and sessions, since event IDs are globally unique within a store.
|
|
92
|
-
|
|
93
|
-
The helper performs no I/O. A2 preserves explicit IDs and keeps its automatic
|
|
94
|
-
ID generation unchanged. Changing an existing application's ID recipe changes
|
|
95
|
-
retry identity, so retries of previously committed requests need to retain
|
|
96
|
-
their original IDs.
|
|
97
|
-
|
|
98
|
-
### `SessionCleared`
|
|
99
|
-
|
|
100
|
-
The marker a [clear](#sessionclearoptions) leaves in `session.stream()`: an
|
|
101
|
-
`Event` whose `type` is `'a2.cleared'` and whose `payload` is `null`. It
|
|
102
|
-
never reaches handlers or `history()`. Comparing `event.type` with that string
|
|
103
|
-
narrows a stream item to `SessionCleared`. The session client handles the
|
|
104
|
-
marker on its own, so this matters only when you read the server stream
|
|
105
|
-
yourself.
|
|
106
|
-
|
|
107
|
-
## `experimental-a2/server`
|
|
108
|
-
|
|
109
|
-
### `handler(entry)`
|
|
110
|
-
|
|
111
|
-
Define handlers inline in `createServer({ handlers })` for ordinary use.
|
|
112
|
-
Use this helper when composing an existing handler.
|
|
113
|
-
|
|
114
|
-
Accepts a handler function or an object with `handler` and optional `lane` and
|
|
115
|
-
`abortOn`. Returns the object shape: a function becomes `{ handler: entry }`,
|
|
116
|
-
and an object is returned unchanged. The original function and object types
|
|
117
|
-
are preserved. Use it to compose an existing handler without branching on its
|
|
118
|
-
shape.
|
|
119
|
-
|
|
120
|
-
```ts server/composed-orders.ts
|
|
121
|
-
import * as a2 from 'experimental-a2'
|
|
122
|
-
import { createServer, handler, type HandlerContext } from 'experimental-a2/server'
|
|
123
|
-
import { z } from 'zod'
|
|
124
|
-
|
|
125
|
-
const orders = a2.contract({
|
|
126
|
-
name: 'composed-orders',
|
|
127
|
-
events: {
|
|
128
|
-
created: z.object({ shopId: z.string() }),
|
|
129
|
-
notified: z.object({ shopId: z.string() }),
|
|
130
|
-
},
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
const created = handler({
|
|
134
|
-
lane: 'orders',
|
|
135
|
-
handler: async (ctx: HandlerContext<typeof orders.events, 'created'>) => {
|
|
136
|
-
return orders.batch({ type: 'notified', payload: ctx.event.payload })
|
|
137
|
-
},
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
export const ordersServer = createServer({
|
|
141
|
-
contract: orders,
|
|
142
|
-
handlers: {
|
|
143
|
-
created: {
|
|
144
|
-
...created,
|
|
145
|
-
handler: async (ctx) => {
|
|
146
|
-
const result = await created.handler(ctx)
|
|
147
|
-
// Your additional work goes here:
|
|
148
|
-
// await notifyShop(ctx.event.payload, { idempotencyKey: ctx.event.id })
|
|
149
|
-
return result
|
|
150
|
-
},
|
|
151
|
-
},
|
|
152
|
-
},
|
|
153
|
-
})
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
Spreading the object preserves its `lane` and `abortOn`. Set either property
|
|
157
|
-
after the spread to override it. Changing a built-in AI handler's lane can
|
|
158
|
-
separate it from the handlers it coordinates with.
|
|
159
|
-
|
|
160
|
-
Composition runs in one handler attempt. Work after the original function
|
|
161
|
-
resolves still runs before its returned events commit. A thrown error fails
|
|
162
|
-
the composed attempt; on retry, the wrapper and original handler can run again.
|
|
163
|
-
For a new handler, annotate its context as above or pass an already typed
|
|
164
|
-
function. `handler` does not bind a handler to a contract;
|
|
165
|
-
`createServer({ handlers })` checks that it matches the contract.
|
|
166
|
-
|
|
167
|
-
### `createServer(options)`
|
|
168
|
-
|
|
169
|
-
```ts
|
|
170
|
-
createServer(options: {
|
|
171
|
-
contract: Contract // the vocabulary this server implements
|
|
172
|
-
store?: A2Store // default: sqlite in dev, memory in tests, required in prod
|
|
173
|
-
scheduler?: A2Scheduler
|
|
174
|
-
telemetry?: A2Telemetry // optional instrumentation; see experimental-a2/otel
|
|
175
|
-
presence?: { ttlMs?: number } // presence expiry policy; default 60s
|
|
176
|
-
handlers?: {
|
|
177
|
-
[type]:
|
|
178
|
-
| Handler
|
|
179
|
-
| {
|
|
180
|
-
abortOn?: AbortSpec
|
|
181
|
-
lane?: string | ((ctx: LaneContext) => string)
|
|
182
|
-
handler: Handler
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}): A2Server
|
|
186
|
-
|
|
187
|
-
type Handler = (
|
|
188
|
-
ctx: Context,
|
|
189
|
-
) => Promise<void | AppendInput | readonly AppendInput[]>
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
Implements a contract: binds the vocabulary to storage and reactions.
|
|
193
|
-
Handlers are one table, complete at construction, so a handler can never
|
|
194
|
-
be silently missing because the module that registered it wasn't
|
|
195
|
-
imported. Compose across files by spreading objects into `handlers`
|
|
196
|
-
(note: a duplicate key under spread silently last-wins).
|
|
197
|
-
|
|
198
|
-
Handlers are concurrent by default. A2 starts every eligible event after its
|
|
199
|
-
append commits. Add `lane` when events share a resource and must not overlap.
|
|
200
|
-
Within one session, events with the same lane value run one at a time in log
|
|
201
|
-
order. Different lanes and events without a lane run concurrently. A lane
|
|
202
|
-
resolver receives `{ sessionId, event }`; A2 resolves and stores the value when
|
|
203
|
-
the event is appended, so a later deployment cannot reinterpret pending work.
|
|
204
|
-
For example, `lane: ({ event }) => event.payload.warehouseId` serializes work
|
|
205
|
-
per warehouse while different warehouses continue concurrently.
|
|
206
|
-
|
|
207
|
-
Handlers can return one event or an array. A2 marks the triggering event
|
|
208
|
-
processed and appends the returned batch in one atomic store operation. Returned
|
|
209
|
-
events do not exist when the handler throws. `ctx.session.append(name, ...events)`
|
|
210
|
-
is different: it commits immediately, so its events may run while the current
|
|
211
|
-
handler is still active unless a lane orders them.
|
|
212
|
-
`ctx.session.schedule(name, timing, ...events)` waits for provider acceptance;
|
|
213
|
-
its events append later and are not part of the parent completion transaction.
|
|
214
|
-
|
|
215
|
-
Handlers are optional per event type. An event type without one settles in
|
|
216
|
-
the append transaction with no dispatch attempt. If the session has no older
|
|
217
|
-
pending handler work, A2 starts no drain or scheduler arm. A server with no
|
|
218
|
-
handlers is a durable event log with no reaction infrastructure. See
|
|
219
|
-
[Events without handlers](/concepts/handlers#events-without-handlers).
|
|
220
|
-
|
|
221
|
-
Server-only by construction: `experimental-a2/server` is the only entry point that
|
|
222
|
-
can reach a store backend, and its exports map resolves to a loud error
|
|
223
|
-
under the browser condition.
|
|
224
|
-
|
|
225
|
-
`presence.ttlMs` sets how long a presence value survives without a
|
|
226
|
-
refreshing set (default 60 seconds, as a positive integer of
|
|
227
|
-
milliseconds). Expiry counts on the storage's own clock, never on the
|
|
228
|
-
sender's stamp. The option is valid only on contracts that declare
|
|
229
|
-
`presence`; anywhere else construction throws a `TypeError`.
|
|
230
|
-
|
|
231
|
-
`abortOn` names the events that fire `ctx.signal` while a handler runs.
|
|
232
|
-
an array matches by type; an object takes per-type predicates for
|
|
233
|
-
targeted cancellation:
|
|
234
|
-
|
|
235
|
-
```ts
|
|
236
|
-
generate: {
|
|
237
|
-
abortOn: {
|
|
238
|
-
cancelled: (event, trigger, { attempt }) =>
|
|
239
|
-
event.payload.of === `${trigger.id}:${attempt}`,
|
|
240
|
-
},
|
|
241
|
-
handler: async (ctx) => { /* ... */ },
|
|
242
|
-
}
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
The predicate's third argument contains the triggering event's durable
|
|
246
|
-
`attempt`, so a cancellation can fence a specific recovered run.
|
|
247
|
-
|
|
248
|
-
The context every handler receives:
|
|
249
|
-
|
|
250
|
-
| Property | Type |
|
|
251
|
-
| ------------- | --------------------------------------------------------- |
|
|
252
|
-
| `ctx.event` | `Event`: the triggering event |
|
|
253
|
-
| `ctx.attempt` | durable 1-based dispatch claim ordinal |
|
|
254
|
-
| `ctx.session` | this session's `id`, `append`, `schedule`, `history`, `state`, `stream`, `setPresence` |
|
|
255
|
-
| `ctx.signal` | `AbortSignal`: fires on lease lapse (`CLAIM_EXPIRED`), proven supersession (`SUPERSEDED_ATTEMPT`), and matching `abortOn` events |
|
|
256
|
-
|
|
257
|
-
`ctx.attempt` starts at `1` and increments on every durable claim. It may skip
|
|
258
|
-
when a process dies before handler entry.
|
|
259
|
-
|
|
260
|
-
`ctx.session.id` equals `ctx.event.sessionId`. Its `history`, `state`, and
|
|
261
|
-
`stream` methods share the session surface returned by `server.session(id)`,
|
|
262
|
-
with one causal default: `ctx.session.state(reducer)` folds through the
|
|
263
|
-
triggering event's index. The same event sees the same state across concurrent
|
|
264
|
-
appends and retries. Ask for `{ through: 'latest' }` when a handler intentionally
|
|
265
|
-
needs the committed frontier captured when that read runs.
|
|
266
|
-
|
|
267
|
-
The handler-local append is specialized:
|
|
268
|
-
|
|
269
|
-
```ts
|
|
270
|
-
ctx.session.append(name: string, ...events: AppendInput[]): Promise<Event[]>
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
The name is an idempotency key scoped to the triggering event for events whose
|
|
274
|
-
`id` is omitted. A2 derives each omitted id from the trigger id, name, and event
|
|
275
|
-
position. The same name and generated-id batch returns the existing rows
|
|
276
|
-
across retries. Changing that batch conflicts with those rows. An explicit
|
|
277
|
-
event `id` wins over the generated id, which lets different triggering events
|
|
278
|
-
converge on one fact. A root `server.session(id).append(...events)` takes no
|
|
279
|
-
name.
|
|
280
|
-
|
|
281
|
-
`ctx.session.schedule(name, timing, ...events)` includes the triggering event
|
|
282
|
-
in its stable task identity. A relative delay is anchored to that event's
|
|
283
|
-
durable `createdAt`, so a handler retry keeps the same delivery time. The call
|
|
284
|
-
returns `Promise<void>` after the configured scheduler accepts the task.
|
|
285
|
-
|
|
286
|
-
A latest state read and following append are not atomic. Concurrent appends and
|
|
287
|
-
retries may move the frontier between them. A generic join should read with
|
|
288
|
-
`{ through: 'latest' }`, use a monotone readiness predicate, and give its output
|
|
289
|
-
a stable explicit event `id`, so every eligible attempt converges on the same
|
|
290
|
-
append.
|
|
291
|
-
|
|
292
|
-
### `server.fetch(request, options?)`
|
|
293
|
-
|
|
294
|
-
```ts
|
|
295
|
-
server.fetch(
|
|
296
|
-
request: Request,
|
|
297
|
-
options?: {
|
|
298
|
-
authorize?: (operation: A2Operation) => boolean | Promise<boolean>
|
|
299
|
-
upgradeWebSocket?: (
|
|
300
|
-
attach: (socket: A2Socket) => void,
|
|
301
|
-
) => Response | Promise<Response>
|
|
302
|
-
multiplexWebSocket?: boolean
|
|
303
|
-
},
|
|
304
|
-
): Promise<Response>
|
|
305
|
-
|
|
306
|
-
type A2Operation =
|
|
307
|
-
| {
|
|
308
|
-
type: 'stream'
|
|
309
|
-
sessionId: string
|
|
310
|
-
startAfter: number
|
|
311
|
-
transport: 'http' | 'websocket'
|
|
312
|
-
}
|
|
313
|
-
| {
|
|
314
|
-
type: 'history'
|
|
315
|
-
sessionId: string
|
|
316
|
-
gte: number
|
|
317
|
-
lte: number
|
|
318
|
-
transport: 'http'
|
|
319
|
-
}
|
|
320
|
-
| {
|
|
321
|
-
type: 'push'
|
|
322
|
-
sessionId: string
|
|
323
|
-
events: readonly {
|
|
324
|
-
type: ContractEventType | string
|
|
325
|
-
payload: unknown
|
|
326
|
-
id?: string
|
|
327
|
-
}[]
|
|
328
|
-
presence?: {
|
|
329
|
-
participant: string
|
|
330
|
-
values: Readonly<Record<string, unknown>>
|
|
331
|
-
seen?: number
|
|
332
|
-
at?: number
|
|
333
|
-
}
|
|
334
|
-
transport: 'http' | 'websocket'
|
|
335
|
-
}
|
|
336
|
-
```
|
|
337
|
-
|
|
338
|
-
The bound Fetch API route for this server. An unprotected route can export it
|
|
339
|
-
directly:
|
|
340
|
-
|
|
341
|
-
```ts app/api/public-order-events/route.ts
|
|
342
|
-
import { ordersServer } from '@/server/orders'
|
|
343
|
-
|
|
344
|
-
export const GET = ordersServer.fetch
|
|
345
|
-
export const POST = ordersServer.fetch
|
|
346
|
-
```
|
|
347
|
-
|
|
348
|
-
A plain GET streams one session over SSE, resumed after the `index` query
|
|
349
|
-
parameter. A GET with `gte` and `lte` returns the closed history range as JSON
|
|
350
|
-
and sets `a2-history-covered` to `true` when every requested index was returned.
|
|
351
|
-
When the range includes the latest clear marker, it also sets `a2-clear-index`,
|
|
352
|
-
and coverage counts only the requested indexes after that marker. A range
|
|
353
|
-
entirely below the marker returns `[]`.
|
|
354
|
-
POST accepts `{ sessionId, events, presence? }` and returns the appended events.
|
|
355
|
-
Presence is inferred from the contract. A contract that declares presence
|
|
356
|
-
streams it and accepts presence pushes automatically.
|
|
357
|
-
|
|
358
|
-
`authorize` runs after the request or socket frame has been parsed, but before
|
|
359
|
-
contract schema validation, presence validation, or I/O. Event names and
|
|
360
|
-
presence field names are typed from the contract. Their unvalidated values stay
|
|
361
|
-
`unknown`. Return `false` to deny the operation with `FORBIDDEN`. A thrown error
|
|
362
|
-
becomes `STORE_UNAVAILABLE`, because authorization infrastructure failed rather
|
|
363
|
-
than denied access. Operation objects, push events, event arrays, and presence
|
|
364
|
-
patches are frozen. Authorize them, do not rewrite them.
|
|
365
|
-
|
|
366
|
-
Authentication stays outside A2, where the route can read headers, cookies, and
|
|
367
|
-
framework context. Capture the authenticated principal in `authorize`:
|
|
368
|
-
|
|
369
|
-
```ts app/api/protected-order-events/route.ts
|
|
370
|
-
import { ordersServer } from '@/server/orders'
|
|
371
|
-
|
|
372
|
-
async function fetchOrders(request: Request): Promise<Response> {
|
|
373
|
-
// your authentication:
|
|
374
|
-
const user = { id: 'user-1' }
|
|
375
|
-
if (!user) return new Response(null, { status: 401 })
|
|
376
|
-
|
|
377
|
-
return ordersServer.fetch(request, {
|
|
378
|
-
authorize(operation) {
|
|
379
|
-
// your per-session and per-operation authorization:
|
|
380
|
-
// return canAccess(user, operation)
|
|
381
|
-
return true
|
|
382
|
-
},
|
|
383
|
-
})
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
export const GET = fetchOrders
|
|
387
|
-
export const POST = fetchOrders
|
|
388
|
-
```
|
|
389
|
-
|
|
390
|
-
Direct `session.append`, handler appends, scheduled appends, and server-side
|
|
391
|
-
`setPresence` bypass `authorize`. It is an ingress policy for this fetch call,
|
|
392
|
-
not a global server policy.
|
|
393
|
-
|
|
394
|
-
A GET with `Upgrade: websocket` uses `upgradeWebSocket`. Without that option it
|
|
395
|
-
returns 426. The upgrade function receives an `attach` callback for the
|
|
396
|
-
platform socket:
|
|
397
|
-
|
|
398
|
-
```ts app/api/socket-order-events/route.ts
|
|
399
|
-
import { experimental_upgradeWebSocket } from '@vercel/functions'
|
|
400
|
-
import { ordersServer } from '@/server/orders'
|
|
401
|
-
|
|
402
|
-
export const POST = ordersServer.fetch
|
|
403
|
-
|
|
404
|
-
export function GET(request: Request): Promise<Response> {
|
|
405
|
-
return ordersServer.fetch(request, {
|
|
406
|
-
upgradeWebSocket: (attach) =>
|
|
407
|
-
experimental_upgradeWebSocket(attach, {
|
|
408
|
-
maxPayload: 4 * 1024 * 1024,
|
|
409
|
-
}),
|
|
410
|
-
})
|
|
411
|
-
}
|
|
412
|
-
```
|
|
413
|
-
|
|
414
|
-
By default, the `sessionId` and `index` query parameters bind one session to the
|
|
415
|
-
socket. A2 authorizes the stream before upgrading. Frames on that socket cannot
|
|
416
|
-
select another session.
|
|
417
|
-
|
|
418
|
-
Set `multiplexWebSocket: true` here and `multiplex: true` in the client's `ws`
|
|
419
|
-
API to share one socket.
|
|
420
|
-
Authenticate the physical WebSocket GET before calling `server.fetch`. Validate
|
|
421
|
-
`Origin` too when browser cookies authenticate it; WebSocket handshakes do not
|
|
422
|
-
use a CORS preflight. Use `authorize` for each session subscribe and each event
|
|
423
|
-
or presence push. Only enable multiplexing when that request context may access
|
|
424
|
-
every session accepted by `authorize`. A denied subscribe receives
|
|
425
|
-
`unsubscribed` and does not affect other sessions. A denied event push receives
|
|
426
|
-
a `FORBIDDEN` ack. Denied presence is silently dropped because it is
|
|
427
|
-
fire-and-forget.
|
|
428
|
-
|
|
429
|
-
SSE and WebSocket lifetimes use the platform's ambient invocation deadline.
|
|
430
|
-
A2 also uses the platform's ambient `waitUntil` capability for background work.
|
|
431
|
-
Neither capability is reordered or passed through `server.fetch` options.
|
|
432
|
-
|
|
433
|
-
### `server.session(id)`
|
|
434
|
-
|
|
435
|
-
```ts
|
|
436
|
-
server.session(id: string): Session
|
|
437
|
-
```
|
|
438
|
-
|
|
439
|
-
A handle on one instance of the contract. The session is the unit of log
|
|
440
|
-
ordering, lane keys, recovery, state, and live sync. Handler execution is
|
|
441
|
-
concurrent unless events share a lane. Creating the handle does no I/O;
|
|
442
|
-
nothing loads until you append, read, or stream.
|
|
443
|
-
|
|
444
|
-
```ts
|
|
445
|
-
const session = server.session('order-42')
|
|
446
|
-
|
|
447
|
-
session.id
|
|
448
|
-
session.append(...events)
|
|
449
|
-
session.schedule('expire-order', { delay: '5d' }, {
|
|
450
|
-
type: 'expired',
|
|
451
|
-
payload: {},
|
|
452
|
-
})
|
|
453
|
-
session.history({ gte, lte })
|
|
454
|
-
session.state(reducer)
|
|
455
|
-
session.stream({ startAfter })
|
|
456
|
-
```
|
|
457
|
-
|
|
458
|
-
`session.id` is the id passed to `server.session(id)`. Root `append` takes only
|
|
459
|
-
events. The handler-local form at `ctx.session.append` adds its required name
|
|
460
|
-
before the events. Both root and handler sessions expose `schedule`.
|
|
461
|
-
|
|
462
|
-
### `server.drain(sessionId)`
|
|
463
|
-
|
|
464
|
-
```ts
|
|
465
|
-
server.drain(sessionId: string): Promise<{ settled: boolean }>
|
|
466
|
-
```
|
|
467
|
-
|
|
468
|
-
Claims every currently eligible event and runs their handlers concurrently.
|
|
469
|
-
`settled` means no actionable or live-claimed work remains. A dead-lettered
|
|
470
|
-
event can leave later work in its lane blocked while other lanes continue.
|
|
471
|
-
You'll rarely call this yourself; it is the primitive scheduler callbacks use.
|
|
472
|
-
The public result stays this simple boolean.
|
|
473
|
-
|
|
474
|
-
### `A2Store`
|
|
475
|
-
|
|
476
|
-
Custom adapters implement these atomic drain methods:
|
|
477
|
-
|
|
478
|
-
```ts
|
|
479
|
-
// Custom server store adapter:
|
|
480
|
-
type EventCause = {
|
|
481
|
-
index: number
|
|
482
|
-
attempt: number
|
|
483
|
-
batchSize?: number // present on named handler appends
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
type AppendEvent = {
|
|
487
|
-
type: string
|
|
488
|
-
payload: unknown
|
|
489
|
-
id?: string
|
|
490
|
-
cause?: EventCause
|
|
491
|
-
lane?: string
|
|
492
|
-
settled?: true // internal: no handler is registered for this event type
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
type ReturnedEvent = AppendEvent & { id: string }
|
|
496
|
-
|
|
497
|
-
type StoredEvent = Event & {
|
|
498
|
-
cause: EventCause | null
|
|
499
|
-
lane: string | null
|
|
500
|
-
processedAt: Date | null
|
|
501
|
-
processedByAttempt: number | null
|
|
502
|
-
returnedEventIds: string[] | null
|
|
503
|
-
firstClaimedAt: Date | null
|
|
504
|
-
lastClaimedAt: Date | null
|
|
505
|
-
attemptCount: number
|
|
506
|
-
claimHolder: string | null
|
|
507
|
-
claimExpiresAt: Date | null
|
|
508
|
-
failureCount: number
|
|
509
|
-
lastFailedAt: Date | null
|
|
510
|
-
lastFailedAttempt: number | null
|
|
511
|
-
lastError: string | null
|
|
512
|
-
failedAt: Date | null
|
|
513
|
-
clearIndex?: number // on claimed events, once the session has been cleared
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
type StoreAppendResult = {
|
|
517
|
-
events: StoredEvent[]
|
|
518
|
-
hasPending: boolean
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
type StoreClaimAvailableResult =
|
|
522
|
-
| { outcome: 'claimed'; events: StoredEvent[] }
|
|
523
|
-
| { outcome: 'busy'; retryAt: Date }
|
|
524
|
-
| { outcome: 'settled' }
|
|
525
|
-
|
|
526
|
-
type CompleteAttemptResult =
|
|
527
|
-
| { outcome: 'completed'; events: StoredEvent[] }
|
|
528
|
-
| { outcome: 'superseded' }
|
|
529
|
-
|
|
530
|
-
interface A2Store {
|
|
531
|
-
append(
|
|
532
|
-
sessionId: string,
|
|
533
|
-
events: AppendEvent[],
|
|
534
|
-
options?: { originIndex?: number },
|
|
535
|
-
): Promise<StoreAppendResult>
|
|
536
|
-
|
|
537
|
-
// optional together; required for session.clear()
|
|
538
|
-
clear?(options: {
|
|
539
|
-
sessionId: string
|
|
540
|
-
ifLastIndex?: number
|
|
541
|
-
cause?: EventCause
|
|
542
|
-
}): Promise<{ cleared: boolean; index: number; clearIndex: number }>
|
|
543
|
-
reclaim?(sessionId: string): Promise<{ deleted: number; done: boolean }>
|
|
544
|
-
|
|
545
|
-
claimAvailable(options: {
|
|
546
|
-
sessionId: string
|
|
547
|
-
holder: string
|
|
548
|
-
ttlMs: number
|
|
549
|
-
expiresAtMs?: number
|
|
550
|
-
excludeIndexes?: readonly number[]
|
|
551
|
-
}): Promise<StoreClaimAvailableResult>
|
|
552
|
-
|
|
553
|
-
renewClaims(options: {
|
|
554
|
-
sessionId: string
|
|
555
|
-
holder: string
|
|
556
|
-
claims: readonly { index: number; attempt: number }[]
|
|
557
|
-
ttlMs: number
|
|
558
|
-
expiresAtMs?: number
|
|
559
|
-
}): Promise<{ renewed: number[]; superseded: number[] }>
|
|
560
|
-
|
|
561
|
-
completeAttempt(options: {
|
|
562
|
-
sessionId: string
|
|
563
|
-
index: number
|
|
564
|
-
attempt: number
|
|
565
|
-
events: ReturnedEvent[]
|
|
566
|
-
}): Promise<CompleteAttemptResult>
|
|
567
|
-
|
|
568
|
-
failAttempt(options: {
|
|
569
|
-
sessionId: string
|
|
570
|
-
index: number
|
|
571
|
-
attempt: number
|
|
572
|
-
error: string
|
|
573
|
-
maxFailures: number
|
|
574
|
-
}): Promise<{
|
|
575
|
-
outcome: 'failed' | 'dead_lettered' | 'superseded'
|
|
576
|
-
failureCount: number
|
|
577
|
-
}>
|
|
578
|
-
}
|
|
579
|
-
```
|
|
580
|
-
|
|
581
|
-
| Method | Atomic effect |
|
|
582
|
-
| --- | --- |
|
|
583
|
-
| `append` | Write a consecutive batch. Events carrying core's internal `settled` flag receive `processedAt` in the same transaction, with no dispatch attempt. An event carrying `cause` is fenced by attempt currency: it commits only while `cause.attempt` is still the parent's latest attempt and the parent is not dead-lettered, throwing `SUPERSEDED_ATTEMPT` otherwise (the check serializes against a concurrent claim; the idempotent-replay path runs first). |
|
|
584
|
-
| `claimAvailable` | Claim every eligible event. All unlaned events are independent; only the lowest-index unfinished event in each lane is eligible. A claim records its holder, expiry, timestamps, and next `attemptCount`. |
|
|
585
|
-
| `renewClaims` | Extend the listed live claims still owned by the holder. `renewed` lists the claims that remain owned; `superseded` lists the claims whose event's `attemptCount` passed the listed attempt. An expired claim no successor took appears in neither: it may still complete. Renewal never revives an expired claim. |
|
|
586
|
-
| `completeAttempt` | Fence on the current attempt, mark the parent processed, store its exact ordered `returnedEventIds`, and append the returned batch in the same transaction. A same-attempt retry returns the committed children; a stale attempt returns `superseded`. |
|
|
587
|
-
| `failAttempt` | Record one current caught failure, clear its claim, and dead-letter at `maxFailures`. A repeated failure acknowledgment is idempotent; stale attempts return `superseded`. |
|
|
588
|
-
| `clear` | Serialized with `append`. Check `ifLastIndex` against the head and the latest marker, and append a settled clear marker at the next index, touching no existing rows. A `cause` is fenced like a handler append. |
|
|
589
|
-
| `reclaim` | Delete one bounded batch of rows below the latest clear marker, with their bookkeeping, event-id records, lane pointers, and cached state. Report how many rows went and `done` when nothing is left. Never wait on a lock that other methods hold. |
|
|
590
|
-
|
|
591
|
-
A store that supports clearing treats every event at or below the latest clear
|
|
592
|
-
marker as superseded, whether or not its row still exists. `claimAvailable`
|
|
593
|
-
never claims it, lane readiness at append and at completion ignores it (a
|
|
594
|
-
dead-lettered lane head stops blocking), `renewClaims` reports it in
|
|
595
|
-
`superseded`, `completeAttempt` and `failAttempt` return `superseded`, and an
|
|
596
|
-
`append` whose `cause` names it throws `SUPERSEDED_ATTEMPT`. An `append` whose
|
|
597
|
-
`originIndex` is at or below the marker throws `SESSION_CLEARED`. Reads start
|
|
598
|
-
at the marker, `readState` returns its index as `clearIndex`, and claimed
|
|
599
|
-
events carry it as `clearIndex`. In `append` and `completeAttempt`, an event id
|
|
600
|
-
whose row is below this session's marker is free to reuse. Each check belongs
|
|
601
|
-
inside the operation's existing atomic step. Without `clear` and `reclaim`,
|
|
602
|
-
`session.clear()` and `session.schedule.clear()` throw a `TypeError` before
|
|
603
|
-
writing anything.
|
|
604
|
-
|
|
605
|
-
`attemptCount` counts claims, including abandoned ones. `failureCount` counts
|
|
606
|
-
caught failures and dead-letters at ten. See
|
|
607
|
-
[Durability](/concepts/durability#one-event-many-attempts) for the recovery
|
|
608
|
-
model. `cause` identifies the event and handler attempt whose
|
|
609
|
-
`ctx.session.append` or return value first persisted the child. A null cause
|
|
610
|
-
means a top-level event.
|
|
611
|
-
`lane` is the session-scoped serialized group resolved before append.
|
|
612
|
-
`returnedEventIds` makes a lost completion
|
|
613
|
-
acknowledgment recoverable without accepting a partial child batch. Lifecycle
|
|
614
|
-
timestamps are adapter clock values for their atomic store operations, not exact
|
|
615
|
-
database commit times.
|
|
616
|
-
Built-in adapters persist these fields inside their existing atomic operations,
|
|
617
|
-
with no extra backend round trip.
|
|
618
|
-
|
|
619
|
-
### `A2Scheduler`
|
|
620
|
-
|
|
621
|
-
Custom scheduler adapters implement two methods:
|
|
622
|
-
|
|
623
|
-
| Method | Effect |
|
|
624
|
-
| --- | --- |
|
|
625
|
-
| `schedule(task: SchedulerTask): Promise<void>` | put one versioned task on durable delivery infrastructure |
|
|
626
|
-
| `handler(...servers)` | return the route that receives drain, append, and clear tasks |
|
|
627
|
-
|
|
628
|
-
Application routes call `scheduler.handler(...servers)` on the configured
|
|
629
|
-
adapter. The handler requires at least one drainable server and rejects
|
|
630
|
-
duplicate contract names synchronously.
|
|
631
|
-
|
|
632
|
-
`SchedulerTask` is `SchedulerDrainTask | SchedulerAppendTask |
|
|
633
|
-
SchedulerClearTask`. These task types, plus `ScheduledEvent`, are exported
|
|
634
|
-
from `experimental-a2/server`.
|
|
635
|
-
|
|
636
|
-
| Task | Fields |
|
|
637
|
-
| --- | --- |
|
|
638
|
-
| `SchedulerDrainTask` | `version: 1`, `kind: 'drain'`, `contract`, `sessionId`, required epoch-ms `dueAt` |
|
|
639
|
-
| `SchedulerAppendTask` | `version: 1`, `kind: 'append'`, stable `id`, `contract`, `sessionId`, required epoch-ms `dueAt`, readonly `events`, optional `originIndex` |
|
|
640
|
-
| `SchedulerClearTask` | `version: 1`, `kind: 'clear'`, stable `id`, `contract`, `sessionId`, required epoch-ms `dueAt`, `ifLastIndex` |
|
|
641
|
-
| `ScheduledEvent` | fixed `id`, `type`, and JSON `payload` |
|
|
642
|
-
|
|
643
|
-
Drain tasks wake pending work and carry no application payload. Append tasks
|
|
644
|
-
carry the events for `session.schedule`; `originIndex` is the scheduling
|
|
645
|
-
handler's event, absent for root schedules. Clear tasks carry the condition
|
|
646
|
-
for `session.schedule.clear`.
|
|
647
|
-
|
|
648
|
-
Custom adapters deliver an authenticated, normalized append or clear task
|
|
649
|
-
through the server helpers:
|
|
650
|
-
|
|
651
|
-
```ts custom-scheduler.ts
|
|
652
|
-
import {
|
|
653
|
-
deliverSchedulerAppend,
|
|
654
|
-
deliverSchedulerClear,
|
|
655
|
-
type DrainableServer,
|
|
656
|
-
type SchedulerAppendTask,
|
|
657
|
-
type SchedulerClearTask,
|
|
658
|
-
} from 'experimental-a2/server'
|
|
659
|
-
|
|
660
|
-
export async function deliver(
|
|
661
|
-
server: DrainableServer,
|
|
662
|
-
task: SchedulerAppendTask | SchedulerClearTask,
|
|
663
|
-
) {
|
|
664
|
-
if (task.kind === 'clear') await deliverSchedulerClear(server, task)
|
|
665
|
-
else await deliverSchedulerAppend(server, task)
|
|
666
|
-
}
|
|
667
|
-
```
|
|
668
|
-
|
|
669
|
-
Import both from `experimental-a2/server`. Each verifies the task and server
|
|
670
|
-
contract match. `deliverSchedulerAppend` revalidates the carried event inputs
|
|
671
|
-
and uses the ordinary top-level append path. The target server must have a
|
|
672
|
-
scheduler. When the append leaves pending handler work, the helper resolves
|
|
673
|
-
only after a recovery watchdog is durable; a send failure keeps the provider
|
|
674
|
-
delivery retryable. A task whose `originIndex` was cleared resolves without
|
|
675
|
-
appending.
|
|
676
|
-
|
|
677
|
-
`deliverSchedulerClear` runs a conditional `clear({ ifLastIndex })` and
|
|
678
|
-
resolves after its deletion finishes. A store failure rejects, so the provider
|
|
679
|
-
redelivers and the retry finishes deleting. Authenticate the transport and
|
|
680
|
-
validate the task envelope before calling either helper.
|
|
681
|
-
|
|
682
|
-
## Session
|
|
683
|
-
|
|
684
|
-
### `session.append(...events)`
|
|
685
|
-
|
|
686
|
-
```ts
|
|
687
|
-
session.append(
|
|
688
|
-
...events: Array<{ type: string; payload: unknown; id?: string }>
|
|
689
|
-
): Promise<Event[]>
|
|
690
|
-
```
|
|
691
|
-
|
|
692
|
-
The only way to move a session forward. Payloads are validated against
|
|
693
|
-
the contract's schemas before anything is written. A multi-event append
|
|
694
|
-
is atomic: all-or-nothing, consecutive positions, one transaction. Pass
|
|
695
|
-
`id` to make an append idempotent across retries; re-sending an
|
|
696
|
-
identical batch returns the original rows. (Events that arrived through
|
|
697
|
-
`server.fetch`'s push lane are accepted directly.) See
|
|
698
|
-
[Durability](/concepts/durability).
|
|
699
|
-
|
|
700
|
-
After the write, A2 dispatches only event types with registered handlers.
|
|
701
|
-
Other event types are already settled by the append itself. An unhandled
|
|
702
|
-
append still starts session healing when older handled work is pending. The
|
|
703
|
-
store reports that session-wide pending state as part of the atomic append, so
|
|
704
|
-
this decision needs no follow-up read.
|
|
705
|
-
|
|
706
|
-
### `session.append.dispatch(...events)`
|
|
707
|
-
|
|
708
|
-
```ts
|
|
709
|
-
session.append.dispatch(
|
|
710
|
-
...events: Array<{ type: string; payload: unknown; id?: string }>
|
|
711
|
-
): Promise<Event[]>
|
|
712
|
-
```
|
|
713
|
-
|
|
714
|
-
Commits the same atomic batch, but sends pending work directly to configured
|
|
715
|
-
scheduler instead of starting an inline drain. The call awaits acceptance of an
|
|
716
|
-
immediate scheduler message. It throws before writing if the server has no
|
|
717
|
-
scheduler adapter.
|
|
718
|
-
|
|
719
|
-
If the queue send fails, the events are already durable. Give them explicit
|
|
720
|
-
IDs and retry the same dispatch safely. A batch that leaves no pending handler
|
|
721
|
-
work does not send a scheduler message. Dispatch chooses how this append wakes
|
|
722
|
-
the session; it does not reserve events for one worker. A drain that is already
|
|
723
|
-
active may still claim newly eligible work first.
|
|
724
|
-
|
|
725
|
-
`dispatch` exists only on a server session's top-level append. Handler-scoped
|
|
726
|
-
`ctx.session.append(name, ...events)` already runs inside an active drain and
|
|
727
|
-
does not expose `dispatch`. Its immediate children and atomically returned
|
|
728
|
-
children become eligible in that drain.
|
|
729
|
-
|
|
730
|
-
### `session.schedule(name, timing, ...events)`
|
|
731
|
-
|
|
732
|
-
`schedule` returns `Promise<void>` and is available on both root and handler
|
|
733
|
-
sessions. `name` must be non-empty. At least one event is required.
|
|
734
|
-
`ScheduleDelay`, `ScheduleTiming`, and `SessionSchedule` are exported from
|
|
735
|
-
`experimental-a2/server`.
|
|
736
|
-
|
|
737
|
-
| Timing | Meaning |
|
|
738
|
-
| --- | --- |
|
|
739
|
-
| `{ delay: '30s' }` | relative delay with unit `ms`, `s`, `m`, `h`, or `d` |
|
|
740
|
-
| `{ at: Date }` | absolute delivery time |
|
|
741
|
-
|
|
742
|
-
The template type checks the unit suffix. At runtime, the delay number must be
|
|
743
|
-
an unsigned base-10 decimal without leading zeros, finite, and greater than
|
|
744
|
-
zero. `0s`, signs, exponent notation, malformed durations, invalid dates, and
|
|
745
|
-
objects containing both timing fields are rejected. A root relative delay
|
|
746
|
-
starts from that call's wall clock. A handler relative delay starts from the
|
|
747
|
-
triggering event's durable `createdAt`, so retries preserve its delivery time.
|
|
748
|
-
Use `{ at }` when a retried root call needs one target time. The requested
|
|
749
|
-
target is the earliest useful delivery time. Adapters encode provider timing
|
|
750
|
-
in whole-second slots. An already-due target is sent immediately.
|
|
751
|
-
|
|
752
|
-
The stable task id derives from the contract, session, and name. Handler tasks
|
|
753
|
-
also include the triggering event id. Omitted event ids derive from that task
|
|
754
|
-
id and event position; explicit ids win. Reuse a name only for an identical
|
|
755
|
-
timing and event batch. Provider deduplication is an optimization. Stable event
|
|
756
|
-
ids make later duplicate deliveries converge in the store.
|
|
757
|
-
|
|
758
|
-
A2 snapshots payloads into plain JSON trees, then validates the event schemas
|
|
759
|
-
before sending. Encode dates as ISO strings and custom classes as plain data;
|
|
760
|
-
values such as `undefined`, bigint, functions, symbols, cycles, accessors, and
|
|
761
|
-
non-plain prototypes are rejected. The promise resolves after provider
|
|
762
|
-
acceptance, not after append. Shared object references become independent JSON
|
|
763
|
-
values. The task carries the snapshotted JSON schema input. At delivery, the
|
|
764
|
-
ordinary top-level append validates that input, so a schema
|
|
765
|
-
transform produces the stored payload from the original input. The events have
|
|
766
|
-
`cause: null` and enter normal handlers, lanes, state, and streams.
|
|
767
|
-
|
|
768
|
-
A task scheduled from a handler belongs to its triggering event's lifetime. If
|
|
769
|
-
the session is cleared before delivery, the task is acknowledged without
|
|
770
|
-
appending. Once a session has been cleared, handler task ids also include the
|
|
771
|
-
clear, so a trigger id reused in the new lifetime schedules a new task. A root
|
|
772
|
-
task has no triggering event, so its events join whichever lifetime exists when
|
|
773
|
-
it is delivered, and its name identifies one task across lifetimes. Put
|
|
774
|
-
something from the new lifetime in the name when you schedule again after a
|
|
775
|
-
clear.
|
|
776
|
-
|
|
777
|
-
There is no cancellation, reschedule, cron, or timer-listing API. See
|
|
778
|
-
[Timers and delays](/guides/timers) for stale-timer guards, provider limits,
|
|
779
|
-
and deployment compatibility.
|
|
780
|
-
|
|
781
|
-
### `session.clear(options?)`
|
|
782
|
-
|
|
783
|
-
```ts
|
|
784
|
-
session.clear(options?: { ifLastIndex?: number }): Promise<{
|
|
785
|
-
cleared: boolean
|
|
786
|
-
index: number
|
|
787
|
-
}>
|
|
788
|
-
```
|
|
789
|
-
|
|
790
|
-
End the session's current lifetime. One atomic step checks the condition and
|
|
791
|
-
appends a reserved clear marker at the next index. The session id stays usable
|
|
792
|
-
and indexes never restart. `ifLastIndex` is a non-negative safe integer; the
|
|
793
|
-
first matching rule decides:
|
|
794
|
-
|
|
795
|
-
| When | Result |
|
|
796
|
-
| --- | --- |
|
|
797
|
-
| the latest marker is at `ifLastIndex + 1` | `{ cleared: true, index }`, a retry of that clear |
|
|
798
|
-
| `ifLastIndex` is given and is not the head | `{ cleared: false, index }`, nothing changes |
|
|
799
|
-
| the head is `0` or already a clear marker | `{ cleared: true, index }`, nothing changes |
|
|
800
|
-
| otherwise | `{ cleared: true, index }`, the new marker |
|
|
801
|
-
|
|
802
|
-
`index` is always the head after the call. Every event at or below the marker
|
|
803
|
-
is superseded: it is never dispatched again and never blocks its lane, its
|
|
804
|
-
running handler's appends throw `SUPERSEDED_ATTEMPT`, and that handler's state
|
|
805
|
-
reads never fold the new lifetime: they throw `SESSION_CLEARED`, except that
|
|
806
|
-
after a clear from another instance, a read bounded at its own event may return
|
|
807
|
-
that event's state from memory until the handler's `ctx.signal` aborts. Pushes and appends after the marker start the
|
|
808
|
-
new lifetime.
|
|
809
|
-
|
|
810
|
-
`clear()` then deletes the old events, their bookkeeping and event-id records,
|
|
811
|
-
and cached state below the marker in short batches, and resolves when they are
|
|
812
|
-
gone. Other work on the session keeps running between batches. If deletion
|
|
813
|
-
fails, the call rejects with the session already cleared; any later clear of
|
|
814
|
-
the session, including a retry, finishes the deletion. There is no background
|
|
815
|
-
sweep. Event ids from the old lifetime can be used again. Presence is not
|
|
816
|
-
cleared.
|
|
817
|
-
|
|
818
|
-
On `ctx.session`, the clear is fenced like the handler's appends and ends the
|
|
819
|
-
handler's own attempt: `ctx.signal` aborts with `SUPERSEDED_ATTEMPT` and
|
|
820
|
-
returned events are discarded. Its deletion keeps running under the platform's
|
|
821
|
-
`waitUntil`. All built-in stores support clearing. See
|
|
822
|
-
[Clearing a session](/concepts/contracts#clearing-a-session).
|
|
823
|
-
|
|
824
|
-
### `session.schedule.clear(name, options)`
|
|
825
|
-
|
|
826
|
-
```ts
|
|
827
|
-
session.schedule.clear(
|
|
828
|
-
name: string,
|
|
829
|
-
options: ({ delay: ScheduleDelay } | { at: Date }) & {
|
|
830
|
-
ifLastIndex: number
|
|
831
|
-
},
|
|
832
|
-
): Promise<void>
|
|
833
|
-
```
|
|
834
|
-
|
|
835
|
-
Publish a conditional clear to the configured scheduler. `name` is non-empty
|
|
836
|
-
and `ifLastIndex` is required. Timing has the same meaning and provider limits
|
|
837
|
-
as `session.schedule`. The task id derives from the contract, session, name,
|
|
838
|
-
and condition, plus the triggering event id and any earlier clear inside a
|
|
839
|
-
handler; reuse a name only for the same timing. The promise resolves after
|
|
840
|
-
provider acceptance.
|
|
841
|
-
|
|
842
|
-
Delivery runs `clear({ ifLastIndex })` and acknowledges after the deletion
|
|
843
|
-
finishes. A stale or duplicate delivery changes nothing. No event is appended.
|
|
844
|
-
Indexes never repeat, so a condition from an older lifetime never matches a
|
|
845
|
-
newer one. Clear tasks, appends, and recovery drains all use the Vercel
|
|
846
|
-
scheduler's `deploymentId` setting.
|
|
847
|
-
|
|
848
|
-
`ifLastIndex: ctx.event.index` matches only while the handler's event is still
|
|
849
|
-
the head. The handler's own appends and returned events move the head, so a
|
|
850
|
-
handler that writes should use the index of its last `ctx.session.append`
|
|
851
|
-
instead. Actor handlers always commit state, so they can't use the trigger's
|
|
852
|
-
index. A delay short enough to deliver while the handler is still running can
|
|
853
|
-
match before those writes land; the clear then ends the handler's attempt and
|
|
854
|
-
discards its returned events.
|
|
855
|
-
|
|
856
|
-
Each delivery attempt resumes the deletion where the last one stopped. Vercel
|
|
857
|
-
Queues redelivers until retention ends. QStash retries one message up to its
|
|
858
|
-
`retries` budget; a deletion that outlasts every retry leaves the message in
|
|
859
|
-
QStash's dead-letter queue with the rest of the old history still invisible,
|
|
860
|
-
and any later clear of the session finishes it.
|
|
861
|
-
|
|
862
|
-
### `session.history()`
|
|
863
|
-
|
|
864
|
-
```ts
|
|
865
|
-
session.history(options?: { gte?: number; lte?: number }): Promise<Event[]>
|
|
866
|
-
```
|
|
867
|
-
|
|
868
|
-
Raw events from the session, oldest first. `gte` and `lte` are inclusive event
|
|
869
|
-
indexes; omit a bound to leave that end of the log open.
|
|
870
|
-
Bounds are non-negative safe integers. `lte: 0` returns `[]`; `gte > lte` throws.
|
|
871
|
-
After a clear, history starts after the marker. The marker itself is not
|
|
872
|
-
returned.
|
|
873
|
-
|
|
874
|
-
Redis reads a fixed committed range in pages of at most 256 events, with up to
|
|
875
|
-
sixteen page reads in flight. Public history omits dispatch metadata. PostgreSQL
|
|
876
|
-
uses one indexed range query selecting only public event columns. Custom store
|
|
877
|
-
adapters can implement `readHistory(sessionId, options)` with the same exclusive
|
|
878
|
-
`afterIndex` and inclusive `throughIndex` bounds as `read()`. A2 falls back to
|
|
879
|
-
`read()` when that capability is absent.
|
|
880
|
-
|
|
881
|
-
### `session.state(reducer, options?)`
|
|
882
|
-
|
|
883
|
-
```ts
|
|
884
|
-
session.state(
|
|
885
|
-
reducer: Reducer<S>,
|
|
886
|
-
options?: { through?: number | 'latest' },
|
|
887
|
-
): Promise<{ state: S; index: number; clearIndex?: number }>
|
|
888
|
-
```
|
|
889
|
-
|
|
890
|
-
The log folded through one committed prefix. `index` is its exact boundary,
|
|
891
|
-
an append-order cursor rather than a handler-settlement marker. Hand it to the
|
|
892
|
-
client to resume there. Snapshot write-back uses platform `waitUntil`; this
|
|
893
|
-
read never dispatches handlers or waits for pending work.
|
|
894
|
-
|
|
895
|
-
On a root session from `server.session(id)`, omitting `through` reads the latest
|
|
896
|
-
committed frontier. On the handler-scoped `ctx.session`, omitting it reads
|
|
897
|
-
through `ctx.event.index`. That boundary is inclusive and stable across retries.
|
|
898
|
-
Use `{ through: 'latest' }` inside a handler only when later committed events
|
|
899
|
-
are intentionally part of the decision. A numeric `through` is an inclusive,
|
|
900
|
-
non-negative safe integer; `through: 0` returns the reducer's initial state.
|
|
901
|
-
|
|
902
|
-
Once a session has been cleared, the result includes `clearIndex`, the latest
|
|
903
|
-
marker, and state folds from there. Forward the whole result when hydrating a
|
|
904
|
-
client. A positive `through` below `clearIndex` throws `SESSION_CLEARED`, and
|
|
905
|
-
`through: 0` returns the seed without `clearIndex`. On
|
|
906
|
-
`ctx.session`, every state read throws it once the triggering event has been
|
|
907
|
-
cleared, including `{ through: 'latest' }`. Handler `history()` and `stream()`
|
|
908
|
-
are not fenced.
|
|
909
|
-
|
|
910
|
-
The default handler read and numeric handler reads automatically retain their
|
|
911
|
-
exact folded checkpoint while the triggering event is unfinished. Completion
|
|
912
|
-
or dead-lettering releases the reference and collects the checkpoint when no
|
|
913
|
-
other unfinished handler uses it. There are no retention options. Handler
|
|
914
|
-
`{ through: 'latest' }` reads and all root reads do not retain history.
|
|
915
|
-
|
|
916
|
-
If the latest snapshot is ahead of a numeric boundary, A2 can use the greatest
|
|
917
|
-
eligible retained checkpoint. A root historical read never creates retention
|
|
918
|
-
or writes a stale checkpoint behind the latest snapshot; it folds from the log
|
|
919
|
-
when no eligible checkpoint exists.
|
|
920
|
-
|
|
921
|
-
Operational snapshot-read failures preserve existing A2 errors; other errors
|
|
922
|
-
become `STORE_UNAVAILABLE` with the original error as the cause. They do not retry through individual
|
|
923
|
-
reads or raw history. Cache misses and `stateSchema` rejection still rebuild
|
|
924
|
-
from the log. Snapshot write-back remains background work; failures are reported
|
|
925
|
-
through `a2.snapshot` telemetry or the default console sink, without failing the
|
|
926
|
-
completed read or attaching snapshot contents.
|
|
927
|
-
|
|
928
|
-
### `session.stream(options?)`
|
|
929
|
-
|
|
930
|
-
```ts
|
|
931
|
-
session.stream(options?: { startAfter?: number }): AsyncIterable<Event | SessionCleared>
|
|
932
|
-
session.stream(options: {
|
|
933
|
-
startAfter?: number
|
|
934
|
-
presence: true
|
|
935
|
-
}): AsyncIterable<Event | SessionCleared | PresencePatch | PresenceSnapshot>
|
|
936
|
-
```
|
|
937
|
-
|
|
938
|
-
A live feed of the session's events. `startAfter` is a non-negative safe integer;
|
|
939
|
-
`startAfter: 20` begins with event 21.
|
|
940
|
-
Server-side only; `server.fetch` exposes it over SSE and WebSocket. Subscribing
|
|
941
|
-
never dispatches handlers.
|
|
942
|
-
|
|
943
|
-
A clear arrives in order as a `SessionCleared` marker, whose `type` is
|
|
944
|
-
`'a2.cleared'`. A stream that opens below the
|
|
945
|
-
latest marker begins with it. A stream that is already running delivers the
|
|
946
|
-
log in order, so events from before the clear can arrive ahead of the marker.
|
|
947
|
-
Reset when the marker arrives. Clearing never closes a stream.
|
|
948
|
-
|
|
949
|
-
With `presence: true`, the feed yields one `PresenceSnapshot` first:
|
|
950
|
-
`{ snapshot }`, the current pruned map with each field's own `value`,
|
|
951
|
-
`seen`, and `at` stamp. Live presence patches then interleave with
|
|
952
|
-
events. The stream response sends both as named SSE frames, so clients
|
|
953
|
-
that don't know them skip them. The return type widens only under the
|
|
954
|
-
literal `presence: true`; without it, existing consumers keep
|
|
955
|
-
`AsyncIterable<Event>`. The option itself exists only on sessions of
|
|
956
|
-
contracts that declare `presence`; elsewhere it is a type error, not a
|
|
957
|
-
runtime throw. A `PresencePatch` is `{ participant, values, seen, at }`
|
|
958
|
-
with `values` typed by the contract's presence schemas (`null` marks a
|
|
959
|
-
cleared field).
|
|
960
|
-
|
|
961
|
-
### `session.setPresence(patch)`
|
|
962
|
-
|
|
963
|
-
```ts
|
|
964
|
-
session.setPresence(patch: {
|
|
965
|
-
participant: string
|
|
966
|
-
values: { [field]: payload | null } // partial; null clears a field
|
|
967
|
-
seen?: number
|
|
968
|
-
at?: number // the sender's LWW stamp, epoch ms; receipt time when absent
|
|
969
|
-
}): Promise<void>
|
|
970
|
-
```
|
|
971
|
-
|
|
972
|
-
Validates each field against the contract's `presence` schemas, then
|
|
973
|
-
broadcasts. Last writer wins per participant per field, ordered by
|
|
974
|
-
`at`, the sender's stamp in epoch milliseconds (the browser client
|
|
975
|
-
stamps its sends automatically). When `at` is absent, receipt time
|
|
976
|
-
stands in: a stampless caller is a single writer, so receipt order is
|
|
977
|
-
its send order. No log row, no
|
|
978
|
-
dispatch, no scheduler arm; `history()` and `state()` never see it. A
|
|
979
|
-
bad payload throws `INVALID_PAYLOAD`; an unknown field throws
|
|
980
|
-
`UNKNOWN_PRESENCE_FIELD`; a non-finite or negative `at` throws a
|
|
981
|
-
`TypeError`. `seen` is the sender's stream frontier, the
|
|
982
|
-
stamp view code uses to reconcile presence against the fold (see
|
|
983
|
-
[Presence](/guides/presence#reconciling-the-planes-seen)). Values expire
|
|
984
|
-
by backend TTL when a participant goes silent (default 60 seconds; set
|
|
985
|
-
`createServer({ presence: { ttlMs } })` to change it). Expiry counts on
|
|
986
|
-
the storage clock, not the sender stamp, so a hostile stamp can only
|
|
987
|
-
vandalize its own field and still expires on schedule.
|
|
988
|
-
|
|
989
|
-
The patch is structurally the `presence` sibling of the push envelope;
|
|
990
|
-
`server.fetch`'s push lane forwards it whole to `session.setPresence(presence)`.
|
|
991
|
-
The API mirrors the wire.
|
|
992
|
-
|
|
993
|
-
The handler-scoped form `ctx.session.setPresence(...)` is the same
|
|
994
|
-
operation; `seen` defaults to the triggering event's `index`.
|
|
995
|
-
|
|
996
|
-
### `session.presence()`
|
|
997
|
-
|
|
998
|
-
```ts
|
|
999
|
-
session.presence(): Promise<{
|
|
1000
|
-
[participant: string]: {
|
|
1001
|
-
[field: string]: { value: unknown; seen: number; at: Date }
|
|
1002
|
-
}
|
|
1003
|
-
}>
|
|
1004
|
-
```
|
|
1005
|
-
|
|
1006
|
-
The current presence map, expired values pruned. A point-in-time read
|
|
1007
|
-
for server renders; live consumers use `stream({ presence: true })`.
|
|
1008
|
-
Each entry's `at` is the sender's stamp for when the value was set,
|
|
1009
|
-
the last-writer-wins comparator; how long a value stays painted is
|
|
1010
|
-
still view logic, decided at render time.
|
|
1011
|
-
|
|
1012
|
-
## Event
|
|
1013
|
-
|
|
1014
|
-
```ts
|
|
1015
|
-
type Event = {
|
|
1016
|
-
id: string
|
|
1017
|
-
type: string
|
|
1018
|
-
payload: unknown // typed by the contract's schema for `type`
|
|
1019
|
-
index: number // position in the session's log, from 1
|
|
1020
|
-
sessionId: string
|
|
1021
|
-
createdAt: Date
|
|
1022
|
-
}
|
|
1023
|
-
```
|
|
1024
|
-
|
|
1025
|
-
## `experimental-a2/react`
|
|
1026
|
-
|
|
1027
|
-
### `createReact(options)`
|
|
1028
|
-
|
|
1029
|
-
```ts
|
|
1030
|
-
createReact(options: {
|
|
1031
|
-
client: A2Client // bind a shared client: api + contract + session identity
|
|
1032
|
-
}): { SessionProvider, useSession }
|
|
1033
|
-
|
|
1034
|
-
// or, bind the contract and supply api through the Provider:
|
|
1035
|
-
createReact(options: {
|
|
1036
|
-
contract: Contract // requires a public reducer
|
|
1037
|
-
}): { SessionProvider, useSession }
|
|
1038
|
-
```
|
|
1039
|
-
|
|
1040
|
-
A factory, like `createContext`: call it once in a `'use client'`
|
|
1041
|
-
module, export the bound pair. Server components import
|
|
1042
|
-
`SessionProvider` from that module as a client reference; client
|
|
1043
|
-
components import `useSession` from the same file. See
|
|
1044
|
-
[Live UI](/guides/react#bind-react-once).
|
|
1045
|
-
|
|
1046
|
-
`createReact({ contract })` is also supported for provider-bound clients. Use a
|
|
1047
|
-
shared `A2Client` when code outside the provider needs to push before a route
|
|
1048
|
-
transition and the destination should adopt the same optimistic session.
|
|
1049
|
-
|
|
1050
|
-
### `<SessionProvider>`
|
|
1051
|
-
|
|
1052
|
-
| Prop | What it is |
|
|
1053
|
-
| --------------- | ------------------------------------------------------ |
|
|
1054
|
-
| `sessionId` | which session to subscribe to |
|
|
1055
|
-
| `initial` | optional snapshot or Promise of a snapshot; `null` means bootstrap |
|
|
1056
|
-
| `initialEvents` | optional earlier raw events for a history UI |
|
|
1057
|
-
| `participant` | overrides the client's `participant`; one of the two is required to call `setPresence` |
|
|
1058
|
-
|
|
1059
|
-
Supplies scope and optional initial data. Mounted enabled hooks own
|
|
1060
|
-
connection leases and reconnect from the last streamed event. `participant` binds at the session's
|
|
1061
|
-
first resolution; a changed provider prop is ignored for the session's
|
|
1062
|
-
runtime lifetime.
|
|
1063
|
-
|
|
1064
|
-
`initial` carries the complete state/index snapshot, plus `clearIndex` when
|
|
1065
|
-
the session has been cleared.
|
|
1066
|
-
For a new session, the stream starts after `initial.index`, including events
|
|
1067
|
-
appended between the server render and the connection. `initialEvents` only preloads the raw
|
|
1068
|
-
`events` feed for a UI that needs entries from before that frontier. It does
|
|
1069
|
-
not affect state hydration or stream resumption. Later snapshots advance
|
|
1070
|
-
state while preserving the stream's resume position, so delayed events
|
|
1071
|
-
still enter the feed without folding into state again.
|
|
1072
|
-
|
|
1073
|
-
`initial` accepts `InitialSnapshot<S>`: `StateSnapshot<S> | null | undefined |
|
|
1074
|
-
Promise<StateSnapshot<S> | null | undefined>`. The type is exported from
|
|
1075
|
-
`experimental-a2` and `experimental-a2/client`. A cold session waits for a
|
|
1076
|
-
supplied Promise before connecting. A snapshot result seeds state; `null`
|
|
1077
|
-
or `undefined` selects subscription bootstrap. Rejection pauses cold startup
|
|
1078
|
-
and reports through `connection.error`; supply a new Promise, snapshot, or
|
|
1079
|
-
`null` to resume. A warm session keeps its current state and connection.
|
|
1080
|
-
The Provider itself does not suspend. Keep Promise identity stable.
|
|
1081
|
-
|
|
1082
|
-
### `useSession(options?)`
|
|
1083
|
-
|
|
1084
|
-
```ts
|
|
1085
|
-
const { state, hydrated, push, reconnect, events, index, loadHistory, history, connection } = useSession()
|
|
1086
|
-
```
|
|
1087
|
-
|
|
1088
|
-
`state` has the reducer's client state type. A supplied snapshot seeds the
|
|
1089
|
-
first render. Otherwise it displays `reducer.initialState`, through the client
|
|
1090
|
-
projection when present, until a snapshot replaces it atomically. Without a
|
|
1091
|
-
supplied snapshot, the server sends one first on the same subscription.
|
|
1092
|
-
Bootstrap errors and retries use `connection`.
|
|
1093
|
-
`hydrated` is a boolean shared with `session.getSnapshot()`. It is `false`
|
|
1094
|
-
while only the reducer seed is available and becomes `true` when an initial
|
|
1095
|
-
snapshot or bootstrap is applied, even for an empty state at index 0.
|
|
1096
|
-
It stays `true` while cached state survives reconnects, disabled hooks, or a
|
|
1097
|
-
failed replacement preload. It does not imply a live connection or fresh data.
|
|
1098
|
-
The hook accepts `sessionId`, `client`, `participant`, `initial`, `enabled`, and
|
|
1099
|
-
`suspend` options; scope defaults come from its Provider and bound client.
|
|
1100
|
-
`suspend` defaults to `false`. With `true`, React `use()` reads the supplied
|
|
1101
|
-
`initial` Promise, using the nearest Suspense and Error boundaries. Automatic
|
|
1102
|
-
subscription bootstrap never suspends, including when the Promise resolves
|
|
1103
|
-
to `null` or `undefined`. The default renders the reducer seed while a cold
|
|
1104
|
-
initial Promise is pending. `enabled` controls connection ownership independently.
|
|
1105
|
-
|
|
1106
|
-
`state` starts from that snapshot and folds live events through the
|
|
1107
|
-
shared reducer. `events` is the raw event feed: observed, explicitly
|
|
1108
|
-
seeded, or backscrolled. Unless `initialEvents` seeds earlier entries or
|
|
1109
|
-
`loadHistory` fetches them, it begins after `initial.index`. `index` is
|
|
1110
|
-
the latest server-confirmed state index, the `lastSeenIndex` for
|
|
1111
|
-
[cancellation](/guides/cancellation).
|
|
1112
|
-
|
|
1113
|
-
`loadHistory({ before?, limit? })` backscrolls: it fetches a bounded
|
|
1114
|
-
slice of the log from below the frontier (the same route, `gte`/`lte`
|
|
1115
|
-
query parameters) and merges it into `events`, deduped, ordered, and
|
|
1116
|
-
shared across every handle of the session. It resolves with the events
|
|
1117
|
-
in the requested range. Defaults walk backward 50 at a time from the
|
|
1118
|
-
oldest loaded event; `before` is an exclusive upper bound. After a
|
|
1119
|
-
newer snapshot hydrates the session, default paging still continues from
|
|
1120
|
-
the oldest loaded event. Pass an explicit `before` to read another range.
|
|
1121
|
-
It never touches `state` or the optimistic overlay. Calls
|
|
1122
|
-
serialize per session and already-loaded ranges are not refetched.
|
|
1123
|
-
`history` is the progress: `{ loading, complete, oldestLoaded }`, where
|
|
1124
|
-
`complete` means the feed reaches index 1, or the first event after the latest
|
|
1125
|
-
clear (or the log is empty). The `ws` api has no history lane; there `loadHistory`
|
|
1126
|
-
throws a `TypeError`.
|
|
1127
|
-
|
|
1128
|
-
When a clear reaches the session, `state` resets to the reducer's initial
|
|
1129
|
-
state and `events` drops everything at or below the marker. Unconfirmed pushes
|
|
1130
|
-
stay in the overlay and apply to the reset state. A push acked below the marker
|
|
1131
|
-
was cleared with it: it leaves the overlay and its `confirmed` resolves.
|
|
1132
|
-
|
|
1133
|
-
`presence` is the replicated ephemeral map,
|
|
1134
|
-
`Record<participantId, { [field]: { value, seen, at } }>`, including
|
|
1135
|
-
this client. `setPresence(values)` is fire-and-forget: validated
|
|
1136
|
-
locally, merged into the local map immediately, coalesced on the wire
|
|
1137
|
-
to a fixed cadence (a leading send immediately, then at most one
|
|
1138
|
-
merged send per interval; later values win per field), stamped with
|
|
1139
|
-
the frontier at send time as `seen`. Parallel sends are safe: merges
|
|
1140
|
-
are last-writer-wins by `at`, and the client's stamps are monotonic
|
|
1141
|
-
per session (wall-seeded, guarded so overlapping sends can never
|
|
1142
|
-
invert). No ack, no `confirmed`, no retry.
|
|
1143
|
-
|
|
1144
|
-
Both members exist only when the contract declares `presence`; their
|
|
1145
|
-
value and field types come from its schemas, through the reducer, with
|
|
1146
|
-
no type arguments (the reducer is the client's typed handle on the
|
|
1147
|
-
contract; it still never folds presence). `setPresence` requires a
|
|
1148
|
-
`participant`: the client's default, or the provider's override. See
|
|
1149
|
-
[Presence](/guides/presence).
|
|
1150
|
-
|
|
1151
|
-
`push` appends optimistically: validated locally, rolled back on
|
|
1152
|
-
rejection, retried for `STORE_UNAVAILABLE` and transport failures. Awaiting it gives the
|
|
1153
|
-
server ack; the same result carries `confirmed`, a lazy promise for
|
|
1154
|
-
the later moment when the live stream has delivered the batch back and
|
|
1155
|
-
the view shows server truth:
|
|
1156
|
-
|
|
1157
|
-
```ts
|
|
1158
|
-
const t0 = performance.now()
|
|
1159
|
-
const result = push({ type: 'created', payload })
|
|
1160
|
-
await result // the server ack: push→ack latency
|
|
1161
|
-
await result.confirmed // the stream round-trip: ack→stream latency
|
|
1162
|
-
const roundtripMs = performance.now() - t0
|
|
1163
|
-
```
|
|
1164
|
-
|
|
1165
|
-
Overlapping calls through the same session client update the view
|
|
1166
|
-
immediately, then enter the transport in invocation order. Each call waits
|
|
1167
|
-
only for the previous call's ack or final rejection, not for `confirmed`.
|
|
1168
|
-
Other session IDs and clients remain concurrent; they converge on the log's
|
|
1169
|
-
durable order.
|
|
1170
|
-
|
|
1171
|
-
A rejected push rejects both promises with the same `A2ClientError`;
|
|
1172
|
-
`confirmed` is materialized only when accessed, so ignoring it costs
|
|
1173
|
-
nothing. It also reads as intent: `await push(...).confirmed` is
|
|
1174
|
-
"continue once this is server truth".
|
|
1175
|
-
|
|
1176
|
-
`session.reconnect(): void` immediately restarts a leased subscription,
|
|
1177
|
-
including one paused by the reconnection policy,
|
|
1178
|
-
from its current cursor. It preserves state, pending writes, and leases, and
|
|
1179
|
-
does nothing without an active lease. The React `useSession()` result exposes
|
|
1180
|
-
the same `reconnect` function. Use it after a custom REST mutation; it requests
|
|
1181
|
-
a restart rather than returning a promise for catch-up completion. A
|
|
1182
|
-
multiplexed WebSocket keeps its physical socket and other session lanes;
|
|
1183
|
-
a single-session WebSocket replaces its socket.
|
|
1184
|
-
|
|
1185
|
-
`connection` is a discriminated union; impossible states are
|
|
1186
|
-
unrepresentable (an `error` only exists while disconnected):
|
|
1187
|
-
|
|
1188
|
-
```ts
|
|
1189
|
-
// in a client module:
|
|
1190
|
-
import type { A2ClientError } from 'experimental-a2/client'
|
|
1191
|
-
|
|
1192
|
-
type Connection =
|
|
1193
|
-
| { status: 'idle' }
|
|
1194
|
-
| { status: 'connecting'; reconnects: number; error: A2ClientError | null }
|
|
1195
|
-
| { status: 'paused'; reconnects: number; error: A2ClientError | null }
|
|
1196
|
-
| { status: 'live'; reconnects: number }
|
|
1197
|
-
| { status: 'closed' }
|
|
1198
|
-
```
|
|
1199
|
-
|
|
1200
|
-
"Reconnecting…" is `status === 'connecting' && reconnects > 0`. And
|
|
1201
|
-
it's honest about silence: the server heartbeats the stream (`: ping`
|
|
1202
|
-
every 15s), and a client that hears nothing for two beats treats the
|
|
1203
|
-
connection as dead (aborts it and reconnects), so `live` means bytes
|
|
1204
|
-
are actually flowing, not "the socket hasn't errored yet". See
|
|
1205
|
-
[Live UI](/guides/react).
|
|
1206
|
-
|
|
1207
|
-
### `useSession(client, sessionId, options?)`
|
|
1208
|
-
|
|
1209
|
-
```ts
|
|
1210
|
-
useSession(client: A2Client, sessionId: string, options?: {
|
|
1211
|
-
participant?: string // overrides the client's participant
|
|
1212
|
-
initial?: InitialSnapshot<S>
|
|
1213
|
-
enabled?: boolean
|
|
1214
|
-
suspend?: boolean
|
|
1215
|
-
}): UseSessionResult
|
|
1216
|
-
```
|
|
1217
|
-
|
|
1218
|
-
The standalone, provider-less hook. It returns the same result shape
|
|
1219
|
-
as the bound hook, for apps whose components reach sessions ad hoc (a
|
|
1220
|
-
sidebar of channel sessions, a user session read from a menu). The
|
|
1221
|
-
handle is identity-mapped: every hook and provider mounting the same
|
|
1222
|
-
session shares one runtime. The stream is refcounted: the first mount
|
|
1223
|
-
connects, the last unmount closes (StrictMode-safe).
|
|
1224
|
-
|
|
1225
|
-
`initial` accepts the result of `session.state(reducer)` or its Promise. Pass it
|
|
1226
|
-
whole: its `clearIndex` lets a cached client drop history from before a clear
|
|
1227
|
-
it never streamed. The automatic snapshot carries it too. Without it,
|
|
1228
|
-
the subscription obtains a snapshot automatically. To defer automatic I/O,
|
|
1229
|
-
pass `enabled: false`. Disabled hooks keep observing cached state; other
|
|
1230
|
-
owners continue normally. Explicit pushes can still acquire a connection.
|
|
1231
|
-
For deliberate full replay, supply `{ state: reducer.initialState, index: 0 }`.
|
|
1232
|
-
See [Live UI](/guides/react).
|
|
1233
|
-
|
|
1234
|
-
## `experimental-a2/ai`
|
|
1235
|
-
|
|
1236
|
-
The isomorphic AI protocol: event schemas, the `AIState` reducer, pure input
|
|
1237
|
-
builders, and `agentContract()` for extending both. It has a peer dependency on
|
|
1238
|
-
AI SDK v7, used as types only here.
|
|
1239
|
-
|
|
1240
|
-
### `agentContract(options)`
|
|
1241
|
-
|
|
1242
|
-
```ts
|
|
1243
|
-
agentContract<M extends UIMessage = UIMessage>(options: {
|
|
1244
|
-
name: string
|
|
1245
|
-
events?: Record<string, StandardSchemaV1>
|
|
1246
|
-
messageSchema?: StandardSchemaV1<M>
|
|
1247
|
-
reducer?: {
|
|
1248
|
-
name: string // snapshot identity: rename it when fold changes
|
|
1249
|
-
initialState: X // its keys may not shadow AIState's
|
|
1250
|
-
fold: (state: X, event: ContractEvent) => X
|
|
1251
|
-
}
|
|
1252
|
-
}): Contract<Events, {}, AgentReducer<M, Events, X>>
|
|
1253
|
-
```
|
|
1254
|
-
|
|
1255
|
-
A core contract carrying the built-in AI protocol plus `events`, with a reducer
|
|
1256
|
-
that folds `AIState` and, with `reducer`, your application state beside it.
|
|
1257
|
-
The session's state is `AIState<M> & X`. Share it when a client needs
|
|
1258
|
-
application events or state: serve it with `agent({ contract })` and use it
|
|
1259
|
-
with [`useAgent({ api, id, contract })`](#useagentoptions). Otherwise
|
|
1260
|
-
`agent({ name })` builds one on the server and clients use the built-in
|
|
1261
|
-
protocol.
|
|
1262
|
-
|
|
1263
|
-
Built-in event names cannot be replaced by `options.events`. `messageSchema`
|
|
1264
|
-
validates a narrower `UIMessage` type in message payloads; the explicit type
|
|
1265
|
-
argument types messages without one. `reducer.fold` sees `X` and every event,
|
|
1266
|
-
never the transcript; it runs after the AI fold on the server and in every
|
|
1267
|
-
client, so optimistic display carries through. A key of `initialState` that
|
|
1268
|
-
`AIState` also has throws a `TypeError` and fails to typecheck. The reducer name
|
|
1269
|
-
is `AI_STATE_REDUCER` (`a2.ai.state.v26`) without `reducer`, and
|
|
1270
|
-
`${reducer.name}+a2.ai.state.v26` with one, so checkpoints advance when either
|
|
1271
|
-
fold changes. `AgentReducer` types that name, which is what lets
|
|
1272
|
-
[`createAgentClient()`](#createagentclientoptions) accept a plain
|
|
1273
|
-
`agentContract()`, and carries `X` to `agent()`'s resolvers.
|
|
1274
|
-
|
|
1275
|
-
### `TranscriptMessage<M>`
|
|
1276
|
-
|
|
1277
|
-
```ts
|
|
1278
|
-
type TranscriptMessage<M extends UIMessage> = Omit<M, 'parts'> & {
|
|
1279
|
-
parts: Array<M['parts'][number] | CompactionPart>
|
|
1280
|
-
}
|
|
1281
|
-
```
|
|
1282
|
-
|
|
1283
|
-
The message type of `AIState<M>['messages']` and `deriveUIMessages()`: your
|
|
1284
|
-
message type with the [compaction part](#iscompactionpartpart) added, so
|
|
1285
|
-
`isCompactionPart()` narrows even when `M` declares its own data parts.
|
|
1286
|
-
|
|
1287
|
-
### `AIMessageOf<Events>`
|
|
1288
|
-
|
|
1289
|
-
The `UIMessage` type a contract's `ai.message.created` schema carries, such as
|
|
1290
|
-
`AIMessageOf<typeof contract.events>`. It falls back to `UIMessage`. `agent()`
|
|
1291
|
-
uses it to type messages for any contract carrying the AI protocol.
|
|
1292
|
-
|
|
1293
|
-
### `handlerContext(contract?)`
|
|
1294
|
-
|
|
1295
|
-
Reads the durable A2 handler context ambient to the current tool
|
|
1296
|
-
execution. Tools are plain AI SDK `tool()` definitions; A2 passes their
|
|
1297
|
-
options through to the AI SDK without interpreting them and runs each
|
|
1298
|
-
local `execute` inside an ambient context scope. Call
|
|
1299
|
-
`handlerContext()` anywhere below `execute`, including awaited helpers and
|
|
1300
|
-
async iteration. A call outside any tool execution throws a `TypeError`.
|
|
1301
|
-
Without an argument, the context is typed for the built-in protocol. Pass a
|
|
1302
|
-
shared `agentContract()` to type application events; A2 verifies it against the
|
|
1303
|
-
executing agent and throws a `TypeError` on a mismatch.
|
|
1304
|
-
|
|
1305
|
-
The returned `AgentToolContext<D>` supplies the current A2 handler's `event`,
|
|
1306
|
-
`attempt`, `session`, and `signal`. Local tools execute the explicit
|
|
1307
|
-
`ai.tool.execution.requested` work event. Its payload contains the admitted
|
|
1308
|
-
`call`, `generation`, `turnId`, and execution `version`. Calls such as
|
|
1309
|
-
`ctx.session.schedule()` use the scheduler of the agent executing the tool.
|
|
1310
|
-
|
|
1311
|
-
The context is runtime capability, not event data. A2 creates it for the
|
|
1312
|
-
current attempt, never persists it, and never sends it to the model or a
|
|
1313
|
-
provider-executed tool. Each retry reads that attempt's fresh context.
|
|
1314
|
-
Model-phase tool options (`contextSchema`, dynamic descriptions,
|
|
1315
|
-
`needsApproval` predicates, input lifecycle callbacks) are the AI SDK's
|
|
1316
|
-
own; A2 forwards them untouched, and durable execution passes no AI SDK
|
|
1317
|
-
`context` option. `needsApproval` and `generation.toolApproval`
|
|
1318
|
-
predicates run during the model step and are re-evaluated on recovery
|
|
1319
|
-
for the same durable tool call, so keep them deterministic over the tool
|
|
1320
|
-
input. Model-phase hooks see tool definitions without local `execute`
|
|
1321
|
-
functions.
|
|
1322
|
-
|
|
1323
|
-
### `inputs`
|
|
1324
|
-
|
|
1325
|
-
Pure typed inputs for `append()` and `push()`. The `useAgent()` actions build
|
|
1326
|
-
the browser ones:
|
|
1327
|
-
|
|
1328
|
-
| Input | Events |
|
|
1329
|
-
| --- | --- |
|
|
1330
|
-
| `inputs.message(message, { generate? })` | queues input; `generate: false` records passive context at admission |
|
|
1331
|
-
| `inputs.seed(message)` | queues passive context; non-user roles require a trusted append |
|
|
1332
|
-
| `inputs.approval(response)` | answers a pending tool approval |
|
|
1333
|
-
| `inputs.input(response)` | answers a pending application input |
|
|
1334
|
-
| `inputs.requestInput(request)` | requests application input from trusted server code |
|
|
1335
|
-
| `inputs.retry(options)` | retries a failed response |
|
|
1336
|
-
| `inputs.stop({ turnId, lastSeenIndex? })` | ends the observed turn and advances when admission is open |
|
|
1337
|
-
| `inputs.steer({ turnId, message, lastSeenIndex? })` | puts the new input first for a handoff after the observed turn's current step |
|
|
1338
|
-
| `inputs.queue.edit({ message })` | changes a pending input; its ID stays fixed |
|
|
1339
|
-
| `inputs.queue.move({ inputId, beforeId })` | moves pending input before an ID; `null` means the end |
|
|
1340
|
-
| `inputs.queue.remove({ inputId })` | removes pending input; its submission stays in the log |
|
|
1341
|
-
| `inputs.queue.sendNow({ inputId, turnId, lastSeenIndex? })` | promotes an existing queued input for a handoff after the current step; preserves pause |
|
|
1342
|
-
| `inputs.pause({ when: 'now' \| 'after-turn' })` | pauses execution now or inbox admission after the turn |
|
|
1343
|
-
| `inputs.resume()` | resumes retained work, then inbox admission |
|
|
1344
|
-
| `inputs.toolResult(result)` | records a deferred provider-executed tool result from trusted server code |
|
|
1345
|
-
| `inputs.interrupt(options)` | interrupts a specific generation/request owner |
|
|
1346
|
-
|
|
1347
|
-
Edits are last-write-wins in session log order and preserve the pending message's
|
|
1348
|
-
identity and role. They require no revision or confirmation before another edit.
|
|
1349
|
-
Admission freezes the message; subsequent edits return `not-pending`.
|
|
1350
|
-
|
|
1351
|
-
All builders emit `ai.control.requested`. Persistence and application are
|
|
1352
|
-
separate: `ai.control.decided` returns `{ commandId, outcome, reason? }` through
|
|
1353
|
-
the existing stream. Outcomes are `applied` or `rejected`. Rejection reasons
|
|
1354
|
-
include `not-pending`, `role-mismatch`, `stale-turn`, `not-found`,
|
|
1355
|
-
`not-user-input`, and `closed`.
|
|
1356
|
-
`state.receipt` holds the latest user receipt.
|
|
1357
|
-
|
|
1358
|
-
Steering validates and prioritizes new input even if the observed turn has
|
|
1359
|
-
finished. A matching active turn finishes its current model response and tool
|
|
1360
|
-
batch, then yields before another model step. Running local workers and deferred
|
|
1361
|
-
provider results settle first; unanswered human approval/input waits can be
|
|
1362
|
-
closed at that point. Pause stays closed. Otherwise the input starts when idle
|
|
1363
|
-
or waits first behind a different active turn. The priority change is atomic;
|
|
1364
|
-
the later handoff and new work are committed atomically at the step boundary.
|
|
1365
|
-
Steer and Send now do not use `lastSeenIndex` to cut output.
|
|
1366
|
-
|
|
1367
|
-
Queue removal leaves the original submission in the log, so replay does not recreate the input.
|
|
1368
|
-
Send now preserves the queued input ID and latest accepted content. It requires
|
|
1369
|
-
an observed `turnId` (or null for idle), preserves pause, and only accepts
|
|
1370
|
-
user-role input. It promotes passive user input to generation. Missing targets
|
|
1371
|
-
and stale turns reject before any queue change.
|
|
1372
|
-
|
|
1373
|
-
`state.inbox` contains `paused` and ordered `items`. Each item contains
|
|
1374
|
-
`{ id, message, generate, afterStepOf? }`. The optional turn ID
|
|
1375
|
-
`afterStepOf` records the observed steering turn while that item remains queued;
|
|
1376
|
-
a matching active turn hands off after its current step.
|
|
1377
|
-
Removal cancels it and admission consumes it. `state.active` is null or contains
|
|
1378
|
-
`{ turnId, input, phase }`; admission freezes `input`. On the server, pending
|
|
1379
|
-
inputs are absent from `messages` and model context. The `paused` status and the `pausing`/`paused`
|
|
1380
|
-
active phases represent retained work. Resume uses a fresh model request and
|
|
1381
|
-
execution version. Tools without terminal outcomes can retry; their stable
|
|
1382
|
-
`toolCallId` supports external idempotency.
|
|
1383
|
-
|
|
1384
|
-
`inputs` deliberately has no session lifecycle methods. Append explicit
|
|
1385
|
-
`ai.session.created` and `ai.session.closed` events from trusted server code
|
|
1386
|
-
when an application uses them. Message, seed, and steering builders set the event ID to the input message ID.
|
|
1387
|
-
The two IDs must match for all new-input commands. Caller command/input IDs must
|
|
1388
|
-
be nonempty and cannot use the reserved `a2.ai:` prefix. Identical events replay;
|
|
1389
|
-
conflicting ID reuse rejects before append under core deduplication rules. IDs
|
|
1390
|
-
are unique across the store, including different contracts and sessions.
|
|
1391
|
-
Approval, input-response, and retry builders use stable interaction IDs.
|
|
1392
|
-
Other commands use IDs assigned by `push()` or `append()`; transport retries
|
|
1393
|
-
preserve those IDs. Browser
|
|
1394
|
-
ingress accepts user messages, approval and input responses, interruptions,
|
|
1395
|
-
and explicit retries. Only built-in server handlers append generation requests
|
|
1396
|
-
and AI lifecycle events. `inputs.requestInput()`, `inputs.toolResult()`, and non-user messages built
|
|
1397
|
-
with `inputs.seed()` are for trusted server appends.
|
|
1398
|
-
|
|
1399
|
-
`inputs.message(message, { generate: false })` is valid browser input. It admits
|
|
1400
|
-
the user message to `AIState.messages` without starting a model turn. The next
|
|
1401
|
-
user message that allows generation includes passive user messages before it
|
|
1402
|
-
in model context. Passive user messages after that trigger wait for a later
|
|
1403
|
-
generating message. Omitting the option preserves the default scheduling
|
|
1404
|
-
behavior. `inputs.seed()` writes
|
|
1405
|
-
the same passive flag. Trusted assistant and system seeds are context, not user
|
|
1406
|
-
queue cutpoints. The server never schedules them, regardless of the `generate`
|
|
1407
|
-
field. Model output is generation progress rather than another
|
|
1408
|
-
`ai.message.created` fact, so an assistant response cannot recursively start a
|
|
1409
|
-
new turn.
|
|
1410
|
-
|
|
1411
|
-
Approval and input request/response payloads require the active
|
|
1412
|
-
`requestId` and `generationId`. Clients copy both from the pending request, which prevents a
|
|
1413
|
-
delayed response from satisfying a newer model step.
|
|
1414
|
-
|
|
1415
|
-
`ai.retry.requested` carries `{ messageId, responseMessageId, retryId }`.
|
|
1416
|
-
`retryId` identifies one user action. The server converts the fact into an
|
|
1417
|
-
`ai.generation.requested` whose reason is `retry`.
|
|
1418
|
-
Retry reruns the failed model call while retaining earlier completed model steps,
|
|
1419
|
-
their tool results, and committed compactions, including steps completed after
|
|
1420
|
-
the latest compaction.
|
|
1421
|
-
The failed model call's uncommitted step is removed from the active prompt and
|
|
1422
|
-
response. Conversation forks and edit-and-resend flows for admitted messages are
|
|
1423
|
-
application-owned.
|
|
1424
|
-
|
|
1425
|
-
A2-generated requests, generations, assistant responses, worker jobs, reports,
|
|
1426
|
-
and lifecycle events have fixed-length deterministic IDs in the reserved
|
|
1427
|
-
`a2.ai:` namespace. An assistant message's ID equals its initial model-request
|
|
1428
|
-
event ID. Control state keeps no lifetime input/response-ID registry. IDs are opaque;
|
|
1429
|
-
ownership is carried by event fields. `ai.generation.requested` requires
|
|
1430
|
-
`responseMessageId`; a tool continuation also requires `sourceGenerationId`.
|
|
1431
|
-
|
|
1432
|
-
`ai.generation.failed` sets `stepLimit: true` when `maxSteps` rejects a
|
|
1433
|
-
continuation before another model step starts.
|
|
1434
|
-
|
|
1435
|
-
`ai.message.interrupted` carries either `{ messageId, requestId, reason?,
|
|
1436
|
-
lastSeenIndex? }` or `{ messageId, generationId, reason?, lastSeenIndex? }`.
|
|
1437
|
-
Exactly one owner ID is required. Clients copy `requestId` from
|
|
1438
|
-
`activeRequestId` before generation start and switch to the observed
|
|
1439
|
-
`activeGeneration.generationId` after start. A request-owned interruption
|
|
1440
|
-
remains valid if that same request starts before the append commits. A delayed
|
|
1441
|
-
interruption for an older owner is a projection and cancellation no-op.
|
|
1442
|
-
The message and owner IDs are validated as one pair. If either ID does not
|
|
1443
|
-
identify that same response owner, the interruption is stale and is a no-op.
|
|
1444
|
-
|
|
1445
|
-
An accepted interruption terminally fences its request or generation. Later
|
|
1446
|
-
generation, tool, approval, input, and compaction events remain in raw history
|
|
1447
|
-
but cannot alter `AIState`, satisfy pending work, or reactivate the response.
|
|
1448
|
-
Completed tool results at or before `lastSeenIndex` remain completed. A final
|
|
1449
|
-
tool result that races an ordinary generation failure remains authoritative in
|
|
1450
|
-
either commit order, while the generation stays failed.
|
|
1451
|
-
|
|
1452
|
-
Interruption and failure projections discard tool parts still in
|
|
1453
|
-
`input-streaming`. These argument drafts are not completed tool calls and do not
|
|
1454
|
-
enter later model prompts. Raw progress remains in the log. Available inputs,
|
|
1455
|
-
approval-waiting calls, and completed results retain their existing terminal
|
|
1456
|
-
handling.
|
|
1457
|
-
|
|
1458
|
-
### `reasoningLevels`
|
|
1459
|
-
|
|
1460
|
-
```ts
|
|
1461
|
-
type ReasoningLevel = 'provider-default' | 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh'
|
|
1462
|
-
const reasoningLevels: readonly ReasoningLevel[]
|
|
1463
|
-
```
|
|
1464
|
-
|
|
1465
|
-
Every `ReasoningLevel`, in order, for pickers. A `model` option returns one in
|
|
1466
|
-
[`ModelSettings`](#modelsettings).
|
|
1467
|
-
|
|
1468
|
-
### `isCompactionPart(part)`
|
|
1469
|
-
|
|
1470
|
-
```ts
|
|
1471
|
-
isCompactionPart(part: { type: string }): part is CompactionPart
|
|
1472
|
-
|
|
1473
|
-
type CompactionPart = {
|
|
1474
|
-
type: 'data-a2-compaction'
|
|
1475
|
-
id: string // the compacting generation's ID
|
|
1476
|
-
data: {
|
|
1477
|
-
status: 'running' | 'completed' | 'failed' | 'interrupted'
|
|
1478
|
-
inputTokens?: number
|
|
1479
|
-
thresholdTokens?: number
|
|
1480
|
-
summary?: string
|
|
1481
|
-
usage?: LanguageModelUsage
|
|
1482
|
-
}
|
|
1483
|
-
}
|
|
1484
|
-
```
|
|
1485
|
-
|
|
1486
|
-
The transcript part that marks a compaction. When `ai.compaction.requested`
|
|
1487
|
-
folds, the reducer appends it to the active response, between the steps
|
|
1488
|
-
before it and the model step it compacted for; a compaction before the first
|
|
1489
|
-
step creates the response with this part. The part counts toward that step's
|
|
1490
|
-
recovery base, so Stop cutoffs, failures, and retries of the step keep it.
|
|
1491
|
-
`ai.compaction.completed` settles it in place with the summary and usage. A
|
|
1492
|
-
running compaction whose generation ends first becomes `interrupted` (Stop,
|
|
1493
|
-
Pause now, or session close) or `failed` (failure). A pending Stop previews it
|
|
1494
|
-
as `interrupted` on the client. A superseded worker attempt's part is removed,
|
|
1495
|
-
and its replacement compacts again if it needs to. AI SDK
|
|
1496
|
-
`convertToModelMessages()` skips data parts by default, and model context never
|
|
1497
|
-
contains it.
|
|
1498
|
-
|
|
1499
|
-
### `events` and `createEvents(options?)`
|
|
1500
|
-
|
|
1501
|
-
The built-in Standard Schema definitions. Use `events` for the default AI SDK
|
|
1502
|
-
`UIMessage`; use `createEvents({ messageSchema })` to validate a more specific
|
|
1503
|
-
message type. Both are isomorphic.
|
|
1504
|
-
|
|
1505
|
-
### Optimistic AI client state
|
|
1506
|
-
|
|
1507
|
-
Every client folding an AI reducer exposes `state.messages`, `state.inbox`, and
|
|
1508
|
-
`state.starting` with pending send, edit, move, remove, Send now, steer, pause,
|
|
1509
|
-
resume, and Stop commands applied for display. This includes contracts with
|
|
1510
|
-
application state from `agentContract({ reducer })`. Idle, unpaused
|
|
1511
|
-
sends appear in the conversation immediately. A generating input occupies the
|
|
1512
|
-
optimistic turn, so subsequent sends queue. Busy or paused sessions queue new
|
|
1513
|
-
sends. `starting` indicates a speculative turn awaiting acceptance; it does not
|
|
1514
|
-
indicate model execution. Confirmed turns take precedence during reconciliation.
|
|
1515
|
-
Steering inputs, and Send now inputs targeting an active turn, appear in
|
|
1516
|
-
`state.messages` after the current response and are omitted from the client
|
|
1517
|
-
inbox. This display survives receipt and hydration, including while paused.
|
|
1518
|
-
They remain pending server inputs until admission and do not enter the model
|
|
1519
|
-
prompt early. Multiple steering inputs follow inbox admission order.
|
|
1520
|
-
|
|
1521
|
-
New active turn identities come from accepted control decisions. A pending Stop
|
|
1522
|
-
for that turn clears the displayed `state.active`, generation, and pending
|
|
1523
|
-
approvals/inputs, and previews an idle status (or paused when the inbox is paused). With
|
|
1524
|
-
`lastSeenIndex`, its retained generation projection freezes at the observed
|
|
1525
|
-
frontier. The preview lasts through the request echo until the controller's
|
|
1526
|
-
decision; rejected requests restore the confirmed active turn and output.
|
|
1527
|
-
Server reads and reducer folds retain confirmed messages and inbox entries;
|
|
1528
|
-
`state.starting` is false there. Custom reducers that call `reduceAIState()` or
|
|
1529
|
-
nest the AI fold do not inherit the client display behavior. Repeat editing and
|
|
1530
|
-
Send now remain available while earlier edits are pending. Later local edits
|
|
1531
|
-
stay visible through earlier commands' acknowledgments and receipts.
|
|
1532
|
-
|
|
1533
|
-
The pending-command ledger survives request echo and retires on the matching
|
|
1534
|
-
`ai.control.decided` receipt or session close. Failed pushes roll back through
|
|
1535
|
-
the client's existing optimistic overlay. This display adds no I/O and does not
|
|
1536
|
-
read raw history.
|
|
1537
|
-
|
|
1538
|
-
### `AIState`
|
|
1539
|
-
|
|
1540
|
-
The AI reducer's `initialState`, `fold`, server `session.state()`, and client
|
|
1541
|
-
snapshots all use `AIState`. Keep state and its log index together when
|
|
1542
|
-
hydrating; do not fold already-seen history twice. `messages` holds
|
|
1543
|
-
[`TranscriptMessage<M>`](#transcriptmessagem) values. Model context has its own
|
|
1544
|
-
built-in fold and schema, which carries the contract's application state.
|
|
1545
|
-
|
|
1546
|
-
Persistence encoding is internal. Tool inputs, outputs, and provider metadata
|
|
1547
|
-
live in message parts; the tool activity list retains only identity and status.
|
|
1548
|
-
No event or tool payload is truncated. `AIState`
|
|
1549
|
-
contains `inbox`, `active`, `starting`, the latest command `receipt`, session lifecycle,
|
|
1550
|
-
messages, generation status, pending approvals
|
|
1551
|
-
and input, tool activity, compaction, usage, the last error, and
|
|
1552
|
-
`activeRequestId`, `activeRequestReason`, `activeRequestSourceGenerationId`, `activeResponseMessageId`,
|
|
1553
|
-
`responseOwners: Record<string, { generationId: string; requestId: string; attempt: number }>`, and terminal request and
|
|
1554
|
-
generation ownership fences.
|
|
1555
|
-
`activeRequestId` is the server-authorized generation request and fences
|
|
1556
|
-
delayed requests before their generation starts. `activeResponseMessageId`
|
|
1557
|
-
identifies the requested response until `activeGeneration` exists.
|
|
1558
|
-
`activeProjection` holds the indexed generation frontier while a generation is
|
|
1559
|
-
active, after a generation step completes while its response waits on tool,
|
|
1560
|
-
approval, or input barriers, while paused, and after a generation fails while it awaits retry
|
|
1561
|
-
or interruption. Response completion, interruption, supersession, retry, or a
|
|
1562
|
-
later generation clears or replaces it. `responseOwners` keeps the
|
|
1563
|
-
latest generation owner for each response message, so late events from a
|
|
1564
|
-
superseded owner cannot alter the projection. `terminalRequestIds` and
|
|
1565
|
-
`terminalGenerations` preserve accepted completion, failure, interruption,
|
|
1566
|
-
and supersession fences across snapshots and recovery. They are optional
|
|
1567
|
-
snapshot-compatible fields with the shapes `Record<string, true>` and
|
|
1568
|
-
`Record<string, 'completed' | 'failed' | 'interrupted' | 'superseded'>`.
|
|
1569
|
-
The active projection also stores its prompt boundary and a serializable stream
|
|
1570
|
-
cursor for open text, reasoning, and tool-input parts. Ordered progress applies
|
|
1571
|
-
only its new chunks. `activeProjection.batches` stores indexed batches as
|
|
1572
|
-
`{ length, tail, blocks }`: a bounded tail and shared completed blocks. Appending
|
|
1573
|
-
ordered progress does not copy the full retained batch list. The blocks are
|
|
1574
|
-
ordinary JSON with logarithmic nesting depth, so checkpoints can be serialized
|
|
1575
|
-
and cloned. Retained batches support exact interruption cutoffs and out-of-order
|
|
1576
|
-
reconstruction. The cursor and blocks share the active projection's lifetime.
|
|
1577
|
-
Checkpoint size and serialization cost still grow with retained progress.
|
|
1578
|
-
Tool-input deltas update `input-streaming` parts with best-effort partial JSON
|
|
1579
|
-
for display. The state stays `input-streaming` until a completed input arrives;
|
|
1580
|
-
partial parsing does not authorize execution or change interruption handling.
|
|
1581
|
-
Application events are ignored by the AI fold.
|
|
1582
|
-
|
|
1583
|
-
### `deriveUIMessages(history)` and `reduceAIState(state, event)`
|
|
1584
|
-
|
|
1585
|
-
`deriveUIMessages()` folds a raw A2 event history directly into
|
|
1586
|
-
`TranscriptMessage<M>[]`, AI SDK messages that include compaction parts. `reduceAIState()` is the event-at-a-time form used by the
|
|
1587
|
-
standard reducer. Both synchronously project the same chunk-only protocol.
|
|
1588
|
-
|
|
1589
|
-
## `experimental-a2/ai/server`
|
|
1590
|
-
|
|
1591
|
-
### `agent(options)`
|
|
1592
|
-
|
|
1593
|
-
```ts
|
|
1594
|
-
agent({
|
|
1595
|
-
name: string, // or contract: agentContract(...), never both
|
|
1596
|
-
events?, messageSchema?, // with name: server-only application events
|
|
1597
|
-
model: LanguageModel | ModelSettings | (context => LanguageModel | ModelSettings),
|
|
1598
|
-
tools?,
|
|
1599
|
-
instructions?,
|
|
1600
|
-
generation?,
|
|
1601
|
-
maxSteps?,
|
|
1602
|
-
messageMetadata?,
|
|
1603
|
-
generate?,
|
|
1604
|
-
compaction?,
|
|
1605
|
-
progress?,
|
|
1606
|
-
toolLane?: string, // run local tool executions one at a time on this lane
|
|
1607
|
-
handlers?, // application event handlers
|
|
1608
|
-
store?, scheduler?, telemetry?,
|
|
1609
|
-
}): Agent<Events, State, Name> // an A2Server
|
|
1610
|
-
```
|
|
1611
|
-
|
|
1612
|
-
Serves an AI agent as one ordinary A2 server: the built-in protocol, its model
|
|
1613
|
-
and tool handlers, and A2's store, scheduler, and telemetry. Pass `name` for the
|
|
1614
|
-
built-in protocol (with optional server-only `events` and `messageSchema`), or a
|
|
1615
|
-
shared `agentContract()` as `contract` when clients fold application state.
|
|
1616
|
-
Any other contract throws a `TypeError` and fails to typecheck; a fully custom
|
|
1617
|
-
contract is core A2, served by `createServer()`. Messages are typed by the
|
|
1618
|
-
contract's `ai.message.created` schema through
|
|
1619
|
-
[`AIMessageOf`](#aimessageofevents).
|
|
1620
|
-
The result is an `A2Server` with `fetch`, `session(id)`, `drain()`, and
|
|
1621
|
-
`contract`. `typeof` it types `useAgent()` and
|
|
1622
|
-
`createAgentClient()` through a type-only import; `Name` is its reducer name,
|
|
1623
|
-
which decides whether the contract-free client accepts it.
|
|
1624
|
-
|
|
1625
|
-
A2 runs one AI SDK `streamText()` model step per durable generation, persists
|
|
1626
|
-
its `UIMessageChunk`s once in durable progress batches, projects messages
|
|
1627
|
-
synchronously, extracts tool and approval lifecycle events, executes local
|
|
1628
|
-
tools, and records completion, usage, interruption, and failure. `model`
|
|
1629
|
-
accepts an AI SDK model string or provider model, or
|
|
1630
|
-
[`ModelSettings`](#modelsettings) to choose its reasoning level and speed with
|
|
1631
|
-
it. `model`, `instructions`, and `generation` can be values or per-generation
|
|
1632
|
-
async resolvers. A resolver that throws, or returns an invalid value, fails the
|
|
1633
|
-
turn once with its message, like a provider error, instead of retrying; the
|
|
1634
|
-
attempt's `ai.generation.started` omits `model` when the model never resolved.
|
|
1635
|
-
`retry()` runs the step again. Store failures (`A2Error`) still retry durably.
|
|
1636
|
-
|
|
1637
|
-
`fetch` accepts reads and pushes of public AI controls:
|
|
1638
|
-
`ai.control.requested` commands other than the server-authored
|
|
1639
|
-
`request-input` and `tool-result`, with
|
|
1640
|
-
`send`, `edit`, and `steer` limited to user messages. With a `contract`, it also
|
|
1641
|
-
accepts the contract's application events, like any A2 server; `events` passed
|
|
1642
|
-
with `name` stay server-only, since no client can push them. Every other pushed
|
|
1643
|
-
event answers `400`. `fetch(request, { authorize })` adds per-operation policy on
|
|
1644
|
-
top, such as who may push which application event. `handlers` handles application events; naming an
|
|
1645
|
-
event the agent handles itself throws a `TypeError` at construction.
|
|
1646
|
-
|
|
1647
|
-
The agent's `telemetry` instruments both Core and AI operations in the same
|
|
1648
|
-
active trace. See the [span catalogue](#the-span-catalogue).
|
|
1649
|
-
|
|
1650
|
-
`generation` contains per-step settings such as `temperature`,
|
|
1651
|
-
`maxOutputTokens`, `topP`, provider options, and tool approval policy. A2 owns
|
|
1652
|
-
the one-step stop condition, local tool execution, and continuation. Responses
|
|
1653
|
-
have no step limit by default. Set `maxSteps` to a positive integer to limit one
|
|
1654
|
-
complete assistant response. Support for individual model settings depends on
|
|
1655
|
-
the selected model and provider. `generation` excludes `stopWhen`, tool
|
|
1656
|
-
execution callbacks, tool callers, sandbox execution, and the tool approval
|
|
1657
|
-
secret. Everything else, including `toolsContext`, passes through to the AI SDK
|
|
1658
|
-
untouched. A local tool reads its durable context with `handlerContext()` when
|
|
1659
|
-
its handler executes. Model-step timeouts remain available; tool-execution
|
|
1660
|
-
timeouts are not.
|
|
1661
|
-
|
|
1662
|
-
A `generation` resolver receives `{ event, messages, state, session, signal }`
|
|
1663
|
-
and returns `AgentGenerationSettings` or a promise. It runs once per attempt
|
|
1664
|
-
before compaction and model dispatch; its settings also govern tool approvals
|
|
1665
|
-
and automatic-compaction compatibility. Static settings are validated at
|
|
1666
|
-
construction, resolved settings before use. Failures and cancellation follow
|
|
1667
|
-
model resolution.
|
|
1668
|
-
|
|
1669
|
-
`messageMetadata(context)` synchronously maps each default AI SDK stream part
|
|
1670
|
-
to the metadata type of the agent's `UIMessage`. It receives the same context
|
|
1671
|
-
as `generate(context)`, plus the current `TextStreamPart` as `part`; its
|
|
1672
|
-
`generation` is the settings the default generator sends, with A2's Gateway
|
|
1673
|
-
defaults applied. Metadata returned for standard `start` and `finish` parts is
|
|
1674
|
-
recorded with generation progress and shallow-merged onto the projected
|
|
1675
|
-
assistant message. Use the `finish` part's `totalUsage` to retain final token
|
|
1676
|
-
usage. A custom `generate` function owns its metadata chunks and does not use
|
|
1677
|
-
this mapper.
|
|
1678
|
-
|
|
1679
|
-
`generate(context)` optionally replaces the default AI SDK generation. It
|
|
1680
|
-
receives the resolver context, `{ event, messages, state, session, signal }`,
|
|
1681
|
-
plus the step's `generationId`, `modelMessages`, and the resolved `model`,
|
|
1682
|
-
`instructions`, `tools`, and `generation` settings. `messages` and
|
|
1683
|
-
`modelMessages` reflect this step's compaction. Its stream writes the assistant
|
|
1684
|
-
message `event.payload.responseMessageId`. It returns exactly one model step as
|
|
1685
|
-
a `ReadableStream<UIMessageChunk>`. A2 continues to own tool execution,
|
|
1686
|
-
continuation, and the durable lifecycle around that stream.
|
|
1687
|
-
|
|
1688
|
-
Generation requests use one durable lane, so model calls for a session do not
|
|
1689
|
-
overlap. Queued-turn policy also waits to schedule a later user message until
|
|
1690
|
-
the active assistant response, including its complete tool loop, reaches a
|
|
1691
|
-
terminal event. The policy matters in addition to lane FIFO because a tool
|
|
1692
|
-
continuation may be appended after the later user message. A generation
|
|
1693
|
-
failure keeps later messages queued until the failed response is explicitly
|
|
1694
|
-
retried or interrupted.
|
|
1695
|
-
|
|
1696
|
-
Independent authorized tool handlers run concurrently without a fixed
|
|
1697
|
-
concurrency limit. `toolLane` names a lane for every local tool execution, so
|
|
1698
|
-
they run one at a time in admission order; model steps keep their own lane. A private coordinator reducer tracks generation closure,
|
|
1699
|
-
cancellation, calls, approvals, and terminal results for the active response.
|
|
1700
|
-
Completed responses do not accumulate in its state. Its
|
|
1701
|
-
`ctx.session.state(coordinator, { through: 'latest' })` reads the durable
|
|
1702
|
-
snapshot plus log tail because these joins intentionally observe concurrent
|
|
1703
|
-
tool results. Process memory is not authoritative. Concurrent join checks return
|
|
1704
|
-
the same deterministic continuation event, so they use `ctx.session.append()`
|
|
1705
|
-
and storage deduplicates the race.
|
|
1706
|
-
|
|
1707
|
-
Tool results and generation completion have no fixed relative order. The
|
|
1708
|
-
continuation predicate needs both the closed model step and every required
|
|
1709
|
-
terminal result.
|
|
1710
|
-
|
|
1711
|
-
Single-owner terminal events use returned-event causality. They commit
|
|
1712
|
-
atomically with parent completion and receive the parent's durable cause.
|
|
1713
|
-
Many-owner joins and preliminary streaming outputs use immediate idempotent
|
|
1714
|
-
appends. Provider calls and external tool side effects are at least once and
|
|
1715
|
-
require their own idempotency. Approved provider-executed calls may satisfy the
|
|
1716
|
-
join from the approval response. Provider tools with declared deferred-result
|
|
1717
|
-
support wait for their provider result instead of a local executor. An
|
|
1718
|
-
authenticated provider callback appends the terminal `ai.tool.result` through
|
|
1719
|
-
the trusted server session API; `fetch` rejects it from browsers.
|
|
1720
|
-
|
|
1721
|
-
`compaction` defaults to automatic for supported Gateway requests. It accepts
|
|
1722
|
-
`false` to disable compaction and discovery, `{ thresholdTokens?, instructions? }`
|
|
1723
|
-
to override automatic behavior, or the existing custom
|
|
1724
|
-
`{ shouldCompact(context), compact(context) }` policy. It also accepts a
|
|
1725
|
-
resolver `(context: AgentResolverContext) => CompactionOptions | Promise<CompactionOptions>`.
|
|
1726
|
-
The resolver runs once per generation and returns a valid policy or `false`.
|
|
1727
|
-
It can await application reducers through `context.session.state(reducer)`, whose
|
|
1728
|
-
default boundary is the captured prompt frontier. Model, instruction, compaction,
|
|
1729
|
-
metadata, and custom-generation contexts expose state access and omit raw history.
|
|
1730
|
-
Callbacks receive prompt messages and the triggering `event`, without an
|
|
1731
|
-
`execution` argument. Model, instruction, and generation-setting resolvers receive
|
|
1732
|
-
`{ event, messages, state, session, signal }`, where `state` is the contract's
|
|
1733
|
-
application state (from `agentContract({ reducer })`, `{}` without one) as of the
|
|
1734
|
-
captured snapshot; it rides in the model-context checkpoint, so it costs no
|
|
1735
|
-
read. Compaction, metadata, and custom-generation callbacks receive the same
|
|
1736
|
-
context plus `modelMessages`, including committed summaries. The contract's reducer continues to expose the full
|
|
1737
|
-
transcript. `context.session.state()` returns its `AIState` directly, paired
|
|
1738
|
-
with its log index for hydration.
|
|
1739
|
-
Static options are validated at construction; resolved options are validated
|
|
1740
|
-
before that generation starts. Each session uses its own resolved value. `thresholdTokens` is an
|
|
1741
|
-
optional positive safe integer. `instructions` adds to A2's internal summary
|
|
1742
|
-
prompt without replacing the agent's instructions.
|
|
1743
|
-
|
|
1744
|
-
Without an explicit threshold, the first generation batches a metadata request
|
|
1745
|
-
into its existing start write. An independent handler reads the public Gateway
|
|
1746
|
-
catalog and records that session/model's limits. It does not delay model dispatch.
|
|
1747
|
-
The default threshold is 75% of the context window, lowered for a larger configured
|
|
1748
|
-
output allowance. A pending or unavailable lookup leaves the threshold unknown;
|
|
1749
|
-
the first request proceeds, and provider context-limit errors fail normally.
|
|
1750
|
-
`state.modelMetadata` retains each model's pending, resolved, or unavailable state.
|
|
1751
|
-
Pending also covers exhausted handler retries; no new request is added per turn.
|
|
1752
|
-
The catalog has a shared one-hour process cache and a five-second fetch timeout.
|
|
1753
|
-
|
|
1754
|
-
Direct/custom providers, fallback model lists, and custom generators need explicit
|
|
1755
|
-
compaction configuration. Provider-executed tools, structured output, and forced
|
|
1756
|
-
tool choices skip implicit compaction and reject explicit automatic policies.
|
|
1757
|
-
Custom policies remain available for these configurations.
|
|
1758
|
-
|
|
1759
|
-
The raw input estimate uses serialized bytes of messages, tools, and instructions,
|
|
1760
|
-
excluding provider options on messages and content parts. These options still reach
|
|
1761
|
-
the model unchanged. It is not a provider tokenizer. A preceding same-model
|
|
1762
|
-
completion anchors later growth to reported input usage, correcting both overestimates
|
|
1763
|
-
and underestimates. A smaller prompt, retry, compaction, or model change stops
|
|
1764
|
-
that calibration.
|
|
1765
|
-
|
|
1766
|
-
Completed generations record the raw `inputTokenEstimate` beside usage.
|
|
1767
|
-
Automatic `ai.compaction.requested` events also record the pre-compaction
|
|
1768
|
-
`inputTokenEstimate`, the calibrated `inputTokens` used for the decision, and
|
|
1769
|
-
`thresholdTokens`. Custom-policy requests omit these diagnostics. They add no
|
|
1770
|
-
model calls or database round trips.
|
|
1771
|
-
|
|
1772
|
-
The built-in summary adds one model request and preserves the request prefix for
|
|
1773
|
-
caching. It does not invoke application generation callbacks, transforms, or tool
|
|
1774
|
-
hooks. Both custom policy callbacks and `generate` receive `modelMessages`, the
|
|
1775
|
-
model-ready prompt including summaries, alongside typed UI `messages`.
|
|
1776
|
-
|
|
1777
|
-
Built-in summaries remain separate from application-typed messages. The
|
|
1778
|
-
transcript marks each compaction with a
|
|
1779
|
-
[`data-a2-compaction` part](#iscompactionpartpart), and
|
|
1780
|
-
`state.compaction?.status === 'running'` reports current activity. The
|
|
1781
|
-
conversation in `state.messages` remains intact. Model execution
|
|
1782
|
-
uses a separate context checkpoint; existing compactions do not require a
|
|
1783
|
-
transcript read. Successful compaction is permanent, including across failure,
|
|
1784
|
-
interruption, retry, and recovery. Retry reruns the failed step from the latest
|
|
1785
|
-
committed context. Compaction defers while tool exchanges or A2-owned tool work
|
|
1786
|
-
remain unsettled, checked with one fresh coordinator state read on each candidate.
|
|
1787
|
-
Unresolved dead-lettered tool work also defers compaction until repair and
|
|
1788
|
-
settlement or session closure. `progress` controls durable batching. By
|
|
1789
|
-
default each batch is written as soon as the previous one finishes, so a slow
|
|
1790
|
-
model's chunks are written one at a time and chunks that arrive during a write
|
|
1791
|
-
share the next batch, up to `maxChunks` (128). `maxDelayMs` (default 0) lets a
|
|
1792
|
-
batch keep collecting chunks for up to that long after the previous write;
|
|
1793
|
-
boundary chunks and `maxChunks` write sooner. Raising it trades token latency
|
|
1794
|
-
for fewer progress events.
|
|
1795
|
-
|
|
1796
|
-
This entry point is server-only and resolves to a throwing browser stub.
|
|
1797
|
-
|
|
1798
|
-
### `ModelSettings`
|
|
1799
|
-
|
|
1800
|
-
```ts
|
|
1801
|
-
type ModelSettings = {
|
|
1802
|
-
model: LanguageModel
|
|
1803
|
-
reasoning?: ReasoningLevel
|
|
1804
|
-
speed?: 'fast' | 'standard'
|
|
1805
|
-
}
|
|
1806
|
-
```
|
|
1807
|
-
|
|
1808
|
-
A model with its reasoning level and speed, which a `model` option or resolver
|
|
1809
|
-
returns to choose all three together. `reasoning` becomes the AI SDK
|
|
1810
|
-
`reasoning` call option. `speed: 'fast'` sets `providerOptions.gateway.speed`
|
|
1811
|
-
to prefer AI Gateway's faster tier when the model offers one, and `'standard'`
|
|
1812
|
-
removes a configured speed. Both override the same settings in `generation`.
|
|
1813
|
-
[`reasoningLevels`](#reasoninglevels) lists the levels.
|
|
1814
|
-
|
|
1815
|
-
### `runWithHandlerContext(ctx, fn)`
|
|
1816
|
-
|
|
1817
|
-
The testing seam for context-reading tools: runs `fn` with `ctx`
|
|
1818
|
-
installed as the ambient handler context, so a tool's `execute` can be
|
|
1819
|
-
called directly in tests. Inside `fn`, `handlerContext()` returns `ctx`
|
|
1820
|
-
with or without a contract argument.
|
|
1821
|
-
|
|
1822
|
-
See [Durable AI agents](/guides/ai-agents) for the protocol and complete
|
|
1823
|
-
examples.
|
|
1824
|
-
|
|
1825
|
-
## `experimental-a2/ai/client`
|
|
1826
|
-
|
|
1827
|
-
### `createAgentClient(options)`
|
|
1828
|
-
|
|
1829
|
-
```ts
|
|
1830
|
-
createAgentClient<typeof agent>(options: {
|
|
1831
|
-
api: ClientApi
|
|
1832
|
-
gcTime?: number
|
|
1833
|
-
participant?: string
|
|
1834
|
-
reconnect?, fetch?, webSocket?, // as createClient
|
|
1835
|
-
}): AgentClient // an A2Client over the built-in AI protocol
|
|
1836
|
-
```
|
|
1837
|
-
|
|
1838
|
-
A contract-free client for an `agent()` route. It accepts only agents whose
|
|
1839
|
-
reducer carries the built-in name (`typeof AI_STATE_REDUCER`): `agent({ name })`
|
|
1840
|
-
or `agent({ contract })` with an `agentContract()` that has no `reducer`. The AI protocol and its
|
|
1841
|
-
`AIState` reducer are library code, and the type argument, taken from a
|
|
1842
|
-
type-only import of the server module, types messages. Use it for pushes
|
|
1843
|
-
outside React, such as a first message sent before navigating, and pass it to
|
|
1844
|
-
`useAgent({ client })` so the hook shares its sessions. It pushes the built-in
|
|
1845
|
-
AI controls only. Agents built from an extended `agentContract()` use
|
|
1846
|
-
`createClient({ contract })` instead, so the client folds the same state.
|
|
1847
|
-
|
|
1848
|
-
## `experimental-a2/ai/react`
|
|
1849
|
-
|
|
1850
|
-
### `useAgent(options)`
|
|
1851
|
-
|
|
1852
|
-
```ts
|
|
1853
|
-
useAgent<typeof agent>(options: {
|
|
1854
|
-
id: string
|
|
1855
|
-
initial?: InitialSnapshot<AIState>
|
|
1856
|
-
participant?: string
|
|
1857
|
-
} & ({ api: string } | { client: AgentClient })): UseAgentResult
|
|
1858
|
-
|
|
1859
|
-
useAgent(options: {
|
|
1860
|
-
id: string
|
|
1861
|
-
initial?: InitialSnapshot<State>
|
|
1862
|
-
participant?: string
|
|
1863
|
-
} & (
|
|
1864
|
-
| { api: string; contract: Contract } // a shared agentContract(), imported for real
|
|
1865
|
-
| { client: A2Client } // createClient({ contract, api })
|
|
1866
|
-
)): UseAgentResult
|
|
1867
|
-
```
|
|
1868
|
-
|
|
1869
|
-
One agent session in React: its live optimistic `AIState` and actions that
|
|
1870
|
-
fill in protocol identities. With `api`, the hook keeps one shared client per
|
|
1871
|
-
route for the page's lifetime, reading `globalThis.fetch` on each request; with
|
|
1872
|
-
`client`, it uses yours, for a custom `fetch`, headers, or `gcTime`. Both forms
|
|
1873
|
-
accept only agents whose reducer carries the built-in name, like
|
|
1874
|
-
`createAgentClient()`. With `contract`, it folds the contract's application
|
|
1875
|
-
state too, keeping one client per contract and route; `state` is
|
|
1876
|
-
`AIState & X` and `push` accepts the contract's events. Pass that contract's
|
|
1877
|
-
`createClient({ contract, api })` as `client` instead to share it with pushes
|
|
1878
|
-
outside React, such as a first message sent before navigating. A custom
|
|
1879
|
-
contract, one not built by `agentContract()`, uses core A2's `useSession()` and
|
|
1880
|
-
`inputs` directly. Keep one options form per call site;
|
|
1881
|
-
the form must not change between renders.
|
|
1882
|
-
|
|
1883
|
-
The result is the session result (`state`, `index`, `connection`, `push`,
|
|
1884
|
-
`events`, `hydrated`, `reconnect`, `loadHistory`, `history`) plus:
|
|
1885
|
-
|
|
1886
|
-
| Member | Does |
|
|
1887
|
-
| --- | --- |
|
|
1888
|
-
| `messages` | `state.messages` |
|
|
1889
|
-
| `status` | `state.status` |
|
|
1890
|
-
| `indicator` | `'starting'`, `'thinking'`, `'compacting'`, or `null` |
|
|
1891
|
-
| `sendMessage(message, { generate? })` | queues a string (one text part) or a `UIMessage` |
|
|
1892
|
-
| `steer(message)` | steers the active turn after its current step, or sends when idle |
|
|
1893
|
-
| `stop()` | stops the active turn at the rendered frontier |
|
|
1894
|
-
| `approve(approval)`, `deny(approval, { reason? })` | answers a pending approval |
|
|
1895
|
-
| `respond(input, value)` | answers a pending application input |
|
|
1896
|
-
| `retry()` | retries the failed response with a fresh `retryId` |
|
|
1897
|
-
| `pause({ when })`, `resume()` | pauses and resumes |
|
|
1898
|
-
| `queue.edit(message)`, `queue.move({ inputId, beforeId })`, `queue.remove(inputId)`, `queue.sendNow(inputId)` | edits the inbox |
|
|
1899
|
-
|
|
1900
|
-
Every action returns a `PushResult`, like `push()`: it resolves to the
|
|
1901
|
-
persisted command events and rejects when the push fails, and `.confirmed`
|
|
1902
|
-
resolves when the stream delivers them back. Each event's `id` is the
|
|
1903
|
-
`commandId` of its `ai.control.decided` receipt. Actions read identities from
|
|
1904
|
-
the state of the render that created them, so `stop()` and `queue.sendNow()`
|
|
1905
|
-
carry the frontier the user saw. An action with nothing to address, such as
|
|
1906
|
-
`stop()` without an active turn or `retry()` without a failed one, resolves to
|
|
1907
|
-
an empty array. Actions are rebuilt on every render; their identity is not
|
|
1908
|
-
stable.
|
|
1909
|
-
|
|
1910
|
-
`indicator` is `starting` for an optimistic turn awaiting acceptance,
|
|
1911
|
-
`compacting` while a summary runs, `thinking` while the current model step has
|
|
1912
|
-
no visible part (reasoning and step boundaries do not count), and `null`
|
|
1913
|
-
otherwise.
|
|
1914
|
-
|
|
1915
|
-
## `experimental-a2/client`
|
|
1916
|
-
|
|
1917
|
-
```ts
|
|
1918
|
-
createClient(options: {
|
|
1919
|
-
contract: Contract // requires a public reducer
|
|
1920
|
-
api: ClientApi
|
|
1921
|
-
gcTime?: number // idle session lifetime; 5 minutes by default
|
|
1922
|
-
participant?: string // default presence identity for every session
|
|
1923
|
-
reconnect?: (failure: {
|
|
1924
|
-
sessionId: string
|
|
1925
|
-
error: A2ClientError | null
|
|
1926
|
-
attempt: number
|
|
1927
|
-
}) => number | false
|
|
1928
|
-
}): A2Client
|
|
1929
|
-
|
|
1930
|
-
type ClientApi =
|
|
1931
|
-
| SessionUrl // one route: GET SSE stream + POST push
|
|
1932
|
-
| { type: 'http'; push: SessionUrl; stream: SessionUrl }
|
|
1933
|
-
| { type: 'ws'; url: SessionUrl; multiplex?: false }
|
|
1934
|
-
| { type: 'ws'; url: string; multiplex: true }
|
|
1935
|
-
|
|
1936
|
-
type SessionUrl = string | ((sessionId: string) => string)
|
|
1937
|
-
```
|
|
1938
|
-
|
|
1939
|
-
`reconnect` runs after a failed connection or a stream ending, including a
|
|
1940
|
-
clean end with `error: null`. Return the delay in milliseconds before the next
|
|
1941
|
-
attempt, or `false` to pause automatic reconnection. Delays must be finite,
|
|
1942
|
-
non-negative, and at most 2,147,483,647 ms. The callback is synchronous; a
|
|
1943
|
-
throw or invalid result pauses the connection with an `A2ClientError`.
|
|
1944
|
-
|
|
1945
|
-
`attempt` starts at 1 for the first retry. It counts consecutive attempts
|
|
1946
|
-
per session and resets when the stream becomes live or `session.reconnect()`
|
|
1947
|
-
explicitly restarts it. It is separate from the lifetime `reconnects` count.
|
|
1948
|
-
Without a callback, A2 retries indefinitely with delays starting at 500 ms,
|
|
1949
|
-
doubling up to five seconds. The callback does not run for an explicit
|
|
1950
|
-
restart, an idle-stream refresh, or a caller closing the stream.
|
|
1951
|
-
|
|
1952
|
-
While paused, `connection.status` is `paused` and its last error remains
|
|
1953
|
-
available. State, hydration, optimistic writes, and connection leases remain
|
|
1954
|
-
intact. Another consumer taking a lease does not resume a paused stream.
|
|
1955
|
-
Call `session.reconnect()` to resume immediately with a fresh attempt count.
|
|
1956
|
-
Releasing the last lease still closes the stream. A later first lease starts
|
|
1957
|
-
a new connection. Push retries are independent of this policy; a push does
|
|
1958
|
-
not resume a paused stream. An acknowledged push's `confirmed` promise waits
|
|
1959
|
-
until the stream delivers through the acknowledged index. A bootstrap
|
|
1960
|
-
snapshot, or a stream that starts after a supplied initial index, delivers
|
|
1961
|
-
through that index. Hydration does not otherwise confirm an unobserved
|
|
1962
|
-
acknowledgement. A push never waits for bootstrap. An HTTP push does not
|
|
1963
|
-
wait for a pending initial Promise either; a WebSocket push rides its
|
|
1964
|
-
connection, which opens once that Promise settles.
|
|
1965
|
-
Resumption preserves the stream's own position, even when
|
|
1966
|
-
hydration has advanced state while paused.
|
|
1967
|
-
|
|
1968
|
-
A `SessionUrl` function runs for each HTTP request or WebSocket connection.
|
|
1969
|
-
Use it for session-specific paths. A2 still adds its `sessionId` and read-bound
|
|
1970
|
-
query parameters. The function does not authorize that ID. Bind any path scope
|
|
1971
|
-
or URL token to `operation.sessionId` in `authorize`; keep URL tokens short-lived
|
|
1972
|
-
because URLs can appear in infrastructure logs. A multiplexed WebSocket URL is
|
|
1973
|
-
a string because its connection carries more than one session.
|
|
1974
|
-
|
|
1975
|
-
The framework-agnostic session client `experimental-a2/react` is built on: the SSE
|
|
1976
|
-
subscription with frontier resume and reconnection, the optimistic push
|
|
1977
|
-
queue with ack/rollback, and the local fold. `client.session(id, {
|
|
1978
|
-
initial?, initialEvents?, participant? })` returns a
|
|
1979
|
-
handle with `getSnapshot()`/`subscribe()` (the `useSyncExternalStore`
|
|
1980
|
-
contract), `push()`, `loadHistory()`, `connect()`, `reconnect()`, and `close()`.
|
|
1981
|
-
`connect()` takes a lease on the live stream and returns its release:
|
|
1982
|
-
leases refcount per handle (the first connects, releasing the last
|
|
1983
|
-
closes, releasing twice is a no-op), so independent consumers of one
|
|
1984
|
-
identity-mapped handle never fight over the stream. `close()` is the
|
|
1985
|
-
hard stop: it drops every outstanding lease and closes now; a later
|
|
1986
|
-
`connect()` starts fresh.
|
|
1987
|
-
Snapshots carry `state`, `events`, `index`, `history`, and `connection`
|
|
1988
|
-
(the same fields `useSession` exposes), and `push` returns the same
|
|
1989
|
-
ack-then-`confirmed` result. On contracts that declare `presence` the handle also carries
|
|
1990
|
-
`setPresence()` and snapshots carry the `presence` map, exactly like
|
|
1991
|
-
the hook; `participant` is the identity `setPresence` sends under:
|
|
1992
|
-
stated once on `createClient` as the default for every handle, or per
|
|
1993
|
-
session as the override. Use
|
|
1994
|
-
it directly from any other framework, or none.
|
|
1995
|
-
|
|
1996
|
-
Within one `A2Client`, repeated `session(id)` calls return the same live
|
|
1997
|
-
handle. A later call with a further server-rendered frontier advances the
|
|
1998
|
-
handle and does not drop pending optimistic events; a stale render cannot move
|
|
1999
|
-
it backward. Optional `initialEvents` seed earlier raw events for history UIs
|
|
2000
|
-
and merge into that feed. They do not affect the folded state or stream
|
|
2001
|
-
frontier. Idle handles are evicted after `gcTime`. This memory layer is
|
|
2002
|
-
separate from `experimental-a2/cache-indexeddb`: memory preserves identity
|
|
2003
|
-
across route transitions, while IndexedDB preserves the replica across reloads.
|
|
2004
|
-
|
|
2005
|
-
`initial` accepts the same `InitialSnapshot<S>` as React. A pending Promise
|
|
2006
|
-
delays cold connection startup, including a connection acquired by `push`.
|
|
2007
|
-
An HTTP push is sent without waiting; a WebSocket push waits for that
|
|
2008
|
-
connection. It does not start another request. Replacing the source or calling `close()`
|
|
2009
|
-
invalidates its pending result and any deferred adoption behind a write.
|
|
2010
|
-
Releasing the last connection lease does not cancel the caller's Promise;
|
|
2011
|
-
it can still seed cached data without reopening transport. Idle eviction
|
|
2012
|
-
detaches A2's listener. Repeating the same Promise within that cached runtime
|
|
2013
|
-
does not restart cancelled work.
|
|
2014
|
-
|
|
2015
|
-
## `experimental-a2/cache-indexeddb`
|
|
2016
|
-
|
|
2017
|
-
```ts
|
|
2018
|
-
indexedDb(): A2ClientCache
|
|
2019
|
-
```
|
|
2020
|
-
|
|
2021
|
-
A browser-side cache for `SessionProvider`: the session's events
|
|
2022
|
-
(immutable; writes are idempotent by index), a folded snapshot
|
|
2023
|
-
(invalidated by a reducer rename), and the pending-push queue.
|
|
2024
|
-
`cache.clear(sessionId?)` is the logout story. Create instances in client
|
|
2025
|
-
code; they're live objects, not serializable props. See
|
|
2026
|
-
[Local-first](/guides/local-first).
|
|
2027
|
-
|
|
2028
|
-
## `experimental-a2/otel`
|
|
2029
|
-
|
|
2030
|
-
```ts
|
|
2031
|
-
otel(options?: { tracer?: Tracer; tracerName?: string }): A2Telemetry
|
|
2032
|
-
```
|
|
2033
|
-
|
|
2034
|
-
OpenTelemetry instrumentation for a server (`@opentelemetry/api` is a
|
|
2035
|
-
peer dependency). Pass it as `telemetry`; spans nest through the active
|
|
2036
|
-
context, so the happy path lands in one trace: request → append → drain →
|
|
2037
|
-
each handler → the events those handlers append.
|
|
2038
|
-
|
|
2039
|
-
### The span catalogue
|
|
2040
|
-
|
|
2041
|
-
All spans carry `a2.contract` and `a2.session_id`. Core spans describe the
|
|
2042
|
-
durable runtime; `a2.ai.*` spans describe the built-in AI handlers:
|
|
2043
|
-
|
|
2044
|
-
| Layer | Span | Wraps |
|
|
2045
|
-
| ----- | ---- | ----- |
|
|
2046
|
-
| Core | `a2.append` | validation + the store write (one batch) |
|
|
2047
|
-
| Core | `a2.drain` | one drain pass, including concurrent handlers |
|
|
2048
|
-
| Core | `a2.event` | one claimed dispatch, including its handler and completion |
|
|
2049
|
-
| Core | `a2.claim` | one `claimAvailable` store operation |
|
|
2050
|
-
| Core | `a2.commit` | one `completeAttempt` or `failAttempt` store operation |
|
|
2051
|
-
| Core | `a2.state` | one `state()` read: snapshot-plus-tail load + reducer fold |
|
|
2052
|
-
| Core | `a2.snapshot` | one background batch of checkpoint writes |
|
|
2053
|
-
| AI | `a2.ai.control` | applying an input/work report/session-close decision and preparing its returned events |
|
|
2054
|
-
| AI | `a2.ai.generate` | one generation handler, including Core calls and model/SDK work |
|
|
2055
|
-
| AI | `a2.ai.context` | constructing model messages, including tool prompt reconstruction when uncached |
|
|
2056
|
-
| AI | `a2.ai.compaction` | automatic summarization or the custom compaction callback |
|
|
2057
|
-
| AI | `a2.ai.tool` | one durable tool handler, including application execution |
|
|
2058
|
-
| AI | `a2.ai.ack` | waiting for a persisted work report's canonical events and control receipt |
|
|
2059
|
-
|
|
2060
|
-
Durations measure elapsed time, including nested work: `a2.event` contains its AI
|
|
2061
|
-
handler and later commit, AI handlers call Core, and `a2.state` includes reducer code.
|
|
2062
|
-
Acknowledgements overlap control processing; claims can cover several events,
|
|
2063
|
-
and snapshots run in the background. Summing spans or subtracting summed children
|
|
2064
|
-
does not yield exclusive Core/AI overhead. Model and application I/O is included;
|
|
2065
|
-
inspect its child spans for detail.
|
|
2066
|
-
|
|
2067
|
-
Attributes arrive in two waves: **at start** (passed to `span()`) and
|
|
2068
|
-
**mid-span** (via `handle.setAttribute`; outcomes and counts aren't
|
|
2069
|
-
known until the work ran). A recorder must merge both; the ones you
|
|
2070
|
-
alert on are all mid-span.
|
|
2071
|
-
|
|
2072
|
-
| Attribute | Span | When | Values |
|
|
2073
|
-
| --------------------- | ----------- | ----- | ----------------------------------------------------- |
|
|
2074
|
-
| `a2.append.source` | `a2.append` | start | `external` \| `handler` |
|
|
2075
|
-
| `a2.append.mode` | `a2.append` | start | `inline` \| `dispatch` |
|
|
2076
|
-
| `a2.append.types` | `a2.append` | start | comma-joined event types |
|
|
2077
|
-
| `a2.append.count` | `a2.append` | start | batch size |
|
|
2078
|
-
| `a2.append.armed` | `a2.append` | mid | `false` when the scheduler arm failed and this append degraded to append-driven healing |
|
|
2079
|
-
| `a2.drain.outcome` | `a2.drain` | mid | `settled` \| `busy` \| `stalled` |
|
|
2080
|
-
| `a2.drain.processed` | `a2.drain` | mid | events processed this pass |
|
|
2081
|
-
| `a2.claim.outcome` | `a2.claim` | mid | `claimed` \| `busy` \| `settled` |
|
|
2082
|
-
| `a2.claim.count` | `a2.claim` | mid | number of claimed events, or zero |
|
|
2083
|
-
| `a2.commit.operation` | `a2.commit` | start | `complete` \| `fail` |
|
|
2084
|
-
| `a2.commit.index` | `a2.commit` | start | event log position |
|
|
2085
|
-
| `a2.commit.attempt` | `a2.commit` | start | durable 1-based attempt ordinal |
|
|
2086
|
-
| `a2.commit.outcome` | `a2.commit` | mid | `completed` \| `failed` \| `dead_lettered` \| `superseded` |
|
|
2087
|
-
| `a2.event.type` | `a2.event` | start | the event's type |
|
|
2088
|
-
| `a2.event.index` | `a2.event` | start | log position |
|
|
2089
|
-
| `a2.event.id` | `a2.event` | start | event id |
|
|
2090
|
-
| `a2.event.attempt` | `a2.event` | start | same durable 1-based ordinal as `ctx.attempt` |
|
|
2091
|
-
| `a2.event.lane` | `a2.event` | start | stored lane value; absent for concurrent unlaned work |
|
|
2092
|
-
| `a2.event.handled` | `a2.event` | start | normally `true`; `false` when a custom-adapter row has no handler |
|
|
2093
|
-
| `a2.event.outcome` | `a2.event` | mid | `processed` \| `failed` \| `dead_lettered` \| `superseded` \| `surrendered` |
|
|
2094
|
-
| `a2.event.aborted` | `a2.event` | mid | `true` when `abortOn` fired during the run |
|
|
2095
|
-
| `a2.state.reducer` | `a2.state` | start | the reducer's name |
|
|
2096
|
-
| `a2.state.snapshot` | `a2.state` | mid | `hit` \| `miss` \| `rejected` (schema guard discarded it) \| `memory` (served from the server's in-memory state) |
|
|
2097
|
-
| `a2.state.folded` | `a2.state` | mid | events folded past the snapshot |
|
|
2098
|
-
| `a2.state.index` | `a2.state` | mid | the frontier the returned state reflects |
|
|
2099
|
-
| `a2.snapshot.reducer` | `a2.snapshot` | start | the reducer's name |
|
|
2100
|
-
| `a2.snapshot.index` | `a2.snapshot` | start | greatest checkpoint index in the write batch |
|
|
2101
|
-
| `a2.snapshot.count` | `a2.snapshot` | start | number of checkpoints in the write batch |
|
|
2102
|
-
| `a2.ai.control.type` | `a2.ai.control` | start | `ai.control.requested` \| `ai.work.reported` \| `ai.session.closed` |
|
|
2103
|
-
| `a2.ai.control.id` | `a2.ai.control` | start | triggering event id |
|
|
2104
|
-
| `a2.ai.control.attempt` | `a2.ai.control` | start | durable 1-based attempt ordinal |
|
|
2105
|
-
| `a2.ai.generate.request_id` | `a2.ai.generate` | start | generation request event id |
|
|
2106
|
-
| `a2.ai.generate.attempt` | `a2.ai.generate` | start | durable 1-based attempt ordinal |
|
|
2107
|
-
| `a2.ai.context.request_id` | `a2.ai.context` | start | generation request event id |
|
|
2108
|
-
| `a2.ai.context.phase` | `a2.ai.context` | start | `generation` \| `refresh` \| `tool` |
|
|
2109
|
-
| `a2.ai.compaction.request_id` | `a2.ai.compaction` | start | generation request event id |
|
|
2110
|
-
| `a2.ai.compaction.generation_id` | `a2.ai.compaction` | start | generation attempt id |
|
|
2111
|
-
| `a2.ai.compaction.policy` | `a2.ai.compaction` | start | `automatic` \| `custom` |
|
|
2112
|
-
| `a2.ai.tool.request_id` | `a2.ai.tool` | start | originating generation request event id |
|
|
2113
|
-
| `a2.ai.tool.call_id` | `a2.ai.tool` | start | tool call id |
|
|
2114
|
-
| `a2.ai.tool.name` | `a2.ai.tool` | start | tool name |
|
|
2115
|
-
| `a2.ai.tool.attempt` | `a2.ai.tool` | start | durable 1-based attempt ordinal |
|
|
2116
|
-
| `a2.ai.ack.request_id` | `a2.ai.ack` | start | reporting worker event id |
|
|
2117
|
-
| `a2.ai.ack.report_id` | `a2.ai.ack` | start | work report event id, matching the control span's id |
|
|
2118
|
-
|
|
2119
|
-
Drain outcomes: `settled` means nothing actionable is left; `busy` means live
|
|
2120
|
-
per-event claims remain; `stalled` means a caught failure needs a later retry.
|
|
2121
|
-
A dead-lettered event blocks only later events in its lane. Other lanes remain
|
|
2122
|
-
eligible.
|
|
2123
|
-
|
|
2124
|
-
A failing handler marks its `a2.event` span with the exception and
|
|
2125
|
-
error status; `a2.event.outcome = dead_lettered` is the attribute to
|
|
2126
|
-
alert on. `superseded` means a stale completion or failure did not change the
|
|
2127
|
-
log.
|
|
2128
|
-
`A2Telemetry` is a one-method interface; anything with
|
|
2129
|
-
`span(name, attributes, fn)` works;
|
|
2130
|
-
`otel()` is simply the adapter A2 ships. This catalogue is a contract:
|
|
2131
|
-
renames and additions are breaking for dashboards, and are called out in the
|
|
2132
|
-
package's `CHANGELOG.md`.
|
|
2133
|
-
|
|
2134
|
-
## `experimental-a2/scheduler-vercel`
|
|
2135
|
-
|
|
2136
|
-
`vercelQueues(options?)` returns an `A2Scheduler` backed by Vercel Queues.
|
|
2137
|
-
|
|
2138
|
-
```ts server/scheduler-vercel.ts
|
|
2139
|
-
import { vercelQueues } from 'experimental-a2/scheduler-vercel'
|
|
2140
|
-
|
|
2141
|
-
export const scheduler = vercelQueues()
|
|
2142
|
-
```
|
|
2143
|
-
|
|
2144
|
-
| Option | Type | Default |
|
|
2145
|
-
| --- | --- | --- |
|
|
2146
|
-
| `topic` | `string` matching `[A-Za-z0-9_-]+` | `'a2'` |
|
|
2147
|
-
| `deploymentId` | `string \| null` | `VERCEL_DEPLOYMENT_ID` |
|
|
2148
|
-
| `transport` | `QueueTransport` | lazy `@vercel/queue` client |
|
|
2149
|
-
| `clock` | `{ now(): Date }` | system clock |
|
|
2150
|
-
|
|
2151
|
-
`deploymentId` is passed to the Queue SDK unchanged and applies to every task,
|
|
2152
|
-
including recovery watchdogs. Omit it to use `VERCEL_DEPLOYMENT_ID`, pass a
|
|
2153
|
-
deployment ID to target that deployment, or pass `null` for deploymentless
|
|
2154
|
-
delivery. Publishing and consuming clients must use the same deployment scope.
|
|
2155
|
-
The same configured client handles sends, payload fetches, acknowledgements,
|
|
2156
|
-
and visibility changes. Callback contents never choose the client.
|
|
2157
|
-
|
|
2158
|
-
Pinned tasks stay with their target deployment after another deployment is
|
|
2159
|
-
promoted. Deploymentless delivery lets Vercel choose an eligible consumer; it
|
|
2160
|
-
does not select the latest production deployment. Every eligible consumer must
|
|
2161
|
-
use `deploymentId: null` and remain compatible with pending tasks and session
|
|
2162
|
-
work.
|
|
2163
|
-
|
|
2164
|
-
The adapter retains every task for seven days and accepts delays through six
|
|
2165
|
-
days, leaving the final day for delivery and retries. `transport` and `clock`
|
|
2166
|
-
are advanced injection seams for tests and custom delivery. An injected
|
|
2167
|
-
transport receives the topic, an exact `SchedulerTask`, and send options.
|
|
2168
|
-
`deploymentId` configures the built-in Queue SDK client only; an injected
|
|
2169
|
-
transport owns its provider configuration.
|
|
2170
|
-
|
|
2171
|
-
## `experimental-a2/scheduler-qstash`
|
|
2172
|
-
|
|
2173
|
-
`qstash(options?)` returns an `A2Scheduler` backed by QStash HTTP delivery.
|
|
2174
|
-
Vercel Queues remains the recommended default on Vercel. Use QStash when you
|
|
2175
|
-
want a scheduler that calls a stable public endpoint.
|
|
2176
|
-
|
|
2177
|
-
```ts server/scheduler-qstash.ts
|
|
2178
|
-
import { qstash } from 'experimental-a2/scheduler-qstash'
|
|
2179
|
-
|
|
2180
|
-
export const scheduler = qstash()
|
|
2181
|
-
```
|
|
2182
|
-
|
|
2183
|
-
| Option | Type | Default |
|
|
2184
|
-
| --- | --- | --- |
|
|
2185
|
-
| `url` | `string` | conventional local or Vercel callback; required for process-less runtimes, other production, and custom routes |
|
|
2186
|
-
| `token` | `string` | `QSTASH_TOKEN` |
|
|
2187
|
-
| `currentSigningKey` | `string` | `QSTASH_CURRENT_SIGNING_KEY` |
|
|
2188
|
-
| `nextSigningKey` | `string` | `QSTASH_NEXT_SIGNING_KEY` |
|
|
2189
|
-
| `headers` | `Readonly<Record<string, string>>` | no additional callback headers |
|
|
2190
|
-
| `client` | `QStashPublishingClient` | lazy `@upstash/qstash` client |
|
|
2191
|
-
| `retries` | non-negative integer | the QStash provider default |
|
|
2192
|
-
| `maxBodyBytes` | positive integer | `1_048_576` (1 MiB) for callbacks and reserved outbound bodies |
|
|
2193
|
-
| `transport` | `QStashTransport` | lazy `@upstash/qstash` client and receiver |
|
|
2194
|
-
| `clock` | `{ now(): Date }` | system clock |
|
|
2195
|
-
|
|
2196
|
-
`headers` adds static headers to every callback delivery, including recovery
|
|
2197
|
-
successors. A2 validates and snapshots the record when `qstash()` runs, then
|
|
2198
|
-
asks QStash to redact every configured value from its dashboard and API. QStash
|
|
2199
|
-
still receives, stores, and delivers the original values. Header names are
|
|
2200
|
-
case-insensitive. Provider-owned `Upstash-*` headers, `Content-Type*`, HTTP
|
|
2201
|
-
framing headers, and A2's managed Vercel identity headers are reserved.
|
|
2202
|
-
|
|
2203
|
-
`client` accepts the structural `publishJSON(...)` part of a configured
|
|
2204
|
-
`@upstash/qstash` `Client`. A2 still creates the `Receiver` lazily from the
|
|
2205
|
-
signing-key options. `client` cannot be combined with `token` or `transport`.
|
|
2206
|
-
Use it to configure the QStash API endpoint, publish-request retry policy, or
|
|
2207
|
-
telemetry. Use A2's `headers` option for callback credentials. Client-global
|
|
2208
|
-
callback headers and redaction are outside A2's merge contract. The injected
|
|
2209
|
-
client receives raw callback credentials, including an automatic preview
|
|
2210
|
-
identity, and must honor the per-publish redaction request.
|
|
2211
|
-
|
|
2212
|
-
`transport` and `clock` are advanced injection seams for tests and custom
|
|
2213
|
-
delivery. A `QStashTransport` implements `publish(request)` and
|
|
2214
|
-
`verify(request)`. Publish requests contain the callback `url`, a complete
|
|
2215
|
-
`SchedulerTask` body, optional callback `headers`, optional `redact.header`
|
|
2216
|
-
names, optional Unix-seconds `notBefore`, and a deterministic
|
|
2217
|
-
`deduplicationId`. Store-outage successors add provider-private generation
|
|
2218
|
-
metadata to that body. Custom transports must preserve the body, deliver every
|
|
2219
|
-
header, and apply every redaction request. An explicit `retries` value is
|
|
2220
|
-
forwarded; omission lets QStash use its provider default. Verify requests
|
|
2221
|
-
contain the signature, raw body, exact callback URL, and optional Upstash
|
|
2222
|
-
region.
|
|
2223
|
-
|
|
2224
|
-
An explicit `url` always wins. Otherwise, Vercel production resolves to
|
|
2225
|
-
`https://${VERCEL_PROJECT_PRODUCTION_URL}/api/a2/scheduler` and preview resolves
|
|
2226
|
-
to `https://${VERCEL_BRANCH_URL}/api/a2/scheduler`. Outside those deployments,
|
|
2227
|
-
a runtime that exposes `process.env` and is not in `NODE_ENV=production`
|
|
2228
|
-
resolves to `http://localhost:${PORT ?? 3000}/api/a2/scheduler` and passes
|
|
2229
|
-
`devMode: true` to the SDK client and receiver. `QSTASH_DEV` is not required.
|
|
2230
|
-
`PORT` must be an integer from 1 through 65535. `QSTASH_DEV_PORT` configures the
|
|
2231
|
-
local QStash server, not the application callback. Automatic startup requires
|
|
2232
|
-
`@upstash/qstash` 2.11.0 or newer in a Node.js runtime. The lazy transport
|
|
2233
|
-
starts it on the first publish or verification. The direct SDK override means
|
|
2234
|
-
`QSTASH_DEV=false` does not disable an inferred local route.
|
|
2235
|
-
|
|
2236
|
-
A non-Vercel production runtime or a process-less runtime requires an explicit
|
|
2237
|
-
absolute URL. Pass one for a custom host or path, an application port not
|
|
2238
|
-
available through `PORT`, or Docker networking. An Edge runtime that exposes
|
|
2239
|
-
`process.env` can infer the local URL, but it must start the QStash development
|
|
2240
|
-
server separately. Outside production, an explicit URL does not override the
|
|
2241
|
-
SDK's development mode, so `QSTASH_DEV` and manually supplied `QSTASH_URL` plus
|
|
2242
|
-
credentials retain their SDK-defined behavior. A2 passes `devMode: false` in
|
|
2243
|
-
production. The inferred Vercel hosts are moving aliases, so pending tasks can
|
|
2244
|
-
reach later deployments on the same production or branch URL.
|
|
2245
|
-
|
|
2246
|
-
Mount the application handler once and pass every server that shares the
|
|
2247
|
-
scheduler:
|
|
2248
|
-
|
|
2249
|
-
```ts app/api/a2/scheduler/route.ts
|
|
2250
|
-
import { billingServer, ordersServer, scheduler } from '@/server'
|
|
2251
|
-
|
|
2252
|
-
export const POST = scheduler.handler(ordersServer, billingServer)
|
|
2253
|
-
```
|
|
2254
|
-
|
|
2255
|
-
The route must be reachable by the selected QStash server after any platform
|
|
2256
|
-
edge authentication. The development server calls localhost directly. The
|
|
2257
|
-
adapter reads its raw body once and verifies `Upstash-Signature`, the exact URL,
|
|
2258
|
-
and `Upstash-Region` before it parses the delivery or touches a server. Missing
|
|
2259
|
-
or invalid signatures return 401, so ordinary unsigned traffic cannot execute
|
|
2260
|
-
scheduler work. A signed malformed envelope returns 489 with
|
|
2261
|
-
`Upstash-NonRetryable-Error: true`. The same non-retryable response applies
|
|
2262
|
-
when the declared or streamed body exceeds `maxBodyBytes`. Before an initial
|
|
2263
|
-
publish, the adapter checks the normalized task with enough reserved room for
|
|
2264
|
-
its largest possible private store-outage lineage. Append and clear tasks
|
|
2265
|
-
reserve a maximum safe-integer generation. Drain tasks also reserve their actual root
|
|
2266
|
-
`dueAt`. Store-outage successors check their actual body. The adapter therefore
|
|
2267
|
-
cannot initially accept a task that would exceed the same limit solely when
|
|
2268
|
-
lineage is added. Raise the option to match a larger paid-plan payload limit
|
|
2269
|
-
when needed. Keep one value across every deployment that can receive this
|
|
2270
|
-
callback URL. A lower value deployed while older tasks remain can return a
|
|
2271
|
-
non-retryable 489 for a task accepted by the previous value.
|
|
2272
|
-
|
|
2273
|
-
For an inferred Vercel preview callback, the adapter reads the current
|
|
2274
|
-
invocation's `x-vercel-oidc-token` for each publish. When it is a parseable JWT
|
|
2275
|
-
whose expiration remains at least 60 seconds after the task's first due time,
|
|
2276
|
-
the adapter supplies it as `x-vercel-trusted-oidc-idp-token` and requests QStash
|
|
2277
|
-
header redaction. A matching Vercel Trusted Sources rule can then admit the
|
|
2278
|
-
first delivery. QStash receives the credential even though its dashboard and
|
|
2279
|
-
API redact it.
|
|
2280
|
-
|
|
2281
|
-
This is opportunistic, not durable. Missing, malformed, and near-expiry tokens
|
|
2282
|
-
fall back to an ordinary publish, preserving public-preview behavior. An
|
|
2283
|
-
explicit `url` opts out. Inferred production and local callbacks never forward
|
|
2284
|
-
the token. QStash stores the static header and reuses it on retries, so delayed
|
|
2285
|
-
tasks and retries can outlive it. Use an explicit automation bypass or public
|
|
2286
|
-
ingress when every protected delivery must remain authorized. Applications can
|
|
2287
|
-
supply a dedicated bypass through `headers`; A2 requests redaction for its
|
|
2288
|
-
value, but pending messages retain the captured credential after rotation. A
|
|
2289
|
-
delivery-time Vercel relay remains a separate application architecture that
|
|
2290
|
-
requires a Trusted Sources rule, strict target allowlisting, and a versioned
|
|
2291
|
-
routing envelope. QStash signature verification remains mandatory after the
|
|
2292
|
-
edge-access check.
|
|
2293
|
-
|
|
2294
|
-
Settled deliveries return 200. Busy deliveries await a durable successor
|
|
2295
|
-
watchdog before returning 200. Stalled deliveries publish a fresh successor
|
|
2296
|
-
before returning 200, which resets the per-message retry budget. A retryable
|
|
2297
|
-
store outage also hands drain, append, and clear tasks to a fresh successor
|
|
2298
|
-
before acknowledging. Store-outage successors wait 30 seconds, then multiply the delay
|
|
2299
|
-
by four for each signed generation, capped at one day. Their generation keeps
|
|
2300
|
-
increasing after the cap so each handoff has a new deduplication id. Drain
|
|
2301
|
-
lineage includes the first failing task's `dueAt`, contract, and session.
|
|
2302
|
-
Append and clear lineage includes the task id, contract, and session. Duplicate delivery of one
|
|
2303
|
-
generation therefore publishes the same successor key without merging a later
|
|
2304
|
-
drain episode. Append tasks otherwise return 200 after their ordinary fixed-id
|
|
2305
|
-
append succeeds and any recovery watchdog for pending handler work is durable.
|
|
2306
|
-
Clear tasks return 200 after their conditional clear and its deletion finish.
|
|
2307
|
-
Unknown contracts, missing scheduler configuration, schema failures, and
|
|
2308
|
-
successor publish failures keep the current delivery retryable. Delivery is at
|
|
2309
|
-
least once, so handler side effects still need idempotency keys.
|
|
2310
|
-
|
|
2311
|
-
The adapter leaves `retries` unset by default so QStash applies its provider
|
|
2312
|
-
default. An explicit non-negative value is passed through. Every delivery
|
|
2313
|
-
attempt is billable. A prolonged outage in which QStash cannot reach the
|
|
2314
|
-
callback still requires DLQ monitoring and redrive because no running callback
|
|
2315
|
-
can publish a successor. QStash's
|
|
2316
|
-
documented 10-minute deduplication window can reduce duplicate watchdogs but is
|
|
2317
|
-
not part of A2's correctness model. Stable scheduled-event ids also make timer
|
|
2318
|
-
redelivery safe after that window. One-shot delays are currently capped at 7
|
|
2319
|
-
days on free plans and 1 year on usage-based plans, with no fixed maximum on
|
|
2320
|
-
fixed plans. Leave at least one second of margin at a plan maximum because
|
|
2321
|
-
`notBefore` uses whole Unix seconds. The adapter carries both recovery watchdogs and
|
|
2322
|
-
`session.schedule` tasks.
|
|
2323
|
-
|
|
2324
|
-
The built-in QStash and Vercel adapters preserve the original provider error
|
|
2325
|
-
from a rejected send. A recognized definite 4xx status or error is terminal
|
|
2326
|
-
except for 408, 409, 425, and 429. Those statuses, 5xx responses, network
|
|
2327
|
-
failures, and errors with no recognized status remain retryable. The Vercel
|
|
2328
|
-
adapter also recognizes the SDK's name-only `BadRequestError`,
|
|
2329
|
-
`UnauthorizedError`, and `ForbiddenError` as terminal. Its native
|
|
2330
|
-
duplicate-message error is success. A2's durable AI tool execution uses this
|
|
2331
|
-
private classification. Direct `session.schedule()` calls still reject with
|
|
2332
|
-
the original error object.
|
|
2333
|
-
|
|
2334
|
-
## Entry points
|
|
2335
|
-
|
|
2336
|
-
| Entry point | Ships | Peer dependency |
|
|
2337
|
-
| -------------------- | ---------------------------------- | --------------- |
|
|
2338
|
-
| `experimental-a2` | `contract`, `A2Error`: isomorphic core | none |
|
|
2339
|
-
| `experimental-a2/server` | `createServer`: implement a contract | none |
|
|
2340
|
-
| `experimental-a2/client` | `createClient`: framework-agnostic session client | none |
|
|
2341
|
-
| `experimental-a2/react` | `createReact` | `react` |
|
|
2342
|
-
| `experimental-a2/ai` | `agentContract`, `handlerContext`, `inputs`, and the AI reducer: isomorphic | `ai` |
|
|
2343
|
-
| `experimental-a2/ai/server` | `agent`: the AI SDK runner as an A2 server | `ai` |
|
|
2344
|
-
| `experimental-a2/ai/client` | `createAgentClient`: contract-free agent client | `ai` |
|
|
2345
|
-
| `experimental-a2/ai/react` | `useAgent` | `ai`, `react` |
|
|
2346
|
-
| `experimental-a2/store-postgres` | `postgres`: Postgres store backend | `pg` (or inject a client) |
|
|
2347
|
-
| `experimental-a2/store-redis` | `redis`: Redis Streams store backend, push-native streaming | `ioredis` (or inject a client) |
|
|
2348
|
-
| `experimental-a2/store-redis-http` | `redisHttp`: the same Redis store over provider REST APIs (Upstash) | none |
|
|
2349
|
-
| `experimental-a2/store-sqlite` | SQLite store backend | none |
|
|
2350
|
-
| `experimental-a2/store-memory` | in-memory store backend | none |
|
|
2351
|
-
| `experimental-a2/scheduler-vercel` | `vercelQueues`: recovery and timers | `@vercel/queue` |
|
|
2352
|
-
| `experimental-a2/scheduler-qstash` | `qstash`: signed recovery and timers | `@upstash/qstash` |
|
|
2353
|
-
| `experimental-a2/cache-indexeddb` | `indexedDb` browser cache | none |
|
|
2354
|
-
| `experimental-a2/otel` | `otel` telemetry adapter | `@opentelemetry/api` |
|
|
2355
|
-
|
|
2356
|
-
Core `experimental-a2` imports none of the backends, enforced by a browser-bundle
|
|
2357
|
-
test in CI, not just convention. Importing `experimental-a2/store-postgres` is what
|
|
2358
|
-
pulls in Postgres, never `experimental-a2` itself, and never your client bundle.
|