experimental-a2 0.15.0 → 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.
Files changed (101) hide show
  1. package/CHANGELOG.md +73 -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-CrLYwNEx.js +2431 -0
  9. package/dist/ai-CrLYwNEx.js.map +1 -0
  10. package/dist/{ai-Cai-lCbj.d.ts → ai-HJ9fHfYI.d.ts} +72 -13
  11. package/dist/ai-HJ9fHfYI.d.ts.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 +755 -290
  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 +134 -43
  59. package/docs/reference/01-api.mdx +104 -27
  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 +0 -2
  74. package/src/ai-context-schema.ts +296 -0
  75. package/src/ai-context.ts +680 -0
  76. package/src/ai-control-server.ts +160 -131
  77. package/src/ai-control-state.ts +6 -0
  78. package/src/ai-control.ts +6 -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 +348 -0
  83. package/src/ai-server.ts +232 -334
  84. package/src/ai-stored-state.ts +484 -0
  85. package/src/ai.ts +252 -418
  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
package/src/ai.ts CHANGED
@@ -21,6 +21,7 @@ import {
21
21
  export type { PendingQueueCommand } from './ai-client-state.ts'
22
22
  import { applyControlChanges } from './ai-control-state.ts'
23
23
  import { initialControlState } from './ai-control.ts'
24
+ import { AI_EVENT_ID_PREFIX } from './ai-id.ts'
24
25
  import type {
25
26
  ControlCommit,
26
27
  ActiveTurn,
@@ -42,13 +43,23 @@ import {
42
43
  interruptUIMessage,
43
44
  pauseUIMessage,
44
45
  projectUIMessage,
45
- projectUIMessageStream,
46
46
  type UIMessageCursor,
47
47
  } from './ai-projector.ts'
48
48
  import { ambientToolScopeStorage } from './internal.ts'
49
+ import {
50
+ upsertMessage,
51
+ upsertResponse,
52
+ answerApproval,
53
+ approvalRequestedChunk,
54
+ toolResultChunk,
55
+ advanceProjection,
56
+ projectedMessage,
57
+ reduceToolActivity,
58
+ } from './ai-message-projection.ts'
49
59
  import { clientStateProjection, type Reducer } from './reducer.ts'
60
+ import { storedAIReducer, type AIStoredReducer } from './ai-stored-state.ts'
61
+ export type { AIStoredState, AIStoredReducer } from './ai-stored-state.ts'
50
62
  import {
51
- appendProgressBatch,
52
63
  flattenProgressBatches,
53
64
  progressBatches,
54
65
  type ProgressBatches,
@@ -85,7 +96,8 @@ export type GenerationRequestedPayload = {
85
96
  control?: { turnId: string; version: number }
86
97
  messageId: string
87
98
  reason: GenerationReason
88
- responseMessageId?: string
99
+ responseMessageId: string
100
+ sourceGenerationId?: string
89
101
  }
90
102
  export type RetryGenerationOptions = {
91
103
  messageId: string
@@ -162,6 +174,7 @@ export type ToolResultPayload = {
162
174
  toolMetadata?: unknown
163
175
  }
164
176
  export type ApprovalRequestedPayload = {
177
+ requestId: string
165
178
  messageId: string
166
179
  generationId: string
167
180
  approvalId: string
@@ -170,6 +183,7 @@ export type ApprovalRequestedPayload = {
170
183
  signature?: string
171
184
  }
172
185
  export type ApprovalRespondedPayload = {
186
+ requestId: string
173
187
  messageId: string
174
188
  generationId: string
175
189
  approvalId: string
@@ -177,6 +191,7 @@ export type ApprovalRespondedPayload = {
177
191
  reason?: string
178
192
  }
179
193
  export type InputRequestedPayload = {
194
+ requestId: string
180
195
  messageId: string
181
196
  generationId: string
182
197
  inputId: string
@@ -184,6 +199,7 @@ export type InputRequestedPayload = {
184
199
  question?: JSONValue
185
200
  }
186
201
  export type InputRespondedPayload = {
202
+ requestId: string
187
203
  messageId: string
188
204
  generationId: string
189
205
  inputId: string
@@ -208,6 +224,9 @@ export type CompactionRequestedPayload = {
208
224
  generationId: string
209
225
  throughMessageId: string
210
226
  throughIndex?: number
227
+ inputTokenEstimate?: number
228
+ inputTokens?: number
229
+ thresholdTokens?: number
211
230
  }
212
231
  export type CompactionCompletedPayload<M extends UIMessage> = {
213
232
  generationId: string
@@ -335,9 +354,14 @@ export type AIState<M extends UIMessage = UIMessage> = {
335
354
  status: AgentStatus
336
355
  activeGeneration: ActiveGeneration | null
337
356
  activeRequestId: string | null
357
+ activeRequestReason: GenerationReason | null
358
+ activeRequestSourceGenerationId: string | null
338
359
  activeResponseMessageId: string | null
339
360
  activeProjection: ActiveGenerationProjection<M> | null
340
- responseGenerationIds: Record<string, string>
361
+ responseOwners: Record<
362
+ string,
363
+ Pick<GenerationStartedPayload, 'generationId' | 'requestId' | 'attempt'>
364
+ >
341
365
  terminalRequestIds?: Record<string, true>
342
366
  terminalGenerations?: Record<
343
367
  string,
@@ -357,7 +381,7 @@ export type AgentDefinition<
357
381
  D extends EventDefs = AIEventDefs<M>,
358
382
  > = {
359
383
  readonly contract: Contract<D>
360
- readonly reducer: Reducer<D, AIState<M>>
384
+ readonly reducer: AIStoredReducer<D, AIState<M>>
361
385
  }
362
386
 
363
387
  export type AgentToolEventDefs = EventDefs &
@@ -625,9 +649,14 @@ export function createEvents<M extends UIMessage = UIMessage>(options?: {
625
649
  const parsed = validateMessage(value['message'], messageSchema)
626
650
  if (parsed.issues) return parsed
627
651
  if (
628
- (action === 'edit' &&
629
- (!Number.isSafeInteger(value['expectedRevision']) ||
630
- Number(value['expectedRevision']) < 0)) ||
652
+ (action === 'send' || action === 'steer') &&
653
+ (parsed.value.id.length === 0 ||
654
+ parsed.value.id.startsWith(AI_EVENT_ID_PREFIX))
655
+ )
656
+ return issue(
657
+ 'AI input IDs must be nonempty and outside the reserved internal prefix',
658
+ )
659
+ if (
631
660
  (action === 'steer' &&
632
661
  (!stringField(value, 'turnId') ||
633
662
  !optionalIndexField(value, 'lastSeenIndex'))) ||
@@ -762,7 +791,10 @@ export function createEvents<M extends UIMessage = UIMessage>(options?: {
762
791
  'ai.generation.requested',
763
792
  (value) =>
764
793
  stringField(value, 'messageId') &&
765
- optionalStringField(value, 'responseMessageId') &&
794
+ stringField(value, 'responseMessageId') &&
795
+ optionalStringField(value, 'sourceGenerationId') &&
796
+ (value['reason'] !== 'tool' ||
797
+ stringField(value, 'sourceGenerationId')) &&
766
798
  (value['reason'] === 'message' ||
767
799
  value['reason'] === 'tool' ||
768
800
  value['reason'] === 'input' ||
@@ -844,6 +876,7 @@ export function createEvents<M extends UIMessage = UIMessage>(options?: {
844
876
  'ai.approval.requested': jsonObject(
845
877
  'ai.approval.requested',
846
878
  (value) =>
879
+ stringField(value, 'requestId') &&
847
880
  stringField(value, 'messageId') &&
848
881
  stringField(value, 'generationId') &&
849
882
  stringField(value, 'approvalId') &&
@@ -855,6 +888,7 @@ export function createEvents<M extends UIMessage = UIMessage>(options?: {
855
888
  'ai.approval.responded': jsonObject(
856
889
  'ai.approval.responded',
857
890
  (value) =>
891
+ stringField(value, 'requestId') &&
858
892
  stringField(value, 'messageId') &&
859
893
  stringField(value, 'generationId') &&
860
894
  stringField(value, 'approvalId') &&
@@ -864,6 +898,7 @@ export function createEvents<M extends UIMessage = UIMessage>(options?: {
864
898
  'ai.input.requested': jsonObject(
865
899
  'ai.input.requested',
866
900
  (value) =>
901
+ stringField(value, 'requestId') &&
867
902
  stringField(value, 'messageId') &&
868
903
  stringField(value, 'generationId') &&
869
904
  stringField(value, 'inputId') &&
@@ -872,6 +907,7 @@ export function createEvents<M extends UIMessage = UIMessage>(options?: {
872
907
  'ai.input.responded': jsonObject(
873
908
  'ai.input.responded',
874
909
  (value) =>
910
+ stringField(value, 'requestId') &&
875
911
  stringField(value, 'messageId') &&
876
912
  stringField(value, 'generationId') &&
877
913
  stringField(value, 'inputId') &&
@@ -882,7 +918,10 @@ export function createEvents<M extends UIMessage = UIMessage>(options?: {
882
918
  (value) =>
883
919
  stringField(value, 'generationId') &&
884
920
  stringField(value, 'throughMessageId') &&
885
- optionalIndexField(value, 'throughIndex'),
921
+ optionalIndexField(value, 'throughIndex') &&
922
+ optionalIndexField(value, 'inputTokenEstimate') &&
923
+ optionalIndexField(value, 'inputTokens') &&
924
+ optionalIndexField(value, 'thresholdTokens'),
886
925
  ),
887
926
  'ai.compaction.completed': compactionPayloadSchema(messageSchema),
888
927
  'ai.model.metadata.requested': jsonObject(
@@ -919,9 +958,11 @@ const initialState = <M extends UIMessage>(): AIState<M> => ({
919
958
  status: 'idle',
920
959
  activeGeneration: null,
921
960
  activeRequestId: null,
961
+ activeRequestReason: null,
962
+ activeRequestSourceGenerationId: null,
922
963
  activeResponseMessageId: null,
923
964
  activeProjection: null,
924
- responseGenerationIds: {},
965
+ responseOwners: {},
925
966
  pendingApprovals: [],
926
967
  pendingInputs: [],
927
968
  tools: [],
@@ -931,62 +972,6 @@ const initialState = <M extends UIMessage>(): AIState<M> => ({
931
972
  error: null,
932
973
  })
933
974
 
934
- const upsertMessage = <M extends UIMessage>(messages: M[], message: M): M[] => {
935
- const index = messages.findIndex((candidate) => candidate.id === message.id)
936
- if (index === -1) return [...messages, message]
937
- const next = [...messages]
938
- next[index] = message
939
- return next
940
- }
941
-
942
- const upsertResponse = <M extends UIMessage>(options: {
943
- messages: M[]
944
- message: M
945
- inputId: string
946
- }): M[] => {
947
- const { messages, message, inputId } = options
948
- if (messages.some((item) => item.id === message.id))
949
- return upsertMessage(messages, message)
950
- const inputIndex = messages.findIndex((item) => item.id === inputId)
951
- if (inputIndex === -1) return [...messages, message]
952
- return [
953
- ...messages.slice(0, inputIndex + 1),
954
- message,
955
- ...messages.slice(inputIndex + 1),
956
- ]
957
- }
958
-
959
- const answerApproval = <M extends UIMessage>(
960
- messages: M[],
961
- response: ApprovalRespondedPayload,
962
- ): M[] =>
963
- messages.map((message) => {
964
- if (message.id !== response.messageId) return message
965
- let changed = false
966
- const parts = message.parts.map((part) => {
967
- const candidate = part as unknown
968
- if (
969
- !isRecord(candidate) ||
970
- candidate['state'] !== 'approval-requested' ||
971
- !isRecord(candidate['approval']) ||
972
- candidate['approval']['id'] !== response.approvalId
973
- ) {
974
- return part
975
- }
976
- changed = true
977
- return {
978
- ...part,
979
- state: 'approval-responded',
980
- approval: {
981
- ...candidate['approval'],
982
- approved: response.approved,
983
- ...(response.reason === undefined ? {} : { reason: response.reason }),
984
- },
985
- } as typeof part
986
- })
987
- return changed ? ({ ...message, parts } as M) : message
988
- })
989
-
990
975
  const projectMessageChunks = <M extends UIMessage>(
991
976
  messages: M[],
992
977
  messageId: string,
@@ -998,90 +983,6 @@ const projectMessageChunks = <M extends UIMessage>(
998
983
  : message,
999
984
  )
1000
985
 
1001
- const approvalRequestedChunk = (
1002
- payload: ApprovalRequestedPayload,
1003
- ): UIMessageChunk => ({
1004
- type: 'tool-approval-request',
1005
- approvalId: payload.approvalId,
1006
- toolCallId: payload.toolCallId,
1007
- ...(payload.isAutomatic === undefined
1008
- ? {}
1009
- : { isAutomatic: payload.isAutomatic }),
1010
- ...(payload.signature === undefined ? {} : { signature: payload.signature }),
1011
- })
1012
-
1013
- const approvalRespondedChunk = (
1014
- payload: ApprovalRespondedPayload,
1015
- ): UIMessageChunk => ({
1016
- type: 'tool-approval-response',
1017
- approvalId: payload.approvalId,
1018
- approved: payload.approved,
1019
- ...(payload.reason === undefined ? {} : { reason: payload.reason }),
1020
- })
1021
-
1022
- const toolResultChunk = (payload: ToolResultPayload): UIMessageChunk => {
1023
- if (payload.phase === 'input') {
1024
- return {
1025
- type: 'tool-input-error',
1026
- toolCallId: payload.toolCallId,
1027
- toolName: payload.toolName ?? 'unknown',
1028
- input: payload.rawInput ?? payload.input,
1029
- errorText: payload.error ?? 'Invalid tool input',
1030
- ...(payload.dynamic === undefined ? {} : { dynamic: payload.dynamic }),
1031
- ...(payload.providerExecuted === undefined
1032
- ? {}
1033
- : { providerExecuted: payload.providerExecuted }),
1034
- ...(payload.providerMetadata === undefined
1035
- ? {}
1036
- : { providerMetadata: payload.providerMetadata }),
1037
- ...(payload.toolMetadata === undefined
1038
- ? {}
1039
- : { toolMetadata: payload.toolMetadata }),
1040
- } as UIMessageChunk
1041
- }
1042
- if (payload.denied === true) {
1043
- return {
1044
- type: 'tool-output-denied',
1045
- toolCallId: payload.toolCallId,
1046
- } as UIMessageChunk
1047
- }
1048
- if (payload.error !== undefined) {
1049
- return {
1050
- type: 'tool-output-error',
1051
- toolCallId: payload.toolCallId,
1052
- errorText: payload.error,
1053
- ...(payload.dynamic === undefined ? {} : { dynamic: payload.dynamic }),
1054
- ...(payload.providerExecuted === undefined
1055
- ? {}
1056
- : { providerExecuted: payload.providerExecuted }),
1057
- ...(payload.providerMetadata === undefined
1058
- ? {}
1059
- : { providerMetadata: payload.providerMetadata }),
1060
- ...(payload.toolMetadata === undefined
1061
- ? {}
1062
- : { toolMetadata: payload.toolMetadata }),
1063
- } as UIMessageChunk
1064
- }
1065
- return {
1066
- type: 'tool-output-available',
1067
- toolCallId: payload.toolCallId,
1068
- output: payload.output,
1069
- ...(payload.preliminary === undefined
1070
- ? {}
1071
- : { preliminary: payload.preliminary }),
1072
- ...(payload.dynamic === undefined ? {} : { dynamic: payload.dynamic }),
1073
- ...(payload.providerExecuted === undefined
1074
- ? {}
1075
- : { providerExecuted: payload.providerExecuted }),
1076
- ...(payload.providerMetadata === undefined
1077
- ? {}
1078
- : { providerMetadata: payload.providerMetadata }),
1079
- ...(payload.toolMetadata === undefined
1080
- ? {}
1081
- : { toolMetadata: payload.toolMetadata }),
1082
- } as UIMessageChunk
1083
- }
1084
-
1085
986
  const waitingStatus = <M extends UIMessage>(state: AIState<M>): AgentStatus => {
1086
987
  if (state.pendingApprovals.length > 0 || state.pendingInputs.length > 0) {
1087
988
  return 'waiting'
@@ -1101,49 +1002,16 @@ const lifecycleStatus = <M extends UIMessage>(
1101
1002
  const isStaleGeneration = <M extends UIMessage>(
1102
1003
  state: AIState<M>,
1103
1004
  messageId: string,
1104
- generationId: string,
1005
+ owner: { generationId: string; requestId: string },
1105
1006
  ): boolean => {
1106
- const current = state.responseGenerationIds[messageId]
1107
- return current !== undefined && current !== generationId
1108
- }
1109
-
1110
- const projectedMessageStream = <M extends UIMessage>(
1111
- projection: ActiveGenerationProjection<M>,
1112
- ): { message: M; cursor: UIMessageCursor } => {
1113
- const projectionEvents = flattenProgressBatches(projection.batches)
1114
- for (const event of projection.toolEvents) {
1115
- if (event.type === 'ai.tool.result') {
1116
- projectionEvents.push({
1117
- index: event.index,
1118
- chunks: [toolResultChunk(event.payload)],
1119
- })
1120
- }
1121
- }
1122
- for (const event of projection.approvalEvents) {
1123
- projectionEvents.push({
1124
- index: event.index,
1125
- chunks: [
1126
- event.type === 'ai.approval.requested'
1127
- ? approvalRequestedChunk(event.payload)
1128
- : approvalRespondedChunk(event.payload),
1129
- ],
1130
- })
1131
- }
1132
- return projectUIMessageStream({
1133
- messageId: projection.responseMessageId,
1134
- ...(projection.baseMessage === undefined
1135
- ? {}
1136
- : { base: projection.baseMessage }),
1137
- chunks: projectionEvents
1138
- .toSorted((left, right) => left.index - right.index)
1139
- .flatMap((event) => event.chunks),
1140
- })
1007
+ const current = state.responseOwners[messageId]
1008
+ return (
1009
+ current !== undefined &&
1010
+ (current.generationId !== owner.generationId ||
1011
+ current.requestId !== owner.requestId)
1012
+ )
1141
1013
  }
1142
1014
 
1143
- const projectedMessage = <M extends UIMessage>(
1144
- projection: ActiveGenerationProjection<M>,
1145
- ): M => projectedMessageStream(projection).message
1146
-
1147
1015
  const projectedResponse = <M extends UIMessage>(options: {
1148
1016
  state: AIState<M>
1149
1017
  projection: ActiveGenerationProjection<M>
@@ -1159,86 +1027,6 @@ const projectedResponse = <M extends UIMessage>(options: {
1159
1027
  : message
1160
1028
  }
1161
1029
 
1162
- const reduceToolActivity = (
1163
- tools: ToolActivity[],
1164
- event:
1165
- | { type: 'ai.tool.called'; payload: ToolCalledPayload }
1166
- | { type: 'ai.tool.result'; payload: ToolResultPayload },
1167
- ): ToolActivity[] => {
1168
- if (event.type === 'ai.tool.called') {
1169
- const payload = event.payload
1170
- const { providerMetadata, ...called } = payload
1171
- const activity: ToolActivity = {
1172
- ...called,
1173
- ...(providerMetadata === undefined
1174
- ? {}
1175
- : { callProviderMetadata: providerMetadata }),
1176
- status: 'running',
1177
- }
1178
- return [
1179
- ...tools.filter(
1180
- (candidate) =>
1181
- candidate.generationId !== payload.generationId ||
1182
- candidate.toolCallId !== payload.toolCallId,
1183
- ),
1184
- activity,
1185
- ]
1186
- }
1187
-
1188
- const payload = event.payload
1189
- const existing = tools.find(
1190
- (candidate) =>
1191
- candidate.generationId === payload.generationId &&
1192
- candidate.toolCallId === payload.toolCallId,
1193
- )
1194
- const toolName = payload.toolName ?? existing?.toolName
1195
- const input = payload.input === undefined ? existing?.input : payload.input
1196
- const dynamic = payload.dynamic ?? existing?.dynamic
1197
- const providerExecuted =
1198
- payload.providerExecuted ?? existing?.providerExecuted
1199
- const toolMetadata = payload.toolMetadata ?? existing?.toolMetadata
1200
- const resultProviderMetadata =
1201
- payload.providerMetadata ?? existing?.resultProviderMetadata
1202
- const activity: ToolActivity = {
1203
- requestId: payload.requestId,
1204
- messageId: payload.messageId,
1205
- generationId: payload.generationId,
1206
- toolCallId: payload.toolCallId,
1207
- ...(toolName === undefined ? {} : { toolName }),
1208
- ...(input === undefined ? {} : { input }),
1209
- ...(payload.rawInput === undefined ? {} : { rawInput: payload.rawInput }),
1210
- ...(payload.output === undefined ? {} : { output: payload.output }),
1211
- ...(payload.error === undefined ? {} : { error: payload.error }),
1212
- ...(payload.preliminary === undefined
1213
- ? {}
1214
- : { preliminary: payload.preliminary }),
1215
- ...(payload.phase === undefined ? {} : { phase: payload.phase }),
1216
- ...(dynamic === undefined ? {} : { dynamic }),
1217
- ...(providerExecuted === undefined ? {} : { providerExecuted }),
1218
- ...(existing?.callProviderMetadata === undefined
1219
- ? {}
1220
- : { callProviderMetadata: existing.callProviderMetadata }),
1221
- ...(resultProviderMetadata === undefined ? {} : { resultProviderMetadata }),
1222
- ...(toolMetadata === undefined ? {} : { toolMetadata }),
1223
- ...(existing?.title === undefined ? {} : { title: existing.title }),
1224
- status: payload.denied
1225
- ? 'denied'
1226
- : payload.error
1227
- ? 'failed'
1228
- : payload.preliminary
1229
- ? 'running'
1230
- : 'completed',
1231
- }
1232
- return [
1233
- ...tools.filter(
1234
- (candidate) =>
1235
- candidate.generationId !== payload.generationId ||
1236
- candidate.toolCallId !== payload.toolCallId,
1237
- ),
1238
- activity,
1239
- ]
1240
- }
1241
-
1242
1030
  const projectedTools = <M extends UIMessage>(
1243
1031
  projection: ActiveGenerationProjection<M>,
1244
1032
  ): ToolActivity[] =>
@@ -1251,7 +1039,7 @@ const restoreGenerationBase = <M extends UIMessage>(
1251
1039
  state: AIState<M>,
1252
1040
  generationId: string,
1253
1041
  responseMessageId: string,
1254
- reason: 'interrupted' | 'superseded',
1042
+ reason: 'interrupted' | 'superseded' | 'failed',
1255
1043
  ): AIState<M> => {
1256
1044
  const projection =
1257
1045
  state.activeProjection?.generationId === generationId
@@ -1360,19 +1148,13 @@ const interruptGenerationProjection = <M extends UIMessage>(
1360
1148
  }
1361
1149
  }
1362
1150
 
1363
- const generationRequestId = (generationId: string): string | undefined => {
1364
- const marker = ':generation:'
1365
- const markerIndex = generationId.lastIndexOf(marker)
1366
- return markerIndex === -1 ? undefined : generationId.slice(0, markerIndex)
1367
- }
1368
-
1369
1151
  const terminalGenerationReason = <M extends UIMessage>(
1370
1152
  state: AIState<M>,
1371
1153
  generationId: string,
1154
+ requestId?: string,
1372
1155
  ): 'completed' | 'failed' | 'interrupted' | 'superseded' | undefined => {
1373
1156
  const terminal = state.terminalGenerations?.[generationId]
1374
1157
  if (terminal !== undefined) return terminal
1375
- const requestId = generationRequestId(generationId)
1376
1158
  return requestId !== undefined &&
1377
1159
  state.terminalRequestIds?.[requestId] === true
1378
1160
  ? 'interrupted'
@@ -1429,6 +1211,8 @@ export function reduceAIState<M extends UIMessage>(
1429
1211
  ...visible,
1430
1212
  status,
1431
1213
  activeRequestId: commit.view.requestId,
1214
+ activeRequestReason: commit.view.requestReason,
1215
+ activeRequestSourceGenerationId: commit.view.sourceGenerationId,
1432
1216
  activeResponseMessageId: commit.view.responseMessageId,
1433
1217
  }
1434
1218
  }
@@ -1548,6 +1332,8 @@ export function reduceAIState<M extends UIMessage>(
1548
1332
  : state.compaction,
1549
1333
  activeGeneration: null,
1550
1334
  activeRequestId: null,
1335
+ activeRequestReason: null,
1336
+ activeRequestSourceGenerationId: null,
1551
1337
  activeResponseMessageId: null,
1552
1338
  activeProjection: null,
1553
1339
  }
@@ -1564,17 +1350,20 @@ export function reduceAIState<M extends UIMessage>(
1564
1350
  }
1565
1351
  case 'ai.message.completed': {
1566
1352
  const { messageId } = event.payload as MessageCompletedPayload
1567
- const generationId = state.responseGenerationIds[messageId]
1353
+ const owner = state.responseOwners[messageId]
1354
+ const generationId = owner?.generationId
1355
+ const requestId = owner?.requestId
1568
1356
  if (
1569
1357
  generationId === undefined ||
1570
- terminalGenerationReason(state, generationId) !== undefined
1358
+ terminalGenerationReason(state, generationId, requestId) !== undefined
1571
1359
  ) {
1572
1360
  return state
1573
1361
  }
1574
- const requestId = generationRequestId(generationId)
1575
1362
  return {
1576
1363
  ...state,
1577
1364
  activeRequestId: null,
1365
+ activeRequestReason: null,
1366
+ activeRequestSourceGenerationId: null,
1578
1367
  activeResponseMessageId: null,
1579
1368
  activeProjection:
1580
1369
  state.activeProjection?.generationId === generationId
@@ -1597,18 +1386,10 @@ export function reduceAIState<M extends UIMessage>(
1597
1386
  case 'ai.generation.requested': {
1598
1387
  const payload = event.payload as GenerationRequestedPayload
1599
1388
  const schedulesToolContinuation = payload.reason === 'tool'
1600
- const responseMessageId =
1601
- payload.responseMessageId ??
1602
- (payload.reason === 'message'
1603
- ? `${payload.messageId}:assistant`
1604
- : payload.messageId)
1605
- const ownerGenerationId =
1606
- state.responseGenerationIds[responseMessageId] ??
1607
- state.activeGeneration?.generationId
1608
- const ownerRequestId =
1609
- ownerGenerationId === undefined
1610
- ? undefined
1611
- : generationRequestId(ownerGenerationId)
1389
+ const responseMessageId = payload.responseMessageId
1390
+ const owner = state.responseOwners[responseMessageId]
1391
+ const ownerGenerationId = owner?.generationId
1392
+ const ownerRequestId = owner?.requestId
1612
1393
  const requestId = (event as { id?: string }).id
1613
1394
  if (
1614
1395
  requestId !== undefined &&
@@ -1621,30 +1402,35 @@ export function reduceAIState<M extends UIMessage>(
1621
1402
  payload.reason === 'message' ||
1622
1403
  (payload.reason === 'tool'
1623
1404
  ? ownerGenerationId !== undefined &&
1624
- requestId === `ai.generate:tools:${ownerGenerationId}` &&
1405
+ payload.sourceGenerationId === ownerGenerationId &&
1625
1406
  state.activeRequestId === ownerRequestId &&
1626
1407
  state.status !== 'failed'
1627
1408
  : payload.reason === 'retry'
1628
1409
  ? state.status === 'failed'
1629
1410
  : ownerGenerationId !== undefined &&
1630
- requestId?.startsWith(
1631
- `ai.generate:input:${encodeURIComponent(responseMessageId)}:${encodeURIComponent(ownerGenerationId)}:`,
1632
- ) === true &&
1411
+ payload.sourceGenerationId === ownerGenerationId &&
1633
1412
  state.activeRequestId === ownerRequestId &&
1634
1413
  state.status !== 'failed')
1635
1414
  if (!acceptsRequest) return state
1415
+ const base =
1416
+ payload.reason === 'retry' &&
1417
+ state.activeProjection &&
1418
+ state.activeProjection.completionIndex === undefined
1419
+ ? restoreGenerationBase(
1420
+ state,
1421
+ state.activeProjection.generationId,
1422
+ responseMessageId,
1423
+ 'failed',
1424
+ )
1425
+ : state
1636
1426
  return {
1637
- ...state,
1638
- messages:
1639
- payload.reason === 'retry' && payload.responseMessageId !== undefined
1640
- ? state.messages.filter(
1641
- (message) => message.id !== payload.responseMessageId,
1642
- )
1643
- : state.messages,
1427
+ ...base,
1644
1428
  activeProjection:
1645
1429
  payload.reason === 'retry' ? null : state.activeProjection,
1646
1430
  status: schedulesToolContinuation ? state.status : 'generating',
1647
1431
  activeRequestId: requestId ?? null,
1432
+ activeRequestReason: payload.reason,
1433
+ activeRequestSourceGenerationId: payload.sourceGenerationId ?? null,
1648
1434
  activeResponseMessageId: responseMessageId,
1649
1435
  error: schedulesToolContinuation ? state.error : null,
1650
1436
  }
@@ -1653,7 +1439,11 @@ export function reduceAIState<M extends UIMessage>(
1653
1439
  const payload = event.payload as GenerationStartedPayload
1654
1440
  if (
1655
1441
  state.terminalRequestIds?.[payload.requestId] === true ||
1656
- terminalGenerationReason(state, payload.generationId) !== undefined
1442
+ terminalGenerationReason(
1443
+ state,
1444
+ payload.generationId,
1445
+ payload.requestId,
1446
+ ) !== undefined
1657
1447
  ) {
1658
1448
  return state
1659
1449
  }
@@ -1663,31 +1453,26 @@ export function reduceAIState<M extends UIMessage>(
1663
1453
  ) {
1664
1454
  return state
1665
1455
  }
1666
- const currentOwner =
1667
- state.responseGenerationIds[payload.responseMessageId]
1456
+ const currentOwner = state.responseOwners[payload.responseMessageId]
1668
1457
  const currentTerminal =
1669
1458
  currentOwner === undefined
1670
1459
  ? undefined
1671
- : terminalGenerationReason(state, currentOwner)
1460
+ : terminalGenerationReason(
1461
+ state,
1462
+ currentOwner.generationId,
1463
+ currentOwner.requestId,
1464
+ )
1672
1465
  if (
1673
1466
  currentTerminal === 'completed' ||
1674
1467
  currentTerminal === 'interrupted'
1675
1468
  ) {
1676
1469
  return state
1677
1470
  }
1678
- const marker = ':generation:'
1679
- const currentAttempt = currentOwner?.startsWith(
1680
- `${payload.requestId}${marker}`,
1681
- )
1682
- ? Number(currentOwner.slice(`${payload.requestId}${marker}`.length))
1683
- : undefined
1684
1471
  if (
1685
- currentAttempt !== undefined &&
1686
- Number.isFinite(currentAttempt) &&
1687
- currentAttempt >= payload.attempt
1688
- ) {
1472
+ currentOwner?.requestId === payload.requestId &&
1473
+ currentOwner.attempt >= payload.attempt
1474
+ )
1689
1475
  return state
1690
- }
1691
1476
  const activeGeneration = state.activeGeneration
1692
1477
  const replacesActiveAttempt =
1693
1478
  activeGeneration !== null &&
@@ -1711,9 +1496,13 @@ export function reduceAIState<M extends UIMessage>(
1711
1496
  status: 'generating',
1712
1497
  activeGeneration: payload,
1713
1498
  activeResponseMessageId: null,
1714
- responseGenerationIds: {
1715
- ...baseState.responseGenerationIds,
1716
- [payload.responseMessageId]: payload.generationId,
1499
+ responseOwners: {
1500
+ ...baseState.responseOwners,
1501
+ [payload.responseMessageId]: {
1502
+ generationId: payload.generationId,
1503
+ requestId: payload.requestId,
1504
+ attempt: payload.attempt,
1505
+ },
1717
1506
  },
1718
1507
  activeProjection: {
1719
1508
  generationId: payload.generationId,
@@ -1732,83 +1521,61 @@ export function reduceAIState<M extends UIMessage>(
1732
1521
  case 'ai.generation.progress': {
1733
1522
  const payload = event.payload as GenerationProgressPayload
1734
1523
  if (state.pausedGenerationIds?.[payload.generationId]) return state
1735
- if (terminalGenerationReason(state, payload.generationId) !== undefined) {
1524
+ if (
1525
+ terminalGenerationReason(
1526
+ state,
1527
+ payload.generationId,
1528
+ payload.requestId,
1529
+ ) !== undefined
1530
+ ) {
1736
1531
  return state
1737
1532
  }
1738
1533
  const activeProjection = state.activeProjection
1739
1534
  if (
1740
1535
  state.activeGeneration?.generationId !== payload.generationId ||
1536
+ state.activeGeneration.requestId !== payload.requestId ||
1741
1537
  activeProjection?.generationId !== payload.generationId
1742
1538
  ) {
1743
1539
  return state
1744
1540
  }
1745
- const ordered =
1746
- event.index > (activeProjection.batches.tail.at(-1)?.index ?? -1)
1747
- const projection: ActiveGenerationProjection<M> = {
1748
- ...activeProjection,
1749
- batches: ordered
1750
- ? appendProgressBatch({
1751
- batches: activeProjection.batches,
1752
- batch: { index: event.index, chunks: payload.chunks },
1753
- })
1754
- : progressBatches(
1755
- [
1756
- ...flattenProgressBatches(activeProjection.batches).filter(
1757
- (batch) => batch.index !== event.index,
1758
- ),
1759
- { index: event.index, chunks: payload.chunks },
1760
- ].toSorted((left, right) => left.index - right.index),
1761
- ),
1762
- }
1763
- const base = state.messages.find(
1764
- (message) => message.id === projection.responseMessageId,
1765
- )
1766
- const incremental =
1767
- base !== undefined &&
1768
- activeProjection.cursor != null &&
1769
- event.index > activeProjection.cursor.throughIndex
1770
- const projected = incremental
1771
- ? projectUIMessageStream({
1772
- messageId: projection.responseMessageId,
1773
- base,
1774
- cursor: activeProjection.cursor!.stream,
1775
- chunks: payload.chunks,
1776
- })
1777
- : projectedMessageStream(projection)
1778
- projection.cursor = {
1779
- throughIndex: incremental
1780
- ? event.index
1781
- : Math.max(
1782
- event.index,
1783
- projection.batches.tail.at(-1)?.index ?? 0,
1784
- projection.toolEvents.reduce(
1785
- (index, entry) => Math.max(index, entry.index),
1786
- 0,
1787
- ),
1788
- projection.approvalEvents.reduce(
1789
- (index, entry) => Math.max(index, entry.index),
1790
- 0,
1791
- ),
1792
- ),
1793
- stream: projected.cursor,
1794
- }
1541
+ const progress = advanceProjection({
1542
+ projection: activeProjection,
1543
+ base: state.messages.find(
1544
+ (message) => message.id === activeProjection.responseMessageId,
1545
+ ),
1546
+ chunks: payload.chunks,
1547
+ index: event.index,
1548
+ })
1795
1549
  return {
1796
1550
  ...state,
1797
1551
  messages: upsertResponse({
1798
1552
  messages: state.messages,
1799
- message: projected.message,
1553
+ message: progress.message,
1800
1554
  inputId: state.activeGeneration.messageId,
1801
1555
  }),
1802
- activeProjection: projection,
1556
+ activeProjection: {
1557
+ ...activeProjection,
1558
+ batches: progress.batches,
1559
+ cursor: progress.cursor,
1560
+ },
1803
1561
  status: 'generating',
1804
1562
  }
1805
1563
  }
1806
1564
  case 'ai.generation.completed': {
1807
1565
  const payload = event.payload as GenerationCompletedPayload
1808
- if (terminalGenerationReason(state, payload.generationId) !== undefined) {
1566
+ if (
1567
+ terminalGenerationReason(
1568
+ state,
1569
+ payload.generationId,
1570
+ payload.requestId,
1571
+ ) !== undefined
1572
+ ) {
1809
1573
  return state
1810
1574
  }
1811
- if (state.activeGeneration?.generationId !== payload.generationId) {
1575
+ if (
1576
+ state.activeGeneration?.generationId !== payload.generationId ||
1577
+ state.activeGeneration.requestId !== payload.requestId
1578
+ ) {
1812
1579
  return state
1813
1580
  }
1814
1581
  const usage = payload.usage
@@ -1832,17 +1599,26 @@ export function reduceAIState<M extends UIMessage>(
1832
1599
  }
1833
1600
  case 'ai.generation.failed': {
1834
1601
  const payload = event.payload as GenerationFailedPayload
1835
- if (terminalGenerationReason(state, payload.generationId) !== undefined) {
1602
+ if (
1603
+ terminalGenerationReason(
1604
+ state,
1605
+ payload.generationId,
1606
+ payload.requestId,
1607
+ ) !== undefined
1608
+ ) {
1836
1609
  return state
1837
1610
  }
1838
1611
  if (
1839
- state.activeGeneration?.generationId !== payload.generationId &&
1612
+ (state.activeGeneration?.generationId !== payload.generationId ||
1613
+ state.activeGeneration.requestId !== payload.requestId) &&
1840
1614
  !(
1841
1615
  payload.stepLimit === true &&
1842
- state.responseGenerationIds[payload.responseMessageId] ===
1616
+ state.activeRequestReason === 'tool' &&
1617
+ state.responseOwners[payload.responseMessageId]?.requestId ===
1618
+ payload.requestId &&
1619
+ state.responseOwners[payload.responseMessageId]?.generationId ===
1843
1620
  payload.generationId &&
1844
- state.activeRequestId ===
1845
- `ai.generate:tools:${payload.generationId}` &&
1621
+ state.activeRequestSourceGenerationId === payload.generationId &&
1846
1622
  state.activeResponseMessageId === payload.responseMessageId
1847
1623
  )
1848
1624
  ) {
@@ -1912,9 +1688,13 @@ export function reduceAIState<M extends UIMessage>(
1912
1688
  }
1913
1689
  case 'ai.message.interrupted': {
1914
1690
  const payload = event.payload as MessageInterruptedPayload
1915
- const currentGenerationId = state.responseGenerationIds[payload.messageId]
1691
+ const owner = state.responseOwners[payload.messageId]
1692
+ const currentGenerationId = owner?.generationId
1916
1693
  const targetRequestId =
1917
- payload.requestId ?? generationRequestId(payload.generationId)
1694
+ payload.requestId ??
1695
+ (owner?.generationId === payload.generationId
1696
+ ? owner?.requestId
1697
+ : undefined)
1918
1698
  const ownsRequestPhase =
1919
1699
  payload.requestId !== undefined &&
1920
1700
  state.activeGeneration === null &&
@@ -1925,7 +1705,7 @@ export function reduceAIState<M extends UIMessage>(
1925
1705
  !ownsRequestPhase &&
1926
1706
  (payload.generationId !== undefined
1927
1707
  ? currentGenerationId !== payload.generationId
1928
- : generationRequestId(currentGenerationId) !== payload.requestId)
1708
+ : owner?.requestId !== payload.requestId)
1929
1709
  ) {
1930
1710
  return state
1931
1711
  }
@@ -1940,7 +1720,11 @@ export function reduceAIState<M extends UIMessage>(
1940
1720
  const currentTerminal =
1941
1721
  currentGenerationId === undefined
1942
1722
  ? undefined
1943
- : terminalGenerationReason(state, currentGenerationId)
1723
+ : terminalGenerationReason(
1724
+ state,
1725
+ currentGenerationId,
1726
+ owner?.requestId,
1727
+ )
1944
1728
  if (
1945
1729
  currentTerminal === 'completed' ||
1946
1730
  currentTerminal === 'interrupted' ||
@@ -1970,6 +1754,8 @@ export function reduceAIState<M extends UIMessage>(
1970
1754
  ...baseState,
1971
1755
  status: 'idle',
1972
1756
  activeRequestId: null,
1757
+ activeRequestReason: null,
1758
+ activeRequestSourceGenerationId: null,
1973
1759
  activeResponseMessageId: null,
1974
1760
  activeProjection: null,
1975
1761
  terminalRequestIds: {
@@ -1990,7 +1776,7 @@ export function reduceAIState<M extends UIMessage>(
1990
1776
  const targetGenerationId =
1991
1777
  payload.generationId ??
1992
1778
  (currentGenerationId !== undefined &&
1993
- generationRequestId(currentGenerationId) === payload.requestId
1779
+ owner?.requestId === payload.requestId
1994
1780
  ? currentGenerationId
1995
1781
  : undefined)
1996
1782
  if (targetGenerationId === undefined) return state
@@ -2019,7 +1805,8 @@ export function reduceAIState<M extends UIMessage>(
2019
1805
  currentGenerationId === targetGenerationId &&
2020
1806
  (state.activeRequestId === null ||
2021
1807
  state.activeRequestId === targetRequestId ||
2022
- state.activeRequestId === `ai.generate:tools:${targetGenerationId}`)
1808
+ (state.activeRequestReason === 'tool' &&
1809
+ state.activeRequestSourceGenerationId === targetGenerationId))
2023
1810
  const resolvesCurrentGeneration =
2024
1811
  currentGenerationId === targetGenerationId &&
2025
1812
  currentTerminal === undefined
@@ -2041,6 +1828,8 @@ export function reduceAIState<M extends UIMessage>(
2041
1828
  const next = {
2042
1829
  ...baseState,
2043
1830
  activeRequestId: null,
1831
+ activeRequestReason: null,
1832
+ activeRequestSourceGenerationId: null,
2044
1833
  activeResponseMessageId: null,
2045
1834
  activeProjection: null,
2046
1835
  messages:
@@ -2114,6 +1903,8 @@ export function reduceAIState<M extends UIMessage>(
2114
1903
  const next = {
2115
1904
  ...baseState,
2116
1905
  activeRequestId: null,
1906
+ activeRequestReason: null,
1907
+ activeRequestSourceGenerationId: null,
2117
1908
  activeResponseMessageId: null,
2118
1909
  terminalRequestIds: {
2119
1910
  ...baseState.terminalRequestIds,
@@ -2135,8 +1926,12 @@ export function reduceAIState<M extends UIMessage>(
2135
1926
  const payload = event.payload as ToolCalledPayload
2136
1927
  const projection = state.activeProjection
2137
1928
  if (
2138
- isStaleGeneration(state, payload.messageId, payload.generationId) ||
2139
- terminalGenerationReason(state, payload.generationId) !== undefined
1929
+ isStaleGeneration(state, payload.messageId, payload) ||
1930
+ terminalGenerationReason(
1931
+ state,
1932
+ payload.generationId,
1933
+ payload.requestId,
1934
+ ) !== undefined
2140
1935
  ) {
2141
1936
  return state
2142
1937
  }
@@ -2181,6 +1976,7 @@ export function reduceAIState<M extends UIMessage>(
2181
1976
  const terminalReason = terminalGenerationReason(
2182
1977
  state,
2183
1978
  payload.generationId,
1979
+ payload.requestId,
2184
1980
  )
2185
1981
  const completesFailedTool =
2186
1982
  terminalReason === 'failed' &&
@@ -2194,7 +1990,7 @@ export function reduceAIState<M extends UIMessage>(
2194
1990
  activity.status === 'running',
2195
1991
  )
2196
1992
  if (
2197
- isStaleGeneration(state, payload.messageId, payload.generationId) ||
1993
+ isStaleGeneration(state, payload.messageId, payload) ||
2198
1994
  (terminalReason !== undefined && !completesFailedTool)
2199
1995
  ) {
2200
1996
  return state
@@ -2266,8 +2062,12 @@ export function reduceAIState<M extends UIMessage>(
2266
2062
  const payload = event.payload as ApprovalRequestedPayload
2267
2063
  const projection = state.activeProjection
2268
2064
  if (
2269
- isStaleGeneration(state, payload.messageId, payload.generationId) ||
2270
- terminalGenerationReason(state, payload.generationId) !== undefined
2065
+ isStaleGeneration(state, payload.messageId, payload) ||
2066
+ terminalGenerationReason(
2067
+ state,
2068
+ payload.generationId,
2069
+ payload.requestId,
2070
+ ) !== undefined
2271
2071
  ) {
2272
2072
  return state
2273
2073
  }
@@ -2329,8 +2129,12 @@ export function reduceAIState<M extends UIMessage>(
2329
2129
  case 'ai.approval.responded': {
2330
2130
  const payload = event.payload as ApprovalRespondedPayload
2331
2131
  if (
2332
- isStaleGeneration(state, payload.messageId, payload.generationId) ||
2333
- terminalGenerationReason(state, payload.generationId) !== undefined
2132
+ isStaleGeneration(state, payload.messageId, payload) ||
2133
+ terminalGenerationReason(
2134
+ state,
2135
+ payload.generationId,
2136
+ payload.requestId,
2137
+ ) !== undefined
2334
2138
  ) {
2335
2139
  return state
2336
2140
  }
@@ -2398,8 +2202,12 @@ export function reduceAIState<M extends UIMessage>(
2398
2202
  case 'ai.input.requested': {
2399
2203
  const payload = event.payload as InputRequestedPayload
2400
2204
  if (
2401
- isStaleGeneration(state, payload.messageId, payload.generationId) ||
2402
- terminalGenerationReason(state, payload.generationId) !== undefined
2205
+ isStaleGeneration(state, payload.messageId, payload) ||
2206
+ terminalGenerationReason(
2207
+ state,
2208
+ payload.generationId,
2209
+ payload.requestId,
2210
+ ) !== undefined
2403
2211
  ) {
2404
2212
  return state
2405
2213
  }
@@ -2420,8 +2228,12 @@ export function reduceAIState<M extends UIMessage>(
2420
2228
  case 'ai.input.responded': {
2421
2229
  const payload = event.payload as InputRespondedPayload
2422
2230
  if (
2423
- isStaleGeneration(state, payload.messageId, payload.generationId) ||
2424
- terminalGenerationReason(state, payload.generationId) !== undefined
2231
+ isStaleGeneration(state, payload.messageId, payload) ||
2232
+ terminalGenerationReason(
2233
+ state,
2234
+ payload.generationId,
2235
+ payload.requestId,
2236
+ ) !== undefined
2425
2237
  ) {
2426
2238
  return state
2427
2239
  }
@@ -2455,13 +2267,18 @@ export function reduceAIState<M extends UIMessage>(
2455
2267
  ) {
2456
2268
  return state
2457
2269
  }
2270
+ const compaction: CompactionState<M> = {
2271
+ status: 'completed',
2272
+ ...payload,
2273
+ throughIndex:
2274
+ payload.throughIndex ?? state.activeProjection!.promptThroughIndex!,
2275
+ }
2458
2276
  return {
2459
2277
  ...state,
2460
- compaction: {
2461
- status: 'completed',
2462
- ...payload,
2463
- throughIndex:
2464
- payload.throughIndex ?? state.activeProjection!.promptThroughIndex!,
2278
+ compaction,
2279
+ activeProjection: state.activeProjection && {
2280
+ ...state.activeProjection,
2281
+ baseCompaction: compaction,
2465
2282
  },
2466
2283
  }
2467
2284
  }
@@ -2483,22 +2300,31 @@ export function deriveUIMessages<M extends UIMessage = UIMessage>(
2483
2300
  export function createReducer<D extends EventDefs>(options: {
2484
2301
  contract: Contract<D>
2485
2302
  name?: string
2486
- }): Reducer<D, AIState<AIMessageFrom<D>>> {
2303
+ fold?: Reducer<D, AIState<AIMessageFrom<D>>>['fold']
2304
+ stateSchema?: StandardSchemaV1<unknown, AIState<AIMessageFrom<D>>>
2305
+ }): AIStoredReducer<D, AIState<AIMessageFrom<D>>> {
2487
2306
  type M = AIMessageFrom<D>
2488
2307
  const reducer = options.contract
2489
2308
  .reducer({
2490
- name: options.name ?? 'a2.ai.state.v17',
2309
+ name: options.name ?? 'a2.ai.state.v21',
2491
2310
  initialState: initialState<M>(),
2311
+ ...(options.stateSchema === undefined
2312
+ ? {}
2313
+ : { stateSchema: options.stateSchema }),
2492
2314
  })
2493
- .fold((state, event) => reduceAIState(state, event))
2494
- return { ...reducer, [clientStateProjection]: projectAIClientState }
2315
+ .fold(options.fold ?? ((state, event) => reduceAIState(state, event)))
2316
+ return storedAIReducer({
2317
+ ...reducer,
2318
+ [clientStateProjection]: projectAIClientState,
2319
+ })
2495
2320
  }
2496
2321
 
2497
2322
  type CommandInput<C> = { type: 'ai.control.requested'; payload: C; id?: string }
2498
2323
  type ControlInput<
2499
2324
  M extends UIMessage,
2500
2325
  A extends ControlCommand<M>['action'],
2501
- > = CommandInput<Extract<ControlCommand<M>, { action: A }>>
2326
+ > = CommandInput<Extract<ControlCommand<M>, { action: A }>> &
2327
+ (A extends 'send' | 'steer' ? { id: string } : {})
2502
2328
 
2503
2329
  export type AIInputs = {
2504
2330
  message<M extends UIMessage>(
@@ -2515,7 +2341,6 @@ export type AIInputs = {
2515
2341
  }): ControlInput<UIMessage, 'send-now'>[]
2516
2342
  edit<M extends UIMessage>(options: {
2517
2343
  message: M
2518
- expectedRevision: number
2519
2344
  }): ControlInput<M, 'edit'>[]
2520
2345
  move(options: {
2521
2346
  inputId: string
@@ -2560,7 +2385,7 @@ const messageInput = <M extends UIMessage>(
2560
2385
  ): ControlInput<M, 'send'>[] => [
2561
2386
  {
2562
2387
  type: 'ai.control.requested',
2563
- id: `ai.message:${message.id}`,
2388
+ id: message.id,
2564
2389
  payload: {
2565
2390
  action: 'send',
2566
2391
  message,
@@ -2579,7 +2404,13 @@ export const inputs: AIInputs = {
2579
2404
  move: (options) => commandInput({ action: 'move', ...options }),
2580
2405
  },
2581
2406
  stop: (options) => commandInput({ action: 'stop', ...options }),
2582
- steer: (options) => commandInput({ action: 'steer', ...options }),
2407
+ steer: (options) => [
2408
+ {
2409
+ type: 'ai.control.requested',
2410
+ id: options.message.id,
2411
+ payload: { action: 'steer', ...options },
2412
+ },
2413
+ ],
2583
2414
  pause: (options) => commandInput({ action: 'pause', ...options }),
2584
2415
  resume: () => commandInput({ action: 'resume' }),
2585
2416
  message<M extends UIMessage>(message: M, options?: { generate?: boolean }) {
@@ -2591,19 +2422,19 @@ export const inputs: AIInputs = {
2591
2422
  approval: (response) => [
2592
2423
  {
2593
2424
  ...commandInput({ action: 'approval', response })[0]!,
2594
- id: `ai.approval:${encodeURIComponent(response.messageId)}:${encodeURIComponent(response.generationId)}:${encodeURIComponent(response.approvalId)}:response`,
2425
+ id: `ai.approval:${encodeURIComponent(response.requestId)}:${encodeURIComponent(response.messageId)}:${encodeURIComponent(response.generationId)}:${encodeURIComponent(response.approvalId)}:response`,
2595
2426
  },
2596
2427
  ],
2597
2428
  input: (response) => [
2598
2429
  {
2599
2430
  ...commandInput({ action: 'input', response })[0]!,
2600
- id: `ai.input:${encodeURIComponent(response.messageId)}:${encodeURIComponent(response.generationId)}:${encodeURIComponent(response.inputId)}:response`,
2431
+ id: `ai.input:${encodeURIComponent(response.requestId)}:${encodeURIComponent(response.messageId)}:${encodeURIComponent(response.generationId)}:${encodeURIComponent(response.inputId)}:response`,
2601
2432
  },
2602
2433
  ],
2603
2434
  requestInput: (request) => [
2604
2435
  {
2605
2436
  ...commandInput({ action: 'request-input', request })[0]!,
2606
- id: `ai.input:${encodeURIComponent(request.messageId)}:${encodeURIComponent(request.generationId)}:${encodeURIComponent(request.inputId)}:request`,
2437
+ id: `ai.input:${encodeURIComponent(request.requestId)}:${encodeURIComponent(request.messageId)}:${encodeURIComponent(request.generationId)}:${encodeURIComponent(request.inputId)}:request`,
2607
2438
  },
2608
2439
  ],
2609
2440
  retry: (request) => [
@@ -2674,14 +2505,17 @@ export function agent<
2674
2505
  })
2675
2506
  const reducer = agentContract
2676
2507
  .reducer({
2677
- name: options.reducerName ?? 'a2.ai.state.v17',
2508
+ name: options.reducerName ?? 'a2.ai.state.v21',
2678
2509
  initialState: initialState<M>(),
2679
2510
  })
2680
2511
  .fold((state, event) => reduceAIState(state, event))
2681
2512
 
2682
2513
  const definition = {
2683
2514
  contract: agentContract,
2684
- reducer: { ...reducer, [clientStateProjection]: projectAIClientState<M> },
2515
+ reducer: storedAIReducer({
2516
+ ...reducer,
2517
+ [clientStateProjection]: projectAIClientState<M>,
2518
+ }),
2685
2519
  }
2686
2520
  return definition as unknown as AgentDefinition<M, AIEventDefs<M> & E>
2687
2521
  }