experimental-a2 0.14.1 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +81 -0
- package/dist/actor-client.d.ts +1 -1
- package/dist/{actor-BfQSE0KC.d.ts → actor-ohPC-81x.d.ts} +5 -5
- package/dist/{actor-BfQSE0KC.d.ts.map → actor-ohPC-81x.d.ts.map} +1 -1
- package/dist/actor-react.d.ts +3 -3
- package/dist/actor.d.ts +1 -1
- package/dist/actor.js +1 -1
- package/dist/ai-CrLYwNEx.js +2431 -0
- package/dist/ai-CrLYwNEx.js.map +1 -0
- package/dist/{ai-Cai-lCbj.d.ts → ai-HJ9fHfYI.d.ts} +72 -13
- package/dist/ai-HJ9fHfYI.d.ts.map +1 -0
- package/dist/ai-server.d.ts +4 -5
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +755 -290
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +2 -2
- package/dist/ai.js +1 -1999
- package/dist/client-BYzHjkwU.js.map +1 -1
- package/dist/{client-BAEABRZB.d.ts → client-CzyacQpJ.d.ts} +9 -9
- package/dist/client-CzyacQpJ.d.ts.map +1 -0
- package/dist/client.d.ts +1 -1
- package/dist/idempotent-id-BRJVeylj.js +13 -0
- package/dist/idempotent-id-BRJVeylj.js.map +1 -0
- package/dist/index.d.ts +8 -4
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -1
- package/dist/otel.d.ts +1 -1
- package/dist/react.d.ts +13 -13
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js.map +1 -1
- package/dist/reducer-DEMjEY_O.js.map +1 -1
- package/dist/{reducer-BcS9VDKC.d.ts → reducer-otzHjuJj.d.ts} +3 -3
- package/dist/{reducer-BcS9VDKC.d.ts.map → reducer-otzHjuJj.d.ts.map} +1 -1
- 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-CjJSGcF7.js → server-BD5ckxZb.js} +39 -26
- package/dist/server-BD5ckxZb.js.map +1 -0
- package/dist/{server-Bp5Nd1pF.d.ts → server-DbD7IJVK.d.ts} +5 -5
- package/dist/{server-Bp5Nd1pF.d.ts.map → server-DbD7IJVK.d.ts.map} +1 -1
- package/dist/server.d.ts +3 -3
- package/dist/server.js +1 -1
- package/dist/{store-D_yhNdPz.d.ts → store-DyZM6fS5.d.ts} +3 -2
- package/dist/{store-D_yhNdPz.d.ts.map → store-DyZM6fS5.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-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +1 -1
- package/dist/{store-redis-notify-D2EI6gwX.js → store-redis-notify-BVLUfI3j.js} +14 -13
- package/dist/store-redis-notify-BVLUfI3j.js.map +1 -0
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.js +1 -1
- package/dist/store-sqlite.d.ts +1 -1
- package/dist/{telemetry-CpeclqB2.d.ts → telemetry-B5jzpy6y.d.ts} +2 -2
- package/dist/telemetry-B5jzpy6y.d.ts.map +1 -0
- package/docs/concepts/04-state.mdx +9 -4
- package/docs/guides/06-ai-agents.mdx +134 -43
- package/docs/reference/01-api.mdx +129 -27
- package/examples/playground/app/agent/[agentId]/agent-client.tsx +20 -28
- package/examples/playground/app/agent/[agentId]/agent-queue.test.tsx +123 -0
- package/examples/playground/app/agent/[agentId]/agent-queue.tsx +37 -43
- package/examples/playground/app/agent/[agentId]/compaction-panel.tsx +44 -24
- package/examples/playground/app/agent/compaction-timeline.test.ts +47 -7
- package/examples/playground/app/agent/compaction-timeline.ts +13 -3
- package/examples/playground/app/chat/[chatId]/chat-client.tsx +1 -5
- package/examples/playground/app/chat/[chatId]/session.ts +17 -2
- package/examples/playground/app/chat/model.test.ts +4 -4
- package/examples/playground/app/chat/model.ts +18 -6
- package/examples/playground/app/chat/server.ts +6 -3
- package/examples/playground/package.json +3 -1
- package/package.json +1 -1
- package/src/ai-client-state.ts +0 -2
- package/src/ai-context-schema.ts +296 -0
- package/src/ai-context.ts +680 -0
- package/src/ai-control-server.ts +160 -131
- package/src/ai-control-state.ts +6 -0
- package/src/ai-control.ts +6 -7
- package/src/ai-coordinator.ts +92 -38
- package/src/ai-id.ts +6 -0
- package/src/ai-input-tokens.ts +75 -0
- package/src/ai-message-projection.ts +348 -0
- package/src/ai-server.ts +232 -334
- package/src/ai-stored-state.ts +484 -0
- package/src/ai.ts +252 -418
- package/src/client.ts +21 -15
- package/src/idempotent-id.ts +11 -0
- package/src/index.ts +1 -0
- package/src/react.ts +25 -19
- package/src/reducer.ts +2 -1
- package/src/server.ts +73 -56
- package/src/store-redis-core.ts +15 -17
- package/src/store.ts +1 -0
- package/src/telemetry.ts +2 -1
- package/dist/ai-Cai-lCbj.d.ts.map +0 -1
- package/dist/ai-control-CcD4hh3y.js +0 -119
- package/dist/ai-control-CcD4hh3y.js.map +0 -1
- package/dist/ai.js.map +0 -1
- package/dist/client-BAEABRZB.d.ts.map +0 -1
- package/dist/server-CjJSGcF7.js.map +0 -1
- package/dist/store-redis-notify-D2EI6gwX.js.map +0 -1
- 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
|
|
24
|
-
|
|
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`, `
|
|
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
|
-
`
|
|
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
|
|
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,
|
|
@@ -738,12 +752,19 @@ copy the owner from a pending approval, pending input, or running tool while
|
|
|
738
752
|
the response is still waiting.
|
|
739
753
|
|
|
740
754
|
After a failed model call, `inputs.retry({ messageId, responseMessageId,
|
|
741
|
-
retryId })` records a retry request. The server
|
|
755
|
+
retryId })` records a retry request. The server reruns that failed model call.
|
|
756
|
+
Earlier completed model steps and their tool results remain, including steps
|
|
757
|
+
completed after the latest compaction. The committed summary also remains. For
|
|
758
|
+
example, if a tool succeeds after compaction and the next model call fails, retry
|
|
759
|
+
uses the summary and that tool's result.
|
|
742
760
|
`retryId` identifies the user's action, so resending one retry is safe while a
|
|
743
|
-
later retry remains distinct.
|
|
744
|
-
but is excluded from the new prompt. Later user messages remain queued while
|
|
761
|
+
later retry remains distinct. The failed model call's uncommitted step remains in
|
|
762
|
+
raw history but is excluded from the new prompt and response. Later user messages remain queued while
|
|
745
763
|
the failed response awaits an explicit retry or interruption.
|
|
746
764
|
|
|
765
|
+
Applications own conversation forks and edit-and-resend flows for previously
|
|
766
|
+
admitted messages. A2's retry operation continues the current response.
|
|
767
|
+
|
|
747
768
|
## Append from the server
|
|
748
769
|
|
|
749
770
|
Browser interactions should use `push()` for instant state. Server-originated
|
|
@@ -802,17 +823,17 @@ failure or use an explicit threshold in that case.
|
|
|
802
823
|
The default threshold is 75% of the context window. A larger configured
|
|
803
824
|
`generation.maxOutputTokens` lowers the threshold further to reserve that output
|
|
804
825
|
allowance. This is an input-context policy, not a new output limit. Limits and
|
|
805
|
-
metadata status are available
|
|
826
|
+
metadata status are available through an explicit transcript reducer read.
|
|
806
827
|
|
|
807
828
|
`compaction: { thresholdTokens: 80_000 }` overrides discovery with an explicit
|
|
808
829
|
positive safe integer. `compaction: { instructions: 'Preserve exact IDs.' }`
|
|
809
830
|
adds guidance to the built-in summary prompt while keeping the derived threshold.
|
|
810
831
|
`compaction: false` disables both compaction and metadata lookup. The `compaction`
|
|
811
832
|
option can also be a resolver receiving the same
|
|
812
|
-
`{ event,
|
|
833
|
+
`{ event, messages, session, signal }` context as `model` and `instructions`. It
|
|
813
834
|
returns `false`, automatic options, or a custom policy once per generation and
|
|
814
835
|
can await `session.state(settingsReducer)` to read application settings.
|
|
815
|
-
Use this to select a threshold from the
|
|
836
|
+
Use this to select a threshold from application settings at the prompt boundary.
|
|
816
837
|
Changes after resolution apply to a later model step. Disabling compaction
|
|
817
838
|
preserves summaries that were already recorded. Custom provider
|
|
818
839
|
models and custom global SDK providers need an explicit threshold. A custom
|
|
@@ -849,7 +870,13 @@ the summary, its usage, and the exact event frontier it covers. Later model
|
|
|
849
870
|
requests receive the summary plus everything produced after that frontier,
|
|
850
871
|
including later tool steps in the same assistant message. Queued user messages
|
|
851
872
|
remain queued. Compaction waits until earlier tool calls have terminal results.
|
|
852
|
-
The raw log and `
|
|
873
|
+
The raw log and transcript from `agent.reducer` remain available in full.
|
|
874
|
+
Server execution uses a separate model-context reducer and checkpoint. Successful
|
|
875
|
+
compaction permanently replaces covered context. Callbacks receive their prompt
|
|
876
|
+
messages and request information, not reducer state or execution internals.
|
|
877
|
+
Application reducers remain explicit reads through `session.state(reducer)`.
|
|
878
|
+
To read the full transcript, load `session.state(agent.reducer)` and pass its
|
|
879
|
+
`state` to `agent.reducer.view()`. Client hooks continue to expose `AIState`.
|
|
853
880
|
|
|
854
881
|
Custom `shouldCompact(context)` and `compact(context)` callbacks remain
|
|
855
882
|
available for deterministic replacement messages or application-specific
|
|
@@ -873,16 +900,25 @@ export function CompactionStatus() {
|
|
|
873
900
|
}
|
|
874
901
|
```
|
|
875
902
|
|
|
876
|
-
|
|
877
|
-
|
|
903
|
+
Successful compaction is a permanent boundary. Failure, interruption, retry, and
|
|
904
|
+
worker recovery never restore the messages it replaced. Retry reruns the failed
|
|
905
|
+
model step from the latest committed context and preserves earlier completed
|
|
906
|
+
steps. A failed summary attempt leaves the previous committed context intact.
|
|
907
|
+
The synthetic summary request is not a user turn or an assistant reply in
|
|
878
908
|
`state.messages`.
|
|
879
909
|
|
|
910
|
+
Compaction runs only when retained tool exchanges have final results and all
|
|
911
|
+
A2-owned tool work has settled, including work from stopped or superseded turns.
|
|
912
|
+
A2 checks a fresh coordinator checkpoint before starting a candidate compaction.
|
|
913
|
+
This adds one state read on that path. Unresolved dead-lettered tool work keeps
|
|
914
|
+
compaction deferred until repair and settlement or session closure.
|
|
915
|
+
|
|
880
916
|
## Extend the assembly
|
|
881
917
|
|
|
882
918
|
The convenience API is ordinary A2 parts:
|
|
883
919
|
|
|
884
920
|
- `events` and `createEvents()` export the built-in schemas.
|
|
885
|
-
- `createReducer({ contract })` builds the
|
|
921
|
+
- `createReducer({ contract })` builds the normalized state and `AIState` view for a
|
|
886
922
|
compatible contract.
|
|
887
923
|
- `agent()` combines the built-in protocol, application events, and reducer.
|
|
888
924
|
- `tool()` binds an AI SDK function-tool definition to an agent's typed A2
|
|
@@ -979,20 +1015,56 @@ emit typed metadata chunks itself.
|
|
|
979
1015
|
|
|
980
1016
|
## Checkpoints and recovery reads
|
|
981
1017
|
|
|
982
|
-
|
|
983
|
-
|
|
1018
|
+
The AI reducer stores normalized `AIStoredState`. `session.state(agent.reducer)`
|
|
1019
|
+
returns that state and its log index, ready to pass to `SessionProvider` or client
|
|
1020
|
+
hydration. Client and React snapshots derive the familiar `AIState` view
|
|
1021
|
+
immediately, including streamed message parts and tool activity.
|
|
1022
|
+
|
|
1023
|
+
For server-side inspection, use the reducer's view:
|
|
1024
|
+
|
|
1025
|
+
```ts server/read-agent.ts
|
|
1026
|
+
import type { UIMessage } from 'ai'
|
|
1027
|
+
import { assistantServer } from './assistant'
|
|
1028
|
+
import { assistant } from '../assistant'
|
|
1029
|
+
|
|
1030
|
+
export async function readMessages(sessionId: string): Promise<UIMessage[]> {
|
|
1031
|
+
const snapshot = await assistantServer.session(sessionId).state(assistant.reducer)
|
|
1032
|
+
return assistant.reducer.view(snapshot.state).messages
|
|
1033
|
+
}
|
|
1034
|
+
```
|
|
1035
|
+
|
|
1036
|
+
Each snapshot stores repeated payloads once and keeps only the changes needed
|
|
1037
|
+
to restore the current generation's prompt and projection. The live view is cached while
|
|
1038
|
+
events fold; normalization does not run for every token. Reading or serializing
|
|
1039
|
+
the stored fields materializes the records. The log and displayed conversation
|
|
1040
|
+
retain their full content.
|
|
1041
|
+
|
|
1042
|
+
Model steps read the model-context reducer and coordinator at a fixed log
|
|
1043
|
+
boundary. Model, instruction, compaction, and custom-generation callbacks
|
|
1044
|
+
receive prompt messages and request information without reducer state. Custom transcript folds and
|
|
1045
|
+
schemas apply only to the transcript; read application state explicitly with
|
|
1046
|
+
`session.state(yourReducer)` when a callback needs it.
|
|
984
1047
|
They do not receive a raw history array. Their `session.state(reducer)` reads
|
|
985
1048
|
application reducers at the captured prompt boundary by default; explicit
|
|
986
1049
|
`through` options follow the ordinary state-read contract. Compaction selection
|
|
987
1050
|
can be asynchronous when it needs a settings-state read.
|
|
988
1051
|
|
|
989
|
-
Ordinary generation uses
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1052
|
+
Ordinary generation uses the model-context checkpoint without reading the
|
|
1053
|
+
transcript or replaying history to apply an existing compaction. The context
|
|
1054
|
+
checkpoint has its own identity and fold. Recovery and explicit retry discard
|
|
1055
|
+
only the unfinished model step. Completed compactions remain committed, so these
|
|
1056
|
+
paths never reconstruct pre-compaction prompts or retain rollback copies.
|
|
1057
|
+
|
|
1058
|
+
Tools recovering in a fresh process read context at their tool-admission index.
|
|
1059
|
+
The saved generation projection restores their original prompt, preserving the
|
|
1060
|
+
committed summary and excluding later arrivals. No separate history read is
|
|
1061
|
+
needed. A2 core can rebuild a missing or invalid reducer snapshot from the log.
|
|
1062
|
+
|
|
1063
|
+
Compaction immediately releases covered message and tool content from model
|
|
1064
|
+
context. Execution retains the active turn, queued inputs, unsettled tool work
|
|
1065
|
+
IDs, and token calibration. Model metadata is cached per distinct model.
|
|
1066
|
+
Historical input uniqueness comes from core event deduplication. The full
|
|
1067
|
+
transcript remains available independently.
|
|
996
1068
|
|
|
997
1069
|
Streaming progress updates the current message incrementally. Snapshots preserve
|
|
998
1070
|
open text and reasoning parts and partial tool inputs. Indexed progress remains
|
|
@@ -1037,9 +1109,23 @@ agent session when adopting it. Existing event logs remain readable; finish
|
|
|
1037
1109
|
pending work with the runtime that created it before upgrading.
|
|
1038
1110
|
|
|
1039
1111
|
`inputs.message(message)` places a message in the durable inbox. The controller
|
|
1040
|
-
admits one input at a time, freezes its
|
|
1112
|
+
admits one input at a time, freezes its content, and adds it to `state.messages`.
|
|
1041
1113
|
Pending inputs appear in `state.inbox.items`; they do not enter the model prompt.
|
|
1042
|
-
Each item has an `id`, `
|
|
1114
|
+
Each item has an `id`, `message`, and `generate` flag.
|
|
1115
|
+
|
|
1116
|
+
New-input commands from `inputs.message()`, `inputs.seed()`, and `inputs.steer()`
|
|
1117
|
+
use the message ID as their event ID. Those IDs must match on hand-written
|
|
1118
|
+
commands too. Caller IDs cannot start with the reserved `a2.ai:` prefix. IDs must
|
|
1119
|
+
be unique within the store; include contract/session scope when deriving them
|
|
1120
|
+
with `idempotentId()`.
|
|
1121
|
+
|
|
1122
|
+
An identical submission replays its original event without creating more work,
|
|
1123
|
+
even after removal or compaction. Reusing the ID with different content, a
|
|
1124
|
+
different command action, or another session rejects at append with
|
|
1125
|
+
`PARTIAL_DUPLICATE_BATCH`. Use `queue.edit()` to change a pending input and a fresh
|
|
1126
|
+
message ID to create another input. Edit, move, remove, and other commands have
|
|
1127
|
+
their own event IDs. The controller stores no historical input-ID registry.
|
|
1128
|
+
|
|
1043
1129
|
|
|
1044
1130
|
`state.active` identifies the logical turn, its frozen `input`, and its `phase`.
|
|
1045
1131
|
The turn ID stays the same across model steps, tools, and a pause. Stop ends that
|
|
@@ -1049,8 +1135,8 @@ turn and admits the next queued input when admission is running.
|
|
|
1049
1135
|
import { inputs, type AIState } from 'experimental-a2/ai'
|
|
1050
1136
|
import type { UIMessage } from 'ai'
|
|
1051
1137
|
|
|
1052
|
-
export function editPending(message: UIMessage
|
|
1053
|
-
return inputs.queue.edit({ message
|
|
1138
|
+
export function editPending(message: UIMessage) {
|
|
1139
|
+
return inputs.queue.edit({ message })
|
|
1054
1140
|
}
|
|
1055
1141
|
|
|
1056
1142
|
export function movePending(inputId: string, beforeId: string | null) {
|
|
@@ -1083,13 +1169,15 @@ export const resume = () => inputs.resume()
|
|
|
1083
1169
|
Pass these arrays to `push(...events)` from `useSession()`, or to a server
|
|
1084
1170
|
session's `append(...events)`. `beforeId: null` moves an item to the end.
|
|
1085
1171
|
Removal only applies to pending input. It preserves the immutable log and keeps
|
|
1086
|
-
|
|
1087
|
-
Removing an
|
|
1088
|
-
|
|
1172
|
+
its original submission event, so replay cannot recreate a removed prompt.
|
|
1173
|
+
Removing an input that is no longer pending returns `not-pending`, whether it was
|
|
1174
|
+
admitted, removed, or never queued.
|
|
1089
1175
|
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1176
|
+
Edits to pending input use last-write-wins in session log order. Clients can send
|
|
1177
|
+
another edit before an earlier one is confirmed. Editing preserves the message's
|
|
1178
|
+
identity and role; changing its role returns `role-mismatch`. An edit that loses
|
|
1179
|
+
to admission returns `not-pending`. A Stop aimed at a completed turn returns
|
|
1180
|
+
`stale-turn`.
|
|
1093
1181
|
|
|
1094
1182
|
Steer validates the new input and places it ahead of the inbox in one decision.
|
|
1095
1183
|
If the observed turn is still active, it finishes its current model response and
|
|
@@ -1109,7 +1197,7 @@ Steering preserves the admission gate: steering while paused does not resume
|
|
|
1109
1197
|
execution. Duplicate input identities and closed sessions still reject the command.
|
|
1110
1198
|
|
|
1111
1199
|
Send now uses the selected queued message's existing identity and latest
|
|
1112
|
-
accepted
|
|
1200
|
+
accepted content. It validates the selected input and the observed `turnId`,
|
|
1113
1201
|
then moves the input first for the same handoff after the current step. Use
|
|
1114
1202
|
`turnId: null` when no turn is active. If that expectation is stale, the command
|
|
1115
1203
|
returns `stale-turn` without changing the queue.
|
|
@@ -1117,8 +1205,8 @@ returns `stale-turn` without changing the queue.
|
|
|
1117
1205
|
Send now preserves pause just like steering. A paused queue keeps the selected
|
|
1118
1206
|
input first until Resume. It accepts user-role inputs, including passive user
|
|
1119
1207
|
input, which it marks for generation. Non-user context returns `not-user-input`;
|
|
1120
|
-
|
|
1121
|
-
is separate from the accepted queue
|
|
1208
|
+
reserved internal IDs are rejected before append. Unsaved editor text
|
|
1209
|
+
is separate from the accepted queue content; save it before sending.
|
|
1122
1210
|
|
|
1123
1211
|
A queued item's optional `afterStepOf` records the observed turn for steering.
|
|
1124
1212
|
A matching active turn hands off after its current step.
|
|
@@ -1175,12 +1263,15 @@ Server `session.state(reducer)`, reducer folds, checkpoints, and model prompts
|
|
|
1175
1263
|
contain confirmed messages and inbox entries, with `starting: false`. The client
|
|
1176
1264
|
applies pending intent and displays accepted steering only when returning its
|
|
1177
1265
|
state, after folding events.
|
|
1178
|
-
A custom reducer
|
|
1179
|
-
|
|
1266
|
+
A custom reducer can keep normalized AI state by nesting `agent.reducer.fold()`
|
|
1267
|
+
and derive its view with `agent.reducer.view()`. It does not inherit the built-in
|
|
1268
|
+
optimistic client display. `reduceAIState()` remains a pure transition over the
|
|
1269
|
+
confirmed `AIState` view for applications that project raw event histories.
|
|
1180
1270
|
`state.pendingQueueCommands` tracks unresolved queue commands incrementally, so
|
|
1181
|
-
the client does not scan raw history.
|
|
1182
|
-
|
|
1183
|
-
|
|
1271
|
+
the client does not scan raw history. Pending edits apply in order, so the latest
|
|
1272
|
+
local edit stays visible while earlier commands are confirmed. Editing and Send
|
|
1273
|
+
now can remain available while an edit awaits its receipt. Close an editor as
|
|
1274
|
+
soon as its save is pushed; retain the draft if the push fails.
|
|
1184
1275
|
|
|
1185
1276
|
### Persistence and application are separate
|
|
1186
1277
|
|
|
@@ -63,6 +63,31 @@ The shared base for A2 failures, discriminated by `code`. The session
|
|
|
63
63
|
client exposes its `A2ClientError` subclass. See
|
|
64
64
|
[Errors](/reference/errors).
|
|
65
65
|
|
|
66
|
+
### `idempotentId(...parts)`
|
|
67
|
+
|
|
68
|
+
`idempotentId(...parts: string[]): Promise<string>` derives a stable UUIDv8
|
|
69
|
+
from an ordered tuple of strings. It runs on the server and in the browser.
|
|
70
|
+
|
|
71
|
+
```ts shared/request-id.ts
|
|
72
|
+
import { idempotentId } from 'experimental-a2'
|
|
73
|
+
|
|
74
|
+
export const id = await idempotentId('orders', 'shop-7', 'request-42')
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Pass the result as an event's `id` when retries should converge on the same
|
|
78
|
+
event. The same tuple produces the same ID across runtimes and A2 versions.
|
|
79
|
+
String boundaries, order, and case matter; strings are not normalized.
|
|
80
|
+
Empty strings and an empty tuple are valid inputs.
|
|
81
|
+
|
|
82
|
+
Every argument has the same role. A prefix such as `'orders'` is a namespace
|
|
83
|
+
by convention. Include enough context to distinguish operations across
|
|
84
|
+
contracts and sessions, since event IDs are globally unique within a store.
|
|
85
|
+
|
|
86
|
+
The helper performs no I/O. A2 preserves explicit IDs and keeps its automatic
|
|
87
|
+
ID generation unchanged. Changing an existing application's ID recipe changes
|
|
88
|
+
retry identity, so retries of previously committed requests need to retain
|
|
89
|
+
their original IDs.
|
|
90
|
+
|
|
66
91
|
## `experimental-a2/server`
|
|
67
92
|
|
|
68
93
|
### `handler(entry)`
|
|
@@ -736,6 +761,13 @@ eligible retained checkpoint. A root historical read never creates retention
|
|
|
736
761
|
or writes a stale checkpoint behind the latest snapshot; it folds from the log
|
|
737
762
|
when no eligible checkpoint exists.
|
|
738
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
|
+
|
|
739
771
|
### `session.stream(options?)`
|
|
740
772
|
|
|
741
773
|
```ts
|
|
@@ -1073,20 +1105,24 @@ Pure typed inputs for `append()` and `push()`:
|
|
|
1073
1105
|
| `inputs.retry(options)` | retries a failed response |
|
|
1074
1106
|
| `inputs.stop({ turnId, lastSeenIndex? })` | ends the observed turn and advances when admission is open |
|
|
1075
1107
|
| `inputs.steer({ turnId, message, lastSeenIndex? })` | puts the new input first for a handoff after the observed turn's current step |
|
|
1076
|
-
| `inputs.queue.edit({ message
|
|
1108
|
+
| `inputs.queue.edit({ message })` | changes a pending input; its ID stays fixed |
|
|
1077
1109
|
| `inputs.queue.move({ inputId, beforeId })` | moves pending input before an ID; `null` means the end |
|
|
1078
|
-
| `inputs.queue.remove({ inputId })` | removes pending input; its
|
|
1110
|
+
| `inputs.queue.remove({ inputId })` | removes pending input; its submission stays in the log |
|
|
1079
1111
|
| `inputs.queue.sendNow({ inputId, turnId, lastSeenIndex? })` | promotes an existing queued input for a handoff after the current step; preserves pause |
|
|
1080
1112
|
| `inputs.pause({ when: 'now' \| 'after-turn' })` | pauses execution now or inbox admission after the turn |
|
|
1081
1113
|
| `inputs.resume()` | resumes retained work, then inbox admission |
|
|
1082
1114
|
| `inputs.toolResult(result)` | records a deferred provider-executed tool result from trusted server code |
|
|
1083
1115
|
| `inputs.interrupt(options)` | interrupts a specific generation/request owner |
|
|
1084
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
|
+
|
|
1085
1121
|
All builders emit `ai.control.requested`. Persistence and application are
|
|
1086
1122
|
separate: `ai.control.decided` returns `{ commandId, outcome, reason? }` through
|
|
1087
1123
|
the existing stream. Outcomes are `applied` or `rejected`. Rejection reasons
|
|
1088
|
-
include `
|
|
1089
|
-
`
|
|
1124
|
+
include `not-pending`, `role-mismatch`, `stale-turn`, `not-found`,
|
|
1125
|
+
`not-user-input`, and `closed`. `state.receipt` holds the latest user receipt.
|
|
1090
1126
|
|
|
1091
1127
|
Steering validates and prioritizes new input even if the observed turn has
|
|
1092
1128
|
finished. A matching active turn finishes its current model response and tool
|
|
@@ -1097,15 +1133,14 @@ or waits first behind a different active turn. The priority change is atomic;
|
|
|
1097
1133
|
the later handoff and new work are committed atomically at the step boundary.
|
|
1098
1134
|
Steer and Send now do not use `lastSeenIndex` to cut output.
|
|
1099
1135
|
|
|
1100
|
-
Queue removal leaves the log
|
|
1101
|
-
Send now preserves the queued input ID and latest accepted
|
|
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
|
|
1102
1138
|
an observed `turnId` (or null for idle), preserves pause, and only accepts
|
|
1103
|
-
user-role input. It promotes passive user input to generation
|
|
1104
|
-
|
|
1105
|
-
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.
|
|
1106
1141
|
|
|
1107
1142
|
`state.inbox` contains `paused` and ordered `items`. Each item contains
|
|
1108
|
-
`{ id,
|
|
1143
|
+
`{ id, message, generate, afterStepOf? }`. The optional turn ID
|
|
1109
1144
|
`afterStepOf` records the observed steering turn while that item remains queued;
|
|
1110
1145
|
a matching active turn hands off after its current step.
|
|
1111
1146
|
Removal cancels it and admission consumes it. `state.active` is null or contains
|
|
@@ -1117,7 +1152,12 @@ execution version. Tools without terminal outcomes can retry; their stable
|
|
|
1117
1152
|
|
|
1118
1153
|
`inputs` deliberately has no session lifecycle methods. Append explicit
|
|
1119
1154
|
`ai.session.created` and `ai.session.closed` events from trusted server code
|
|
1120
|
-
when an application uses them. Message,
|
|
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.
|
|
1121
1161
|
Other commands use IDs assigned by `push()` or `append()`; transport retries
|
|
1122
1162
|
preserve those IDs. Browser
|
|
1123
1163
|
ingress accepts user messages, approval and input responses, interruptions,
|
|
@@ -1138,12 +1178,25 @@ field. Model output is generation progress rather than another
|
|
|
1138
1178
|
new turn.
|
|
1139
1179
|
|
|
1140
1180
|
Approval and input request/response payloads require the active
|
|
1141
|
-
`generationId`. Clients copy
|
|
1181
|
+
`requestId` and `generationId`. Clients copy both from the pending request, which prevents a
|
|
1142
1182
|
delayed response from satisfying a newer model step.
|
|
1143
1183
|
|
|
1144
1184
|
`ai.retry.requested` carries `{ messageId, responseMessageId, retryId }`.
|
|
1145
1185
|
`retryId` identifies one user action. The server converts the fact into an
|
|
1146
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`.
|
|
1147
1200
|
|
|
1148
1201
|
`ai.generation.failed` sets `stepLimit: true` when `maxSteps` rejects a
|
|
1149
1202
|
continuation before another model step starts.
|
|
@@ -1192,11 +1245,12 @@ inbox. This display survives receipt and hydration, including while paused.
|
|
|
1192
1245
|
They remain pending server inputs until admission and do not enter the model
|
|
1193
1246
|
prompt early. Multiple steering inputs follow inbox admission order.
|
|
1194
1247
|
|
|
1195
|
-
`state.active
|
|
1248
|
+
`state.active` and model execution stay confirmed.
|
|
1196
1249
|
Server reads and reducer folds retain confirmed messages and inbox entries;
|
|
1197
1250
|
`state.starting` is false there. Custom reducers that call `reduceAIState()` or
|
|
1198
|
-
nest the AI fold do not inherit the client display behavior.
|
|
1199
|
-
|
|
1251
|
+
nest the AI fold do not inherit the client display behavior. Repeat editing and
|
|
1252
|
+
Send now remain available while earlier edits are pending. Later local edits
|
|
1253
|
+
stay visible through earlier commands' acknowledgments and receipts.
|
|
1200
1254
|
|
|
1201
1255
|
The pending-command ledger survives request echo and retires on the matching
|
|
1202
1256
|
`ai.control.decided` receipt or session close. Failed pushes roll back through
|
|
@@ -1206,15 +1260,32 @@ read raw history.
|
|
|
1206
1260
|
### `createReducer(options)`
|
|
1207
1261
|
|
|
1208
1262
|
```ts
|
|
1209
|
-
createReducer({ contract, name? }):
|
|
1263
|
+
createReducer({ contract, name?, fold?, stateSchema? }): AIStoredReducer
|
|
1210
1264
|
```
|
|
1211
1265
|
|
|
1212
|
-
Builds
|
|
1266
|
+
Builds a normalized AI reducer for a compatible contract. Its `initialState`,
|
|
1267
|
+
`fold`, and server `session.state(reducer)` use `AIStoredState`. Client and
|
|
1268
|
+
React snapshots expose the inferred `AIState` view automatically.
|
|
1269
|
+
|
|
1270
|
+
`reducer.view(storedState)` derives confirmed `AIState` for server code.
|
|
1271
|
+
`reducer.fromView(state)` converts a confirmed view into stored state, including
|
|
1272
|
+
when migrating an application-owned hydration value. Keep the stored state and
|
|
1273
|
+
its log index together when hydrating; do not fold already-seen history twice.
|
|
1274
|
+
`fold` can customize the confirmed `AIState` transition, and `stateSchema`
|
|
1275
|
+
validates that view after restoration. Both apply only to the transcript.
|
|
1276
|
+
Model context has its own built-in fold and schema. Changed custom fold semantics
|
|
1277
|
+
still require a new `name`.
|
|
1278
|
+
|
|
1279
|
+
Stored state is versioned, serializable data. Shared payload records and
|
|
1280
|
+
inverse changes replace repeated tool values and complete recovery copies.
|
|
1281
|
+
The reducer caches its live view and builds the records when the stored fields
|
|
1282
|
+
are read or serialized. Treat state and views as immutable. No event or tool
|
|
1283
|
+
payload is truncated. `AIState`
|
|
1213
1284
|
contains `inbox`, `active`, `starting`, the latest command `receipt`, session lifecycle,
|
|
1214
1285
|
messages, generation status, pending approvals
|
|
1215
1286
|
and input, tool activity, compaction, usage, the last error, and
|
|
1216
|
-
`activeRequestId`, `activeResponseMessageId`,
|
|
1217
|
-
`
|
|
1287
|
+
`activeRequestId`, `activeRequestReason`, `activeRequestSourceGenerationId`, `activeResponseMessageId`,
|
|
1288
|
+
`responseOwners: Record<string, { generationId: string; requestId: string; attempt: number }>`, and terminal request and
|
|
1218
1289
|
generation ownership fences.
|
|
1219
1290
|
`activeRequestId` is the server-authorized generation request and fences
|
|
1220
1291
|
delayed requests before their generation starts. `activeResponseMessageId`
|
|
@@ -1223,7 +1294,7 @@ identifies the requested response until `activeGeneration` exists.
|
|
|
1223
1294
|
active, after a generation step completes while its response waits on tool,
|
|
1224
1295
|
approval, or input barriers, while paused, and after a generation fails while it awaits retry
|
|
1225
1296
|
or interruption. Response completion, interruption, supersession, retry, or a
|
|
1226
|
-
later generation clears or replaces it. `
|
|
1297
|
+
later generation clears or replaces it. `responseOwners` keeps the
|
|
1227
1298
|
latest generation owner for each response message, so late events from a
|
|
1228
1299
|
superseded owner cannot alter the projection. `terminalRequestIds` and
|
|
1229
1300
|
`terminalGenerations` preserve accepted completion, failure, interruption,
|
|
@@ -1242,7 +1313,7 @@ Checkpoint size and serialization cost still grow with retained progress.
|
|
|
1242
1313
|
Tool-input deltas update `input-streaming` parts with best-effort partial JSON
|
|
1243
1314
|
for display. The state stays `input-streaming` until a completed input arrives;
|
|
1244
1315
|
partial parsing does not authorize execution or change interruption handling.
|
|
1245
|
-
Extension events are ignored. The default reducer name is `a2.ai.state.
|
|
1316
|
+
Extension events are ignored. The default reducer name is `a2.ai.state.v21:records-v2`.
|
|
1246
1317
|
|
|
1247
1318
|
### `deriveUIMessages(history)` and `reduceAIState(state, event)`
|
|
1248
1319
|
|
|
@@ -1345,6 +1416,16 @@ The resolver runs once per generation and returns a valid policy or `false`.
|
|
|
1345
1416
|
It can await application reducers through `context.session.state(reducer)`, whose
|
|
1346
1417
|
default boundary is the captured prompt frontier. Model, instruction, compaction,
|
|
1347
1418
|
metadata, and custom-generation contexts expose state access and omit raw history.
|
|
1419
|
+
Callbacks receive prompt messages and request identity, without `state` or
|
|
1420
|
+
`execution` arguments. Model and instruction resolvers receive
|
|
1421
|
+
`{ event, messages, session, signal }`. Compaction and generation callbacks also
|
|
1422
|
+
receive `modelMessages`, including committed summaries. Custom transcript folds
|
|
1423
|
+
and schemas apply only to the transcript. `agent.reducer` and `createReducer()`
|
|
1424
|
+
continue to expose the full
|
|
1425
|
+
transcript view. To read it explicitly, load
|
|
1426
|
+
`context.session.state(agent.reducer)` and pass its `state` to
|
|
1427
|
+
`agent.reducer.view()`. The stored state also remains suitable for client
|
|
1428
|
+
hydration.
|
|
1348
1429
|
Static options are validated at construction; resolved options are validated
|
|
1349
1430
|
before that generation starts. Each session uses its own resolved value. `thresholdTokens` is an
|
|
1350
1431
|
optional positive safe integer. `instructions` adds to A2's internal summary
|
|
@@ -1365,10 +1446,20 @@ compaction configuration. Provider-executed tools, structured output, and forced
|
|
|
1365
1446
|
tool choices skip implicit compaction and reject explicit automatic policies.
|
|
1366
1447
|
Custom policies remain available for these configurations.
|
|
1367
1448
|
|
|
1368
|
-
The input estimate
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1449
|
+
The raw input estimate uses serialized bytes of messages, tools, and instructions,
|
|
1450
|
+
excluding provider options on messages and content parts. These options still reach
|
|
1451
|
+
the model unchanged. It is not a provider tokenizer. A preceding same-model
|
|
1452
|
+
completion anchors later growth to reported input usage, correcting both overestimates
|
|
1453
|
+
and underestimates. A smaller prompt, retry, compaction, or model change stops
|
|
1454
|
+
that calibration.
|
|
1455
|
+
|
|
1456
|
+
Completed generations record the raw `inputTokenEstimate` beside usage.
|
|
1457
|
+
Automatic `ai.compaction.requested` events also record the pre-compaction
|
|
1458
|
+
`inputTokenEstimate`, the calibrated `inputTokens` used for the decision, and
|
|
1459
|
+
`thresholdTokens`. Custom-policy requests omit these diagnostics. They add no
|
|
1460
|
+
model calls or database round trips.
|
|
1461
|
+
|
|
1462
|
+
The built-in summary adds one model request and preserves the request prefix for
|
|
1372
1463
|
caching. It does not invoke application generation callbacks, transforms, or tool
|
|
1373
1464
|
hooks. Both custom policy callbacks and `generate` receive `modelMessages`, the
|
|
1374
1465
|
model-ready prompt including summaries, alongside typed UI `messages`.
|
|
@@ -1376,7 +1467,14 @@ model-ready prompt including summaries, alongside typed UI `messages`.
|
|
|
1376
1467
|
Built-in summaries remain separate from application-typed messages. Render
|
|
1377
1468
|
`state.compaction?.status === 'running'` as current activity, and the durable
|
|
1378
1469
|
`ai.compaction.*` events as historical timeline markers. The conversation in
|
|
1379
|
-
`state.messages` remains intact.
|
|
1470
|
+
`state.messages` from the transcript reducer remains intact. Model execution
|
|
1471
|
+
uses a separate context checkpoint; existing compactions do not require a
|
|
1472
|
+
transcript read. Successful compaction is permanent, including across failure,
|
|
1473
|
+
interruption, retry, and recovery. Retry reruns the failed step from the latest
|
|
1474
|
+
committed context. Compaction defers while tool exchanges or A2-owned tool work
|
|
1475
|
+
remain unsettled, checked with one fresh coordinator state read on each candidate.
|
|
1476
|
+
Unresolved dead-lettered tool work also defers compaction until repair and
|
|
1477
|
+
settlement or session closure. `progress` controls durable batching with
|
|
1380
1478
|
`maxChunks` and `maxDelayMs`.
|
|
1381
1479
|
|
|
1382
1480
|
This entry point is server-only and resolves to a throwing browser stub.
|
|
@@ -1519,7 +1617,7 @@ each handler → the events those handlers append.
|
|
|
1519
1617
|
|
|
1520
1618
|
### The span catalogue
|
|
1521
1619
|
|
|
1522
|
-
|
|
1620
|
+
Five spans, all carrying `a2.contract` and `a2.session_id`:
|
|
1523
1621
|
|
|
1524
1622
|
| Span | Wraps |
|
|
1525
1623
|
| ----------- | ------------------------------------------------------ |
|
|
@@ -1527,6 +1625,7 @@ Four spans, all carrying `a2.contract` and `a2.session_id`:
|
|
|
1527
1625
|
| `a2.drain` | one drain pass over a session's backlog |
|
|
1528
1626
|
| `a2.event` | one claimed dispatch of one event |
|
|
1529
1627
|
| `a2.state` | one `state()` read: snapshot-plus-tail load + fold |
|
|
1628
|
+
| `a2.snapshot` | one background batch of checkpoint writes |
|
|
1530
1629
|
|
|
1531
1630
|
Attributes arrive in two waves: **at start** (passed to `span()`) and
|
|
1532
1631
|
**mid-span** (via `handle.setAttribute`; outcomes and counts aren't
|
|
@@ -1554,6 +1653,9 @@ alert on are all mid-span.
|
|
|
1554
1653
|
| `a2.state.snapshot` | `a2.state` | mid | `hit` \| `miss` \| `rejected` (schema guard discarded it) |
|
|
1555
1654
|
| `a2.state.folded` | `a2.state` | mid | events folded past the snapshot |
|
|
1556
1655
|
| `a2.state.index` | `a2.state` | mid | the frontier the returned state reflects |
|
|
1656
|
+
| `a2.snapshot.reducer` | `a2.snapshot` | start | the reducer's name |
|
|
1657
|
+
| `a2.snapshot.index` | `a2.snapshot` | start | greatest checkpoint index in the write batch |
|
|
1658
|
+
| `a2.snapshot.count` | `a2.snapshot` | start | number of checkpoints in the write batch |
|
|
1557
1659
|
|
|
1558
1660
|
Drain outcomes: `settled` means nothing actionable is left; `busy` means live
|
|
1559
1661
|
per-event claims remain; `stalled` means a caught failure needs a later retry.
|