experimental-a2 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/dist/actor-client.d.ts +1 -1
  3. package/dist/{actor-BfQSE0KC.d.ts → actor-ohPC-81x.d.ts} +5 -5
  4. package/dist/{actor-BfQSE0KC.d.ts.map → actor-ohPC-81x.d.ts.map} +1 -1
  5. package/dist/actor-react.d.ts +3 -3
  6. package/dist/actor.d.ts +1 -1
  7. package/dist/actor.js +1 -1
  8. package/dist/ai-CrLYwNEx.js +2431 -0
  9. package/dist/ai-CrLYwNEx.js.map +1 -0
  10. package/dist/{ai-Cai-lCbj.d.ts → ai-HJ9fHfYI.d.ts} +72 -13
  11. package/dist/ai-HJ9fHfYI.d.ts.map +1 -0
  12. package/dist/ai-server.d.ts +4 -5
  13. package/dist/ai-server.d.ts.map +1 -1
  14. package/dist/ai-server.js +755 -290
  15. package/dist/ai-server.js.map +1 -1
  16. package/dist/ai.d.ts +2 -2
  17. package/dist/ai.js +1 -1999
  18. package/dist/client-BYzHjkwU.js.map +1 -1
  19. package/dist/{client-BAEABRZB.d.ts → client-CzyacQpJ.d.ts} +9 -9
  20. package/dist/client-CzyacQpJ.d.ts.map +1 -0
  21. package/dist/client.d.ts +1 -1
  22. package/dist/idempotent-id-BRJVeylj.js +13 -0
  23. package/dist/idempotent-id-BRJVeylj.js.map +1 -0
  24. package/dist/index.d.ts +3 -3
  25. package/dist/index.js +1 -12
  26. package/dist/otel.d.ts +1 -1
  27. package/dist/react.d.ts +13 -13
  28. package/dist/react.d.ts.map +1 -1
  29. package/dist/react.js.map +1 -1
  30. package/dist/reducer-DEMjEY_O.js.map +1 -1
  31. package/dist/{reducer-BcS9VDKC.d.ts → reducer-otzHjuJj.d.ts} +3 -3
  32. package/dist/{reducer-BcS9VDKC.d.ts.map → reducer-otzHjuJj.d.ts.map} +1 -1
  33. package/dist/scheduler-qstash.d.ts +2 -2
  34. package/dist/scheduler-qstash.js +1 -1
  35. package/dist/scheduler-vercel.d.ts +2 -2
  36. package/dist/scheduler-vercel.js +1 -1
  37. package/dist/{server-CjJSGcF7.js → server-BD5ckxZb.js} +39 -26
  38. package/dist/server-BD5ckxZb.js.map +1 -0
  39. package/dist/{server-Bp5Nd1pF.d.ts → server-DbD7IJVK.d.ts} +5 -5
  40. package/dist/{server-Bp5Nd1pF.d.ts.map → server-DbD7IJVK.d.ts.map} +1 -1
  41. package/dist/server.d.ts +3 -3
  42. package/dist/server.js +1 -1
  43. package/dist/{store-D_yhNdPz.d.ts → store-DyZM6fS5.d.ts} +3 -2
  44. package/dist/{store-D_yhNdPz.d.ts.map → store-DyZM6fS5.d.ts.map} +1 -1
  45. package/dist/store-N8PXxDAS.js.map +1 -1
  46. package/dist/store-memory.d.ts +1 -1
  47. package/dist/store-postgres.d.ts +1 -1
  48. package/dist/store-redis-http.d.ts +1 -1
  49. package/dist/store-redis-http.js +1 -1
  50. package/dist/{store-redis-notify-D2EI6gwX.js → store-redis-notify-BVLUfI3j.js} +14 -13
  51. package/dist/store-redis-notify-BVLUfI3j.js.map +1 -0
  52. package/dist/store-redis.d.ts +1 -1
  53. package/dist/store-redis.js +1 -1
  54. package/dist/store-sqlite.d.ts +1 -1
  55. package/dist/{telemetry-CpeclqB2.d.ts → telemetry-B5jzpy6y.d.ts} +2 -2
  56. package/dist/telemetry-B5jzpy6y.d.ts.map +1 -0
  57. package/docs/concepts/04-state.mdx +9 -4
  58. package/docs/guides/06-ai-agents.mdx +134 -43
  59. package/docs/reference/01-api.mdx +104 -27
  60. package/examples/playground/app/agent/[agentId]/agent-client.tsx +20 -28
  61. package/examples/playground/app/agent/[agentId]/agent-queue.test.tsx +123 -0
  62. package/examples/playground/app/agent/[agentId]/agent-queue.tsx +37 -43
  63. package/examples/playground/app/agent/[agentId]/compaction-panel.tsx +44 -24
  64. package/examples/playground/app/agent/compaction-timeline.test.ts +47 -7
  65. package/examples/playground/app/agent/compaction-timeline.ts +13 -3
  66. package/examples/playground/app/chat/[chatId]/chat-client.tsx +1 -5
  67. package/examples/playground/app/chat/[chatId]/session.ts +17 -2
  68. package/examples/playground/app/chat/model.test.ts +4 -4
  69. package/examples/playground/app/chat/model.ts +18 -6
  70. package/examples/playground/app/chat/server.ts +6 -3
  71. package/examples/playground/package.json +3 -1
  72. package/package.json +1 -1
  73. package/src/ai-client-state.ts +0 -2
  74. package/src/ai-context-schema.ts +296 -0
  75. package/src/ai-context.ts +680 -0
  76. package/src/ai-control-server.ts +160 -131
  77. package/src/ai-control-state.ts +6 -0
  78. package/src/ai-control.ts +6 -7
  79. package/src/ai-coordinator.ts +92 -38
  80. package/src/ai-id.ts +6 -0
  81. package/src/ai-input-tokens.ts +75 -0
  82. package/src/ai-message-projection.ts +348 -0
  83. package/src/ai-server.ts +232 -334
  84. package/src/ai-stored-state.ts +484 -0
  85. package/src/ai.ts +252 -418
  86. package/src/client.ts +21 -15
  87. package/src/react.ts +25 -19
  88. package/src/reducer.ts +2 -1
  89. package/src/server.ts +73 -56
  90. package/src/store-redis-core.ts +15 -17
  91. package/src/store.ts +1 -0
  92. package/src/telemetry.ts +2 -1
  93. package/dist/ai-Cai-lCbj.d.ts.map +0 -1
  94. package/dist/ai-control-CcD4hh3y.js +0 -119
  95. package/dist/ai-control-CcD4hh3y.js.map +0 -1
  96. package/dist/ai.js.map +0 -1
  97. package/dist/client-BAEABRZB.d.ts.map +0 -1
  98. package/dist/index.js.map +0 -1
  99. package/dist/server-CjJSGcF7.js.map +0 -1
  100. package/dist/store-redis-notify-D2EI6gwX.js.map +0 -1
  101. package/dist/telemetry-CpeclqB2.d.ts.map +0 -1
@@ -0,0 +1,680 @@
1
+ import type { UIMessage } from 'ai'
2
+ import type {
3
+ AgentDefinition,
4
+ ApprovalRequestedPayload,
5
+ ApprovalRespondedPayload,
6
+ CompactionCompletedPayload,
7
+ CompactionRequestedPayload,
8
+ CompactionState,
9
+ GenerationCompletedPayload,
10
+ GenerationFailedPayload,
11
+ GenerationProgressPayload,
12
+ GenerationRequestedPayload,
13
+ GenerationStartedPayload,
14
+ MessageCreatedPayload,
15
+ MessageInterruptedPayload,
16
+ ModelMetadataRequestedPayload,
17
+ ModelMetadataResolvedPayload,
18
+ ModelMetadataState,
19
+ ToolCalledPayload,
20
+ ToolResultPayload,
21
+ } from './ai.ts'
22
+ import type { ControlCommit } from './ai-control.ts'
23
+ import { contextSchema } from './ai-context-schema.ts'
24
+ import type { ContractEvent, EventDefs } from './contract.ts'
25
+ import {
26
+ projectedMessage,
27
+ advanceProjection,
28
+ reduceToolActivity,
29
+ upsertMessage,
30
+ upsertResponse,
31
+ type MessageProjection,
32
+ } from './ai-message-projection.ts'
33
+ import { interruptUIMessage, pauseUIMessage } from './ai-projector.ts'
34
+ import {
35
+ flattenProgressBatches,
36
+ progressBatches,
37
+ } from './ai-progress-batches.ts'
38
+ import { storedAIReducer, type AIStoredReducer } from './ai-stored-state.ts'
39
+
40
+ export type AIModelContext<M extends UIMessage = UIMessage> = {
41
+ messages: M[]
42
+ compaction: Omit<
43
+ CompactionState<M>,
44
+ 'messages' | 'retainedMessageIds' | 'usage'
45
+ > | null
46
+ modelMetadata: Record<string, ModelMetadataState>
47
+ }
48
+
49
+ export type AIContextState<M extends UIMessage> = AIModelContext<M> & {
50
+ closed: boolean
51
+ authority: Pick<
52
+ ControlCommit['view'],
53
+ | 'requestId'
54
+ | 'requestReason'
55
+ | 'sourceGenerationId'
56
+ | 'responseMessageId'
57
+ | 'phase'
58
+ > | null
59
+ generation: {
60
+ owner: GenerationStartedPayload
61
+ phase: 'streaming' | 'completed' | 'paused' | 'failed' | 'superseded'
62
+ error?: string
63
+ responseIndex?: number
64
+ } | null
65
+ activeProjection: MessageProjection<M> | null
66
+ messageIndexes: Record<string, number>
67
+ }
68
+
69
+ const replaceResponse = <M extends UIMessage>({
70
+ state,
71
+ message,
72
+ index,
73
+ }: {
74
+ state: AIContextState<M>
75
+ message: M
76
+ index: number
77
+ }): AIContextState<M> => {
78
+ let messages = upsertResponse({
79
+ messages: state.messages,
80
+ message,
81
+ inputId: state.generation!.owner.messageId,
82
+ })
83
+ if (
84
+ state.generation?.responseIndex !== undefined &&
85
+ !state.messages.some((item) => item.id === message.id)
86
+ ) {
87
+ messages = messages.filter((item) => item.id !== message.id)
88
+ messages.splice(state.generation.responseIndex, 0, message)
89
+ }
90
+ return {
91
+ ...state,
92
+ messages,
93
+ messageIndexes: Object.hasOwn(state.messageIndexes, message.id)
94
+ ? state.messageIndexes
95
+ : { ...state.messageIndexes, [message.id]: index },
96
+ }
97
+ }
98
+
99
+ const calledTools = <M extends UIMessage>(
100
+ projection: MessageProjection<M>,
101
+ ): Set<string> =>
102
+ new Set([
103
+ ...(projection.baseMessage?.parts.flatMap((part) =>
104
+ 'toolCallId' in part ? [part.toolCallId] : [],
105
+ ) ?? []),
106
+ ...projection.toolEvents
107
+ .filter((event) => event.type === 'ai.tool.called')
108
+ .map((event) => event.payload.toolCallId),
109
+ ])
110
+
111
+ const renderProjection = <M extends UIMessage>({
112
+ state,
113
+ index,
114
+ }: {
115
+ state: AIContextState<M>
116
+ index: number
117
+ }): AIContextState<M> => {
118
+ const projection = state.activeProjection!
119
+ let message = projectedMessage(projection)
120
+ if (
121
+ projection.baseMessage === undefined &&
122
+ projection.batches.length === 0 &&
123
+ message.parts.length === 0
124
+ )
125
+ return state
126
+ if (state.generation?.phase === 'paused')
127
+ message = pauseUIMessage({ message, toolCallIds: calledTools(projection) })
128
+ if (state.generation?.phase === 'failed')
129
+ message = interruptUIMessage(
130
+ message,
131
+ `Generation failed before tool completion: ${state.generation.error}`,
132
+ )
133
+ return replaceResponse({
134
+ state: { ...state, activeProjection: { ...projection, cursor: null } },
135
+ message,
136
+ index,
137
+ })
138
+ }
139
+
140
+ export const generationBase = <M extends UIMessage>(
141
+ state: AIContextState<M>,
142
+ ): AIContextState<M> => {
143
+ const projection = state.activeProjection
144
+ if (!projection) return state
145
+ return {
146
+ ...state,
147
+ messages:
148
+ projection.baseMessage === undefined
149
+ ? state.messages.filter(
150
+ (message) => message.id !== projection.responseMessageId,
151
+ )
152
+ : upsertMessage(state.messages, projection.baseMessage),
153
+ compaction: projection.baseCompaction ?? null,
154
+ }
155
+ }
156
+
157
+ export const contextForAttempt = <M extends UIMessage>(
158
+ state: AIContextState<M>,
159
+ ): AIContextState<M> => ({
160
+ ...(state.activeProjection?.completionIndex === undefined
161
+ ? generationBase(state)
162
+ : state),
163
+ generation: state.generation && { ...state.generation, phase: 'superseded' },
164
+ activeProjection: null,
165
+ })
166
+
167
+ const endGeneration = <M extends UIMessage>(
168
+ state: AIContextState<M>,
169
+ ): AIContextState<M> => ({
170
+ ...state,
171
+ authority: null,
172
+ generation: null,
173
+ activeProjection: null,
174
+ })
175
+
176
+ const ownsGeneration = <M extends UIMessage>(
177
+ state: AIContextState<M>,
178
+ owner: { generationId: string; requestId: string },
179
+ ): boolean =>
180
+ state.generation?.owner.generationId === owner.generationId &&
181
+ state.generation.owner.requestId === owner.requestId &&
182
+ state.generation.phase !== 'superseded'
183
+
184
+ export const createContextReducer = <M extends UIMessage, D extends EventDefs>({
185
+ agent,
186
+ }: {
187
+ agent: AgentDefinition<M, D>
188
+ }): AIStoredReducer<D, AIContextState<M>> => {
189
+ const initialState: AIContextState<M> = {
190
+ messages: [],
191
+ compaction: null,
192
+ modelMetadata: {},
193
+ closed: false,
194
+ authority: null,
195
+ generation: null,
196
+ activeProjection: null,
197
+ messageIndexes: {},
198
+ }
199
+ const reducer = agent.contract
200
+ .reducer({
201
+ name: 'a2.ai.context.v3',
202
+ initialState,
203
+ stateSchema: contextSchema<M>(),
204
+ })
205
+ .fold((state, event) => reduceContext(state, event))
206
+ return storedAIReducer(reducer)
207
+ }
208
+
209
+ const reduceContext = <M extends UIMessage, D extends EventDefs>(
210
+ state: AIContextState<M>,
211
+ event: ContractEvent<D>,
212
+ ): AIContextState<M> => {
213
+ if (state.closed) return state
214
+ switch (event.type) {
215
+ case 'ai.control.committed': {
216
+ const { view } = event.payload as ControlCommit
217
+ const {
218
+ requestId,
219
+ requestReason,
220
+ sourceGenerationId,
221
+ responseMessageId,
222
+ phase,
223
+ } = view
224
+ const next = {
225
+ ...state,
226
+ authority: {
227
+ requestId,
228
+ requestReason,
229
+ sourceGenerationId,
230
+ responseMessageId,
231
+ phase,
232
+ },
233
+ closed: view.closed,
234
+ }
235
+ return view.closed ? endGeneration(next) : next
236
+ }
237
+ case 'ai.session.closed':
238
+ return endGeneration({
239
+ ...state,
240
+ closed: true,
241
+ compaction:
242
+ state.compaction?.status === 'running'
243
+ ? (state.activeProjection?.baseCompaction ?? null)
244
+ : state.compaction,
245
+ })
246
+ case 'ai.message.created': {
247
+ const { message } = event.payload as MessageCreatedPayload<M>
248
+ return {
249
+ ...state,
250
+ messages: upsertMessage(state.messages, message),
251
+ activeProjection:
252
+ state.activeProjection?.responseMessageId === message.id
253
+ ? { ...state.activeProjection, cursor: null }
254
+ : state.activeProjection,
255
+ messageIndexes: { ...state.messageIndexes, [message.id]: event.index },
256
+ }
257
+ }
258
+ case 'ai.generation.requested': {
259
+ const request = event.payload as GenerationRequestedPayload
260
+ if (
261
+ state.authority?.requestId !== event.id ||
262
+ request.responseMessageId !== state.authority.responseMessageId ||
263
+ request.reason !== state.authority.requestReason ||
264
+ (request.sourceGenerationId ?? null) !==
265
+ state.authority.sourceGenerationId ||
266
+ request.reason !== 'retry'
267
+ )
268
+ return state
269
+ return contextForAttempt(state)
270
+ }
271
+ case 'ai.generation.started': {
272
+ const owner = event.payload as GenerationStartedPayload
273
+ if (
274
+ state.authority?.requestId !== owner.requestId ||
275
+ state.authority.responseMessageId !== owner.responseMessageId ||
276
+ state.authority.phase === 'paused' ||
277
+ state.authority.phase === 'pausing'
278
+ )
279
+ return state
280
+ const previous = state.generation
281
+ if (
282
+ previous?.owner.requestId === owner.requestId &&
283
+ (previous.owner.attempt >= owner.attempt ||
284
+ previous.phase === 'failed' ||
285
+ previous.phase === 'completed' ||
286
+ state.activeProjection?.completionIndex !== undefined)
287
+ )
288
+ return state
289
+ const base =
290
+ previous?.owner.requestId === owner.requestId
291
+ ? generationBase(state)
292
+ : state
293
+ const baseMessage = base.messages.find(
294
+ (message) => message.id === owner.responseMessageId,
295
+ )
296
+ return {
297
+ ...base,
298
+ generation: {
299
+ owner,
300
+ phase: 'streaming',
301
+ ...(previous?.owner.responseMessageId === owner.responseMessageId &&
302
+ previous.responseIndex !== undefined
303
+ ? { responseIndex: previous.responseIndex }
304
+ : {}),
305
+ },
306
+ activeProjection: {
307
+ generationId: owner.generationId,
308
+ responseMessageId: owner.responseMessageId,
309
+ promptThroughIndex: owner.promptThroughIndex ?? event.index - 1,
310
+ ...(baseMessage === undefined ? {} : { baseMessage }),
311
+ baseCompaction: base.compaction,
312
+ batches: progressBatches(),
313
+ toolEvents: [],
314
+ approvalEvents: [],
315
+ },
316
+ }
317
+ }
318
+ case 'ai.generation.progress': {
319
+ const payload = event.payload as GenerationProgressPayload
320
+ if (
321
+ !ownsGeneration(state, payload) ||
322
+ state.generation!.owner.responseMessageId !==
323
+ payload.responseMessageId ||
324
+ state.generation!.phase !== 'streaming' ||
325
+ !state.activeProjection
326
+ )
327
+ return state
328
+ const projection = state.activeProjection
329
+ const progress = advanceProjection({
330
+ projection,
331
+ base: state.messages.find(
332
+ (message) => message.id === projection.responseMessageId,
333
+ ),
334
+ chunks: payload.chunks,
335
+ index: event.index,
336
+ })
337
+ return replaceResponse({
338
+ state: {
339
+ ...state,
340
+ activeProjection: {
341
+ ...projection,
342
+ batches: progress.batches,
343
+ cursor: progress.cursor,
344
+ },
345
+ },
346
+ message: progress.message,
347
+ index: event.index,
348
+ })
349
+ }
350
+ case 'ai.generation.completed': {
351
+ const payload = event.payload as GenerationCompletedPayload
352
+ if (
353
+ !ownsGeneration(state, payload) ||
354
+ state.generation!.phase !== 'streaming'
355
+ )
356
+ return state
357
+ return {
358
+ ...state,
359
+ generation: { ...state.generation!, phase: 'completed' },
360
+ activeProjection: {
361
+ ...state.activeProjection!,
362
+ completionIndex: event.index,
363
+ cursor: null,
364
+ },
365
+ }
366
+ }
367
+ case 'ai.generation.failed': {
368
+ const payload = event.payload as GenerationFailedPayload
369
+ if (!ownsGeneration(state, payload)) return state
370
+ if (
371
+ state.generation!.phase !== 'streaming' &&
372
+ !(
373
+ payload.stepLimit &&
374
+ state.authority?.requestReason === 'tool' &&
375
+ state.authority.sourceGenerationId === payload.generationId
376
+ )
377
+ )
378
+ return state
379
+ if (payload.superseded) {
380
+ const next = generationBase(state)
381
+ return {
382
+ ...next,
383
+ generation: { ...state.generation!, phase: 'superseded' },
384
+ activeProjection: null,
385
+ }
386
+ }
387
+ return renderProjection({
388
+ state: {
389
+ ...state,
390
+ compaction: state.activeProjection?.baseCompaction ?? null,
391
+ generation: {
392
+ ...state.generation!,
393
+ phase: 'failed',
394
+ error: payload.error,
395
+ },
396
+ },
397
+ index: event.index,
398
+ })
399
+ }
400
+ case 'ai.message.interrupted': {
401
+ const payload = event.payload as MessageInterruptedPayload
402
+ const owner = state.generation?.owner
403
+ const ownsRequest =
404
+ payload.requestId !== undefined &&
405
+ payload.requestId === state.authority?.requestId &&
406
+ payload.messageId === state.authority.responseMessageId
407
+ const ownsOwner =
408
+ owner?.responseMessageId === payload.messageId &&
409
+ (payload.generationId === owner.generationId ||
410
+ payload.requestId === owner.requestId)
411
+ if (!ownsRequest && !ownsOwner) return state
412
+ if (
413
+ ownsOwner &&
414
+ state.authority?.requestId !== owner!.requestId &&
415
+ !ownsRequest
416
+ )
417
+ return state
418
+ const projection = state.activeProjection
419
+ if (!projection) return endGeneration(state)
420
+ const cutoff = payload.lastSeenIndex ?? Number.POSITIVE_INFINITY
421
+ const retained = {
422
+ ...projection,
423
+ batches: progressBatches(
424
+ flattenProgressBatches(projection.batches).filter(
425
+ (batch) => batch.index <= cutoff,
426
+ ),
427
+ ),
428
+ toolEvents: projection.toolEvents.filter(
429
+ (item) => item.index <= cutoff,
430
+ ),
431
+ approvalEvents: projection.approvalEvents.filter(
432
+ (item) => item.index <= cutoff,
433
+ ),
434
+ }
435
+ let next = generationBase(state)
436
+ if (retained.baseMessage !== undefined || retained.batches.length > 0)
437
+ next = replaceResponse({
438
+ state: next,
439
+ message: interruptUIMessage(projectedMessage(retained)),
440
+ index: event.index,
441
+ })
442
+ return endGeneration(next)
443
+ }
444
+ case 'ai.message.completed': {
445
+ const { messageId } = event.payload as { messageId: string }
446
+ return state.generation?.owner.responseMessageId === messageId &&
447
+ state.generation.phase !== 'failed' &&
448
+ state.generation.phase !== 'superseded'
449
+ ? endGeneration(state)
450
+ : state
451
+ }
452
+ case 'ai.generation.paused': {
453
+ const { generationId } = event.payload as { generationId: string }
454
+ if (
455
+ state.generation?.owner.generationId !== generationId ||
456
+ state.generation.phase === 'failed' ||
457
+ state.generation.phase === 'superseded' ||
458
+ !state.activeProjection
459
+ )
460
+ return state
461
+ const toolCallIds = calledTools(state.activeProjection)
462
+ return renderProjection({
463
+ state: {
464
+ ...state,
465
+ generation: { ...state.generation, phase: 'paused' },
466
+ compaction:
467
+ state.compaction?.status === 'running'
468
+ ? (state.activeProjection.baseCompaction ?? null)
469
+ : state.compaction,
470
+ activeProjection: {
471
+ ...state.activeProjection,
472
+ cursor: null,
473
+ batches: progressBatches(
474
+ flattenProgressBatches(state.activeProjection.batches).map(
475
+ (batch) => ({
476
+ index: batch.index,
477
+ chunks: batch.chunks.filter(
478
+ (chunk) =>
479
+ !('toolCallId' in chunk) ||
480
+ toolCallIds.has(chunk.toolCallId),
481
+ ),
482
+ }),
483
+ ),
484
+ ),
485
+ },
486
+ },
487
+ index: event.index,
488
+ })
489
+ }
490
+ case 'ai.tool.called':
491
+ case 'ai.tool.result': {
492
+ const payload = event.payload as ToolCalledPayload | ToolResultPayload
493
+ if (
494
+ !ownsGeneration(state, payload) ||
495
+ !state.activeProjection ||
496
+ state.generation!.owner.responseMessageId !== payload.messageId
497
+ )
498
+ return state
499
+ if (state.generation!.phase === 'failed') {
500
+ const result = payload as ToolResultPayload
501
+ const running = state.activeProjection.toolEvents
502
+ .reduce(
503
+ (tools, item) => reduceToolActivity(tools, item),
504
+ [] as ReturnType<typeof reduceToolActivity>,
505
+ )
506
+ .some(
507
+ (tool) =>
508
+ tool.toolCallId === payload.toolCallId &&
509
+ tool.status === 'running',
510
+ )
511
+ if (event.type !== 'ai.tool.result' || result.preliminary || !running)
512
+ return state
513
+ }
514
+ const toolEvent =
515
+ event.type === 'ai.tool.called'
516
+ ? {
517
+ index: event.index,
518
+ type: 'ai.tool.called' as const,
519
+ payload: payload as ToolCalledPayload,
520
+ }
521
+ : {
522
+ index: event.index,
523
+ type: 'ai.tool.result' as const,
524
+ payload: payload as ToolResultPayload,
525
+ }
526
+ const next = {
527
+ ...state,
528
+ activeProjection: {
529
+ ...state.activeProjection,
530
+ toolEvents: [...state.activeProjection.toolEvents, toolEvent],
531
+ },
532
+ }
533
+ if (event.type === 'ai.tool.called') return next
534
+ next.activeProjection.cursor = null
535
+ return renderProjection({ state: next, index: event.index })
536
+ }
537
+ case 'ai.approval.requested':
538
+ case 'ai.approval.responded': {
539
+ const payload = event.payload as
540
+ ApprovalRequestedPayload | ApprovalRespondedPayload
541
+ if (
542
+ !ownsGeneration(state, payload) ||
543
+ !state.activeProjection ||
544
+ state.generation!.phase === 'failed' ||
545
+ state.generation!.owner.responseMessageId !== payload.messageId
546
+ )
547
+ return state
548
+ const approvalEvent =
549
+ event.type === 'ai.approval.requested'
550
+ ? {
551
+ index: event.index,
552
+ type: 'ai.approval.requested' as const,
553
+ payload: payload as ApprovalRequestedPayload,
554
+ }
555
+ : {
556
+ index: event.index,
557
+ type: 'ai.approval.responded' as const,
558
+ payload: payload as ApprovalRespondedPayload,
559
+ }
560
+ return renderProjection({
561
+ state: {
562
+ ...state,
563
+ activeProjection: {
564
+ ...state.activeProjection,
565
+ cursor: null,
566
+ approvalEvents: [
567
+ ...state.activeProjection.approvalEvents,
568
+ approvalEvent,
569
+ ],
570
+ },
571
+ },
572
+ index: event.index,
573
+ })
574
+ }
575
+ case 'ai.compaction.requested': {
576
+ const payload = event.payload as CompactionRequestedPayload
577
+ if (
578
+ state.generation?.owner.generationId !== payload.generationId ||
579
+ state.generation.phase !== 'streaming'
580
+ )
581
+ return state
582
+ return {
583
+ ...state,
584
+ compaction: {
585
+ status: 'running',
586
+ generationId: payload.generationId,
587
+ throughMessageId: payload.throughMessageId,
588
+ throughIndex:
589
+ payload.throughIndex ?? state.activeProjection!.promptThroughIndex!,
590
+ },
591
+ }
592
+ }
593
+ case 'ai.compaction.completed': {
594
+ const payload = event.payload as CompactionCompletedPayload<M>
595
+ const projection = state.activeProjection
596
+ if (
597
+ state.generation?.owner.generationId !== payload.generationId ||
598
+ state.generation.phase !== 'streaming' ||
599
+ state.compaction?.status !== 'running' ||
600
+ state.compaction.generationId !== payload.generationId ||
601
+ !projection
602
+ )
603
+ return state
604
+ const throughIndex =
605
+ payload.throughIndex ?? projection.promptThroughIndex!
606
+ const replacements = new Set(
607
+ payload.messages.map((message) => message.id),
608
+ )
609
+ const retained = new Set(payload.retainedMessageIds ?? [])
610
+ const messages = [
611
+ ...payload.messages,
612
+ ...state.messages.filter(
613
+ (message) =>
614
+ !replacements.has(message.id) &&
615
+ (retained.has(message.id) ||
616
+ (state.messageIndexes[message.id] ?? 0) > throughIndex),
617
+ ),
618
+ ]
619
+ const activeProjection = { ...projection, cursor: null }
620
+ delete activeProjection.baseMessage
621
+ const baseMessage = messages.find(
622
+ (message) => message.id === projection.responseMessageId,
623
+ )
624
+ if (baseMessage !== undefined) activeProjection.baseMessage = baseMessage
625
+ const compaction: AIModelContext<M>['compaction'] = {
626
+ status: 'completed',
627
+ generationId: payload.generationId,
628
+ throughMessageId: payload.throughMessageId,
629
+ throughIndex,
630
+ ...(payload.summary === undefined ? {} : { summary: payload.summary }),
631
+ }
632
+ activeProjection.baseCompaction = compaction
633
+ return {
634
+ ...state,
635
+ messages,
636
+ compaction,
637
+ generation: {
638
+ ...state.generation,
639
+ responseIndex: payload.messages.length,
640
+ },
641
+ activeProjection,
642
+ messageIndexes: Object.fromEntries(
643
+ messages.map((message) => [
644
+ message.id,
645
+ replacements.has(message.id)
646
+ ? throughIndex
647
+ : state.messageIndexes[message.id]!,
648
+ ]),
649
+ ),
650
+ }
651
+ }
652
+ case 'ai.model.metadata.requested': {
653
+ const { modelId } = event.payload as ModelMetadataRequestedPayload
654
+ return Object.hasOwn(state.modelMetadata, modelId)
655
+ ? state
656
+ : {
657
+ ...state,
658
+ modelMetadata: {
659
+ ...state.modelMetadata,
660
+ [modelId]: { status: 'pending' },
661
+ },
662
+ }
663
+ }
664
+ case 'ai.model.metadata.resolved': {
665
+ const { modelId, limits } = event.payload as ModelMetadataResolvedPayload
666
+ return {
667
+ ...state,
668
+ modelMetadata: {
669
+ ...state.modelMetadata,
670
+ [modelId]:
671
+ limits === null
672
+ ? { status: 'unavailable' }
673
+ : { status: 'resolved', limits },
674
+ },
675
+ }
676
+ }
677
+ default:
678
+ return state
679
+ }
680
+ }