experimental-a2 0.5.1 → 0.7.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 +67 -0
- package/dist/ai-server.d.ts +4 -5
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +8 -7
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +334 -2
- package/dist/ai.d.ts.map +1 -0
- package/dist/ai.js +1 -1
- package/dist/client.d.ts +202 -2
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +1025 -1
- package/dist/client.js.map +1 -0
- package/dist/errors-BQuJpe82.js.map +1 -1
- package/dist/index.d.ts +22 -3
- package/dist/index.d.ts.map +1 -0
- package/dist/{internal-DstsI6Re.js → internal-DRXJ56EI.js} +5 -28
- package/dist/internal-DRXJ56EI.js.map +1 -0
- package/dist/react.d.ts +41 -7
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +74 -35
- package/dist/react.js.map +1 -1
- package/dist/scheduler-qstash.d.ts +3 -3
- package/dist/scheduler-qstash.js +4 -5
- package/dist/scheduler-qstash.js.map +1 -1
- package/dist/scheduler-vercel.d.ts +2 -2
- package/dist/scheduler-vercel.js +4 -4
- package/dist/scheduler-vercel.js.map +1 -1
- package/dist/{server-Duw6MVlB.js → server-286j79Mt.js} +708 -79
- package/dist/server-286j79Mt.js.map +1 -0
- package/dist/{server-DpvjhdoE.d.ts → server-DgXmORIq.d.ts} +67 -49
- package/dist/server-DgXmORIq.d.ts.map +1 -0
- package/dist/server.d.ts +3 -3
- package/dist/server.js +1 -1
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/{store-DysUkTH3.d.ts → store-flRz1OWh.d.ts} +2 -57
- package/dist/store-flRz1OWh.d.ts.map +1 -0
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-memory.d.ts.map +1 -1
- package/dist/store-memory.js +1 -59
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-dSeLxzfb.js → store-polling-6DW7F1DT.js} +2 -2
- package/dist/{store-polling-dSeLxzfb.js.map → store-polling-6DW7F1DT.js.map} +1 -1
- package/dist/store-postgres.d.ts +1 -1
- package/dist/store-postgres.js +1 -81
- package/dist/store-postgres.js.map +1 -1
- package/dist/{store-redis-core-BFLwz0Wj.js → store-redis-core-DEYO8Ryv.js} +48 -134
- package/dist/store-redis-core-DEYO8Ryv.js.map +1 -0
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +2 -3
- package/dist/store-redis-http.js.map +1 -1
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.js +3 -4
- package/dist/store-redis.js.map +1 -1
- package/dist/store-sqlite.d.ts +1 -1
- package/dist/store-sqlite.d.ts.map +1 -1
- package/dist/store-sqlite.js +1 -72
- package/dist/store-sqlite.js.map +1 -1
- package/dist/{wire-BFQmSJ-9.js → wire-B6te_wns.js} +4 -3
- package/dist/wire-B6te_wns.js.map +1 -0
- package/docs/guides/03-react.mdx +118 -29
- package/docs/guides/05-production.mdx +9 -11
- package/docs/guides/06-ai-agents.mdx +8 -14
- package/docs/guides/09-presence.mdx +19 -21
- package/docs/guides/10-transports.mdx +104 -86
- package/docs/reference/01-api.mdx +186 -279
- package/docs/reference/02-errors.mdx +5 -7
- package/package.json +1 -14
- package/src/ai-server.ts +9 -5
- package/src/client.ts +71 -35
- package/src/errors.ts +1 -0
- package/src/internal.ts +3 -62
- package/src/push-envelope.ts +24 -21
- package/src/react.ts +118 -44
- package/src/scheduler-qstash.ts +3 -3
- package/src/scheduler-vercel.ts +2 -2
- package/src/server-fetch.ts +344 -0
- package/src/server.ts +73 -225
- package/src/session-socket.ts +36 -20
- package/src/sse.ts +2 -2
- package/src/store-memory.ts +0 -81
- package/src/store-postgres.ts +0 -100
- package/src/store-redis-core.ts +47 -211
- package/src/store-redis-http.ts +0 -1
- package/src/store-redis.ts +0 -1
- package/src/store-sqlite.ts +0 -119
- package/src/store.ts +0 -60
- package/src/wire.ts +2 -1
- package/dist/ai-D_PGS-JR.d.ts +0 -334
- package/dist/ai-D_PGS-JR.d.ts.map +0 -1
- package/dist/cli-B3VuxoDe.js +0 -599
- package/dist/cli-B3VuxoDe.js.map +0 -1
- package/dist/cli-bin.d.ts +0 -1
- package/dist/cli-bin.js +0 -7
- package/dist/cli-bin.js.map +0 -1
- package/dist/cli.d.ts +0 -20
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -2
- package/dist/client-BKlyLiOU.js +0 -1008
- package/dist/client-BKlyLiOU.js.map +0 -1
- package/dist/client-D7mvIXrF.d.ts +0 -191
- package/dist/client-D7mvIXrF.d.ts.map +0 -1
- package/dist/devtools-J_jZ2vQf.d.ts +0 -152
- package/dist/devtools-J_jZ2vQf.d.ts.map +0 -1
- package/dist/devtools-kJJaORn-.js +0 -340
- package/dist/devtools-kJJaORn-.js.map +0 -1
- package/dist/devtools-server.browser.d.ts +0 -1
- package/dist/devtools-server.browser.js +0 -6
- package/dist/devtools-server.browser.js.map +0 -1
- package/dist/devtools-server.d.ts +0 -23
- package/dist/devtools-server.d.ts.map +0 -1
- package/dist/devtools-server.js +0 -1270
- package/dist/devtools-server.js.map +0 -1
- package/dist/devtools.d.ts +0 -2
- package/dist/devtools.js +0 -2
- package/dist/errors-W6nwJ-fm.d.ts +0 -21
- package/dist/errors-W6nwJ-fm.d.ts.map +0 -1
- package/dist/http.d.ts +0 -151
- package/dist/http.d.ts.map +0 -1
- package/dist/http.js +0 -706
- package/dist/http.js.map +0 -1
- package/dist/inspection-DaxB5jM2.js +0 -13
- package/dist/inspection-DaxB5jM2.js.map +0 -1
- package/dist/internal-DstsI6Re.js.map +0 -1
- package/dist/platform-B4TnJtWu.js +0 -34
- package/dist/platform-B4TnJtWu.js.map +0 -1
- package/dist/server-DpvjhdoE.d.ts.map +0 -1
- package/dist/server-Duw6MVlB.js.map +0 -1
- package/dist/store-DysUkTH3.d.ts.map +0 -1
- package/dist/store-redis-core-BFLwz0Wj.js.map +0 -1
- package/dist/testing.browser.d.ts +0 -1
- package/dist/testing.browser.js +0 -6
- package/dist/testing.browser.js.map +0 -1
- package/dist/testing.d.ts +0 -32
- package/dist/testing.d.ts.map +0 -1
- package/dist/testing.js +0 -103
- package/dist/testing.js.map +0 -1
- package/dist/wire-BFQmSJ-9.js.map +0 -1
- package/docs/guides/07-devtools.mdx +0 -229
- package/src/cli-bin.ts +0 -5
- package/src/cli.ts +0 -1046
- package/src/devtools-app.ts +0 -989
- package/src/devtools-server.browser.ts +0 -5
- package/src/devtools-server.ts +0 -604
- package/src/devtools.ts +0 -716
- package/src/http.ts +0 -394
- package/src/inspection.ts +0 -39
- package/src/testing.browser.ts +0 -5
- package/src/testing.ts +0 -185
|
@@ -72,7 +72,6 @@ createServer(options: {
|
|
|
72
72
|
store?: A2Store // default: sqlite in dev, memory in tests, required in prod
|
|
73
73
|
scheduler?: A2Scheduler
|
|
74
74
|
telemetry?: A2Telemetry // optional instrumentation; see experimental-a2/otel
|
|
75
|
-
validatePush?: (context: PushValidationContext) => void | PromiseLike<void>
|
|
76
75
|
presence?: { ttlMs?: number } // presence expiry policy; default 60s
|
|
77
76
|
handlers?: {
|
|
78
77
|
[type]:
|
|
@@ -123,19 +122,6 @@ Server-only by construction: `experimental-a2/server` is the only entry point th
|
|
|
123
122
|
can reach a store backend, and its exports map resolves to a loud error
|
|
124
123
|
under the browser condition.
|
|
125
124
|
|
|
126
|
-
`validatePush(context)` runs only for input that arrived over the wire
|
|
127
|
-
through `handle`'s push lane, once per plane. The events plane invokes it with `{ sessionId, events }` before
|
|
128
|
-
contract schema validation and before the store append, so throwing rejects the
|
|
129
|
-
complete push without writing anything. The presence plane invokes it with
|
|
130
|
-
`{ sessionId, events: [], presence }`, the whole pushed patch with its
|
|
131
|
-
participant, before field validation and the broadcast, so authorizing the
|
|
132
|
-
participant id (and applying any size or cardinality policy) happens at the
|
|
133
|
-
same seam. The patch arrives frozen: authorize, don't rewrite (a mutation
|
|
134
|
-
attempt throws and fails the push). Direct trusted server appends, handler appends, and server-side
|
|
135
|
-
`setPresence` bypass it. The envelope parser inside `handle` creates
|
|
136
|
-
the runtime provenance brand after reading the envelope; a caller-supplied
|
|
137
|
-
field with the same name is ignored, and the brand is not stored in the log.
|
|
138
|
-
|
|
139
125
|
`presence.ttlMs` sets how long a presence value survives without a
|
|
140
126
|
refreshing set (default 60 seconds, as a positive integer of
|
|
141
127
|
milliseconds). Expiry counts on the storage's own clock, never on the
|
|
@@ -203,6 +189,136 @@ retries may move the frontier between them. A generic join should use a
|
|
|
203
189
|
monotone readiness predicate and a stable explicit output event `id`, so every
|
|
204
190
|
eligible attempt converges on the same append.
|
|
205
191
|
|
|
192
|
+
### `server.fetch(request, options?)`
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
server.fetch(
|
|
196
|
+
request: Request,
|
|
197
|
+
options?: {
|
|
198
|
+
authorize?: (operation: A2Operation) => boolean | Promise<boolean>
|
|
199
|
+
upgradeWebSocket?: (
|
|
200
|
+
attach: (socket: A2Socket) => void,
|
|
201
|
+
) => Response | Promise<Response>
|
|
202
|
+
},
|
|
203
|
+
): Promise<Response>
|
|
204
|
+
|
|
205
|
+
type A2Operation =
|
|
206
|
+
| {
|
|
207
|
+
type: 'stream'
|
|
208
|
+
sessionId: string
|
|
209
|
+
startAfter: number
|
|
210
|
+
transport: 'http' | 'websocket'
|
|
211
|
+
}
|
|
212
|
+
| {
|
|
213
|
+
type: 'history'
|
|
214
|
+
sessionId: string
|
|
215
|
+
gte: number
|
|
216
|
+
lte: number
|
|
217
|
+
transport: 'http'
|
|
218
|
+
}
|
|
219
|
+
| {
|
|
220
|
+
type: 'push'
|
|
221
|
+
sessionId: string
|
|
222
|
+
events: readonly {
|
|
223
|
+
type: ContractEventType | string
|
|
224
|
+
payload: unknown
|
|
225
|
+
id?: string
|
|
226
|
+
}[]
|
|
227
|
+
presence?: {
|
|
228
|
+
participant: string
|
|
229
|
+
values: Readonly<Record<string, unknown>>
|
|
230
|
+
seen?: number
|
|
231
|
+
at?: number
|
|
232
|
+
}
|
|
233
|
+
transport: 'http' | 'websocket'
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
The bound Fetch API route for this server. An unprotected route can export it
|
|
238
|
+
directly:
|
|
239
|
+
|
|
240
|
+
```ts app/api/public-order-events/route.ts
|
|
241
|
+
import { ordersServer } from '@/server/orders'
|
|
242
|
+
|
|
243
|
+
export const GET = ordersServer.fetch
|
|
244
|
+
export const POST = ordersServer.fetch
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
A plain GET streams one session over SSE, resumed after the `index` query
|
|
248
|
+
parameter. A GET with `gte` and `lte` returns the closed history range as JSON
|
|
249
|
+
and sets `a2-history-covered` to `true` when every requested index was returned.
|
|
250
|
+
POST accepts `{ sessionId, events, presence? }` and returns the appended events.
|
|
251
|
+
Presence is inferred from the contract. A contract that declares presence
|
|
252
|
+
streams it and accepts presence pushes automatically.
|
|
253
|
+
|
|
254
|
+
`authorize` runs after the request or socket frame has been parsed, but before
|
|
255
|
+
contract schema validation, presence validation, or I/O. Event names and
|
|
256
|
+
presence field names are typed from the contract. Their unvalidated values stay
|
|
257
|
+
`unknown`. Return `false` to deny the operation with `FORBIDDEN`. A thrown error
|
|
258
|
+
becomes `STORE_UNAVAILABLE`, because authorization infrastructure failed rather
|
|
259
|
+
than denied access. Operation objects, push events, event arrays, and presence
|
|
260
|
+
patches are frozen. Authorize them, do not rewrite them.
|
|
261
|
+
|
|
262
|
+
Authentication stays outside A2, where the route can read headers, cookies, and
|
|
263
|
+
framework context. Capture the authenticated principal in `authorize`:
|
|
264
|
+
|
|
265
|
+
```ts app/api/protected-order-events/route.ts
|
|
266
|
+
import { ordersServer } from '@/server/orders'
|
|
267
|
+
|
|
268
|
+
async function fetchOrders(request: Request): Promise<Response> {
|
|
269
|
+
// your authentication:
|
|
270
|
+
const user = { id: 'user-1' }
|
|
271
|
+
if (!user) return new Response(null, { status: 401 })
|
|
272
|
+
|
|
273
|
+
return ordersServer.fetch(request, {
|
|
274
|
+
authorize(operation) {
|
|
275
|
+
// your per-session and per-operation authorization:
|
|
276
|
+
// return canAccess(user, operation)
|
|
277
|
+
return true
|
|
278
|
+
},
|
|
279
|
+
})
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export const GET = fetchOrders
|
|
283
|
+
export const POST = fetchOrders
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Direct `session.append`, handler appends, scheduled appends, and server-side
|
|
287
|
+
`setPresence` bypass `authorize`. It is an ingress policy for this fetch call,
|
|
288
|
+
not a global server policy.
|
|
289
|
+
|
|
290
|
+
A GET with `Upgrade: websocket` uses `upgradeWebSocket`. Without that option it
|
|
291
|
+
returns 426. The upgrade function receives an `attach` callback for the
|
|
292
|
+
platform socket:
|
|
293
|
+
|
|
294
|
+
```ts app/api/socket-order-events/route.ts
|
|
295
|
+
import { experimental_upgradeWebSocket } from '@vercel/functions'
|
|
296
|
+
import { ordersServer } from '@/server/orders'
|
|
297
|
+
|
|
298
|
+
export const POST = ordersServer.fetch
|
|
299
|
+
|
|
300
|
+
export function GET(request: Request): Promise<Response> {
|
|
301
|
+
return ordersServer.fetch(request, {
|
|
302
|
+
upgradeWebSocket: (attach) =>
|
|
303
|
+
experimental_upgradeWebSocket(attach, {
|
|
304
|
+
maxPayload: 4 * 1024 * 1024,
|
|
305
|
+
}),
|
|
306
|
+
})
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Authenticate the physical WebSocket GET before calling `server.fetch`. Then
|
|
311
|
+
use `authorize` for each session subscribe and each event or presence push.
|
|
312
|
+
There is no WebSocket-upgrade operation in `A2Operation`. A denied subscribe
|
|
313
|
+
receives `unsubscribed` and does not affect other sessions. A denied event push
|
|
314
|
+
receives a `FORBIDDEN` ack. Denied presence is silently dropped because it is
|
|
315
|
+
fire-and-forget. Failed presence authorization is dropped the same way and does
|
|
316
|
+
not disconnect unrelated sessions on the multiplexed socket.
|
|
317
|
+
|
|
318
|
+
SSE and WebSocket lifetimes use the platform's ambient invocation deadline.
|
|
319
|
+
A2 also uses the platform's ambient `waitUntil` capability for background work.
|
|
320
|
+
Neither capability is reordered or passed through `server.fetch` options.
|
|
321
|
+
|
|
206
322
|
### `server.session(id)`
|
|
207
323
|
|
|
208
324
|
```ts
|
|
@@ -372,9 +488,9 @@ Custom scheduler adapters implement two methods:
|
|
|
372
488
|
| `schedule(task: SchedulerTask): Promise<void>` | put one versioned task on durable delivery infrastructure |
|
|
373
489
|
| `handler(...servers)` | return the route that receives drain and append tasks |
|
|
374
490
|
|
|
375
|
-
`handler`
|
|
376
|
-
|
|
377
|
-
|
|
491
|
+
Application routes call `scheduler.handler(...servers)` on the configured
|
|
492
|
+
adapter. The handler requires at least one drainable server and rejects
|
|
493
|
+
duplicate contract names synchronously.
|
|
378
494
|
|
|
379
495
|
`SchedulerTask` is `SchedulerDrainTask | SchedulerAppendTask`. These task types,
|
|
380
496
|
plus `ScheduledEvent`, are exported from `experimental-a2/server`.
|
|
@@ -429,7 +545,7 @@ the contract's schemas before anything is written. A multi-event append
|
|
|
429
545
|
is atomic: all-or-nothing, consecutive positions, one transaction. Pass
|
|
430
546
|
`id` to make an append idempotent across retries; re-sending an
|
|
431
547
|
identical batch returns the original rows. (Events that arrived through
|
|
432
|
-
`
|
|
548
|
+
`server.fetch`'s push lane are accepted directly.) See
|
|
433
549
|
[Durability](/concepts/durability).
|
|
434
550
|
|
|
435
551
|
After the write, A2 dispatches only event types with registered handlers.
|
|
@@ -537,7 +653,7 @@ session.stream(options: {
|
|
|
537
653
|
|
|
538
654
|
A live feed of the session's events. `startAfter` is a non-negative safe integer;
|
|
539
655
|
`startAfter: 20` begins with event 21.
|
|
540
|
-
Server-side only; `
|
|
656
|
+
Server-side only; `server.fetch` exposes it over SSE, and over the multiplexed
|
|
541
657
|
socket when `upgrade` is set. Subscribing never dispatches handlers.
|
|
542
658
|
|
|
543
659
|
With `presence: true`, the feed yields one `PresenceSnapshot` first:
|
|
@@ -581,7 +697,7 @@ the storage clock, not the sender stamp, so a hostile stamp can only
|
|
|
581
697
|
vandalize its own field and still expires on schedule.
|
|
582
698
|
|
|
583
699
|
The patch is structurally the `presence` sibling of the push envelope;
|
|
584
|
-
`
|
|
700
|
+
`server.fetch`'s push lane forwards it whole to `session.setPresence(presence)`.
|
|
585
701
|
The API mirrors the wire.
|
|
586
702
|
|
|
587
703
|
The handler-scoped form `ctx.session.setPresence(...)` is the same
|
|
@@ -650,7 +766,7 @@ transition and the destination should adopt the same optimistic session.
|
|
|
650
766
|
| `initialState` | server-rendered state |
|
|
651
767
|
| `initialIndex` | the fold's frontier, where the stream resumes |
|
|
652
768
|
| `initialEvents` | optional earlier raw events for a history UI |
|
|
653
|
-
| `participant` |
|
|
769
|
+
| `participant` | overrides the client's `participant`; one of the two is required to call `setPresence` |
|
|
654
770
|
|
|
655
771
|
Opens the stream on mount, closes it on unmount, reconnects with
|
|
656
772
|
backoff from the current frontier. `participant` binds at the session's
|
|
@@ -706,9 +822,8 @@ invert). No ack, no `confirmed`, no retry.
|
|
|
706
822
|
Both members exist only when the contract declares `presence`; their
|
|
707
823
|
value and field types come from its schemas, through the reducer, with
|
|
708
824
|
no type arguments (the reducer is the client's typed handle on the
|
|
709
|
-
contract; it still never folds presence). `setPresence` requires
|
|
710
|
-
|
|
711
|
-
participants active, the GET route forgot `presence: true`. See
|
|
825
|
+
contract; it still never folds presence). `setPresence` requires a
|
|
826
|
+
`participant`: the client's default, or the provider's override. See
|
|
712
827
|
[Presence](/guides/presence).
|
|
713
828
|
|
|
714
829
|
`push` appends optimistically: validated locally, rolled back on
|
|
@@ -754,6 +869,37 @@ connection as dead (aborts it and reconnects), so `live` means bytes
|
|
|
754
869
|
are actually flowing, not "the socket hasn't errored yet". See
|
|
755
870
|
[Live UI](/guides/react).
|
|
756
871
|
|
|
872
|
+
### `useSession(client, sessionId, options?)`
|
|
873
|
+
|
|
874
|
+
```ts
|
|
875
|
+
useSession(client: A2Client, sessionId: string, options?: {
|
|
876
|
+
participant?: string // overrides the client's participant
|
|
877
|
+
hydrate?: { state: S; index: number }
|
|
878
|
+
}): UseSessionResult
|
|
879
|
+
```
|
|
880
|
+
|
|
881
|
+
The standalone, provider-less hook. It returns the same result shape
|
|
882
|
+
as the bound hook, for apps whose components reach sessions ad hoc (a
|
|
883
|
+
sidebar of channel sessions, a user session read from a menu). The
|
|
884
|
+
handle is identity-mapped: every hook and provider mounting the same
|
|
885
|
+
session shares one runtime. The stream is refcounted: the first mount
|
|
886
|
+
connects, the last unmount closes (StrictMode-safe).
|
|
887
|
+
|
|
888
|
+
`hydrate` is the hydration input, one atomic option: the
|
|
889
|
+
`{ state, index }` pair `session.state(reducer)` returns, fetched by
|
|
890
|
+
your app through its own route and handed over whenever it lands.
|
|
891
|
+
While it is `undefined`, the hook holds the stream (connecting without
|
|
892
|
+
a fold would replay the whole log). There is no pending flag on the
|
|
893
|
+
result: whether the fold has been handed over is your own input, so
|
|
894
|
+
your data layer's loading state is the pending state.
|
|
895
|
+
When it arrives, the session hydrates under the usual
|
|
896
|
+
never-move-backward rule and the stream connects at that frontier.
|
|
897
|
+
For a deliberate full replay, hand the fold's true starting point:
|
|
898
|
+
`{ state: reducer.initialState, index: 0 }`. State at index 0 is the
|
|
899
|
+
reducer's seed by definition, so the explicit replay needs no special
|
|
900
|
+
vocabulary. See
|
|
901
|
+
[Client-first sessions](/guides/react#client-first-sessions).
|
|
902
|
+
|
|
757
903
|
## `experimental-a2/ai`
|
|
758
904
|
|
|
759
905
|
### `agent(options)`
|
|
@@ -1013,17 +1159,10 @@ it into `createServer({ handlers })` beside application handlers when you need
|
|
|
1013
1159
|
a custom assembly. The table handles input facts, generation requests, model
|
|
1014
1160
|
step completion, tool calls, approval responses, and terminal tool results.
|
|
1015
1161
|
Application handlers spread later can deliberately replace a built-in
|
|
1016
|
-
handler.
|
|
1017
|
-
`
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
`validateAgentPush({ sessionId, events }): void`
|
|
1022
|
-
|
|
1023
|
-
Accepts the browser interaction allowlist: user messages, approval and input
|
|
1024
|
-
responses, interruptions, and explicit retries. It rejects server-authored
|
|
1025
|
-
scheduling and lifecycle events, seeded non-user messages, and input requests.
|
|
1026
|
-
`createAgentServer()` installs it automatically.
|
|
1162
|
+
handler. A custom assembly owns its browser ingress policy. Use
|
|
1163
|
+
`server.fetch(request, { authorize })` to reject server-authored AI event names
|
|
1164
|
+
before they reach the server. `createAgentServer()` installs the built-in
|
|
1165
|
+
browser allowlist automatically.
|
|
1027
1166
|
|
|
1028
1167
|
See [Durable AI agents](/guides/ai-agents) for the protocol and complete
|
|
1029
1168
|
examples.
|
|
@@ -1035,6 +1174,7 @@ createClient(options: {
|
|
|
1035
1174
|
reducer: Reducer
|
|
1036
1175
|
api: ClientApi
|
|
1037
1176
|
gcTime?: number // idle session lifetime; 5 minutes by default
|
|
1177
|
+
participant?: string // default presence identity for every session
|
|
1038
1178
|
}): A2Client
|
|
1039
1179
|
|
|
1040
1180
|
type ClientApi =
|
|
@@ -1049,11 +1189,19 @@ queue with ack/rollback, and the local fold. `client.session(id, {
|
|
|
1049
1189
|
initialState?, initialIndex?, initialEvents?, participant? })` returns a
|
|
1050
1190
|
handle with `getSnapshot()`/`subscribe()` (the `useSyncExternalStore`
|
|
1051
1191
|
contract), `push()`, `loadHistory()`, `connect()`, and `close()`.
|
|
1192
|
+
`connect()` takes a lease on the live stream and returns its release:
|
|
1193
|
+
leases refcount per handle (the first connects, releasing the last
|
|
1194
|
+
closes, releasing twice is a no-op), so independent consumers of one
|
|
1195
|
+
identity-mapped handle never fight over the stream. `close()` is the
|
|
1196
|
+
hard stop: it drops every outstanding lease and closes now; a later
|
|
1197
|
+
`connect()` starts fresh.
|
|
1052
1198
|
Snapshots carry `state`, `events`, `index`, `history`, and `connection`
|
|
1053
1199
|
(the same fields `useSession` exposes), and `push` returns the same
|
|
1054
1200
|
ack-then-`confirmed` result. On contracts that declare `presence` the handle also carries
|
|
1055
1201
|
`setPresence()` and snapshots carry the `presence` map, exactly like
|
|
1056
|
-
the hook; `participant` is the identity `setPresence` sends under
|
|
1202
|
+
the hook; `participant` is the identity `setPresence` sends under:
|
|
1203
|
+
stated once on `createClient` as the default for every handle, or per
|
|
1204
|
+
session as the override. Use
|
|
1057
1205
|
it directly from any other framework, or none.
|
|
1058
1206
|
|
|
1059
1207
|
Within one `A2Client`, repeated `session(id)` calls return the same live
|
|
@@ -1065,96 +1213,6 @@ frontier. Idle handles are evicted after `gcTime`. This memory layer is
|
|
|
1065
1213
|
separate from `experimental-a2/cache-indexeddb`: memory preserves identity
|
|
1066
1214
|
across route transitions, while IndexedDB preserves the replica across reloads.
|
|
1067
1215
|
|
|
1068
|
-
## `experimental-a2/http`
|
|
1069
|
-
|
|
1070
|
-
### `handle(server, options?)`
|
|
1071
|
-
|
|
1072
|
-
```ts
|
|
1073
|
-
handle(server: A2Server, options?: {
|
|
1074
|
-
before?(args: { request: Request; intent: A2Intent }):
|
|
1075
|
-
Response | undefined | void | Promise<Response | undefined | void>
|
|
1076
|
-
after?(args: { request: Request; intent: A2Intent; outcome: A2Outcome; response: Response }):
|
|
1077
|
-
Response | undefined | void | Promise<Response | undefined | void>
|
|
1078
|
-
upgrade?: UpgradeFn // e.g. (attach) => experimental_upgradeWebSocket(attach)
|
|
1079
|
-
presence?: boolean // interleave presence on every stream lane
|
|
1080
|
-
deadline?: number // epoch ms: close sockets cleanly before it
|
|
1081
|
-
}): { GET(req: Request): Promise<Response>; POST(req: Request): Promise<Response> }
|
|
1082
|
-
|
|
1083
|
-
type A2Intent =
|
|
1084
|
-
| { type: 'ws-upgrade' }
|
|
1085
|
-
| { type: 'stream'; sessionId: string; startAfter: number; transport: 'sse' | 'ws' }
|
|
1086
|
-
| { type: 'history'; sessionId: string; gte: number; lte: number }
|
|
1087
|
-
| { type: 'push'; sessionId: string; events: PushedEvent[]; presence?: PushedPresence; transport: 'http' | 'ws' }
|
|
1088
|
-
|
|
1089
|
-
type A2Outcome =
|
|
1090
|
-
| { type: 'stream' }
|
|
1091
|
-
| { type: 'history'; covered: boolean; events: Event[] }
|
|
1092
|
-
| { type: 'push'; appended: Event[] }
|
|
1093
|
-
```
|
|
1094
|
-
|
|
1095
|
-
The session route pair as one call: `export const { GET, POST } =
|
|
1096
|
-
handle(server)` in a route module (any framework speaking
|
|
1097
|
-
`(req: Request) => Promise<Response>`). A plain `GET` is the live SSE
|
|
1098
|
-
stream, resumed after the `index` query parameter, with a `: connected`
|
|
1099
|
-
prelude, a `: ping` heartbeat every 15s, and a clean close one second
|
|
1100
|
-
before an ambient Vercel invocation deadline when available; presence
|
|
1101
|
-
patches ride as named frames when `presence: true`. A `GET` with
|
|
1102
|
-
`gte`/`lte` query parameters is a history slice: the closed log range
|
|
1103
|
-
as JSON wire events, the read `loadHistory` rides. `POST` is the push
|
|
1104
|
-
envelope `{ sessionId, events, presence? }`, answered with the appended
|
|
1105
|
-
events. A `GET` carrying an upgrade header becomes the multiplexed
|
|
1106
|
-
WebSocket when `options.upgrade` is present, and answers `426` when it
|
|
1107
|
-
is not.
|
|
1108
|
-
|
|
1109
|
-
Parsing is protocol, hooks are policy. A request that fails to parse
|
|
1110
|
-
(missing `sessionId`, malformed bounds, a bad push envelope) answers
|
|
1111
|
-
`INVALID_PAYLOAD` on the wire before any hook runs. `before` sees every
|
|
1112
|
-
parsed intent, HTTP requests and socket frames alike; over the socket,
|
|
1113
|
-
each subscribe arrives as a `stream` intent and each push as a `push`
|
|
1114
|
-
intent, with `request` always the original upgrade Request. Returning a
|
|
1115
|
-
Response short-circuits: over HTTP it is the response, verbatim; over
|
|
1116
|
-
the socket it is translated into the wire's own vocabulary (a denied
|
|
1117
|
-
subscribe answers `unsubscribed`, a denied push a non-retryable error
|
|
1118
|
-
ack), because a Response cannot cross a socket.
|
|
1119
|
-
|
|
1120
|
-
`after` runs only where the library produced an HTTP response: never
|
|
1121
|
-
after a short-circuit, never for `ws-upgrade` or socket frames. It may
|
|
1122
|
-
mutate `response.headers` in place or return a replacement Response.
|
|
1123
|
-
`outcome.covered` on a history read means the closed range came back
|
|
1124
|
-
fully covered (`events.length === lte - gte + 1`): an immutable slice
|
|
1125
|
-
of the append-only log, safe to cache under whatever policy your
|
|
1126
|
-
`after` applies. The history response carries no cache headers of its
|
|
1127
|
-
own.
|
|
1128
|
-
|
|
1129
|
-
The socket is one connection for all of a client's sessions:
|
|
1130
|
-
`subscribe`/`unsubscribe` frames open and close per-session lanes at
|
|
1131
|
-
their own resume frontiers, `sessionId` tags route pushes, presence,
|
|
1132
|
-
and acks, and a lane ending or failing answers `unsubscribed` without
|
|
1133
|
-
taking the socket down. See [Transports](/guides/transports).
|
|
1134
|
-
|
|
1135
|
-
### The rest of the entry
|
|
1136
|
-
|
|
1137
|
-
| Helper | What it does |
|
|
1138
|
-
| ----------------------- | ------------------------------------------------------------------------------ |
|
|
1139
|
-
| `schedulerHandler(...servers)` | returns the delivery route after synchronously verifying one shared scheduler |
|
|
1140
|
-
| `errorResponse(err)` | serializes an `A2Error` to `{ error: { code, message, details } }` + status; the natural return value of a refusing `before` hook |
|
|
1141
|
-
| `deserializeError(body)` | rebuilds an `A2Error` from a wire body, or `null` if the body isn't one |
|
|
1142
|
-
|
|
1143
|
-
`schedulerHandler(...servers)` is the application-facing scheduler route. It
|
|
1144
|
-
requires at least one A2 server. Every server must have a scheduler, use the
|
|
1145
|
-
exact same scheduler instance, and have a unique contract name. Non-A2 values
|
|
1146
|
-
also fail. These checks throw before the request handler is returned, so bad
|
|
1147
|
-
wiring fails when the route module loads. The helper then delegates delivery
|
|
1148
|
-
to the shared adapter's `A2Scheduler.handler(...)` method.
|
|
1149
|
-
|
|
1150
|
-
Different scheduler instances use different routes. Match each QStash route to
|
|
1151
|
-
that instance's resolved `url`; additional QStash routes pass an explicit
|
|
1152
|
-
`url`. Match each Vercel Queues route and trigger to that instance's `topic`.
|
|
1153
|
-
|
|
1154
|
-
`errorResponse` and `deserializeError` are the `A2Error` wire format
|
|
1155
|
-
that `push` and the push lane share. See
|
|
1156
|
-
[Errors](/reference/errors#over-the-wire).
|
|
1157
|
-
|
|
1158
1216
|
## `experimental-a2/cache-indexeddb`
|
|
1159
1217
|
|
|
1160
1218
|
```ts
|
|
@@ -1232,112 +1290,6 @@ log.
|
|
|
1232
1290
|
renames and additions are breaking for dashboards, and are called out in the
|
|
1233
1291
|
package's `CHANGELOG.md`.
|
|
1234
1292
|
|
|
1235
|
-
## `experimental-a2/devtools`
|
|
1236
|
-
|
|
1237
|
-
The shared, isomorphic protocol for browser Devtools, the CLI, and `.a2log`
|
|
1238
|
-
captures.
|
|
1239
|
-
|
|
1240
|
-
| Export | Contract |
|
|
1241
|
-
| --- | --- |
|
|
1242
|
-
| `DEVTOOLS_PROTOCOL_VERSION` | Current HTTP wire protocol version |
|
|
1243
|
-
| `DEVTOOLS_CAPTURE_VERSION` | Current `.a2log` record format version |
|
|
1244
|
-
| `DEVTOOLS_CAPTURE_MEDIA_TYPE` | `application/x-ndjson` |
|
|
1245
|
-
| `DEVTOOLS_SESSION_PAGE_LIMIT` | Default event count requested per session page |
|
|
1246
|
-
| `DEVTOOLS_SESSION_PAGE_MAX_LIMIT` | Maximum event count accepted per session page |
|
|
1247
|
-
| `DEVTOOLS_CAPABILITIES` | Protocol resources and capture format advertised by the server |
|
|
1248
|
-
| `devtoolsSessionRevision({ events, snapshots })` | Compute the lifecycle invalidation token after assembling pages |
|
|
1249
|
-
| `encodeDevtoolsCapture(detail, { capturedAt? })` | Encode one session detail as exact NDJSON and add its SHA-256 footer |
|
|
1250
|
-
| `parseDevtoolsCapture(value)` | Parse NDJSON text or bytes into a `DevtoolsCapture` while retaining unknown fields on known records |
|
|
1251
|
-
| `verifyDevtoolsCapture(capture)` | Verify versions, record shapes, session identity, indexes, counts, dates, and the SHA-256 digest |
|
|
1252
|
-
|
|
1253
|
-
The wire types are `DevtoolsWireSessionSummary`, `DevtoolsWireEvent`,
|
|
1254
|
-
`DevtoolsWireSnapshot`, `DevtoolsContractsResponse`,
|
|
1255
|
-
`DevtoolsSessionsResponse`, `DevtoolsSessionDetail`, `DevtoolsSessionPage`, and
|
|
1256
|
-
`DevtoolsCapabilities`. Capture records use `DevtoolsCaptureManifest`,
|
|
1257
|
-
`DevtoolsCaptureEvent`, `DevtoolsCaptureSnapshot`,
|
|
1258
|
-
`DevtoolsCaptureFooter`, and `DevtoolsCaptureRecord`. A parsed
|
|
1259
|
-
`DevtoolsCapture` exposes the same canonical sequence through `records`.
|
|
1260
|
-
|
|
1261
|
-
```ts test/verify-a2log.ts
|
|
1262
|
-
import { readFile } from 'node:fs/promises'
|
|
1263
|
-
import {
|
|
1264
|
-
parseDevtoolsCapture,
|
|
1265
|
-
verifyDevtoolsCapture,
|
|
1266
|
-
} from 'experimental-a2/devtools'
|
|
1267
|
-
|
|
1268
|
-
export async function verifyA2Log(path: string) {
|
|
1269
|
-
const capture = parseDevtoolsCapture(await readFile(path))
|
|
1270
|
-
await verifyDevtoolsCapture(capture)
|
|
1271
|
-
return capture
|
|
1272
|
-
}
|
|
1273
|
-
```
|
|
1274
|
-
|
|
1275
|
-
The manifest records the contract, session, revision, capture time, protocol
|
|
1276
|
-
version, and capture version. Event records contain the exact durable payload
|
|
1277
|
-
and operational bookkeeping. Snapshot records contain metadata but never
|
|
1278
|
-
cached reducer state. The final `end` record contains counts, the highest event
|
|
1279
|
-
index, and a digest of every preceding encoded record. The session revision is
|
|
1280
|
-
an invalidation token, not the integrity digest. Encoding rejects durable
|
|
1281
|
-
values that canonical JSON would coerce or omit.
|
|
1282
|
-
|
|
1283
|
-
## `experimental-a2/devtools/server`
|
|
1284
|
-
|
|
1285
|
-
```ts
|
|
1286
|
-
// anywhere on the server:
|
|
1287
|
-
import type { DevtoolsServer } from 'experimental-a2/devtools/server'
|
|
1288
|
-
|
|
1289
|
-
declare function createDevtools(options: {
|
|
1290
|
-
servers: readonly DevtoolsServer[]
|
|
1291
|
-
authorize?: (
|
|
1292
|
-
request: Request,
|
|
1293
|
-
) => boolean | Response | Promise<boolean | Response>
|
|
1294
|
-
}): {
|
|
1295
|
-
handler(): (request: Request) => Promise<Response>
|
|
1296
|
-
}
|
|
1297
|
-
```
|
|
1298
|
-
|
|
1299
|
-
A read-only dashboard over the servers' durable logs. The handler serves the
|
|
1300
|
-
complete browser application, its JSON endpoints, and live SSE invalidations.
|
|
1301
|
-
It discovers sessions and reads stored causal, dispatch, completion, failure,
|
|
1302
|
-
and snapshot metadata. The causal forest and lifecycle timeline come directly
|
|
1303
|
-
from stored events. The initial page response includes the selected dashboard
|
|
1304
|
-
data so the browser does not need a contracts, sessions, and detail request
|
|
1305
|
-
waterfall. It never drains or heals a session.
|
|
1306
|
-
|
|
1307
|
-
Every route is a `GET`. The versioned resources are `capabilities`,
|
|
1308
|
-
`contracts`, `sessions`, `session`, `watch`, and `export` under the handler's
|
|
1309
|
-
`_a2/` path. `export` returns an integrity-checkable `.a2log` with the media
|
|
1310
|
-
type from `DEVTOOLS_CAPTURE_MEDIA_TYPE`.
|
|
1311
|
-
|
|
1312
|
-
`session` without pagination parameters returns one complete detail. Add a
|
|
1313
|
-
positive `limit` to receive a `DevtoolsSessionPage`. Its `throughIndex` is the
|
|
1314
|
-
inclusive event frontier frozen by the first request. Pass its opaque `cursor`
|
|
1315
|
-
back until the cursor is null. Snapshot metadata appears on the first page
|
|
1316
|
-
only. The cursor is bound to its contract and session, and the server rejects
|
|
1317
|
-
reuse against another log. The advertised maximum page size is clamped by the
|
|
1318
|
-
server.
|
|
1319
|
-
|
|
1320
|
-
The browser, CLI, watch loop, and export route walk these bounded pages for
|
|
1321
|
-
you. They reject a gap or early end. An exact export includes every event
|
|
1322
|
-
through one finite frontier, even when a backend provider limits one range
|
|
1323
|
-
response. Events appended during the walk belong to a later read.
|
|
1324
|
-
|
|
1325
|
-
Without `authorize`, the handler is available only when `NODE_ENV` is exactly
|
|
1326
|
-
`development`. Any other value, including unset, returns 404. When `authorize`
|
|
1327
|
-
is present, only a literal `true` grants access. Returning `false` also returns
|
|
1328
|
-
404. Returning a `Response` passes that response through, which supports
|
|
1329
|
-
redirects and authentication challenges. The mounted handler is the
|
|
1330
|
-
authorization boundary for the browser, CLI, SSE stream, and exact capture
|
|
1331
|
-
download. A2 does not add a separate Devtools credential store.
|
|
1332
|
-
|
|
1333
|
-
The built-in memory, SQLite, Postgres, and Redis stores support inspection.
|
|
1334
|
-
Custom `A2Store` implementations can omit the optional `inspect` interface; the
|
|
1335
|
-
dashboard returns 501 for those logs. A custom inspection implementation may
|
|
1336
|
-
add `readEvents(sessionId, { afterIndex, throughIndex?, limit })` for efficient
|
|
1337
|
-
bounded reads. It returns `{ events, throughIndex }`, where the first call
|
|
1338
|
-
freezes the inclusive frontier and continuations preserve it. Without this
|
|
1339
|
-
method, A2 reads the complete log for each page and slices it in memory.
|
|
1340
|
-
|
|
1341
1293
|
## `experimental-a2/scheduler-vercel`
|
|
1342
1294
|
|
|
1343
1295
|
`vercelQueues(options?)` returns an `A2Scheduler` backed by Vercel Queues.
|
|
@@ -1449,10 +1401,9 @@ Mount the application handler once and pass every server that shares the
|
|
|
1449
1401
|
scheduler:
|
|
1450
1402
|
|
|
1451
1403
|
```ts app/api/a2/scheduler/route.ts
|
|
1452
|
-
import {
|
|
1453
|
-
import { ordersServer, billingServer } from '@/server'
|
|
1404
|
+
import { billingServer, ordersServer, scheduler } from '@/server'
|
|
1454
1405
|
|
|
1455
|
-
export const POST =
|
|
1406
|
+
export const POST = scheduler.handler(ordersServer, billingServer)
|
|
1456
1407
|
```
|
|
1457
1408
|
|
|
1458
1409
|
The route must be reachable by the selected QStash server after any platform
|
|
@@ -1533,46 +1484,6 @@ duplicate-message error is success. A2's durable AI tool execution uses this
|
|
|
1533
1484
|
private classification. Direct `session.schedule()` calls still reject with
|
|
1534
1485
|
the original error object.
|
|
1535
1486
|
|
|
1536
|
-
## `a2 devtools`
|
|
1537
|
-
|
|
1538
|
-
The package installs an `a2` binary with a read-only `devtools` namespace.
|
|
1539
|
-
|
|
1540
|
-
| Command | Purpose |
|
|
1541
|
-
| --- | --- |
|
|
1542
|
-
| `contracts` | List contracts mounted at the Devtools URL |
|
|
1543
|
-
| `sessions --contract NAME` | List session summaries, with optional `--limit` and `--cursor` |
|
|
1544
|
-
| `show --contract NAME --session ID` | Read one session's exact durable detail |
|
|
1545
|
-
| `export --contract NAME --session ID` | Verify and save an `.a2log`; use `--output` and opt into replacement with `--force` |
|
|
1546
|
-
| `check --contract NAME --session ID` | Verify a live capture and optionally require `--settled`, `--no-dead-letters`, `--no-caught-failures`, or `--max-redispatches N` |
|
|
1547
|
-
| `verify FILE` | Verify a saved capture offline |
|
|
1548
|
-
|
|
1549
|
-
Live commands read the mounted URL from `--url` or `A2_DEVTOOLS_URL`.
|
|
1550
|
-
`A2_DEVTOOLS_TOKEN` supplies the default bearer token. `--bearer-env ENV` and
|
|
1551
|
-
repeatable `--header-env HEADER=ENV` reference other environment variables.
|
|
1552
|
-
Credential values are never accepted as CLI arguments or URL components.
|
|
1553
|
-
Output uses `--format human`, `json`, or `ndjson`.
|
|
1554
|
-
|
|
1555
|
-
## `experimental-a2/testing`
|
|
1556
|
-
|
|
1557
|
-
Node-only helpers for verified capture loading and pure reducer replay.
|
|
1558
|
-
|
|
1559
|
-
| Export | Contract |
|
|
1560
|
-
| --- | --- |
|
|
1561
|
-
| `loadCapture(source, contract)` | Read a filesystem path or URL, verify its integrity, validate its contract and payloads, and return typed events |
|
|
1562
|
-
| `prepareCapture(capture, contract)` | Apply the same verification and contract typing to an already parsed capture |
|
|
1563
|
-
| `replayCapture(loaded, reducer, { throughIndex? })` | Fold captured events through the reducer, stopping at an optional inclusive index |
|
|
1564
|
-
|
|
1565
|
-
`LoadedCapture` contains the verified `capture`, the `contract`, typed public
|
|
1566
|
-
`events` with `createdAt` revived as `Date`, and unmodified operational
|
|
1567
|
-
`rawEvents`.
|
|
1568
|
-
`CaptureReplay` returns `{ state, index }`. A cutpoint of 0 returns the initial
|
|
1569
|
-
state and index 0. Supporting types are `CaptureSource`, `LoadedCapture`,
|
|
1570
|
-
`ReplayCaptureOptions`, and `CaptureReplay`.
|
|
1571
|
-
|
|
1572
|
-
Replay does not use cached snapshot state, construct a server, dispatch a
|
|
1573
|
-
handler, invoke an AI model or tool, or repeat external effects. It reproduces
|
|
1574
|
-
only the current reducer's pure projection over already recorded events.
|
|
1575
|
-
|
|
1576
1487
|
## Entry points
|
|
1577
1488
|
|
|
1578
1489
|
| Entry point | Ships | Peer dependency |
|
|
@@ -1583,7 +1494,6 @@ only the current reducer's pure projection over already recorded events.
|
|
|
1583
1494
|
| `experimental-a2/react` | `createReact` | `react` |
|
|
1584
1495
|
| `experimental-a2/ai` | agent contract, `handlerContext`, reducer, and append builders: isomorphic | `ai` |
|
|
1585
1496
|
| `experimental-a2/ai/server` | AI SDK runner and built-in handlers | `ai` |
|
|
1586
|
-
| `experimental-a2/http` | route-side transport and scheduler helpers | none |
|
|
1587
1497
|
| `experimental-a2/store-postgres` | `postgres`: Postgres store backend | `pg` (or inject a client) |
|
|
1588
1498
|
| `experimental-a2/store-redis` | `redis`: Redis Streams store backend, push-native streaming | `ioredis` (or inject a client) |
|
|
1589
1499
|
| `experimental-a2/store-redis-http` | `redisHttp`: the same Redis store over provider REST APIs (Upstash) | none |
|
|
@@ -1593,9 +1503,6 @@ only the current reducer's pure projection over already recorded events.
|
|
|
1593
1503
|
| `experimental-a2/scheduler-qstash` | `qstash`: signed recovery and timers | `@upstash/qstash` |
|
|
1594
1504
|
| `experimental-a2/cache-indexeddb` | `indexedDb` browser cache | none |
|
|
1595
1505
|
| `experimental-a2/otel` | `otel` telemetry adapter | `@opentelemetry/api` |
|
|
1596
|
-
| `experimental-a2/devtools` | versioned wire protocol and exact capture codec | none |
|
|
1597
|
-
| `experimental-a2/devtools/server` | durable read-only dashboard handler | none |
|
|
1598
|
-
| `experimental-a2/testing` | Node-only capture loading and pure reducer replay | none |
|
|
1599
1506
|
|
|
1600
1507
|
Core `experimental-a2` imports none of the backends, enforced by a browser-bundle
|
|
1601
1508
|
test in CI, not just convention. Importing `experimental-a2/store-postgres` is what
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Errors
|
|
3
|
-
description: One error class,
|
|
3
|
+
description: One error class, stable codes, and clear rules about what append will never throw for.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
## `A2Error`
|
|
@@ -35,6 +35,7 @@ export async function POST(req: Request) {
|
|
|
35
35
|
| Code | Thrown when | Retryable |
|
|
36
36
|
| ------------------------- | ------------------------------------------------------------------ | --------- |
|
|
37
37
|
| `INVALID_PAYLOAD` | a payload fails its schema; nothing was written, and `details` carries the issues ([Standard Schema](https://standardschema.dev) format) | no |
|
|
38
|
+
| `FORBIDDEN` | `server.fetch` authorization denies a parsed stream, history read, push, or presence update | no |
|
|
38
39
|
| `UNKNOWN_EVENT_TYPE` | an event type isn't in the contract's `events` map | no |
|
|
39
40
|
| `UNKNOWN_PRESENCE_FIELD` | a presence field isn't in the contract's `presence` map | no |
|
|
40
41
|
| `PRESENCE_NOT_SUPPORTED` | the contract declares `presence` but the configured store has no presence capability (thrown at construction; for environment-resolved stores, deferred to the first store use) | no |
|
|
@@ -73,12 +74,9 @@ The push route serializes an `A2Error` as:
|
|
|
73
74
|
```
|
|
74
75
|
|
|
75
76
|
with a mapped status: `INVALID_PAYLOAD`, `UNKNOWN_EVENT_TYPE`, and
|
|
76
|
-
`PARTIAL_DUPLICATE_BATCH` are 400; `
|
|
77
|
-
`STORE_UNAVAILABLE` is 503.
|
|
77
|
+
`PARTIAL_DUPLICATE_BATCH` are 400; `FORBIDDEN` is 403;
|
|
78
|
+
`SUPERSEDED_ATTEMPT` is 409; `STORE_UNAVAILABLE` is 503.
|
|
78
79
|
|
|
79
80
|
The client's `push` deserializes the body back into an `A2Error`, so client
|
|
80
81
|
and server code branch on identical codes. `push` auto-retries only
|
|
81
|
-
`STORE_UNAVAILABLE`.
|
|
82
|
-
`deserializeError`, ships in `experimental-a2/http` alongside `handle`;
|
|
83
|
-
a `before` hook that wants the wire's own error shapes returns
|
|
84
|
-
`errorResponse(new A2Error(...))`.
|
|
82
|
+
`STORE_UNAVAILABLE`. `server.fetch` owns the HTTP serialization.
|
package/package.json
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "experimental-a2",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Durable sync and reactions for things with a lifecycle: one event log, derived state, and live client per session.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
|
-
"bin": {
|
|
8
|
-
"a2": "./dist/cli-bin.js"
|
|
9
|
-
},
|
|
10
7
|
"sideEffects": false,
|
|
11
8
|
"engines": {
|
|
12
9
|
"node": ">=22.13"
|
|
@@ -39,7 +36,6 @@
|
|
|
39
36
|
"browser": "./dist/ai-server.browser.js",
|
|
40
37
|
"default": "./dist/ai-server.js"
|
|
41
38
|
},
|
|
42
|
-
"./http": "./dist/http.js",
|
|
43
39
|
"./store-memory": "./dist/store-memory.js",
|
|
44
40
|
"./store-sqlite": "./dist/store-sqlite.js",
|
|
45
41
|
"./store-postgres": "./dist/store-postgres.js",
|
|
@@ -49,15 +45,6 @@
|
|
|
49
45
|
"./scheduler-vercel": "./dist/scheduler-vercel.js",
|
|
50
46
|
"./cache-indexeddb": "./dist/cache-indexeddb.js",
|
|
51
47
|
"./otel": "./dist/otel.js",
|
|
52
|
-
"./devtools": "./dist/devtools.js",
|
|
53
|
-
"./devtools/server": {
|
|
54
|
-
"browser": "./dist/devtools-server.browser.js",
|
|
55
|
-
"default": "./dist/devtools-server.js"
|
|
56
|
-
},
|
|
57
|
-
"./testing": {
|
|
58
|
-
"browser": "./dist/testing.browser.js",
|
|
59
|
-
"default": "./dist/testing.js"
|
|
60
|
-
},
|
|
61
48
|
"./package.json": "./package.json"
|
|
62
49
|
},
|
|
63
50
|
"peerDependencies": {
|