experimental-a2 0.3.0 → 0.5.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 (205) hide show
  1. package/CHANGELOG.md +207 -0
  2. package/dist/ai-CFNeCrRl.d.ts +334 -0
  3. package/dist/ai-CFNeCrRl.d.ts.map +1 -0
  4. package/dist/ai-server.browser.js +2 -0
  5. package/dist/ai-server.browser.js.map +1 -0
  6. package/dist/ai-server.d.ts +49 -9
  7. package/dist/ai-server.d.ts.map +1 -0
  8. package/dist/ai-server.js +123 -49
  9. package/dist/ai-server.js.map +1 -0
  10. package/dist/ai.d.ts +2 -303
  11. package/dist/ai.js +233 -86
  12. package/dist/ai.js.map +1 -0
  13. package/dist/cli-B3VuxoDe.js +599 -0
  14. package/dist/cli-B3VuxoDe.js.map +1 -0
  15. package/dist/cli-bin.d.ts +1 -0
  16. package/dist/cli-bin.js +7 -0
  17. package/dist/cli-bin.js.map +1 -0
  18. package/dist/cli.d.ts +20 -0
  19. package/dist/cli.d.ts.map +1 -0
  20. package/dist/cli.js +2 -0
  21. package/dist/client-BKlyLiOU.js +1008 -0
  22. package/dist/client-BKlyLiOU.js.map +1 -0
  23. package/dist/client-D7mvIXrF.d.ts +191 -0
  24. package/dist/client-D7mvIXrF.d.ts.map +1 -0
  25. package/dist/client.d.ts +2 -90
  26. package/dist/client.js +1 -409
  27. package/dist/{contract-CG_adnu_.js → contract-48bUMgcL.js} +12 -2
  28. package/dist/contract-48bUMgcL.js.map +1 -0
  29. package/dist/{contract-C_3dIIEU.d.ts → contract-jIfaR085.d.ts} +63 -8
  30. package/dist/contract-jIfaR085.d.ts.map +1 -0
  31. package/dist/devtools-J_jZ2vQf.d.ts +152 -0
  32. package/dist/devtools-J_jZ2vQf.d.ts.map +1 -0
  33. package/dist/devtools-kJJaORn-.js +340 -0
  34. package/dist/devtools-kJJaORn-.js.map +1 -0
  35. package/dist/devtools-server.browser.js +3 -1
  36. package/dist/devtools-server.browser.js.map +1 -0
  37. package/dist/devtools-server.d.ts +4 -3
  38. package/dist/devtools-server.d.ts.map +1 -0
  39. package/dist/devtools-server.js +226 -43
  40. package/dist/devtools-server.js.map +1 -0
  41. package/dist/devtools.d.ts +2 -0
  42. package/dist/devtools.js +2 -0
  43. package/dist/{errors-BJRMd-h6.js → errors-BQuJpe82.js} +6 -4
  44. package/dist/errors-BQuJpe82.js.map +1 -0
  45. package/dist/{errors-xL_JTXsY.d.ts → errors-W6nwJ-fm.d.ts} +3 -2
  46. package/dist/errors-W6nwJ-fm.d.ts.map +1 -0
  47. package/dist/http.d.ts +135 -28
  48. package/dist/http.d.ts.map +1 -0
  49. package/dist/http.js +647 -61
  50. package/dist/http.js.map +1 -0
  51. package/dist/{idempotent-replay-BMyHrP0L.js → idempotent-replay-DuqEkYA7.js} +4 -2
  52. package/dist/idempotent-replay-DuqEkYA7.js.map +1 -0
  53. package/dist/index.d.ts +5 -5
  54. package/dist/index.js +2 -2
  55. package/dist/{inspection-E7qbD0Xj.js → inspection-DaxB5jM2.js} +4 -1
  56. package/dist/inspection-DaxB5jM2.js.map +1 -0
  57. package/dist/internal-DstsI6Re.js +211 -0
  58. package/dist/internal-DstsI6Re.js.map +1 -0
  59. package/dist/otel.d.ts +3 -2
  60. package/dist/otel.d.ts.map +1 -0
  61. package/dist/otel.js +2 -0
  62. package/dist/otel.js.map +1 -0
  63. package/dist/platform-B4TnJtWu.js +34 -0
  64. package/dist/platform-B4TnJtWu.js.map +1 -0
  65. package/dist/react.d.ts +47 -15
  66. package/dist/react.d.ts.map +1 -0
  67. package/dist/react.js +30 -15
  68. package/dist/react.js.map +1 -0
  69. package/dist/retryable-lazy-DZWmHpii.js +2 -0
  70. package/dist/retryable-lazy-DZWmHpii.js.map +1 -0
  71. package/dist/scheduler-qstash.d.ts +79 -0
  72. package/dist/scheduler-qstash.d.ts.map +1 -0
  73. package/dist/scheduler-qstash.js +501 -0
  74. package/dist/scheduler-qstash.js.map +1 -0
  75. package/dist/scheduler-task-BpzhPnRS.js +56 -0
  76. package/dist/scheduler-task-BpzhPnRS.js.map +1 -0
  77. package/dist/{recovery-vercel.d.ts → scheduler-vercel.d.ts} +18 -24
  78. package/dist/scheduler-vercel.d.ts.map +1 -0
  79. package/dist/scheduler-vercel.js +228 -0
  80. package/dist/scheduler-vercel.js.map +1 -0
  81. package/dist/server-C72KOw51.js +1457 -0
  82. package/dist/server-C72KOw51.js.map +1 -0
  83. package/dist/server-DUF9pjsx.d.ts +261 -0
  84. package/dist/server-DUF9pjsx.d.ts.map +1 -0
  85. package/dist/server.browser.js +2 -0
  86. package/dist/server.browser.js.map +1 -0
  87. package/dist/server.d.ts +4 -155
  88. package/dist/server.js +2 -2
  89. package/dist/{log-ldf5g8Cx.d.ts → store-DysUkTH3.d.ts} +120 -35
  90. package/dist/store-DysUkTH3.d.ts.map +1 -0
  91. package/dist/{log-yJbXUf72.js → store-N8PXxDAS.js} +3 -1
  92. package/dist/store-N8PXxDAS.js.map +1 -0
  93. package/dist/store-codec-DTG0Ftek.js +10 -0
  94. package/dist/store-codec-DTG0Ftek.js.map +1 -0
  95. package/dist/store-memory.d.ts +12 -0
  96. package/dist/store-memory.d.ts.map +1 -0
  97. package/dist/{log-memory.js → store-memory.js} +145 -34
  98. package/dist/store-memory.js.map +1 -0
  99. package/dist/{log-polling-DZ1MiKLg.js → store-polling-dSeLxzfb.js} +7 -5
  100. package/dist/store-polling-dSeLxzfb.js.map +1 -0
  101. package/dist/{log-postgres.d.ts → store-postgres.d.ts} +7 -6
  102. package/dist/store-postgres.d.ts.map +1 -0
  103. package/dist/{log-postgres.js → store-postgres.js} +214 -24
  104. package/dist/store-postgres.js.map +1 -0
  105. package/dist/{log-redis-core-CyJ5L8yR.js → store-redis-core-BFLwz0Wj.js} +271 -66
  106. package/dist/store-redis-core-BFLwz0Wj.js.map +1 -0
  107. package/dist/{log-redis-http.d.ts → store-redis-http.d.ts} +8 -7
  108. package/dist/store-redis-http.d.ts.map +1 -0
  109. package/dist/{log-redis-http.js → store-redis-http.js} +22 -12
  110. package/dist/store-redis-http.js.map +1 -0
  111. package/dist/{log-redis.d.ts → store-redis.d.ts} +7 -6
  112. package/dist/store-redis.d.ts.map +1 -0
  113. package/dist/{log-redis.js → store-redis.js} +101 -39
  114. package/dist/store-redis.js.map +1 -0
  115. package/dist/{log-sqlite.d.ts → store-sqlite.d.ts} +7 -6
  116. package/dist/store-sqlite.d.ts.map +1 -0
  117. package/dist/{log-sqlite.js → store-sqlite.js} +118 -22
  118. package/dist/store-sqlite.js.map +1 -0
  119. package/dist/{telemetry-Cso0qyHQ.d.ts → telemetry-BjYHTfh2.d.ts} +3 -2
  120. package/dist/telemetry-BjYHTfh2.d.ts.map +1 -0
  121. package/dist/testing.browser.d.ts +1 -0
  122. package/dist/testing.browser.js +6 -0
  123. package/dist/testing.browser.js.map +1 -0
  124. package/dist/testing.d.ts +32 -0
  125. package/dist/testing.d.ts.map +1 -0
  126. package/dist/testing.js +103 -0
  127. package/dist/testing.js.map +1 -0
  128. package/dist/validate-XKT4FSNn.js +2 -0
  129. package/dist/validate-XKT4FSNn.js.map +1 -0
  130. package/dist/wire-BFQmSJ-9.js +284 -0
  131. package/dist/wire-BFQmSJ-9.js.map +1 -0
  132. package/docs/01-quickstart.mdx +3 -3
  133. package/docs/concepts/01-contracts.mdx +18 -13
  134. package/docs/concepts/02-handlers.mdx +7 -7
  135. package/docs/concepts/03-durability.mdx +21 -21
  136. package/docs/concepts/04-state.mdx +18 -21
  137. package/docs/guides/01-timers.mdx +154 -49
  138. package/docs/guides/02-cancellation.mdx +30 -4
  139. package/docs/guides/03-react.mdx +76 -57
  140. package/docs/guides/04-local-first.mdx +1 -1
  141. package/docs/guides/05-production.mdx +310 -61
  142. package/docs/guides/06-ai-agents.mdx +252 -74
  143. package/docs/guides/07-devtools.mdx +137 -12
  144. package/docs/guides/08-application-data.mdx +1 -1
  145. package/docs/guides/09-presence.mdx +263 -0
  146. package/docs/guides/10-transports.mdx +140 -0
  147. package/docs/index.mdx +18 -14
  148. package/docs/reference/01-api.mdx +839 -110
  149. package/docs/reference/02-errors.mdx +13 -6
  150. package/package.json +24 -7
  151. package/src/ai-coordinator.ts +358 -0
  152. package/src/ai-projector.ts +524 -0
  153. package/src/ai-sdk-step.ts +261 -0
  154. package/src/ai-server.browser.ts +5 -0
  155. package/src/ai-server.ts +1719 -0
  156. package/src/ai.ts +2155 -0
  157. package/src/cache-indexeddb.ts +10 -0
  158. package/src/cli-bin.ts +5 -0
  159. package/src/cli.ts +1046 -0
  160. package/src/client.ts +1826 -0
  161. package/src/contract.ts +206 -0
  162. package/src/deterministic-id.ts +72 -0
  163. package/src/devtools-app.ts +989 -0
  164. package/src/devtools-server.browser.ts +5 -0
  165. package/src/devtools-server.ts +604 -0
  166. package/src/devtools.ts +716 -0
  167. package/src/errors.ts +50 -0
  168. package/src/http.ts +394 -0
  169. package/src/idempotent-replay.ts +53 -0
  170. package/src/index.ts +37 -0
  171. package/src/inspection.ts +39 -0
  172. package/src/internal.ts +426 -0
  173. package/src/otel.ts +59 -0
  174. package/src/platform.ts +60 -0
  175. package/src/push-envelope.ts +137 -0
  176. package/src/react.ts +284 -0
  177. package/src/reducer.ts +108 -0
  178. package/src/retryable-lazy.ts +27 -0
  179. package/src/scheduler-qstash.ts +915 -0
  180. package/src/scheduler-task.ts +106 -0
  181. package/src/scheduler-vercel.ts +437 -0
  182. package/src/server.browser.ts +12 -0
  183. package/src/server.ts +2700 -0
  184. package/src/session-socket.ts +548 -0
  185. package/src/sse.ts +141 -0
  186. package/src/standard-schema.ts +77 -0
  187. package/src/store-codec.ts +10 -0
  188. package/src/store-memory.ts +788 -0
  189. package/src/store-polling.ts +102 -0
  190. package/src/store-postgres.ts +1212 -0
  191. package/src/store-redis-core.ts +1494 -0
  192. package/src/store-redis-http.ts +116 -0
  193. package/src/store-redis.ts +458 -0
  194. package/src/store-sqlite.ts +1108 -0
  195. package/src/store.ts +385 -0
  196. package/src/telemetry.ts +47 -0
  197. package/src/testing.browser.ts +5 -0
  198. package/src/testing.ts +185 -0
  199. package/src/validate.ts +39 -0
  200. package/src/wire.ts +454 -0
  201. package/dist/internal-gCd5qMry.js +0 -44
  202. package/dist/log-memory.d.ts +0 -11
  203. package/dist/recovery-vercel.js +0 -119
  204. package/dist/server-BcLa4RFL.js +0 -877
  205. package/dist/wire-BVsgR8o9.js +0 -62
package/src/ai.ts ADDED
@@ -0,0 +1,2155 @@
1
+ /**
2
+ * experimental-a2/ai — an isomorphic, extensible agent definition.
3
+ *
4
+ * `agent()` assembles a normal A2 contract, a reducer over the built-in
5
+ * AI protocol, and pure builders for the events applications append.
6
+ * Model calls and tool implementations stay in experimental-a2/ai/server.
7
+ */
8
+
9
+ import type {
10
+ FinishReason,
11
+ JSONValue,
12
+ LanguageModelUsage,
13
+ UIMessage,
14
+ UIMessageChunk,
15
+ } from 'ai'
16
+ import { contract as createContract } from './contract.ts'
17
+ import type { AppendInput, Contract, EventDefs } from './contract.ts'
18
+ import { interruptUIMessage, projectUIMessage } from './ai-projector.ts'
19
+ import { ambientToolScopeStorage } from './internal.ts'
20
+ import type { Reducer } from './reducer.ts'
21
+ import type { HandlerContext } from './server.ts'
22
+ import type { StandardSchemaV1 } from './standard-schema.ts'
23
+ import { assertSyncSchema, validateSync } from './validate.ts'
24
+
25
+ export type AgentStatus =
26
+ 'idle' | 'generating' | 'waiting' | 'failed' | 'closed'
27
+
28
+ export type GenerationReason = 'message' | 'tool' | 'input' | 'retry'
29
+
30
+ export type SessionCreatedPayload = { metadata?: JSONValue }
31
+ export type SessionClosedPayload = { reason?: string }
32
+ export type MessageCreatedPayload<M extends UIMessage> = {
33
+ message: M
34
+ /** For user messages, false records passive context. Omission or true schedules. */
35
+ generate?: boolean
36
+ }
37
+ export type MessageCompletedPayload = { messageId: string }
38
+ type MessageInterruption = {
39
+ messageId: string
40
+ reason?: string
41
+ /** Last confirmed event-log index visible to the interrupter. */
42
+ lastSeenIndex?: number
43
+ }
44
+ export type MessageInterruptedPayload = MessageInterruption &
45
+ (
46
+ | { generationId: string; requestId?: never }
47
+ | { requestId: string; generationId?: never }
48
+ )
49
+ export type GenerationRequestedPayload = {
50
+ messageId: string
51
+ reason: GenerationReason
52
+ responseMessageId?: string
53
+ }
54
+ export type RetryGenerationOptions = {
55
+ messageId: string
56
+ responseMessageId: string
57
+ retryId: string
58
+ }
59
+ export type RetryRequestedPayload = RetryGenerationOptions
60
+ export type GenerationStartedPayload = {
61
+ requestId: string
62
+ messageId: string
63
+ generationId: string
64
+ responseMessageId: string
65
+ attempt: number
66
+ model: string
67
+ }
68
+ export type GenerationProgressPayload = {
69
+ requestId: string
70
+ messageId: string
71
+ generationId: string
72
+ responseMessageId: string
73
+ sequence: number
74
+ chunks: UIMessageChunk[]
75
+ }
76
+ export type GenerationCompletedPayload = {
77
+ requestId: string
78
+ messageId: string
79
+ generationId: string
80
+ responseMessageId: string
81
+ finishReason?: FinishReason
82
+ usage?: LanguageModelUsage
83
+ }
84
+ export type GenerationFailedPayload = {
85
+ requestId: string
86
+ messageId: string
87
+ generationId: string
88
+ responseMessageId: string
89
+ error: string
90
+ /** True when a later attempt replaced an incomplete attempt. */
91
+ superseded?: boolean
92
+ stepLimit?: boolean
93
+ }
94
+ export type ToolCalledPayload = {
95
+ requestId: string
96
+ messageId: string
97
+ generationId: string
98
+ toolCallId: string
99
+ toolName: string
100
+ input: unknown
101
+ dynamic?: boolean
102
+ providerExecuted?: boolean
103
+ supportsDeferredResults?: boolean
104
+ providerMetadata?: unknown
105
+ toolMetadata?: unknown
106
+ title?: string
107
+ }
108
+ export type ToolResultPayload = {
109
+ requestId: string
110
+ messageId: string
111
+ generationId: string
112
+ toolCallId: string
113
+ toolName?: string
114
+ input?: unknown
115
+ rawInput?: unknown
116
+ output?: unknown
117
+ error?: string
118
+ denied?: boolean
119
+ preliminary?: boolean
120
+ phase?: 'input' | 'execution'
121
+ dynamic?: boolean
122
+ providerExecuted?: boolean
123
+ providerMetadata?: unknown
124
+ toolMetadata?: unknown
125
+ }
126
+ export type ApprovalRequestedPayload = {
127
+ messageId: string
128
+ generationId: string
129
+ approvalId: string
130
+ toolCallId: string
131
+ isAutomatic?: boolean
132
+ signature?: string
133
+ }
134
+ export type ApprovalRespondedPayload = {
135
+ messageId: string
136
+ generationId: string
137
+ approvalId: string
138
+ approved: boolean
139
+ reason?: string
140
+ }
141
+ export type InputRequestedPayload = {
142
+ messageId: string
143
+ generationId: string
144
+ inputId: string
145
+ name: string
146
+ question?: JSONValue
147
+ }
148
+ export type InputRespondedPayload = {
149
+ messageId: string
150
+ generationId: string
151
+ inputId: string
152
+ name: string
153
+ value: JSONValue
154
+ }
155
+ export type CompactionRequestedPayload = {
156
+ generationId: string
157
+ throughMessageId: string
158
+ }
159
+ export type CompactionCompletedPayload<M extends UIMessage> = {
160
+ generationId: string
161
+ throughMessageId: string
162
+ messages: M[]
163
+ retainedMessageIds?: string[]
164
+ }
165
+
166
+ export type AIEventDefs<M extends UIMessage = UIMessage> = {
167
+ 'ai.session.created': StandardSchemaV1<SessionCreatedPayload>
168
+ 'ai.session.closed': StandardSchemaV1<SessionClosedPayload>
169
+ 'ai.message.created': StandardSchemaV1<MessageCreatedPayload<M>>
170
+ 'ai.message.completed': StandardSchemaV1<MessageCompletedPayload>
171
+ 'ai.message.interrupted': StandardSchemaV1<MessageInterruptedPayload>
172
+ 'ai.generation.requested': StandardSchemaV1<GenerationRequestedPayload>
173
+ 'ai.retry.requested': StandardSchemaV1<RetryRequestedPayload>
174
+ 'ai.generation.started': StandardSchemaV1<GenerationStartedPayload>
175
+ 'ai.generation.progress': StandardSchemaV1<GenerationProgressPayload>
176
+ 'ai.generation.completed': StandardSchemaV1<GenerationCompletedPayload>
177
+ 'ai.generation.failed': StandardSchemaV1<GenerationFailedPayload>
178
+ 'ai.tool.called': StandardSchemaV1<ToolCalledPayload>
179
+ 'ai.tool.result': StandardSchemaV1<ToolResultPayload>
180
+ 'ai.approval.requested': StandardSchemaV1<ApprovalRequestedPayload>
181
+ 'ai.approval.responded': StandardSchemaV1<ApprovalRespondedPayload>
182
+ 'ai.input.requested': StandardSchemaV1<InputRequestedPayload>
183
+ 'ai.input.responded': StandardSchemaV1<InputRespondedPayload>
184
+ 'ai.compaction.requested': StandardSchemaV1<CompactionRequestedPayload>
185
+ 'ai.compaction.completed': StandardSchemaV1<CompactionCompletedPayload<M>>
186
+ }
187
+
188
+ export type ActiveGeneration = GenerationStartedPayload
189
+
190
+ export type ActiveGenerationProjection<M extends UIMessage = UIMessage> = {
191
+ generationId: string
192
+ responseMessageId: string
193
+ /** Log index of the completed model step while its response remains interruptible. */
194
+ completionIndex?: number
195
+ baseMessage?: M
196
+ baseCompaction?: CompactionState<M> | null
197
+ batches: Array<{ index: number; chunks: UIMessageChunk[] }>
198
+ baseTools: ToolActivity[]
199
+ toolEvents: Array<
200
+ | { index: number; type: 'ai.tool.called'; payload: ToolCalledPayload }
201
+ | { index: number; type: 'ai.tool.result'; payload: ToolResultPayload }
202
+ >
203
+ approvalEvents: Array<
204
+ | {
205
+ index: number
206
+ type: 'ai.approval.requested'
207
+ payload: ApprovalRequestedPayload
208
+ }
209
+ | {
210
+ index: number
211
+ type: 'ai.approval.responded'
212
+ payload: ApprovalRespondedPayload
213
+ }
214
+ >
215
+ }
216
+
217
+ export type PendingApproval = ApprovalRequestedPayload
218
+ export type PendingInput = InputRequestedPayload
219
+
220
+ export type ToolActivity = {
221
+ requestId: string
222
+ messageId: string
223
+ generationId: string
224
+ toolCallId: string
225
+ toolName?: string
226
+ input?: unknown
227
+ rawInput?: unknown
228
+ output?: unknown
229
+ error?: string
230
+ preliminary?: boolean
231
+ phase?: 'input' | 'execution'
232
+ dynamic?: boolean
233
+ providerExecuted?: boolean
234
+ callProviderMetadata?: unknown
235
+ resultProviderMetadata?: unknown
236
+ toolMetadata?: unknown
237
+ title?: string
238
+ status: 'running' | 'completed' | 'failed' | 'denied'
239
+ }
240
+
241
+ export type CompactionState<M extends UIMessage> = {
242
+ status: 'running' | 'completed'
243
+ generationId: string
244
+ throughMessageId: string
245
+ messages?: M[]
246
+ retainedMessageIds?: string[]
247
+ }
248
+
249
+ export type GenerationUsage = {
250
+ generationId: string
251
+ usage: LanguageModelUsage
252
+ }
253
+
254
+ export type AIState<M extends UIMessage = UIMessage> = {
255
+ session: { created: boolean; metadata?: JSONValue }
256
+ messages: M[]
257
+ status: AgentStatus
258
+ activeGeneration: ActiveGeneration | null
259
+ activeRequestId: string | null
260
+ activeResponseMessageId: string | null
261
+ activeProjection: ActiveGenerationProjection<M> | null
262
+ responseGenerationIds: Record<string, string>
263
+ terminalRequestIds?: Record<string, true>
264
+ terminalGenerations?: Record<
265
+ string,
266
+ 'completed' | 'failed' | 'interrupted' | 'superseded'
267
+ >
268
+ pendingApprovals: PendingApproval[]
269
+ pendingInputs: PendingInput[]
270
+ tools: ToolActivity[]
271
+ compaction: CompactionState<M> | null
272
+ usage: GenerationUsage[]
273
+ error: string | null
274
+ }
275
+
276
+ export type AgentDefinition<
277
+ M extends UIMessage = UIMessage,
278
+ D extends EventDefs = AIEventDefs<M>,
279
+ > = {
280
+ readonly contract: Contract<D>
281
+ readonly reducer: Reducer<D, AIState<M>>
282
+ }
283
+
284
+ export type AgentToolEventDefs = EventDefs &
285
+ Pick<AIEventDefs, 'ai.tool.called' | 'ai.approval.responded'>
286
+
287
+ /** The durable handler context supplied when A2 executes a local AI tool. */
288
+ export type AgentToolContext<D extends AgentToolEventDefs> =
289
+ | HandlerContext<D, 'ai.tool.called'>
290
+ | HandlerContext<D, 'ai.approval.responded'>
291
+
292
+ export type AgentOptions<M extends UIMessage, E extends EventDefs> = {
293
+ name: string
294
+ events?: E
295
+ /** Optional stricter schema for custom UIMessage metadata/data/tool types. */
296
+ messageSchema?: StandardSchemaV1<M>
297
+ /** Snapshot identity. Bump when application-level projection semantics change. */
298
+ reducerName?: string
299
+ }
300
+
301
+ type AIMessageFrom<D extends EventDefs> =
302
+ D extends Record<'ai.message.created', infer S extends StandardSchemaV1>
303
+ ? StandardSchemaV1.InferOutput<S> extends MessageCreatedPayload<
304
+ infer M extends UIMessage
305
+ >
306
+ ? M
307
+ : UIMessage
308
+ : UIMessage
309
+
310
+ type SchemaResult<T> = StandardSchemaV1.Result<T>
311
+
312
+ const issue = <T>(message: string): SchemaResult<T> => ({
313
+ issues: [{ message }],
314
+ })
315
+
316
+ const schema = <T>(
317
+ label: string,
318
+ parse: (value: unknown) => SchemaResult<T>,
319
+ ): StandardSchemaV1<T> => ({
320
+ '~standard': {
321
+ version: 1,
322
+ vendor: 'a2',
323
+ validate(value) {
324
+ try {
325
+ return parse(value)
326
+ } catch (error) {
327
+ return issue(
328
+ `${label}: ${error instanceof Error ? error.message : String(error)}`,
329
+ )
330
+ }
331
+ },
332
+ },
333
+ })
334
+
335
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
336
+ typeof value === 'object' && value !== null && !Array.isArray(value)
337
+
338
+ const isJSONCompatible = (
339
+ value: unknown,
340
+ seen: Set<object> = new Set(),
341
+ ): boolean => {
342
+ if (
343
+ value === null ||
344
+ typeof value === 'string' ||
345
+ typeof value === 'boolean'
346
+ ) {
347
+ return true
348
+ }
349
+ if (typeof value === 'number') return Number.isFinite(value)
350
+ if (typeof value === 'undefined') return true
351
+ if (typeof value !== 'object') return false
352
+ if (seen.has(value)) return false
353
+ seen.add(value)
354
+ const valid = Array.isArray(value)
355
+ ? value.every((item) => isJSONCompatible(item, seen))
356
+ : Object.getPrototypeOf(value) === Object.prototype &&
357
+ Object.values(value).every((item) => isJSONCompatible(item, seen))
358
+ seen.delete(value)
359
+ return valid
360
+ }
361
+
362
+ const jsonObject = <T extends object>(
363
+ label: string,
364
+ check: (value: Record<string, unknown>) => boolean,
365
+ ): StandardSchemaV1<T> =>
366
+ schema(label, (value) => {
367
+ if (!isRecord(value) || !isJSONCompatible(value) || !check(value)) {
368
+ return issue(`invalid ${label}`)
369
+ }
370
+ return { value: value as T }
371
+ })
372
+
373
+ const stringField = (value: Record<string, unknown>, key: string): boolean =>
374
+ typeof value[key] === 'string' && value[key].length > 0
375
+
376
+ const optionalStringField = (
377
+ value: Record<string, unknown>,
378
+ key: string,
379
+ ): boolean => value[key] === undefined || typeof value[key] === 'string'
380
+
381
+ const messageValue = (value: unknown): value is UIMessage =>
382
+ isRecord(value) &&
383
+ typeof value['id'] === 'string' &&
384
+ (value['role'] === 'user' ||
385
+ value['role'] === 'assistant' ||
386
+ value['role'] === 'system') &&
387
+ Array.isArray(value['parts']) &&
388
+ value['parts'].every(
389
+ (part) => isRecord(part) && typeof part['type'] === 'string',
390
+ ) &&
391
+ isJSONCompatible(value)
392
+
393
+ const validateMessage = <M extends UIMessage>(
394
+ value: unknown,
395
+ messageSchema?: StandardSchemaV1<M>,
396
+ ): SchemaResult<M> => {
397
+ if (!messageValue(value)) return issue('invalid AI SDK UIMessage')
398
+ if (!messageSchema) return { value: value as M }
399
+ return validateSync(messageSchema, value, 'the AI message')
400
+ }
401
+
402
+ const messagePayloadSchema = <M extends UIMessage>(
403
+ messageSchema?: StandardSchemaV1<M>,
404
+ ): StandardSchemaV1<MessageCreatedPayload<M>> =>
405
+ schema('ai.message.created', (value) => {
406
+ if (!isRecord(value)) return issue('invalid ai.message.created payload')
407
+ const message = validateMessage(value['message'], messageSchema)
408
+ if (message.issues) return { issues: message.issues }
409
+ const generate = value['generate']
410
+ if (generate !== undefined && typeof generate !== 'boolean') {
411
+ return issue('invalid ai.message.created generate flag')
412
+ }
413
+ return {
414
+ value: {
415
+ message: message.value,
416
+ ...(generate === false ? { generate: false } : {}),
417
+ },
418
+ }
419
+ })
420
+
421
+ const progressPayloadSchema: StandardSchemaV1<GenerationProgressPayload> =
422
+ schema('ai.generation.progress', (value) => {
423
+ if (
424
+ !isRecord(value) ||
425
+ !isJSONCompatible(value) ||
426
+ !stringField(value, 'requestId') ||
427
+ !stringField(value, 'messageId') ||
428
+ !stringField(value, 'generationId') ||
429
+ !stringField(value, 'responseMessageId') ||
430
+ typeof value['sequence'] !== 'number' ||
431
+ !Number.isInteger(value['sequence']) ||
432
+ value['sequence'] < 0 ||
433
+ !Array.isArray(value['chunks']) ||
434
+ !value['chunks'].every(
435
+ (chunk) => isRecord(chunk) && typeof chunk['type'] === 'string',
436
+ )
437
+ ) {
438
+ return issue('invalid ai.generation.progress payload')
439
+ }
440
+ return { value: value as GenerationProgressPayload }
441
+ })
442
+
443
+ const completedPayloadSchema: StandardSchemaV1<GenerationCompletedPayload> =
444
+ schema('ai.generation.completed', (value) => {
445
+ if (
446
+ !isRecord(value) ||
447
+ !isJSONCompatible(value) ||
448
+ !stringField(value, 'requestId') ||
449
+ !stringField(value, 'messageId') ||
450
+ !stringField(value, 'generationId') ||
451
+ !stringField(value, 'responseMessageId') ||
452
+ !optionalStringField(value, 'finishReason')
453
+ ) {
454
+ return issue('invalid ai.generation.completed payload')
455
+ }
456
+ return { value: value as GenerationCompletedPayload }
457
+ })
458
+
459
+ const compactionPayloadSchema = <M extends UIMessage>(
460
+ messageSchema?: StandardSchemaV1<M>,
461
+ ): StandardSchemaV1<CompactionCompletedPayload<M>> =>
462
+ schema('ai.compaction.completed', (value) => {
463
+ if (
464
+ !isRecord(value) ||
465
+ !isJSONCompatible(value) ||
466
+ !stringField(value, 'generationId') ||
467
+ !stringField(value, 'throughMessageId') ||
468
+ !Array.isArray(value['messages']) ||
469
+ (value['retainedMessageIds'] !== undefined &&
470
+ (!Array.isArray(value['retainedMessageIds']) ||
471
+ !value['retainedMessageIds'].every(
472
+ (candidate) => typeof candidate === 'string',
473
+ )))
474
+ ) {
475
+ return issue('invalid ai.compaction.completed payload')
476
+ }
477
+ const messages: M[] = []
478
+ for (const candidate of value['messages']) {
479
+ const result = validateMessage(candidate, messageSchema)
480
+ if (result.issues) return { issues: result.issues }
481
+ messages.push(result.value)
482
+ }
483
+ return {
484
+ value: {
485
+ generationId: value['generationId'] as string,
486
+ throughMessageId: value['throughMessageId'] as string,
487
+ messages,
488
+ ...(value['retainedMessageIds'] === undefined
489
+ ? {}
490
+ : { retainedMessageIds: value['retainedMessageIds'] as string[] }),
491
+ },
492
+ }
493
+ })
494
+
495
+ export function createEvents<M extends UIMessage = UIMessage>(options?: {
496
+ messageSchema?: StandardSchemaV1<M>
497
+ }): AIEventDefs<M> {
498
+ const messageSchema = options?.messageSchema
499
+ if (messageSchema) assertSyncSchema(messageSchema, 'the AI message schema')
500
+ const definition: AIEventDefs<M> = {
501
+ 'ai.session.created': jsonObject('ai.session.created', () => true),
502
+ 'ai.session.closed': jsonObject('ai.session.closed', (value) =>
503
+ optionalStringField(value, 'reason'),
504
+ ),
505
+ 'ai.message.created': messagePayloadSchema(messageSchema),
506
+ 'ai.message.completed': jsonObject('ai.message.completed', (value) =>
507
+ stringField(value, 'messageId'),
508
+ ),
509
+ 'ai.message.interrupted': jsonObject(
510
+ 'ai.message.interrupted',
511
+ (value) =>
512
+ stringField(value, 'messageId') &&
513
+ ((stringField(value, 'generationId') &&
514
+ value['requestId'] === undefined) ||
515
+ (stringField(value, 'requestId') &&
516
+ value['generationId'] === undefined)) &&
517
+ optionalStringField(value, 'reason') &&
518
+ (value['lastSeenIndex'] === undefined ||
519
+ (typeof value['lastSeenIndex'] === 'number' &&
520
+ Number.isInteger(value['lastSeenIndex']) &&
521
+ value['lastSeenIndex'] >= 0)),
522
+ ),
523
+ 'ai.generation.requested': jsonObject(
524
+ 'ai.generation.requested',
525
+ (value) =>
526
+ stringField(value, 'messageId') &&
527
+ optionalStringField(value, 'responseMessageId') &&
528
+ (value['reason'] === 'message' ||
529
+ value['reason'] === 'tool' ||
530
+ value['reason'] === 'input' ||
531
+ value['reason'] === 'retry'),
532
+ ),
533
+ 'ai.retry.requested': jsonObject(
534
+ 'ai.retry.requested',
535
+ (value) =>
536
+ stringField(value, 'messageId') &&
537
+ stringField(value, 'responseMessageId') &&
538
+ stringField(value, 'retryId'),
539
+ ),
540
+ 'ai.generation.started': jsonObject(
541
+ 'ai.generation.started',
542
+ (value) =>
543
+ stringField(value, 'requestId') &&
544
+ stringField(value, 'messageId') &&
545
+ stringField(value, 'generationId') &&
546
+ stringField(value, 'responseMessageId') &&
547
+ typeof value['attempt'] === 'number' &&
548
+ Number.isInteger(value['attempt']) &&
549
+ value['attempt'] > 0 &&
550
+ stringField(value, 'model'),
551
+ ),
552
+ 'ai.generation.progress': progressPayloadSchema,
553
+ 'ai.generation.completed': completedPayloadSchema,
554
+ 'ai.generation.failed': jsonObject(
555
+ 'ai.generation.failed',
556
+ (value) =>
557
+ stringField(value, 'requestId') &&
558
+ stringField(value, 'messageId') &&
559
+ stringField(value, 'generationId') &&
560
+ stringField(value, 'responseMessageId') &&
561
+ stringField(value, 'error') &&
562
+ (value['superseded'] === undefined ||
563
+ typeof value['superseded'] === 'boolean') &&
564
+ (value['stepLimit'] === undefined ||
565
+ typeof value['stepLimit'] === 'boolean'),
566
+ ),
567
+ 'ai.tool.called': jsonObject(
568
+ 'ai.tool.called',
569
+ (value) =>
570
+ stringField(value, 'requestId') &&
571
+ stringField(value, 'messageId') &&
572
+ stringField(value, 'generationId') &&
573
+ stringField(value, 'toolCallId') &&
574
+ stringField(value, 'toolName') &&
575
+ (value['dynamic'] === undefined ||
576
+ typeof value['dynamic'] === 'boolean') &&
577
+ (value['providerExecuted'] === undefined ||
578
+ typeof value['providerExecuted'] === 'boolean') &&
579
+ (value['supportsDeferredResults'] === undefined ||
580
+ typeof value['supportsDeferredResults'] === 'boolean') &&
581
+ optionalStringField(value, 'title'),
582
+ ),
583
+ 'ai.tool.result': jsonObject(
584
+ 'ai.tool.result',
585
+ (value) =>
586
+ stringField(value, 'requestId') &&
587
+ stringField(value, 'messageId') &&
588
+ stringField(value, 'generationId') &&
589
+ stringField(value, 'toolCallId') &&
590
+ optionalStringField(value, 'toolName') &&
591
+ optionalStringField(value, 'error') &&
592
+ (value['denied'] === undefined ||
593
+ typeof value['denied'] === 'boolean') &&
594
+ (value['preliminary'] === undefined ||
595
+ typeof value['preliminary'] === 'boolean') &&
596
+ (value['phase'] === undefined ||
597
+ value['phase'] === 'input' ||
598
+ value['phase'] === 'execution') &&
599
+ (value['dynamic'] === undefined ||
600
+ typeof value['dynamic'] === 'boolean') &&
601
+ (value['providerExecuted'] === undefined ||
602
+ typeof value['providerExecuted'] === 'boolean'),
603
+ ),
604
+ 'ai.approval.requested': jsonObject(
605
+ 'ai.approval.requested',
606
+ (value) =>
607
+ stringField(value, 'messageId') &&
608
+ stringField(value, 'generationId') &&
609
+ stringField(value, 'approvalId') &&
610
+ stringField(value, 'toolCallId') &&
611
+ (value['isAutomatic'] === undefined ||
612
+ typeof value['isAutomatic'] === 'boolean') &&
613
+ optionalStringField(value, 'signature'),
614
+ ),
615
+ 'ai.approval.responded': jsonObject(
616
+ 'ai.approval.responded',
617
+ (value) =>
618
+ stringField(value, 'messageId') &&
619
+ stringField(value, 'generationId') &&
620
+ stringField(value, 'approvalId') &&
621
+ typeof value['approved'] === 'boolean' &&
622
+ optionalStringField(value, 'reason'),
623
+ ),
624
+ 'ai.input.requested': jsonObject(
625
+ 'ai.input.requested',
626
+ (value) =>
627
+ stringField(value, 'messageId') &&
628
+ stringField(value, 'generationId') &&
629
+ stringField(value, 'inputId') &&
630
+ stringField(value, 'name'),
631
+ ),
632
+ 'ai.input.responded': jsonObject(
633
+ 'ai.input.responded',
634
+ (value) =>
635
+ stringField(value, 'messageId') &&
636
+ stringField(value, 'generationId') &&
637
+ stringField(value, 'inputId') &&
638
+ stringField(value, 'name'),
639
+ ),
640
+ 'ai.compaction.requested': jsonObject(
641
+ 'ai.compaction.requested',
642
+ (value) =>
643
+ stringField(value, 'generationId') &&
644
+ stringField(value, 'throughMessageId'),
645
+ ),
646
+ 'ai.compaction.completed': compactionPayloadSchema(messageSchema),
647
+ }
648
+ return definition
649
+ }
650
+
651
+ export const events: AIEventDefs<UIMessage> = createEvents()
652
+
653
+ const initialState = <M extends UIMessage>(): AIState<M> => ({
654
+ session: { created: false },
655
+ messages: [],
656
+ status: 'idle',
657
+ activeGeneration: null,
658
+ activeRequestId: null,
659
+ activeResponseMessageId: null,
660
+ activeProjection: null,
661
+ responseGenerationIds: {},
662
+ pendingApprovals: [],
663
+ pendingInputs: [],
664
+ tools: [],
665
+ compaction: null,
666
+ usage: [],
667
+ error: null,
668
+ })
669
+
670
+ const upsertMessage = <M extends UIMessage>(messages: M[], message: M): M[] => {
671
+ const index = messages.findIndex((candidate) => candidate.id === message.id)
672
+ if (index === -1) return [...messages, message]
673
+ const next = [...messages]
674
+ next[index] = message
675
+ return next
676
+ }
677
+
678
+ const answerApproval = <M extends UIMessage>(
679
+ messages: M[],
680
+ response: ApprovalRespondedPayload,
681
+ ): M[] =>
682
+ messages.map((message) => {
683
+ if (message.id !== response.messageId) return message
684
+ let changed = false
685
+ const parts = message.parts.map((part) => {
686
+ const candidate = part as unknown
687
+ if (
688
+ !isRecord(candidate) ||
689
+ candidate['state'] !== 'approval-requested' ||
690
+ !isRecord(candidate['approval']) ||
691
+ candidate['approval']['id'] !== response.approvalId
692
+ ) {
693
+ return part
694
+ }
695
+ changed = true
696
+ return {
697
+ ...part,
698
+ state: 'approval-responded',
699
+ approval: {
700
+ ...candidate['approval'],
701
+ approved: response.approved,
702
+ ...(response.reason === undefined ? {} : { reason: response.reason }),
703
+ },
704
+ } as typeof part
705
+ })
706
+ return changed ? ({ ...message, parts } as M) : message
707
+ })
708
+
709
+ const projectMessageChunks = <M extends UIMessage>(
710
+ messages: M[],
711
+ messageId: string,
712
+ chunks: UIMessageChunk[],
713
+ ): M[] =>
714
+ messages.map((message) =>
715
+ message.id === messageId
716
+ ? projectUIMessage({ messageId, base: message, chunks })
717
+ : message,
718
+ )
719
+
720
+ const approvalRequestedChunk = (
721
+ payload: ApprovalRequestedPayload,
722
+ ): UIMessageChunk => ({
723
+ type: 'tool-approval-request',
724
+ approvalId: payload.approvalId,
725
+ toolCallId: payload.toolCallId,
726
+ ...(payload.isAutomatic === undefined
727
+ ? {}
728
+ : { isAutomatic: payload.isAutomatic }),
729
+ ...(payload.signature === undefined ? {} : { signature: payload.signature }),
730
+ })
731
+
732
+ const approvalRespondedChunk = (
733
+ payload: ApprovalRespondedPayload,
734
+ ): UIMessageChunk => ({
735
+ type: 'tool-approval-response',
736
+ approvalId: payload.approvalId,
737
+ approved: payload.approved,
738
+ ...(payload.reason === undefined ? {} : { reason: payload.reason }),
739
+ })
740
+
741
+ const toolResultChunk = (payload: ToolResultPayload): UIMessageChunk => {
742
+ if (payload.phase === 'input') {
743
+ return {
744
+ type: 'tool-input-error',
745
+ toolCallId: payload.toolCallId,
746
+ toolName: payload.toolName ?? 'unknown',
747
+ input: payload.rawInput ?? payload.input,
748
+ errorText: payload.error ?? 'Invalid tool input',
749
+ ...(payload.dynamic === undefined ? {} : { dynamic: payload.dynamic }),
750
+ ...(payload.providerExecuted === undefined
751
+ ? {}
752
+ : { providerExecuted: payload.providerExecuted }),
753
+ ...(payload.providerMetadata === undefined
754
+ ? {}
755
+ : { providerMetadata: payload.providerMetadata }),
756
+ ...(payload.toolMetadata === undefined
757
+ ? {}
758
+ : { toolMetadata: payload.toolMetadata }),
759
+ } as UIMessageChunk
760
+ }
761
+ if (payload.denied === true) {
762
+ return {
763
+ type: 'tool-output-denied',
764
+ toolCallId: payload.toolCallId,
765
+ } as UIMessageChunk
766
+ }
767
+ if (payload.error !== undefined) {
768
+ return {
769
+ type: 'tool-output-error',
770
+ toolCallId: payload.toolCallId,
771
+ errorText: payload.error,
772
+ ...(payload.dynamic === undefined ? {} : { dynamic: payload.dynamic }),
773
+ ...(payload.providerExecuted === undefined
774
+ ? {}
775
+ : { providerExecuted: payload.providerExecuted }),
776
+ ...(payload.providerMetadata === undefined
777
+ ? {}
778
+ : { providerMetadata: payload.providerMetadata }),
779
+ ...(payload.toolMetadata === undefined
780
+ ? {}
781
+ : { toolMetadata: payload.toolMetadata }),
782
+ } as UIMessageChunk
783
+ }
784
+ return {
785
+ type: 'tool-output-available',
786
+ toolCallId: payload.toolCallId,
787
+ output: payload.output,
788
+ ...(payload.preliminary === undefined
789
+ ? {}
790
+ : { preliminary: payload.preliminary }),
791
+ ...(payload.dynamic === undefined ? {} : { dynamic: payload.dynamic }),
792
+ ...(payload.providerExecuted === undefined
793
+ ? {}
794
+ : { providerExecuted: payload.providerExecuted }),
795
+ ...(payload.providerMetadata === undefined
796
+ ? {}
797
+ : { providerMetadata: payload.providerMetadata }),
798
+ ...(payload.toolMetadata === undefined
799
+ ? {}
800
+ : { toolMetadata: payload.toolMetadata }),
801
+ } as UIMessageChunk
802
+ }
803
+
804
+ const waitingStatus = <M extends UIMessage>(state: AIState<M>): AgentStatus => {
805
+ if (state.pendingApprovals.length > 0 || state.pendingInputs.length > 0) {
806
+ return 'waiting'
807
+ }
808
+ return state.activeGeneration !== null ||
809
+ state.tools.some((activity) => activity.status === 'running')
810
+ ? 'generating'
811
+ : 'idle'
812
+ }
813
+
814
+ const lifecycleStatus = <M extends UIMessage>(
815
+ previous: AIState<M>,
816
+ next: AIState<M>,
817
+ ): AgentStatus =>
818
+ previous.status === 'failed' ? 'failed' : waitingStatus(next)
819
+
820
+ const isStaleGeneration = <M extends UIMessage>(
821
+ state: AIState<M>,
822
+ messageId: string,
823
+ generationId: string,
824
+ ): boolean => {
825
+ const current = state.responseGenerationIds[messageId]
826
+ return current !== undefined && current !== generationId
827
+ }
828
+
829
+ const projectedMessage = <M extends UIMessage>(
830
+ projection: ActiveGenerationProjection<M>,
831
+ ): M => {
832
+ const projectionEvents: Array<{ index: number; chunks: UIMessageChunk[] }> = [
833
+ ...projection.batches,
834
+ ]
835
+ for (const event of projection.toolEvents) {
836
+ if (event.type === 'ai.tool.result') {
837
+ projectionEvents.push({
838
+ index: event.index,
839
+ chunks: [toolResultChunk(event.payload)],
840
+ })
841
+ }
842
+ }
843
+ for (const event of projection.approvalEvents) {
844
+ projectionEvents.push({
845
+ index: event.index,
846
+ chunks: [
847
+ event.type === 'ai.approval.requested'
848
+ ? approvalRequestedChunk(event.payload)
849
+ : approvalRespondedChunk(event.payload),
850
+ ],
851
+ })
852
+ }
853
+ return projectUIMessage({
854
+ messageId: projection.responseMessageId,
855
+ ...(projection.baseMessage === undefined
856
+ ? {}
857
+ : { base: projection.baseMessage }),
858
+ chunks: projectionEvents
859
+ .toSorted((left, right) => left.index - right.index)
860
+ .flatMap((event) => event.chunks),
861
+ })
862
+ }
863
+
864
+ const reduceToolActivity = (
865
+ tools: ToolActivity[],
866
+ event:
867
+ | { type: 'ai.tool.called'; payload: ToolCalledPayload }
868
+ | { type: 'ai.tool.result'; payload: ToolResultPayload },
869
+ ): ToolActivity[] => {
870
+ if (event.type === 'ai.tool.called') {
871
+ const payload = event.payload
872
+ const { providerMetadata, ...called } = payload
873
+ const activity: ToolActivity = {
874
+ ...called,
875
+ ...(providerMetadata === undefined
876
+ ? {}
877
+ : { callProviderMetadata: providerMetadata }),
878
+ status: 'running',
879
+ }
880
+ return [
881
+ ...tools.filter(
882
+ (candidate) =>
883
+ candidate.generationId !== payload.generationId ||
884
+ candidate.toolCallId !== payload.toolCallId,
885
+ ),
886
+ activity,
887
+ ]
888
+ }
889
+
890
+ const payload = event.payload
891
+ const existing = tools.find(
892
+ (candidate) =>
893
+ candidate.generationId === payload.generationId &&
894
+ candidate.toolCallId === payload.toolCallId,
895
+ )
896
+ const toolName = payload.toolName ?? existing?.toolName
897
+ const input = payload.input === undefined ? existing?.input : payload.input
898
+ const dynamic = payload.dynamic ?? existing?.dynamic
899
+ const providerExecuted =
900
+ payload.providerExecuted ?? existing?.providerExecuted
901
+ const toolMetadata = payload.toolMetadata ?? existing?.toolMetadata
902
+ const resultProviderMetadata =
903
+ payload.providerMetadata ?? existing?.resultProviderMetadata
904
+ const activity: ToolActivity = {
905
+ requestId: payload.requestId,
906
+ messageId: payload.messageId,
907
+ generationId: payload.generationId,
908
+ toolCallId: payload.toolCallId,
909
+ ...(toolName === undefined ? {} : { toolName }),
910
+ ...(input === undefined ? {} : { input }),
911
+ ...(payload.rawInput === undefined ? {} : { rawInput: payload.rawInput }),
912
+ ...(payload.output === undefined ? {} : { output: payload.output }),
913
+ ...(payload.error === undefined ? {} : { error: payload.error }),
914
+ ...(payload.preliminary === undefined
915
+ ? {}
916
+ : { preliminary: payload.preliminary }),
917
+ ...(payload.phase === undefined ? {} : { phase: payload.phase }),
918
+ ...(dynamic === undefined ? {} : { dynamic }),
919
+ ...(providerExecuted === undefined ? {} : { providerExecuted }),
920
+ ...(existing?.callProviderMetadata === undefined
921
+ ? {}
922
+ : { callProviderMetadata: existing.callProviderMetadata }),
923
+ ...(resultProviderMetadata === undefined ? {} : { resultProviderMetadata }),
924
+ ...(toolMetadata === undefined ? {} : { toolMetadata }),
925
+ ...(existing?.title === undefined ? {} : { title: existing.title }),
926
+ status: payload.denied
927
+ ? 'denied'
928
+ : payload.error
929
+ ? 'failed'
930
+ : payload.preliminary
931
+ ? 'running'
932
+ : 'completed',
933
+ }
934
+ return [
935
+ ...tools.filter(
936
+ (candidate) =>
937
+ candidate.generationId !== payload.generationId ||
938
+ candidate.toolCallId !== payload.toolCallId,
939
+ ),
940
+ activity,
941
+ ]
942
+ }
943
+
944
+ const projectedTools = <M extends UIMessage>(
945
+ projection: ActiveGenerationProjection<M>,
946
+ ): ToolActivity[] =>
947
+ projection.toolEvents.reduce(
948
+ (tools, event) => reduceToolActivity(tools, event),
949
+ projection.baseTools,
950
+ )
951
+
952
+ const restoreGenerationBase = <M extends UIMessage>(
953
+ state: AIState<M>,
954
+ generationId: string,
955
+ responseMessageId: string,
956
+ reason: 'interrupted' | 'superseded',
957
+ ): AIState<M> => {
958
+ const projection =
959
+ state.activeProjection?.generationId === generationId
960
+ ? state.activeProjection
961
+ : null
962
+ return {
963
+ ...state,
964
+ messages:
965
+ projection?.baseMessage === undefined
966
+ ? state.messages.filter((message) => message.id !== responseMessageId)
967
+ : upsertMessage(state.messages, projection.baseMessage),
968
+ tools:
969
+ projection === null
970
+ ? state.tools.filter(
971
+ (activity) => activity.generationId !== generationId,
972
+ )
973
+ : projection.baseTools,
974
+ pendingApprovals: state.pendingApprovals.filter(
975
+ (approval) => approval.generationId !== generationId,
976
+ ),
977
+ pendingInputs: state.pendingInputs.filter(
978
+ (input) => input.generationId !== generationId,
979
+ ),
980
+ activeGeneration: null,
981
+ activeProjection: null,
982
+ terminalGenerations: {
983
+ ...state.terminalGenerations,
984
+ [generationId]: reason,
985
+ },
986
+ compaction:
987
+ projection === null
988
+ ? state.compaction?.generationId === generationId
989
+ ? null
990
+ : state.compaction
991
+ : (projection.baseCompaction ?? null),
992
+ }
993
+ }
994
+
995
+ const stopGenerationTools = (
996
+ tools: ToolActivity[],
997
+ generationId: string | undefined,
998
+ responseMessageId: string,
999
+ error: string,
1000
+ ): ToolActivity[] =>
1001
+ tools.map((activity) =>
1002
+ activity.status === 'running' &&
1003
+ (generationId === undefined
1004
+ ? activity.messageId === responseMessageId
1005
+ : activity.generationId === generationId)
1006
+ ? { ...activity, status: 'failed', error, preliminary: false }
1007
+ : activity,
1008
+ )
1009
+
1010
+ const interruptGenerationProjection = <M extends UIMessage>(
1011
+ state: AIState<M>,
1012
+ generationId: string,
1013
+ responseMessageId: string,
1014
+ cutoff: number,
1015
+ ): AIState<M> | null => {
1016
+ const projection =
1017
+ state.activeProjection?.generationId === generationId
1018
+ ? state.activeProjection
1019
+ : null
1020
+ if (projection === null) return null
1021
+ const retained: ActiveGenerationProjection<M> = {
1022
+ ...projection,
1023
+ batches: projection.batches.filter((batch) => batch.index <= cutoff),
1024
+ toolEvents: projection.toolEvents.filter(
1025
+ (toolEvent) => toolEvent.index <= cutoff,
1026
+ ),
1027
+ approvalEvents: projection.approvalEvents.filter(
1028
+ (approvalEvent) => approvalEvent.index <= cutoff,
1029
+ ),
1030
+ }
1031
+ const shouldProject =
1032
+ retained.baseMessage !== undefined || retained.batches.length > 0
1033
+ const restored = restoreGenerationBase(
1034
+ state,
1035
+ generationId,
1036
+ responseMessageId,
1037
+ 'interrupted',
1038
+ )
1039
+ return {
1040
+ ...restored,
1041
+ messages: shouldProject
1042
+ ? upsertMessage(
1043
+ restored.messages,
1044
+ interruptUIMessage(projectedMessage(retained)),
1045
+ )
1046
+ : restored.messages,
1047
+ tools: stopGenerationTools(
1048
+ projectedTools(retained),
1049
+ generationId,
1050
+ responseMessageId,
1051
+ 'Tool execution was interrupted.',
1052
+ ),
1053
+ usage:
1054
+ projection.completionIndex === undefined ||
1055
+ projection.completionIndex <= cutoff
1056
+ ? restored.usage
1057
+ : restored.usage.filter((entry) => entry.generationId !== generationId),
1058
+ }
1059
+ }
1060
+
1061
+ const generationRequestId = (generationId: string): string | undefined => {
1062
+ const marker = ':generation:'
1063
+ const markerIndex = generationId.lastIndexOf(marker)
1064
+ return markerIndex === -1 ? undefined : generationId.slice(0, markerIndex)
1065
+ }
1066
+
1067
+ const terminalGenerationReason = <M extends UIMessage>(
1068
+ state: AIState<M>,
1069
+ generationId: string,
1070
+ ): 'completed' | 'failed' | 'interrupted' | 'superseded' | undefined => {
1071
+ const terminal = state.terminalGenerations?.[generationId]
1072
+ if (terminal !== undefined) return terminal
1073
+ const requestId = generationRequestId(generationId)
1074
+ return requestId !== undefined &&
1075
+ state.terminalRequestIds?.[requestId] === true
1076
+ ? 'interrupted'
1077
+ : undefined
1078
+ }
1079
+
1080
+ /** Pure projection of the built-in AI protocol. Unknown extension events are ignored. */
1081
+ export function reduceAIState<M extends UIMessage>(
1082
+ state: AIState<M>,
1083
+ event: { type: string; payload: unknown; index: number; id?: string },
1084
+ ): AIState<M> {
1085
+ if (state.status === 'closed') return state
1086
+ switch (event.type) {
1087
+ case 'ai.session.created': {
1088
+ const payload = event.payload as SessionCreatedPayload
1089
+ return {
1090
+ ...state,
1091
+ session: {
1092
+ created: true,
1093
+ ...(payload.metadata === undefined
1094
+ ? state.session.metadata === undefined
1095
+ ? {}
1096
+ : { metadata: state.session.metadata }
1097
+ : { metadata: payload.metadata }),
1098
+ },
1099
+ }
1100
+ }
1101
+ case 'ai.session.closed':
1102
+ return {
1103
+ ...state,
1104
+ status: 'closed',
1105
+ activeGeneration: null,
1106
+ activeRequestId: null,
1107
+ activeResponseMessageId: null,
1108
+ activeProjection: null,
1109
+ }
1110
+ case 'ai.message.created': {
1111
+ const { message } = event.payload as MessageCreatedPayload<M>
1112
+ return { ...state, messages: upsertMessage(state.messages, message) }
1113
+ }
1114
+ case 'ai.message.completed': {
1115
+ const { messageId } = event.payload as MessageCompletedPayload
1116
+ const generationId = state.responseGenerationIds[messageId]
1117
+ if (
1118
+ generationId === undefined ||
1119
+ terminalGenerationReason(state, generationId) !== undefined
1120
+ ) {
1121
+ return state
1122
+ }
1123
+ const requestId = generationRequestId(generationId)
1124
+ return {
1125
+ ...state,
1126
+ activeRequestId: null,
1127
+ activeResponseMessageId: null,
1128
+ activeProjection:
1129
+ state.activeProjection?.generationId === generationId
1130
+ ? null
1131
+ : state.activeProjection,
1132
+ terminalGenerations: {
1133
+ ...state.terminalGenerations,
1134
+ [generationId]: 'completed',
1135
+ },
1136
+ ...(requestId === undefined
1137
+ ? {}
1138
+ : {
1139
+ terminalRequestIds: {
1140
+ ...state.terminalRequestIds,
1141
+ [requestId]: true,
1142
+ },
1143
+ }),
1144
+ }
1145
+ }
1146
+ case 'ai.generation.requested': {
1147
+ const payload = event.payload as GenerationRequestedPayload
1148
+ const schedulesToolContinuation = payload.reason === 'tool'
1149
+ const responseMessageId =
1150
+ payload.responseMessageId ??
1151
+ (payload.reason === 'message'
1152
+ ? `${payload.messageId}:assistant`
1153
+ : payload.messageId)
1154
+ const ownerGenerationId =
1155
+ state.responseGenerationIds[responseMessageId] ??
1156
+ state.activeGeneration?.generationId
1157
+ const ownerRequestId =
1158
+ ownerGenerationId === undefined
1159
+ ? undefined
1160
+ : generationRequestId(ownerGenerationId)
1161
+ const requestId = (event as { id?: string }).id
1162
+ if (
1163
+ requestId !== undefined &&
1164
+ state.terminalRequestIds?.[requestId] === true
1165
+ ) {
1166
+ return state
1167
+ }
1168
+ const acceptsRequest =
1169
+ payload.reason === 'message' ||
1170
+ (payload.reason === 'tool'
1171
+ ? ownerGenerationId !== undefined &&
1172
+ requestId === `ai.generate:tools:${ownerGenerationId}` &&
1173
+ state.activeRequestId === ownerRequestId &&
1174
+ state.status !== 'failed'
1175
+ : payload.reason === 'retry'
1176
+ ? state.status === 'failed'
1177
+ : ownerGenerationId !== undefined &&
1178
+ requestId?.startsWith(
1179
+ `ai.generate:input:${encodeURIComponent(responseMessageId)}:${encodeURIComponent(ownerGenerationId)}:`,
1180
+ ) === true &&
1181
+ state.activeRequestId === ownerRequestId &&
1182
+ state.status !== 'failed')
1183
+ if (!acceptsRequest) return state
1184
+ return {
1185
+ ...state,
1186
+ messages:
1187
+ payload.reason === 'retry' && payload.responseMessageId !== undefined
1188
+ ? state.messages.filter(
1189
+ (message) => message.id !== payload.responseMessageId,
1190
+ )
1191
+ : state.messages,
1192
+ activeProjection:
1193
+ payload.reason === 'retry' ? null : state.activeProjection,
1194
+ status: schedulesToolContinuation ? state.status : 'generating',
1195
+ activeRequestId: requestId ?? null,
1196
+ activeResponseMessageId: responseMessageId,
1197
+ error: schedulesToolContinuation ? state.error : null,
1198
+ }
1199
+ }
1200
+ case 'ai.generation.started': {
1201
+ const payload = event.payload as GenerationStartedPayload
1202
+ if (
1203
+ state.terminalRequestIds?.[payload.requestId] === true ||
1204
+ terminalGenerationReason(state, payload.generationId) !== undefined
1205
+ ) {
1206
+ return state
1207
+ }
1208
+ if (
1209
+ state.activeRequestId !== null &&
1210
+ state.activeRequestId !== payload.requestId
1211
+ ) {
1212
+ return state
1213
+ }
1214
+ const currentOwner =
1215
+ state.responseGenerationIds[payload.responseMessageId]
1216
+ const currentTerminal =
1217
+ currentOwner === undefined
1218
+ ? undefined
1219
+ : terminalGenerationReason(state, currentOwner)
1220
+ if (
1221
+ currentTerminal === 'completed' ||
1222
+ currentTerminal === 'interrupted'
1223
+ ) {
1224
+ return state
1225
+ }
1226
+ const marker = ':generation:'
1227
+ const currentAttempt = currentOwner?.startsWith(
1228
+ `${payload.requestId}${marker}`,
1229
+ )
1230
+ ? Number(currentOwner.slice(`${payload.requestId}${marker}`.length))
1231
+ : undefined
1232
+ if (
1233
+ currentAttempt !== undefined &&
1234
+ Number.isFinite(currentAttempt) &&
1235
+ currentAttempt >= payload.attempt
1236
+ ) {
1237
+ return state
1238
+ }
1239
+ const activeGeneration = state.activeGeneration
1240
+ const replacesActiveAttempt =
1241
+ activeGeneration !== null &&
1242
+ activeGeneration.requestId === payload.requestId &&
1243
+ activeGeneration.responseMessageId === payload.responseMessageId &&
1244
+ activeGeneration.attempt < payload.attempt &&
1245
+ state.activeProjection?.generationId === activeGeneration.generationId
1246
+ const baseState = replacesActiveAttempt
1247
+ ? restoreGenerationBase(
1248
+ state,
1249
+ activeGeneration.generationId,
1250
+ activeGeneration.responseMessageId,
1251
+ 'superseded',
1252
+ )
1253
+ : state
1254
+ const baseMessage = baseState.messages.find(
1255
+ (message) => message.id === payload.responseMessageId,
1256
+ )
1257
+ return {
1258
+ ...baseState,
1259
+ status: 'generating',
1260
+ activeGeneration: payload,
1261
+ activeResponseMessageId: null,
1262
+ responseGenerationIds: {
1263
+ ...baseState.responseGenerationIds,
1264
+ [payload.responseMessageId]: payload.generationId,
1265
+ },
1266
+ activeProjection: {
1267
+ generationId: payload.generationId,
1268
+ responseMessageId: payload.responseMessageId,
1269
+ ...(baseMessage === undefined ? {} : { baseMessage }),
1270
+ baseCompaction: baseState.compaction,
1271
+ batches: [],
1272
+ baseTools: baseState.tools,
1273
+ toolEvents: [],
1274
+ approvalEvents: [],
1275
+ },
1276
+ error: null,
1277
+ }
1278
+ }
1279
+ case 'ai.generation.progress': {
1280
+ const payload = event.payload as GenerationProgressPayload
1281
+ if (terminalGenerationReason(state, payload.generationId) !== undefined) {
1282
+ return state
1283
+ }
1284
+ const activeProjection = state.activeProjection
1285
+ if (
1286
+ state.activeGeneration?.generationId !== payload.generationId ||
1287
+ activeProjection?.generationId !== payload.generationId
1288
+ ) {
1289
+ return state
1290
+ }
1291
+ const projection: ActiveGenerationProjection<M> = {
1292
+ ...activeProjection,
1293
+ batches: [
1294
+ ...activeProjection.batches.filter(
1295
+ (batch) => batch.index !== event.index,
1296
+ ),
1297
+ { index: event.index, chunks: payload.chunks },
1298
+ ].toSorted((left, right) => left.index - right.index),
1299
+ }
1300
+ return {
1301
+ ...state,
1302
+ messages: upsertMessage(state.messages, projectedMessage(projection)),
1303
+ activeProjection: projection,
1304
+ status: 'generating',
1305
+ }
1306
+ }
1307
+ case 'ai.generation.completed': {
1308
+ const payload = event.payload as GenerationCompletedPayload
1309
+ if (terminalGenerationReason(state, payload.generationId) !== undefined) {
1310
+ return state
1311
+ }
1312
+ if (state.activeGeneration?.generationId !== payload.generationId) {
1313
+ return state
1314
+ }
1315
+ const usage = payload.usage
1316
+ ? [
1317
+ ...state.usage.filter(
1318
+ (entry) => entry.generationId !== payload.generationId,
1319
+ ),
1320
+ { generationId: payload.generationId, usage: payload.usage },
1321
+ ]
1322
+ : state.usage
1323
+ const next = {
1324
+ ...state,
1325
+ activeGeneration: null,
1326
+ activeProjection:
1327
+ state.activeProjection?.generationId === payload.generationId
1328
+ ? { ...state.activeProjection, completionIndex: event.index }
1329
+ : state.activeProjection,
1330
+ usage,
1331
+ }
1332
+ return { ...next, status: waitingStatus(next) }
1333
+ }
1334
+ case 'ai.generation.failed': {
1335
+ const payload = event.payload as GenerationFailedPayload
1336
+ if (terminalGenerationReason(state, payload.generationId) !== undefined) {
1337
+ return state
1338
+ }
1339
+ if (
1340
+ state.activeGeneration?.generationId !== payload.generationId &&
1341
+ !(
1342
+ payload.stepLimit === true &&
1343
+ state.responseGenerationIds[payload.responseMessageId] ===
1344
+ payload.generationId &&
1345
+ state.activeRequestId ===
1346
+ `ai.generate:tools:${payload.generationId}` &&
1347
+ state.activeResponseMessageId === payload.responseMessageId
1348
+ )
1349
+ ) {
1350
+ return state
1351
+ }
1352
+ if (payload.superseded === true) {
1353
+ return restoreGenerationBase(
1354
+ state,
1355
+ payload.generationId,
1356
+ payload.responseMessageId,
1357
+ 'superseded',
1358
+ )
1359
+ }
1360
+ const projection =
1361
+ state.activeProjection?.generationId === payload.generationId
1362
+ ? state.activeProjection
1363
+ : null
1364
+ const shouldProject =
1365
+ projection !== null &&
1366
+ (projection.baseMessage !== undefined || projection.batches.length > 0)
1367
+ return {
1368
+ ...state,
1369
+ messages: shouldProject
1370
+ ? upsertMessage(
1371
+ state.messages,
1372
+ interruptUIMessage(
1373
+ projectedMessage(projection),
1374
+ `Generation failed before tool completion: ${payload.error}`,
1375
+ ),
1376
+ )
1377
+ : state.messages,
1378
+ status: 'failed',
1379
+ activeGeneration: null,
1380
+ activeResponseMessageId: null,
1381
+ activeProjection: projection,
1382
+ terminalGenerations: {
1383
+ ...state.terminalGenerations,
1384
+ [payload.generationId]: 'failed',
1385
+ },
1386
+ terminalRequestIds: {
1387
+ ...state.terminalRequestIds,
1388
+ [payload.requestId]: true,
1389
+ ...(state.activeRequestId === null
1390
+ ? {}
1391
+ : { [state.activeRequestId]: true as const }),
1392
+ },
1393
+ compaction:
1394
+ projection?.generationId === payload.generationId
1395
+ ? (projection.baseCompaction ?? null)
1396
+ : state.compaction?.generationId === payload.generationId
1397
+ ? null
1398
+ : state.compaction,
1399
+ pendingApprovals: state.pendingApprovals.filter(
1400
+ (approval) => approval.generationId !== payload.generationId,
1401
+ ),
1402
+ pendingInputs: state.pendingInputs.filter(
1403
+ (input) => input.generationId !== payload.generationId,
1404
+ ),
1405
+ tools: stopGenerationTools(
1406
+ state.tools,
1407
+ payload.generationId,
1408
+ payload.responseMessageId,
1409
+ payload.error,
1410
+ ),
1411
+ error: payload.error,
1412
+ }
1413
+ }
1414
+ case 'ai.message.interrupted': {
1415
+ const payload = event.payload as MessageInterruptedPayload
1416
+ const currentGenerationId = state.responseGenerationIds[payload.messageId]
1417
+ const targetRequestId =
1418
+ payload.requestId ?? generationRequestId(payload.generationId)
1419
+ const ownsRequestPhase =
1420
+ payload.requestId !== undefined &&
1421
+ state.activeGeneration === null &&
1422
+ state.activeRequestId === payload.requestId &&
1423
+ state.activeResponseMessageId === payload.messageId
1424
+ if (
1425
+ currentGenerationId !== undefined &&
1426
+ !ownsRequestPhase &&
1427
+ (payload.generationId !== undefined
1428
+ ? currentGenerationId !== payload.generationId
1429
+ : generationRequestId(currentGenerationId) !== payload.requestId)
1430
+ ) {
1431
+ return state
1432
+ }
1433
+ if (
1434
+ state.activeGeneration === null &&
1435
+ state.activeRequestId !== null &&
1436
+ state.activeResponseMessageId === payload.messageId &&
1437
+ targetRequestId !== state.activeRequestId
1438
+ ) {
1439
+ return state
1440
+ }
1441
+ const currentTerminal =
1442
+ currentGenerationId === undefined
1443
+ ? undefined
1444
+ : terminalGenerationReason(state, currentGenerationId)
1445
+ if (
1446
+ currentTerminal === 'completed' ||
1447
+ currentTerminal === 'interrupted' ||
1448
+ currentTerminal === 'superseded'
1449
+ ) {
1450
+ return state
1451
+ }
1452
+ const active = state.activeGeneration
1453
+ if (active === null) {
1454
+ if (
1455
+ payload.requestId !== undefined &&
1456
+ state.activeRequestId !== null &&
1457
+ state.activeRequestId === payload.requestId &&
1458
+ state.activeResponseMessageId === payload.messageId
1459
+ ) {
1460
+ const projectedState =
1461
+ currentGenerationId === undefined
1462
+ ? null
1463
+ : interruptGenerationProjection(
1464
+ state,
1465
+ currentGenerationId,
1466
+ payload.messageId,
1467
+ payload.lastSeenIndex ?? Number.POSITIVE_INFINITY,
1468
+ )
1469
+ const baseState = projectedState ?? state
1470
+ return {
1471
+ ...baseState,
1472
+ status: 'idle',
1473
+ activeRequestId: null,
1474
+ activeResponseMessageId: null,
1475
+ activeProjection: null,
1476
+ terminalRequestIds: {
1477
+ ...baseState.terminalRequestIds,
1478
+ [payload.requestId]: true,
1479
+ },
1480
+ ...(currentGenerationId === undefined
1481
+ ? {}
1482
+ : {
1483
+ terminalGenerations: {
1484
+ ...baseState.terminalGenerations,
1485
+ [currentGenerationId]: 'interrupted' as const,
1486
+ },
1487
+ }),
1488
+ error: null,
1489
+ }
1490
+ }
1491
+ const targetGenerationId =
1492
+ payload.generationId ??
1493
+ (currentGenerationId !== undefined &&
1494
+ generationRequestId(currentGenerationId) === payload.requestId
1495
+ ? currentGenerationId
1496
+ : undefined)
1497
+ if (targetGenerationId === undefined) return state
1498
+ if (currentGenerationId !== targetGenerationId) return state
1499
+ const matchesGeneration = (generationId: string): boolean =>
1500
+ targetGenerationId === generationId
1501
+ const hasMessageWork =
1502
+ state.pendingApprovals.some(
1503
+ (approval) =>
1504
+ approval.messageId === payload.messageId &&
1505
+ matchesGeneration(approval.generationId),
1506
+ ) ||
1507
+ state.pendingInputs.some(
1508
+ (input) =>
1509
+ input.messageId === payload.messageId &&
1510
+ matchesGeneration(input.generationId),
1511
+ ) ||
1512
+ state.tools.some(
1513
+ (activity) =>
1514
+ activity.messageId === payload.messageId &&
1515
+ activity.status === 'running' &&
1516
+ matchesGeneration(activity.generationId),
1517
+ )
1518
+ const resolvesFailedGeneration =
1519
+ state.status === 'failed' &&
1520
+ currentGenerationId === targetGenerationId &&
1521
+ (state.activeRequestId === null ||
1522
+ state.activeRequestId === targetRequestId ||
1523
+ state.activeRequestId === `ai.generate:tools:${targetGenerationId}`)
1524
+ const resolvesCurrentGeneration =
1525
+ currentGenerationId === targetGenerationId &&
1526
+ currentTerminal === undefined
1527
+ if (
1528
+ !hasMessageWork &&
1529
+ !resolvesFailedGeneration &&
1530
+ !resolvesCurrentGeneration
1531
+ ) {
1532
+ return state
1533
+ }
1534
+ const pendingRequestId = state.activeRequestId
1535
+ const projectedState = interruptGenerationProjection(
1536
+ state,
1537
+ targetGenerationId,
1538
+ payload.messageId,
1539
+ payload.lastSeenIndex ?? Number.POSITIVE_INFINITY,
1540
+ )
1541
+ const baseState = projectedState ?? state
1542
+ const next = {
1543
+ ...baseState,
1544
+ activeRequestId: null,
1545
+ activeResponseMessageId: null,
1546
+ activeProjection: null,
1547
+ messages:
1548
+ projectedState === null
1549
+ ? state.messages.map((message) =>
1550
+ message.id === payload.messageId
1551
+ ? interruptUIMessage(message)
1552
+ : message,
1553
+ )
1554
+ : baseState.messages,
1555
+ pendingApprovals: baseState.pendingApprovals.filter(
1556
+ (approval) =>
1557
+ approval.messageId !== payload.messageId ||
1558
+ !matchesGeneration(approval.generationId),
1559
+ ),
1560
+ pendingInputs: baseState.pendingInputs.filter(
1561
+ (input) =>
1562
+ input.messageId !== payload.messageId ||
1563
+ !matchesGeneration(input.generationId),
1564
+ ),
1565
+ tools:
1566
+ projectedState === null
1567
+ ? stopGenerationTools(
1568
+ state.tools,
1569
+ targetGenerationId,
1570
+ payload.messageId,
1571
+ 'Tool execution was interrupted.',
1572
+ )
1573
+ : baseState.tools,
1574
+ terminalGenerations: {
1575
+ ...baseState.terminalGenerations,
1576
+ [targetGenerationId]: 'interrupted' as const,
1577
+ },
1578
+ ...(pendingRequestId === null
1579
+ ? {}
1580
+ : {
1581
+ terminalRequestIds: {
1582
+ ...baseState.terminalRequestIds,
1583
+ [pendingRequestId]: true as const,
1584
+ },
1585
+ }),
1586
+ error: resolvesFailedGeneration ? null : state.error,
1587
+ }
1588
+ return {
1589
+ ...next,
1590
+ status: waitingStatus(next),
1591
+ }
1592
+ }
1593
+ if (
1594
+ payload.messageId !== active.responseMessageId ||
1595
+ (payload.generationId !== active.generationId &&
1596
+ payload.requestId !== active.requestId)
1597
+ ) {
1598
+ return state
1599
+ }
1600
+ const cutoff = payload.lastSeenIndex ?? Number.POSITIVE_INFINITY
1601
+ const projectedState = interruptGenerationProjection(
1602
+ state,
1603
+ active.generationId,
1604
+ active.responseMessageId,
1605
+ cutoff,
1606
+ )
1607
+ const baseState =
1608
+ projectedState ??
1609
+ restoreGenerationBase(
1610
+ state,
1611
+ active.generationId,
1612
+ active.responseMessageId,
1613
+ 'interrupted',
1614
+ )
1615
+ const next = {
1616
+ ...baseState,
1617
+ activeRequestId: null,
1618
+ activeResponseMessageId: null,
1619
+ terminalRequestIds: {
1620
+ ...baseState.terminalRequestIds,
1621
+ [active.requestId]: true as const,
1622
+ },
1623
+ tools:
1624
+ projectedState === null
1625
+ ? stopGenerationTools(
1626
+ baseState.tools,
1627
+ active.generationId,
1628
+ active.responseMessageId,
1629
+ 'Tool execution was interrupted.',
1630
+ )
1631
+ : baseState.tools,
1632
+ }
1633
+ return { ...next, status: lifecycleStatus(state, next) }
1634
+ }
1635
+ case 'ai.tool.called': {
1636
+ const payload = event.payload as ToolCalledPayload
1637
+ const projection = state.activeProjection
1638
+ if (
1639
+ isStaleGeneration(state, payload.messageId, payload.generationId) ||
1640
+ terminalGenerationReason(state, payload.generationId) !== undefined
1641
+ ) {
1642
+ return state
1643
+ }
1644
+ if (
1645
+ state.tools.some(
1646
+ (activity) =>
1647
+ activity.generationId === payload.generationId &&
1648
+ activity.toolCallId === payload.toolCallId &&
1649
+ activity.status === 'failed' &&
1650
+ state.activeGeneration?.generationId !== payload.generationId,
1651
+ )
1652
+ ) {
1653
+ return state
1654
+ }
1655
+ if (projection?.generationId !== payload.generationId) {
1656
+ const next = {
1657
+ ...state,
1658
+ tools: reduceToolActivity(state.tools, {
1659
+ type: 'ai.tool.called',
1660
+ payload,
1661
+ }),
1662
+ }
1663
+ return { ...next, status: lifecycleStatus(state, next) }
1664
+ }
1665
+ const activeProjection: ActiveGenerationProjection<M> = {
1666
+ ...projection,
1667
+ toolEvents: [
1668
+ ...projection.toolEvents,
1669
+ { index: event.index, type: 'ai.tool.called', payload },
1670
+ ],
1671
+ }
1672
+ const next = {
1673
+ ...state,
1674
+ activeProjection,
1675
+ tools: projectedTools(activeProjection),
1676
+ }
1677
+ return { ...next, status: lifecycleStatus(state, next) }
1678
+ }
1679
+ case 'ai.tool.result': {
1680
+ const payload = event.payload as ToolResultPayload
1681
+ const projection = state.activeProjection
1682
+ const terminalReason = terminalGenerationReason(
1683
+ state,
1684
+ payload.generationId,
1685
+ )
1686
+ const completesFailedTool =
1687
+ terminalReason === 'failed' &&
1688
+ state.status === 'failed' &&
1689
+ payload.preliminary !== true &&
1690
+ projection?.generationId === payload.generationId &&
1691
+ projectedTools(projection).some(
1692
+ (activity) =>
1693
+ activity.generationId === payload.generationId &&
1694
+ activity.toolCallId === payload.toolCallId &&
1695
+ activity.status === 'running',
1696
+ )
1697
+ if (
1698
+ isStaleGeneration(state, payload.messageId, payload.generationId) ||
1699
+ (terminalReason !== undefined && !completesFailedTool)
1700
+ ) {
1701
+ return state
1702
+ }
1703
+ if (
1704
+ state.tools.some(
1705
+ (activity) =>
1706
+ activity.generationId === payload.generationId &&
1707
+ activity.toolCallId === payload.toolCallId &&
1708
+ activity.status === 'failed' &&
1709
+ state.activeGeneration?.generationId !== payload.generationId &&
1710
+ !completesFailedTool,
1711
+ )
1712
+ ) {
1713
+ return state
1714
+ }
1715
+ if (projection?.generationId !== payload.generationId) {
1716
+ const next = {
1717
+ ...state,
1718
+ messages: projectMessageChunks(state.messages, payload.messageId, [
1719
+ toolResultChunk(payload),
1720
+ ]),
1721
+ tools: reduceToolActivity(state.tools, {
1722
+ type: 'ai.tool.result',
1723
+ payload,
1724
+ }),
1725
+ }
1726
+ return { ...next, status: lifecycleStatus(state, next) }
1727
+ }
1728
+ const activeProjection: ActiveGenerationProjection<M> = {
1729
+ ...projection,
1730
+ toolEvents: [
1731
+ ...projection.toolEvents,
1732
+ { index: event.index, type: 'ai.tool.result', payload },
1733
+ ],
1734
+ }
1735
+ const projected = projectedMessage(activeProjection)
1736
+ const tools = projectedTools(activeProjection)
1737
+ const failureError = state.error ?? 'Generation failed.'
1738
+ const next = {
1739
+ ...state,
1740
+ messages: upsertMessage(
1741
+ state.messages,
1742
+ terminalReason === 'failed'
1743
+ ? interruptUIMessage(
1744
+ projected,
1745
+ `Generation failed before tool completion: ${failureError}`,
1746
+ )
1747
+ : projected,
1748
+ ),
1749
+ activeProjection,
1750
+ tools:
1751
+ terminalReason === 'failed'
1752
+ ? stopGenerationTools(
1753
+ tools,
1754
+ payload.generationId,
1755
+ payload.messageId,
1756
+ failureError,
1757
+ )
1758
+ : tools,
1759
+ }
1760
+ return { ...next, status: lifecycleStatus(state, next) }
1761
+ }
1762
+ case 'ai.approval.requested': {
1763
+ const payload = event.payload as ApprovalRequestedPayload
1764
+ const projection = state.activeProjection
1765
+ if (
1766
+ isStaleGeneration(state, payload.messageId, payload.generationId) ||
1767
+ terminalGenerationReason(state, payload.generationId) !== undefined
1768
+ ) {
1769
+ return state
1770
+ }
1771
+ if (
1772
+ state.tools.some(
1773
+ (activity) =>
1774
+ activity.generationId === payload.generationId &&
1775
+ activity.toolCallId === payload.toolCallId &&
1776
+ activity.status === 'failed' &&
1777
+ state.activeGeneration?.generationId !== payload.generationId,
1778
+ )
1779
+ ) {
1780
+ return state
1781
+ }
1782
+ const activeProjection =
1783
+ projection?.generationId === payload.generationId
1784
+ ? {
1785
+ ...projection,
1786
+ approvalEvents: [
1787
+ ...projection.approvalEvents,
1788
+ {
1789
+ index: event.index,
1790
+ type: 'ai.approval.requested' as const,
1791
+ payload,
1792
+ },
1793
+ ],
1794
+ }
1795
+ : projection
1796
+ const next = {
1797
+ ...state,
1798
+ messages:
1799
+ activeProjection === null || activeProjection === projection
1800
+ ? projectMessageChunks(state.messages, payload.messageId, [
1801
+ approvalRequestedChunk(payload),
1802
+ ])
1803
+ : upsertMessage(state.messages, projectedMessage(activeProjection)),
1804
+ activeProjection,
1805
+ pendingApprovals:
1806
+ payload.isAutomatic === true
1807
+ ? state.pendingApprovals
1808
+ : [
1809
+ ...state.pendingApprovals.filter(
1810
+ (approval) =>
1811
+ approval.approvalId !== payload.approvalId ||
1812
+ approval.messageId !== payload.messageId,
1813
+ ),
1814
+ payload,
1815
+ ],
1816
+ }
1817
+ return {
1818
+ ...next,
1819
+ status: lifecycleStatus(state, next),
1820
+ }
1821
+ }
1822
+ case 'ai.approval.responded': {
1823
+ const payload = event.payload as ApprovalRespondedPayload
1824
+ if (
1825
+ isStaleGeneration(state, payload.messageId, payload.generationId) ||
1826
+ terminalGenerationReason(state, payload.generationId) !== undefined
1827
+ ) {
1828
+ return state
1829
+ }
1830
+ const projection = state.activeProjection
1831
+ const pendingRequest = state.pendingApprovals.find(
1832
+ (candidate) =>
1833
+ candidate.approvalId === payload.approvalId &&
1834
+ candidate.messageId === payload.messageId &&
1835
+ candidate.generationId === payload.generationId,
1836
+ )
1837
+ const activeRequest = projection?.approvalEvents.findLast(
1838
+ (candidate) =>
1839
+ candidate.type === 'ai.approval.requested' &&
1840
+ candidate.payload.approvalId === payload.approvalId &&
1841
+ candidate.payload.messageId === payload.messageId &&
1842
+ candidate.payload.generationId === payload.generationId,
1843
+ )
1844
+ if (
1845
+ state.status === 'failed' &&
1846
+ pendingRequest === undefined &&
1847
+ activeRequest === undefined
1848
+ ) {
1849
+ return state
1850
+ }
1851
+ const activeProjection =
1852
+ projection !== null && activeRequest !== undefined
1853
+ ? {
1854
+ ...projection,
1855
+ approvalEvents: [
1856
+ ...projection.approvalEvents,
1857
+ {
1858
+ index: event.index,
1859
+ type: 'ai.approval.responded' as const,
1860
+ payload,
1861
+ },
1862
+ ],
1863
+ }
1864
+ : projection
1865
+ const next = {
1866
+ ...state,
1867
+ messages:
1868
+ activeProjection === null || activeProjection === projection
1869
+ ? answerApproval(state.messages, payload)
1870
+ : upsertMessage(state.messages, projectedMessage(activeProjection)),
1871
+ activeProjection,
1872
+ pendingApprovals: state.pendingApprovals.filter(
1873
+ (approval) =>
1874
+ approval.approvalId !== payload.approvalId ||
1875
+ approval.messageId !== payload.messageId ||
1876
+ approval.generationId !== payload.generationId,
1877
+ ),
1878
+ }
1879
+ return {
1880
+ ...next,
1881
+ status: lifecycleStatus(state, next),
1882
+ }
1883
+ }
1884
+ case 'ai.input.requested': {
1885
+ const payload = event.payload as InputRequestedPayload
1886
+ if (
1887
+ isStaleGeneration(state, payload.messageId, payload.generationId) ||
1888
+ terminalGenerationReason(state, payload.generationId) !== undefined
1889
+ ) {
1890
+ return state
1891
+ }
1892
+ const next = {
1893
+ ...state,
1894
+ pendingInputs: [
1895
+ ...state.pendingInputs.filter(
1896
+ (input) =>
1897
+ input.messageId !== payload.messageId ||
1898
+ input.generationId !== payload.generationId ||
1899
+ input.inputId !== payload.inputId,
1900
+ ),
1901
+ payload,
1902
+ ],
1903
+ }
1904
+ return { ...next, status: lifecycleStatus(state, next) }
1905
+ }
1906
+ case 'ai.input.responded': {
1907
+ const payload = event.payload as InputRespondedPayload
1908
+ if (
1909
+ isStaleGeneration(state, payload.messageId, payload.generationId) ||
1910
+ terminalGenerationReason(state, payload.generationId) !== undefined
1911
+ ) {
1912
+ return state
1913
+ }
1914
+ const next = {
1915
+ ...state,
1916
+ pendingInputs: state.pendingInputs.filter(
1917
+ (input) =>
1918
+ input.inputId !== payload.inputId ||
1919
+ input.messageId !== payload.messageId ||
1920
+ input.generationId !== payload.generationId,
1921
+ ),
1922
+ }
1923
+ return { ...next, status: lifecycleStatus(state, next) }
1924
+ }
1925
+ case 'ai.compaction.requested': {
1926
+ const payload = event.payload as CompactionRequestedPayload
1927
+ if (state.activeGeneration?.generationId !== payload.generationId) {
1928
+ return state
1929
+ }
1930
+ return {
1931
+ ...state,
1932
+ compaction: { status: 'running', ...payload },
1933
+ }
1934
+ }
1935
+ case 'ai.compaction.completed': {
1936
+ const payload = event.payload as CompactionCompletedPayload<M>
1937
+ if (
1938
+ state.activeGeneration?.generationId !== payload.generationId ||
1939
+ state.compaction?.status !== 'running' ||
1940
+ state.compaction.generationId !== payload.generationId
1941
+ ) {
1942
+ return state
1943
+ }
1944
+ return {
1945
+ ...state,
1946
+ compaction: { status: 'completed', ...payload },
1947
+ }
1948
+ }
1949
+ default:
1950
+ return state
1951
+ }
1952
+ }
1953
+
1954
+ /** Derive AI SDK UI messages directly from an A2 event history. */
1955
+ export function deriveUIMessages<M extends UIMessage = UIMessage>(
1956
+ history: readonly { type: string; payload: unknown; index: number }[],
1957
+ ): M[] {
1958
+ let state = initialState<M>()
1959
+ for (const event of history) state = reduceAIState(state, event)
1960
+ return state.messages
1961
+ }
1962
+
1963
+ /** Build the standard AI state projection for any contract containing the protocol. */
1964
+ export function createReducer<D extends EventDefs>(options: {
1965
+ contract: Contract<D>
1966
+ name?: string
1967
+ }): Reducer<D, AIState<AIMessageFrom<D>>> {
1968
+ type M = AIMessageFrom<D>
1969
+ return options.contract
1970
+ .reducer({
1971
+ name: options.name ?? 'a2.ai.state.v8',
1972
+ initialState: initialState<M>(),
1973
+ })
1974
+ .fold((state, event) => reduceAIState(state, event))
1975
+ }
1976
+
1977
+ type ProtocolInput<
1978
+ M extends UIMessage,
1979
+ K extends keyof AIEventDefs<M> & string,
1980
+ > = AppendInput<Pick<AIEventDefs<M>, K>>
1981
+
1982
+ export type AIInputs = {
1983
+ /** Record a message. User messages schedule unless generate is false. */
1984
+ message<M extends UIMessage>(
1985
+ message: M,
1986
+ options?: { generate?: boolean },
1987
+ ): ProtocolInput<M, 'ai.message.created'>[]
1988
+ /** Record a message without requesting a generation. */
1989
+ seed<M extends UIMessage>(
1990
+ message: M,
1991
+ ): ProtocolInput<M, 'ai.message.created'>[]
1992
+ /** Answer an AI SDK tool approval. The tool barrier owns continuation. */
1993
+ approval(
1994
+ response: ApprovalRespondedPayload,
1995
+ ): ProtocolInput<UIMessage, 'ai.approval.responded'>[]
1996
+ /** Answer an application-defined input request. */
1997
+ input(
1998
+ response: InputRespondedPayload,
1999
+ ): ProtocolInput<UIMessage, 'ai.input.responded'>[]
2000
+ /** Record an application-defined request for input. */
2001
+ requestInput(
2002
+ request: InputRequestedPayload,
2003
+ ): ProtocolInput<UIMessage, 'ai.input.requested'>[]
2004
+ /** Request a fresh attempt for a failed response. */
2005
+ retry(
2006
+ options: RetryGenerationOptions,
2007
+ ): ProtocolInput<UIMessage, 'ai.retry.requested'>[]
2008
+ /** Interrupt the active response for a message. */
2009
+ interrupt(
2010
+ interruption: MessageInterruptedPayload,
2011
+ ): ProtocolInput<UIMessage, 'ai.message.interrupted'>[]
2012
+ }
2013
+
2014
+ const messageInput = <M extends UIMessage>(
2015
+ message: M,
2016
+ generate?: boolean,
2017
+ ): ProtocolInput<M, 'ai.message.created'>[] => [
2018
+ {
2019
+ type: 'ai.message.created',
2020
+ id: `ai.message:${message.id}`,
2021
+ payload: {
2022
+ message,
2023
+ ...(generate === false ? { generate: false } : {}),
2024
+ },
2025
+ },
2026
+ ]
2027
+
2028
+ /** Pure append inputs for the built-in AI protocol. */
2029
+ export const inputs: AIInputs = {
2030
+ message<M extends UIMessage>(message: M, options?: { generate?: boolean }) {
2031
+ return messageInput(message, options?.generate)
2032
+ },
2033
+ seed<M extends UIMessage>(message: M) {
2034
+ return messageInput(message, false)
2035
+ },
2036
+ approval(response) {
2037
+ return [
2038
+ {
2039
+ type: 'ai.approval.responded',
2040
+ id: `ai.approval:${encodeURIComponent(response.messageId)}:${encodeURIComponent(response.generationId)}:${encodeURIComponent(response.approvalId)}:response`,
2041
+ payload: response,
2042
+ },
2043
+ ]
2044
+ },
2045
+ input(response) {
2046
+ return [
2047
+ {
2048
+ type: 'ai.input.responded',
2049
+ id: `ai.input:${encodeURIComponent(response.messageId)}:${encodeURIComponent(response.generationId)}:${encodeURIComponent(response.inputId)}:response`,
2050
+ payload: response,
2051
+ },
2052
+ ]
2053
+ },
2054
+ requestInput(request) {
2055
+ return [
2056
+ {
2057
+ type: 'ai.input.requested',
2058
+ id: `ai.input:${encodeURIComponent(request.messageId)}:${encodeURIComponent(request.generationId)}:${encodeURIComponent(request.inputId)}:request`,
2059
+ payload: request,
2060
+ },
2061
+ ]
2062
+ },
2063
+ retry(retryOptions) {
2064
+ return [
2065
+ {
2066
+ type: 'ai.retry.requested',
2067
+ id: `ai.retry:${retryOptions.retryId}`,
2068
+ payload: retryOptions,
2069
+ },
2070
+ ]
2071
+ },
2072
+ interrupt(interruption) {
2073
+ return [
2074
+ {
2075
+ type: 'ai.message.interrupted',
2076
+ id: `ai.interrupt:${interruption.generationId ?? interruption.requestId}`,
2077
+ payload: interruption,
2078
+ },
2079
+ ]
2080
+ },
2081
+ }
2082
+
2083
+ /**
2084
+ * Read the durable handler context ambient to the current AI tool
2085
+ * execution. A2 runs each local tool's `execute` inside this scope, so
2086
+ * plain AI SDK `tool()` definitions reach their `event`, `attempt`,
2087
+ * `session`, and `signal` without an A2-specific wrapper. The agent (or
2088
+ * contract) argument carries the event types and is verified against
2089
+ * the executing server's contract.
2090
+ */
2091
+ export function handlerContext<D extends AgentToolEventDefs>(
2092
+ agentOrContract: Contract<D> | { readonly contract: Contract<D> },
2093
+ ): AgentToolContext<D> {
2094
+ const contract =
2095
+ 'contract' in agentOrContract ? agentOrContract.contract : agentOrContract
2096
+ const scope = ambientToolScopeStorage()?.getStore()
2097
+ if (scope === undefined) {
2098
+ throw new TypeError(
2099
+ `handlerContext('${contract.name}') requires an ambient A2 tool execution: call it inside a local tool's execute, or wrap direct tool tests with runWithHandlerContext`,
2100
+ )
2101
+ }
2102
+ if (scope.contract !== undefined && scope.contract.name !== contract.name) {
2103
+ throw new TypeError(
2104
+ `handlerContext('${contract.name}') was called during a tool execution for agent '${scope.contract.name}'`,
2105
+ )
2106
+ }
2107
+ return scope.context as AgentToolContext<D>
2108
+ }
2109
+
2110
+ /**
2111
+ * Define an agent: built-in AI protocol + application events and one state
2112
+ * reducer. The result is isomorphic.
2113
+ */
2114
+ export function agent<
2115
+ M extends UIMessage = UIMessage,
2116
+ E extends EventDefs = Record<never, never>,
2117
+ >(options: AgentOptions<M, E>): AgentDefinition<M, AIEventDefs<M> & E> {
2118
+ const builtIns = createEvents(
2119
+ options.messageSchema === undefined
2120
+ ? undefined
2121
+ : { messageSchema: options.messageSchema },
2122
+ )
2123
+ const extensions = options.events ?? ({} as E)
2124
+ for (const key of Object.keys(extensions)) {
2125
+ if (Object.hasOwn(builtIns, key)) {
2126
+ throw new TypeError(
2127
+ `agent event '${key}' conflicts with a built-in event`,
2128
+ )
2129
+ }
2130
+ }
2131
+ const agentContract = createContract({
2132
+ name: options.name,
2133
+ events: { ...builtIns, ...extensions },
2134
+ })
2135
+ const reducer = agentContract
2136
+ .reducer({
2137
+ name: options.reducerName ?? 'a2.ai.state.v8',
2138
+ initialState: initialState<M>(),
2139
+ })
2140
+ .fold((state, event) => reduceAIState(state, event))
2141
+
2142
+ const definition = {
2143
+ contract: agentContract,
2144
+ reducer,
2145
+ }
2146
+ return definition as unknown as AgentDefinition<M, AIEventDefs<M> & E>
2147
+ }
2148
+
2149
+ export type {
2150
+ FinishReason,
2151
+ JSONValue,
2152
+ LanguageModelUsage,
2153
+ UIMessage,
2154
+ UIMessageChunk,
2155
+ }