experimental-a2 0.15.0 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/dist/actor-client.d.ts +1 -1
  3. package/dist/{actor-BfQSE0KC.d.ts → actor-ohPC-81x.d.ts} +5 -5
  4. package/dist/{actor-BfQSE0KC.d.ts.map → actor-ohPC-81x.d.ts.map} +1 -1
  5. package/dist/actor-react.d.ts +3 -3
  6. package/dist/actor.d.ts +1 -1
  7. package/dist/actor.js +1 -1
  8. package/dist/{ai-Cai-lCbj.d.ts → ai-CrEf6p_W.d.ts} +73 -14
  9. package/dist/ai-CrEf6p_W.d.ts.map +1 -0
  10. package/dist/ai-DgOBltJ_.js +2451 -0
  11. package/dist/ai-DgOBltJ_.js.map +1 -0
  12. package/dist/ai-server.d.ts +4 -5
  13. package/dist/ai-server.d.ts.map +1 -1
  14. package/dist/ai-server.js +788 -296
  15. package/dist/ai-server.js.map +1 -1
  16. package/dist/ai.d.ts +2 -2
  17. package/dist/ai.js +1 -1999
  18. package/dist/client-BYzHjkwU.js.map +1 -1
  19. package/dist/{client-BAEABRZB.d.ts → client-CzyacQpJ.d.ts} +9 -9
  20. package/dist/client-CzyacQpJ.d.ts.map +1 -0
  21. package/dist/client.d.ts +1 -1
  22. package/dist/idempotent-id-BRJVeylj.js +13 -0
  23. package/dist/idempotent-id-BRJVeylj.js.map +1 -0
  24. package/dist/index.d.ts +3 -3
  25. package/dist/index.js +1 -12
  26. package/dist/otel.d.ts +1 -1
  27. package/dist/react.d.ts +13 -13
  28. package/dist/react.d.ts.map +1 -1
  29. package/dist/react.js.map +1 -1
  30. package/dist/reducer-DEMjEY_O.js.map +1 -1
  31. package/dist/{reducer-BcS9VDKC.d.ts → reducer-otzHjuJj.d.ts} +3 -3
  32. package/dist/{reducer-BcS9VDKC.d.ts.map → reducer-otzHjuJj.d.ts.map} +1 -1
  33. package/dist/scheduler-qstash.d.ts +2 -2
  34. package/dist/scheduler-qstash.js +1 -1
  35. package/dist/scheduler-vercel.d.ts +2 -2
  36. package/dist/scheduler-vercel.js +1 -1
  37. package/dist/{server-CjJSGcF7.js → server-BD5ckxZb.js} +39 -26
  38. package/dist/server-BD5ckxZb.js.map +1 -0
  39. package/dist/{server-Bp5Nd1pF.d.ts → server-DbD7IJVK.d.ts} +5 -5
  40. package/dist/{server-Bp5Nd1pF.d.ts.map → server-DbD7IJVK.d.ts.map} +1 -1
  41. package/dist/server.d.ts +3 -3
  42. package/dist/server.js +1 -1
  43. package/dist/{store-D_yhNdPz.d.ts → store-DyZM6fS5.d.ts} +3 -2
  44. package/dist/{store-D_yhNdPz.d.ts.map → store-DyZM6fS5.d.ts.map} +1 -1
  45. package/dist/store-N8PXxDAS.js.map +1 -1
  46. package/dist/store-memory.d.ts +1 -1
  47. package/dist/store-postgres.d.ts +1 -1
  48. package/dist/store-redis-http.d.ts +1 -1
  49. package/dist/store-redis-http.js +1 -1
  50. package/dist/{store-redis-notify-D2EI6gwX.js → store-redis-notify-BVLUfI3j.js} +14 -13
  51. package/dist/store-redis-notify-BVLUfI3j.js.map +1 -0
  52. package/dist/store-redis.d.ts +1 -1
  53. package/dist/store-redis.js +1 -1
  54. package/dist/store-sqlite.d.ts +1 -1
  55. package/dist/{telemetry-CpeclqB2.d.ts → telemetry-B5jzpy6y.d.ts} +2 -2
  56. package/dist/telemetry-B5jzpy6y.d.ts.map +1 -0
  57. package/docs/concepts/04-state.mdx +9 -4
  58. package/docs/guides/06-ai-agents.mdx +149 -48
  59. package/docs/reference/01-api.mdx +110 -28
  60. package/examples/playground/app/agent/[agentId]/agent-client.tsx +20 -28
  61. package/examples/playground/app/agent/[agentId]/agent-queue.test.tsx +123 -0
  62. package/examples/playground/app/agent/[agentId]/agent-queue.tsx +37 -43
  63. package/examples/playground/app/agent/[agentId]/compaction-panel.tsx +44 -24
  64. package/examples/playground/app/agent/compaction-timeline.test.ts +47 -7
  65. package/examples/playground/app/agent/compaction-timeline.ts +13 -3
  66. package/examples/playground/app/chat/[chatId]/chat-client.tsx +1 -5
  67. package/examples/playground/app/chat/[chatId]/session.ts +17 -2
  68. package/examples/playground/app/chat/model.test.ts +4 -4
  69. package/examples/playground/app/chat/model.ts +18 -6
  70. package/examples/playground/app/chat/server.ts +6 -3
  71. package/examples/playground/package.json +3 -1
  72. package/package.json +1 -1
  73. package/src/ai-client-state.ts +36 -4
  74. package/src/ai-context-schema.ts +296 -0
  75. package/src/ai-context.ts +680 -0
  76. package/src/ai-control-server.ts +183 -135
  77. package/src/ai-control-state.ts +6 -0
  78. package/src/ai-control.ts +8 -7
  79. package/src/ai-coordinator.ts +92 -38
  80. package/src/ai-id.ts +6 -0
  81. package/src/ai-input-tokens.ts +75 -0
  82. package/src/ai-message-projection.ts +474 -0
  83. package/src/ai-server.ts +247 -336
  84. package/src/ai-stored-state.ts +484 -0
  85. package/src/ai.ts +255 -538
  86. package/src/client.ts +21 -15
  87. package/src/react.ts +25 -19
  88. package/src/reducer.ts +2 -1
  89. package/src/server.ts +73 -56
  90. package/src/store-redis-core.ts +15 -17
  91. package/src/store.ts +1 -0
  92. package/src/telemetry.ts +2 -1
  93. package/dist/ai-Cai-lCbj.d.ts.map +0 -1
  94. package/dist/ai-control-CcD4hh3y.js +0 -119
  95. package/dist/ai-control-CcD4hh3y.js.map +0 -1
  96. package/dist/ai.js.map +0 -1
  97. package/dist/client-BAEABRZB.d.ts.map +0 -1
  98. package/dist/index.js.map +0 -1
  99. package/dist/server-CjJSGcF7.js.map +0 -1
  100. package/dist/store-redis-notify-D2EI6gwX.js.map +0 -1
  101. package/dist/telemetry-CpeclqB2.d.ts.map +0 -1
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,27 @@ 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
+ projectedTools,
57
+ restoreGenerationBase,
58
+ stopGenerationTools,
59
+ interruptGenerationProjection,
60
+ projectedMessage,
61
+ reduceToolActivity,
62
+ } from './ai-message-projection.ts'
49
63
  import { clientStateProjection, type Reducer } from './reducer.ts'
64
+ import { storedAIReducer, type AIStoredReducer } from './ai-stored-state.ts'
65
+ export type { AIStoredState, AIStoredReducer } from './ai-stored-state.ts'
50
66
  import {
51
- appendProgressBatch,
52
67
  flattenProgressBatches,
53
68
  progressBatches,
54
69
  type ProgressBatches,
@@ -85,7 +100,8 @@ export type GenerationRequestedPayload = {
85
100
  control?: { turnId: string; version: number }
86
101
  messageId: string
87
102
  reason: GenerationReason
88
- responseMessageId?: string
103
+ responseMessageId: string
104
+ sourceGenerationId?: string
89
105
  }
90
106
  export type RetryGenerationOptions = {
91
107
  messageId: string
@@ -162,6 +178,7 @@ export type ToolResultPayload = {
162
178
  toolMetadata?: unknown
163
179
  }
164
180
  export type ApprovalRequestedPayload = {
181
+ requestId: string
165
182
  messageId: string
166
183
  generationId: string
167
184
  approvalId: string
@@ -170,6 +187,7 @@ export type ApprovalRequestedPayload = {
170
187
  signature?: string
171
188
  }
172
189
  export type ApprovalRespondedPayload = {
190
+ requestId: string
173
191
  messageId: string
174
192
  generationId: string
175
193
  approvalId: string
@@ -177,6 +195,7 @@ export type ApprovalRespondedPayload = {
177
195
  reason?: string
178
196
  }
179
197
  export type InputRequestedPayload = {
198
+ requestId: string
180
199
  messageId: string
181
200
  generationId: string
182
201
  inputId: string
@@ -184,6 +203,7 @@ export type InputRequestedPayload = {
184
203
  question?: JSONValue
185
204
  }
186
205
  export type InputRespondedPayload = {
206
+ requestId: string
187
207
  messageId: string
188
208
  generationId: string
189
209
  inputId: string
@@ -208,6 +228,9 @@ export type CompactionRequestedPayload = {
208
228
  generationId: string
209
229
  throughMessageId: string
210
230
  throughIndex?: number
231
+ inputTokenEstimate?: number
232
+ inputTokens?: number
233
+ thresholdTokens?: number
211
234
  }
212
235
  export type CompactionCompletedPayload<M extends UIMessage> = {
213
236
  generationId: string
@@ -335,9 +358,14 @@ export type AIState<M extends UIMessage = UIMessage> = {
335
358
  status: AgentStatus
336
359
  activeGeneration: ActiveGeneration | null
337
360
  activeRequestId: string | null
361
+ activeRequestReason: GenerationReason | null
362
+ activeRequestSourceGenerationId: string | null
338
363
  activeResponseMessageId: string | null
339
364
  activeProjection: ActiveGenerationProjection<M> | null
340
- responseGenerationIds: Record<string, string>
365
+ responseOwners: Record<
366
+ string,
367
+ Pick<GenerationStartedPayload, 'generationId' | 'requestId' | 'attempt'>
368
+ >
341
369
  terminalRequestIds?: Record<string, true>
342
370
  terminalGenerations?: Record<
343
371
  string,
@@ -357,7 +385,7 @@ export type AgentDefinition<
357
385
  D extends EventDefs = AIEventDefs<M>,
358
386
  > = {
359
387
  readonly contract: Contract<D>
360
- readonly reducer: Reducer<D, AIState<M>>
388
+ readonly reducer: AIStoredReducer<D, AIState<M>>
361
389
  }
362
390
 
363
391
  export type AgentToolEventDefs = EventDefs &
@@ -625,9 +653,14 @@ export function createEvents<M extends UIMessage = UIMessage>(options?: {
625
653
  const parsed = validateMessage(value['message'], messageSchema)
626
654
  if (parsed.issues) return parsed
627
655
  if (
628
- (action === 'edit' &&
629
- (!Number.isSafeInteger(value['expectedRevision']) ||
630
- Number(value['expectedRevision']) < 0)) ||
656
+ (action === 'send' || action === 'steer') &&
657
+ (parsed.value.id.length === 0 ||
658
+ parsed.value.id.startsWith(AI_EVENT_ID_PREFIX))
659
+ )
660
+ return issue(
661
+ 'AI input IDs must be nonempty and outside the reserved internal prefix',
662
+ )
663
+ if (
631
664
  (action === 'steer' &&
632
665
  (!stringField(value, 'turnId') ||
633
666
  !optionalIndexField(value, 'lastSeenIndex'))) ||
@@ -762,7 +795,10 @@ export function createEvents<M extends UIMessage = UIMessage>(options?: {
762
795
  'ai.generation.requested',
763
796
  (value) =>
764
797
  stringField(value, 'messageId') &&
765
- optionalStringField(value, 'responseMessageId') &&
798
+ stringField(value, 'responseMessageId') &&
799
+ optionalStringField(value, 'sourceGenerationId') &&
800
+ (value['reason'] !== 'tool' ||
801
+ stringField(value, 'sourceGenerationId')) &&
766
802
  (value['reason'] === 'message' ||
767
803
  value['reason'] === 'tool' ||
768
804
  value['reason'] === 'input' ||
@@ -844,6 +880,7 @@ export function createEvents<M extends UIMessage = UIMessage>(options?: {
844
880
  'ai.approval.requested': jsonObject(
845
881
  'ai.approval.requested',
846
882
  (value) =>
883
+ stringField(value, 'requestId') &&
847
884
  stringField(value, 'messageId') &&
848
885
  stringField(value, 'generationId') &&
849
886
  stringField(value, 'approvalId') &&
@@ -855,6 +892,7 @@ export function createEvents<M extends UIMessage = UIMessage>(options?: {
855
892
  'ai.approval.responded': jsonObject(
856
893
  'ai.approval.responded',
857
894
  (value) =>
895
+ stringField(value, 'requestId') &&
858
896
  stringField(value, 'messageId') &&
859
897
  stringField(value, 'generationId') &&
860
898
  stringField(value, 'approvalId') &&
@@ -864,6 +902,7 @@ export function createEvents<M extends UIMessage = UIMessage>(options?: {
864
902
  'ai.input.requested': jsonObject(
865
903
  'ai.input.requested',
866
904
  (value) =>
905
+ stringField(value, 'requestId') &&
867
906
  stringField(value, 'messageId') &&
868
907
  stringField(value, 'generationId') &&
869
908
  stringField(value, 'inputId') &&
@@ -872,6 +911,7 @@ export function createEvents<M extends UIMessage = UIMessage>(options?: {
872
911
  'ai.input.responded': jsonObject(
873
912
  'ai.input.responded',
874
913
  (value) =>
914
+ stringField(value, 'requestId') &&
875
915
  stringField(value, 'messageId') &&
876
916
  stringField(value, 'generationId') &&
877
917
  stringField(value, 'inputId') &&
@@ -882,7 +922,10 @@ export function createEvents<M extends UIMessage = UIMessage>(options?: {
882
922
  (value) =>
883
923
  stringField(value, 'generationId') &&
884
924
  stringField(value, 'throughMessageId') &&
885
- optionalIndexField(value, 'throughIndex'),
925
+ optionalIndexField(value, 'throughIndex') &&
926
+ optionalIndexField(value, 'inputTokenEstimate') &&
927
+ optionalIndexField(value, 'inputTokens') &&
928
+ optionalIndexField(value, 'thresholdTokens'),
886
929
  ),
887
930
  'ai.compaction.completed': compactionPayloadSchema(messageSchema),
888
931
  'ai.model.metadata.requested': jsonObject(
@@ -919,9 +962,11 @@ const initialState = <M extends UIMessage>(): AIState<M> => ({
919
962
  status: 'idle',
920
963
  activeGeneration: null,
921
964
  activeRequestId: null,
965
+ activeRequestReason: null,
966
+ activeRequestSourceGenerationId: null,
922
967
  activeResponseMessageId: null,
923
968
  activeProjection: null,
924
- responseGenerationIds: {},
969
+ responseOwners: {},
925
970
  pendingApprovals: [],
926
971
  pendingInputs: [],
927
972
  tools: [],
@@ -931,62 +976,6 @@ const initialState = <M extends UIMessage>(): AIState<M> => ({
931
976
  error: null,
932
977
  })
933
978
 
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
979
  const projectMessageChunks = <M extends UIMessage>(
991
980
  messages: M[],
992
981
  messageId: string,
@@ -998,90 +987,6 @@ const projectMessageChunks = <M extends UIMessage>(
998
987
  : message,
999
988
  )
1000
989
 
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
990
  const waitingStatus = <M extends UIMessage>(state: AIState<M>): AgentStatus => {
1086
991
  if (state.pendingApprovals.length > 0 || state.pendingInputs.length > 0) {
1087
992
  return 'waiting'
@@ -1101,49 +1006,16 @@ const lifecycleStatus = <M extends UIMessage>(
1101
1006
  const isStaleGeneration = <M extends UIMessage>(
1102
1007
  state: AIState<M>,
1103
1008
  messageId: string,
1104
- generationId: string,
1009
+ owner: { generationId: string; requestId: string },
1105
1010
  ): 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
- })
1011
+ const current = state.responseOwners[messageId]
1012
+ return (
1013
+ current !== undefined &&
1014
+ (current.generationId !== owner.generationId ||
1015
+ current.requestId !== owner.requestId)
1016
+ )
1141
1017
  }
1142
1018
 
1143
- const projectedMessage = <M extends UIMessage>(
1144
- projection: ActiveGenerationProjection<M>,
1145
- ): M => projectedMessageStream(projection).message
1146
-
1147
1019
  const projectedResponse = <M extends UIMessage>(options: {
1148
1020
  state: AIState<M>
1149
1021
  projection: ActiveGenerationProjection<M>
@@ -1159,220 +1031,13 @@ const projectedResponse = <M extends UIMessage>(options: {
1159
1031
  : message
1160
1032
  }
1161
1033
 
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
- const projectedTools = <M extends UIMessage>(
1243
- projection: ActiveGenerationProjection<M>,
1244
- ): ToolActivity[] =>
1245
- projection.toolEvents.reduce(
1246
- (tools, event) => reduceToolActivity(tools, event),
1247
- projection.baseTools,
1248
- )
1249
-
1250
- const restoreGenerationBase = <M extends UIMessage>(
1251
- state: AIState<M>,
1252
- generationId: string,
1253
- responseMessageId: string,
1254
- reason: 'interrupted' | 'superseded',
1255
- ): AIState<M> => {
1256
- const projection =
1257
- state.activeProjection?.generationId === generationId
1258
- ? state.activeProjection
1259
- : null
1260
- return {
1261
- ...state,
1262
- messages:
1263
- projection?.baseMessage === undefined
1264
- ? state.messages.filter((message) => message.id !== responseMessageId)
1265
- : upsertMessage(state.messages, projection.baseMessage),
1266
- tools:
1267
- projection === null
1268
- ? state.tools.filter(
1269
- (activity) => activity.generationId !== generationId,
1270
- )
1271
- : projection.baseTools,
1272
- pendingApprovals: state.pendingApprovals.filter(
1273
- (approval) => approval.generationId !== generationId,
1274
- ),
1275
- pendingInputs: state.pendingInputs.filter(
1276
- (input) => input.generationId !== generationId,
1277
- ),
1278
- activeGeneration: null,
1279
- activeProjection: null,
1280
- terminalGenerations: {
1281
- ...state.terminalGenerations,
1282
- [generationId]: reason,
1283
- },
1284
- compaction:
1285
- projection === null
1286
- ? state.compaction?.generationId === generationId
1287
- ? null
1288
- : state.compaction
1289
- : (projection.baseCompaction ?? null),
1290
- }
1291
- }
1292
-
1293
- const stopGenerationTools = (
1294
- tools: ToolActivity[],
1295
- generationId: string | undefined,
1296
- responseMessageId: string,
1297
- error: string,
1298
- ): ToolActivity[] =>
1299
- tools.map((activity) =>
1300
- activity.status === 'running' &&
1301
- (generationId === undefined
1302
- ? activity.messageId === responseMessageId
1303
- : activity.generationId === generationId)
1304
- ? { ...activity, status: 'failed', error, preliminary: false }
1305
- : activity,
1306
- )
1307
-
1308
- const interruptGenerationProjection = <M extends UIMessage>(
1309
- state: AIState<M>,
1310
- generationId: string,
1311
- responseMessageId: string,
1312
- cutoff: number,
1313
- ): AIState<M> | null => {
1314
- const projection =
1315
- state.activeProjection?.generationId === generationId
1316
- ? state.activeProjection
1317
- : null
1318
- if (projection === null) return null
1319
- const retained: ActiveGenerationProjection<M> = {
1320
- ...projection,
1321
- batches: progressBatches(
1322
- flattenProgressBatches(projection.batches).filter(
1323
- (batch) => batch.index <= cutoff,
1324
- ),
1325
- ),
1326
- toolEvents: projection.toolEvents.filter(
1327
- (toolEvent) => toolEvent.index <= cutoff,
1328
- ),
1329
- approvalEvents: projection.approvalEvents.filter(
1330
- (approvalEvent) => approvalEvent.index <= cutoff,
1331
- ),
1332
- }
1333
- const shouldProject =
1334
- retained.baseMessage !== undefined || retained.batches.length > 0
1335
- const restored = restoreGenerationBase(
1336
- state,
1337
- generationId,
1338
- responseMessageId,
1339
- 'interrupted',
1340
- )
1341
- return {
1342
- ...restored,
1343
- messages: shouldProject
1344
- ? upsertMessage(
1345
- state.messages,
1346
- interruptUIMessage(projectedMessage(retained)),
1347
- )
1348
- : restored.messages,
1349
- tools: stopGenerationTools(
1350
- projectedTools(retained),
1351
- generationId,
1352
- responseMessageId,
1353
- 'Tool execution was interrupted.',
1354
- ),
1355
- usage:
1356
- projection.completionIndex === undefined ||
1357
- projection.completionIndex <= cutoff
1358
- ? restored.usage
1359
- : restored.usage.filter((entry) => entry.generationId !== generationId),
1360
- }
1361
- }
1362
-
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
1034
  const terminalGenerationReason = <M extends UIMessage>(
1370
1035
  state: AIState<M>,
1371
1036
  generationId: string,
1037
+ requestId?: string,
1372
1038
  ): 'completed' | 'failed' | 'interrupted' | 'superseded' | undefined => {
1373
1039
  const terminal = state.terminalGenerations?.[generationId]
1374
1040
  if (terminal !== undefined) return terminal
1375
- const requestId = generationRequestId(generationId)
1376
1041
  return requestId !== undefined &&
1377
1042
  state.terminalRequestIds?.[requestId] === true
1378
1043
  ? 'interrupted'
@@ -1429,6 +1094,8 @@ export function reduceAIState<M extends UIMessage>(
1429
1094
  ...visible,
1430
1095
  status,
1431
1096
  activeRequestId: commit.view.requestId,
1097
+ activeRequestReason: commit.view.requestReason,
1098
+ activeRequestSourceGenerationId: commit.view.sourceGenerationId,
1432
1099
  activeResponseMessageId: commit.view.responseMessageId,
1433
1100
  }
1434
1101
  }
@@ -1548,6 +1215,8 @@ export function reduceAIState<M extends UIMessage>(
1548
1215
  : state.compaction,
1549
1216
  activeGeneration: null,
1550
1217
  activeRequestId: null,
1218
+ activeRequestReason: null,
1219
+ activeRequestSourceGenerationId: null,
1551
1220
  activeResponseMessageId: null,
1552
1221
  activeProjection: null,
1553
1222
  }
@@ -1564,17 +1233,20 @@ export function reduceAIState<M extends UIMessage>(
1564
1233
  }
1565
1234
  case 'ai.message.completed': {
1566
1235
  const { messageId } = event.payload as MessageCompletedPayload
1567
- const generationId = state.responseGenerationIds[messageId]
1236
+ const owner = state.responseOwners[messageId]
1237
+ const generationId = owner?.generationId
1238
+ const requestId = owner?.requestId
1568
1239
  if (
1569
1240
  generationId === undefined ||
1570
- terminalGenerationReason(state, generationId) !== undefined
1241
+ terminalGenerationReason(state, generationId, requestId) !== undefined
1571
1242
  ) {
1572
1243
  return state
1573
1244
  }
1574
- const requestId = generationRequestId(generationId)
1575
1245
  return {
1576
1246
  ...state,
1577
1247
  activeRequestId: null,
1248
+ activeRequestReason: null,
1249
+ activeRequestSourceGenerationId: null,
1578
1250
  activeResponseMessageId: null,
1579
1251
  activeProjection:
1580
1252
  state.activeProjection?.generationId === generationId
@@ -1597,18 +1269,10 @@ export function reduceAIState<M extends UIMessage>(
1597
1269
  case 'ai.generation.requested': {
1598
1270
  const payload = event.payload as GenerationRequestedPayload
1599
1271
  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)
1272
+ const responseMessageId = payload.responseMessageId
1273
+ const owner = state.responseOwners[responseMessageId]
1274
+ const ownerGenerationId = owner?.generationId
1275
+ const ownerRequestId = owner?.requestId
1612
1276
  const requestId = (event as { id?: string }).id
1613
1277
  if (
1614
1278
  requestId !== undefined &&
@@ -1621,30 +1285,35 @@ export function reduceAIState<M extends UIMessage>(
1621
1285
  payload.reason === 'message' ||
1622
1286
  (payload.reason === 'tool'
1623
1287
  ? ownerGenerationId !== undefined &&
1624
- requestId === `ai.generate:tools:${ownerGenerationId}` &&
1288
+ payload.sourceGenerationId === ownerGenerationId &&
1625
1289
  state.activeRequestId === ownerRequestId &&
1626
1290
  state.status !== 'failed'
1627
1291
  : payload.reason === 'retry'
1628
1292
  ? state.status === 'failed'
1629
1293
  : ownerGenerationId !== undefined &&
1630
- requestId?.startsWith(
1631
- `ai.generate:input:${encodeURIComponent(responseMessageId)}:${encodeURIComponent(ownerGenerationId)}:`,
1632
- ) === true &&
1294
+ payload.sourceGenerationId === ownerGenerationId &&
1633
1295
  state.activeRequestId === ownerRequestId &&
1634
1296
  state.status !== 'failed')
1635
1297
  if (!acceptsRequest) return state
1298
+ const base =
1299
+ payload.reason === 'retry' &&
1300
+ state.activeProjection &&
1301
+ state.activeProjection.completionIndex === undefined
1302
+ ? restoreGenerationBase(
1303
+ state,
1304
+ state.activeProjection.generationId,
1305
+ responseMessageId,
1306
+ 'failed',
1307
+ )
1308
+ : state
1636
1309
  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,
1310
+ ...base,
1644
1311
  activeProjection:
1645
1312
  payload.reason === 'retry' ? null : state.activeProjection,
1646
1313
  status: schedulesToolContinuation ? state.status : 'generating',
1647
1314
  activeRequestId: requestId ?? null,
1315
+ activeRequestReason: payload.reason,
1316
+ activeRequestSourceGenerationId: payload.sourceGenerationId ?? null,
1648
1317
  activeResponseMessageId: responseMessageId,
1649
1318
  error: schedulesToolContinuation ? state.error : null,
1650
1319
  }
@@ -1653,7 +1322,11 @@ export function reduceAIState<M extends UIMessage>(
1653
1322
  const payload = event.payload as GenerationStartedPayload
1654
1323
  if (
1655
1324
  state.terminalRequestIds?.[payload.requestId] === true ||
1656
- terminalGenerationReason(state, payload.generationId) !== undefined
1325
+ terminalGenerationReason(
1326
+ state,
1327
+ payload.generationId,
1328
+ payload.requestId,
1329
+ ) !== undefined
1657
1330
  ) {
1658
1331
  return state
1659
1332
  }
@@ -1663,31 +1336,26 @@ export function reduceAIState<M extends UIMessage>(
1663
1336
  ) {
1664
1337
  return state
1665
1338
  }
1666
- const currentOwner =
1667
- state.responseGenerationIds[payload.responseMessageId]
1339
+ const currentOwner = state.responseOwners[payload.responseMessageId]
1668
1340
  const currentTerminal =
1669
1341
  currentOwner === undefined
1670
1342
  ? undefined
1671
- : terminalGenerationReason(state, currentOwner)
1343
+ : terminalGenerationReason(
1344
+ state,
1345
+ currentOwner.generationId,
1346
+ currentOwner.requestId,
1347
+ )
1672
1348
  if (
1673
1349
  currentTerminal === 'completed' ||
1674
1350
  currentTerminal === 'interrupted'
1675
1351
  ) {
1676
1352
  return state
1677
1353
  }
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
1354
  if (
1685
- currentAttempt !== undefined &&
1686
- Number.isFinite(currentAttempt) &&
1687
- currentAttempt >= payload.attempt
1688
- ) {
1355
+ currentOwner?.requestId === payload.requestId &&
1356
+ currentOwner.attempt >= payload.attempt
1357
+ )
1689
1358
  return state
1690
- }
1691
1359
  const activeGeneration = state.activeGeneration
1692
1360
  const replacesActiveAttempt =
1693
1361
  activeGeneration !== null &&
@@ -1711,9 +1379,13 @@ export function reduceAIState<M extends UIMessage>(
1711
1379
  status: 'generating',
1712
1380
  activeGeneration: payload,
1713
1381
  activeResponseMessageId: null,
1714
- responseGenerationIds: {
1715
- ...baseState.responseGenerationIds,
1716
- [payload.responseMessageId]: payload.generationId,
1382
+ responseOwners: {
1383
+ ...baseState.responseOwners,
1384
+ [payload.responseMessageId]: {
1385
+ generationId: payload.generationId,
1386
+ requestId: payload.requestId,
1387
+ attempt: payload.attempt,
1388
+ },
1717
1389
  },
1718
1390
  activeProjection: {
1719
1391
  generationId: payload.generationId,
@@ -1732,83 +1404,61 @@ export function reduceAIState<M extends UIMessage>(
1732
1404
  case 'ai.generation.progress': {
1733
1405
  const payload = event.payload as GenerationProgressPayload
1734
1406
  if (state.pausedGenerationIds?.[payload.generationId]) return state
1735
- if (terminalGenerationReason(state, payload.generationId) !== undefined) {
1407
+ if (
1408
+ terminalGenerationReason(
1409
+ state,
1410
+ payload.generationId,
1411
+ payload.requestId,
1412
+ ) !== undefined
1413
+ ) {
1736
1414
  return state
1737
1415
  }
1738
1416
  const activeProjection = state.activeProjection
1739
1417
  if (
1740
1418
  state.activeGeneration?.generationId !== payload.generationId ||
1419
+ state.activeGeneration.requestId !== payload.requestId ||
1741
1420
  activeProjection?.generationId !== payload.generationId
1742
1421
  ) {
1743
1422
  return state
1744
1423
  }
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
- }
1424
+ const progress = advanceProjection({
1425
+ projection: activeProjection,
1426
+ base: state.messages.find(
1427
+ (message) => message.id === activeProjection.responseMessageId,
1428
+ ),
1429
+ chunks: payload.chunks,
1430
+ index: event.index,
1431
+ })
1795
1432
  return {
1796
1433
  ...state,
1797
1434
  messages: upsertResponse({
1798
1435
  messages: state.messages,
1799
- message: projected.message,
1436
+ message: progress.message,
1800
1437
  inputId: state.activeGeneration.messageId,
1801
1438
  }),
1802
- activeProjection: projection,
1439
+ activeProjection: {
1440
+ ...activeProjection,
1441
+ batches: progress.batches,
1442
+ cursor: progress.cursor,
1443
+ },
1803
1444
  status: 'generating',
1804
1445
  }
1805
1446
  }
1806
1447
  case 'ai.generation.completed': {
1807
1448
  const payload = event.payload as GenerationCompletedPayload
1808
- if (terminalGenerationReason(state, payload.generationId) !== undefined) {
1449
+ if (
1450
+ terminalGenerationReason(
1451
+ state,
1452
+ payload.generationId,
1453
+ payload.requestId,
1454
+ ) !== undefined
1455
+ ) {
1809
1456
  return state
1810
1457
  }
1811
- if (state.activeGeneration?.generationId !== payload.generationId) {
1458
+ if (
1459
+ state.activeGeneration?.generationId !== payload.generationId ||
1460
+ state.activeGeneration.requestId !== payload.requestId
1461
+ ) {
1812
1462
  return state
1813
1463
  }
1814
1464
  const usage = payload.usage
@@ -1832,17 +1482,26 @@ export function reduceAIState<M extends UIMessage>(
1832
1482
  }
1833
1483
  case 'ai.generation.failed': {
1834
1484
  const payload = event.payload as GenerationFailedPayload
1835
- if (terminalGenerationReason(state, payload.generationId) !== undefined) {
1485
+ if (
1486
+ terminalGenerationReason(
1487
+ state,
1488
+ payload.generationId,
1489
+ payload.requestId,
1490
+ ) !== undefined
1491
+ ) {
1836
1492
  return state
1837
1493
  }
1838
1494
  if (
1839
- state.activeGeneration?.generationId !== payload.generationId &&
1495
+ (state.activeGeneration?.generationId !== payload.generationId ||
1496
+ state.activeGeneration.requestId !== payload.requestId) &&
1840
1497
  !(
1841
1498
  payload.stepLimit === true &&
1842
- state.responseGenerationIds[payload.responseMessageId] ===
1499
+ state.activeRequestReason === 'tool' &&
1500
+ state.responseOwners[payload.responseMessageId]?.requestId ===
1501
+ payload.requestId &&
1502
+ state.responseOwners[payload.responseMessageId]?.generationId ===
1843
1503
  payload.generationId &&
1844
- state.activeRequestId ===
1845
- `ai.generate:tools:${payload.generationId}` &&
1504
+ state.activeRequestSourceGenerationId === payload.generationId &&
1846
1505
  state.activeResponseMessageId === payload.responseMessageId
1847
1506
  )
1848
1507
  ) {
@@ -1912,9 +1571,13 @@ export function reduceAIState<M extends UIMessage>(
1912
1571
  }
1913
1572
  case 'ai.message.interrupted': {
1914
1573
  const payload = event.payload as MessageInterruptedPayload
1915
- const currentGenerationId = state.responseGenerationIds[payload.messageId]
1574
+ const owner = state.responseOwners[payload.messageId]
1575
+ const currentGenerationId = owner?.generationId
1916
1576
  const targetRequestId =
1917
- payload.requestId ?? generationRequestId(payload.generationId)
1577
+ payload.requestId ??
1578
+ (owner?.generationId === payload.generationId
1579
+ ? owner?.requestId
1580
+ : undefined)
1918
1581
  const ownsRequestPhase =
1919
1582
  payload.requestId !== undefined &&
1920
1583
  state.activeGeneration === null &&
@@ -1925,7 +1588,7 @@ export function reduceAIState<M extends UIMessage>(
1925
1588
  !ownsRequestPhase &&
1926
1589
  (payload.generationId !== undefined
1927
1590
  ? currentGenerationId !== payload.generationId
1928
- : generationRequestId(currentGenerationId) !== payload.requestId)
1591
+ : owner?.requestId !== payload.requestId)
1929
1592
  ) {
1930
1593
  return state
1931
1594
  }
@@ -1940,7 +1603,11 @@ export function reduceAIState<M extends UIMessage>(
1940
1603
  const currentTerminal =
1941
1604
  currentGenerationId === undefined
1942
1605
  ? undefined
1943
- : terminalGenerationReason(state, currentGenerationId)
1606
+ : terminalGenerationReason(
1607
+ state,
1608
+ currentGenerationId,
1609
+ owner?.requestId,
1610
+ )
1944
1611
  if (
1945
1612
  currentTerminal === 'completed' ||
1946
1613
  currentTerminal === 'interrupted' ||
@@ -1970,6 +1637,8 @@ export function reduceAIState<M extends UIMessage>(
1970
1637
  ...baseState,
1971
1638
  status: 'idle',
1972
1639
  activeRequestId: null,
1640
+ activeRequestReason: null,
1641
+ activeRequestSourceGenerationId: null,
1973
1642
  activeResponseMessageId: null,
1974
1643
  activeProjection: null,
1975
1644
  terminalRequestIds: {
@@ -1990,7 +1659,7 @@ export function reduceAIState<M extends UIMessage>(
1990
1659
  const targetGenerationId =
1991
1660
  payload.generationId ??
1992
1661
  (currentGenerationId !== undefined &&
1993
- generationRequestId(currentGenerationId) === payload.requestId
1662
+ owner?.requestId === payload.requestId
1994
1663
  ? currentGenerationId
1995
1664
  : undefined)
1996
1665
  if (targetGenerationId === undefined) return state
@@ -2019,7 +1688,8 @@ export function reduceAIState<M extends UIMessage>(
2019
1688
  currentGenerationId === targetGenerationId &&
2020
1689
  (state.activeRequestId === null ||
2021
1690
  state.activeRequestId === targetRequestId ||
2022
- state.activeRequestId === `ai.generate:tools:${targetGenerationId}`)
1691
+ (state.activeRequestReason === 'tool' &&
1692
+ state.activeRequestSourceGenerationId === targetGenerationId))
2023
1693
  const resolvesCurrentGeneration =
2024
1694
  currentGenerationId === targetGenerationId &&
2025
1695
  currentTerminal === undefined
@@ -2041,6 +1711,8 @@ export function reduceAIState<M extends UIMessage>(
2041
1711
  const next = {
2042
1712
  ...baseState,
2043
1713
  activeRequestId: null,
1714
+ activeRequestReason: null,
1715
+ activeRequestSourceGenerationId: null,
2044
1716
  activeResponseMessageId: null,
2045
1717
  activeProjection: null,
2046
1718
  messages:
@@ -2114,6 +1786,8 @@ export function reduceAIState<M extends UIMessage>(
2114
1786
  const next = {
2115
1787
  ...baseState,
2116
1788
  activeRequestId: null,
1789
+ activeRequestReason: null,
1790
+ activeRequestSourceGenerationId: null,
2117
1791
  activeResponseMessageId: null,
2118
1792
  terminalRequestIds: {
2119
1793
  ...baseState.terminalRequestIds,
@@ -2135,8 +1809,12 @@ export function reduceAIState<M extends UIMessage>(
2135
1809
  const payload = event.payload as ToolCalledPayload
2136
1810
  const projection = state.activeProjection
2137
1811
  if (
2138
- isStaleGeneration(state, payload.messageId, payload.generationId) ||
2139
- terminalGenerationReason(state, payload.generationId) !== undefined
1812
+ isStaleGeneration(state, payload.messageId, payload) ||
1813
+ terminalGenerationReason(
1814
+ state,
1815
+ payload.generationId,
1816
+ payload.requestId,
1817
+ ) !== undefined
2140
1818
  ) {
2141
1819
  return state
2142
1820
  }
@@ -2181,6 +1859,7 @@ export function reduceAIState<M extends UIMessage>(
2181
1859
  const terminalReason = terminalGenerationReason(
2182
1860
  state,
2183
1861
  payload.generationId,
1862
+ payload.requestId,
2184
1863
  )
2185
1864
  const completesFailedTool =
2186
1865
  terminalReason === 'failed' &&
@@ -2194,7 +1873,7 @@ export function reduceAIState<M extends UIMessage>(
2194
1873
  activity.status === 'running',
2195
1874
  )
2196
1875
  if (
2197
- isStaleGeneration(state, payload.messageId, payload.generationId) ||
1876
+ isStaleGeneration(state, payload.messageId, payload) ||
2198
1877
  (terminalReason !== undefined && !completesFailedTool)
2199
1878
  ) {
2200
1879
  return state
@@ -2266,8 +1945,12 @@ export function reduceAIState<M extends UIMessage>(
2266
1945
  const payload = event.payload as ApprovalRequestedPayload
2267
1946
  const projection = state.activeProjection
2268
1947
  if (
2269
- isStaleGeneration(state, payload.messageId, payload.generationId) ||
2270
- terminalGenerationReason(state, payload.generationId) !== undefined
1948
+ isStaleGeneration(state, payload.messageId, payload) ||
1949
+ terminalGenerationReason(
1950
+ state,
1951
+ payload.generationId,
1952
+ payload.requestId,
1953
+ ) !== undefined
2271
1954
  ) {
2272
1955
  return state
2273
1956
  }
@@ -2329,8 +2012,12 @@ export function reduceAIState<M extends UIMessage>(
2329
2012
  case 'ai.approval.responded': {
2330
2013
  const payload = event.payload as ApprovalRespondedPayload
2331
2014
  if (
2332
- isStaleGeneration(state, payload.messageId, payload.generationId) ||
2333
- terminalGenerationReason(state, payload.generationId) !== undefined
2015
+ isStaleGeneration(state, payload.messageId, payload) ||
2016
+ terminalGenerationReason(
2017
+ state,
2018
+ payload.generationId,
2019
+ payload.requestId,
2020
+ ) !== undefined
2334
2021
  ) {
2335
2022
  return state
2336
2023
  }
@@ -2398,8 +2085,12 @@ export function reduceAIState<M extends UIMessage>(
2398
2085
  case 'ai.input.requested': {
2399
2086
  const payload = event.payload as InputRequestedPayload
2400
2087
  if (
2401
- isStaleGeneration(state, payload.messageId, payload.generationId) ||
2402
- terminalGenerationReason(state, payload.generationId) !== undefined
2088
+ isStaleGeneration(state, payload.messageId, payload) ||
2089
+ terminalGenerationReason(
2090
+ state,
2091
+ payload.generationId,
2092
+ payload.requestId,
2093
+ ) !== undefined
2403
2094
  ) {
2404
2095
  return state
2405
2096
  }
@@ -2420,8 +2111,12 @@ export function reduceAIState<M extends UIMessage>(
2420
2111
  case 'ai.input.responded': {
2421
2112
  const payload = event.payload as InputRespondedPayload
2422
2113
  if (
2423
- isStaleGeneration(state, payload.messageId, payload.generationId) ||
2424
- terminalGenerationReason(state, payload.generationId) !== undefined
2114
+ isStaleGeneration(state, payload.messageId, payload) ||
2115
+ terminalGenerationReason(
2116
+ state,
2117
+ payload.generationId,
2118
+ payload.requestId,
2119
+ ) !== undefined
2425
2120
  ) {
2426
2121
  return state
2427
2122
  }
@@ -2455,13 +2150,18 @@ export function reduceAIState<M extends UIMessage>(
2455
2150
  ) {
2456
2151
  return state
2457
2152
  }
2153
+ const compaction: CompactionState<M> = {
2154
+ status: 'completed',
2155
+ ...payload,
2156
+ throughIndex:
2157
+ payload.throughIndex ?? state.activeProjection!.promptThroughIndex!,
2158
+ }
2458
2159
  return {
2459
2160
  ...state,
2460
- compaction: {
2461
- status: 'completed',
2462
- ...payload,
2463
- throughIndex:
2464
- payload.throughIndex ?? state.activeProjection!.promptThroughIndex!,
2161
+ compaction,
2162
+ activeProjection: state.activeProjection && {
2163
+ ...state.activeProjection,
2164
+ baseCompaction: compaction,
2465
2165
  },
2466
2166
  }
2467
2167
  }
@@ -2483,22 +2183,31 @@ export function deriveUIMessages<M extends UIMessage = UIMessage>(
2483
2183
  export function createReducer<D extends EventDefs>(options: {
2484
2184
  contract: Contract<D>
2485
2185
  name?: string
2486
- }): Reducer<D, AIState<AIMessageFrom<D>>> {
2186
+ fold?: Reducer<D, AIState<AIMessageFrom<D>>>['fold']
2187
+ stateSchema?: StandardSchemaV1<unknown, AIState<AIMessageFrom<D>>>
2188
+ }): AIStoredReducer<D, AIState<AIMessageFrom<D>>> {
2487
2189
  type M = AIMessageFrom<D>
2488
2190
  const reducer = options.contract
2489
2191
  .reducer({
2490
- name: options.name ?? 'a2.ai.state.v17',
2192
+ name: options.name ?? 'a2.ai.state.v22',
2491
2193
  initialState: initialState<M>(),
2194
+ ...(options.stateSchema === undefined
2195
+ ? {}
2196
+ : { stateSchema: options.stateSchema }),
2492
2197
  })
2493
- .fold((state, event) => reduceAIState(state, event))
2494
- return { ...reducer, [clientStateProjection]: projectAIClientState }
2198
+ .fold(options.fold ?? ((state, event) => reduceAIState(state, event)))
2199
+ return storedAIReducer({
2200
+ ...reducer,
2201
+ [clientStateProjection]: projectAIClientState,
2202
+ })
2495
2203
  }
2496
2204
 
2497
2205
  type CommandInput<C> = { type: 'ai.control.requested'; payload: C; id?: string }
2498
2206
  type ControlInput<
2499
2207
  M extends UIMessage,
2500
2208
  A extends ControlCommand<M>['action'],
2501
- > = CommandInput<Extract<ControlCommand<M>, { action: A }>>
2209
+ > = CommandInput<Extract<ControlCommand<M>, { action: A }>> &
2210
+ (A extends 'send' | 'steer' ? { id: string } : {})
2502
2211
 
2503
2212
  export type AIInputs = {
2504
2213
  message<M extends UIMessage>(
@@ -2515,7 +2224,6 @@ export type AIInputs = {
2515
2224
  }): ControlInput<UIMessage, 'send-now'>[]
2516
2225
  edit<M extends UIMessage>(options: {
2517
2226
  message: M
2518
- expectedRevision: number
2519
2227
  }): ControlInput<M, 'edit'>[]
2520
2228
  move(options: {
2521
2229
  inputId: string
@@ -2560,7 +2268,7 @@ const messageInput = <M extends UIMessage>(
2560
2268
  ): ControlInput<M, 'send'>[] => [
2561
2269
  {
2562
2270
  type: 'ai.control.requested',
2563
- id: `ai.message:${message.id}`,
2271
+ id: message.id,
2564
2272
  payload: {
2565
2273
  action: 'send',
2566
2274
  message,
@@ -2579,7 +2287,13 @@ export const inputs: AIInputs = {
2579
2287
  move: (options) => commandInput({ action: 'move', ...options }),
2580
2288
  },
2581
2289
  stop: (options) => commandInput({ action: 'stop', ...options }),
2582
- steer: (options) => commandInput({ action: 'steer', ...options }),
2290
+ steer: (options) => [
2291
+ {
2292
+ type: 'ai.control.requested',
2293
+ id: options.message.id,
2294
+ payload: { action: 'steer', ...options },
2295
+ },
2296
+ ],
2583
2297
  pause: (options) => commandInput({ action: 'pause', ...options }),
2584
2298
  resume: () => commandInput({ action: 'resume' }),
2585
2299
  message<M extends UIMessage>(message: M, options?: { generate?: boolean }) {
@@ -2591,19 +2305,19 @@ export const inputs: AIInputs = {
2591
2305
  approval: (response) => [
2592
2306
  {
2593
2307
  ...commandInput({ action: 'approval', response })[0]!,
2594
- id: `ai.approval:${encodeURIComponent(response.messageId)}:${encodeURIComponent(response.generationId)}:${encodeURIComponent(response.approvalId)}:response`,
2308
+ id: `ai.approval:${encodeURIComponent(response.requestId)}:${encodeURIComponent(response.messageId)}:${encodeURIComponent(response.generationId)}:${encodeURIComponent(response.approvalId)}:response`,
2595
2309
  },
2596
2310
  ],
2597
2311
  input: (response) => [
2598
2312
  {
2599
2313
  ...commandInput({ action: 'input', response })[0]!,
2600
- id: `ai.input:${encodeURIComponent(response.messageId)}:${encodeURIComponent(response.generationId)}:${encodeURIComponent(response.inputId)}:response`,
2314
+ id: `ai.input:${encodeURIComponent(response.requestId)}:${encodeURIComponent(response.messageId)}:${encodeURIComponent(response.generationId)}:${encodeURIComponent(response.inputId)}:response`,
2601
2315
  },
2602
2316
  ],
2603
2317
  requestInput: (request) => [
2604
2318
  {
2605
2319
  ...commandInput({ action: 'request-input', request })[0]!,
2606
- id: `ai.input:${encodeURIComponent(request.messageId)}:${encodeURIComponent(request.generationId)}:${encodeURIComponent(request.inputId)}:request`,
2320
+ id: `ai.input:${encodeURIComponent(request.requestId)}:${encodeURIComponent(request.messageId)}:${encodeURIComponent(request.generationId)}:${encodeURIComponent(request.inputId)}:request`,
2607
2321
  },
2608
2322
  ],
2609
2323
  retry: (request) => [
@@ -2674,14 +2388,17 @@ export function agent<
2674
2388
  })
2675
2389
  const reducer = agentContract
2676
2390
  .reducer({
2677
- name: options.reducerName ?? 'a2.ai.state.v17',
2391
+ name: options.reducerName ?? 'a2.ai.state.v22',
2678
2392
  initialState: initialState<M>(),
2679
2393
  })
2680
2394
  .fold((state, event) => reduceAIState(state, event))
2681
2395
 
2682
2396
  const definition = {
2683
2397
  contract: agentContract,
2684
- reducer: { ...reducer, [clientStateProjection]: projectAIClientState<M> },
2398
+ reducer: storedAIReducer({
2399
+ ...reducer,
2400
+ [clientStateProjection]: projectAIClientState<M>,
2401
+ }),
2685
2402
  }
2686
2403
  return definition as unknown as AgentDefinition<M, AIEventDefs<M> & E>
2687
2404
  }