experimental-a2 0.18.0 → 0.19.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 +39 -0
- package/dist/{actor-Bjce9_5U.d.ts → actor-C7b-1arn.d.ts} +3 -3
- package/dist/{actor-Bjce9_5U.d.ts.map → actor-C7b-1arn.d.ts.map} +1 -1
- package/dist/actor-client.d.ts +1 -1
- package/dist/actor-react.d.ts +1 -1
- package/dist/actor-react.js +1 -1
- package/dist/actor.d.ts +1 -1
- package/dist/actor.js +2 -2
- package/dist/{ai-DrUaMyRt.d.ts → ai-Cz3hqVhk.d.ts} +34 -17
- package/dist/ai-Cz3hqVhk.d.ts.map +1 -0
- package/dist/ai-client.d.ts +1 -1
- package/dist/ai-client.js +2 -2
- package/dist/ai-control-CvLbW10B.js +46 -0
- package/dist/ai-control-CvLbW10B.js.map +1 -0
- package/dist/ai-fx.d.ts +1 -1
- package/dist/ai-fx.d.ts.map +1 -1
- package/dist/ai-fx.js +122 -44
- package/dist/ai-fx.js.map +1 -1
- package/dist/{ai-harness-BuLh_EYi.js → ai-harness-BuuC-gJ3.js} +79 -15
- package/dist/ai-harness-BuuC-gJ3.js.map +1 -0
- package/dist/{ai-harness-DvsPeJeB.d.ts → ai-harness-CV6rXbB3.d.ts} +2 -2
- package/dist/ai-harness-CV6rXbB3.d.ts.map +1 -0
- package/dist/{ai-BpOGm-zB.js → ai-mMiWul1T.js} +117 -78
- package/dist/ai-mMiWul1T.js.map +1 -0
- package/dist/ai-pi.d.ts +1 -1
- package/dist/ai-pi.d.ts.map +1 -1
- package/dist/ai-pi.js +114 -23
- package/dist/ai-pi.js.map +1 -1
- package/dist/ai-react.d.ts +1 -1
- package/dist/ai-react.js +2 -2
- package/dist/ai-server.d.ts +3 -3
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +127 -55
- package/dist/ai-server.js.map +1 -1
- package/dist/ai-tool-input-DmSqMUQi.js +40 -0
- package/dist/ai-tool-input-DmSqMUQi.js.map +1 -0
- package/dist/ai.d.ts +2 -2
- package/dist/ai.js +1 -1
- package/dist/{client-BReKqwZC.js → client-BhHLoNKF.js} +3 -3
- package/dist/{client-BReKqwZC.js.map → client-BhHLoNKF.js.map} +1 -1
- package/dist/client.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/{internal-B2T3U4Rq.js → internal-CscSY7K_.js} +4 -2
- package/dist/{internal-B2T3U4Rq.js.map → internal-CscSY7K_.js.map} +1 -1
- package/dist/react.js +1 -1
- package/dist/scheduler-qstash.d.ts +2 -2
- package/dist/scheduler-qstash.js +3 -3
- package/dist/scheduler-qstash.js.map +1 -1
- package/dist/scheduler-vercel.d.ts +7 -7
- package/dist/scheduler-vercel.d.ts.map +1 -1
- package/dist/scheduler-vercel.js +10 -14
- package/dist/scheduler-vercel.js.map +1 -1
- package/dist/{server-Dw_k_7kO.js → server-D-Y4pqwu.js} +37 -11
- package/dist/server-D-Y4pqwu.js.map +1 -0
- package/dist/{server-En7cbi3v.d.ts → server-DYAATFuT.d.ts} +11 -5
- package/dist/server-DYAATFuT.d.ts.map +1 -0
- package/dist/server.d.ts +2 -2
- package/dist/server.js +1 -1
- package/dist/{session-clear-Bo-XTFEy.d.ts → session-clear-C-QwTXV1.d.ts} +2 -2
- package/dist/session-clear-C-QwTXV1.d.ts.map +1 -0
- package/dist/{store-Djdv0bnZ.d.ts → store-CyLUy86v.d.ts} +23 -4
- package/dist/store-CyLUy86v.d.ts.map +1 -0
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-memory.js +25 -6
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-B5FcwJfo.js → store-polling-7WJV0YWP.js} +2 -2
- package/dist/{store-polling-B5FcwJfo.js.map → store-polling-7WJV0YWP.js.map} +1 -1
- package/dist/store-postgres.d.ts +1 -1
- package/dist/store-postgres.d.ts.map +1 -1
- package/dist/store-postgres.js +55 -25
- package/dist/store-postgres.js.map +1 -1
- package/dist/{store-presence-polling-BAwS8RWn.js → store-presence-polling-B1ixeqIS.js} +3 -3
- package/dist/{store-presence-polling-BAwS8RWn.js.map → store-presence-polling-B1ixeqIS.js.map} +1 -1
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +3 -3
- package/dist/{store-redis-notify-BRITti_o.js → store-redis-notify-CM0AfLY0.js} +88 -37
- package/dist/store-redis-notify-CM0AfLY0.js.map +1 -0
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.js +1 -1
- package/dist/store-sqlite.d.ts +1 -1
- package/dist/store-sqlite.js +35 -20
- package/dist/store-sqlite.js.map +1 -1
- package/dist/{wire-rSUglYo8.js → wire-BKxH1vbP.js} +2 -2
- package/dist/{wire-rSUglYo8.js.map → wire-BKxH1vbP.js.map} +1 -1
- package/docs/agents/03-conversations.mdx +27 -15
- package/docs/agents/04-harnesses.mdx +51 -5
- package/docs/contract/03-sessions.mdx +1 -1
- package/docs/index.mdx +3 -3
- package/docs/reference/02-server.mdx +12 -7
- package/docs/reference/04-ai.mdx +41 -23
- package/docs/reference/05-adapters.mdx +6 -6
- package/docs/server/01-handlers.mdx +1 -1
- package/docs/server/02-timers.mdx +1 -1
- package/docs/server/03-cancellation.mdx +3 -2
- package/docs/server/05-production.mdx +4 -3
- package/docs/server/06-guarantees.mdx +20 -11
- package/examples/playground/DEPLOY.md +11 -4
- package/examples/playground/app/a0/[appId]/page.tsx +2 -8
- package/examples/playground/app/a0/a0.css +244 -5
- package/examples/playground/app/a0/app-view.tsx +2 -1
- package/examples/playground/app/a0/attachments.ts +133 -0
- package/examples/playground/app/a0/chat.test.tsx +504 -0
- package/examples/playground/app/a0/chat.tsx +623 -63
- package/examples/playground/app/a0/context-menu.tsx +38 -11
- package/examples/playground/app/a0/devtools.tsx +2 -8
- package/examples/playground/app/a0/model.test.ts +28 -0
- package/examples/playground/app/a0/model.ts +30 -0
- package/examples/playground/app/a0/server.ts +13 -19
- package/examples/playground/app/a0/uploads/route.ts +29 -0
- package/examples/playground/app/agent/[agentId]/agent-client.tsx +2 -1
- package/examples/playground/app/agent/[agentId]/agent-queue.test.tsx +1 -0
- package/examples/playground/app/agent/model.test.ts +12 -3
- package/examples/playground/app/chat/[chatId]/agent-state.test.ts +1 -5
- package/examples/playground/next.config.ts +7 -4
- package/examples/playground/package.json +3 -1
- package/package.json +1 -1
- package/src/ai-agent.ts +14 -3
- package/src/ai-client-state.ts +21 -9
- package/src/ai-context-schema.ts +28 -4
- package/src/ai-context.ts +83 -3
- package/src/ai-control-server.ts +70 -43
- package/src/ai-control-state.ts +3 -4
- package/src/ai-control.ts +59 -10
- package/src/ai-coordinator.ts +7 -6
- package/src/ai-fx.ts +184 -67
- package/src/ai-harness.ts +153 -29
- package/src/ai-message-shape.ts +41 -0
- package/src/ai-pi.ts +161 -46
- package/src/ai-stored-state.ts +1 -3
- package/src/ai-tool-input.ts +45 -0
- package/src/ai.ts +116 -77
- package/src/internal.ts +6 -0
- package/src/libfx.d.ts +8 -1
- package/src/scheduler-qstash.ts +5 -5
- package/src/scheduler-vercel.ts +15 -27
- package/src/server.ts +58 -12
- package/src/store-memory.ts +50 -9
- package/src/store-postgres.ts +79 -28
- package/src/store-redis-core.ts +112 -31
- package/src/store-sqlite.ts +61 -28
- package/src/store.ts +22 -3
- package/dist/ai-BpOGm-zB.js.map +0 -1
- package/dist/ai-DrUaMyRt.d.ts.map +0 -1
- package/dist/ai-harness-BuLh_EYi.js.map +0 -1
- package/dist/ai-harness-DvsPeJeB.d.ts.map +0 -1
- package/dist/server-Dw_k_7kO.js.map +0 -1
- package/dist/server-En7cbi3v.d.ts.map +0 -1
- package/dist/session-clear-Bo-XTFEy.d.ts.map +0 -1
- package/dist/store-Djdv0bnZ.d.ts.map +0 -1
- package/dist/store-redis-notify-BRITti_o.js.map +0 -1
- package/examples/playground/app/a0/no-photon.cjs +0 -4
|
@@ -110,17 +110,63 @@ Both harnesses call models through Vercel AI Gateway. `model` is a Gateway
|
|
|
110
110
|
model ID, or [`ModelSettings`](/reference/ai#modelsettings) with one, resolved
|
|
111
111
|
for each step like any agent's model; `pi()` takes the Gateway models Pi's own
|
|
112
112
|
catalog lists. `reasoning` becomes FX's effort or Pi's thinking level, and
|
|
113
|
-
`speed: 'fast'` asks
|
|
114
|
-
`AI_GATEWAY_API_KEY`, or from Vercel's OIDC
|
|
115
|
-
|
|
116
|
-
|
|
113
|
+
`speed: 'fast'` asks AI Gateway for the model's faster tier under either
|
|
114
|
+
harness. Credentials come from `AI_GATEWAY_API_KEY`, or from Vercel's OIDC
|
|
115
|
+
token on Vercel. Seeded assistant messages do not reach the harness.
|
|
116
|
+
|
|
117
|
+
## Images and files
|
|
118
|
+
|
|
119
|
+
The harness reads each user message's text and its image files. Neither FX nor
|
|
120
|
+
Pi takes URLs, so A2 fetches each image from its `https:` or `data:` URL and
|
|
121
|
+
hands the harness its bytes. The fetch goes through the
|
|
122
|
+
AI SDK's download, which refuses private addresses, checks each redirect, and
|
|
123
|
+
stops past 32 MiB of one message's files. Each step of a response replays its
|
|
124
|
+
turn, so a process keeps the images it fetched instead of fetching them again.
|
|
125
|
+
|
|
126
|
+
A file that isn't an image, or that A2 can't fetch, fails the turn with an
|
|
127
|
+
error that names it. So does a steer with files under FX, which steers with
|
|
128
|
+
text only. The harness applies its own rules to the images it gets: FX fails a
|
|
129
|
+
turn whose model doesn't take images, and Pi resizes them. Every step sends the
|
|
130
|
+
conversation so far, files and all, so a file that fails one turn fails the
|
|
131
|
+
turns after it too.
|
|
132
|
+
|
|
133
|
+
A2 fetches whatever URL a user message names, so check file URLs on the way
|
|
134
|
+
in. An agent's `messageSchema` can require them to point at your storage:
|
|
135
|
+
|
|
136
|
+
```ts server/reviewer.ts
|
|
137
|
+
import type { UIMessage } from 'ai'
|
|
138
|
+
import { agent } from 'experimental-a2/ai/server'
|
|
139
|
+
import { fx } from 'experimental-a2/ai/fx'
|
|
140
|
+
import { z } from 'zod'
|
|
141
|
+
|
|
142
|
+
// your storage's public URL:
|
|
143
|
+
const uploads = 'https://uploads.example.com/'
|
|
144
|
+
|
|
145
|
+
export const reviewer = agent({
|
|
146
|
+
name: 'reviewer',
|
|
147
|
+
harness: fx(),
|
|
148
|
+
model: 'anthropic/claude-sonnet-5.5',
|
|
149
|
+
instructions: 'Review the screenshots the user sends. Keep replies short.',
|
|
150
|
+
messageSchema: z
|
|
151
|
+
.custom<UIMessage>()
|
|
152
|
+
.refine(
|
|
153
|
+
(message) =>
|
|
154
|
+
message.role !== 'user' ||
|
|
155
|
+
message.parts.every(
|
|
156
|
+
(part) => part.type !== 'file' || part.url.startsWith(uploads),
|
|
157
|
+
),
|
|
158
|
+
"A user's files must be uploads",
|
|
159
|
+
),
|
|
160
|
+
})
|
|
161
|
+
```
|
|
117
162
|
|
|
118
163
|
## What the harness owns
|
|
119
164
|
|
|
120
165
|
The harness owns its loop, its prompt, and its compaction, so `generation`,
|
|
121
166
|
`generate`, `compaction`, and `messageMetadata` exist only in
|
|
122
167
|
`{ type: 'ai-sdk' }`. A harness runs its tools without approval, so a tool that
|
|
123
|
-
sets `needsApproval` is rejected.
|
|
168
|
+
sets `needsApproval` is rejected. It hands the engine only tools A2 runs, so a
|
|
169
|
+
provider-defined tool, such as a provider's web search, is rejected too. `maxSteps` limits the model calls in one
|
|
124
170
|
response under every harness. Steering enters at the next step, as with any
|
|
125
171
|
agent: the harness sees the steered message before its next model call.
|
|
126
172
|
|
|
@@ -109,7 +109,7 @@ append returned.
|
|
|
109
109
|
Delivery runs the same conditional clear and finishes the deletion before it
|
|
110
110
|
acknowledges, so provider retries complete an interrupted delete. No event is
|
|
111
111
|
appended. The clear happens at delivery, not at an exact deadline, within
|
|
112
|
-
provider limits: Vercel Queues accepts delays up to
|
|
112
|
+
provider limits: Vercel Queues accepts delays up to seven days.
|
|
113
113
|
|
|
114
114
|
## Sessions and your database
|
|
115
115
|
|
package/docs/index.mdx
CHANGED
|
@@ -197,9 +197,9 @@ derived view exactly. See [Cancellation](/server/cancellation).
|
|
|
197
197
|
<details>
|
|
198
198
|
<summary>What happens when a handler keeps failing?</summary>
|
|
199
199
|
|
|
200
|
-
It retries with backoff, then dead-letters after ten
|
|
201
|
-
|
|
202
|
-
other lanes continue.
|
|
200
|
+
It retries with backoff, then dead-letters after ten failed attempts, whether
|
|
201
|
+
the handler threw or its process ended. Later events in the same lane wait
|
|
202
|
+
until you retry or skip it; unlaned events and other lanes continue.
|
|
203
203
|
[Guarantees](/server/guarantees) has the exact guarantees and the
|
|
204
204
|
recovery story behind them.
|
|
205
205
|
|
|
@@ -252,7 +252,8 @@ server.drain(sessionId: string): Promise<{ settled: boolean }>
|
|
|
252
252
|
```
|
|
253
253
|
|
|
254
254
|
Claims every eligible event and runs their handlers concurrently.
|
|
255
|
-
`settled` means no actionable or live-claimed work remains.
|
|
255
|
+
`settled` means no actionable or live-claimed work remains. Past the
|
|
256
|
+
invocation's deadline it claims nothing and returns `settled: false`. A dead-lettered
|
|
256
257
|
event can block later work in its lane while other lanes continue, until
|
|
257
258
|
[`session.redrive()`](#sessionredriveindex-options) resolves it. Scheduler
|
|
258
259
|
callbacks use it; you'll rarely call it yourself. The public result stays this
|
|
@@ -343,6 +344,7 @@ interface A2Store {
|
|
|
343
344
|
ttlMs: number
|
|
344
345
|
expiresAtMs?: number
|
|
345
346
|
excludeIndexes?: readonly number[]
|
|
347
|
+
maxFailures?: number
|
|
346
348
|
}): Promise<StoreClaimAvailableResult>
|
|
347
349
|
|
|
348
350
|
renewClaims(options: {
|
|
@@ -366,6 +368,7 @@ interface A2Store {
|
|
|
366
368
|
attempt: number
|
|
367
369
|
error: string
|
|
368
370
|
maxFailures: number
|
|
371
|
+
retryAfterMs?: number
|
|
369
372
|
}): Promise<{
|
|
370
373
|
outcome: 'failed' | 'dead_lettered' | 'superseded'
|
|
371
374
|
failureCount: number
|
|
@@ -376,10 +379,10 @@ interface A2Store {
|
|
|
376
379
|
| Method | Atomic effect |
|
|
377
380
|
| --- | --- |
|
|
378
381
|
| `append` | Write a consecutive batch and report session-wide pending handler work as `hasPending`. Events with the internal `settled` flag get `processedAt` in the same transaction, with no dispatch attempt. An event with a `cause` commits only while `cause.attempt` is the parent's latest attempt and the parent isn't dead-lettered; otherwise throw `SUPERSEDED_ATTEMPT`. The check serializes against a concurrent claim, after the idempotent-replay path. |
|
|
379
|
-
| `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`. |
|
|
382
|
+
| `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`. Taking over a lapsed claim, one its holder never completed or failed, adds one to `failureCount`; when that reaches `maxFailures`, dead-letter the event instead of claiming it: advance `attemptCount` to fence the lapsed attempt, leave `lastFailedAttempt` unchanged, and return it with `failedAt` set. A failure's retry hold has no holder, so it is no lapsed claim. |
|
|
380
383
|
| `renewClaims` | Extend the listed live claims the holder still owns. `renewed` lists those; `superseded` lists claims whose event's `attemptCount` passed the listed attempt. An expired claim no successor took is in neither and may still complete; renewal never revives it. |
|
|
381
384
|
| `completeAttempt` | In one transaction: fence on the current attempt, mark the parent processed, store its exact ordered `returnedEventIds`, and append the returned batch. A same-attempt retry returns the committed children; a stale attempt returns `superseded`. |
|
|
382
|
-
| `failAttempt` | Record one current caught failure, clear its claim, and dead-letter at `maxFailures`. A repeated acknowledgment is idempotent; a stale attempt returns `superseded`. |
|
|
385
|
+
| `failAttempt` | Record one current caught failure, clear its claim, and dead-letter at `maxFailures`. Short of that, with `retryAfterMs` the event stays unclaimable for that long: set its claim expiry with no holder, so `claimAvailable` reports it `busy` until then. A repeated acknowledgment is idempotent; a stale attempt returns `superseded`. |
|
|
383
386
|
| `clear` | Serialized with `append`. Check `ifLastIndex` against the head and the latest marker, then append a settled clear marker at the next index, touching no existing rows. A `cause` is fenced like a handler append. |
|
|
384
387
|
| `reclaim` | Delete one bounded batch of rows below the latest clear marker, with their bookkeeping, event-id records, lane pointers, and cached state. Report how many rows went, and `done` when none are left. Never wait on a lock other methods hold. |
|
|
385
388
|
| `deadLetters` | Return the unprocessed events with `failedAt` set above the latest clear marker, oldest first. |
|
|
@@ -412,7 +415,8 @@ stores support both.
|
|
|
412
415
|
Stored fields:
|
|
413
416
|
|
|
414
417
|
- `attemptCount` counts claims, including abandoned ones. `failureCount`
|
|
415
|
-
counts caught failures and
|
|
418
|
+
counts failed attempts: caught failures, and lapsed claims a later claim
|
|
419
|
+
took over. Ten dead-letter the event. See
|
|
416
420
|
[Guarantees](/server/guarantees#one-event-many-attempts).
|
|
417
421
|
- `cause` names the event and handler attempt whose `ctx.session.append` or
|
|
418
422
|
return value first persisted the child; `null` means a top-level event.
|
|
@@ -679,9 +683,10 @@ type DeadLetter = Event & {
|
|
|
679
683
|
The session's dead-lettered events, oldest first, each with why it failed. See
|
|
680
684
|
[When an event dead-letters](/server/production#3-when-an-event-dead-letters).
|
|
681
685
|
|
|
682
|
-
- `failureCount` counts the
|
|
683
|
-
|
|
684
|
-
last
|
|
686
|
+
- `failureCount` counts the attempts that spent its budget: caught failures,
|
|
687
|
+
and attempts that ended before they finished. `attemptCount` counts every
|
|
688
|
+
claim. `lastError` is the last error a handler threw, stringified, or null
|
|
689
|
+
when every failed attempt ended before it finished.
|
|
685
690
|
- Events at or below the latest clear marker are never listed.
|
|
686
691
|
- Only on `server.session(id)`. `DeadLetter` is exported from
|
|
687
692
|
`experimental-a2/server`.
|
package/docs/reference/04-ai.mdx
CHANGED
|
@@ -39,8 +39,8 @@ state: serve it with `agent({ contract })` and use it with
|
|
|
39
39
|
through.
|
|
40
40
|
- An `initialState` key that `AIState` also has throws a `TypeError` and fails
|
|
41
41
|
to typecheck.
|
|
42
|
-
- The reducer name is `AI_STATE_REDUCER` (`a2.ai.state.
|
|
43
|
-
`reducer`, else `${reducer.name}+a2.ai.state.
|
|
42
|
+
- The reducer name is `AI_STATE_REDUCER` (`a2.ai.state.v27`) without
|
|
43
|
+
`reducer`, else `${reducer.name}+a2.ai.state.v27`, so checkpoints advance when
|
|
44
44
|
either fold changes. `AgentReducer` types that name (so
|
|
45
45
|
[`createAgentClient()`](#createagentclientoptions) accepts a plain
|
|
46
46
|
`agentContract()`) and carries `X` to `agent()`'s resolvers.
|
|
@@ -61,9 +61,9 @@ even when `M` declares its own data parts.
|
|
|
61
61
|
|
|
62
62
|
### `AIMessageOf<Events>`
|
|
63
63
|
|
|
64
|
-
The `UIMessage` type a contract's `ai.
|
|
65
|
-
`AIMessageOf<typeof contract.events>`; `UIMessage` otherwise.
|
|
66
|
-
messages with it.
|
|
64
|
+
The `UIMessage` type a contract's input commands (`ai.control.requested`)
|
|
65
|
+
carry, such as `AIMessageOf<typeof contract.events>`; `UIMessage` otherwise.
|
|
66
|
+
`agent()` types messages with it.
|
|
67
67
|
|
|
68
68
|
### `handlerContext(contract?)`
|
|
69
69
|
|
|
@@ -105,13 +105,13 @@ Pure typed builders of `ai.control.requested` commands, for `append()` and
|
|
|
105
105
|
| `inputs.requestInput(request)` | requests application input from trusted server code |
|
|
106
106
|
| `inputs.retry(options)` | retries a failed response |
|
|
107
107
|
| `inputs.stop({ turnId, lastSeenIndex? })` | ends the observed turn and advances when admission is open |
|
|
108
|
-
| `inputs.steer({ turnId, message, lastSeenIndex? })` |
|
|
108
|
+
| `inputs.steer({ turnId, message, lastSeenIndex? })` | queues the new input right after the observed turn's earlier steering, or first, for a handoff after its current step; the turn's steering enters together, its last generating input starting the turn |
|
|
109
109
|
| `inputs.queue.edit({ message })` | changes a pending input; its ID stays fixed |
|
|
110
110
|
| `inputs.queue.move({ inputId, beforeId })` | moves pending input before an ID; `null` means the end |
|
|
111
111
|
| `inputs.queue.remove({ inputId })` | removes pending input; its submission stays in the log |
|
|
112
|
-
| `inputs.queue.sendNow({ inputId, turnId, lastSeenIndex? })` | promotes an existing queued input for a handoff after the current step; preserves pause |
|
|
112
|
+
| `inputs.queue.sendNow({ inputId, turnId, lastSeenIndex? })` | promotes an existing queued input, after the observed turn's earlier steering, for a handoff after the current step; preserves pause |
|
|
113
113
|
| `inputs.pause({ when: 'now' \| 'after-turn' })` | pauses execution now or inbox admission after the turn |
|
|
114
|
-
| `inputs.resume()` | resumes retained work, then inbox admission |
|
|
114
|
+
| `inputs.resume()` | resumes retained work, then inbox admission; a turn paused now with steering hands it off instead |
|
|
115
115
|
| `inputs.toolResult(result)` | records a deferred provider-executed tool result from trusted server code |
|
|
116
116
|
| `inputs.interrupt(options)` | interrupts a specific generation/request owner |
|
|
117
117
|
|
|
@@ -174,7 +174,9 @@ Pure typed builders of `ai.control.requested` commands, for `append()` and
|
|
|
174
174
|
- **Interruption.** `ai.message.interrupted` is `{ messageId, requestId,
|
|
175
175
|
reason?, lastSeenIndex? }` or `{ messageId, generationId, reason?,
|
|
176
176
|
lastSeenIndex? }`: copy `activeRequestId` before the generation starts,
|
|
177
|
-
`activeGeneration.generationId` after.
|
|
177
|
+
`activeGeneration.generationId` after. A2 writes the reason
|
|
178
|
+
`'steered after step'` itself when steering ends a response, so an
|
|
179
|
+
interruption you send can't use it. A request-owned interruption survives
|
|
178
180
|
that request starting first. An older or mismatched message/owner pair is a
|
|
179
181
|
no-op. An accepted one fences its owner for good: later events stay in raw
|
|
180
182
|
history but cannot alter `AIState`, satisfy pending work, or reactivate the
|
|
@@ -249,7 +251,7 @@ extra I/O or raw-history reads.
|
|
|
249
251
|
win.
|
|
250
252
|
- Steer, and Send now against an active turn, show in `state.messages` after
|
|
251
253
|
the current response, not the inbox, until admission; they stay out of the
|
|
252
|
-
prompt. Several steering inputs show in
|
|
254
|
+
prompt. Several steering inputs show in the order they were sent,
|
|
253
255
|
and the display survives receipt and hydration, including while paused.
|
|
254
256
|
- A pending Stop clears the displayed `state.active`, generation, and pending
|
|
255
257
|
approvals and inputs, previews an idle status (paused when the inbox is), and
|
|
@@ -274,8 +276,8 @@ events. Encoding is internal; no event or tool payload is truncated.
|
|
|
274
276
|
| Field | What it holds |
|
|
275
277
|
| --- | --- |
|
|
276
278
|
| `messages` | [`TranscriptMessage<M>`](#transcriptmessagem) values; server-side, without pending inputs |
|
|
277
|
-
| `inbox` | `{ paused, items }`; items are `{ id, message, generate, afterStepOf? }`, where `afterStepOf` is an observed steering turn that hands off after its current step (removal cancels it, admission consumes it) |
|
|
278
|
-
| `active` | `null` or `{ turnId, input, phase }
|
|
279
|
+
| `inbox` | `{ paused, items }`; items are `{ id, role, message, generate, afterStepOf? }`, where `afterStepOf` is an observed steering turn that hands off after its current step (removal cancels it, admission consumes it) |
|
|
280
|
+
| `active` | `null` or `{ turnId, input, phase }`, where `input` is the admitted input's `{ id, role, generate }` |
|
|
279
281
|
| `status` | generation status |
|
|
280
282
|
| `starting` | a speculative optimistic turn |
|
|
281
283
|
| `receipt` | the latest user command receipt |
|
|
@@ -311,7 +313,10 @@ See [What happens after a send](/agents/introduction#what-happens-after-a-send).
|
|
|
311
313
|
|
|
312
314
|
`deriveUIMessages()` folds a raw event history into `TranscriptMessage<M>[]`.
|
|
313
315
|
`reduceAIState()` is the event-at-a-time form the standard reducer uses. Both
|
|
314
|
-
project the same chunk-only protocol synchronously.
|
|
316
|
+
project the same chunk-only protocol synchronously. A message's content is in
|
|
317
|
+
the command that brought it in, so a fold needs each `ai.message.created`'s
|
|
318
|
+
command before it; one without throws. Only A2's control lane writes
|
|
319
|
+
`ai.message.created`.
|
|
315
320
|
|
|
316
321
|
## `experimental-a2/ai/server`
|
|
317
322
|
|
|
@@ -525,7 +530,11 @@ name the harness's own tools, such as Pi's `bash`. `model` is resolved per step
|
|
|
525
530
|
as for any agent. `instructions` and `tools` reach the harness; how they
|
|
526
531
|
combine with its own prompt and tools depends on the harness. Tool approval
|
|
527
532
|
policy lives in `harness.generation`, so a harness agent's tools run without
|
|
528
|
-
approval, and a tool that sets `needsApproval` is rejected.
|
|
533
|
+
approval, and a tool that sets `needsApproval` is rejected. So is a
|
|
534
|
+
provider-defined tool (`type: 'provider'`), which the engine can't run. A user
|
|
535
|
+
message reaches the harness as its text and its images, which A2 fetches from
|
|
536
|
+
their `https:` or `data:` URLs; any other file fails the turn. See
|
|
537
|
+
[Images and files](/agents/harnesses#images-and-files).
|
|
529
538
|
|
|
530
539
|
A harness resumes after a crash at the interrupted step. Finished model calls
|
|
531
540
|
are not made again and finished tool calls do not run again. A2 rebuilds the
|
|
@@ -577,7 +586,10 @@ fx(): Harness<'fx'>
|
|
|
577
586
|
The [FX](https://fx.sh) harness for `agent({ harness })`, running FX's kernel
|
|
578
587
|
from `libfx` (an optional peer dependency) in the server process. FX adds no
|
|
579
588
|
system prompt and brings no tools: `instructions` is the whole system prompt
|
|
580
|
-
and the agent's `tools` are all FX can call.
|
|
589
|
+
and the agent's `tools` are all FX can call. A call's input goes through its
|
|
590
|
+
tool's schema, as under `{ type: 'ai-sdk' }`: `execute` receives the schema's
|
|
591
|
+
output, defaults included, and input the schema rejects goes back to the model
|
|
592
|
+
as an error, without running the tool. `reasoning` becomes FX's effort and
|
|
581
593
|
`speed: 'fast'` its fast tier. FX compacts its own context; a compaction
|
|
582
594
|
appears in the transcript as a `data-a2-compaction` part with its summary.
|
|
583
595
|
|
|
@@ -586,8 +598,8 @@ continue a response, A2 restores FX's latest checkpoint and replays the
|
|
|
586
598
|
response so far from the log: recorded model responses and tool results answer
|
|
587
599
|
the kernel without network calls or tool runs, then FX makes its next model
|
|
588
600
|
call. Replay grows with the response, roughly 0.1 to 0.5 seconds for one of 80
|
|
589
|
-
to 200 tool calls.
|
|
590
|
-
|
|
601
|
+
to 200 tool calls. When libfx can't take a checkpoint, A2 replays from an
|
|
602
|
+
earlier one.
|
|
591
603
|
|
|
592
604
|
Credentials come from `AI_GATEWAY_API_KEY`, or Vercel's OIDC token on Vercel.
|
|
593
605
|
This entry point is server-only.
|
|
@@ -607,12 +619,18 @@ agent session from `@earendil-works/pi-coding-agent` (with
|
|
|
607
619
|
`@earendil-works/pi-ai`, both optional peer dependencies). Pi brings its system
|
|
608
620
|
prompt and its `read`, `write`, `edit`, and `bash` tools, which run in the
|
|
609
621
|
session's sandbox. `instructions` is added to Pi's prompt. The agent's `tools`
|
|
610
|
-
are added to Pi's; a tool with one of their names replaces Pi's.
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
622
|
+
are added to Pi's; a tool with one of their names replaces Pi's. Every call
|
|
623
|
+
first passes Pi's own checks: Pi coerces its arguments to the tool's JSON Schema
|
|
624
|
+
(the string `"5"` becomes `5` for a number) and rejects what that schema
|
|
625
|
+
forbids, such as a property it does not list. A call to one of the agent's tools
|
|
626
|
+
then goes through the tool's schema, as under `{ type: 'ai-sdk' }`: `execute`
|
|
627
|
+
receives the schema's output, defaults included. Input either check rejects goes
|
|
628
|
+
back to the model as an error, without running the tool. `reasoning` becomes
|
|
629
|
+
Pi's thinking level and `speed: 'fast'` asks AI Gateway for the model's fast
|
|
630
|
+
tier; `model` is one of the Gateway models Pi's catalog lists. Pi works in the sandbox's working directory; its `bash` runs with the
|
|
631
|
+
sandbox's environment, never the server's. Pi compacts its own context; a
|
|
632
|
+
compaction appears in the transcript as a `data-a2-compaction` part with its
|
|
633
|
+
summary.
|
|
616
634
|
|
|
617
635
|
- `sandbox` (`HarnessSandbox`, given a `HarnessSandboxContext`) resolves the
|
|
618
636
|
session's AI SDK sandbox session (`Experimental_SandboxSession` from `ai`).
|
|
@@ -104,8 +104,8 @@ mid-span.
|
|
|
104
104
|
| `a2.ai.ack.report_id` | `a2.ai.ack` | start | work report event id, matching the control span's id |
|
|
105
105
|
|
|
106
106
|
- Drain outcomes: `settled` means nothing actionable is left; `busy` means live
|
|
107
|
-
per-event claims remain; `stalled` means a
|
|
108
|
-
retry.
|
|
107
|
+
per-event claims or a failed event's retry hold remain; `stalled` means a
|
|
108
|
+
caught failure in this drain needs a later retry.
|
|
109
109
|
- A failing handler marks its `a2.event` span with the exception and error
|
|
110
110
|
status. Alert on `a2.event.outcome = dead_lettered`. A dead-lettered event
|
|
111
111
|
blocks only later events in its lane, until `session.redrive()` resolves it;
|
|
@@ -189,8 +189,8 @@ export const scheduler = vercelQueues()
|
|
|
189
189
|
Deploymentless delivery lets Vercel choose an eligible consumer, not the
|
|
190
190
|
latest production deployment; every eligible consumer must use
|
|
191
191
|
`deploymentId: null` and stay compatible with pending tasks and session work.
|
|
192
|
-
- Tasks are retained for seven days
|
|
193
|
-
|
|
192
|
+
- Tasks are retained for seven days, Vercel Queues' maximum. A delay goes out
|
|
193
|
+
as asked; Vercel Queues refuses one longer than the retention.
|
|
194
194
|
- `transport` and `clock` are advanced injection seams for tests and custom
|
|
195
195
|
delivery. An injected transport receives the topic, an exact
|
|
196
196
|
`SchedulerTask`, and send options, and owns its provider configuration;
|
|
@@ -294,8 +294,8 @@ export const POST = scheduler.handler(ordersServer, billingServer)
|
|
|
294
294
|
signatures return 401.
|
|
295
295
|
- A signed malformed envelope, or a declared or streamed body over
|
|
296
296
|
`maxBodyBytes`, returns 489 with `Upstash-NonRetryable-Error: true`.
|
|
297
|
-
- Returns 200 when settled; when busy, after a durable successor
|
|
298
|
-
exists
|
|
297
|
+
- Returns 200 when settled; when busy or stalled, after a durable successor
|
|
298
|
+
watchdog exists, so each caught failure gets a fresh message and a fresh
|
|
299
299
|
per-message retry budget. Append tasks return 200 after their fixed-id append
|
|
300
300
|
succeeds and any recovery watchdog for pending handler work is durable; clear
|
|
301
301
|
tasks after their conditional clear and its deletion finish.
|
|
@@ -192,7 +192,7 @@ records completion runs the handler again.
|
|
|
192
192
|
## When handlers throw
|
|
193
193
|
|
|
194
194
|
Throwing means "retry me." A2 records the failure and retries with backoff.
|
|
195
|
-
The failed event blocks only its lane. After ten
|
|
195
|
+
The failed event blocks only its lane. After ten failed attempts it
|
|
196
196
|
dead-letters and keeps blocking its lane until
|
|
197
197
|
[you retry or skip it](/server/production#3-when-an-event-dead-letters).
|
|
198
198
|
|
|
@@ -119,7 +119,7 @@ handler schedule the next one.
|
|
|
119
119
|
|
|
120
120
|
| Adapter | Maximum one-shot delay |
|
|
121
121
|
| --- | --- |
|
|
122
|
-
| Vercel Queues |
|
|
122
|
+
| Vercel Queues | 7 days |
|
|
123
123
|
| QStash free | 7 days |
|
|
124
124
|
| QStash usage-based | 1 year |
|
|
125
125
|
| QStash fixed | no fixed maximum |
|
|
@@ -70,8 +70,9 @@ what stops it racing its successor outside A2.
|
|
|
70
70
|
Pass `ctx.signal` to your I/O and let the abort propagate. That handles every
|
|
71
71
|
case:
|
|
72
72
|
|
|
73
|
-
- **A propagated lease abort** surrenders the attempt: no completion,
|
|
74
|
-
|
|
73
|
+
- **A propagated lease abort** surrenders the attempt: no completion, and
|
|
74
|
+
normal recovery. Like a crash, it costs the event one attempt of its
|
|
75
|
+
failure budget.
|
|
75
76
|
- **A user cancellation** (`abortOn`) is the one to catch and return normally
|
|
76
77
|
from. The stop is the outcome, so the completion counts.
|
|
77
78
|
- **Return normally after a lease abort** only when the work is done. A lapsed
|
|
@@ -143,8 +143,8 @@ The trigger is required. It connects the route to Vercel Queues and makes the
|
|
|
143
143
|
function private, so only queue infrastructure can invoke it. Don't expose the
|
|
144
144
|
callback as a public route or wrap it in a second authentication scheme.
|
|
145
145
|
|
|
146
|
-
The adapter keeps messages for 7 days (Vercel Queues' default is 24 hours)
|
|
147
|
-
|
|
146
|
+
The adapter keeps messages for 7 days (Vercel Queues' default is 24 hours), so
|
|
147
|
+
scheduled appends and clears can be up to 7 days out.
|
|
148
148
|
|
|
149
149
|
### QStash
|
|
150
150
|
|
|
@@ -241,7 +241,8 @@ import { ordersServer } from '@/server'
|
|
|
241
241
|
const order = ordersServer.session(orderId)
|
|
242
242
|
const [dead] = await order.deadLetters()
|
|
243
243
|
if (dead) {
|
|
244
|
-
// dead.lastError says why
|
|
244
|
+
// dead.lastError says why (null if every attempt timed out or crashed);
|
|
245
|
+
// deploy the handler fix first, then:
|
|
245
246
|
await order.redrive(dead.index, { action: 'retry' })
|
|
246
247
|
}
|
|
247
248
|
```
|
|
@@ -22,8 +22,9 @@ and the log decides what remains. Reads never dispatch handlers.
|
|
|
22
22
|
Each claim expires. If its holder dies, the claim lapses and the next wakeup
|
|
23
23
|
claims the event again with a larger `ctx.attempt`. The retry gets the same
|
|
24
24
|
event id, index, type, and payload. A kill after the claim can use up an
|
|
25
|
-
attempt before your code runs,
|
|
26
|
-
older attempt can never
|
|
25
|
+
attempt before your code runs, and that attempt counts toward the event's
|
|
26
|
+
[failure budget](#when-a-handler-keeps-failing). An older attempt can never
|
|
27
|
+
overwrite a newer one's completion or failure.
|
|
27
28
|
|
|
28
29
|
:::note[Recovery is not an event]
|
|
29
30
|
A2 does not append `recovered` or `continued`. Recovery retries the same event.
|
|
@@ -86,20 +87,28 @@ keeps its original cause.
|
|
|
86
87
|
## A chain that crosses the function timeout
|
|
87
88
|
|
|
88
89
|
A function killed mid-handler restarts that handler from its entry point in a
|
|
89
|
-
fresh invocation: same event, new attempt
|
|
90
|
-
processed
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
fresh invocation: same event, new attempt. Events already processed stay
|
|
91
|
+
processed, so a chain of handlers can outlast one invocation, but each handler
|
|
92
|
+
must fit in a fresh one: an attempt that runs out of time is a failed attempt,
|
|
93
|
+
and a handler that never fits dead-letters after ten. Split longer work into
|
|
94
|
+
smaller events or increase the function duration. On Vercel, A2 caps the claim window
|
|
93
95
|
at the function deadline when the platform exposes it, so a retry starts
|
|
94
|
-
promptly after a timeout.
|
|
96
|
+
promptly after a timeout. Once the deadline passes, A2 claims nothing more in
|
|
97
|
+
that invocation, so a timeout costs the event one attempt.
|
|
95
98
|
|
|
96
99
|
## When a handler keeps failing
|
|
97
100
|
|
|
98
101
|
A caught failure counts against that event, releases its claim, and leaves
|
|
99
|
-
other eligible events running. A2
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
102
|
+
other eligible events running. A2 holds it back for a delay that starts at
|
|
103
|
+
one second and doubles with each failed attempt up to 30 seconds, then retries
|
|
104
|
+
it on the session's next wake. With a scheduler, that's a few seconds at first,
|
|
105
|
+
then the delay itself. An attempt that ends before it finishes, because its
|
|
106
|
+
process timed out, crashed, or lost its claim, counts too, once the next
|
|
107
|
+
attempt takes its claim over. Ten failed attempts dead-letter the event: later
|
|
108
|
+
events in its lane stay blocked, while unlaned events and other lanes continue.
|
|
109
|
+
A crash costs every event it interrupts one attempt, so a session whose work
|
|
110
|
+
keeps outrunning its function dead-letters: bound its fan-out, or put work that
|
|
111
|
+
waits on the same resource in one lane. The event waits until you
|
|
103
112
|
[retry or skip it](/server/production#3-when-an-event-dead-letters) with
|
|
104
113
|
`session.redrive()`.
|
|
105
114
|
|
|
@@ -116,12 +116,19 @@ of a public deployment. A0's sandboxes are named `a2-a0-*`, which is where to
|
|
|
116
116
|
look when cleaning up. The sidebar lists the apps this browser opened, from a
|
|
117
117
|
cookie.
|
|
118
118
|
|
|
119
|
+
Files attached in A0's chat upload from the browser straight to a public
|
|
120
|
+
Vercel Blob store, through a token from `/a0/uploads`. Connect a Blob store to
|
|
121
|
+
the project, which sets `BLOB_READ_WRITE_TOKEN`; `vercel env pull` brings it
|
|
122
|
+
to local runs. Images go to the model with the message; other files go into
|
|
123
|
+
its text as links, which the agent downloads in its sandbox. Each lives at an
|
|
124
|
+
unguessable public URL under `a0/<app id>/`, which anyone with the URL can
|
|
125
|
+
read. Nothing deletes them: the conversation that stays in the log still links
|
|
126
|
+
them.
|
|
127
|
+
|
|
119
128
|
A0's dev tools have a Crash button, which ends the function instance running
|
|
120
129
|
an app's agent, mid-turn, so the scheduler resumes the turn in a fresh one.
|
|
121
|
-
Crashing ends everything else that instance is serving,
|
|
122
|
-
|
|
123
|
-
allow it anywhere else. Under `next dev` that takes the dev server down with
|
|
124
|
-
it, so it's for trying recovery, not for everyday local runs.
|
|
130
|
+
Crashing ends everything else that instance is serving, and under `next dev`
|
|
131
|
+
it takes the dev server down with it.
|
|
125
132
|
|
|
126
133
|
## 6. WebSockets for the canvas
|
|
127
134
|
|
|
@@ -4,7 +4,7 @@ import { AppView } from '../app-view'
|
|
|
4
4
|
import { isAppId } from '../apps'
|
|
5
5
|
import { DevTools } from '../devtools'
|
|
6
6
|
import { harnessFrom, logPage } from '../model'
|
|
7
|
-
import { agentServers
|
|
7
|
+
import { agentServers } from '../server'
|
|
8
8
|
import { SessionProvider } from '../session'
|
|
9
9
|
|
|
10
10
|
/** An app, from its current state; the provider follows its log from there. */
|
|
@@ -40,13 +40,7 @@ export default async function A0App({
|
|
|
40
40
|
<AppView
|
|
41
41
|
appId={appId}
|
|
42
42
|
harness={harness}
|
|
43
|
-
devtools={
|
|
44
|
-
<DevTools
|
|
45
|
-
appId={appId}
|
|
46
|
-
deployed={!!process.env.VERCEL}
|
|
47
|
-
crashesAllowed={crashesAllowed}
|
|
48
|
-
/>
|
|
49
|
-
}
|
|
43
|
+
devtools={<DevTools appId={appId} deployed={!!process.env.VERCEL} />}
|
|
50
44
|
/>
|
|
51
45
|
</SessionProvider>
|
|
52
46
|
)
|