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
|
@@ -60,9 +60,14 @@ default. `model` also accepts any AI SDK `LanguageModel`, such as a provider
|
|
|
60
60
|
model returned by `openai('gpt-5.6-terra')`, or an async resolver that chooses a
|
|
61
61
|
model for each generation.
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
approval policy.
|
|
63
|
+
Each durable generation runs one AI SDK `streamText()` step. `generation`
|
|
64
|
+
contains per-step settings such as sampling, token limits, provider options,
|
|
65
|
+
and tool approval policy. A2 owns the stop condition, local tool execution,
|
|
66
|
+
and continuation between steps. `maxSteps` limits one complete assistant
|
|
67
|
+
response and defaults to 20. Individual providers decide which model settings
|
|
68
|
+
they support. `generation` therefore excludes `stopWhen`, tool execution
|
|
69
|
+
callbacks, tool callers, tool context, sandbox execution, and the tool approval
|
|
70
|
+
secret. Model-step timeouts are supported; tool-execution timeouts are not.
|
|
66
71
|
|
|
67
72
|
`experimental-a2/ai/server` is server-only. The isomorphic `experimental-a2/ai` entry point contains the
|
|
68
73
|
contract, reducer, schemas, and pure inputs; it never imports a model provider
|
|
@@ -106,9 +111,10 @@ export async function POST(req: Request): Promise<Response> {
|
|
|
106
111
|
}
|
|
107
112
|
```
|
|
108
113
|
|
|
109
|
-
The route never calls the model directly.
|
|
110
|
-
`ai.
|
|
111
|
-
the
|
|
114
|
+
The route never calls the model directly. The browser appends user facts such
|
|
115
|
+
as `ai.message.created`. Built-in server handlers schedule
|
|
116
|
+
`ai.generation.requested`, run the AI SDK, and append progress back to the same
|
|
117
|
+
log.
|
|
112
118
|
|
|
113
119
|
## Bind the reducer to React
|
|
114
120
|
|
|
@@ -182,13 +188,7 @@ export async function openAgent(
|
|
|
182
188
|
message: UIMessage,
|
|
183
189
|
router: { push(href: string): void },
|
|
184
190
|
): Promise<void> {
|
|
185
|
-
await assistantClient.session(sessionId).push(
|
|
186
|
-
{
|
|
187
|
-
type: 'ai.session.created',
|
|
188
|
-
payload: { metadata: {} },
|
|
189
|
-
},
|
|
190
|
-
...inputs.message(message),
|
|
191
|
-
)
|
|
191
|
+
await assistantClient.session(sessionId).push(...inputs.message(message))
|
|
192
192
|
router.push(`/agent/${sessionId}`)
|
|
193
193
|
}
|
|
194
194
|
```
|
|
@@ -216,7 +216,7 @@ export function AgentClient() {
|
|
|
216
216
|
const [draft, setDraft] = useState('')
|
|
217
217
|
const [error, setError] = useState<string | null>(null)
|
|
218
218
|
|
|
219
|
-
const canSend = state.status
|
|
219
|
+
const canSend = state.status !== 'closed'
|
|
220
220
|
const lastMessage = state.messages.at(-1)
|
|
221
221
|
const lastText =
|
|
222
222
|
lastMessage?.parts
|
|
@@ -289,16 +289,21 @@ export function AgentClient() {
|
|
|
289
289
|
}
|
|
290
290
|
```
|
|
291
291
|
|
|
292
|
-
There is no separate message state to reconcile. `push()` folds
|
|
293
|
-
`ai.message.created`
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
292
|
+
There is no separate message state to reconcile. `push()` folds the
|
|
293
|
+
`ai.message.created` fact locally before starting the request, so the user
|
|
294
|
+
message appears immediately. If the request fails, A2 removes that optimistic
|
|
295
|
+
event and the component restores the draft. The server owns the corresponding
|
|
296
|
+
generation request.
|
|
297
297
|
|
|
298
298
|
The form gives Enter its normal submit behavior. The thinking row is also
|
|
299
|
-
derived from `AIState`: it appears with the Agent label
|
|
300
|
-
|
|
301
|
-
|
|
299
|
+
derived from `AIState`: it appears with the Agent label when the server's
|
|
300
|
+
generation request folds, then the first visible assistant content replaces
|
|
301
|
+
it. Empty stream-start messages never create a blank conversation row.
|
|
302
|
+
|
|
303
|
+
Users can send another message while a response is active. The message enters
|
|
304
|
+
the durable log immediately, but its turn waits until the active assistant
|
|
305
|
+
response, including every tool step and approval, reaches a terminal event.
|
|
306
|
+
Queued messages keep log order.
|
|
302
307
|
|
|
303
308
|
## What happens after `push()`
|
|
304
309
|
|
|
@@ -306,26 +311,52 @@ One user interaction becomes a durable sequence:
|
|
|
306
311
|
|
|
307
312
|
```text
|
|
308
313
|
browser ai.message.created optimistic, then durable
|
|
309
|
-
|
|
314
|
+
server ai.generation.requested scheduled from durable facts
|
|
310
315
|
server ai.generation.started
|
|
311
316
|
server ai.generation.progress batched UIMessageChunk[]
|
|
312
317
|
server ai.tool.called when present
|
|
313
|
-
server ai.tool.result when present
|
|
314
318
|
server ai.approval.requested when present
|
|
315
|
-
server ai.
|
|
319
|
+
server ai.tool.result after concurrent execution or denial
|
|
320
|
+
server ai.generation.completed closes this model step
|
|
321
|
+
server ai.generation.requested after all sibling tools are terminal
|
|
322
|
+
... another one-step generation
|
|
316
323
|
server ai.message.completed
|
|
324
|
+
server ai.generation.requested next queued user turn, when present
|
|
317
325
|
```
|
|
318
326
|
|
|
327
|
+
Tool results and generation completion may arrive in either order. A fast
|
|
328
|
+
automatic tool can finish while the model stream is still open. The join waits
|
|
329
|
+
for both step closure and every required terminal result.
|
|
330
|
+
|
|
319
331
|
The SSE connection delivers the server events to the same reducer. Progress
|
|
320
332
|
stores every AI SDK chunk once. The reducer and the exported
|
|
321
333
|
`deriveUIMessages()` apply those chunks synchronously to produce the current
|
|
322
334
|
`UIMessage`; cumulative message snapshots are not duplicated in the log.
|
|
323
335
|
|
|
324
|
-
`AIState` exposes `messages`, `status`, `activeGeneration`, `
|
|
336
|
+
`AIState` exposes `messages`, `status`, `activeGeneration`, `activeRequestId`,
|
|
337
|
+
`activeResponseMessageId`, `activeProjection`, `responseGenerationIds`,
|
|
325
338
|
`pendingApprovals`, `pendingInputs`, `tools`, `compaction`, per-generation
|
|
326
|
-
`usage`, and the last generation `error`.
|
|
327
|
-
indexed chunk/tool frontier used for exact
|
|
328
|
-
a terminal event.
|
|
339
|
+
`usage`, and the last generation `error`.
|
|
340
|
+
`activeProjection` is the temporary indexed chunk/tool frontier used for exact
|
|
341
|
+
interruption and becomes `null` at a terminal event.
|
|
342
|
+
|
|
343
|
+
`activeRequestId` identifies the server-authorized generation request. It
|
|
344
|
+
prevents a delayed request or recovered attempt from taking ownership from the
|
|
345
|
+
current response before its generation starts.
|
|
346
|
+
|
|
347
|
+
`activeResponseMessageId` identifies that request's response before
|
|
348
|
+
`activeGeneration` exists. It is `null` after the generation starts.
|
|
349
|
+
|
|
350
|
+
`responseGenerationIds` keeps the latest generation owner for each response
|
|
351
|
+
message. Late lifecycle events from a superseded generation stay in raw
|
|
352
|
+
history but cannot alter the projected message, even after its replacement
|
|
353
|
+
finishes.
|
|
354
|
+
|
|
355
|
+
Generation requests share one durable lane, so model calls do not overlap.
|
|
356
|
+
The queued-turn policy is the stronger ordering rule: a later user message is
|
|
357
|
+
not scheduled until the current response's complete tool loop finishes. Lane
|
|
358
|
+
FIFO alone would not prevent a newly appended user turn from overtaking a tool
|
|
359
|
+
continuation that has not been appended yet.
|
|
329
360
|
|
|
330
361
|
## The built-in inputs
|
|
331
362
|
|
|
@@ -333,27 +364,32 @@ a terminal event.
|
|
|
333
364
|
|
|
334
365
|
| Input | Events |
|
|
335
366
|
| --- | --- |
|
|
336
|
-
| `inputs.message(message)` | records a message
|
|
337
|
-
| `inputs.seed(message)` | records a message without
|
|
338
|
-
| `inputs.approval(response)` | records an approval decision
|
|
339
|
-
| `inputs.input(response)` | records application input
|
|
340
|
-
| `inputs.requestInput(request)` | records
|
|
341
|
-
| `inputs.retry(options)` |
|
|
367
|
+
| `inputs.message(message)` | records a message fact; the server schedules user turns |
|
|
368
|
+
| `inputs.seed(message)` | records a trusted server message without scheduling a turn |
|
|
369
|
+
| `inputs.approval(response)` | records an approval decision fact |
|
|
370
|
+
| `inputs.input(response)` | records an application input response fact |
|
|
371
|
+
| `inputs.requestInput(request)` | records a trusted server request for application input |
|
|
372
|
+
| `inputs.retry(options)` | records `ai.retry.requested` for a failed response |
|
|
342
373
|
| `inputs.interrupt(options)` | interrupts an active response |
|
|
343
374
|
|
|
344
375
|
The builders hide stable event ids, so the same interaction is safe to resend.
|
|
345
|
-
|
|
346
|
-
`
|
|
376
|
+
Browser `push()` accepts user messages, approval and input responses,
|
|
377
|
+
interruptions, and explicit retries. `inputs.seed()` and
|
|
378
|
+
`inputs.requestInput()` are for trusted server appends. Browser inputs never
|
|
379
|
+
append server scheduling, seeded non-user messages, or lifecycle events.
|
|
380
|
+
Approval and input request/response payloads carry the active `generationId`;
|
|
381
|
+
clients copy it from the pending request so a stale interaction cannot satisfy
|
|
382
|
+
a later model step.
|
|
347
383
|
|
|
348
384
|
`inputs` deliberately has no session lifecycle methods. The A2 log begins with
|
|
349
385
|
the first append. Apps that use explicit `ai.session.created` or
|
|
350
|
-
`ai.session.closed` lifecycle events
|
|
386
|
+
`ai.session.closed` lifecycle events append those trusted server facts
|
|
351
387
|
directly.
|
|
352
388
|
|
|
353
389
|
## Add tools and approval
|
|
354
390
|
|
|
355
391
|
Pass ordinary AI SDK tools to the server. Approval policy belongs in
|
|
356
|
-
`generation`, beside the other
|
|
392
|
+
`generation`, beside the other per-step settings:
|
|
357
393
|
|
|
358
394
|
```ts server/with-tools.ts
|
|
359
395
|
import { tool } from 'ai'
|
|
@@ -405,6 +441,7 @@ export function ApprovalControls() {
|
|
|
405
441
|
void push(
|
|
406
442
|
...inputs.approval({
|
|
407
443
|
messageId: approval.messageId,
|
|
444
|
+
generationId: approval.generationId,
|
|
408
445
|
approvalId: approval.approvalId,
|
|
409
446
|
approved: true,
|
|
410
447
|
}),
|
|
@@ -418,6 +455,7 @@ export function ApprovalControls() {
|
|
|
418
455
|
void push(
|
|
419
456
|
...inputs.approval({
|
|
420
457
|
messageId: approval.messageId,
|
|
458
|
+
generationId: approval.generationId,
|
|
421
459
|
approvalId: approval.approvalId,
|
|
422
460
|
approved: false,
|
|
423
461
|
reason: 'Denied by the user',
|
|
@@ -432,9 +470,10 @@ export function ApprovalControls() {
|
|
|
432
470
|
}
|
|
433
471
|
```
|
|
434
472
|
|
|
435
|
-
`inputs.approval()`
|
|
436
|
-
|
|
437
|
-
|
|
473
|
+
`inputs.approval()` records only the decision and updates the approval part in
|
|
474
|
+
the projected `UIMessage`. A built-in handler executes the approved tool or
|
|
475
|
+
records a denied result. It never bypasses the same sibling join used by
|
|
476
|
+
automatic tools.
|
|
438
477
|
|
|
439
478
|
Tool activity follows the full AI SDK chunk lifecycle. Input validation errors,
|
|
440
479
|
execution errors, denials, and preliminary and final outputs become durable
|
|
@@ -443,11 +482,36 @@ output completes it. Each result has its own event id, so a preliminary result
|
|
|
443
482
|
cannot deduplicate the final one. Dynamic-tool, provider-executed, provider
|
|
444
483
|
metadata, and tool metadata fields are preserved in the projection.
|
|
445
484
|
|
|
485
|
+
Independent tool handlers run concurrently as soon as their durable inputs and
|
|
486
|
+
authorization are ready. There is no fixed tool concurrency limit. A private
|
|
487
|
+
coordinator reducer tracks generation closure, cancellation, calls, approvals,
|
|
488
|
+
and terminal results for the active response. Its retained state is bounded;
|
|
489
|
+
completed responses do not accumulate in the coordinator. Each join reads a
|
|
490
|
+
durable reducer snapshot plus the log tail through `ctx.session.state()`. The snapshot
|
|
491
|
+
is only a cache. Recovery can rebuild the same coordinator state from the event
|
|
492
|
+
log after process death.
|
|
493
|
+
|
|
494
|
+
A continuation needs the generation to close and every sibling tool to have a
|
|
495
|
+
terminal success, error, or denied result. Racing join handlers call
|
|
496
|
+
`ctx.session.append()` with the same deterministic continuation ID, so append
|
|
497
|
+
idempotency resolves the race. A handler's single-owner terminal result is
|
|
498
|
+
returned and committed atomically with that handler's completion. Preliminary
|
|
499
|
+
streaming tool outputs use immediate appends. An approved provider-executed
|
|
500
|
+
call can satisfy the join through its response. A provider tool that declares
|
|
501
|
+
deferred-result support can wait for its provider result instead of a local
|
|
502
|
+
executor. The authenticated provider callback appends that `ai.tool.result`
|
|
503
|
+
through the trusted server session API. Browser `push()` rejects lifecycle
|
|
504
|
+
events, including provider results.
|
|
505
|
+
|
|
446
506
|
`inputs.input()` provides the same durable request/response shape for
|
|
447
507
|
application-defined input. Its value stays in raw history and
|
|
448
508
|
`state.pendingInputs`; resolve instructions dynamically or replace `generate`
|
|
449
509
|
when the value should become model context.
|
|
450
510
|
|
|
511
|
+
Approval and application-input request/response facts carry the active
|
|
512
|
+
`generationId`. Echo that value from the pending request so a delayed UI action
|
|
513
|
+
cannot authorize or resume a later model step.
|
|
514
|
+
|
|
451
515
|
## Interrupt and retry
|
|
452
516
|
|
|
453
517
|
Interrupt the active response from the client:
|
|
@@ -489,10 +553,18 @@ keeps the partial response the user actually saw, and turns incomplete visible
|
|
|
489
553
|
tools into `output-error`. Progress, completion, or failure from that generation
|
|
490
554
|
cannot reactivate it after the interruption.
|
|
491
555
|
|
|
556
|
+
`generationId` is optional only between `ai.generation.requested` and
|
|
557
|
+
`ai.generation.started`, when no generation id exists yet. In that phase, use
|
|
558
|
+
`state.activeResponseMessageId` as `messageId` and omit `generationId`. Once
|
|
559
|
+
`activeGeneration` exists, copy both ids from it so a delayed interruption
|
|
560
|
+
cannot stop a later generation.
|
|
561
|
+
|
|
492
562
|
After a failed model call, `inputs.retry({ messageId, responseMessageId,
|
|
493
|
-
retryId })`
|
|
494
|
-
resending one retry is safe while a
|
|
495
|
-
partial progress remains in raw history
|
|
563
|
+
retryId })` records a retry request. The server schedules its fresh attempt.
|
|
564
|
+
`retryId` identifies the user's action, so resending one retry is safe while a
|
|
565
|
+
later retry remains distinct. Failed partial progress remains in raw history
|
|
566
|
+
but is excluded from the new prompt. Later user messages remain queued while
|
|
567
|
+
the failed response awaits an explicit retry or interruption.
|
|
496
568
|
|
|
497
569
|
## Append from the server
|
|
498
570
|
|
|
@@ -567,7 +639,10 @@ different server assembly:
|
|
|
567
639
|
```ts server/custom.ts
|
|
568
640
|
import { z } from 'zod'
|
|
569
641
|
import { agent } from 'experimental-a2/ai'
|
|
570
|
-
import {
|
|
642
|
+
import {
|
|
643
|
+
createHandlers,
|
|
644
|
+
validateAgentPush,
|
|
645
|
+
} from 'experimental-a2/ai/server'
|
|
571
646
|
import { createServer } from 'experimental-a2/server'
|
|
572
647
|
|
|
573
648
|
const supportAgent = agent({
|
|
@@ -584,6 +659,7 @@ const aiHandlers = createHandlers({
|
|
|
584
659
|
|
|
585
660
|
export const customAssistantServer = createServer({
|
|
586
661
|
contract: supportAgent.contract,
|
|
662
|
+
validatePush: validateAgentPush,
|
|
587
663
|
handlers: {
|
|
588
664
|
...aiHandlers,
|
|
589
665
|
'ticket.linked': async ({ event }) => {
|
|
@@ -596,16 +672,18 @@ export const customAssistantServer = createServer({
|
|
|
596
672
|
|
|
597
673
|
Application events stay fully typed. The standard AI reducer ignores unknown
|
|
598
674
|
events, so another reducer can project application state without forking the AI
|
|
599
|
-
protocol.
|
|
675
|
+
protocol. `validateAgentPush` preserves the same browser boundary as
|
|
676
|
+
`createAgentServer()`: user facts may enter through a parsed push, while model
|
|
677
|
+
scheduling, tool lifecycle, and trusted seed messages stay server-authored.
|
|
600
678
|
|
|
601
679
|
### Replace generation, not durability
|
|
602
680
|
|
|
603
|
-
The default generator uses
|
|
604
|
-
model-facing step for fixtures, provider routing, or a different AI
|
|
605
|
-
assembly:
|
|
681
|
+
The default generator uses AI SDK `streamText()` for one model step. Replace
|
|
682
|
+
only that model-facing step for fixtures, provider routing, or a different AI
|
|
683
|
+
SDK assembly:
|
|
606
684
|
|
|
607
685
|
```ts server/custom-generation.ts
|
|
608
|
-
import {
|
|
686
|
+
import { convertToModelMessages, streamText } from 'ai'
|
|
609
687
|
import { createAgentServer } from 'experimental-a2/ai/server'
|
|
610
688
|
import { assistant } from '../assistant'
|
|
611
689
|
|
|
@@ -615,26 +693,20 @@ export const customGenerationServer = createAgentServer({
|
|
|
615
693
|
generate: async ({
|
|
616
694
|
messages,
|
|
617
695
|
model,
|
|
618
|
-
tools,
|
|
619
696
|
instructions,
|
|
620
|
-
generation,
|
|
621
|
-
generationId,
|
|
622
697
|
responseMessageId,
|
|
623
698
|
signal,
|
|
624
699
|
}) => {
|
|
625
|
-
const
|
|
626
|
-
...generation,
|
|
627
|
-
id: generationId,
|
|
700
|
+
const result = streamText({
|
|
628
701
|
model,
|
|
629
|
-
|
|
702
|
+
messages: await convertToModelMessages(messages),
|
|
703
|
+
abortSignal: signal,
|
|
630
704
|
...(instructions === undefined ? {} : { instructions }),
|
|
631
705
|
})
|
|
632
706
|
|
|
633
|
-
return
|
|
634
|
-
|
|
635
|
-
uiMessages: messages,
|
|
707
|
+
return result.toUIMessageStream({
|
|
708
|
+
originalMessages: messages,
|
|
636
709
|
generateMessageId: () => responseMessageId,
|
|
637
|
-
abortSignal: signal,
|
|
638
710
|
})
|
|
639
711
|
},
|
|
640
712
|
})
|
|
@@ -642,17 +714,26 @@ export const customGenerationServer = createAgentServer({
|
|
|
642
714
|
|
|
643
715
|
`generate` receives compacted messages, resolved model and instructions, tools,
|
|
644
716
|
generation settings, durable request identity, current state and history, and
|
|
645
|
-
the abort signal. It returns one
|
|
646
|
-
|
|
647
|
-
|
|
717
|
+
the abort signal. It returns exactly one model step as a
|
|
718
|
+
`ReadableStream<UIMessageChunk>`. A tool-aware replacement sends definitions to
|
|
719
|
+
the model without running local `execute` functions. A2 still owns durable
|
|
720
|
+
progress, tool execution, approval, continuation, interruption, and failure.
|
|
648
721
|
|
|
649
722
|
## Delivery semantics
|
|
650
723
|
|
|
651
|
-
Model
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
724
|
+
Model steps and tools run inside at-least-once A2 handlers. The returned-event
|
|
725
|
+
transaction makes each successful handler completion and its terminal events
|
|
726
|
+
one atomic durable operation. Deterministic ids make scheduling and lifecycle
|
|
727
|
+
appends effectively once, and A2 marks an incomplete model attempt
|
|
728
|
+
`superseded` before starting its replacement.
|
|
729
|
+
|
|
730
|
+
Provider calls and external tool side effects remain outside that transaction.
|
|
731
|
+
A process can die after an external effect succeeds and before its handler
|
|
732
|
+
completion commits, so recovery may run the call again. Give side-effecting
|
|
733
|
+
tools their own idempotency strategy, normally keyed by the AI SDK tool call
|
|
734
|
+
id. This is the same at-least-once boundary as every other A2 handler. A fast
|
|
735
|
+
tool may also start before its model step later fails. The failure prevents a
|
|
736
|
+
continuation, but it cannot roll back that external effect.
|
|
656
737
|
|
|
657
738
|
Configure [production recovery](/guides/production) exactly as for any other A2
|
|
658
739
|
server. Recovery wakes an interrupted generation after the original serverless
|
|
@@ -44,7 +44,7 @@ returning a `Response` supports a redirect or authentication challenge.
|
|
|
44
44
|
## Causal forest
|
|
45
45
|
|
|
46
46
|
Every stored `cause` connects a child to the event index and handler attempt
|
|
47
|
-
whose `ctx.append` first persisted it. The dashboard builds that forest
|
|
47
|
+
whose `ctx.session.append` first persisted it. The dashboard builds that forest
|
|
48
48
|
directly from the session log. It does not need a trace table or another write
|
|
49
49
|
on the append path.
|
|
50
50
|
|
|
@@ -75,10 +75,13 @@ dashboard shows the first dispatch time as unknown instead of moving it to
|
|
|
75
75
|
`createdAt` or `lastClaimedAt`.
|
|
76
76
|
|
|
77
77
|
The bar is event lifetime, not handler execution time. A2 stores lifecycle
|
|
78
|
-
summaries, not one span row for every attempt. Time spent waiting
|
|
79
|
-
handler,
|
|
78
|
+
summaries, not one span row for every attempt. Time spent waiting for a lane,
|
|
79
|
+
running a handler, holding a claim, or waiting for recovery remains one honest
|
|
80
80
|
interval.
|
|
81
81
|
|
|
82
|
+
An event type without a handler is complete at append. Its bar has no waiting
|
|
83
|
+
time or dispatch marker, and its attempt count stays at zero.
|
|
84
|
+
|
|
82
85
|
Snapshots appear as their reducer name, event frontier, and last update time.
|
|
83
86
|
The dashboard does not send cached snapshot state to the browser.
|
|
84
87
|
|
|
@@ -34,9 +34,9 @@ orders / order-43
|
|
|
34
34
|
agents / run-abc
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
The session is the boundary A2
|
|
38
|
-
in the same session when they
|
|
39
|
-
|
|
37
|
+
The session is the boundary A2 logs, recovers, folds, and streams. Put events
|
|
38
|
+
in the same session when they form one history or view. Handler execution is
|
|
39
|
+
concurrent unless events share a lane.
|
|
40
40
|
|
|
41
41
|
A contract is not a table. A session is not a table either. One contract
|
|
42
42
|
usually serves many sessions, and one session may describe changes that touch
|
|
@@ -77,8 +77,7 @@ a cost: boards and search span many sessions and need another query model. If
|
|
|
77
77
|
those collection views are the center of the product, let the database lead.
|
|
78
78
|
|
|
79
79
|
Do not put an entire workspace in one session just to make cross-issue queries
|
|
80
|
-
possible. Every event
|
|
81
|
-
that session follows the same growing log.
|
|
80
|
+
possible. Every event joins the same growing log and every client follows it.
|
|
82
81
|
|
|
83
82
|
## Database writes from handlers
|
|
84
83
|
|
|
@@ -99,7 +98,7 @@ integration:
|
|
|
99
98
|
|
|
100
99
|
- it is eventually consistent with the session logs;
|
|
101
100
|
- writes must be idempotent;
|
|
102
|
-
- A2
|
|
101
|
+
- A2 assigns log order within one session, not across sessions;
|
|
103
102
|
- A2 does not expose one API that scans and replays every session to rebuild
|
|
104
103
|
the index.
|
|
105
104
|
|
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>
|