experimental-a2 0.0.0 → 0.2.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 +43 -0
- package/dist/ai-server.browser.js +2 -2
- package/dist/ai-server.d.ts +19 -7
- package/dist/ai-server.js +730 -96
- package/dist/ai.d.ts +32 -11
- package/dist/ai.js +253 -75
- package/dist/client.d.ts +1 -1
- package/dist/client.js +4 -4
- package/dist/{contract-B0kAXoaL.js → contract-CG_adnu_.js} +2 -1
- package/dist/{contract-DL8btVd9.d.ts → contract-C_3dIIEU.d.ts} +4 -1
- package/dist/devtools-server.browser.js +2 -2
- package/dist/devtools-server.js +1 -1
- package/dist/http.d.ts +1 -1
- package/dist/http.js +4 -3
- package/dist/idempotent-replay-BMyHrP0L.js +19 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +1 -1
- package/dist/{internal-Dm8Ejnud.js → internal-D6wNxTck.js} +3 -3
- package/dist/{log-Dg1I8NRr.d.ts → log-ldf5g8Cx.d.ts} +74 -56
- package/dist/log-memory.d.ts +1 -1
- package/dist/log-memory.js +173 -96
- package/dist/{log-polling-RO7kclzR.js → log-polling-6COoN60V.js} +1 -1
- package/dist/log-postgres.d.ts +1 -1
- package/dist/log-postgres.js +235 -192
- package/dist/log-redis.d.ts +1 -1
- package/dist/log-redis.js +453 -263
- package/dist/log-sqlite.d.ts +1 -1
- package/dist/log-sqlite.js +216 -127
- package/dist/otel.d.ts +1 -1
- package/dist/otel.js +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/recovery-vercel.d.ts +2 -2
- package/dist/recovery-vercel.js +9 -10
- package/dist/server-DJgD2YWP.js +877 -0
- package/dist/server.browser.js +4 -4
- package/dist/server.d.ts +46 -27
- package/dist/server.js +1 -1
- package/dist/{telemetry-C78al20p.d.ts → telemetry-Cso0qyHQ.d.ts} +1 -1
- package/dist/{wire-2QpU1EtJ.js → wire-BVsgR8o9.js} +1 -1
- package/docs/01-quickstart.mdx +7 -7
- package/docs/concepts/01-contracts.mdx +22 -22
- package/docs/concepts/02-handlers.mdx +223 -89
- package/docs/concepts/03-durability.mdx +199 -112
- package/docs/concepts/04-state.mdx +27 -1
- package/docs/guides/01-timers.mdx +4 -4
- package/docs/guides/02-cancellation.mdx +32 -4
- package/docs/guides/05-production.mdx +61 -27
- package/docs/guides/06-ai-agents.mdx +151 -70
- package/docs/guides/07-devtools.mdx +6 -3
- package/docs/guides/08-application-data.mdx +5 -6
- package/docs/index.mdx +30 -14
- package/docs/reference/01-api.mdx +305 -70
- package/package.json +31 -31
- package/dist/server-DYsnKTTy.js +0 -780
package/docs/index.mdx
CHANGED
|
@@ -10,7 +10,7 @@ pnpm add experimental-a2
|
|
|
10
10
|
A2 is durable sync and reactions for things with a lifecycle.
|
|
11
11
|
|
|
12
12
|
An order, agent run, approval, or import gets a session. You append events.
|
|
13
|
-
Handlers react to them, and usually
|
|
13
|
+
Handlers react to them, and usually return the next one. The browser follows
|
|
14
14
|
the same log and folds the same state as the server. That's the whole model;
|
|
15
15
|
the rest of this page is it happening.
|
|
16
16
|
|
|
@@ -41,7 +41,7 @@ export const orders = a2.contract({
|
|
|
41
41
|
|
|
42
42
|
Handlers are plain async functions: no determinism rules, no replay, no
|
|
43
43
|
wrappers around side effects. Each one reacts to a fact and usually
|
|
44
|
-
|
|
44
|
+
returns the next one.
|
|
45
45
|
|
|
46
46
|
```ts server/orders.ts
|
|
47
47
|
import { createServer } from 'experimental-a2/server'
|
|
@@ -50,10 +50,10 @@ import { orders } from '@/contracts'
|
|
|
50
50
|
export const ordersServer = createServer({
|
|
51
51
|
contract: orders,
|
|
52
52
|
handlers: {
|
|
53
|
-
created: async ({ event
|
|
53
|
+
created: async ({ event }) => {
|
|
54
54
|
// your side effect; event.id makes a stable idempotency key:
|
|
55
55
|
// await notifyShop(event.payload, { idempotencyKey: event.id })
|
|
56
|
-
|
|
56
|
+
return { type: 'shop.notified', payload: {} }
|
|
57
57
|
},
|
|
58
58
|
},
|
|
59
59
|
})
|
|
@@ -61,8 +61,8 @@ export const ordersServer = createServer({
|
|
|
61
61
|
|
|
62
62
|
## Open a session
|
|
63
63
|
|
|
64
|
-
A session is one instance of the contract, with its own log
|
|
65
|
-
|
|
64
|
+
A session is one instance of the contract, with its own ordered log. It is the
|
|
65
|
+
thing A2 keeps durable and live. The id is yours:
|
|
66
66
|
|
|
67
67
|
- `session('order-8194')`: an order moving through fulfillment
|
|
68
68
|
- `session('chat-a8c1')`: an agent chat streaming its reply
|
|
@@ -70,7 +70,8 @@ ordering. It is the thing A2 keeps durable and live. The id is yours:
|
|
|
70
70
|
- `session('doc-roadmap')`: a document with three people typing in it
|
|
71
71
|
|
|
72
72
|
Anything whose story spans more than one request. Put events in the same
|
|
73
|
-
session when they
|
|
73
|
+
session when they form one history or view. Handlers are concurrent unless
|
|
74
|
+
they share a lane. Any server code can open
|
|
74
75
|
one and append: a route handler, a webhook, a cron job.
|
|
75
76
|
|
|
76
77
|
```ts app/api/orders/route.ts
|
|
@@ -165,11 +166,13 @@ twenty lines; [Live UI](/guides/react) wires it end to end.
|
|
|
165
166
|
|
|
166
167
|
## The properties
|
|
167
168
|
|
|
168
|
-
- **Handlers are plain async functions.**
|
|
169
|
-
|
|
169
|
+
- **Handlers are plain async functions.** They run concurrently by default.
|
|
170
|
+
Use a lane for selective FIFO execution.
|
|
171
|
+
- **Handlers are optional per event type.** A log-only event settles during
|
|
172
|
+
append and creates no reaction or recovery work.
|
|
170
173
|
- **History is real.** [`history()`](/concepts/state) returns what
|
|
171
174
|
actually happened, in order. Debugging is reading, not reconstructing.
|
|
172
|
-
- **Nothing inside a session is sacred except its log.** Snapshots,
|
|
175
|
+
- **Nothing inside a session is sacred except its log.** Snapshots, claims, queue
|
|
173
176
|
messages: all disposable caches. Delete them and A2 rebuilds
|
|
174
177
|
everything from the log.
|
|
175
178
|
|
|
@@ -201,6 +204,18 @@ framework. A contract, a log, handlers, reducers.
|
|
|
201
204
|
|
|
202
205
|
</details>
|
|
203
206
|
|
|
207
|
+
<details>
|
|
208
|
+
<summary>Can I use A2 without handlers?</summary>
|
|
209
|
+
|
|
210
|
+
Yes. Omit `handlers` and use `append`, `history`, reducers, and live streams as
|
|
211
|
+
a durable event log. Events settle in their append transaction, with no drain,
|
|
212
|
+
claim, or queue message. A contract can also mix handled and log-only event
|
|
213
|
+
types. A2 decides per event type.
|
|
214
|
+
|
|
215
|
+
See [Events without handlers](/concepts/handlers#events-without-handlers).
|
|
216
|
+
|
|
217
|
+
</details>
|
|
218
|
+
|
|
204
219
|
<details>
|
|
205
220
|
<summary>How do I wait five days?</summary>
|
|
206
221
|
|
|
@@ -223,8 +238,8 @@ derived view exactly. See [Cancellation](/guides/cancellation).
|
|
|
223
238
|
<details>
|
|
224
239
|
<summary>What happens when a handler keeps failing?</summary>
|
|
225
240
|
|
|
226
|
-
It retries with backoff, then dead-letters after ten caught failures
|
|
227
|
-
|
|
241
|
+
It retries with backoff, then dead-letters after ten caught failures. Later
|
|
242
|
+
events in the same lane wait; unlaned events and other lanes continue.
|
|
228
243
|
[Durability](/concepts/durability) has the exact guarantees and the
|
|
229
244
|
recovery story behind them.
|
|
230
245
|
|
|
@@ -233,8 +248,9 @@ recovery story behind them.
|
|
|
233
248
|
<details>
|
|
234
249
|
<summary>What does production need?</summary>
|
|
235
250
|
|
|
236
|
-
|
|
237
|
-
|
|
251
|
+
A durable log backend, such as Postgres or Redis. Add queue-backed recovery
|
|
252
|
+
when the contract has handlers. A log-only contract needs no reaction
|
|
253
|
+
infrastructure. Development needs neither: SQLite appears under `.a2/` and
|
|
238
254
|
state survives restarts. See [Going to production](/guides/production).
|
|
239
255
|
|
|
240
256
|
</details>
|
|
@@ -25,6 +25,12 @@ ArkType, anything that implements it. Validators must be synchronous
|
|
|
25
25
|
(async ones are rejected here, at definition time), and the validated
|
|
26
26
|
output is what's stored.
|
|
27
27
|
|
|
28
|
+
### `contract.batch(...events)`
|
|
29
|
+
|
|
30
|
+
Preserves the literal types in a heterogeneous handler-returned event array.
|
|
31
|
+
It returns the same events as a readonly tuple; A2 validates them when the
|
|
32
|
+
handler completes.
|
|
33
|
+
|
|
28
34
|
### `contract.reducer(options)`
|
|
29
35
|
|
|
30
36
|
```ts
|
|
@@ -60,12 +66,21 @@ createServer(options: {
|
|
|
60
66
|
log?: A2Log // default: sqlite in dev, memory in tests, required in prod
|
|
61
67
|
recovery?: A2Recovery
|
|
62
68
|
telemetry?: A2Telemetry // optional instrumentation; see experimental-a2/otel
|
|
69
|
+
validatePush?: (context: PushValidationContext) => void | PromiseLike<void>
|
|
63
70
|
handlers?: {
|
|
64
71
|
[type]:
|
|
65
|
-
|
|
|
66
|
-
| {
|
|
72
|
+
| Handler
|
|
73
|
+
| {
|
|
74
|
+
abortOn?: AbortSpec
|
|
75
|
+
lane?: string | ((ctx: LaneContext) => string)
|
|
76
|
+
handler: Handler
|
|
77
|
+
}
|
|
67
78
|
}
|
|
68
79
|
}): A2Server
|
|
80
|
+
|
|
81
|
+
type Handler = (
|
|
82
|
+
ctx: Context,
|
|
83
|
+
) => Promise<void | AppendInput | readonly AppendInput[]>
|
|
69
84
|
```
|
|
70
85
|
|
|
71
86
|
Implements a contract: binds the vocabulary to storage and reactions.
|
|
@@ -74,43 +89,118 @@ be silently missing because the module that registered it wasn't
|
|
|
74
89
|
imported. Compose across files by spreading objects into `handlers`
|
|
75
90
|
(note: a duplicate key under spread silently last-wins).
|
|
76
91
|
|
|
92
|
+
Handlers are concurrent by default. A2 starts every eligible event after its
|
|
93
|
+
append commits. Add `lane` when events share a resource and must not overlap.
|
|
94
|
+
Within one session, events with the same lane value run one at a time in log
|
|
95
|
+
order. Different lanes and events without a lane run concurrently. A lane
|
|
96
|
+
resolver receives `{ sessionId, event }`; A2 resolves and stores the value when
|
|
97
|
+
the event is appended, so a later deployment cannot reinterpret pending work.
|
|
98
|
+
For example, `lane: ({ event }) => event.payload.warehouseId` serializes work
|
|
99
|
+
per warehouse while different warehouses continue concurrently.
|
|
100
|
+
|
|
101
|
+
Handlers can return one event or an array. A2 marks the triggering event
|
|
102
|
+
processed and appends the returned batch in one atomic log operation. Returned
|
|
103
|
+
events do not exist when the handler throws. `ctx.session.append(name, ...events)`
|
|
104
|
+
is different: it commits immediately, so its events may run while the current
|
|
105
|
+
handler is still active unless a lane orders them.
|
|
106
|
+
|
|
107
|
+
Handlers are optional per event type. An event type without one settles in
|
|
108
|
+
the append transaction with no dispatch attempt. If the session has no older
|
|
109
|
+
pending handler work, A2 starts no drain or recovery arm. A server with no
|
|
110
|
+
handlers is a durable event log with no reaction infrastructure. See
|
|
111
|
+
[Events without handlers](/concepts/handlers#events-without-handlers).
|
|
112
|
+
|
|
77
113
|
Server-only by construction: `experimental-a2/server` is the only entry point that
|
|
78
114
|
can reach a log backend, and its exports map resolves to a loud error
|
|
79
115
|
under the browser condition.
|
|
80
116
|
|
|
117
|
+
`validatePush({ sessionId, events })` runs only when `events` came from
|
|
118
|
+
`parsePushBody()`. It runs before contract schema validation and before the log
|
|
119
|
+
append, so throwing rejects the complete push without writing anything. Direct
|
|
120
|
+
trusted server appends and handler appends bypass it. `parsePushBody()` creates
|
|
121
|
+
the runtime provenance brand after reading the envelope; a caller-supplied
|
|
122
|
+
field with the same name is ignored, and the brand is not stored in the log.
|
|
123
|
+
|
|
81
124
|
`abortOn` names the events that fire `ctx.signal` while a handler runs.
|
|
82
125
|
an array matches by type; an object takes per-type predicates for
|
|
83
126
|
targeted cancellation:
|
|
84
127
|
|
|
85
128
|
```ts
|
|
86
129
|
generate: {
|
|
87
|
-
abortOn: {
|
|
130
|
+
abortOn: {
|
|
131
|
+
cancelled: (event, trigger, { attempt }) =>
|
|
132
|
+
event.payload.of === `${trigger.id}:${attempt}`,
|
|
133
|
+
},
|
|
88
134
|
handler: async (ctx) => { /* ... */ },
|
|
89
135
|
}
|
|
90
136
|
```
|
|
91
137
|
|
|
138
|
+
The predicate's third argument contains the triggering event's durable
|
|
139
|
+
`attempt`, so a cancellation can fence a specific recovered run.
|
|
140
|
+
|
|
92
141
|
The context every handler receives:
|
|
93
142
|
|
|
94
|
-
| Property
|
|
95
|
-
|
|
|
96
|
-
| `ctx.event`
|
|
97
|
-
| `ctx.attempt`
|
|
98
|
-
| `ctx.
|
|
99
|
-
| `ctx.
|
|
100
|
-
| `ctx.signal` | `AbortSignal`: active only with `abortOn` |
|
|
143
|
+
| Property | Type |
|
|
144
|
+
| ------------- | --------------------------------------------------------- |
|
|
145
|
+
| `ctx.event` | `Event`: the triggering event |
|
|
146
|
+
| `ctx.attempt` | durable 1-based dispatch claim ordinal |
|
|
147
|
+
| `ctx.session` | this session's `id`, `append`, `history`, `state`, `stream` |
|
|
148
|
+
| `ctx.signal` | `AbortSignal`: active only with `abortOn` |
|
|
101
149
|
|
|
102
150
|
`ctx.attempt` starts at `1` and increments on every durable claim. It may skip
|
|
103
151
|
when a process dies before handler entry.
|
|
104
152
|
|
|
153
|
+
`ctx.session.id` equals `ctx.event.sessionId`. Its `history`, `state`, and
|
|
154
|
+
`stream` methods are the same session operations returned by
|
|
155
|
+
`server.session(id)`. `ctx.session.state(reducer)` reads the cached snapshot
|
|
156
|
+
plus immutable log tail and returns `{ state, index }`. The index is a
|
|
157
|
+
consistent committed frontier captured when the call runs. It includes the
|
|
158
|
+
triggering event and may include events committed later while another handler
|
|
159
|
+
is active.
|
|
160
|
+
|
|
161
|
+
The handler-local append is specialized:
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
ctx.session.append(name: string, ...events: AppendInput[]): Promise<Event[]>
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
The name is an idempotency key scoped to the triggering event for events whose
|
|
168
|
+
`id` is omitted. A2 derives each omitted id from the trigger id, name, and event
|
|
169
|
+
position. The same name and generated-id batch returns the existing rows
|
|
170
|
+
across retries. Changing that batch conflicts with those rows. An explicit
|
|
171
|
+
event `id` wins over the generated id, which lets different triggering events
|
|
172
|
+
converge on one fact. A root `server.session(id).append(...events)` takes no
|
|
173
|
+
name.
|
|
174
|
+
|
|
175
|
+
A state read and following append are not atomic. Concurrent appends and
|
|
176
|
+
retries may move the frontier between them. A generic join should use a
|
|
177
|
+
monotone readiness predicate and a stable explicit output event `id`, so every
|
|
178
|
+
eligible attempt converges on the same append.
|
|
179
|
+
|
|
105
180
|
### `server.session(id)`
|
|
106
181
|
|
|
107
182
|
```ts
|
|
108
183
|
server.session(id: string): Session
|
|
109
184
|
```
|
|
110
185
|
|
|
111
|
-
A handle on one instance of the contract. The session is the unit of
|
|
112
|
-
recovery, state, and live sync.
|
|
113
|
-
|
|
186
|
+
A handle on one instance of the contract. The session is the unit of log
|
|
187
|
+
ordering, lane keys, recovery, state, and live sync. Handler execution is
|
|
188
|
+
concurrent unless events share a lane. Creating the handle does no I/O;
|
|
189
|
+
nothing loads until you append, read, or stream.
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
const session = server.session('order-42')
|
|
193
|
+
|
|
194
|
+
session.id
|
|
195
|
+
session.append(...events)
|
|
196
|
+
session.history()
|
|
197
|
+
session.state(reducer)
|
|
198
|
+
session.stream({ startAt })
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
`session.id` is the id passed to `server.session(id)`. Root `append` takes only
|
|
202
|
+
events. The handler-local form at `ctx.session.append` adds its required name
|
|
203
|
+
before the events.
|
|
114
204
|
|
|
115
205
|
### `server.drain(sessionId)`
|
|
116
206
|
|
|
@@ -118,10 +208,11 @@ until you append, read, or stream.
|
|
|
118
208
|
server.drain(sessionId: string): Promise<{ settled: boolean }>
|
|
119
209
|
```
|
|
120
210
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
211
|
+
Claims every currently eligible event and runs their handlers concurrently.
|
|
212
|
+
`settled` means no actionable or live-claimed work remains. A dead-lettered
|
|
213
|
+
event can leave later work in its lane blocked while other lanes continue.
|
|
214
|
+
You'll rarely call this yourself; it is the primitive recovery callbacks use.
|
|
215
|
+
The public result stays this simple boolean.
|
|
125
216
|
|
|
126
217
|
### `A2Log`
|
|
127
218
|
|
|
@@ -132,15 +223,31 @@ Custom adapters implement these atomic drain methods:
|
|
|
132
223
|
type EventCause = {
|
|
133
224
|
index: number
|
|
134
225
|
attempt: number
|
|
226
|
+
batchSize?: number // present on named handler appends
|
|
135
227
|
}
|
|
136
228
|
|
|
229
|
+
type AppendEvent = {
|
|
230
|
+
type: string
|
|
231
|
+
payload: unknown
|
|
232
|
+
id?: string
|
|
233
|
+
cause?: EventCause
|
|
234
|
+
lane?: string
|
|
235
|
+
settled?: true // internal: no handler is registered for this event type
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
type ReturnedEvent = AppendEvent & { id: string }
|
|
239
|
+
|
|
137
240
|
type StoredEvent = Event & {
|
|
138
241
|
cause: EventCause | null
|
|
242
|
+
lane: string | null
|
|
139
243
|
processedAt: Date | null
|
|
140
244
|
processedByAttempt: number | null
|
|
245
|
+
returnedEventIds: string[] | null
|
|
141
246
|
firstClaimedAt: Date | null
|
|
142
247
|
lastClaimedAt: Date | null
|
|
143
248
|
attemptCount: number
|
|
249
|
+
claimHolder: string | null
|
|
250
|
+
claimExpiresAt: Date | null
|
|
144
251
|
failureCount: number
|
|
145
252
|
lastFailedAt: Date | null
|
|
146
253
|
lastFailedAttempt: number | null
|
|
@@ -148,31 +255,48 @@ type StoredEvent = Event & {
|
|
|
148
255
|
failedAt: Date | null
|
|
149
256
|
}
|
|
150
257
|
|
|
151
|
-
type
|
|
152
|
-
|
|
153
|
-
|
|
258
|
+
type LogAppendResult = {
|
|
259
|
+
events: StoredEvent[]
|
|
260
|
+
hasPending: boolean
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
type LogClaimAvailableResult =
|
|
264
|
+
| { outcome: 'claimed'; events: StoredEvent[] }
|
|
265
|
+
| { outcome: 'busy'; retryAt: Date }
|
|
154
266
|
| { outcome: 'settled' }
|
|
155
267
|
|
|
156
|
-
type
|
|
157
|
-
|
|
|
268
|
+
type CompleteAttemptResult =
|
|
269
|
+
| { outcome: 'completed'; events: StoredEvent[] }
|
|
158
270
|
| { outcome: 'superseded' }
|
|
159
271
|
|
|
160
272
|
interface A2Log {
|
|
161
|
-
|
|
273
|
+
append(
|
|
274
|
+
sessionId: string,
|
|
275
|
+
events: AppendEvent[],
|
|
276
|
+
): Promise<LogAppendResult>
|
|
277
|
+
|
|
278
|
+
claimAvailable(options: {
|
|
162
279
|
sessionId: string
|
|
163
280
|
holder: string
|
|
164
281
|
ttlMs: number
|
|
165
282
|
expiresAtMs?: number
|
|
166
|
-
|
|
167
|
-
}): Promise<
|
|
283
|
+
excludeIndexes?: readonly number[]
|
|
284
|
+
}): Promise<LogClaimAvailableResult>
|
|
168
285
|
|
|
169
|
-
|
|
286
|
+
renewClaims(options: {
|
|
170
287
|
sessionId: string
|
|
171
288
|
holder: string
|
|
172
|
-
|
|
289
|
+
indexes: number[]
|
|
290
|
+
ttlMs: number
|
|
291
|
+
expiresAtMs?: number
|
|
292
|
+
}): Promise<number[]>
|
|
293
|
+
|
|
294
|
+
completeAttempt(options: {
|
|
295
|
+
sessionId: string
|
|
296
|
+
index: number
|
|
173
297
|
attempt: number
|
|
174
|
-
|
|
175
|
-
}): Promise<
|
|
298
|
+
events: ReturnedEvent[]
|
|
299
|
+
}): Promise<CompleteAttemptResult>
|
|
176
300
|
|
|
177
301
|
failAttempt(options: {
|
|
178
302
|
sessionId: string
|
|
@@ -189,17 +313,21 @@ interface A2Log {
|
|
|
189
313
|
|
|
190
314
|
| Method | Atomic effect |
|
|
191
315
|
| --- | --- |
|
|
192
|
-
| `
|
|
193
|
-
| `
|
|
194
|
-
| `
|
|
316
|
+
| `append` | Write a consecutive batch. Events carrying core's internal `settled` flag receive `processedAt` in the same transaction, with no dispatch attempt. |
|
|
317
|
+
| `claimAvailable` | Claim every eligible event. All unlaned events are independent; only the lowest-index unfinished event in each lane is eligible. A claim records its holder, expiry, timestamps, and next `attemptCount`. |
|
|
318
|
+
| `renewClaims` | Extend the listed live claims still owned by the holder. Expired, completed, failed, and superseded claims are omitted and cannot be revived. |
|
|
319
|
+
| `completeAttempt` | Fence on the current attempt, mark the parent processed, store its exact ordered `returnedEventIds`, and append the returned batch in the same transaction. A same-attempt retry returns the committed children; a stale attempt returns `superseded`. |
|
|
320
|
+
| `failAttempt` | Record one current caught failure, clear its claim, and dead-letter at `maxFailures`. A repeated failure acknowledgment is idempotent; stale attempts return `superseded`. |
|
|
195
321
|
|
|
196
322
|
`attemptCount` counts claims, including abandoned ones. `failureCount` counts
|
|
197
323
|
caught failures and dead-letters at ten. See
|
|
198
324
|
[Durability](/concepts/durability#one-event-many-attempts) for the recovery
|
|
199
|
-
model. `cause` identifies the event and handler attempt whose
|
|
200
|
-
first persisted the child. A null cause
|
|
201
|
-
|
|
202
|
-
|
|
325
|
+
model. `cause` identifies the event and handler attempt whose
|
|
326
|
+
`ctx.session.append` or return value first persisted the child. A null cause
|
|
327
|
+
means a top-level event.
|
|
328
|
+
`lane` is the session-scoped serialized group resolved before append.
|
|
329
|
+
`returnedEventIds` makes a lost completion
|
|
330
|
+
acknowledgment recoverable without accepting a partial child batch. Lifecycle
|
|
203
331
|
timestamps are adapter clock values for their atomic log operations, not exact
|
|
204
332
|
database commit times.
|
|
205
333
|
Built-in adapters persist these fields inside their existing atomic operations,
|
|
@@ -223,6 +351,36 @@ identical batch returns the original rows. (Events parsed by
|
|
|
223
351
|
`parsePushBody` are accepted directly, the push-route path.) See
|
|
224
352
|
[Durability](/concepts/durability).
|
|
225
353
|
|
|
354
|
+
After the write, A2 dispatches only event types with registered handlers.
|
|
355
|
+
Other event types are already settled by the append itself. An unhandled
|
|
356
|
+
append still starts session healing when older handled work is pending. The
|
|
357
|
+
log reports that session-wide pending state as part of the atomic append, so
|
|
358
|
+
this decision needs no follow-up read.
|
|
359
|
+
|
|
360
|
+
### `session.append.dispatch(...events)`
|
|
361
|
+
|
|
362
|
+
```ts
|
|
363
|
+
session.append.dispatch(
|
|
364
|
+
...events: Array<{ type: string; payload: unknown; id?: string }>
|
|
365
|
+
): Promise<Event[]>
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
Commits the same atomic batch, but sends pending work directly to configured
|
|
369
|
+
recovery instead of starting an inline drain. The call awaits acceptance of an
|
|
370
|
+
immediate recovery message. It throws before writing if the server has no
|
|
371
|
+
recovery adapter.
|
|
372
|
+
|
|
373
|
+
If the queue send fails, the events are already durable. Give them explicit
|
|
374
|
+
IDs and retry the same dispatch safely. A batch that leaves no pending handler
|
|
375
|
+
work does not send a recovery message. Dispatch chooses how this append wakes
|
|
376
|
+
the session; it does not reserve events for one worker. A drain that is already
|
|
377
|
+
active may still claim newly eligible work first.
|
|
378
|
+
|
|
379
|
+
`dispatch` exists only on a server session's top-level append. Handler-scoped
|
|
380
|
+
`ctx.session.append(name, ...events)` already runs inside an active drain and
|
|
381
|
+
does not expose `dispatch`. Its immediate children and atomically returned
|
|
382
|
+
children become eligible in that drain.
|
|
383
|
+
|
|
226
384
|
### `session.history()`
|
|
227
385
|
|
|
228
386
|
```ts
|
|
@@ -375,18 +533,38 @@ Pure typed inputs for `append()` and `push()`:
|
|
|
375
533
|
|
|
376
534
|
| Input | Events |
|
|
377
535
|
| --- | --- |
|
|
378
|
-
| `inputs.message(message)` | `ai.message.created
|
|
379
|
-
| `inputs.seed(message)` | `ai.message.created`
|
|
380
|
-
| `inputs.approval(response)` | `ai.approval.responded`
|
|
381
|
-
| `inputs.input(response)` | `ai.input.responded`
|
|
382
|
-
| `inputs.requestInput(request)` | `ai.input.requested` |
|
|
383
|
-
| `inputs.retry(options)` |
|
|
536
|
+
| `inputs.message(message)` | `ai.message.created`; the server schedules a user turn |
|
|
537
|
+
| `inputs.seed(message)` | `ai.message.created` for a trusted server append |
|
|
538
|
+
| `inputs.approval(response)` | `ai.approval.responded` only |
|
|
539
|
+
| `inputs.input(response)` | `ai.input.responded` only |
|
|
540
|
+
| `inputs.requestInput(request)` | `ai.input.requested` for a trusted server append |
|
|
541
|
+
| `inputs.retry(options)` | `ai.retry.requested` |
|
|
384
542
|
| `inputs.interrupt(options)` | `ai.message.interrupted` |
|
|
385
543
|
|
|
386
|
-
`inputs` deliberately has no session lifecycle methods.
|
|
387
|
-
`ai.session.created` and `ai.session.closed` events
|
|
388
|
-
application uses them. Input event ids are stable for the interaction
|
|
389
|
-
describe, so a lost append acknowledgment can be resent safely.
|
|
544
|
+
`inputs` deliberately has no session lifecycle methods. Append explicit
|
|
545
|
+
`ai.session.created` and `ai.session.closed` events from trusted server code
|
|
546
|
+
when an application uses them. Input event ids are stable for the interaction
|
|
547
|
+
they describe, so a lost append acknowledgment can be resent safely. Browser
|
|
548
|
+
ingress accepts user messages, approval and input responses, interruptions,
|
|
549
|
+
and explicit retries. Only built-in server handlers append generation requests
|
|
550
|
+
and AI lifecycle events. `inputs.seed()` and `inputs.requestInput()` are for
|
|
551
|
+
trusted server appends.
|
|
552
|
+
|
|
553
|
+
Approval and input request/response payloads require the active
|
|
554
|
+
`generationId`. Clients copy it from the pending request, which prevents a
|
|
555
|
+
delayed response from satisfying a newer model step.
|
|
556
|
+
|
|
557
|
+
`ai.retry.requested` carries `{ messageId, responseMessageId, retryId }`.
|
|
558
|
+
`retryId` identifies one user action. The server converts the fact into an
|
|
559
|
+
`ai.generation.requested` whose reason is `retry`.
|
|
560
|
+
|
|
561
|
+
`ai.generation.failed` sets `stepLimit: true` when `maxSteps` rejects a
|
|
562
|
+
continuation before another model step starts.
|
|
563
|
+
|
|
564
|
+
`ai.message.interrupted` carries `{ messageId, generationId?, reason?,
|
|
565
|
+
lastSeenIndex? }`. Omit `generationId` only while the matching response is in
|
|
566
|
+
the requested phase and `activeGeneration` does not exist yet. Once a
|
|
567
|
+
generation starts, include its id to fence delayed interruption actions.
|
|
390
568
|
|
|
391
569
|
### `events` and `createEvents(options?)`
|
|
392
570
|
|
|
@@ -402,9 +580,16 @@ createReducer({ contract, name? }): Reducer<AIState>
|
|
|
402
580
|
|
|
403
581
|
Builds the standard AI projection for a compatible contract. `AIState`
|
|
404
582
|
contains session lifecycle, messages, generation status, pending approvals
|
|
405
|
-
and input, tool activity, compaction, usage,
|
|
583
|
+
and input, tool activity, compaction, usage, the last error, and
|
|
584
|
+
`activeRequestId` and `activeResponseMessageId`, plus
|
|
585
|
+
`responseGenerationIds: Record<string, string>`.
|
|
586
|
+
`activeRequestId` is the server-authorized generation request and fences
|
|
587
|
+
delayed requests before their generation starts. `activeResponseMessageId`
|
|
588
|
+
identifies the requested response until `activeGeneration` exists.
|
|
406
589
|
`activeProjection` holds the indexed chunk/tool frontier only while a
|
|
407
|
-
generation is active; terminal events clear it.
|
|
590
|
+
generation is active; terminal events clear it. `responseGenerationIds` keeps
|
|
591
|
+
the latest generation owner for each response message, so late events from a
|
|
592
|
+
superseded owner cannot alter the projection. Extension events are ignored.
|
|
408
593
|
|
|
409
594
|
### `deriveUIMessages(history)` and `reduceAIState(state, event)`
|
|
410
595
|
|
|
@@ -423,6 +608,7 @@ createAgentServer({
|
|
|
423
608
|
tools?,
|
|
424
609
|
instructions?,
|
|
425
610
|
generation?,
|
|
611
|
+
maxSteps?,
|
|
426
612
|
generate?,
|
|
427
613
|
compaction?,
|
|
428
614
|
progress?,
|
|
@@ -430,21 +616,58 @@ createAgentServer({
|
|
|
430
616
|
}): A2Server
|
|
431
617
|
```
|
|
432
618
|
|
|
433
|
-
Creates the standard server for an agent. A2 runs
|
|
434
|
-
persists each `UIMessageChunk` once in a
|
|
435
|
-
messages synchronously, extracts tool and
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
619
|
+
Creates the standard server for an agent. A2 runs one AI SDK `streamText()`
|
|
620
|
+
model step per durable generation, persists each `UIMessageChunk` once in a
|
|
621
|
+
durable progress batch, projects messages synchronously, extracts tool and
|
|
622
|
+
approval lifecycle events, executes local tools, and records completion,
|
|
623
|
+
usage, interruption, and failure. `model` accepts an AI SDK model string or
|
|
624
|
+
provider model. `model` and `instructions` can be values or per-generation
|
|
625
|
+
async resolvers.
|
|
626
|
+
|
|
627
|
+
`generation` contains per-step settings such as `temperature`,
|
|
628
|
+
`maxOutputTokens`, `topP`, provider options, and tool approval policy. A2 owns
|
|
629
|
+
the one-step stop condition, local tool execution, and continuation. `maxSteps`
|
|
630
|
+
limits one complete assistant response and defaults to 20. Support for
|
|
631
|
+
individual model settings depends on the selected model and provider.
|
|
632
|
+
`generation` excludes `stopWhen`, tool execution callbacks, tool callers, tool
|
|
633
|
+
context, sandbox execution, and the tool approval secret. Model-step timeouts
|
|
634
|
+
remain available; tool-execution timeouts do not.
|
|
442
635
|
|
|
443
636
|
`generate(context)` optionally replaces the default AI SDK generation. It
|
|
444
637
|
receives messages, the resolved model and instructions, tools, generation
|
|
445
|
-
settings, request state, and the abort signal. It returns
|
|
446
|
-
`ReadableStream<UIMessageChunk>`. A2 continues to own
|
|
447
|
-
around that stream.
|
|
638
|
+
settings, request state, and the abort signal. It returns exactly one model
|
|
639
|
+
step as a `ReadableStream<UIMessageChunk>`. A2 continues to own tool execution,
|
|
640
|
+
continuation, and the durable lifecycle around that stream.
|
|
641
|
+
|
|
642
|
+
Generation requests use one durable lane, so model calls for a session do not
|
|
643
|
+
overlap. Queued-turn policy also waits to schedule a later user message until
|
|
644
|
+
the active assistant response, including its complete tool loop, reaches a
|
|
645
|
+
terminal event. The policy matters in addition to lane FIFO because a tool
|
|
646
|
+
continuation may be appended after the later user message. A generation
|
|
647
|
+
failure keeps later messages queued until the failed response is explicitly
|
|
648
|
+
retried or interrupted.
|
|
649
|
+
|
|
650
|
+
Independent authorized tool handlers run concurrently without a fixed
|
|
651
|
+
concurrency limit. A private coordinator reducer tracks generation closure,
|
|
652
|
+
cancellation, calls, approvals, and terminal results for the active response.
|
|
653
|
+
Completed responses do not accumulate in its state. Its `ctx.session.state()` reads the
|
|
654
|
+
durable snapshot plus log tail. Process memory is not authoritative. Concurrent
|
|
655
|
+
join checks return the same deterministic continuation event, so they use
|
|
656
|
+
`ctx.session.append()` and storage deduplicates the race.
|
|
657
|
+
|
|
658
|
+
Tool results and generation completion have no fixed relative order. The
|
|
659
|
+
continuation predicate needs both the closed model step and every required
|
|
660
|
+
terminal result.
|
|
661
|
+
|
|
662
|
+
Single-owner terminal events use returned-event causality. They commit
|
|
663
|
+
atomically with parent completion and receive the parent's durable cause.
|
|
664
|
+
Many-owner joins and preliminary streaming outputs use immediate idempotent
|
|
665
|
+
appends. Provider calls and external tool side effects are at least once and
|
|
666
|
+
require their own idempotency. Approved provider-executed calls may satisfy the
|
|
667
|
+
join from the approval response. Provider tools with declared deferred-result
|
|
668
|
+
support wait for their provider result instead of a local executor. An
|
|
669
|
+
authenticated provider callback appends the terminal `ai.tool.result` through
|
|
670
|
+
the trusted server session API; the browser push allowlist rejects it.
|
|
448
671
|
|
|
449
672
|
`compaction` has `shouldCompact(context)` and `compact(context)` callbacks.
|
|
450
673
|
When selected, both the request and the replacement messages enter the log.
|
|
@@ -456,8 +679,20 @@ This entry point is server-only and resolves to a throwing browser stub.
|
|
|
456
679
|
|
|
457
680
|
Returns the built-in A2 handler table without constructing a server. Spread
|
|
458
681
|
it into `createServer({ handlers })` beside application handlers when you need
|
|
459
|
-
a custom assembly.
|
|
460
|
-
|
|
682
|
+
a custom assembly. The table handles input facts, generation requests, model
|
|
683
|
+
step completion, tool calls, approval responses, and terminal tool results.
|
|
684
|
+
Application handlers spread later can deliberately replace a built-in
|
|
685
|
+
handler. Custom assemblies pass `validateAgentPush` as
|
|
686
|
+
`createServer({ validatePush })` to preserve the browser boundary.
|
|
687
|
+
|
|
688
|
+
### `validateAgentPush(context)`
|
|
689
|
+
|
|
690
|
+
`validateAgentPush({ sessionId, events }): void`
|
|
691
|
+
|
|
692
|
+
Accepts the browser interaction allowlist: user messages, approval and input
|
|
693
|
+
responses, interruptions, and explicit retries. It rejects server-authored
|
|
694
|
+
scheduling and lifecycle events, trusted seed messages, and input requests.
|
|
695
|
+
`createAgentServer()` installs it automatically.
|
|
461
696
|
|
|
462
697
|
See [Durable AI agents](/guides/ai-agents) for the protocol and complete
|
|
463
698
|
examples.
|
|
@@ -545,29 +780,29 @@ alert on are all mid-span.
|
|
|
545
780
|
| Attribute | Span | When | Values |
|
|
546
781
|
| --------------------- | ----------- | ----- | ----------------------------------------------------- |
|
|
547
782
|
| `a2.append.source` | `a2.append` | start | `external` \| `handler` |
|
|
783
|
+
| `a2.append.mode` | `a2.append` | start | `inline` \| `dispatch` |
|
|
548
784
|
| `a2.append.types` | `a2.append` | start | comma-joined event types |
|
|
549
785
|
| `a2.append.count` | `a2.append` | start | batch size |
|
|
550
786
|
| `a2.append.armed` | `a2.append` | mid | `false` when the recovery arm failed and this append degraded to append-driven healing |
|
|
551
|
-
| `a2.drain.
|
|
552
|
-
| `a2.drain.outcome` | `a2.drain` | mid | `settled` \| `busy` \| `stalled` \| `handed_off` |
|
|
787
|
+
| `a2.drain.outcome` | `a2.drain` | mid | `settled` \| `busy` \| `stalled` |
|
|
553
788
|
| `a2.drain.processed` | `a2.drain` | mid | events processed this pass |
|
|
554
789
|
| `a2.event.type` | `a2.event` | start | the event's type |
|
|
555
790
|
| `a2.event.index` | `a2.event` | start | log position |
|
|
556
791
|
| `a2.event.id` | `a2.event` | start | event id |
|
|
557
792
|
| `a2.event.attempt` | `a2.event` | start | same durable 1-based ordinal as `ctx.attempt` |
|
|
558
|
-
| `a2.event.
|
|
793
|
+
| `a2.event.lane` | `a2.event` | start | stored lane value; absent for concurrent unlaned work |
|
|
794
|
+
| `a2.event.handled` | `a2.event` | start | normally `true`; `false` when a custom-adapter row has no handler |
|
|
559
795
|
| `a2.event.outcome` | `a2.event` | mid | `processed` \| `failed` \| `dead_lettered` \| `superseded` |
|
|
560
796
|
| `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
797
|
| `a2.state.reducer` | `a2.state` | start | the reducer's name |
|
|
563
798
|
| `a2.state.snapshot` | `a2.state` | mid | `hit` \| `miss` \| `rejected` (schema guard discarded it) |
|
|
564
799
|
| `a2.state.folded` | `a2.state` | mid | events folded past the snapshot |
|
|
565
800
|
| `a2.state.index` | `a2.state` | mid | the frontier the returned state reflects |
|
|
566
801
|
|
|
567
|
-
Drain outcomes: `settled` means nothing actionable is left; `busy` means
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
802
|
+
Drain outcomes: `settled` means nothing actionable is left; `busy` means live
|
|
803
|
+
per-event claims remain; `stalled` means a caught failure needs a later retry.
|
|
804
|
+
A dead-lettered event blocks only later events in its lane. Other lanes remain
|
|
805
|
+
eligible.
|
|
571
806
|
|
|
572
807
|
A failing handler marks its `a2.event` span with the exception and
|
|
573
808
|
error status; `a2.event.outcome = dead_lettered` is the attribute to
|