experimental-a2 0.13.0 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/dist/{actor-DJi3RsNu.d.ts → actor-BfQSE0KC.d.ts} +4 -4
- package/dist/{actor-DJi3RsNu.d.ts.map → actor-BfQSE0KC.d.ts.map} +1 -1
- package/dist/actor-client.d.ts +1 -1
- package/dist/actor-client.js +1 -1
- package/dist/actor-react.d.ts +3 -3
- package/dist/actor-react.js +2 -2
- package/dist/{actor-shared-DI7J5upy.js → actor-shared-B5tJfzt-.js} +2 -2
- package/dist/{actor-shared-DI7J5upy.js.map → actor-shared-B5tJfzt-.js.map} +1 -1
- package/dist/actor.d.ts +1 -1
- package/dist/actor.js +3 -3
- package/dist/ai-Cai-lCbj.d.ts +580 -0
- package/dist/ai-Cai-lCbj.d.ts.map +1 -0
- package/dist/ai-control-CcD4hh3y.js +119 -0
- package/dist/ai-control-CcD4hh3y.js.map +1 -0
- package/dist/ai-server.d.ts +6 -6
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +1053 -513
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +2 -365
- package/dist/ai.js +801 -80
- package/dist/ai.js.map +1 -1
- package/dist/{client-P_NNNRM-.d.ts → client-BAEABRZB.d.ts} +2 -2
- package/dist/{client-P_NNNRM-.d.ts.map → client-BAEABRZB.d.ts.map} +1 -1
- package/dist/{client-Bf6uSEAk.js → client-BYzHjkwU.js} +21 -6
- package/dist/client-BYzHjkwU.js.map +1 -0
- package/dist/client.d.ts +1 -1
- package/dist/client.js +1 -1
- package/dist/{contract-48bUMgcL.js → contract-CKRg_E4q.js} +3 -26
- package/dist/contract-CKRg_E4q.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/react.d.ts +2 -2
- package/dist/react.js +1 -1
- package/dist/{reducer-DJKWm3cp.d.ts → reducer-BcS9VDKC.d.ts} +4 -1
- package/dist/{reducer-DJKWm3cp.d.ts.map → reducer-BcS9VDKC.d.ts.map} +1 -1
- package/dist/reducer-DEMjEY_O.js +29 -0
- package/dist/reducer-DEMjEY_O.js.map +1 -0
- package/dist/scheduler-qstash.d.ts +2 -2
- package/dist/scheduler-qstash.js +1 -1
- package/dist/scheduler-vercel.d.ts +2 -2
- package/dist/scheduler-vercel.js +1 -1
- package/dist/{server-DjZZa1wr.d.ts → server-Bp5Nd1pF.d.ts} +3 -3
- package/dist/{server-DjZZa1wr.d.ts.map → server-Bp5Nd1pF.d.ts.map} +1 -1
- package/dist/{server-BeNADlCI.js → server-CjJSGcF7.js} +4 -3
- package/dist/server-CjJSGcF7.js.map +1 -0
- package/dist/server.d.ts +3 -3
- package/dist/server.js +1 -1
- package/dist/{store-DtDOWLSn.d.ts → store-D_yhNdPz.d.ts} +7 -2
- package/dist/{store-DtDOWLSn.d.ts.map → store-D_yhNdPz.d.ts.map} +1 -1
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-postgres.d.ts +1 -1
- package/dist/store-postgres.js +19 -0
- package/dist/store-postgres.js.map +1 -1
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +1 -1
- package/dist/{store-redis-notify-BUCyXOn0.js → store-redis-notify-D2EI6gwX.js} +27 -2
- package/dist/store-redis-notify-D2EI6gwX.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/docs/guides/06-ai-agents.mdx +287 -65
- package/docs/reference/01-api.mdx +122 -27
- package/examples/playground/app/agent/[agentId]/agent-client.tsx +118 -21
- package/examples/playground/app/agent/[agentId]/agent-queue.tsx +289 -0
- package/examples/playground/app/agent/compaction-settings.test.ts +22 -6
- package/examples/playground/app/agent/model.ts +11 -2
- package/examples/playground/app/agent/server.ts +8 -2
- package/examples/playground/app/chat/[chatId]/chat-client.tsx +3 -13
- package/examples/playground/app/chat/model.ts +2 -2
- package/examples/playground/app/chat/server.ts +24 -17
- package/examples/playground/app/globals.css +179 -0
- package/examples/playground/package.json +1 -1
- package/package.json +1 -1
- package/src/ai-client-state.ts +185 -0
- package/src/ai-control-server.ts +829 -0
- package/src/ai-control-state.ts +152 -0
- package/src/ai-control.ts +139 -0
- package/src/ai-coordinator.ts +99 -32
- package/src/ai-progress-batches.ts +68 -0
- package/src/ai-projector.ts +76 -15
- package/src/ai-sdk-step.ts +0 -1
- package/src/ai-server.ts +429 -619
- package/src/ai.ts +553 -108
- package/src/client.ts +31 -9
- package/src/licenses/Apache-2.0.txt +55 -0
- package/src/parse-partial-json.ts +441 -0
- package/src/reducer.ts +6 -0
- package/src/server.ts +8 -4
- package/src/store-postgres.ts +27 -0
- package/src/store-redis-core.ts +53 -1
- package/src/store-redis-notify.ts +1 -0
- package/src/store.ts +6 -0
- package/dist/ai.d.ts.map +0 -1
- package/dist/client-Bf6uSEAk.js.map +0 -1
- package/dist/contract-48bUMgcL.js.map +0 -1
- package/dist/server-BeNADlCI.js.map +0 -1
- package/dist/store-redis-notify-BUCyXOn0.js.map +0 -1
|
@@ -67,11 +67,30 @@ and continuation between steps. Responses have no step limit by default. Set
|
|
|
67
67
|
`maxSteps` to a positive integer to limit one complete assistant response.
|
|
68
68
|
Individual providers decide which model settings they support. `generation`
|
|
69
69
|
therefore excludes `stopWhen`, tool execution callbacks, tool callers, sandbox
|
|
70
|
-
execution, and the tool approval secret.
|
|
71
|
-
`toolsContext`,
|
|
72
|
-
durable execution context later, from the handler that runs it. Model-step
|
|
70
|
+
execution, and the tool approval secret. A2 preserves all other supplied
|
|
71
|
+
settings, including `toolsContext`, when calling the AI SDK. A local tool
|
|
72
|
+
reads its durable execution context later, from the handler that runs it. Model-step
|
|
73
73
|
timeouts are supported; tool-execution timeouts are not.
|
|
74
74
|
|
|
75
|
+
For AI Gateway models, the default generator sets
|
|
76
|
+
`generation.providerOptions.gateway.caching` to `'auto'`. A2 merges this
|
|
77
|
+
default with your provider options, with caller-supplied values taking
|
|
78
|
+
precedence. Other Gateway options and provider namespaces are preserved.
|
|
79
|
+
The same default applies to automatic compaction. A custom `generate`
|
|
80
|
+
function receives your original settings and owns its caching policy.
|
|
81
|
+
|
|
82
|
+
Gateway calls also send an `x-session-affinity` header derived from a SHA-256
|
|
83
|
+
hash of the agent name and session ID. The value stays stable across turns,
|
|
84
|
+
retries, worker restarts, and automatic compaction. No raw session identifier
|
|
85
|
+
is sent in this header. A caller-supplied `generation.headers` entry takes
|
|
86
|
+
precedence, regardless of header casing; an explicit `undefined` suppresses
|
|
87
|
+
A2's default header. Set overrides here, since A2's per-call default can
|
|
88
|
+
override headers configured on the model itself.
|
|
89
|
+
Affinity helps supporting providers reuse their cache; it does not select
|
|
90
|
+
Gateway's provider or guarantee a cache hit.
|
|
91
|
+
See [Gateway automatic caching](https://vercel.com/docs/ai-gateway/models-and-providers/automatic-caching)
|
|
92
|
+
for provider support and cache-write costs.
|
|
93
|
+
|
|
75
94
|
### Store assistant metadata
|
|
76
95
|
|
|
77
96
|
Extend the agent's `UIMessage` type to keep generation status and usage on the
|
|
@@ -135,10 +154,10 @@ export const GET = assistantServer.fetch
|
|
|
135
154
|
export const POST = assistantServer.fetch
|
|
136
155
|
```
|
|
137
156
|
|
|
138
|
-
The route never calls the model directly. The browser appends
|
|
139
|
-
|
|
140
|
-
`ai.generation.requested
|
|
141
|
-
log.
|
|
157
|
+
The route never calls the model directly. The browser appends
|
|
158
|
+
`ai.control.requested` commands. The controller admits inputs and schedules
|
|
159
|
+
`ai.generation.requested`; workers run the AI SDK and append progress to the
|
|
160
|
+
same log.
|
|
142
161
|
|
|
143
162
|
## Bind the reducer to React
|
|
144
163
|
|
|
@@ -309,11 +328,9 @@ export function AgentClient() {
|
|
|
309
328
|
}
|
|
310
329
|
```
|
|
311
330
|
|
|
312
|
-
|
|
313
|
-
`
|
|
314
|
-
|
|
315
|
-
event and the component restores the draft. The server owns the corresponding
|
|
316
|
-
generation request.
|
|
331
|
+
`push()` persists the command. Its receipt confirms admission to the inbox.
|
|
332
|
+
`state.inbox.items` contains pending inputs; `state.messages` contains the
|
|
333
|
+
admitted conversation. If persistence fails, the component restores the draft.
|
|
317
334
|
|
|
318
335
|
The form gives Enter its normal submit behavior. The thinking row is also
|
|
319
336
|
derived from `AIState`: it appears with the Agent label when the server's
|
|
@@ -323,7 +340,7 @@ it. Empty stream-start messages never create a blank conversation row.
|
|
|
323
340
|
Users can send another message while a response is active. The message enters
|
|
324
341
|
the durable log immediately, but its turn waits until the active assistant
|
|
325
342
|
response, including every tool step and approval, reaches a terminal event.
|
|
326
|
-
|
|
343
|
+
Inbox order defaults to arrival order and can be edited explicitly.
|
|
327
344
|
|
|
328
345
|
Pass `{ generate: false }` when a user message should update the conversation
|
|
329
346
|
without starting a model turn:
|
|
@@ -343,7 +360,7 @@ export async function recordPassiveMessage(
|
|
|
343
360
|
}
|
|
344
361
|
```
|
|
345
362
|
|
|
346
|
-
The passive user message
|
|
363
|
+
The passive user message appears in `AIState.messages` when admitted. A later user
|
|
347
364
|
message with the default generation behavior includes passive user messages
|
|
348
365
|
before it in model context. Passive user messages after that trigger remain
|
|
349
366
|
outside its prompt and wait for the next generating message. This ordering
|
|
@@ -355,7 +372,10 @@ already queued turn.
|
|
|
355
372
|
One generating user interaction becomes a durable sequence:
|
|
356
373
|
|
|
357
374
|
```text
|
|
358
|
-
browser ai.
|
|
375
|
+
browser ai.control.requested durable input command
|
|
376
|
+
server ai.message.created input admitted
|
|
377
|
+
server ai.control.committed accepted state changes
|
|
378
|
+
server ai.control.decided command receipt
|
|
359
379
|
server ai.generation.requested scheduled from durable facts
|
|
360
380
|
server ai.generation.started
|
|
361
381
|
server ai.generation.progress batched UIMessageChunk[]
|
|
@@ -414,15 +434,26 @@ continuation that has not been appended yet.
|
|
|
414
434
|
|
|
415
435
|
| Input | Events |
|
|
416
436
|
| --- | --- |
|
|
417
|
-
| `inputs.message(message, { generate? })` |
|
|
437
|
+
| `inputs.message(message, { generate? })` | queues input; `false` queues passive context |
|
|
418
438
|
| `inputs.seed(message)` | records `generate: false`; non-user roles require a trusted append |
|
|
419
|
-
| `inputs.approval(response)` |
|
|
439
|
+
| `inputs.approval(response)` | requests a decision on a pending approval |
|
|
420
440
|
| `inputs.input(response)` | records an application input response fact |
|
|
421
441
|
| `inputs.requestInput(request)` | records a trusted server request for application input |
|
|
422
|
-
| `inputs.retry(options)` |
|
|
423
|
-
| `inputs.
|
|
424
|
-
|
|
425
|
-
|
|
442
|
+
| `inputs.retry(options)` | requests a fresh attempt for a failed response |
|
|
443
|
+
| `inputs.stop({ turnId })` | ends the observed turn and advances |
|
|
444
|
+
| `inputs.steer({ turnId, message })` | prioritizes input after the observed turn's current step finishes |
|
|
445
|
+
| `inputs.queue.edit({ message, expectedRevision })` | edits a pending input |
|
|
446
|
+
| `inputs.queue.move({ inputId, beforeId })` | reorders pending input; `null` means the end |
|
|
447
|
+
| `inputs.queue.remove({ inputId })` | removes pending input from the inbox |
|
|
448
|
+
| `inputs.queue.sendNow({ inputId, turnId })` | atomically prioritizes queued input and interrupts the observed turn |
|
|
449
|
+
| `inputs.pause({ when })` | pauses now or after the active turn |
|
|
450
|
+
| `inputs.resume()` | resumes retained work and inbox admission |
|
|
451
|
+
| `inputs.toolResult(result)` | records a trusted deferred provider-tool result |
|
|
452
|
+
| `inputs.interrupt(options)` | addresses a specific generation or request |
|
|
453
|
+
|
|
454
|
+
Builders return `ai.control.requested` commands. Message, approval, input, and
|
|
455
|
+
retry builders identify a stable interaction. Other controls use the event ID
|
|
456
|
+
assigned by `push()` or `append()`; transport retries preserve that ID.
|
|
426
457
|
Generation scheduling considers only user-role `ai.message.created` facts.
|
|
427
458
|
Assistant and system message facts remain context regardless of the `generate`
|
|
428
459
|
field, and model output is recorded as generation progress rather than a new
|
|
@@ -482,10 +513,9 @@ The AI SDK infers the tool input and output from its schemas and
|
|
|
482
513
|
scope carrying the current durable handler attempt. Read it with
|
|
483
514
|
`handlerContext(agent)` from `experimental-a2/ai`: it returns the same
|
|
484
515
|
typed context bag an event handler receives, with `event`, `attempt`,
|
|
485
|
-
`session`, and `signal`.
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
event. The agent argument carries the types; A2 verifies it against the
|
|
516
|
+
`session`, and `signal`. Tools execute an `ai.tool.execution.requested`
|
|
517
|
+
event after authorization. Its payload contains the admitted `call`, its
|
|
518
|
+
`generation`, and its turn and execution version. The agent argument carries the types; A2 verifies it against the
|
|
489
519
|
server executing the tool and throws when a tool written for one agent
|
|
490
520
|
runs under another, or when `handlerContext()` is called outside a tool
|
|
491
521
|
execution. The scope survives awaited helpers and async iteration, so
|
|
@@ -547,7 +577,7 @@ and plan.
|
|
|
547
577
|
The schedule name uses `toolCallId`, and the message id uses the durable
|
|
548
578
|
triggering event id. A retry therefore converges on the same timer and message,
|
|
549
579
|
even if a provider reuses tool-call ids in a later generation. When the timer
|
|
550
|
-
arrives, `inputs.message()`
|
|
580
|
+
arrives, `inputs.message()` queues a durable input command. The
|
|
551
581
|
queued-turn policy starts a fresh response after the active response completes
|
|
552
582
|
or is interrupted. A failed response must be retried or interrupted first.
|
|
553
583
|
If the scheduler send fails ambiguously or transiently, A2 retries the same
|
|
@@ -664,40 +694,15 @@ import { useSession } from '../session'
|
|
|
664
694
|
|
|
665
695
|
export function StopButton() {
|
|
666
696
|
const { state, push, index } = useSession()
|
|
667
|
-
const active = state.
|
|
668
|
-
|
|
669
|
-
state.activeRequestId && state.activeResponseMessageId
|
|
670
|
-
? {
|
|
671
|
-
messageId: state.activeResponseMessageId,
|
|
672
|
-
requestId: state.activeRequestId,
|
|
673
|
-
}
|
|
674
|
-
: null
|
|
675
|
-
const waiting =
|
|
676
|
-
state.pendingApprovals[0] ??
|
|
677
|
-
state.pendingInputs[0] ??
|
|
678
|
-
state.tools.find((tool) => tool.status === 'running')
|
|
679
|
-
const target = active
|
|
680
|
-
? {
|
|
681
|
-
messageId: active.responseMessageId,
|
|
682
|
-
generationId: active.generationId,
|
|
683
|
-
}
|
|
684
|
-
: requested ??
|
|
685
|
-
(waiting
|
|
686
|
-
? {
|
|
687
|
-
messageId: waiting.messageId,
|
|
688
|
-
generationId: waiting.generationId,
|
|
689
|
-
}
|
|
690
|
-
: null)
|
|
691
|
-
|
|
692
|
-
if (!target) return null
|
|
697
|
+
const active = state.active
|
|
698
|
+
if (!active) return null
|
|
693
699
|
|
|
694
700
|
return (
|
|
695
701
|
<button
|
|
696
702
|
onClick={() =>
|
|
697
703
|
void push(
|
|
698
|
-
...inputs.
|
|
699
|
-
|
|
700
|
-
reason: 'Stopped by the user',
|
|
704
|
+
...inputs.stop({
|
|
705
|
+
turnId: active.turnId,
|
|
701
706
|
lastSeenIndex: index,
|
|
702
707
|
}),
|
|
703
708
|
)
|
|
@@ -709,17 +714,20 @@ export function StopButton() {
|
|
|
709
714
|
}
|
|
710
715
|
```
|
|
711
716
|
|
|
712
|
-
The
|
|
713
|
-
channel. `lastSeenIndex` is the exact confirmed log frontier visible when the
|
|
717
|
+
The accepted command ends the turn and reaches A2's cancellation channel. `lastSeenIndex` is the exact confirmed log frontier visible when the
|
|
714
718
|
user clicked. The reducer rewinds the indexed generation projection to that
|
|
715
|
-
frontier
|
|
716
|
-
|
|
719
|
+
frontier and keeps the partial text the user actually saw. Tool parts whose
|
|
720
|
+
arguments are still streaming are drafts: interruption removes them from the
|
|
721
|
+
conversation and subsequent model prompts, even if their current JSON parses.
|
|
722
|
+
Their progress remains in the raw log. Accepted tools still awaiting completion
|
|
723
|
+
become `output-error`. An accepted interruption
|
|
717
724
|
terminally fences that request or generation. Later generation, tool, approval,
|
|
718
725
|
input, and compaction events remain in raw history but cannot reactivate it or
|
|
719
726
|
alter the projection.
|
|
720
727
|
Completed tool results at or before the visible frontier remain completed.
|
|
721
728
|
|
|
722
|
-
|
|
729
|
+
For the lower-level `inputs.interrupt()` builder, between
|
|
730
|
+
`ai.generation.requested` and `ai.generation.started`, copy
|
|
723
731
|
`activeRequestId` as `requestId` and `activeResponseMessageId` as `messageId`.
|
|
724
732
|
Once `activeGeneration` exists, send its `generationId` and
|
|
725
733
|
`responseMessageId` instead. A request-owned interruption remains valid if
|
|
@@ -800,10 +808,11 @@ metadata status are available in `state.modelMetadata`.
|
|
|
800
808
|
positive safe integer. `compaction: { instructions: 'Preserve exact IDs.' }`
|
|
801
809
|
adds guidance to the built-in summary prompt while keeping the derived threshold.
|
|
802
810
|
`compaction: false` disables both compaction and metadata lookup. The `compaction`
|
|
803
|
-
option can also be a
|
|
804
|
-
`{ event, state,
|
|
805
|
-
returns `false`, automatic options, or a custom policy once per generation
|
|
806
|
-
|
|
811
|
+
option can also be a resolver receiving the same
|
|
812
|
+
`{ event, state, session, signal }` context as `model` and `instructions`. It
|
|
813
|
+
returns `false`, automatic options, or a custom policy once per generation and
|
|
814
|
+
can await `session.state(settingsReducer)` to read application settings.
|
|
815
|
+
Use this to select a threshold from the checkpointed AI state.
|
|
807
816
|
Changes after resolution apply to a later model step. Disabling compaction
|
|
808
817
|
preserves summaries that were already recorded. Custom provider
|
|
809
818
|
models and custom global SDK providers need an explicit threshold. A custom
|
|
@@ -959,7 +968,7 @@ export const customGenerationServer = createAgentServer({
|
|
|
959
968
|
```
|
|
960
969
|
|
|
961
970
|
`generate` receives compacted messages, resolved model and instructions, tools,
|
|
962
|
-
generation settings, durable request identity, current state
|
|
971
|
+
generation settings, durable request identity, current state, and
|
|
963
972
|
the abort signal. It returns exactly one model step as a
|
|
964
973
|
`ReadableStream<UIMessageChunk>`. A tool-aware replacement sends definitions to
|
|
965
974
|
the model without running local `execute` functions. A2 still owns durable
|
|
@@ -968,6 +977,33 @@ progress, tool execution, approval, continuation, interruption, and failure.
|
|
|
968
977
|
function owns its metadata and can pass a callback to `toUIMessageStream()` or
|
|
969
978
|
emit typed metadata chunks itself.
|
|
970
979
|
|
|
980
|
+
## Checkpoints and recovery reads
|
|
981
|
+
|
|
982
|
+
Model steps read the AI reducer and coordinator at a fixed log boundary. Model,
|
|
983
|
+
instruction, compaction, and custom-generation callbacks receive derived state.
|
|
984
|
+
They do not receive a raw history array. Their `session.state(reducer)` reads
|
|
985
|
+
application reducers at the captured prompt boundary by default; explicit
|
|
986
|
+
`through` options follow the ordinary state-read contract. Compaction selection
|
|
987
|
+
can be asynchronous when it needs a settings-state read.
|
|
988
|
+
|
|
989
|
+
Ordinary generation uses checkpointed state. A recovered attempt reads from its
|
|
990
|
+
saved prompt boundary to the captured current boundary, excluding abandoned
|
|
991
|
+
attempts. Tools recovering in a fresh process reconstruct their original prompt
|
|
992
|
+
from that checkpoint and the owning generation's start and compaction events.
|
|
993
|
+
Compacted context uses its checkpoint and subsequent events. These AI history
|
|
994
|
+
reads always have explicit lower and upper bounds. A2 core can rebuild a missing
|
|
995
|
+
or invalid reducer snapshot from the log.
|
|
996
|
+
|
|
997
|
+
Streaming progress updates the current message incrementally. Snapshots preserve
|
|
998
|
+
open text and reasoning parts and partial tool inputs. Indexed progress remains
|
|
999
|
+
available for exact interruption cutoffs; reordered events rebuild the projection.
|
|
1000
|
+
|
|
1001
|
+
While tool arguments arrive, `input-streaming` parts expose a best-effort partial
|
|
1002
|
+
JSON value in `input`. A command string can appear and grow before its closing
|
|
1003
|
+
quote arrives. This is a display draft, not validated execution input.
|
|
1004
|
+
`tool-input-available` supplies the completed input. Interruption still discards
|
|
1005
|
+
streaming drafts even when their partial JSON is readable.
|
|
1006
|
+
|
|
971
1007
|
## Delivery semantics
|
|
972
1008
|
|
|
973
1009
|
Model steps and tools run inside at-least-once A2 handlers. The returned-event
|
|
@@ -993,3 +1029,189 @@ continuation, but it cannot roll back that external effect.
|
|
|
993
1029
|
Configure a [production scheduler](/guides/production) exactly as for any other A2
|
|
994
1030
|
server. It recovers an incomplete generation attempt after the original
|
|
995
1031
|
serverless invocation disappears.
|
|
1032
|
+
|
|
1033
|
+
## Inbox and turn controls
|
|
1034
|
+
|
|
1035
|
+
This input protocol requires matching client and server versions. Start a new
|
|
1036
|
+
agent session when adopting it. Existing event logs remain readable; finish
|
|
1037
|
+
pending work with the runtime that created it before upgrading.
|
|
1038
|
+
|
|
1039
|
+
`inputs.message(message)` places a message in the durable inbox. The controller
|
|
1040
|
+
admits one input at a time, freezes its revision, and adds it to `state.messages`.
|
|
1041
|
+
Pending inputs appear in `state.inbox.items`; they do not enter the model prompt.
|
|
1042
|
+
Each item has an `id`, `revision`, `message`, and `generate` flag.
|
|
1043
|
+
|
|
1044
|
+
`state.active` identifies the logical turn, its frozen `input`, and its `phase`.
|
|
1045
|
+
The turn ID stays the same across model steps, tools, and a pause. Stop ends that
|
|
1046
|
+
turn and admits the next queued input when admission is running.
|
|
1047
|
+
|
|
1048
|
+
```ts client/turn-controls.ts
|
|
1049
|
+
import { inputs, type AIState } from 'experimental-a2/ai'
|
|
1050
|
+
import type { UIMessage } from 'ai'
|
|
1051
|
+
|
|
1052
|
+
export function editPending(message: UIMessage, expectedRevision: number) {
|
|
1053
|
+
return inputs.queue.edit({ message, expectedRevision })
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
export function movePending(inputId: string, beforeId: string | null) {
|
|
1057
|
+
return inputs.queue.move({ inputId, beforeId })
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
export function removePending(inputId: string) {
|
|
1061
|
+
return inputs.queue.remove({ inputId })
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
export function sendPendingNow(state: AIState, inputId: string) {
|
|
1065
|
+
return inputs.queue.sendNow({ inputId, turnId: state.active?.turnId ?? null })
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
export function stopCurrent(state: AIState) {
|
|
1069
|
+
return state.active ? inputs.stop({ turnId: state.active.turnId }) : []
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
export function steerCurrent(state: AIState, message: UIMessage) {
|
|
1073
|
+
return state.active
|
|
1074
|
+
? inputs.steer({ turnId: state.active.turnId, message })
|
|
1075
|
+
: inputs.message(message)
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
export const pauseAfterTurn = () => inputs.pause({ when: 'after-turn' })
|
|
1079
|
+
export const pauseNow = () => inputs.pause({ when: 'now' })
|
|
1080
|
+
export const resume = () => inputs.resume()
|
|
1081
|
+
```
|
|
1082
|
+
|
|
1083
|
+
Pass these arrays to `push(...events)` from `useSession()`, or to a server
|
|
1084
|
+
session's `append(...events)`. `beforeId: null` moves an item to the end.
|
|
1085
|
+
Removal only applies to pending input. It preserves the immutable log and keeps
|
|
1086
|
+
the input identity reserved, so replay cannot recreate a removed prompt.
|
|
1087
|
+
Removing an admitted input returns `already-active`; removing an absent or
|
|
1088
|
+
already removed input returns `not-found`.
|
|
1089
|
+
|
|
1090
|
+
An edit that loses to admission returns `already-active`. Concurrent edits use
|
|
1091
|
+
`expectedRevision`; an outdated revision returns `revision-conflict`. A Stop
|
|
1092
|
+
aimed at a completed turn returns `stale-turn`.
|
|
1093
|
+
|
|
1094
|
+
Steer validates the new input and places it ahead of the inbox in one decision.
|
|
1095
|
+
If the observed turn is still active, it finishes its current model response and
|
|
1096
|
+
the tools produced by that response before handing off. Parallel tools all
|
|
1097
|
+
settle, and steering runs before the old turn starts another model step.
|
|
1098
|
+
Text and tool results produced after the click remain in the conversation.
|
|
1099
|
+
`lastSeenIndex` is not used to truncate output for steering or Send now.
|
|
1100
|
+
|
|
1101
|
+
When running work settles, unanswered approval or application-input requests
|
|
1102
|
+
are closed so the steering message can take over. The unapproved tool never
|
|
1103
|
+
executes. Provider-executed deferred results still count as running work when
|
|
1104
|
+
execution is authorized; a denied approval cannot block handoff.
|
|
1105
|
+
If the targeted turn has already finished, the input starts when idle or waits
|
|
1106
|
+
first behind a different active turn. Stop remains an immediate interruption.
|
|
1107
|
+
|
|
1108
|
+
Steering preserves the admission gate: steering while paused does not resume
|
|
1109
|
+
execution. Duplicate input identities and closed sessions still reject the command.
|
|
1110
|
+
|
|
1111
|
+
Send now uses the selected queued message's existing identity and latest
|
|
1112
|
+
accepted revision. It validates the selected input and the observed `turnId`,
|
|
1113
|
+
then moves the input first for the same handoff after the current step. Use
|
|
1114
|
+
`turnId: null` when no turn is active. If that expectation is stale, the command
|
|
1115
|
+
returns `stale-turn` without changing the queue.
|
|
1116
|
+
|
|
1117
|
+
Send now preserves pause just like steering. A paused queue keeps the selected
|
|
1118
|
+
input first until Resume. It accepts user-role inputs, including passive user
|
|
1119
|
+
input, which it marks for generation. Non-user context returns `not-user-input`;
|
|
1120
|
+
a conflicting response identity returns `duplicate-input`. Unsaved editor text
|
|
1121
|
+
is separate from the accepted queue revision; save it before sending.
|
|
1122
|
+
|
|
1123
|
+
A queued item's optional `afterStepOf` records the observed turn for steering.
|
|
1124
|
+
A matching active turn hands off after its current step.
|
|
1125
|
+
Editing preserves this intent; removing the item cancels it. Reordering the
|
|
1126
|
+
queue controls which input is admitted at that boundary.
|
|
1127
|
+
|
|
1128
|
+
### Optimistic conversation display
|
|
1129
|
+
|
|
1130
|
+
With `agent().reducer` or `createReducer()`, the client state is already optimistic.
|
|
1131
|
+
An idle, unpaused send appears in `state.messages` immediately, with
|
|
1132
|
+
`state.starting: true`. Another send waits in `state.inbox.items` while that
|
|
1133
|
+
optimistic turn or a confirmed turn is active. Paused sessions keep new sends in
|
|
1134
|
+
the inbox. Render the conversation and queue from `state`; `state.active` and
|
|
1135
|
+
execution controls remain server-confirmed.
|
|
1136
|
+
Steering appears immediately in `state.messages`, after the current response,
|
|
1137
|
+
and stays there while that step finishes. Send now has the same display when
|
|
1138
|
+
an active turn is targeted. These inputs are omitted from the client inbox,
|
|
1139
|
+
including while paused, but remain pending on the server until admission.
|
|
1140
|
+
Accepted steering survives receipt and hydration in this display. Multiple
|
|
1141
|
+
steering inputs appear in admission order, with the most recent first.
|
|
1142
|
+
No additional request, subscription, or component-owned queue is needed.
|
|
1143
|
+
|
|
1144
|
+
```tsx app/agent/[sessionId]/conversation.tsx
|
|
1145
|
+
'use client'
|
|
1146
|
+
import { useSession } from '../session'
|
|
1147
|
+
|
|
1148
|
+
export function Conversation() {
|
|
1149
|
+
const { state } = useSession()
|
|
1150
|
+
const { messages, inbox, starting } = state
|
|
1151
|
+
const text = (message: (typeof messages)[number]) =>
|
|
1152
|
+
message.parts.filter((part) => part.type === 'text').map((part) => part.text).join(' ')
|
|
1153
|
+
return (
|
|
1154
|
+
<section>
|
|
1155
|
+
<div aria-label="Conversation">
|
|
1156
|
+
{messages.map((message) => <p key={message.id}>{text(message)}</p>)}
|
|
1157
|
+
{starting ? <p role="status">Starting…</p> : null}
|
|
1158
|
+
</div>
|
|
1159
|
+
<ol aria-label="Queued messages">
|
|
1160
|
+
{inbox.items.map((item) => <li key={item.id}>{text(item.message)}</li>)}
|
|
1161
|
+
</ol>
|
|
1162
|
+
</section>
|
|
1163
|
+
)
|
|
1164
|
+
}
|
|
1165
|
+
```
|
|
1166
|
+
|
|
1167
|
+
The request echo keeps the optimistic display until `ai.control.decided` arrives. Accepted
|
|
1168
|
+
commits replace the confirmed base; rejected receipts remove the pending intent.
|
|
1169
|
+
A failed POST rolls back through the ordinary client optimistic overlay.
|
|
1170
|
+
Another client's accepted turn can move a speculative ordinary send back into
|
|
1171
|
+
the queue. Steering stays visible as a message while waiting behind that turn. `state.starting` describes an optimistic turn awaiting acceptance; it does
|
|
1172
|
+
not indicate a running model.
|
|
1173
|
+
|
|
1174
|
+
Server `session.state(reducer)`, reducer folds, checkpoints, and model prompts
|
|
1175
|
+
contain confirmed messages and inbox entries, with `starting: false`. The client
|
|
1176
|
+
applies pending intent and displays accepted steering only when returning its
|
|
1177
|
+
state, after folding events.
|
|
1178
|
+
A custom reducer that calls `reduceAIState()` or nests `agent.reducer.fold()`
|
|
1179
|
+
uses that confirmed fold; it does not inherit the built-in reducer's client display.
|
|
1180
|
+
`state.pendingQueueCommands` tracks unresolved queue commands incrementally, so
|
|
1181
|
+
the client does not scan raw history. Revisions remain server-confirmed; keep
|
|
1182
|
+
repeat editing and Send now disabled while an edit for that item awaits its
|
|
1183
|
+
receipt. Moving and removing an item do not require a revision.
|
|
1184
|
+
|
|
1185
|
+
### Persistence and application are separate
|
|
1186
|
+
|
|
1187
|
+
`push()` confirms that the command is persisted. The `ai.control.decided` event
|
|
1188
|
+
confirms its application, using the submitted event's ID as `commandId`. Its
|
|
1189
|
+
`outcome` is `applied` or `rejected`; a rejection includes a `reason`.
|
|
1190
|
+
`state.receipt` exposes the most recent user command receipt. Applications that
|
|
1191
|
+
have several commands in flight correlate receipts through the existing event
|
|
1192
|
+
stream. A rejection is a normal result and does not spend a handler retry.
|
|
1193
|
+
|
|
1194
|
+
### Pause and resume
|
|
1195
|
+
|
|
1196
|
+
Pause after the turn closes inbox admission and lets the active turn finish.
|
|
1197
|
+
Pause now also cancels current model and tool execution. `state.active.phase`
|
|
1198
|
+
becomes `pausing` while old work settles, then `paused`. Resume reopens admission
|
|
1199
|
+
and resumes retained work before admitting another input.
|
|
1200
|
+
|
|
1201
|
+
Resume starts a fresh model request from durable state. It does not resume the
|
|
1202
|
+
provider's old stream or undo completed tool effects. The controller retains
|
|
1203
|
+
accepted tool calls and records their outcomes while paused. A cancelled tool
|
|
1204
|
+
without a terminal result can execute again after resume; use the stable SDK
|
|
1205
|
+
`toolCallId` for external idempotency. No model or tool claim is held merely to
|
|
1206
|
+
keep a turn paused.
|
|
1207
|
+
|
|
1208
|
+
The controller runs on a short lane. Each decision commits accepted facts,
|
|
1209
|
+
compact state changes, work requests, and its receipt atomically. Workers use
|
|
1210
|
+
separate claims, report lifecycle batches, and wait for acceptance at model-start
|
|
1211
|
+
and compaction boundaries. Token progress appends directly. Explicit AI history
|
|
1212
|
+
reads remain bounded to a relevant generation suffix; core checkpoint rebuilding
|
|
1213
|
+
can replay older events when a snapshot is missing.
|
|
1214
|
+
|
|
1215
|
+
For a deferred provider-executed tool, trusted server code submits
|
|
1216
|
+
`inputs.toolResult(result)`. The controller checks its admitted call and records
|
|
1217
|
+
the result before continuing. Browser ingress rejects this server-only input.
|