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.
- package/CHANGELOG.md +128 -0
- package/dist/ai-server.browser.d.ts +1 -0
- package/dist/ai-server.browser.js +4 -0
- package/dist/ai-server.d.ts +65 -0
- package/dist/ai-server.js +494 -0
- package/dist/ai.d.ts +282 -0
- package/dist/ai.js +922 -0
- package/dist/cache-indexeddb.d.ts +1 -0
- package/dist/cache-indexeddb.js +0 -0
- package/dist/client.d.ts +90 -0
- package/dist/client.js +410 -0
- package/dist/contract-B0kAXoaL.js +60 -0
- package/dist/contract-DL8btVd9.d.ts +161 -0
- package/dist/devtools-server.browser.d.ts +1 -0
- package/dist/devtools-server.browser.js +4 -0
- package/dist/devtools-server.d.ts +22 -0
- package/dist/devtools-server.js +1087 -0
- package/dist/errors-BJRMd-h6.js +23 -0
- package/dist/errors-xL_JTXsY.d.ts +20 -0
- package/dist/http.d.ts +44 -0
- package/dist/http.js +119 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +3 -0
- package/dist/inspection-E7qbD0Xj.js +10 -0
- package/dist/internal-Dm8Ejnud.js +36 -0
- package/dist/log-Dg1I8NRr.d.ts +245 -0
- package/dist/log-memory.d.ts +11 -0
- package/dist/log-memory.js +345 -0
- package/dist/log-polling-RO7kclzR.js +83 -0
- package/dist/log-postgres.d.ts +40 -0
- package/dist/log-postgres.js +628 -0
- package/dist/log-redis.d.ts +31 -0
- package/dist/log-redis.js +711 -0
- package/dist/log-sqlite.d.ts +17 -0
- package/dist/log-sqlite.js +450 -0
- package/dist/log-yJbXUf72.js +5 -0
- package/dist/otel.d.ts +12 -0
- package/dist/otel.js +41 -0
- package/dist/react.d.ts +54 -0
- package/dist/react.js +85 -0
- package/dist/recovery-vercel.d.ts +60 -0
- package/dist/recovery-vercel.js +120 -0
- package/dist/retryable-lazy-DZWmHpii.js +19 -0
- package/dist/server-DYsnKTTy.js +780 -0
- package/dist/server.browser.d.ts +1 -0
- package/dist/server.browser.js +11 -0
- package/dist/server.d.ts +136 -0
- package/dist/server.js +2 -0
- package/dist/telemetry-C78al20p.d.ts +32 -0
- package/dist/validate-XKT4FSNn.js +28 -0
- package/dist/wire-2QpU1EtJ.js +62 -0
- package/docs/01-quickstart.mdx +214 -0
- package/docs/concepts/01-contracts.mdx +138 -0
- package/docs/concepts/02-handlers.mdx +146 -0
- package/docs/concepts/03-durability.mdx +230 -0
- package/docs/concepts/04-state.mdx +133 -0
- package/docs/guides/01-timers.mdx +85 -0
- package/docs/guides/02-cancellation.mdx +107 -0
- package/docs/guides/03-react.mdx +234 -0
- package/docs/guides/04-local-first.mdx +88 -0
- package/docs/guides/05-production.mdx +179 -0
- package/docs/guides/06-ai-agents.mdx +659 -0
- package/docs/guides/07-devtools.mdx +101 -0
- package/docs/guides/08-application-data.mdx +114 -0
- package/docs/index.mdx +282 -0
- package/docs/reference/01-api.mdx +637 -0
- package/docs/reference/02-errors.mdx +77 -0
- 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.
|