experimental-a2 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/dist/ai-server.browser.d.ts +1 -0
  3. package/dist/ai-server.browser.js +4 -0
  4. package/dist/ai-server.d.ts +65 -0
  5. package/dist/ai-server.js +494 -0
  6. package/dist/ai.d.ts +282 -0
  7. package/dist/ai.js +922 -0
  8. package/dist/cache-indexeddb.d.ts +1 -0
  9. package/dist/cache-indexeddb.js +0 -0
  10. package/dist/client.d.ts +90 -0
  11. package/dist/client.js +410 -0
  12. package/dist/contract-B0kAXoaL.js +60 -0
  13. package/dist/contract-DL8btVd9.d.ts +161 -0
  14. package/dist/devtools-server.browser.d.ts +1 -0
  15. package/dist/devtools-server.browser.js +4 -0
  16. package/dist/devtools-server.d.ts +22 -0
  17. package/dist/devtools-server.js +1087 -0
  18. package/dist/errors-BJRMd-h6.js +23 -0
  19. package/dist/errors-xL_JTXsY.d.ts +20 -0
  20. package/dist/http.d.ts +44 -0
  21. package/dist/http.js +119 -0
  22. package/dist/index.d.ts +5 -0
  23. package/dist/index.js +3 -0
  24. package/dist/inspection-E7qbD0Xj.js +10 -0
  25. package/dist/internal-Dm8Ejnud.js +36 -0
  26. package/dist/log-Dg1I8NRr.d.ts +245 -0
  27. package/dist/log-memory.d.ts +11 -0
  28. package/dist/log-memory.js +345 -0
  29. package/dist/log-polling-RO7kclzR.js +83 -0
  30. package/dist/log-postgres.d.ts +40 -0
  31. package/dist/log-postgres.js +628 -0
  32. package/dist/log-redis.d.ts +31 -0
  33. package/dist/log-redis.js +711 -0
  34. package/dist/log-sqlite.d.ts +17 -0
  35. package/dist/log-sqlite.js +450 -0
  36. package/dist/log-yJbXUf72.js +5 -0
  37. package/dist/otel.d.ts +12 -0
  38. package/dist/otel.js +41 -0
  39. package/dist/react.d.ts +54 -0
  40. package/dist/react.js +85 -0
  41. package/dist/recovery-vercel.d.ts +60 -0
  42. package/dist/recovery-vercel.js +120 -0
  43. package/dist/retryable-lazy-DZWmHpii.js +19 -0
  44. package/dist/server-DYsnKTTy.js +780 -0
  45. package/dist/server.browser.d.ts +1 -0
  46. package/dist/server.browser.js +11 -0
  47. package/dist/server.d.ts +136 -0
  48. package/dist/server.js +2 -0
  49. package/dist/telemetry-C78al20p.d.ts +32 -0
  50. package/dist/validate-XKT4FSNn.js +28 -0
  51. package/dist/wire-2QpU1EtJ.js +62 -0
  52. package/docs/01-quickstart.mdx +214 -0
  53. package/docs/concepts/01-contracts.mdx +138 -0
  54. package/docs/concepts/02-handlers.mdx +146 -0
  55. package/docs/concepts/03-durability.mdx +230 -0
  56. package/docs/concepts/04-state.mdx +133 -0
  57. package/docs/guides/01-timers.mdx +85 -0
  58. package/docs/guides/02-cancellation.mdx +107 -0
  59. package/docs/guides/03-react.mdx +234 -0
  60. package/docs/guides/04-local-first.mdx +88 -0
  61. package/docs/guides/05-production.mdx +179 -0
  62. package/docs/guides/06-ai-agents.mdx +659 -0
  63. package/docs/guides/07-devtools.mdx +101 -0
  64. package/docs/guides/08-application-data.mdx +114 -0
  65. package/docs/index.mdx +282 -0
  66. package/docs/reference/01-api.mdx +637 -0
  67. package/docs/reference/02-errors.mdx +77 -0
  68. package/package.json +111 -0
@@ -0,0 +1,637 @@
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
+ }): Contract
15
+ ```
16
+
17
+ Defines one kind of lifecycle: a name plus the events it understands. The name
18
+ must be unique per app; it identifies the contract in storage and queue
19
+ messages. The result is a plain, importable, **isomorphic** value: the server
20
+ implements it, reducers derive from it, the browser types its pushes off it.
21
+ See [Contracts and sessions](/concepts/contracts).
22
+
23
+ Schemas are [Standard Schema](https://standardschema.dev): Zod, Valibot,
24
+ ArkType, anything that implements it. Validators must be synchronous
25
+ (async ones are rejected here, at definition time), and the validated
26
+ output is what's stored.
27
+
28
+ ### `contract.reducer(options)`
29
+
30
+ ```ts
31
+ contract.reducer(options: {
32
+ name: string // identity; keys cached snapshots
33
+ initialState: S // the seed (schema input, when stateSchema is set)
34
+ stateSchema?: StandardSchemaV1 // declares S, checks initialState, guards cached folds
35
+ }): { fold(fn: (state: S, event: Event) => S): Reducer<S> }
36
+ ```
37
+
38
+ Derives a view from the contract. Two steps by design: the first call
39
+ fixes the state and event types, so `.fold()` receives fully concrete
40
+ ones. Literal unions in your state survive, with zero annotations.
41
+
42
+ The name is the reducer's identity; it keys cached snapshots, so rename it
43
+ when the fold's logic changes. Two reducers never fight over a cache
44
+ entry, and a rename (or a `stateSchema` mismatch, when present)
45
+ invalidates, and the next read refolds from raw events. See
46
+ [Reading state](/concepts/state).
47
+
48
+ ### `A2Error`
49
+
50
+ Everything A2 throws. One class, discriminated by `code`. See
51
+ [Errors](/reference/errors).
52
+
53
+ ## `experimental-a2/server`
54
+
55
+ ### `createServer(options)`
56
+
57
+ ```ts
58
+ createServer(options: {
59
+ contract: Contract // the vocabulary this server implements
60
+ log?: A2Log // default: sqlite in dev, memory in tests, required in prod
61
+ recovery?: A2Recovery
62
+ telemetry?: A2Telemetry // optional instrumentation; see experimental-a2/otel
63
+ handlers?: {
64
+ [type]:
65
+ | (ctx: Context) => Promise<void>
66
+ | { abortOn: AbortSpec; handler: (ctx: Context) => Promise<void> }
67
+ }
68
+ }): A2Server
69
+ ```
70
+
71
+ Implements a contract: binds the vocabulary to storage and reactions.
72
+ Handlers are one table, complete at construction, so a handler can never
73
+ be silently missing because the module that registered it wasn't
74
+ imported. Compose across files by spreading objects into `handlers`
75
+ (note: a duplicate key under spread silently last-wins).
76
+
77
+ Server-only by construction: `experimental-a2/server` is the only entry point that
78
+ can reach a log backend, and its exports map resolves to a loud error
79
+ under the browser condition.
80
+
81
+ `abortOn` names the events that fire `ctx.signal` while a handler runs.
82
+ an array matches by type; an object takes per-type predicates for
83
+ targeted cancellation:
84
+
85
+ ```ts
86
+ generate: {
87
+ abortOn: { cancelled: (event, trigger) => event.payload.of === trigger.id },
88
+ handler: async (ctx) => { /* ... */ },
89
+ }
90
+ ```
91
+
92
+ The context every handler receives:
93
+
94
+ | Property | Type |
95
+ | ------------------ | ------------------------------------------ |
96
+ | `ctx.event` | `Event`: the triggering event |
97
+ | `ctx.attempt` | durable 1-based dispatch claim ordinal |
98
+ | `ctx.append(...e)` | append to this session, typed, atomic |
99
+ | `ctx.history()` | `Promise<Event[]>`: raw log, oldest first |
100
+ | `ctx.signal` | `AbortSignal`: active only with `abortOn` |
101
+
102
+ `ctx.attempt` starts at `1` and increments on every durable claim. It may skip
103
+ when a process dies before handler entry.
104
+
105
+ ### `server.session(id)`
106
+
107
+ ```ts
108
+ server.session(id: string): Session
109
+ ```
110
+
111
+ A handle on one instance of the contract. The session is the unit of ordering,
112
+ recovery, state, and live sync. Creating the handle does no I/O; nothing loads
113
+ until you append, read, or stream.
114
+
115
+ ### `server.drain(sessionId)`
116
+
117
+ ```ts
118
+ server.drain(sessionId: string): Promise<{ settled: boolean }>
119
+ ```
120
+
121
+ Processes the session's pending events in order. `settled` means every
122
+ event is processed or dead-lettered. You'll rarely call this yourself;
123
+ it's the primitive recovery callbacks use. The public result stays this
124
+ simple boolean. Recovery retries the first event without a processed marker.
125
+
126
+ ### `A2Log`
127
+
128
+ Custom adapters implement these atomic drain methods:
129
+
130
+ ```ts
131
+ // Custom server log adapter:
132
+ type EventCause = {
133
+ index: number
134
+ attempt: number
135
+ }
136
+
137
+ type StoredEvent = Event & {
138
+ cause: EventCause | null
139
+ processedAt: Date | null
140
+ processedByAttempt: number | null
141
+ firstClaimedAt: Date | null
142
+ lastClaimedAt: Date | null
143
+ attemptCount: number
144
+ failureCount: number
145
+ lastFailedAt: Date | null
146
+ lastFailedAttempt: number | null
147
+ lastError: string | null
148
+ failedAt: Date | null
149
+ }
150
+
151
+ type LogClaimResult =
152
+ | { outcome: 'claimed'; event: StoredEvent }
153
+ | { outcome: 'busy' }
154
+ | { outcome: 'settled' }
155
+
156
+ type LogHandoffResult =
157
+ | LogClaimResult
158
+ | { outcome: 'superseded' }
159
+
160
+ interface A2Log {
161
+ claimNext(options: {
162
+ sessionId: string
163
+ holder: string
164
+ ttlMs: number
165
+ expiresAtMs?: number
166
+ maxIndex?: number
167
+ }): Promise<LogClaimResult>
168
+
169
+ completeAndClaimNext(options: {
170
+ sessionId: string
171
+ holder: string
172
+ completedIndex: number
173
+ attempt: number
174
+ maxIndex?: number
175
+ }): Promise<LogHandoffResult>
176
+
177
+ failAttempt(options: {
178
+ sessionId: string
179
+ index: number
180
+ attempt: number
181
+ error: string
182
+ maxFailures: number
183
+ }): Promise<{
184
+ outcome: 'failed' | 'dead_lettered' | 'superseded'
185
+ failureCount: number
186
+ }>
187
+ }
188
+ ```
189
+
190
+ | Method | Atomic effect |
191
+ | --- | --- |
192
+ | `claimNext` | Select the ordered head, respect optional `maxIndex`, acquire the lease, increment `attemptCount`, and update claim timestamps. |
193
+ | `completeAndClaimNext` | Complete the current attempt once; record `processedByAttempt`; claim and timestamp the next only while the same holder owns a live lease. Stale attempts return `superseded`. |
194
+ | `failAttempt` | Record a current caught failure, `lastFailedAt`, and `lastFailedAttempt`; dead-letter at `maxFailures`. Stale attempts return `superseded`. |
195
+
196
+ `attemptCount` counts claims, including abandoned ones. `failureCount` counts
197
+ caught failures and dead-letters at ten. See
198
+ [Durability](/concepts/durability#one-event-many-attempts) for the recovery
199
+ model. `cause` identifies the event and handler attempt whose `ctx.append`
200
+ first persisted the child. A null cause means a root or a legacy event whose
201
+ origin is unknown. Migrated rows can have `firstClaimedAt === null` with a
202
+ nonzero `attemptCount`; the historical first claim is unknowable. Lifecycle
203
+ timestamps are adapter clock values for their atomic log operations, not exact
204
+ database commit times.
205
+ Built-in adapters persist these fields inside their existing atomic operations,
206
+ with no extra backend round trip.
207
+
208
+ ## Session
209
+
210
+ ### `session.append(...events)`
211
+
212
+ ```ts
213
+ session.append(
214
+ ...events: Array<{ type: string; payload: unknown; id?: string }>
215
+ ): Promise<Event[]>
216
+ ```
217
+
218
+ The only way to move a session forward. Payloads are validated against
219
+ the contract's schemas before anything is written. A multi-event append
220
+ is atomic: all-or-nothing, consecutive positions, one transaction. Pass
221
+ `id` to make an append idempotent across retries; re-sending an
222
+ identical batch returns the original rows. (Events parsed by
223
+ `parsePushBody` are accepted directly, the push-route path.) See
224
+ [Durability](/concepts/durability).
225
+
226
+ ### `session.history()`
227
+
228
+ ```ts
229
+ session.history(): Promise<Event[]>
230
+ ```
231
+
232
+ Every event in the session, oldest first. Always the raw log.
233
+
234
+ ### `session.state(reducer)`
235
+
236
+ ```ts
237
+ session.state(reducer: Reducer<S>): Promise<{ state: S; index: number }>
238
+ ```
239
+
240
+ The log folded through a reducer. `index` is the position the state
241
+ reflects. Hand it to the client to resume a stream from exactly there. The
242
+ snapshot and event tail are one log operation; snapshot write-back runs in
243
+ platform `waitUntil`. This read never dispatches handlers.
244
+
245
+ ### `session.stream(options?)`
246
+
247
+ ```ts
248
+ session.stream(options?: { startAt?: number }): AsyncIterable<Event>
249
+ ```
250
+
251
+ A live feed of the session's events, starting after `startAt`.
252
+ Server-side only; expose it over SSE with `sseResponse`. Subscribing never
253
+ dispatches handlers.
254
+
255
+ ## Event
256
+
257
+ ```ts
258
+ type Event = {
259
+ id: string
260
+ type: string
261
+ payload: unknown // typed by the contract's schema for `type`
262
+ index: number // position in the session's log, from 1
263
+ sessionId: string
264
+ createdAt: Date
265
+ }
266
+ ```
267
+
268
+ ## `experimental-a2/react`
269
+
270
+ ### `createReact(options)`
271
+
272
+ ```ts
273
+ createReact(options: {
274
+ client: A2Client // bind a shared client: api + reducer + session identity
275
+ }): { SessionProvider, useSession }
276
+
277
+ // or, standalone (one client per provider mount):
278
+ createReact(options: {
279
+ reducer: Reducer // types everything: state, pushes, the feed
280
+ cache?: A2ClientCache // optional; see local-first
281
+ }): { SessionProvider, useSession }
282
+ ```
283
+
284
+ A factory, like `createContext`: call it once in a `'use client'`
285
+ module, export the bound pair. Server components import
286
+ `SessionProvider` from that module as a client reference; client
287
+ components import `useSession` from the same file. See
288
+ [Live UI](/guides/react#the-session-module).
289
+
290
+ `createReact({ reducer })` is also supported for provider-bound clients. Use a
291
+ shared `A2Client` when code outside the provider needs to push before a route
292
+ transition and the destination should adopt the same optimistic session.
293
+
294
+ ### `<SessionProvider>`
295
+
296
+ | Prop | What it is |
297
+ | --------------- | ------------------------------------------------------ |
298
+ | `sessionId` | which session to subscribe to |
299
+ | `initialState` | server-rendered state |
300
+ | `initialIndex` | the fold's frontier, where the stream resumes |
301
+ | `initialEvents` | optional server-rendered history through that frontier |
302
+
303
+ Opens the stream on mount, closes it on unmount, reconnects with
304
+ backoff from the current frontier.
305
+
306
+ ### `useSession()`
307
+
308
+ ```ts
309
+ const { state, push, events, index, connection } = useSession()
310
+ ```
311
+
312
+ `state` folds live events through the shared reducer, `events` is the
313
+ raw observed feed, and `index` is the stream frontier, the
314
+ `lastSeenIndex` for [cancellation](/guides/cancellation).
315
+
316
+ `push` appends optimistically: validated locally, rolled back on
317
+ rejection, retried only for `LOG_UNAVAILABLE`. Awaiting it gives the
318
+ server ack; the same result carries `confirmed`, a lazy promise for
319
+ the later moment when the live stream has delivered the batch back and
320
+ the view shows server truth:
321
+
322
+ ```ts
323
+ const t0 = performance.now()
324
+ const result = push({ type: 'created', payload })
325
+ await result // the server ack: push→ack latency
326
+ await result.confirmed // the stream round-trip: ack→stream latency
327
+ const roundtripMs = performance.now() - t0
328
+ ```
329
+
330
+ A rejected push rejects both promises with the same `A2Error`;
331
+ `confirmed` is materialized only when accessed, so ignoring it costs
332
+ nothing. It also reads as intent: `await push(...).confirmed` is
333
+ "continue once this is server truth".
334
+
335
+ `connection` is a discriminated union; impossible states are
336
+ unrepresentable (an `error` only exists while disconnected):
337
+
338
+ ```ts
339
+ type Connection =
340
+ | { status: 'idle' }
341
+ | { status: 'connecting'; reconnects: number; error: Error | null }
342
+ | { status: 'live'; reconnects: number }
343
+ | { status: 'closed' }
344
+ ```
345
+
346
+ "Reconnecting…" is `status === 'connecting' && reconnects > 0`. And
347
+ it's honest about silence: the server heartbeats the stream (`: ping`
348
+ every 15s), and a client that hears nothing for two beats treats the
349
+ connection as dead (aborts it and reconnects), so `live` means bytes
350
+ are actually flowing, not "the socket hasn't errored yet". See
351
+ [Live UI](/guides/react).
352
+
353
+ ## `experimental-a2/ai`
354
+
355
+ ### `agent(options)`
356
+
357
+ ```ts
358
+ agent(options: {
359
+ name: string
360
+ events?: Record<string, StandardSchemaV1>
361
+ messageSchema?: StandardSchemaV1<UIMessage>
362
+ reducerName?: string
363
+ }): AgentDefinition
364
+ ```
365
+
366
+ Defines an isomorphic agent as ordinary A2 parts. Its `contract` contains the
367
+ built-in AI protocol plus `events`, and its `reducer` projects `AIState`. The
368
+ definition exposes only `contract` and `reducer`.
369
+
370
+ Built-in event names cannot be replaced by `options.events`.
371
+
372
+ ### `inputs`
373
+
374
+ Pure typed inputs for `append()` and `push()`:
375
+
376
+ | Input | Events |
377
+ | --- | --- |
378
+ | `inputs.message(message)` | `ai.message.created`, plus `ai.generation.requested` for a user message |
379
+ | `inputs.seed(message)` | `ai.message.created` only |
380
+ | `inputs.approval(response)` | `ai.approval.responded` + continuation request |
381
+ | `inputs.input(response)` | `ai.input.responded` + continuation request |
382
+ | `inputs.requestInput(request)` | `ai.input.requested` |
383
+ | `inputs.retry(options)` | a fresh `ai.generation.requested` for a failed response |
384
+ | `inputs.interrupt(options)` | `ai.message.interrupted` |
385
+
386
+ `inputs` deliberately has no session lifecycle methods. Push explicit
387
+ `ai.session.created` and `ai.session.closed` events directly when an
388
+ application uses them. Input event ids are stable for the interaction they
389
+ describe, so a lost append acknowledgment can be resent safely.
390
+
391
+ ### `events` and `createEvents(options?)`
392
+
393
+ The built-in Standard Schema definitions. Use `events` for the default AI SDK
394
+ `UIMessage`; use `createEvents({ messageSchema })` to validate a more specific
395
+ message type. Both are isomorphic.
396
+
397
+ ### `createReducer(options)`
398
+
399
+ ```ts
400
+ createReducer({ contract, name? }): Reducer<AIState>
401
+ ```
402
+
403
+ Builds the standard AI projection for a compatible contract. `AIState`
404
+ contains session lifecycle, messages, generation status, pending approvals
405
+ and input, tool activity, compaction, usage, and the last error.
406
+ `activeProjection` holds the indexed chunk/tool frontier only while a
407
+ generation is active; terminal events clear it. Extension events are ignored.
408
+
409
+ ### `deriveUIMessages(history)` and `reduceAIState(state, event)`
410
+
411
+ `deriveUIMessages()` folds a raw A2 event history directly into AI SDK
412
+ `UIMessage[]`. `reduceAIState()` is the event-at-a-time form used by the
413
+ standard reducer. Both synchronously project the same chunk-only protocol.
414
+
415
+ ## `experimental-a2/ai/server`
416
+
417
+ ### `createAgentServer(options)`
418
+
419
+ ```ts
420
+ createAgentServer({
421
+ agent: AgentDefinition,
422
+ model: LanguageModel | (context => LanguageModel),
423
+ tools?,
424
+ instructions?,
425
+ generation?,
426
+ generate?,
427
+ compaction?,
428
+ progress?,
429
+ log?, recovery?, telemetry?, handlers?,
430
+ }): A2Server
431
+ ```
432
+
433
+ Creates the standard server for an agent. A2 runs an AI SDK `ToolLoopAgent`,
434
+ persists each `UIMessageChunk` once in a durable progress batch, projects
435
+ messages synchronously, extracts tool and approval lifecycle events, and
436
+ records completion, usage, interruption, and failure. `model` accepts an AI
437
+ SDK model string or provider model. `model` and `instructions` can be values or per-generation async
438
+ resolvers. `generation` accepts the remaining `ToolLoopAgent` settings, such
439
+ as `temperature`, `maxOutputTokens`, `topP`, stop conditions, and provider
440
+ options. Support for individual settings depends on the selected model and
441
+ provider.
442
+
443
+ `generate(context)` optionally replaces the default AI SDK generation. It
444
+ receives messages, the resolved model and instructions, tools, generation
445
+ settings, request state, and the abort signal. It returns a
446
+ `ReadableStream<UIMessageChunk>`. A2 continues to own the durable lifecycle
447
+ around that stream.
448
+
449
+ `compaction` has `shouldCompact(context)` and `compact(context)` callbacks.
450
+ When selected, both the request and the replacement messages enter the log.
451
+ `progress` controls durable batching with `maxChunks` and `maxDelayMs`.
452
+
453
+ This entry point is server-only and resolves to a throwing browser stub.
454
+
455
+ ### `createHandlers(options)`
456
+
457
+ Returns the built-in A2 handler table without constructing a server. Spread
458
+ it into `createServer({ handlers })` beside application handlers when you need
459
+ a custom assembly. Application handlers spread later can deliberately replace
460
+ a built-in handler.
461
+
462
+ See [Durable AI agents](/guides/ai-agents) for the protocol and complete
463
+ examples.
464
+
465
+ ## `experimental-a2/client`
466
+
467
+ ```ts
468
+ createClient(options: {
469
+ reducer: Reducer
470
+ api: string
471
+ gcTime?: number // idle session lifetime; 5 minutes by default
472
+ }): A2Client
473
+ ```
474
+
475
+ The framework-agnostic session client `experimental-a2/react` is built on: the SSE
476
+ subscription with frontier resume and reconnection, the optimistic push
477
+ queue with ack/rollback, and the local fold. `client.session(id, {
478
+ initialState?, initialIndex?, initialEvents? })` returns a handle with
479
+ `getSnapshot()`/`subscribe()` (the `useSyncExternalStore` contract),
480
+ `push()`, `connect()`, and `close()`. Snapshots carry `state`, `events`,
481
+ `index`, and `connection` (the same fields `useSession` exposes), and
482
+ `push` returns the same ack-then-`confirmed` result. Use it directly
483
+ from any other framework, or none.
484
+
485
+ Within one `A2Client`, repeated `session(id)` calls return the same live
486
+ handle. `initialEvents` seeds the raw feed for SSR. A later call with a further
487
+ server-rendered frontier advances the handle, merges its history, and does not
488
+ drop pending optimistic events; a stale render cannot move it backward. Idle
489
+ handles are evicted after `gcTime`. This memory layer is separate from
490
+ `experimental-a2/cache-indexeddb`: memory preserves identity across route transitions,
491
+ while IndexedDB preserves the replica across reloads.
492
+
493
+ ## `experimental-a2/http`
494
+
495
+ | Helper | What it does |
496
+ | ----------------------- | ------------------------------------------------------------------------------ |
497
+ | `parsePushBody(req)` | validates the push envelope `{ sessionId, events }`, throws `INVALID_PAYLOAD` |
498
+ | `sseResponse(iterable)` | pipes a `session.stream()` iterable into an SSE `Response`, with a `: connected` prelude and a `: ping` heartbeat every 15s |
499
+ | `errorResponse(err)` | serializes an `A2Error` to `{ error: { code, message, details } }` + status |
500
+ | `deserializeError(body)` | rebuilds an `A2Error` from a wire body, or `null` if the body isn't one |
501
+
502
+ Together the last two are the `A2Error` wire format that `push` and the
503
+ push route share. See [Errors](/reference/errors#over-the-wire).
504
+
505
+ ## `experimental-a2/cache-indexeddb`
506
+
507
+ ```ts
508
+ indexedDb(): A2ClientCache
509
+ ```
510
+
511
+ A browser-side cache for `SessionProvider`: the session's events
512
+ (immutable; writes are idempotent by index), a folded snapshot
513
+ (invalidated by a reducer rename), and the pending-push queue.
514
+ `cache.clear(sessionId?)` is the logout story. Create instances in client
515
+ code; they're live objects, not serializable props. See
516
+ [Local-first](/guides/local-first).
517
+
518
+ ## `experimental-a2/otel`
519
+
520
+ ```ts
521
+ otel(options?: { tracer?: Tracer; tracerName?: string }): A2Telemetry
522
+ ```
523
+
524
+ OpenTelemetry instrumentation for a server (`@opentelemetry/api` is a
525
+ peer dependency). Pass it as `telemetry`; spans nest through the active
526
+ context, so the happy path lands in one trace: request → append → drain →
527
+ each handler → the events those handlers append.
528
+
529
+ ### The span catalogue
530
+
531
+ Four spans, all carrying `a2.contract` and `a2.session_id`:
532
+
533
+ | Span | Wraps |
534
+ | ----------- | ------------------------------------------------------ |
535
+ | `a2.append` | validation + the log write (one batch) |
536
+ | `a2.drain` | one drain pass over a session's backlog |
537
+ | `a2.event` | one claimed dispatch of one event |
538
+ | `a2.state` | one `state()` read: snapshot-plus-tail load + fold |
539
+
540
+ Attributes arrive in two waves: **at start** (passed to `span()`) and
541
+ **mid-span** (via `handle.setAttribute`; outcomes and counts aren't
542
+ known until the work ran). A recorder must merge both; the ones you
543
+ alert on are all mid-span.
544
+
545
+ | Attribute | Span | When | Values |
546
+ | --------------------- | ----------- | ----- | ----------------------------------------------------- |
547
+ | `a2.append.source` | `a2.append` | start | `external` \| `handler` |
548
+ | `a2.append.types` | `a2.append` | start | comma-joined event types |
549
+ | `a2.append.count` | `a2.append` | start | batch size |
550
+ | `a2.append.armed` | `a2.append` | mid | `false` when the recovery arm failed and this append degraded to append-driven healing |
551
+ | `a2.drain.scope` | `a2.drain` | start | `full` \| `tree` |
552
+ | `a2.drain.outcome` | `a2.drain` | mid | `settled` \| `busy` \| `stalled` \| `handed_off` |
553
+ | `a2.drain.processed` | `a2.drain` | mid | events processed this pass |
554
+ | `a2.event.type` | `a2.event` | start | the event's type |
555
+ | `a2.event.index` | `a2.event` | start | log position |
556
+ | `a2.event.id` | `a2.event` | start | event id |
557
+ | `a2.event.attempt` | `a2.event` | start | same durable 1-based ordinal as `ctx.attempt` |
558
+ | `a2.event.handled` | `a2.event` | start | `false` when no handler is registered |
559
+ | `a2.event.outcome` | `a2.event` | mid | `processed` \| `failed` \| `dead_lettered` \| `superseded` |
560
+ | `a2.event.aborted` | `a2.event` | mid | `true` when `abortOn` fired during the run |
561
+ | `a2.event.lease_lost` | `a2.event` | mid | `true` when the lease was lost mid-handler |
562
+ | `a2.state.reducer` | `a2.state` | start | the reducer's name |
563
+ | `a2.state.snapshot` | `a2.state` | mid | `hit` \| `miss` \| `rejected` (schema guard discarded it) |
564
+ | `a2.state.folded` | `a2.state` | mid | events folded past the snapshot |
565
+ | `a2.state.index` | `a2.state` | mid | the frontier the returned state reflects |
566
+
567
+ Drain outcomes: `settled` means nothing actionable is left; `busy` means
568
+ another drainer holds the session (in-process slot or cross-process lease);
569
+ `stalled` means blocked at a dead-lettered event; `handed_off` means leftovers
570
+ went to a scheduled in-process drain.
571
+
572
+ A failing handler marks its `a2.event` span with the exception and
573
+ error status; `a2.event.outcome = dead_lettered` is the attribute to
574
+ alert on. `superseded` means a stale completion or failure did not change the
575
+ log.
576
+ `A2Telemetry` is a one-method interface; anything with
577
+ `span(name, attributes, fn)` works;
578
+ `otel()` is simply the adapter A2 ships. This catalogue is a contract:
579
+ renames and additions are breaking for dashboards, and are called out in the
580
+ package's `CHANGELOG.md`.
581
+
582
+ ## `experimental-a2/devtools/server`
583
+
584
+ ```ts
585
+ // anywhere on the server:
586
+ import type { DevtoolsServer } from 'experimental-a2/devtools/server'
587
+
588
+ declare function createDevtools(options: {
589
+ servers: readonly DevtoolsServer[]
590
+ authorize?: (
591
+ request: Request,
592
+ ) => boolean | Response | Promise<boolean | Response>
593
+ }): {
594
+ handler(): (request: Request) => Promise<Response>
595
+ }
596
+ ```
597
+
598
+ A read-only dashboard over the servers' durable logs. The handler serves the
599
+ complete browser application, its JSON endpoints, and live SSE invalidations.
600
+ It discovers sessions and reads stored causal, dispatch, completion, failure,
601
+ and snapshot metadata. The causal forest and lifecycle timeline come directly
602
+ from stored events. The initial page response includes the selected dashboard
603
+ data so the browser does not need a contracts, sessions, and detail request
604
+ waterfall. It never drains or heals a session.
605
+
606
+ Without `authorize`, the handler is available outside production and returns
607
+ 404 in production. Returning `false` from `authorize` also returns 404.
608
+ Returning a `Response` passes that response through, which supports redirects
609
+ and authentication challenges.
610
+
611
+ The built-in memory, SQLite, Postgres, and Redis logs support inspection.
612
+ Custom `A2Log` implementations can omit the optional `inspect` interface; the
613
+ dashboard returns 501 for those logs.
614
+
615
+ ## Entry points
616
+
617
+ | Entry point | Ships | Peer dependency |
618
+ | -------------------- | ---------------------------------- | --------------- |
619
+ | `experimental-a2` | `contract`, `A2Error`: isomorphic core | none |
620
+ | `experimental-a2/server` | `createServer`: implement a contract | none |
621
+ | `experimental-a2/client` | `createClient`: framework-agnostic session client | none |
622
+ | `experimental-a2/react` | `createReact` | `react` |
623
+ | `experimental-a2/ai` | agent contract, reducer, and append builders: isomorphic | `ai` |
624
+ | `experimental-a2/ai/server` | AI SDK runner and built-in handlers | `ai` |
625
+ | `experimental-a2/http` | route-side transport helpers | none |
626
+ | `experimental-a2/log-postgres` | `postgres`: Postgres log backend | `pg` (or inject a client) |
627
+ | `experimental-a2/log-redis` | `redis`: Redis Streams log backend, push-native streaming | `ioredis` (or inject a client) |
628
+ | `experimental-a2/log-sqlite` | SQLite log backend | none |
629
+ | `experimental-a2/log-memory` | in-memory log backend | none |
630
+ | `experimental-a2/recovery-vercel` | `vercelQueues` recovery | `@vercel/queue` |
631
+ | `experimental-a2/cache-indexeddb` | `indexedDb` browser cache | none |
632
+ | `experimental-a2/otel` | `otel` telemetry adapter | `@opentelemetry/api` |
633
+ | `experimental-a2/devtools/server` | durable read-only dashboard handler | none |
634
+
635
+ Core `experimental-a2` imports none of the backends, enforced by a browser-bundle
636
+ test in CI, not just convention. Importing `experimental-a2/log-postgres` is what
637
+ pulls in Postgres, never `experimental-a2` itself, and never your client bundle.