experimental-a2 0.15.0 → 0.16.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 (101) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/dist/actor-client.d.ts +1 -1
  3. package/dist/{actor-BfQSE0KC.d.ts → actor-ohPC-81x.d.ts} +5 -5
  4. package/dist/{actor-BfQSE0KC.d.ts.map → actor-ohPC-81x.d.ts.map} +1 -1
  5. package/dist/actor-react.d.ts +3 -3
  6. package/dist/actor.d.ts +1 -1
  7. package/dist/actor.js +1 -1
  8. package/dist/{ai-Cai-lCbj.d.ts → ai-CrEf6p_W.d.ts} +73 -14
  9. package/dist/ai-CrEf6p_W.d.ts.map +1 -0
  10. package/dist/ai-DgOBltJ_.js +2451 -0
  11. package/dist/ai-DgOBltJ_.js.map +1 -0
  12. package/dist/ai-server.d.ts +4 -5
  13. package/dist/ai-server.d.ts.map +1 -1
  14. package/dist/ai-server.js +788 -296
  15. package/dist/ai-server.js.map +1 -1
  16. package/dist/ai.d.ts +2 -2
  17. package/dist/ai.js +1 -1999
  18. package/dist/client-BYzHjkwU.js.map +1 -1
  19. package/dist/{client-BAEABRZB.d.ts → client-CzyacQpJ.d.ts} +9 -9
  20. package/dist/client-CzyacQpJ.d.ts.map +1 -0
  21. package/dist/client.d.ts +1 -1
  22. package/dist/idempotent-id-BRJVeylj.js +13 -0
  23. package/dist/idempotent-id-BRJVeylj.js.map +1 -0
  24. package/dist/index.d.ts +3 -3
  25. package/dist/index.js +1 -12
  26. package/dist/otel.d.ts +1 -1
  27. package/dist/react.d.ts +13 -13
  28. package/dist/react.d.ts.map +1 -1
  29. package/dist/react.js.map +1 -1
  30. package/dist/reducer-DEMjEY_O.js.map +1 -1
  31. package/dist/{reducer-BcS9VDKC.d.ts → reducer-otzHjuJj.d.ts} +3 -3
  32. package/dist/{reducer-BcS9VDKC.d.ts.map → reducer-otzHjuJj.d.ts.map} +1 -1
  33. package/dist/scheduler-qstash.d.ts +2 -2
  34. package/dist/scheduler-qstash.js +1 -1
  35. package/dist/scheduler-vercel.d.ts +2 -2
  36. package/dist/scheduler-vercel.js +1 -1
  37. package/dist/{server-CjJSGcF7.js → server-BD5ckxZb.js} +39 -26
  38. package/dist/server-BD5ckxZb.js.map +1 -0
  39. package/dist/{server-Bp5Nd1pF.d.ts → server-DbD7IJVK.d.ts} +5 -5
  40. package/dist/{server-Bp5Nd1pF.d.ts.map → server-DbD7IJVK.d.ts.map} +1 -1
  41. package/dist/server.d.ts +3 -3
  42. package/dist/server.js +1 -1
  43. package/dist/{store-D_yhNdPz.d.ts → store-DyZM6fS5.d.ts} +3 -2
  44. package/dist/{store-D_yhNdPz.d.ts.map → store-DyZM6fS5.d.ts.map} +1 -1
  45. package/dist/store-N8PXxDAS.js.map +1 -1
  46. package/dist/store-memory.d.ts +1 -1
  47. package/dist/store-postgres.d.ts +1 -1
  48. package/dist/store-redis-http.d.ts +1 -1
  49. package/dist/store-redis-http.js +1 -1
  50. package/dist/{store-redis-notify-D2EI6gwX.js → store-redis-notify-BVLUfI3j.js} +14 -13
  51. package/dist/store-redis-notify-BVLUfI3j.js.map +1 -0
  52. package/dist/store-redis.d.ts +1 -1
  53. package/dist/store-redis.js +1 -1
  54. package/dist/store-sqlite.d.ts +1 -1
  55. package/dist/{telemetry-CpeclqB2.d.ts → telemetry-B5jzpy6y.d.ts} +2 -2
  56. package/dist/telemetry-B5jzpy6y.d.ts.map +1 -0
  57. package/docs/concepts/04-state.mdx +9 -4
  58. package/docs/guides/06-ai-agents.mdx +149 -48
  59. package/docs/reference/01-api.mdx +110 -28
  60. package/examples/playground/app/agent/[agentId]/agent-client.tsx +20 -28
  61. package/examples/playground/app/agent/[agentId]/agent-queue.test.tsx +123 -0
  62. package/examples/playground/app/agent/[agentId]/agent-queue.tsx +37 -43
  63. package/examples/playground/app/agent/[agentId]/compaction-panel.tsx +44 -24
  64. package/examples/playground/app/agent/compaction-timeline.test.ts +47 -7
  65. package/examples/playground/app/agent/compaction-timeline.ts +13 -3
  66. package/examples/playground/app/chat/[chatId]/chat-client.tsx +1 -5
  67. package/examples/playground/app/chat/[chatId]/session.ts +17 -2
  68. package/examples/playground/app/chat/model.test.ts +4 -4
  69. package/examples/playground/app/chat/model.ts +18 -6
  70. package/examples/playground/app/chat/server.ts +6 -3
  71. package/examples/playground/package.json +3 -1
  72. package/package.json +1 -1
  73. package/src/ai-client-state.ts +36 -4
  74. package/src/ai-context-schema.ts +296 -0
  75. package/src/ai-context.ts +680 -0
  76. package/src/ai-control-server.ts +183 -135
  77. package/src/ai-control-state.ts +6 -0
  78. package/src/ai-control.ts +8 -7
  79. package/src/ai-coordinator.ts +92 -38
  80. package/src/ai-id.ts +6 -0
  81. package/src/ai-input-tokens.ts +75 -0
  82. package/src/ai-message-projection.ts +474 -0
  83. package/src/ai-server.ts +247 -336
  84. package/src/ai-stored-state.ts +484 -0
  85. package/src/ai.ts +255 -538
  86. package/src/client.ts +21 -15
  87. package/src/react.ts +25 -19
  88. package/src/reducer.ts +2 -1
  89. package/src/server.ts +73 -56
  90. package/src/store-redis-core.ts +15 -17
  91. package/src/store.ts +1 -0
  92. package/src/telemetry.ts +2 -1
  93. package/dist/ai-Cai-lCbj.d.ts.map +0 -1
  94. package/dist/ai-control-CcD4hh3y.js +0 -119
  95. package/dist/ai-control-CcD4hh3y.js.map +0 -1
  96. package/dist/ai.js.map +0 -1
  97. package/dist/client-BAEABRZB.d.ts.map +0 -1
  98. package/dist/index.js.map +0 -1
  99. package/dist/server-CjJSGcF7.js.map +0 -1
  100. package/dist/store-redis-notify-D2EI6gwX.js.map +0 -1
  101. package/dist/telemetry-CpeclqB2.d.ts.map +0 -1
@@ -20,9 +20,9 @@ AI_GATEWAY_API_KEY=your_api_key
20
20
 
21
21
  ## Define the agent
22
22
 
23
- `agent()` defines an isomorphic event contract and its standard `AIState`
24
- reducer. The same value types the server, server-rendered state, and optimistic
25
- client updates.
23
+ `agent()` defines an isomorphic event contract, normalized `AIStoredState`,
24
+ and its `AIState` view. The same reducer types server snapshots, hydration,
25
+ and optimistic client updates.
26
26
 
27
27
  ```ts assistant.ts
28
28
  import { agent } from 'experimental-a2/ai'
@@ -399,7 +399,7 @@ stores every AI SDK chunk once. The reducer and the exported
399
399
  `UIMessage`; cumulative message snapshots are not duplicated in the log.
400
400
 
401
401
  `AIState` exposes `messages`, `status`, `activeGeneration`, `activeRequestId`,
402
- `activeResponseMessageId`, `activeProjection`, `responseGenerationIds`,
402
+ `activeResponseMessageId`, `activeProjection`, `responseOwners`,
403
403
  `terminalRequestIds`, `terminalGenerations`, `pendingApprovals`,
404
404
  `pendingInputs`, `tools`, `compaction`, per-generation `usage`, and the last
405
405
  generation `error`.
@@ -417,11 +417,23 @@ current response before its generation starts.
417
417
  `activeResponseMessageId` identifies that request's response before
418
418
  `activeGeneration` exists. It is `null` after the generation starts.
419
419
 
420
- `responseGenerationIds` keeps the latest generation owner for each response
420
+ `responseOwners` keeps the latest generation ID, request ID, and attempt for each response
421
421
  message. Late lifecycle events from a superseded generation stay in raw
422
422
  history but cannot alter the projected message, even after its replacement
423
423
  finishes.
424
424
 
425
+ A2 generates fixed-length deterministic IDs in its reserved `a2.ai:` namespace
426
+ for requests, generations, responses, and worker lifecycle events. Treat those
427
+ IDs as opaque. An assistant response uses its first model-request event's ID. Generation requests carry
428
+ `responseMessageId` explicitly; tool continuations also carry `sourceGenerationId`.
429
+ Ownership comes from these fields and the generation's `requestId` and `attempt`,
430
+ so ID strings never embed the preceding tool loop.
431
+
432
+ Approval and application-input events carry both `requestId` and `generationId`.
433
+ Copy both from the pending approval, pending input, or active generation when
434
+ sending a response. `activeRequestReason` and `activeRequestSourceGenerationId`
435
+ describe the current request independently from the message projection.
436
+
425
437
  Generation requests share one durable lane, so model calls do not overlap.
426
438
  The queued-turn policy is the stronger ordering rule: a later user message is
427
439
  not scheduled until the current response's complete tool loop finishes. Lane
@@ -442,7 +454,7 @@ continuation that has not been appended yet.
442
454
  | `inputs.retry(options)` | requests a fresh attempt for a failed response |
443
455
  | `inputs.stop({ turnId })` | ends the observed turn and advances |
444
456
  | `inputs.steer({ turnId, message })` | prioritizes input after the observed turn's current step finishes |
445
- | `inputs.queue.edit({ message, expectedRevision })` | edits a pending input |
457
+ | `inputs.queue.edit({ message })` | edits a pending input |
446
458
  | `inputs.queue.move({ inputId, beforeId })` | reorders pending input; `null` means the end |
447
459
  | `inputs.queue.remove({ inputId })` | removes pending input from the inbox |
448
460
  | `inputs.queue.sendNow({ inputId, turnId })` | atomically prioritizes queued input and interrupts the observed turn |
@@ -610,6 +622,7 @@ export function ApprovalControls() {
610
622
  onClick={() =>
611
623
  void push(
612
624
  ...inputs.approval({
625
+ requestId: approval.requestId,
613
626
  messageId: approval.messageId,
614
627
  generationId: approval.generationId,
615
628
  approvalId: approval.approvalId,
@@ -624,6 +637,7 @@ export function ApprovalControls() {
624
637
  onClick={() =>
625
638
  void push(
626
639
  ...inputs.approval({
640
+ requestId: approval.requestId,
627
641
  messageId: approval.messageId,
628
642
  generationId: approval.generationId,
629
643
  approvalId: approval.approvalId,
@@ -714,8 +728,10 @@ export function StopButton() {
714
728
  }
715
729
  ```
716
730
 
717
- The accepted command ends the turn and reaches A2's cancellation channel. `lastSeenIndex` is the exact confirmed log frontier visible when the
718
- user clicked. The reducer rewinds the indexed generation projection to that
731
+ The client immediately previews Stop for the observed turn. `lastSeenIndex` is the exact confirmed log frontier visible when the
732
+ user clicked. While that generation's indexed projection is retained, the client freezes the response at that
733
+ frontier through the request echo and controller decision. A rejected push or decision removes the preview.
734
+ The reducer rewinds the indexed generation projection to that
719
735
  frontier and keeps the partial text the user actually saw. Tool parts whose
720
736
  arguments are still streaming are drafts: interruption removes them from the
721
737
  conversation and subsequent model prompts, even if their current JSON parses.
@@ -726,6 +742,13 @@ input, and compaction events remain in raw history but cannot reactivate it or
726
742
  alter the projection.
727
743
  Completed tool results at or before the visible frontier remain completed.
728
744
 
745
+ Once Stop is persisted, A2's existing cancellation stream aborts running model
746
+ and tool work for that turn without waiting for the control handler. Startup
747
+ checks also skip matching jobs created while Stop is pending. Cancellation is
748
+ cooperative: tools must honor their abort signal. The control handler remains
749
+ responsible for recording the interruption and advancing the inbox; a Stop for
750
+ an older turn cannot cancel its successor.
751
+
729
752
  For the lower-level `inputs.interrupt()` builder, between
730
753
  `ai.generation.requested` and `ai.generation.started`, copy
731
754
  `activeRequestId` as `requestId` and `activeResponseMessageId` as `messageId`.
@@ -738,12 +761,19 @@ copy the owner from a pending approval, pending input, or running tool while
738
761
  the response is still waiting.
739
762
 
740
763
  After a failed model call, `inputs.retry({ messageId, responseMessageId,
741
- retryId })` records a retry request. The server schedules its fresh attempt.
764
+ retryId })` records a retry request. The server reruns that failed model call.
765
+ Earlier completed model steps and their tool results remain, including steps
766
+ completed after the latest compaction. The committed summary also remains. For
767
+ example, if a tool succeeds after compaction and the next model call fails, retry
768
+ uses the summary and that tool's result.
742
769
  `retryId` identifies the user's action, so resending one retry is safe while a
743
- later retry remains distinct. Failed partial progress remains in raw history
744
- but is excluded from the new prompt. Later user messages remain queued while
770
+ later retry remains distinct. The failed model call's uncommitted step remains in
771
+ raw history but is excluded from the new prompt and response. Later user messages remain queued while
745
772
  the failed response awaits an explicit retry or interruption.
746
773
 
774
+ Applications own conversation forks and edit-and-resend flows for previously
775
+ admitted messages. A2's retry operation continues the current response.
776
+
747
777
  ## Append from the server
748
778
 
749
779
  Browser interactions should use `push()` for instant state. Server-originated
@@ -802,17 +832,17 @@ failure or use an explicit threshold in that case.
802
832
  The default threshold is 75% of the context window. A larger configured
803
833
  `generation.maxOutputTokens` lowers the threshold further to reserve that output
804
834
  allowance. This is an input-context policy, not a new output limit. Limits and
805
- metadata status are available in `state.modelMetadata`.
835
+ metadata status are available through an explicit transcript reducer read.
806
836
 
807
837
  `compaction: { thresholdTokens: 80_000 }` overrides discovery with an explicit
808
838
  positive safe integer. `compaction: { instructions: 'Preserve exact IDs.' }`
809
839
  adds guidance to the built-in summary prompt while keeping the derived threshold.
810
840
  `compaction: false` disables both compaction and metadata lookup. The `compaction`
811
841
  option can also be a resolver receiving the same
812
- `{ event, state, session, signal }` context as `model` and `instructions`. It
842
+ `{ event, messages, session, signal }` context as `model` and `instructions`. It
813
843
  returns `false`, automatic options, or a custom policy once per generation and
814
844
  can await `session.state(settingsReducer)` to read application settings.
815
- Use this to select a threshold from the checkpointed AI state.
845
+ Use this to select a threshold from application settings at the prompt boundary.
816
846
  Changes after resolution apply to a later model step. Disabling compaction
817
847
  preserves summaries that were already recorded. Custom provider
818
848
  models and custom global SDK providers need an explicit threshold. A custom
@@ -849,7 +879,13 @@ the summary, its usage, and the exact event frontier it covers. Later model
849
879
  requests receive the summary plus everything produced after that frontier,
850
880
  including later tool steps in the same assistant message. Queued user messages
851
881
  remain queued. Compaction waits until earlier tool calls have terminal results.
852
- The raw log and `state.messages` remain available in full.
882
+ The raw log and transcript from `agent.reducer` remain available in full.
883
+ Server execution uses a separate model-context reducer and checkpoint. Successful
884
+ compaction permanently replaces covered context. Callbacks receive their prompt
885
+ messages and request information, not reducer state or execution internals.
886
+ Application reducers remain explicit reads through `session.state(reducer)`.
887
+ To read the full transcript, load `session.state(agent.reducer)` and pass its
888
+ `state` to `agent.reducer.view()`. Client hooks continue to expose `AIState`.
853
889
 
854
890
  Custom `shouldCompact(context)` and `compact(context)` callbacks remain
855
891
  available for deterministic replacement messages or application-specific
@@ -873,16 +909,25 @@ export function CompactionStatus() {
873
909
  }
874
910
  ```
875
911
 
876
- A failed or interrupted generation restores the prior compaction state. The
877
- synthetic summary request is not a user turn or an assistant reply in
912
+ Successful compaction is a permanent boundary. Failure, interruption, retry, and
913
+ worker recovery never restore the messages it replaced. Retry reruns the failed
914
+ model step from the latest committed context and preserves earlier completed
915
+ steps. A failed summary attempt leaves the previous committed context intact.
916
+ The synthetic summary request is not a user turn or an assistant reply in
878
917
  `state.messages`.
879
918
 
919
+ Compaction runs only when retained tool exchanges have final results and all
920
+ A2-owned tool work has settled, including work from stopped or superseded turns.
921
+ A2 checks a fresh coordinator checkpoint before starting a candidate compaction.
922
+ This adds one state read on that path. Unresolved dead-lettered tool work keeps
923
+ compaction deferred until repair and settlement or session closure.
924
+
880
925
  ## Extend the assembly
881
926
 
882
927
  The convenience API is ordinary A2 parts:
883
928
 
884
929
  - `events` and `createEvents()` export the built-in schemas.
885
- - `createReducer({ contract })` builds the standard `AIState` projection for a
930
+ - `createReducer({ contract })` builds the normalized state and `AIState` view for a
886
931
  compatible contract.
887
932
  - `agent()` combines the built-in protocol, application events, and reducer.
888
933
  - `tool()` binds an AI SDK function-tool definition to an agent's typed A2
@@ -979,20 +1024,56 @@ emit typed metadata chunks itself.
979
1024
 
980
1025
  ## Checkpoints and recovery reads
981
1026
 
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.
1027
+ The AI reducer stores normalized `AIStoredState`. `session.state(agent.reducer)`
1028
+ returns that state and its log index, ready to pass to `SessionProvider` or client
1029
+ hydration. Client and React snapshots derive the familiar `AIState` view
1030
+ immediately, including streamed message parts and tool activity.
1031
+
1032
+ For server-side inspection, use the reducer's view:
1033
+
1034
+ ```ts server/read-agent.ts
1035
+ import type { UIMessage } from 'ai'
1036
+ import { assistantServer } from './assistant'
1037
+ import { assistant } from '../assistant'
1038
+
1039
+ export async function readMessages(sessionId: string): Promise<UIMessage[]> {
1040
+ const snapshot = await assistantServer.session(sessionId).state(assistant.reducer)
1041
+ return assistant.reducer.view(snapshot.state).messages
1042
+ }
1043
+ ```
1044
+
1045
+ Each snapshot stores repeated payloads once and keeps only the changes needed
1046
+ to restore the current generation's prompt and projection. The live view is cached while
1047
+ events fold; normalization does not run for every token. Reading or serializing
1048
+ the stored fields materializes the records. The log and displayed conversation
1049
+ retain their full content.
1050
+
1051
+ Model steps read the model-context reducer and coordinator at a fixed log
1052
+ boundary. Model, instruction, compaction, and custom-generation callbacks
1053
+ receive prompt messages and request information without reducer state. Custom transcript folds and
1054
+ schemas apply only to the transcript; read application state explicitly with
1055
+ `session.state(yourReducer)` when a callback needs it.
984
1056
  They do not receive a raw history array. Their `session.state(reducer)` reads
985
1057
  application reducers at the captured prompt boundary by default; explicit
986
1058
  `through` options follow the ordinary state-read contract. Compaction selection
987
1059
  can be asynchronous when it needs a settings-state read.
988
1060
 
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.
1061
+ Ordinary generation uses the model-context checkpoint without reading the
1062
+ transcript or replaying history to apply an existing compaction. The context
1063
+ checkpoint has its own identity and fold. Recovery and explicit retry discard
1064
+ only the unfinished model step. Completed compactions remain committed, so these
1065
+ paths never reconstruct pre-compaction prompts or retain rollback copies.
1066
+
1067
+ Tools recovering in a fresh process read context at their tool-admission index.
1068
+ The saved generation projection restores their original prompt, preserving the
1069
+ committed summary and excluding later arrivals. No separate history read is
1070
+ needed. A2 core can rebuild a missing or invalid reducer snapshot from the log.
1071
+
1072
+ Compaction immediately releases covered message and tool content from model
1073
+ context. Execution retains the active turn, queued inputs, unsettled tool work
1074
+ IDs, and token calibration. Model metadata is cached per distinct model.
1075
+ Historical input uniqueness comes from core event deduplication. The full
1076
+ transcript remains available independently.
996
1077
 
997
1078
  Streaming progress updates the current message incrementally. Snapshots preserve
998
1079
  open text and reasoning parts and partial tool inputs. Indexed progress remains
@@ -1037,9 +1118,23 @@ agent session when adopting it. Existing event logs remain readable; finish
1037
1118
  pending work with the runtime that created it before upgrading.
1038
1119
 
1039
1120
  `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`.
1121
+ admits one input at a time, freezes its content, and adds it to `state.messages`.
1041
1122
  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.
1123
+ Each item has an `id`, `message`, and `generate` flag.
1124
+
1125
+ New-input commands from `inputs.message()`, `inputs.seed()`, and `inputs.steer()`
1126
+ use the message ID as their event ID. Those IDs must match on hand-written
1127
+ commands too. Caller IDs cannot start with the reserved `a2.ai:` prefix. IDs must
1128
+ be unique within the store; include contract/session scope when deriving them
1129
+ with `idempotentId()`.
1130
+
1131
+ An identical submission replays its original event without creating more work,
1132
+ even after removal or compaction. Reusing the ID with different content, a
1133
+ different command action, or another session rejects at append with
1134
+ `PARTIAL_DUPLICATE_BATCH`. Use `queue.edit()` to change a pending input and a fresh
1135
+ message ID to create another input. Edit, move, remove, and other commands have
1136
+ their own event IDs. The controller stores no historical input-ID registry.
1137
+
1043
1138
 
1044
1139
  `state.active` identifies the logical turn, its frozen `input`, and its `phase`.
1045
1140
  The turn ID stays the same across model steps, tools, and a pause. Stop ends that
@@ -1049,8 +1144,8 @@ turn and admits the next queued input when admission is running.
1049
1144
  import { inputs, type AIState } from 'experimental-a2/ai'
1050
1145
  import type { UIMessage } from 'ai'
1051
1146
 
1052
- export function editPending(message: UIMessage, expectedRevision: number) {
1053
- return inputs.queue.edit({ message, expectedRevision })
1147
+ export function editPending(message: UIMessage) {
1148
+ return inputs.queue.edit({ message })
1054
1149
  }
1055
1150
 
1056
1151
  export function movePending(inputId: string, beforeId: string | null) {
@@ -1083,13 +1178,15 @@ export const resume = () => inputs.resume()
1083
1178
  Pass these arrays to `push(...events)` from `useSession()`, or to a server
1084
1179
  session's `append(...events)`. `beforeId: null` moves an item to the end.
1085
1180
  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`.
1181
+ its original submission event, so replay cannot recreate a removed prompt.
1182
+ Removing an input that is no longer pending returns `not-pending`, whether it was
1183
+ admitted, removed, or never queued.
1089
1184
 
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`.
1185
+ Edits to pending input use last-write-wins in session log order. Clients can send
1186
+ another edit before an earlier one is confirmed. Editing preserves the message's
1187
+ identity and role; changing its role returns `role-mismatch`. An edit that loses
1188
+ to admission returns `not-pending`. A Stop aimed at a completed turn returns
1189
+ `stale-turn`.
1093
1190
 
1094
1191
  Steer validates the new input and places it ahead of the inbox in one decision.
1095
1192
  If the observed turn is still active, it finishes its current model response and
@@ -1109,7 +1206,7 @@ Steering preserves the admission gate: steering while paused does not resume
1109
1206
  execution. Duplicate input identities and closed sessions still reject the command.
1110
1207
 
1111
1208
  Send now uses the selected queued message's existing identity and latest
1112
- accepted revision. It validates the selected input and the observed `turnId`,
1209
+ accepted content. It validates the selected input and the observed `turnId`,
1113
1210
  then moves the input first for the same handoff after the current step. Use
1114
1211
  `turnId: null` when no turn is active. If that expectation is stale, the command
1115
1212
  returns `stale-turn` without changing the queue.
@@ -1117,8 +1214,8 @@ returns `stale-turn` without changing the queue.
1117
1214
  Send now preserves pause just like steering. A paused queue keeps the selected
1118
1215
  input first until Resume. It accepts user-role inputs, including passive user
1119
1216
  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.
1217
+ reserved internal IDs are rejected before append. Unsaved editor text
1218
+ is separate from the accepted queue content; save it before sending.
1122
1219
 
1123
1220
  A queued item's optional `afterStepOf` records the observed turn for steering.
1124
1221
  A matching active turn hands off after its current step.
@@ -1131,8 +1228,9 @@ With `agent().reducer` or `createReducer()`, the client state is already optimis
1131
1228
  An idle, unpaused send appears in `state.messages` immediately, with
1132
1229
  `state.starting: true`. Another send waits in `state.inbox.items` while that
1133
1230
  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.
1231
+ the inbox. Render the conversation and queue from `state`. A pending Stop clears
1232
+ the displayed `state.active` turn immediately; rejection restores it. New active
1233
+ turn identities still come from accepted control decisions.
1136
1234
  Steering appears immediately in `state.messages`, after the current response,
1137
1235
  and stays there while that step finishes. Send now has the same display when
1138
1236
  an active turn is targeted. These inputs are omitted from the client inbox,
@@ -1175,12 +1273,15 @@ Server `session.state(reducer)`, reducer folds, checkpoints, and model prompts
1175
1273
  contain confirmed messages and inbox entries, with `starting: false`. The client
1176
1274
  applies pending intent and displays accepted steering only when returning its
1177
1275
  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.
1276
+ A custom reducer can keep normalized AI state by nesting `agent.reducer.fold()`
1277
+ and derive its view with `agent.reducer.view()`. It does not inherit the built-in
1278
+ optimistic client display. `reduceAIState()` remains a pure transition over the
1279
+ confirmed `AIState` view for applications that project raw event histories.
1280
+ `state.pendingQueueCommands` tracks unresolved queue commands and Stops incrementally, so
1281
+ the client does not scan raw history. Pending edits apply in order, so the latest
1282
+ local edit stays visible while earlier commands are confirmed. Editing and Send
1283
+ now can remain available while an edit awaits its receipt. Close an editor as
1284
+ soon as its save is pushed; retain the draft if the push fails.
1184
1285
 
1185
1286
  ### Persistence and application are separate
1186
1287
 
@@ -761,6 +761,13 @@ eligible retained checkpoint. A root historical read never creates retention
761
761
  or writes a stale checkpoint behind the latest snapshot; it folds from the log
762
762
  when no eligible checkpoint exists.
763
763
 
764
+ Operational snapshot-read failures preserve existing A2 errors; other errors
765
+ become `STORE_UNAVAILABLE` with the original error as the cause. They do not retry through individual
766
+ reads or raw history. Cache misses and `stateSchema` rejection still rebuild
767
+ from the log. Snapshot write-back remains background work; failures are reported
768
+ through `a2.snapshot` telemetry or the default console sink, without failing the
769
+ completed read or attaching snapshot contents.
770
+
764
771
  ### `session.stream(options?)`
765
772
 
766
773
  ```ts
@@ -1098,20 +1105,24 @@ Pure typed inputs for `append()` and `push()`:
1098
1105
  | `inputs.retry(options)` | retries a failed response |
1099
1106
  | `inputs.stop({ turnId, lastSeenIndex? })` | ends the observed turn and advances when admission is open |
1100
1107
  | `inputs.steer({ turnId, message, lastSeenIndex? })` | puts the new input first for a handoff after the observed turn's current step |
1101
- | `inputs.queue.edit({ message, expectedRevision })` | changes a pending input; its ID stays fixed |
1108
+ | `inputs.queue.edit({ message })` | changes a pending input; its ID stays fixed |
1102
1109
  | `inputs.queue.move({ inputId, beforeId })` | moves pending input before an ID; `null` means the end |
1103
- | `inputs.queue.remove({ inputId })` | removes pending input; its identity stays reserved |
1110
+ | `inputs.queue.remove({ inputId })` | removes pending input; its submission stays in the log |
1104
1111
  | `inputs.queue.sendNow({ inputId, turnId, lastSeenIndex? })` | promotes an existing queued input for a handoff after the current step; preserves pause |
1105
1112
  | `inputs.pause({ when: 'now' \| 'after-turn' })` | pauses execution now or inbox admission after the turn |
1106
1113
  | `inputs.resume()` | resumes retained work, then inbox admission |
1107
1114
  | `inputs.toolResult(result)` | records a deferred provider-executed tool result from trusted server code |
1108
1115
  | `inputs.interrupt(options)` | interrupts a specific generation/request owner |
1109
1116
 
1117
+ Edits are last-write-wins in session log order and preserve the pending message's
1118
+ identity and role. They require no revision or confirmation before another edit.
1119
+ Admission freezes the message; subsequent edits return `not-pending`.
1120
+
1110
1121
  All builders emit `ai.control.requested`. Persistence and application are
1111
1122
  separate: `ai.control.decided` returns `{ commandId, outcome, reason? }` through
1112
1123
  the existing stream. Outcomes are `applied` or `rejected`. Rejection reasons
1113
- include `already-active`, `revision-conflict`, `stale-turn`, `not-found`,
1114
- `duplicate-input`, `not-user-input`, and `closed`. `state.receipt` holds the latest user receipt.
1124
+ include `not-pending`, `role-mismatch`, `stale-turn`, `not-found`,
1125
+ `not-user-input`, and `closed`. `state.receipt` holds the latest user receipt.
1115
1126
 
1116
1127
  Steering validates and prioritizes new input even if the observed turn has
1117
1128
  finished. A matching active turn finishes its current model response and tool
@@ -1122,15 +1133,14 @@ or waits first behind a different active turn. The priority change is atomic;
1122
1133
  the later handoff and new work are committed atomically at the step boundary.
1123
1134
  Steer and Send now do not use `lastSeenIndex` to cut output.
1124
1135
 
1125
- Queue removal leaves the log intact and retains the removed identity's reservation.
1126
- Send now preserves the queued input ID and latest accepted revision. It requires
1136
+ Queue removal leaves the original submission in the log, so replay does not recreate the input.
1137
+ Send now preserves the queued input ID and latest accepted content. It requires
1127
1138
  an observed `turnId` (or null for idle), preserves pause, and only accepts
1128
- user-role input. It promotes passive user input to generation after reserving its
1129
- response ID. Missing targets, stale turns, and identity collisions reject before
1130
- any queue change.
1139
+ user-role input. It promotes passive user input to generation. Missing targets
1140
+ and stale turns reject before any queue change.
1131
1141
 
1132
1142
  `state.inbox` contains `paused` and ordered `items`. Each item contains
1133
- `{ id, revision, message, generate, afterStepOf? }`. The optional turn ID
1143
+ `{ id, message, generate, afterStepOf? }`. The optional turn ID
1134
1144
  `afterStepOf` records the observed steering turn while that item remains queued;
1135
1145
  a matching active turn hands off after its current step.
1136
1146
  Removal cancels it and admission consumes it. `state.active` is null or contains
@@ -1142,7 +1152,12 @@ execution version. Tools without terminal outcomes can retry; their stable
1142
1152
 
1143
1153
  `inputs` deliberately has no session lifecycle methods. Append explicit
1144
1154
  `ai.session.created` and `ai.session.closed` events from trusted server code
1145
- when an application uses them. Message, approval, input, and retry builders use stable interaction IDs.
1155
+ when an application uses them. Message, seed, and steering builders set the event ID to the input message ID.
1156
+ The two IDs must match for all new-input commands. Caller command/input IDs must
1157
+ be nonempty and cannot use the reserved `a2.ai:` prefix. Identical events replay;
1158
+ conflicting ID reuse rejects before append under core deduplication rules. IDs
1159
+ are unique across the store, including different contracts and sessions.
1160
+ Approval, input-response, and retry builders use stable interaction IDs.
1146
1161
  Other commands use IDs assigned by `push()` or `append()`; transport retries
1147
1162
  preserve those IDs. Browser
1148
1163
  ingress accepts user messages, approval and input responses, interruptions,
@@ -1163,12 +1178,25 @@ field. Model output is generation progress rather than another
1163
1178
  new turn.
1164
1179
 
1165
1180
  Approval and input request/response payloads require the active
1166
- `generationId`. Clients copy it from the pending request, which prevents a
1181
+ `requestId` and `generationId`. Clients copy both from the pending request, which prevents a
1167
1182
  delayed response from satisfying a newer model step.
1168
1183
 
1169
1184
  `ai.retry.requested` carries `{ messageId, responseMessageId, retryId }`.
1170
1185
  `retryId` identifies one user action. The server converts the fact into an
1171
1186
  `ai.generation.requested` whose reason is `retry`.
1187
+ Retry reruns the failed model call while retaining earlier completed model steps,
1188
+ their tool results, and committed compactions, including steps completed after
1189
+ the latest compaction.
1190
+ The failed model call's uncommitted step is removed from the active prompt and
1191
+ response. Conversation forks and edit-and-resend flows for admitted messages are
1192
+ application-owned.
1193
+
1194
+ A2-generated requests, generations, assistant responses, worker jobs, reports,
1195
+ and lifecycle events have fixed-length deterministic IDs in the reserved
1196
+ `a2.ai:` namespace. An assistant message's ID equals its initial model-request
1197
+ event ID. Control state keeps no lifetime input/response-ID registry. IDs are opaque;
1198
+ ownership is carried by event fields. `ai.generation.requested` requires
1199
+ `responseMessageId`; a tool continuation also requires `sourceGenerationId`.
1172
1200
 
1173
1201
  `ai.generation.failed` sets `stepLimit: true` when `maxSteps` rejects a
1174
1202
  continuation before another model step starts.
@@ -1206,7 +1234,7 @@ message type. Both are isomorphic.
1206
1234
 
1207
1235
  Clients using `agent().reducer` or `createReducer()` expose `state.messages`,
1208
1236
  `state.inbox`, and `state.starting` with pending send, edit, move, remove,
1209
- Send now, steer, pause, and resume commands applied for display. Idle, unpaused
1237
+ Send now, steer, pause, resume, and Stop commands applied for display. Idle, unpaused
1210
1238
  sends appear in the conversation immediately. A generating input occupies the
1211
1239
  optimistic turn, so subsequent sends queue. Busy or paused sessions queue new
1212
1240
  sends. `starting` indicates a speculative turn awaiting acceptance; it does not
@@ -1217,11 +1245,17 @@ inbox. This display survives receipt and hydration, including while paused.
1217
1245
  They remain pending server inputs until admission and do not enter the model
1218
1246
  prompt early. Multiple steering inputs follow inbox admission order.
1219
1247
 
1220
- `state.active`, model execution, and revision numbers stay confirmed.
1248
+ New active turn identities come from accepted control decisions. A pending Stop
1249
+ for that turn clears the displayed `state.active`, generation, and pending
1250
+ approvals/inputs, and previews an idle status (or paused when the inbox is paused). With
1251
+ `lastSeenIndex`, its retained generation projection freezes at the observed
1252
+ frontier. The preview lasts through the request echo until the controller's
1253
+ decision; rejected requests restore the confirmed active turn and output.
1221
1254
  Server reads and reducer folds retain confirmed messages and inbox entries;
1222
1255
  `state.starting` is false there. Custom reducers that call `reduceAIState()` or
1223
- nest the AI fold do not inherit the client display behavior. Disable repeat editing and Send now while an
1224
- edit for that input is present in `state.pendingQueueCommands`.
1256
+ nest the AI fold do not inherit the client display behavior. Repeat editing and
1257
+ Send now remain available while earlier edits are pending. Later local edits
1258
+ stay visible through earlier commands' acknowledgments and receipts.
1225
1259
 
1226
1260
  The pending-command ledger survives request echo and retires on the matching
1227
1261
  `ai.control.decided` receipt or session close. Failed pushes roll back through
@@ -1231,15 +1265,32 @@ read raw history.
1231
1265
  ### `createReducer(options)`
1232
1266
 
1233
1267
  ```ts
1234
- createReducer({ contract, name? }): Reducer<AIState>
1268
+ createReducer({ contract, name?, fold?, stateSchema? }): AIStoredReducer
1235
1269
  ```
1236
1270
 
1237
- Builds the standard AI projection for a compatible contract. `AIState`
1271
+ Builds a normalized AI reducer for a compatible contract. Its `initialState`,
1272
+ `fold`, and server `session.state(reducer)` use `AIStoredState`. Client and
1273
+ React snapshots expose the inferred `AIState` view automatically.
1274
+
1275
+ `reducer.view(storedState)` derives confirmed `AIState` for server code.
1276
+ `reducer.fromView(state)` converts a confirmed view into stored state, including
1277
+ when migrating an application-owned hydration value. Keep the stored state and
1278
+ its log index together when hydrating; do not fold already-seen history twice.
1279
+ `fold` can customize the confirmed `AIState` transition, and `stateSchema`
1280
+ validates that view after restoration. Both apply only to the transcript.
1281
+ Model context has its own built-in fold and schema. Changed custom fold semantics
1282
+ still require a new `name`.
1283
+
1284
+ Stored state is versioned, serializable data. Shared payload records and
1285
+ inverse changes replace repeated tool values and complete recovery copies.
1286
+ The reducer caches its live view and builds the records when the stored fields
1287
+ are read or serialized. Treat state and views as immutable. No event or tool
1288
+ payload is truncated. `AIState`
1238
1289
  contains `inbox`, `active`, `starting`, the latest command `receipt`, session lifecycle,
1239
1290
  messages, generation status, pending approvals
1240
1291
  and input, tool activity, compaction, usage, the last error, and
1241
- `activeRequestId`, `activeResponseMessageId`,
1242
- `responseGenerationIds: Record<string, string>`, and terminal request and
1292
+ `activeRequestId`, `activeRequestReason`, `activeRequestSourceGenerationId`, `activeResponseMessageId`,
1293
+ `responseOwners: Record<string, { generationId: string; requestId: string; attempt: number }>`, and terminal request and
1243
1294
  generation ownership fences.
1244
1295
  `activeRequestId` is the server-authorized generation request and fences
1245
1296
  delayed requests before their generation starts. `activeResponseMessageId`
@@ -1248,7 +1299,7 @@ identifies the requested response until `activeGeneration` exists.
1248
1299
  active, after a generation step completes while its response waits on tool,
1249
1300
  approval, or input barriers, while paused, and after a generation fails while it awaits retry
1250
1301
  or interruption. Response completion, interruption, supersession, retry, or a
1251
- later generation clears or replaces it. `responseGenerationIds` keeps the
1302
+ later generation clears or replaces it. `responseOwners` keeps the
1252
1303
  latest generation owner for each response message, so late events from a
1253
1304
  superseded owner cannot alter the projection. `terminalRequestIds` and
1254
1305
  `terminalGenerations` preserve accepted completion, failure, interruption,
@@ -1267,7 +1318,7 @@ Checkpoint size and serialization cost still grow with retained progress.
1267
1318
  Tool-input deltas update `input-streaming` parts with best-effort partial JSON
1268
1319
  for display. The state stays `input-streaming` until a completed input arrives;
1269
1320
  partial parsing does not authorize execution or change interruption handling.
1270
- Extension events are ignored. The default reducer name is `a2.ai.state.v17`.
1321
+ Extension events are ignored. The default reducer name is `a2.ai.state.v22:records-v2`.
1271
1322
 
1272
1323
  ### `deriveUIMessages(history)` and `reduceAIState(state, event)`
1273
1324
 
@@ -1370,6 +1421,16 @@ The resolver runs once per generation and returns a valid policy or `false`.
1370
1421
  It can await application reducers through `context.session.state(reducer)`, whose
1371
1422
  default boundary is the captured prompt frontier. Model, instruction, compaction,
1372
1423
  metadata, and custom-generation contexts expose state access and omit raw history.
1424
+ Callbacks receive prompt messages and request identity, without `state` or
1425
+ `execution` arguments. Model and instruction resolvers receive
1426
+ `{ event, messages, session, signal }`. Compaction and generation callbacks also
1427
+ receive `modelMessages`, including committed summaries. Custom transcript folds
1428
+ and schemas apply only to the transcript. `agent.reducer` and `createReducer()`
1429
+ continue to expose the full
1430
+ transcript view. To read it explicitly, load
1431
+ `context.session.state(agent.reducer)` and pass its `state` to
1432
+ `agent.reducer.view()`. The stored state also remains suitable for client
1433
+ hydration.
1373
1434
  Static options are validated at construction; resolved options are validated
1374
1435
  before that generation starts. Each session uses its own resolved value. `thresholdTokens` is an
1375
1436
  optional positive safe integer. `instructions` adds to A2's internal summary
@@ -1390,10 +1451,20 @@ compaction configuration. Provider-executed tools, structured output, and forced
1390
1451
  tool choices skip implicit compaction and reject explicit automatic policies.
1391
1452
  Custom policies remain available for these configurations.
1392
1453
 
1393
- The input estimate includes messages, tools, and instructions, and uses prior
1394
- reported input usage to anchor later growth where applicable. It is not a provider
1395
- tokenizer. Completed generations record `inputTokenEstimate` beside usage. The
1396
- built-in summary adds one model request and preserves the request prefix for
1454
+ The raw input estimate uses serialized bytes of messages, tools, and instructions,
1455
+ excluding provider options on messages and content parts. These options still reach
1456
+ the model unchanged. It is not a provider tokenizer. A preceding same-model
1457
+ completion anchors later growth to reported input usage, correcting both overestimates
1458
+ and underestimates. A smaller prompt, retry, compaction, or model change stops
1459
+ that calibration.
1460
+
1461
+ Completed generations record the raw `inputTokenEstimate` beside usage.
1462
+ Automatic `ai.compaction.requested` events also record the pre-compaction
1463
+ `inputTokenEstimate`, the calibrated `inputTokens` used for the decision, and
1464
+ `thresholdTokens`. Custom-policy requests omit these diagnostics. They add no
1465
+ model calls or database round trips.
1466
+
1467
+ The built-in summary adds one model request and preserves the request prefix for
1397
1468
  caching. It does not invoke application generation callbacks, transforms, or tool
1398
1469
  hooks. Both custom policy callbacks and `generate` receive `modelMessages`, the
1399
1470
  model-ready prompt including summaries, alongside typed UI `messages`.
@@ -1401,7 +1472,14 @@ model-ready prompt including summaries, alongside typed UI `messages`.
1401
1472
  Built-in summaries remain separate from application-typed messages. Render
1402
1473
  `state.compaction?.status === 'running'` as current activity, and the durable
1403
1474
  `ai.compaction.*` events as historical timeline markers. The conversation in
1404
- `state.messages` remains intact. `progress` controls durable batching with
1475
+ `state.messages` from the transcript reducer remains intact. Model execution
1476
+ uses a separate context checkpoint; existing compactions do not require a
1477
+ transcript read. Successful compaction is permanent, including across failure,
1478
+ interruption, retry, and recovery. Retry reruns the failed step from the latest
1479
+ committed context. Compaction defers while tool exchanges or A2-owned tool work
1480
+ remain unsettled, checked with one fresh coordinator state read on each candidate.
1481
+ Unresolved dead-lettered tool work also defers compaction until repair and
1482
+ settlement or session closure. `progress` controls durable batching with
1405
1483
  `maxChunks` and `maxDelayMs`.
1406
1484
 
1407
1485
  This entry point is server-only and resolves to a throwing browser stub.
@@ -1544,7 +1622,7 @@ each handler → the events those handlers append.
1544
1622
 
1545
1623
  ### The span catalogue
1546
1624
 
1547
- Four spans, all carrying `a2.contract` and `a2.session_id`:
1625
+ Five spans, all carrying `a2.contract` and `a2.session_id`:
1548
1626
 
1549
1627
  | Span | Wraps |
1550
1628
  | ----------- | ------------------------------------------------------ |
@@ -1552,6 +1630,7 @@ Four spans, all carrying `a2.contract` and `a2.session_id`:
1552
1630
  | `a2.drain` | one drain pass over a session's backlog |
1553
1631
  | `a2.event` | one claimed dispatch of one event |
1554
1632
  | `a2.state` | one `state()` read: snapshot-plus-tail load + fold |
1633
+ | `a2.snapshot` | one background batch of checkpoint writes |
1555
1634
 
1556
1635
  Attributes arrive in two waves: **at start** (passed to `span()`) and
1557
1636
  **mid-span** (via `handle.setAttribute`; outcomes and counts aren't
@@ -1579,6 +1658,9 @@ alert on are all mid-span.
1579
1658
  | `a2.state.snapshot` | `a2.state` | mid | `hit` \| `miss` \| `rejected` (schema guard discarded it) |
1580
1659
  | `a2.state.folded` | `a2.state` | mid | events folded past the snapshot |
1581
1660
  | `a2.state.index` | `a2.state` | mid | the frontier the returned state reflects |
1661
+ | `a2.snapshot.reducer` | `a2.snapshot` | start | the reducer's name |
1662
+ | `a2.snapshot.index` | `a2.snapshot` | start | greatest checkpoint index in the write batch |
1663
+ | `a2.snapshot.count` | `a2.snapshot` | start | number of checkpoints in the write batch |
1582
1664
 
1583
1665
  Drain outcomes: `settled` means nothing actionable is left; `busy` means live
1584
1666
  per-event claims remain; `stalled` means a caught failure needs a later retry.