experimental-a2 0.2.0 → 0.4.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 +169 -0
- package/dist/ai-B4YhEnfw.d.ts +333 -0
- package/dist/ai-server.d.ts +48 -9
- package/dist/ai-server.js +121 -49
- package/dist/ai.d.ts +2 -303
- package/dist/ai.js +231 -86
- package/dist/cli-B3VuxoDe.js +597 -0
- package/dist/cli-bin.d.ts +1 -0
- package/dist/cli-bin.js +5 -0
- package/dist/cli.d.ts +19 -0
- package/dist/cli.js +2 -0
- package/dist/client-BrfDXQ8A.d.ts +155 -0
- package/dist/client-Bt4tAKi9.js +798 -0
- package/dist/client.d.ts +2 -90
- package/dist/client.js +1 -409
- package/dist/{contract-CG_adnu_.js → contract-48bUMgcL.js} +10 -2
- package/dist/{contract-C_3dIIEU.d.ts → contract-jIfaR085.d.ts} +62 -8
- package/dist/devtools-J_jZ2vQf.d.ts +151 -0
- package/dist/devtools-kJJaORn-.js +338 -0
- package/dist/devtools-server.browser.js +1 -1
- package/dist/devtools-server.d.ts +2 -2
- package/dist/devtools-server.js +224 -43
- package/dist/devtools.d.ts +2 -0
- package/dist/devtools.js +2 -0
- package/dist/{errors-BJRMd-h6.js → errors-BQuJpe82.js} +4 -4
- package/dist/{errors-xL_JTXsY.d.ts → errors-W6nwJ-fm.d.ts} +1 -1
- package/dist/http.d.ts +71 -13
- package/dist/http.js +302 -41
- package/dist/{idempotent-replay-BMyHrP0L.js → idempotent-replay-DuqEkYA7.js} +2 -2
- package/dist/index.d.ts +5 -5
- package/dist/index.js +2 -2
- package/dist/{inspection-E7qbD0Xj.js → inspection-DaxB5jM2.js} +2 -1
- package/dist/internal-aEotMzu_.js +209 -0
- package/dist/otel.d.ts +1 -1
- package/dist/platform-B4TnJtWu.js +32 -0
- package/dist/react.d.ts +37 -14
- package/dist/react.js +26 -15
- package/dist/scheduler-qstash.d.ts +78 -0
- package/dist/scheduler-qstash.js +499 -0
- package/dist/scheduler-task-BpzhPnRS.js +54 -0
- package/dist/{recovery-vercel.d.ts → scheduler-vercel.d.ts} +17 -24
- package/dist/scheduler-vercel.js +226 -0
- package/dist/server-CcNnFnoW.js +1405 -0
- package/dist/server-YtPq7hjw.d.ts +260 -0
- package/dist/server.d.ts +4 -155
- package/dist/server.js +2 -2
- package/dist/{log-ldf5g8Cx.d.ts → store-C3sNAaBT.d.ts} +111 -35
- package/dist/{log-yJbXUf72.js → store-N8PXxDAS.js} +1 -1
- package/dist/store-codec-DTG0Ftek.js +8 -0
- package/dist/store-memory.d.ts +11 -0
- package/dist/{log-memory.js → store-memory.js} +127 -24
- package/dist/{log-polling-6COoN60V.js → store-polling-DgrrAE3d.js} +7 -6
- package/dist/{log-postgres.d.ts → store-postgres.d.ts} +6 -6
- package/dist/{log-postgres.js → store-postgres.js} +158 -24
- package/dist/{log-redis.js → store-redis-core-DWqx3F47.js} +294 -156
- package/dist/store-redis-http.d.ts +21 -0
- package/dist/store-redis-http.js +70 -0
- package/dist/store-redis.d.ts +37 -0
- package/dist/store-redis.js +298 -0
- package/dist/{log-sqlite.d.ts → store-sqlite.d.ts} +6 -6
- package/dist/{log-sqlite.js → store-sqlite.js} +116 -22
- package/dist/{telemetry-Cso0qyHQ.d.ts → telemetry-BjYHTfh2.d.ts} +1 -1
- package/dist/testing.browser.d.ts +1 -0
- package/dist/testing.browser.js +4 -0
- package/dist/testing.d.ts +31 -0
- package/dist/testing.js +101 -0
- package/dist/wire-DCUZBUlT.js +222 -0
- package/docs/01-quickstart.mdx +4 -5
- package/docs/concepts/01-contracts.mdx +21 -17
- package/docs/concepts/02-handlers.mdx +7 -7
- package/docs/concepts/03-durability.mdx +26 -29
- package/docs/concepts/04-state.mdx +18 -21
- package/docs/guides/01-timers.mdx +154 -54
- package/docs/guides/02-cancellation.mdx +30 -4
- package/docs/guides/03-react.mdx +20 -21
- package/docs/guides/04-local-first.mdx +1 -1
- package/docs/guides/05-production.mdx +321 -60
- package/docs/guides/06-ai-agents.mdx +249 -49
- package/docs/guides/07-devtools.mdx +137 -12
- package/docs/guides/08-application-data.mdx +1 -1
- package/docs/guides/09-presence.mdx +284 -0
- package/docs/guides/10-transports.mdx +131 -0
- package/docs/index.mdx +22 -46
- package/docs/reference/01-api.mdx +751 -102
- package/docs/reference/02-errors.mdx +10 -5
- package/package.json +23 -6
- package/dist/internal-D6wNxTck.js +0 -36
- package/dist/log-memory.d.ts +0 -11
- package/dist/log-redis.d.ts +0 -31
- package/dist/recovery-vercel.js +0 -119
- package/dist/server-DJgD2YWP.js +0 -877
- package/dist/wire-BVsgR8o9.js +0 -62
|
@@ -37,8 +37,8 @@ in the server module.
|
|
|
37
37
|
|
|
38
38
|
## Connect the model
|
|
39
39
|
|
|
40
|
-
`createAgentServer()` adds the AI SDK runner and A2's ordinary
|
|
41
|
-
|
|
40
|
+
`createAgentServer()` adds the AI SDK runner and A2's ordinary store,
|
|
41
|
+
scheduler, telemetry, and handler machinery:
|
|
42
42
|
|
|
43
43
|
```ts server/assistant.ts
|
|
44
44
|
import { createAgentServer } from 'experimental-a2/ai/server'
|
|
@@ -63,15 +63,65 @@ model for each generation.
|
|
|
63
63
|
Each durable generation runs one AI SDK `streamText()` step. `generation`
|
|
64
64
|
contains per-step settings such as sampling, token limits, provider options,
|
|
65
65
|
and tool approval policy. A2 owns the stop condition, local tool execution,
|
|
66
|
-
and continuation between steps.
|
|
67
|
-
|
|
68
|
-
they support. `generation`
|
|
69
|
-
|
|
70
|
-
|
|
66
|
+
and continuation between steps. Responses have no step limit by default. Set
|
|
67
|
+
`maxSteps` to a positive integer to limit one complete assistant response.
|
|
68
|
+
Individual providers decide which model settings they support. `generation`
|
|
69
|
+
therefore excludes `stopWhen`, tool execution callbacks, tool callers, sandbox
|
|
70
|
+
execution, and the tool approval secret. Everything else, including
|
|
71
|
+
`toolsContext`, passes through to the AI SDK untouched. A local tool reads its
|
|
72
|
+
durable execution context later, from the handler that runs it. Model-step
|
|
73
|
+
timeouts are supported; tool-execution timeouts are not.
|
|
74
|
+
|
|
75
|
+
### Store assistant metadata
|
|
76
|
+
|
|
77
|
+
Extend the agent's `UIMessage` type to keep generation status and usage on the
|
|
78
|
+
assistant message:
|
|
79
|
+
|
|
80
|
+
```ts metadata-assistant.ts
|
|
81
|
+
import type { LanguageModelUsage, UIMessage } from 'ai'
|
|
82
|
+
import { agent } from 'experimental-a2/ai'
|
|
83
|
+
|
|
84
|
+
export type MetadataAssistantMessage = UIMessage<{
|
|
85
|
+
status: 'writing' | 'complete'
|
|
86
|
+
usage?: LanguageModelUsage
|
|
87
|
+
}>
|
|
88
|
+
|
|
89
|
+
export const metadataAssistant = agent<MetadataAssistantMessage>({
|
|
90
|
+
name: 'metadata-assistant',
|
|
91
|
+
})
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Map the AI SDK's standard stream parts to that metadata type:
|
|
95
|
+
|
|
96
|
+
```ts server/metadata-assistant.ts
|
|
97
|
+
import { createAgentServer } from 'experimental-a2/ai/server'
|
|
98
|
+
import { metadataAssistant } from '../metadata-assistant'
|
|
99
|
+
|
|
100
|
+
export const metadataAssistantServer = createAgentServer({
|
|
101
|
+
agent: metadataAssistant,
|
|
102
|
+
model: 'openai/gpt-5.6-terra',
|
|
103
|
+
messageMetadata: ({ part }) => {
|
|
104
|
+
if (part.type === 'start') return { status: 'writing' as const }
|
|
105
|
+
if (part.type === 'finish') {
|
|
106
|
+
return { status: 'complete' as const, usage: part.totalUsage }
|
|
107
|
+
}
|
|
108
|
+
return undefined
|
|
109
|
+
},
|
|
110
|
+
})
|
|
111
|
+
```
|
|
71
112
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
113
|
+
The default generator passes `messageMetadata` to the AI SDK's
|
|
114
|
+
`toUIMessageStream()`. The callback receives the generation context and its
|
|
115
|
+
current `TextStreamPart`. Its return type follows the metadata type in the
|
|
116
|
+
agent's `UIMessage`. Metadata on standard `start` and `finish` chunks enters
|
|
117
|
+
the event log with the rest of the stream. A2 shallow-merges those values onto
|
|
118
|
+
the projected assistant message, so the example exposes `writing` immediately
|
|
119
|
+
and keeps final token usage after completion. The callback is synchronous.
|
|
120
|
+
|
|
121
|
+
`experimental-a2/ai/server` is server-only. The isomorphic
|
|
122
|
+
`experimental-a2/ai` entry point contains the contract, reducer, schemas,
|
|
123
|
+
`handlerContext()`, and pure inputs; it never imports a model provider or
|
|
124
|
+
backend.
|
|
75
125
|
|
|
76
126
|
## Expose the event stream
|
|
77
127
|
|
|
@@ -86,7 +136,7 @@ import { assistantServer } from '@/server/assistant'
|
|
|
86
136
|
export async function GET(req: Request): Promise<Response> {
|
|
87
137
|
const { searchParams } = new URL(req.url)
|
|
88
138
|
const sessionId = searchParams.get('sessionId')
|
|
89
|
-
const
|
|
139
|
+
const startAfter = Number(searchParams.get('index')) || 0
|
|
90
140
|
|
|
91
141
|
if (!sessionId) {
|
|
92
142
|
return errorResponse(new A2Error('INVALID_PAYLOAD', 'missing sessionId'))
|
|
@@ -94,7 +144,7 @@ export async function GET(req: Request): Promise<Response> {
|
|
|
94
144
|
|
|
95
145
|
// here's where you'd do auth, or any other checks
|
|
96
146
|
|
|
97
|
-
return sseResponse(assistantServer.session(sessionId).stream({
|
|
147
|
+
return sseResponse(assistantServer.session(sessionId).stream({ startAfter }))
|
|
98
148
|
}
|
|
99
149
|
|
|
100
150
|
export async function POST(req: Request): Promise<Response> {
|
|
@@ -153,16 +203,12 @@ export default async function AgentPage({
|
|
|
153
203
|
const { sessionId } = await params
|
|
154
204
|
const session = assistantServer.session(sessionId)
|
|
155
205
|
const { state, index } = await session.state(assistant.reducer)
|
|
156
|
-
const initialEvents = (await session.history()).filter(
|
|
157
|
-
(event) => event.index <= index,
|
|
158
|
-
)
|
|
159
206
|
|
|
160
207
|
return (
|
|
161
208
|
<SessionProvider
|
|
162
209
|
sessionId={sessionId}
|
|
163
210
|
initialState={state}
|
|
164
211
|
initialIndex={index}
|
|
165
|
-
initialEvents={initialEvents}
|
|
166
212
|
>
|
|
167
213
|
<AgentClient />
|
|
168
214
|
</SessionProvider>
|
|
@@ -174,8 +220,8 @@ An empty session folds to the initial `AIState`. Its log is created lazily when
|
|
|
174
220
|
the first message is pushed.
|
|
175
221
|
|
|
176
222
|
Start the session from the previous route, but wait for its durable append
|
|
177
|
-
before navigating. The destination server render then sees the message
|
|
178
|
-
|
|
223
|
+
before navigating. The destination server render then sees the message in its
|
|
224
|
+
folded state instead of racing the `POST`:
|
|
179
225
|
|
|
180
226
|
```tsx app/agent/new-agent-session.tsx
|
|
181
227
|
'use client'
|
|
@@ -194,8 +240,8 @@ export async function openAgent(
|
|
|
194
240
|
```
|
|
195
241
|
|
|
196
242
|
The draft clears immediately while the append is in flight and can be restored
|
|
197
|
-
if it fails. Once navigation begins, SSR already has a durable
|
|
198
|
-
|
|
243
|
+
if it fails. Once navigation begins, SSR already has folded state at a durable
|
|
244
|
+
frontier. When the destination provider mounts, it resolves the same live
|
|
199
245
|
session and reconciles that server render without replacing the object. Idle
|
|
200
246
|
session objects expire after five minutes by default; configure `gcTime` on
|
|
201
247
|
`createClient` when a different lifetime fits the app.
|
|
@@ -305,9 +351,34 @@ the durable log immediately, but its turn waits until the active assistant
|
|
|
305
351
|
response, including every tool step and approval, reaches a terminal event.
|
|
306
352
|
Queued messages keep log order.
|
|
307
353
|
|
|
354
|
+
Pass `{ generate: false }` when a user message should update the conversation
|
|
355
|
+
without starting a model turn:
|
|
356
|
+
|
|
357
|
+
```ts app/agent/passive-message.ts
|
|
358
|
+
import type { UIMessage } from 'ai'
|
|
359
|
+
import { inputs } from 'experimental-a2/ai'
|
|
360
|
+
import { assistantClient } from './session'
|
|
361
|
+
|
|
362
|
+
export async function recordPassiveMessage(
|
|
363
|
+
sessionId: string,
|
|
364
|
+
message: UIMessage,
|
|
365
|
+
): Promise<void> {
|
|
366
|
+
await assistantClient
|
|
367
|
+
.session(sessionId)
|
|
368
|
+
.push(...inputs.message(message, { generate: false }))
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
The passive user message still appears in `AIState.messages`. A later user
|
|
373
|
+
message with the default generation behavior includes passive user messages
|
|
374
|
+
before it in model context. Passive user messages after that trigger remain
|
|
375
|
+
outside its prompt and wait for the next generating message. This ordering
|
|
376
|
+
supports ambient agents without letting new background context leak into an
|
|
377
|
+
already queued turn.
|
|
378
|
+
|
|
308
379
|
## What happens after `push()`
|
|
309
380
|
|
|
310
|
-
One user interaction becomes a durable sequence:
|
|
381
|
+
One generating user interaction becomes a durable sequence:
|
|
311
382
|
|
|
312
383
|
```text
|
|
313
384
|
browser ai.message.created optimistic, then durable
|
|
@@ -335,10 +406,15 @@ stores every AI SDK chunk once. The reducer and the exported
|
|
|
335
406
|
|
|
336
407
|
`AIState` exposes `messages`, `status`, `activeGeneration`, `activeRequestId`,
|
|
337
408
|
`activeResponseMessageId`, `activeProjection`, `responseGenerationIds`,
|
|
338
|
-
`
|
|
339
|
-
`usage`, and the last
|
|
340
|
-
`
|
|
341
|
-
|
|
409
|
+
`terminalRequestIds`, `terminalGenerations`, `pendingApprovals`,
|
|
410
|
+
`pendingInputs`, `tools`, `compaction`, per-generation `usage`, and the last
|
|
411
|
+
generation `error`.
|
|
412
|
+
`activeProjection` is the temporary indexed generation frontier used for exact
|
|
413
|
+
interruption. It remains available after a generation step completes while the
|
|
414
|
+
response waits on tool, approval, or input barriers, and after a generation
|
|
415
|
+
fails while it awaits retry or interruption. It clears when the response
|
|
416
|
+
completes, is interrupted or superseded, a retry begins, or a later generation
|
|
417
|
+
replaces it.
|
|
342
418
|
|
|
343
419
|
`activeRequestId` identifies the server-authorized generation request. It
|
|
344
420
|
prevents a delayed request or recovered attempt from taking ownership from the
|
|
@@ -364,8 +440,8 @@ continuation that has not been appended yet.
|
|
|
364
440
|
|
|
365
441
|
| Input | Events |
|
|
366
442
|
| --- | --- |
|
|
367
|
-
| `inputs.message(message)` | records a message fact;
|
|
368
|
-
| `inputs.seed(message)` | records
|
|
443
|
+
| `inputs.message(message, { generate? })` | records a message fact; `false` keeps context without scheduling |
|
|
444
|
+
| `inputs.seed(message)` | records `generate: false`; non-user roles require a trusted append |
|
|
369
445
|
| `inputs.approval(response)` | records an approval decision fact |
|
|
370
446
|
| `inputs.input(response)` | records an application input response fact |
|
|
371
447
|
| `inputs.requestInput(request)` | records a trusted server request for application input |
|
|
@@ -373,10 +449,15 @@ continuation that has not been appended yet.
|
|
|
373
449
|
| `inputs.interrupt(options)` | interrupts an active response |
|
|
374
450
|
|
|
375
451
|
The builders hide stable event ids, so the same interaction is safe to resend.
|
|
452
|
+
Generation scheduling considers only user-role `ai.message.created` facts.
|
|
453
|
+
Assistant and system message facts remain context regardless of the `generate`
|
|
454
|
+
field, and model output is recorded as generation progress rather than a new
|
|
455
|
+
message fact, so an assistant reply cannot recursively schedule another turn.
|
|
376
456
|
Browser `push()` accepts user messages, approval and input responses,
|
|
377
|
-
interruptions, and explicit retries. `inputs.
|
|
378
|
-
`inputs.
|
|
379
|
-
append server scheduling, seeded non-user messages, or lifecycle
|
|
457
|
+
interruptions, and explicit retries. `inputs.requestInput()` and non-user
|
|
458
|
+
messages built with `inputs.seed()` are for trusted server appends. Browser
|
|
459
|
+
inputs never append server scheduling, seeded non-user messages, or lifecycle
|
|
460
|
+
events.
|
|
380
461
|
Approval and input request/response payloads carry the active `generationId`;
|
|
381
462
|
clients copy it from the pending request so a stale interaction cannot satisfy
|
|
382
463
|
a later model step.
|
|
@@ -388,8 +469,9 @@ directly.
|
|
|
388
469
|
|
|
389
470
|
## Add tools and approval
|
|
390
471
|
|
|
391
|
-
|
|
392
|
-
|
|
472
|
+
Tools are ordinary AI SDK `tool()` definitions. A2 passes them through to
|
|
473
|
+
the AI SDK without interpreting or restricting them. Approval policy
|
|
474
|
+
belongs in `generation`, beside the other per-step settings:
|
|
393
475
|
|
|
394
476
|
```ts server/with-tools.ts
|
|
395
477
|
import { tool } from 'ai'
|
|
@@ -421,6 +503,90 @@ export const assistantServerWithTools = createAgentServer({
|
|
|
421
503
|
})
|
|
422
504
|
```
|
|
423
505
|
|
|
506
|
+
The AI SDK infers the tool input and output from its schemas and
|
|
507
|
+
`execute`. A2 additionally runs each local `execute` inside an ambient
|
|
508
|
+
scope carrying the current durable handler attempt. Read it with
|
|
509
|
+
`handlerContext(agent)` from `experimental-a2/ai`: it returns the same
|
|
510
|
+
typed context bag an event handler receives, with `event`, `attempt`,
|
|
511
|
+
`session`, and `signal`. Automatic tools see an `ai.tool.called` event.
|
|
512
|
+
Approved tools see the `ai.approval.responded` event that authorized
|
|
513
|
+
them. Narrow `ctx.event.type` if you need fields specific to either
|
|
514
|
+
event. The agent argument carries the types; A2 verifies it against the
|
|
515
|
+
server executing the tool and throws when a tool written for one agent
|
|
516
|
+
runs under another, or when `handlerContext()` is called outside a tool
|
|
517
|
+
execution. The scope survives awaited helpers and async iteration, so
|
|
518
|
+
any code below `execute` can read it.
|
|
519
|
+
|
|
520
|
+
`generation.toolApproval` is evaluated during the model step, before A2 has a
|
|
521
|
+
durable tool handler. Static policies such as the example above are simplest.
|
|
522
|
+
An input-dependent policy may be a function that returns `user-approval` or
|
|
523
|
+
`approved` from the validated tool input. Approval predicates are
|
|
524
|
+
re-evaluated on recovery for the same durable tool call, so they must be
|
|
525
|
+
deterministic over the tool input; an impure predicate can fork one
|
|
526
|
+
durable call's flow. Model-phase options such as `toolsContext` and
|
|
527
|
+
per-tool `contextSchema` are yours and pass through to the AI SDK
|
|
528
|
+
untouched. Any model-phase hook that receives tool definitions sees
|
|
529
|
+
them without local `execute` functions. The durable A2 context is
|
|
530
|
+
ambient only inside `execute`.
|
|
531
|
+
|
|
532
|
+
The agent argument supplies types, not infrastructure. The context's
|
|
533
|
+
`session` belongs to the server executing the tool, so its `schedule()`
|
|
534
|
+
method uses that server's scheduler. For example, this tool schedules a
|
|
535
|
+
reminder as a new user message:
|
|
536
|
+
|
|
537
|
+
```ts server/set-reminder.ts
|
|
538
|
+
import { tool } from 'ai'
|
|
539
|
+
import { handlerContext, inputs } from 'experimental-a2/ai'
|
|
540
|
+
import { z } from 'zod'
|
|
541
|
+
import { assistant } from '../assistant'
|
|
542
|
+
|
|
543
|
+
export const setReminder = tool({
|
|
544
|
+
description: 'Set a reminder for yourself',
|
|
545
|
+
inputSchema: z.object({
|
|
546
|
+
delaySeconds: z.number().int().min(1).max(6 * 24 * 60 * 60),
|
|
547
|
+
reminder: z.string(),
|
|
548
|
+
}),
|
|
549
|
+
execute: async ({ delaySeconds, reminder }, { toolCallId }) => {
|
|
550
|
+
const ctx = handlerContext(assistant)
|
|
551
|
+
await ctx.session.schedule(
|
|
552
|
+
`reminder:${toolCallId}`,
|
|
553
|
+
{ delay: `${delaySeconds}s` },
|
|
554
|
+
...inputs.message({
|
|
555
|
+
id: `reminder:${ctx.event.id}`,
|
|
556
|
+
role: 'user',
|
|
557
|
+
parts: [{ type: 'text', text: `Reminder: ${reminder}` }],
|
|
558
|
+
}),
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
return { scheduled: true, delaySeconds }
|
|
562
|
+
},
|
|
563
|
+
})
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
Add `setReminder` to the server's `tools` and configure a
|
|
567
|
+
[scheduler](/guides/production).
|
|
568
|
+
The numeric schema lets the model choose any whole-second delay in the
|
|
569
|
+
provider's supported range instead of selecting from a fixed application enum.
|
|
570
|
+
The example uses A2's six-day Vercel Queues limit. The seventh retention day is
|
|
571
|
+
reserved for delivery and retries. Adjust the maximum to match your scheduler
|
|
572
|
+
and plan.
|
|
573
|
+
The schedule name uses `toolCallId`, and the message id uses the durable
|
|
574
|
+
triggering event id. A retry therefore converges on the same timer and message,
|
|
575
|
+
even if a provider reuses tool-call ids in a later generation. When the timer
|
|
576
|
+
arrives, `inputs.message()` records an ordinary `ai.message.created` fact. The
|
|
577
|
+
queued-turn policy starts a fresh response after the active response completes
|
|
578
|
+
or is interrupted. A failed response must be retried or interrupted first.
|
|
579
|
+
If the scheduler send fails ambiguously or transiently, A2 retries the same
|
|
580
|
+
durable tool handler so that identity and target time stay fixed. Deterministic
|
|
581
|
+
adapter rejections are terminal tool errors. A missing scheduler is also a
|
|
582
|
+
normal tool error, so configure one before exposing the tool.
|
|
583
|
+
|
|
584
|
+
A tool that never reads `handlerContext()` is just an AI SDK tool;
|
|
585
|
+
nothing else changes. To test a context-reading tool directly, wrap the
|
|
586
|
+
call with `runWithHandlerContext(ctx, fn)` from
|
|
587
|
+
`experimental-a2/ai/server`: inside `fn`, `handlerContext()` returns
|
|
588
|
+
`ctx` for any agent.
|
|
589
|
+
|
|
424
590
|
The AI SDK emits an approval request instead of executing the tool. A2 records
|
|
425
591
|
it as `ai.approval.requested`, and the reducer adds it to
|
|
426
592
|
`state.pendingApprovals`. Respond from the same optimistic client path:
|
|
@@ -524,16 +690,38 @@ import { useSession } from '../session'
|
|
|
524
690
|
export function StopButton() {
|
|
525
691
|
const { state, push, index } = useSession()
|
|
526
692
|
const active = state.activeGeneration
|
|
693
|
+
const requested =
|
|
694
|
+
state.activeRequestId && state.activeResponseMessageId
|
|
695
|
+
? {
|
|
696
|
+
messageId: state.activeResponseMessageId,
|
|
697
|
+
requestId: state.activeRequestId,
|
|
698
|
+
}
|
|
699
|
+
: null
|
|
700
|
+
const waiting =
|
|
701
|
+
state.pendingApprovals[0] ??
|
|
702
|
+
state.pendingInputs[0] ??
|
|
703
|
+
state.tools.find((tool) => tool.status === 'running')
|
|
704
|
+
const target = active
|
|
705
|
+
? {
|
|
706
|
+
messageId: active.responseMessageId,
|
|
707
|
+
generationId: active.generationId,
|
|
708
|
+
}
|
|
709
|
+
: requested ??
|
|
710
|
+
(waiting
|
|
711
|
+
? {
|
|
712
|
+
messageId: waiting.messageId,
|
|
713
|
+
generationId: waiting.generationId,
|
|
714
|
+
}
|
|
715
|
+
: null)
|
|
527
716
|
|
|
528
|
-
if (!
|
|
717
|
+
if (!target) return null
|
|
529
718
|
|
|
530
719
|
return (
|
|
531
720
|
<button
|
|
532
721
|
onClick={() =>
|
|
533
722
|
void push(
|
|
534
723
|
...inputs.interrupt({
|
|
535
|
-
|
|
536
|
-
generationId: active.generationId,
|
|
724
|
+
...target,
|
|
537
725
|
reason: 'Stopped by the user',
|
|
538
726
|
lastSeenIndex: index,
|
|
539
727
|
}),
|
|
@@ -548,16 +736,23 @@ export function StopButton() {
|
|
|
548
736
|
|
|
549
737
|
The optimistic event updates the UI immediately and reaches A2's cancellation
|
|
550
738
|
channel. `lastSeenIndex` is the exact confirmed log frontier visible when the
|
|
551
|
-
user clicked. The reducer rewinds
|
|
552
|
-
keeps the partial response the user actually saw, and turns
|
|
553
|
-
tools into `output-error`.
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
`
|
|
560
|
-
|
|
739
|
+
user clicked. The reducer rewinds the indexed generation projection to that
|
|
740
|
+
frontier, keeps the partial response the user actually saw, and turns
|
|
741
|
+
incomplete visible tools into `output-error`. An accepted interruption
|
|
742
|
+
terminally fences that request or generation. Later generation, tool, approval,
|
|
743
|
+
input, and compaction events remain in raw history but cannot reactivate it or
|
|
744
|
+
alter the projection.
|
|
745
|
+
Completed tool results at or before the visible frontier remain completed.
|
|
746
|
+
|
|
747
|
+
Between `ai.generation.requested` and `ai.generation.started`, copy
|
|
748
|
+
`activeRequestId` as `requestId` and `activeResponseMessageId` as `messageId`.
|
|
749
|
+
Once `activeGeneration` exists, send its `generationId` and
|
|
750
|
+
`responseMessageId` instead. A request-owned interruption remains valid if
|
|
751
|
+
that same request starts before the append commits. Every interruption has
|
|
752
|
+
exactly one of `requestId` or `generationId`, so a delayed action cannot stop
|
|
753
|
+
later work that reuses the response message. After a generation completes,
|
|
754
|
+
copy the owner from a pending approval, pending input, or running tool while
|
|
755
|
+
the response is still waiting.
|
|
561
756
|
|
|
562
757
|
After a failed model call, `inputs.retry({ messageId, responseMessageId,
|
|
563
758
|
retryId })` records a retry request. The server schedules its fresh attempt.
|
|
@@ -630,6 +825,8 @@ The convenience API is ordinary A2 parts:
|
|
|
630
825
|
- `createReducer({ contract })` builds the standard `AIState` projection for a
|
|
631
826
|
compatible contract.
|
|
632
827
|
- `agent()` combines the built-in protocol, application events, and reducer.
|
|
828
|
+
- `tool()` binds an AI SDK function-tool definition to an agent's typed A2
|
|
829
|
+
handler context.
|
|
633
830
|
- `createHandlers({ agent, ... })` returns the AI handler table.
|
|
634
831
|
- `createAgentServer()` combines those handlers with `createServer()`.
|
|
635
832
|
|
|
@@ -674,7 +871,7 @@ Application events stay fully typed. The standard AI reducer ignores unknown
|
|
|
674
871
|
events, so another reducer can project application state without forking the AI
|
|
675
872
|
protocol. `validateAgentPush` preserves the same browser boundary as
|
|
676
873
|
`createAgentServer()`: user facts may enter through a parsed push, while model
|
|
677
|
-
scheduling, tool lifecycle, and
|
|
874
|
+
scheduling, tool lifecycle, and seeded non-user messages stay server-authored.
|
|
678
875
|
|
|
679
876
|
### Replace generation, not durability
|
|
680
877
|
|
|
@@ -718,6 +915,9 @@ the abort signal. It returns exactly one model step as a
|
|
|
718
915
|
`ReadableStream<UIMessageChunk>`. A tool-aware replacement sends definitions to
|
|
719
916
|
the model without running local `execute` functions. A2 still owns durable
|
|
720
917
|
progress, tool execution, approval, continuation, interruption, and failure.
|
|
918
|
+
`messageMetadata` applies only to the default generator. A custom `generate`
|
|
919
|
+
function owns its metadata and can pass a callback to `toUIMessageStream()` or
|
|
920
|
+
emit typed metadata chunks itself.
|
|
721
921
|
|
|
722
922
|
## Delivery semantics
|
|
723
923
|
|
|
@@ -735,6 +935,6 @@ id. This is the same at-least-once boundary as every other A2 handler. A fast
|
|
|
735
935
|
tool may also start before its model step later fails. The failure prevents a
|
|
736
936
|
continuation, but it cannot roll back that external effect.
|
|
737
937
|
|
|
738
|
-
Configure [production
|
|
739
|
-
server.
|
|
740
|
-
invocation disappears.
|
|
938
|
+
Configure a [production scheduler](/guides/production) exactly as for any other A2
|
|
939
|
+
server. It recovers an incomplete generation attempt after the original
|
|
940
|
+
serverless invocation disappears.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Devtools
|
|
3
|
-
description: Mount one read-only handler to inspect sessions
|
|
3
|
+
description: Mount one authenticated, read-only handler to inspect and export durable sessions from the browser, CLI, or a test.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
## Mount the dashboard
|
|
@@ -16,8 +16,9 @@ import { billingServer, ordersServer } from '@/server'
|
|
|
16
16
|
const devtools = createDevtools({
|
|
17
17
|
servers: [ordersServer, billingServer],
|
|
18
18
|
authorize: async (_request) => {
|
|
19
|
-
//
|
|
20
|
-
return
|
|
19
|
+
// your authentication and authorization check:
|
|
20
|
+
// return await canInspectA2(_request)
|
|
21
|
+
return process.env.NODE_ENV === 'development'
|
|
21
22
|
},
|
|
22
23
|
})
|
|
23
24
|
|
|
@@ -36,10 +37,132 @@ then takes over navigation and live updates.
|
|
|
36
37
|
The selected contract and session stay in the URL. Refreshing preserves the
|
|
37
38
|
view, links open the same session, and browser back and forward navigation work.
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
The mounted route is the policy boundary for the complete Devtools surface.
|
|
41
|
+
`authorize` runs before the browser application, JSON inspection endpoints,
|
|
42
|
+
live stream, capture export, and CLI requests. A2 does not maintain a second
|
|
43
|
+
credential store. Use your application's authentication and authorization at
|
|
44
|
+
this route, and give Devtools the same protection as the durable data it reads.
|
|
45
|
+
|
|
46
|
+
Omit `authorize` only for local development. An unguarded handler is available
|
|
47
|
+
when `NODE_ENV` is exactly `development`. It returns 404 when the value is
|
|
48
|
+
anything else, including unset. In a deployed dashboard, provide your
|
|
49
|
+
application's access check. Only a literal `true` grants access. Returning
|
|
50
|
+
`false` hides the route with a 404. Returning a `Response` supports a redirect
|
|
51
|
+
or authentication challenge.
|
|
52
|
+
|
|
53
|
+
## Inspect from the CLI
|
|
54
|
+
|
|
55
|
+
The `a2 devtools` CLI reads the same mounted route. Set its URL once, then list
|
|
56
|
+
contracts and sessions or inspect one session:
|
|
57
|
+
|
|
58
|
+
```bash terminal
|
|
59
|
+
export A2_DEVTOOLS_URL='https://example.com/api/a2/devtools'
|
|
60
|
+
export A2_DEVTOOLS_TOKEN="$DEVTOOLS_BEARER_TOKEN"
|
|
61
|
+
|
|
62
|
+
a2 devtools contracts
|
|
63
|
+
a2 devtools sessions --contract orders --limit 50
|
|
64
|
+
a2 devtools show --contract orders --session order-42
|
|
65
|
+
a2 devtools show --contract orders --session order-42 --format json
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`A2_DEVTOOLS_TOKEN` becomes an `Authorization: Bearer` header. For a different
|
|
69
|
+
environment variable, pass `--bearer-env DEVTOOLS_TOKEN`. Custom headers use
|
|
70
|
+
`--header-env HEADER=ENV`, which is repeatable. The CLI accepts credential
|
|
71
|
+
values only through environment variables. It rejects literal token and header
|
|
72
|
+
flags, credentials embedded in `--url`, and URLs with query strings. Requests
|
|
73
|
+
do not follow redirects, so an authentication redirect cannot forward those
|
|
74
|
+
headers to another origin.
|
|
75
|
+
|
|
76
|
+
Human-readable output is the default. `--format json` emits one JSON value and
|
|
77
|
+
`--format ndjson` emits one record per line. Session listing also accepts
|
|
78
|
+
`--cursor` for the next page. `--url` overrides `A2_DEVTOOLS_URL`.
|
|
79
|
+
|
|
80
|
+
## Export and check a capture
|
|
81
|
+
|
|
82
|
+
Export writes one exact `.a2log` capture. `check` verifies a live capture and
|
|
83
|
+
can assert operational conditions. `verify` checks a saved file without a
|
|
84
|
+
server URL or credentials:
|
|
85
|
+
|
|
86
|
+
```bash terminal
|
|
87
|
+
a2 devtools export \
|
|
88
|
+
--contract orders \
|
|
89
|
+
--session order-42 \
|
|
90
|
+
--output artifacts/order-42.a2log
|
|
91
|
+
|
|
92
|
+
a2 devtools check \
|
|
93
|
+
--contract orders \
|
|
94
|
+
--session order-42 \
|
|
95
|
+
--settled \
|
|
96
|
+
--no-dead-letters \
|
|
97
|
+
--no-caught-failures \
|
|
98
|
+
--max-redispatches 0
|
|
99
|
+
|
|
100
|
+
a2 devtools verify artifacts/order-42.a2log
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
An export contains a versioned manifest, every durable event in log order,
|
|
104
|
+
snapshot metadata, and a footer with counts and a SHA-256 digest. Event records
|
|
105
|
+
retain payloads, causal edges, lanes, dispatch attempts, failures, claims, and
|
|
106
|
+
completion metadata. Snapshot state is not included. The CLI checks the digest
|
|
107
|
+
and structure before writing the capture. It refuses to replace an existing
|
|
108
|
+
file unless you pass `--force`, and creates capture files with owner-only
|
|
109
|
+
permissions (`0600`).
|
|
110
|
+
|
|
111
|
+
The CLI and browser read large sessions in bounded pages. The first page
|
|
112
|
+
freezes a finite event frontier, so messages appended during the download do
|
|
113
|
+
not extend it. Export includes every event through that frontier. If a store or
|
|
114
|
+
provider returns a gap or ends early, the read fails instead of producing a
|
|
115
|
+
partial capture with a valid footer.
|
|
116
|
+
|
|
117
|
+
The capture is canonical UTF-8 NDJSON. If a durable payload contains a value
|
|
118
|
+
that JSON would coerce or omit, export fails instead of signing a changed
|
|
119
|
+
representation. Keep event payloads JSON-compatible when they need to be
|
|
120
|
+
portable.
|
|
121
|
+
|
|
122
|
+
Capture verification and failed `check` assertions exit with status 2. Usage,
|
|
123
|
+
network, and filesystem errors exit with status 1. The live commands support
|
|
124
|
+
the same human, JSON, and NDJSON output modes, which makes `check` useful in CI.
|
|
125
|
+
|
|
126
|
+
An exact capture is sensitive production data. It can contain user payloads,
|
|
127
|
+
AI prompts and tool results, provider metadata, usage, and failure messages
|
|
128
|
+
when those values exist in the event log. Route authorization controls who can
|
|
129
|
+
download a capture. It does not redact or encrypt the downloaded file. Store,
|
|
130
|
+
share, and expire `.a2log` files under the same policy as the source log.
|
|
131
|
+
|
|
132
|
+
## Replay reducer state offline
|
|
133
|
+
|
|
134
|
+
`experimental-a2/testing` loads a capture in Node.js, verifies it against the
|
|
135
|
+
current contract, and folds its events through a reducer:
|
|
136
|
+
|
|
137
|
+
```ts test/replay-order.ts
|
|
138
|
+
import { loadCapture, replayCapture } from 'experimental-a2/testing'
|
|
139
|
+
import { orders } from '@/contracts'
|
|
140
|
+
import { ordersReducer } from '@/reducer'
|
|
141
|
+
|
|
142
|
+
export async function replayOrder(path: string) {
|
|
143
|
+
const loaded = await loadCapture(path, orders)
|
|
144
|
+
|
|
145
|
+
const afterFirstEvent = replayCapture(loaded, ordersReducer, {
|
|
146
|
+
throughIndex: 1,
|
|
147
|
+
})
|
|
148
|
+
const current = replayCapture(loaded, ordersReducer)
|
|
149
|
+
|
|
150
|
+
return { afterFirstEvent, current }
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`loadCapture()` verifies the capture digest and identity, validates every event
|
|
155
|
+
type and payload through the current contract schemas, and revives the public
|
|
156
|
+
`createdAt` timestamp as `Date`. The loaded value also keeps `rawEvents` with
|
|
157
|
+
the exact wire payloads and operational metadata.
|
|
158
|
+
|
|
159
|
+
`throughIndex` is inclusive. Pass `0` to return the reducer's initial state at
|
|
160
|
+
index 0. Replay always folds immutable events. Captured snapshots contain
|
|
161
|
+
metadata only and are not used as reducer state.
|
|
162
|
+
|
|
163
|
+
This is pure reducer replay. It does not construct a server, run handlers,
|
|
164
|
+
invoke a model or tool, or repeat an external side effect. Test handler or model
|
|
165
|
+
behavior with a fresh log and explicit test doubles instead.
|
|
43
166
|
|
|
44
167
|
## Causal forest
|
|
45
168
|
|
|
@@ -76,7 +199,7 @@ dashboard shows the first dispatch time as unknown instead of moving it to
|
|
|
76
199
|
|
|
77
200
|
The bar is event lifetime, not handler execution time. A2 stores lifecycle
|
|
78
201
|
summaries, not one span row for every attempt. Time spent waiting for a lane,
|
|
79
|
-
running a handler, holding a claim, or waiting for
|
|
202
|
+
running a handler, holding a claim, or waiting for the scheduler remains one honest
|
|
80
203
|
interval.
|
|
81
204
|
|
|
82
205
|
An event type without a handler is complete at append. Its bar has no waiting
|
|
@@ -96,9 +219,11 @@ While work is pending, the handler checks processing metadata quickly. It
|
|
|
96
219
|
backs off after the session settles and emits heartbeats for intermediaries.
|
|
97
220
|
Only sessions someone is viewing have a live inspection loop.
|
|
98
221
|
|
|
99
|
-
## Custom
|
|
222
|
+
## Custom stores
|
|
100
223
|
|
|
101
224
|
The memory, SQLite, Postgres, and Redis adapters provide the optional read-only
|
|
102
|
-
inspection interface. A custom `
|
|
103
|
-
and `inspect.listSnapshots` to appear in the dashboard.
|
|
104
|
-
|
|
225
|
+
inspection interface. A custom `A2Store` can implement `inspect.listSessions`
|
|
226
|
+
and `inspect.listSnapshots` to appear in the dashboard. Add
|
|
227
|
+
`inspect.readEvents` for bounded storage-native event pages. Without it, A2
|
|
228
|
+
reads the complete session for each page and slices it in memory. Stores without
|
|
229
|
+
inspection continue to work normally; their devtools endpoint returns 501.
|
|
@@ -49,7 +49,7 @@ projects, issue lists, labels, permissions, search, joins, pagination, and
|
|
|
49
49
|
reporting. Use its transactions for constraints that must hold across several
|
|
50
50
|
rows before a request returns.
|
|
51
51
|
|
|
52
|
-
The Postgres adapter in `experimental-a2/
|
|
52
|
+
The Postgres adapter in `experimental-a2/store-postgres` stores A2's own event logs. It does not
|
|
53
53
|
watch your application tables, turn their changes into events, or combine an
|
|
54
54
|
A2 append with your SQL transaction. The A2 log and your tables may share one
|
|
55
55
|
Postgres database, but they remain separate data models and transaction
|