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.
Files changed (99) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/{actor-DJi3RsNu.d.ts → actor-BfQSE0KC.d.ts} +4 -4
  3. package/dist/{actor-DJi3RsNu.d.ts.map → actor-BfQSE0KC.d.ts.map} +1 -1
  4. package/dist/actor-client.d.ts +1 -1
  5. package/dist/actor-client.js +1 -1
  6. package/dist/actor-react.d.ts +3 -3
  7. package/dist/actor-react.js +2 -2
  8. package/dist/{actor-shared-DI7J5upy.js → actor-shared-B5tJfzt-.js} +2 -2
  9. package/dist/{actor-shared-DI7J5upy.js.map → actor-shared-B5tJfzt-.js.map} +1 -1
  10. package/dist/actor.d.ts +1 -1
  11. package/dist/actor.js +3 -3
  12. package/dist/ai-Cai-lCbj.d.ts +580 -0
  13. package/dist/ai-Cai-lCbj.d.ts.map +1 -0
  14. package/dist/ai-control-CcD4hh3y.js +119 -0
  15. package/dist/ai-control-CcD4hh3y.js.map +1 -0
  16. package/dist/ai-server.d.ts +6 -6
  17. package/dist/ai-server.d.ts.map +1 -1
  18. package/dist/ai-server.js +1053 -513
  19. package/dist/ai-server.js.map +1 -1
  20. package/dist/ai.d.ts +2 -365
  21. package/dist/ai.js +801 -80
  22. package/dist/ai.js.map +1 -1
  23. package/dist/{client-P_NNNRM-.d.ts → client-BAEABRZB.d.ts} +2 -2
  24. package/dist/{client-P_NNNRM-.d.ts.map → client-BAEABRZB.d.ts.map} +1 -1
  25. package/dist/{client-Bf6uSEAk.js → client-BYzHjkwU.js} +21 -6
  26. package/dist/client-BYzHjkwU.js.map +1 -0
  27. package/dist/client.d.ts +1 -1
  28. package/dist/client.js +1 -1
  29. package/dist/{contract-48bUMgcL.js → contract-CKRg_E4q.js} +3 -26
  30. package/dist/contract-CKRg_E4q.js.map +1 -0
  31. package/dist/index.d.ts +2 -2
  32. package/dist/index.js +1 -1
  33. package/dist/react.d.ts +2 -2
  34. package/dist/react.js +1 -1
  35. package/dist/{reducer-DJKWm3cp.d.ts → reducer-BcS9VDKC.d.ts} +4 -1
  36. package/dist/{reducer-DJKWm3cp.d.ts.map → reducer-BcS9VDKC.d.ts.map} +1 -1
  37. package/dist/reducer-DEMjEY_O.js +29 -0
  38. package/dist/reducer-DEMjEY_O.js.map +1 -0
  39. package/dist/scheduler-qstash.d.ts +2 -2
  40. package/dist/scheduler-qstash.js +1 -1
  41. package/dist/scheduler-vercel.d.ts +2 -2
  42. package/dist/scheduler-vercel.js +1 -1
  43. package/dist/{server-DjZZa1wr.d.ts → server-Bp5Nd1pF.d.ts} +3 -3
  44. package/dist/{server-DjZZa1wr.d.ts.map → server-Bp5Nd1pF.d.ts.map} +1 -1
  45. package/dist/{server-BeNADlCI.js → server-CjJSGcF7.js} +4 -3
  46. package/dist/server-CjJSGcF7.js.map +1 -0
  47. package/dist/server.d.ts +3 -3
  48. package/dist/server.js +1 -1
  49. package/dist/{store-DtDOWLSn.d.ts → store-D_yhNdPz.d.ts} +7 -2
  50. package/dist/{store-DtDOWLSn.d.ts.map → store-D_yhNdPz.d.ts.map} +1 -1
  51. package/dist/store-N8PXxDAS.js.map +1 -1
  52. package/dist/store-memory.d.ts +1 -1
  53. package/dist/store-postgres.d.ts +1 -1
  54. package/dist/store-postgres.js +19 -0
  55. package/dist/store-postgres.js.map +1 -1
  56. package/dist/store-redis-http.d.ts +1 -1
  57. package/dist/store-redis-http.js +1 -1
  58. package/dist/{store-redis-notify-BUCyXOn0.js → store-redis-notify-D2EI6gwX.js} +27 -2
  59. package/dist/store-redis-notify-D2EI6gwX.js.map +1 -0
  60. package/dist/store-redis.d.ts +1 -1
  61. package/dist/store-redis.js +1 -1
  62. package/dist/store-sqlite.d.ts +1 -1
  63. package/docs/guides/06-ai-agents.mdx +287 -65
  64. package/docs/reference/01-api.mdx +122 -27
  65. package/examples/playground/app/agent/[agentId]/agent-client.tsx +118 -21
  66. package/examples/playground/app/agent/[agentId]/agent-queue.tsx +289 -0
  67. package/examples/playground/app/agent/compaction-settings.test.ts +22 -6
  68. package/examples/playground/app/agent/model.ts +11 -2
  69. package/examples/playground/app/agent/server.ts +8 -2
  70. package/examples/playground/app/chat/[chatId]/chat-client.tsx +3 -13
  71. package/examples/playground/app/chat/model.ts +2 -2
  72. package/examples/playground/app/chat/server.ts +24 -17
  73. package/examples/playground/app/globals.css +179 -0
  74. package/examples/playground/package.json +1 -1
  75. package/package.json +1 -1
  76. package/src/ai-client-state.ts +185 -0
  77. package/src/ai-control-server.ts +829 -0
  78. package/src/ai-control-state.ts +152 -0
  79. package/src/ai-control.ts +139 -0
  80. package/src/ai-coordinator.ts +99 -32
  81. package/src/ai-progress-batches.ts +68 -0
  82. package/src/ai-projector.ts +76 -15
  83. package/src/ai-sdk-step.ts +0 -1
  84. package/src/ai-server.ts +429 -619
  85. package/src/ai.ts +553 -108
  86. package/src/client.ts +31 -9
  87. package/src/licenses/Apache-2.0.txt +55 -0
  88. package/src/parse-partial-json.ts +441 -0
  89. package/src/reducer.ts +6 -0
  90. package/src/server.ts +8 -4
  91. package/src/store-postgres.ts +27 -0
  92. package/src/store-redis-core.ts +53 -1
  93. package/src/store-redis-notify.ts +1 -0
  94. package/src/store.ts +6 -0
  95. package/dist/ai.d.ts.map +0 -1
  96. package/dist/client-Bf6uSEAk.js.map +0 -1
  97. package/dist/contract-48bUMgcL.js.map +0 -1
  98. package/dist/server-BeNADlCI.js.map +0 -1
  99. 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. 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
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 user facts such
139
- as `ai.message.created`. Built-in server handlers schedule
140
- `ai.generation.requested`, run the AI SDK, and append progress back to the same
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
- There is no separate message state to reconcile. `push()` folds the
313
- `ai.message.created` fact locally before starting the request, so the user
314
- message appears immediately. If the request fails, A2 removes that optimistic
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
- Queued messages keep log order.
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 still appears in `AIState.messages`. A later user
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.message.created optimistic, then durable
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? })` | records a message fact; `false` keeps context without scheduling |
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)` | records an approval decision fact |
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)` | records `ai.retry.requested` for a failed response |
423
- | `inputs.interrupt(options)` | interrupts an active response |
424
-
425
- The builders hide stable event ids, so the same interaction is safe to resend.
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`. Automatic tools see an `ai.tool.called` event.
486
- Approved tools see the `ai.approval.responded` event that authorized
487
- them. Narrow `ctx.event.type` if you need fields specific to either
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()` records an ordinary `ai.message.created` fact. The
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.activeGeneration
668
- const requested =
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.interrupt({
699
- ...target,
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 optimistic event updates the UI immediately and reaches A2's cancellation
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, keeps the partial response the user actually saw, and turns
716
- incomplete visible tools into `output-error`. An accepted interruption
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
- Between `ai.generation.requested` and `ai.generation.started`, copy
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 synchronous resolver receiving the same
804
- `{ event, state, history, signal }` context as `model` and `instructions`. It
805
- returns `false`, automatic options, or a custom policy once per generation.
806
- Use this to select a session-specific threshold from already-recorded settings.
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 and history, and
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.