experimental-a2 0.0.0 → 0.1.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 +36 -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-BWffWe5A.js +867 -0
- package/dist/server.browser.js +4 -4
- package/dist/server.d.ts +42 -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 +191 -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 +36 -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 +280 -70
- package/package.json +31 -31
- package/dist/server-DYsnKTTy.js +0 -780
|
@@ -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
|
|
227
|
+
}
|
|
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
|
|
135
236
|
}
|
|
136
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,12 @@ 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
|
+
|
|
226
360
|
### `session.history()`
|
|
227
361
|
|
|
228
362
|
```ts
|
|
@@ -375,18 +509,38 @@ Pure typed inputs for `append()` and `push()`:
|
|
|
375
509
|
|
|
376
510
|
| Input | Events |
|
|
377
511
|
| --- | --- |
|
|
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)` |
|
|
512
|
+
| `inputs.message(message)` | `ai.message.created`; the server schedules a user turn |
|
|
513
|
+
| `inputs.seed(message)` | `ai.message.created` for a trusted server append |
|
|
514
|
+
| `inputs.approval(response)` | `ai.approval.responded` only |
|
|
515
|
+
| `inputs.input(response)` | `ai.input.responded` only |
|
|
516
|
+
| `inputs.requestInput(request)` | `ai.input.requested` for a trusted server append |
|
|
517
|
+
| `inputs.retry(options)` | `ai.retry.requested` |
|
|
384
518
|
| `inputs.interrupt(options)` | `ai.message.interrupted` |
|
|
385
519
|
|
|
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.
|
|
520
|
+
`inputs` deliberately has no session lifecycle methods. Append explicit
|
|
521
|
+
`ai.session.created` and `ai.session.closed` events from trusted server code
|
|
522
|
+
when an application uses them. Input event ids are stable for the interaction
|
|
523
|
+
they describe, so a lost append acknowledgment can be resent safely. Browser
|
|
524
|
+
ingress accepts user messages, approval and input responses, interruptions,
|
|
525
|
+
and explicit retries. Only built-in server handlers append generation requests
|
|
526
|
+
and AI lifecycle events. `inputs.seed()` and `inputs.requestInput()` are for
|
|
527
|
+
trusted server appends.
|
|
528
|
+
|
|
529
|
+
Approval and input request/response payloads require the active
|
|
530
|
+
`generationId`. Clients copy it from the pending request, which prevents a
|
|
531
|
+
delayed response from satisfying a newer model step.
|
|
532
|
+
|
|
533
|
+
`ai.retry.requested` carries `{ messageId, responseMessageId, retryId }`.
|
|
534
|
+
`retryId` identifies one user action. The server converts the fact into an
|
|
535
|
+
`ai.generation.requested` whose reason is `retry`.
|
|
536
|
+
|
|
537
|
+
`ai.generation.failed` sets `stepLimit: true` when `maxSteps` rejects a
|
|
538
|
+
continuation before another model step starts.
|
|
539
|
+
|
|
540
|
+
`ai.message.interrupted` carries `{ messageId, generationId?, reason?,
|
|
541
|
+
lastSeenIndex? }`. Omit `generationId` only while the matching response is in
|
|
542
|
+
the requested phase and `activeGeneration` does not exist yet. Once a
|
|
543
|
+
generation starts, include its id to fence delayed interruption actions.
|
|
390
544
|
|
|
391
545
|
### `events` and `createEvents(options?)`
|
|
392
546
|
|
|
@@ -402,9 +556,16 @@ createReducer({ contract, name? }): Reducer<AIState>
|
|
|
402
556
|
|
|
403
557
|
Builds the standard AI projection for a compatible contract. `AIState`
|
|
404
558
|
contains session lifecycle, messages, generation status, pending approvals
|
|
405
|
-
and input, tool activity, compaction, usage,
|
|
559
|
+
and input, tool activity, compaction, usage, the last error, and
|
|
560
|
+
`activeRequestId` and `activeResponseMessageId`, plus
|
|
561
|
+
`responseGenerationIds: Record<string, string>`.
|
|
562
|
+
`activeRequestId` is the server-authorized generation request and fences
|
|
563
|
+
delayed requests before their generation starts. `activeResponseMessageId`
|
|
564
|
+
identifies the requested response until `activeGeneration` exists.
|
|
406
565
|
`activeProjection` holds the indexed chunk/tool frontier only while a
|
|
407
|
-
generation is active; terminal events clear it.
|
|
566
|
+
generation is active; terminal events clear it. `responseGenerationIds` keeps
|
|
567
|
+
the latest generation owner for each response message, so late events from a
|
|
568
|
+
superseded owner cannot alter the projection. Extension events are ignored.
|
|
408
569
|
|
|
409
570
|
### `deriveUIMessages(history)` and `reduceAIState(state, event)`
|
|
410
571
|
|
|
@@ -423,6 +584,7 @@ createAgentServer({
|
|
|
423
584
|
tools?,
|
|
424
585
|
instructions?,
|
|
425
586
|
generation?,
|
|
587
|
+
maxSteps?,
|
|
426
588
|
generate?,
|
|
427
589
|
compaction?,
|
|
428
590
|
progress?,
|
|
@@ -430,21 +592,58 @@ createAgentServer({
|
|
|
430
592
|
}): A2Server
|
|
431
593
|
```
|
|
432
594
|
|
|
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
|
-
|
|
595
|
+
Creates the standard server for an agent. A2 runs one AI SDK `streamText()`
|
|
596
|
+
model step per durable generation, persists each `UIMessageChunk` once in a
|
|
597
|
+
durable progress batch, projects messages synchronously, extracts tool and
|
|
598
|
+
approval lifecycle events, executes local tools, and records completion,
|
|
599
|
+
usage, interruption, and failure. `model` accepts an AI SDK model string or
|
|
600
|
+
provider model. `model` and `instructions` can be values or per-generation
|
|
601
|
+
async resolvers.
|
|
602
|
+
|
|
603
|
+
`generation` contains per-step settings such as `temperature`,
|
|
604
|
+
`maxOutputTokens`, `topP`, provider options, and tool approval policy. A2 owns
|
|
605
|
+
the one-step stop condition, local tool execution, and continuation. `maxSteps`
|
|
606
|
+
limits one complete assistant response and defaults to 20. Support for
|
|
607
|
+
individual model settings depends on the selected model and provider.
|
|
608
|
+
`generation` excludes `stopWhen`, tool execution callbacks, tool callers, tool
|
|
609
|
+
context, sandbox execution, and the tool approval secret. Model-step timeouts
|
|
610
|
+
remain available; tool-execution timeouts do not.
|
|
442
611
|
|
|
443
612
|
`generate(context)` optionally replaces the default AI SDK generation. It
|
|
444
613
|
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.
|
|
614
|
+
settings, request state, and the abort signal. It returns exactly one model
|
|
615
|
+
step as a `ReadableStream<UIMessageChunk>`. A2 continues to own tool execution,
|
|
616
|
+
continuation, and the durable lifecycle around that stream.
|
|
617
|
+
|
|
618
|
+
Generation requests use one durable lane, so model calls for a session do not
|
|
619
|
+
overlap. Queued-turn policy also waits to schedule a later user message until
|
|
620
|
+
the active assistant response, including its complete tool loop, reaches a
|
|
621
|
+
terminal event. The policy matters in addition to lane FIFO because a tool
|
|
622
|
+
continuation may be appended after the later user message. A generation
|
|
623
|
+
failure keeps later messages queued until the failed response is explicitly
|
|
624
|
+
retried or interrupted.
|
|
625
|
+
|
|
626
|
+
Independent authorized tool handlers run concurrently without a fixed
|
|
627
|
+
concurrency limit. A private coordinator reducer tracks generation closure,
|
|
628
|
+
cancellation, calls, approvals, and terminal results for the active response.
|
|
629
|
+
Completed responses do not accumulate in its state. Its `ctx.session.state()` reads the
|
|
630
|
+
durable snapshot plus log tail. Process memory is not authoritative. Concurrent
|
|
631
|
+
join checks return the same deterministic continuation event, so they use
|
|
632
|
+
`ctx.session.append()` and storage deduplicates the race.
|
|
633
|
+
|
|
634
|
+
Tool results and generation completion have no fixed relative order. The
|
|
635
|
+
continuation predicate needs both the closed model step and every required
|
|
636
|
+
terminal result.
|
|
637
|
+
|
|
638
|
+
Single-owner terminal events use returned-event causality. They commit
|
|
639
|
+
atomically with parent completion and receive the parent's durable cause.
|
|
640
|
+
Many-owner joins and preliminary streaming outputs use immediate idempotent
|
|
641
|
+
appends. Provider calls and external tool side effects are at least once and
|
|
642
|
+
require their own idempotency. Approved provider-executed calls may satisfy the
|
|
643
|
+
join from the approval response. Provider tools with declared deferred-result
|
|
644
|
+
support wait for their provider result instead of a local executor. An
|
|
645
|
+
authenticated provider callback appends the terminal `ai.tool.result` through
|
|
646
|
+
the trusted server session API; the browser push allowlist rejects it.
|
|
448
647
|
|
|
449
648
|
`compaction` has `shouldCompact(context)` and `compact(context)` callbacks.
|
|
450
649
|
When selected, both the request and the replacement messages enter the log.
|
|
@@ -456,8 +655,20 @@ This entry point is server-only and resolves to a throwing browser stub.
|
|
|
456
655
|
|
|
457
656
|
Returns the built-in A2 handler table without constructing a server. Spread
|
|
458
657
|
it into `createServer({ handlers })` beside application handlers when you need
|
|
459
|
-
a custom assembly.
|
|
460
|
-
|
|
658
|
+
a custom assembly. The table handles input facts, generation requests, model
|
|
659
|
+
step completion, tool calls, approval responses, and terminal tool results.
|
|
660
|
+
Application handlers spread later can deliberately replace a built-in
|
|
661
|
+
handler. Custom assemblies pass `validateAgentPush` as
|
|
662
|
+
`createServer({ validatePush })` to preserve the browser boundary.
|
|
663
|
+
|
|
664
|
+
### `validateAgentPush(context)`
|
|
665
|
+
|
|
666
|
+
`validateAgentPush({ sessionId, events }): void`
|
|
667
|
+
|
|
668
|
+
Accepts the browser interaction allowlist: user messages, approval and input
|
|
669
|
+
responses, interruptions, and explicit retries. It rejects server-authored
|
|
670
|
+
scheduling and lifecycle events, trusted seed messages, and input requests.
|
|
671
|
+
`createAgentServer()` installs it automatically.
|
|
461
672
|
|
|
462
673
|
See [Durable AI agents](/guides/ai-agents) for the protocol and complete
|
|
463
674
|
examples.
|
|
@@ -548,26 +759,25 @@ alert on are all mid-span.
|
|
|
548
759
|
| `a2.append.types` | `a2.append` | start | comma-joined event types |
|
|
549
760
|
| `a2.append.count` | `a2.append` | start | batch size |
|
|
550
761
|
| `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` |
|
|
762
|
+
| `a2.drain.outcome` | `a2.drain` | mid | `settled` \| `busy` \| `stalled` |
|
|
553
763
|
| `a2.drain.processed` | `a2.drain` | mid | events processed this pass |
|
|
554
764
|
| `a2.event.type` | `a2.event` | start | the event's type |
|
|
555
765
|
| `a2.event.index` | `a2.event` | start | log position |
|
|
556
766
|
| `a2.event.id` | `a2.event` | start | event id |
|
|
557
767
|
| `a2.event.attempt` | `a2.event` | start | same durable 1-based ordinal as `ctx.attempt` |
|
|
558
|
-
| `a2.event.
|
|
768
|
+
| `a2.event.lane` | `a2.event` | start | stored lane value; absent for concurrent unlaned work |
|
|
769
|
+
| `a2.event.handled` | `a2.event` | start | normally `true`; `false` when a custom-adapter row has no handler |
|
|
559
770
|
| `a2.event.outcome` | `a2.event` | mid | `processed` \| `failed` \| `dead_lettered` \| `superseded` |
|
|
560
771
|
| `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
772
|
| `a2.state.reducer` | `a2.state` | start | the reducer's name |
|
|
563
773
|
| `a2.state.snapshot` | `a2.state` | mid | `hit` \| `miss` \| `rejected` (schema guard discarded it) |
|
|
564
774
|
| `a2.state.folded` | `a2.state` | mid | events folded past the snapshot |
|
|
565
775
|
| `a2.state.index` | `a2.state` | mid | the frontier the returned state reflects |
|
|
566
776
|
|
|
567
|
-
Drain outcomes: `settled` means nothing actionable is left; `busy` means
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
777
|
+
Drain outcomes: `settled` means nothing actionable is left; `busy` means live
|
|
778
|
+
per-event claims remain; `stalled` means a caught failure needs a later retry.
|
|
779
|
+
A dead-lettered event blocks only later events in its lane. Other lanes remain
|
|
780
|
+
eligible.
|
|
571
781
|
|
|
572
782
|
A failing handler marks its `a2.event` span with the exception and
|
|
573
783
|
error status; `a2.event.outcome = dead_lettered` is the attribute to
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "experimental-a2",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Durable sync and reactions for things with a lifecycle: one event log, derived state, and live client per session.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -49,13 +49,6 @@
|
|
|
49
49
|
},
|
|
50
50
|
"./package.json": "./package.json"
|
|
51
51
|
},
|
|
52
|
-
"scripts": {
|
|
53
|
-
"build": "tsdown",
|
|
54
|
-
"dev": "tsdown --watch",
|
|
55
|
-
"typecheck": "tsc --noEmit",
|
|
56
|
-
"test": "vitest run",
|
|
57
|
-
"test:watch": "vitest"
|
|
58
|
-
},
|
|
59
52
|
"peerDependencies": {
|
|
60
53
|
"@opentelemetry/api": "^1.9.0",
|
|
61
54
|
"@vercel/queue": "*",
|
|
@@ -85,27 +78,34 @@
|
|
|
85
78
|
}
|
|
86
79
|
},
|
|
87
80
|
"devDependencies": {
|
|
88
|
-
"@electric-sql/pglite": "
|
|
89
|
-
"ai": "
|
|
90
|
-
"ioredis": "
|
|
91
|
-
"@opentelemetry/api": "
|
|
92
|
-
"@types/pg": "
|
|
93
|
-
"@vercel/queue": "
|
|
94
|
-
"pg": "
|
|
95
|
-
"@testing-library/react": "
|
|
96
|
-
"@types/react": "
|
|
97
|
-
"esbuild": "
|
|
98
|
-
"@types/react-dom": "
|
|
99
|
-
"jsdom": "
|
|
100
|
-
"react": "
|
|
101
|
-
"react-dom": "
|
|
102
|
-
"@opentelemetry/context-async-hooks": "
|
|
103
|
-
"@opentelemetry/sdk-trace-base": "
|
|
104
|
-
"@types/node": "
|
|
105
|
-
"publint": "
|
|
106
|
-
"tsdown": "
|
|
107
|
-
"typescript": "
|
|
108
|
-
"vitest": "
|
|
109
|
-
"zod": "
|
|
81
|
+
"@electric-sql/pglite": "^0.3.14",
|
|
82
|
+
"ai": "^7.0.58",
|
|
83
|
+
"ioredis": "^5.9.0",
|
|
84
|
+
"@opentelemetry/api": "^1.9.0",
|
|
85
|
+
"@types/pg": "^8.15.0",
|
|
86
|
+
"@vercel/queue": "^0.2.0",
|
|
87
|
+
"pg": "^8.16.0",
|
|
88
|
+
"@testing-library/react": "^16.1.0",
|
|
89
|
+
"@types/react": "^19.0.0",
|
|
90
|
+
"esbuild": "^0.25.0",
|
|
91
|
+
"@types/react-dom": "^19.0.0",
|
|
92
|
+
"jsdom": "^26.0.0",
|
|
93
|
+
"react": "^19.0.0",
|
|
94
|
+
"react-dom": "^19.0.0",
|
|
95
|
+
"@opentelemetry/context-async-hooks": "^2.0.0",
|
|
96
|
+
"@opentelemetry/sdk-trace-base": "^2.0.0",
|
|
97
|
+
"@types/node": "^24.0.0",
|
|
98
|
+
"publint": "^0.3.23",
|
|
99
|
+
"tsdown": "^0.22.14",
|
|
100
|
+
"typescript": "^7.0.2",
|
|
101
|
+
"vitest": "^4.1.10",
|
|
102
|
+
"zod": "^4.4.3"
|
|
103
|
+
},
|
|
104
|
+
"scripts": {
|
|
105
|
+
"build": "tsdown",
|
|
106
|
+
"dev": "tsdown --watch",
|
|
107
|
+
"typecheck": "tsc --noEmit",
|
|
108
|
+
"test": "vitest run",
|
|
109
|
+
"test:watch": "vitest"
|
|
110
110
|
}
|
|
111
|
-
}
|
|
111
|
+
}
|