experimental-a2 0.16.0 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ai.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { $ as reduceAIState, A as MessageInterruptedPayload, B as SessionCreatedPayload, C as GenerationUsage, D as LanguageModelUsage, E as JSONValue, F as PendingApproval, G as UIMessageChunk, H as ToolCalledPayload, I as PendingInput, J as createReducer, K as agent, L as RetryGenerationOptions, M as ModelMetadataRequestedPayload, N as ModelMetadataResolvedPayload, O as MessageCompletedPayload, P as ModelMetadataState, Q as inputs, R as RetryRequestedPayload, S as GenerationStartedPayload, T as InputRespondedPayload, U as ToolResultPayload, V as ToolActivity, W as UIMessage, X as events, Y as deriveUIMessages, Z as handlerContext, _ as GenerationCompletedPayload, a as ActiveGenerationProjection, at as ControlReceipt, b as GenerationReason, c as AgentStatus, d as ApprovalRequestedPayload, et as AIStoredReducer, f as ApprovalRespondedPayload, g as FinishReason, h as CompactionState, i as ActiveGeneration, it as ControlCommand, j as ModelLimits, k as MessageCreatedPayload, l as AgentToolContext, m as CompactionRequestedPayload, n as AIInputs, nt as PendingQueueCommand, o as AgentDefinition, ot as InboxItem, p as CompactionCompletedPayload, q as createEvents, r as AIState, rt as ActiveTurn, s as AgentOptions, t as AIEventDefs, tt as AIStoredState, u as AgentToolEventDefs, v as GenerationFailedPayload, w as InputRequestedPayload, x as GenerationRequestedPayload, y as GenerationProgressPayload, z as SessionClosedPayload } from "./ai-HJ9fHfYI.js";
1
+ import { $ as reduceAIState, A as MessageInterruptedPayload, B as SessionCreatedPayload, C as GenerationUsage, D as LanguageModelUsage, E as JSONValue, F as PendingApproval, G as UIMessageChunk, H as ToolCalledPayload, I as PendingInput, J as createReducer, K as agent, L as RetryGenerationOptions, M as ModelMetadataRequestedPayload, N as ModelMetadataResolvedPayload, O as MessageCompletedPayload, P as ModelMetadataState, Q as inputs, R as RetryRequestedPayload, S as GenerationStartedPayload, T as InputRespondedPayload, U as ToolResultPayload, V as ToolActivity, W as UIMessage, X as events, Y as deriveUIMessages, Z as handlerContext, _ as GenerationCompletedPayload, a as ActiveGenerationProjection, at as ControlReceipt, b as GenerationReason, c as AgentStatus, d as ApprovalRequestedPayload, et as AIStoredReducer, f as ApprovalRespondedPayload, g as FinishReason, h as CompactionState, i as ActiveGeneration, it as ControlCommand, j as ModelLimits, k as MessageCreatedPayload, l as AgentToolContext, m as CompactionRequestedPayload, n as AIInputs, nt as PendingQueueCommand, o as AgentDefinition, ot as InboxItem, p as CompactionCompletedPayload, q as createEvents, r as AIState, rt as ActiveTurn, s as AgentOptions, t as AIEventDefs, tt as AIStoredState, u as AgentToolEventDefs, v as GenerationFailedPayload, w as InputRequestedPayload, x as GenerationRequestedPayload, y as GenerationProgressPayload, z as SessionClosedPayload } from "./ai-CrEf6p_W.js";
2
2
  export { AIEventDefs, AIInputs, AIState, type AIStoredReducer, type AIStoredState, ActiveGeneration, ActiveGenerationProjection, type ActiveTurn, AgentDefinition, AgentOptions, AgentStatus, AgentToolContext, AgentToolEventDefs, ApprovalRequestedPayload, ApprovalRespondedPayload, CompactionCompletedPayload, CompactionRequestedPayload, CompactionState, type ControlCommand, type ControlReceipt, type FinishReason, GenerationCompletedPayload, GenerationFailedPayload, GenerationProgressPayload, GenerationReason, GenerationRequestedPayload, GenerationStartedPayload, GenerationUsage, type InboxItem, InputRequestedPayload, InputRespondedPayload, type JSONValue, type LanguageModelUsage, MessageCompletedPayload, MessageCreatedPayload, MessageInterruptedPayload, ModelLimits, ModelMetadataRequestedPayload, ModelMetadataResolvedPayload, ModelMetadataState, PendingApproval, PendingInput, type PendingQueueCommand, RetryGenerationOptions, RetryRequestedPayload, SessionClosedPayload, SessionCreatedPayload, ToolActivity, ToolCalledPayload, ToolResultPayload, type UIMessage, type UIMessageChunk, agent, createEvents, createReducer, deriveUIMessages, events, handlerContext, inputs, reduceAIState };
package/dist/ai.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as events, c as reduceAIState, i as deriveUIMessages, n as createEvents, o as handlerContext, r as createReducer, s as inputs, t as agent } from "./ai-CrLYwNEx.js";
1
+ import { a as events, c as reduceAIState, i as deriveUIMessages, n as createEvents, o as handlerContext, r as createReducer, s as inputs, t as agent } from "./ai-DgOBltJ_.js";
2
2
  export { agent, createEvents, createReducer, deriveUIMessages, events, handlerContext, inputs, reduceAIState };
@@ -728,8 +728,10 @@ export function StopButton() {
728
728
  }
729
729
  ```
730
730
 
731
- The accepted command ends the turn and reaches A2's cancellation channel. `lastSeenIndex` is the exact confirmed log frontier visible when the
732
- user clicked. The reducer rewinds the indexed generation projection to that
731
+ The client immediately previews Stop for the observed turn. `lastSeenIndex` is the exact confirmed log frontier visible when the
732
+ user clicked. While that generation's indexed projection is retained, the client freezes the response at that
733
+ frontier through the request echo and controller decision. A rejected push or decision removes the preview.
734
+ The reducer rewinds the indexed generation projection to that
733
735
  frontier and keeps the partial text the user actually saw. Tool parts whose
734
736
  arguments are still streaming are drafts: interruption removes them from the
735
737
  conversation and subsequent model prompts, even if their current JSON parses.
@@ -740,6 +742,13 @@ input, and compaction events remain in raw history but cannot reactivate it or
740
742
  alter the projection.
741
743
  Completed tool results at or before the visible frontier remain completed.
742
744
 
745
+ Once Stop is persisted, A2's existing cancellation stream aborts running model
746
+ and tool work for that turn without waiting for the control handler. Startup
747
+ checks also skip matching jobs created while Stop is pending. Cancellation is
748
+ cooperative: tools must honor their abort signal. The control handler remains
749
+ responsible for recording the interruption and advancing the inbox; a Stop for
750
+ an older turn cannot cancel its successor.
751
+
743
752
  For the lower-level `inputs.interrupt()` builder, between
744
753
  `ai.generation.requested` and `ai.generation.started`, copy
745
754
  `activeRequestId` as `requestId` and `activeResponseMessageId` as `messageId`.
@@ -1219,8 +1228,9 @@ With `agent().reducer` or `createReducer()`, the client state is already optimis
1219
1228
  An idle, unpaused send appears in `state.messages` immediately, with
1220
1229
  `state.starting: true`. Another send waits in `state.inbox.items` while that
1221
1230
  optimistic turn or a confirmed turn is active. Paused sessions keep new sends in
1222
- the inbox. Render the conversation and queue from `state`; `state.active` and
1223
- execution controls remain server-confirmed.
1231
+ the inbox. Render the conversation and queue from `state`. A pending Stop clears
1232
+ the displayed `state.active` turn immediately; rejection restores it. New active
1233
+ turn identities still come from accepted control decisions.
1224
1234
  Steering appears immediately in `state.messages`, after the current response,
1225
1235
  and stays there while that step finishes. Send now has the same display when
1226
1236
  an active turn is targeted. These inputs are omitted from the client inbox,
@@ -1267,7 +1277,7 @@ A custom reducer can keep normalized AI state by nesting `agent.reducer.fold()`
1267
1277
  and derive its view with `agent.reducer.view()`. It does not inherit the built-in
1268
1278
  optimistic client display. `reduceAIState()` remains a pure transition over the
1269
1279
  confirmed `AIState` view for applications that project raw event histories.
1270
- `state.pendingQueueCommands` tracks unresolved queue commands incrementally, so
1280
+ `state.pendingQueueCommands` tracks unresolved queue commands and Stops incrementally, so
1271
1281
  the client does not scan raw history. Pending edits apply in order, so the latest
1272
1282
  local edit stays visible while earlier commands are confirmed. Editing and Send
1273
1283
  now can remain available while an edit awaits its receipt. Close an editor as
@@ -1234,7 +1234,7 @@ message type. Both are isomorphic.
1234
1234
 
1235
1235
  Clients using `agent().reducer` or `createReducer()` expose `state.messages`,
1236
1236
  `state.inbox`, and `state.starting` with pending send, edit, move, remove,
1237
- Send now, steer, pause, and resume commands applied for display. Idle, unpaused
1237
+ Send now, steer, pause, resume, and Stop commands applied for display. Idle, unpaused
1238
1238
  sends appear in the conversation immediately. A generating input occupies the
1239
1239
  optimistic turn, so subsequent sends queue. Busy or paused sessions queue new
1240
1240
  sends. `starting` indicates a speculative turn awaiting acceptance; it does not
@@ -1245,7 +1245,12 @@ inbox. This display survives receipt and hydration, including while paused.
1245
1245
  They remain pending server inputs until admission and do not enter the model
1246
1246
  prompt early. Multiple steering inputs follow inbox admission order.
1247
1247
 
1248
- `state.active` and model execution stay confirmed.
1248
+ New active turn identities come from accepted control decisions. A pending Stop
1249
+ for that turn clears the displayed `state.active`, generation, and pending
1250
+ approvals/inputs, and previews an idle status (or paused when the inbox is paused). With
1251
+ `lastSeenIndex`, its retained generation projection freezes at the observed
1252
+ frontier. The preview lasts through the request echo until the controller's
1253
+ decision; rejected requests restore the confirmed active turn and output.
1249
1254
  Server reads and reducer folds retain confirmed messages and inbox entries;
1250
1255
  `state.starting` is false there. Custom reducers that call `reduceAIState()` or
1251
1256
  nest the AI fold do not inherit the client display behavior. Repeat editing and
@@ -1313,7 +1318,7 @@ Checkpoint size and serialization cost still grow with retained progress.
1313
1318
  Tool-input deltas update `input-streaming` parts with best-effort partial JSON
1314
1319
  for display. The state stays `input-streaming` until a completed input arrives;
1315
1320
  partial parsing does not authorize execution or change interruption handling.
1316
- Extension events are ignored. The default reducer name is `a2.ai.state.v21:records-v2`.
1321
+ Extension events are ignored. The default reducer name is `a2.ai.state.v22:records-v2`.
1317
1322
 
1318
1323
  ### `deriveUIMessages(history)` and `reduceAIState(state, event)`
1319
1324
 
@@ -22,7 +22,7 @@
22
22
  "@vercel/sandbox": "^3.0.0",
23
23
  "ai": "^7.0.58",
24
24
  "codemirror": "^6.0.2",
25
- "experimental-a2": "0.16.0",
25
+ "experimental-a2": "0.16.1",
26
26
  "ioredis": "^5.9.0",
27
27
  "next": "^16.3.0",
28
28
  "pg": "^8.16.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "experimental-a2",
3
- "version": "0.16.0",
3
+ "version": "0.16.1",
4
4
  "description": "Durable sync and reactions for things with a lifecycle: one event log, derived state, and live client per session.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,6 +1,7 @@
1
1
  import type { UIMessage } from 'ai'
2
2
  import type { AIState } from './ai.ts'
3
3
  import type { ControlCommand, InboxItem } from './ai-control.ts'
4
+ import { interruptGenerationProjection } from './ai-message-projection.ts'
4
5
 
5
6
  export type QueueCommand<M extends UIMessage> = Extract<
6
7
  ControlCommand<M>,
@@ -14,6 +15,7 @@ export type QueueCommand<M extends UIMessage> = Extract<
14
15
  | 'steer'
15
16
  | 'pause'
16
17
  | 'resume'
18
+ | 'stop'
17
19
  }
18
20
  >
19
21
  export type PendingQueueCommand<M extends UIMessage> = {
@@ -31,7 +33,8 @@ export const isQueueCommand = <M extends UIMessage>(
31
33
  command.action === 'send-now' ||
32
34
  command.action === 'steer' ||
33
35
  command.action === 'pause' ||
34
- command.action === 'resume'
36
+ command.action === 'resume' ||
37
+ command.action === 'stop'
35
38
 
36
39
  const moveItem = <M extends UIMessage>({
37
40
  items,
@@ -166,6 +169,8 @@ export function projectAIClientState<M extends UIMessage>(
166
169
  paused = false
167
170
  suspended = false
168
171
  break
172
+ case 'stop':
173
+ continue
169
174
  }
170
175
  admit()
171
176
  }
@@ -174,10 +179,39 @@ export function projectAIClientState<M extends UIMessage>(
174
179
  show(item.message)
175
180
  return false
176
181
  })
177
- return {
182
+ const displayed = {
178
183
  ...state,
179
184
  messages,
180
185
  inbox: { items, paused },
181
186
  starting: starting && !suspended,
182
187
  }
188
+ const stop = pending.find(
189
+ ({ command }) =>
190
+ command.action === 'stop' && command.turnId === state.active?.turnId,
191
+ )?.command
192
+ if (stop?.action !== 'stop') return displayed
193
+ const projection = state.activeProjection
194
+ const stopped =
195
+ projection === null
196
+ ? displayed
197
+ : (interruptGenerationProjection(
198
+ displayed,
199
+ projection.generationId,
200
+ projection.responseMessageId,
201
+ stop.lastSeenIndex ?? Number.POSITIVE_INFINITY,
202
+ ) ?? displayed)
203
+ return {
204
+ ...stopped,
205
+ active: null,
206
+ status: paused ? 'paused' : 'idle',
207
+ starting: false,
208
+ activeGeneration: null,
209
+ activeRequestId: null,
210
+ activeRequestReason: null,
211
+ activeRequestSourceGenerationId: null,
212
+ activeResponseMessageId: null,
213
+ pendingApprovals: [],
214
+ pendingInputs: [],
215
+ error: null,
216
+ }
183
217
  }
@@ -73,17 +73,36 @@ export const createControlRuntime = <
73
73
  }): AIControlRuntime<M, D> => {
74
74
  const reducer = options.agent.contract
75
75
  .reducer({
76
- name: 'a2.ai.control.v4',
76
+ name: 'a2.ai.control.v5',
77
77
  initialState: initialControlState<M>(),
78
78
  })
79
- .fold((state, event) =>
80
- event.type === 'ai.control.committed'
79
+ .fold((state, event) => {
80
+ if (event.type === 'ai.control.requested') {
81
+ const command = event.payload as ControlCommand<M>
82
+ if (command.action === 'stop')
83
+ return {
84
+ ...state,
85
+ pendingStops: {
86
+ ...state.pendingStops,
87
+ [event.id]: command.turnId,
88
+ },
89
+ }
90
+ }
91
+ if (event.type === 'ai.control.decided') {
92
+ const receipt = event.payload as ControlReceipt
93
+ if (Object.hasOwn(state.pendingStops, receipt.commandId)) {
94
+ const pendingStops = { ...state.pendingStops }
95
+ delete pendingStops[receipt.commandId]
96
+ return { ...state, pendingStops }
97
+ }
98
+ }
99
+ return event.type === 'ai.control.committed'
81
100
  ? applyControlChanges({
82
101
  state,
83
102
  changes: (event.payload as ControlCommit).changes,
84
103
  })
85
- : state,
86
- )
104
+ : state
105
+ })
87
106
  const coordinator = aiCoordinatorReducer(options.agent.contract)
88
107
 
89
108
  const handler = async (
package/src/ai-control.ts CHANGED
@@ -77,6 +77,7 @@ export type ControlWork = {
77
77
  }
78
78
 
79
79
  export type AIControlState<M extends UIMessage = UIMessage> = {
80
+ pendingStops: Record<string, string>
80
81
  inbox: { paused: boolean; items: InboxItem<M>[] }
81
82
  active:
82
83
  | (ActiveTurn<M> & {
@@ -109,6 +110,7 @@ export type ToolExecutionRequest = {
109
110
  export const initialControlState = <
110
111
  M extends UIMessage,
111
112
  >(): AIControlState<M> => ({
113
+ pendingStops: {},
112
114
  inbox: { paused: false, items: [] },
113
115
  active: null,
114
116
  coordinator: { closed: false, queued: [] },
@@ -1,13 +1,18 @@
1
1
  import type { UIMessage, UIMessageChunk } from 'ai'
2
2
  import type {
3
3
  ActiveGenerationProjection,
4
+ AIState,
4
5
  ApprovalRequestedPayload,
5
6
  ApprovalRespondedPayload,
6
7
  ToolCalledPayload,
7
8
  ToolResultPayload,
8
9
  ToolActivity,
9
10
  } from './ai.ts'
10
- import { type UIMessageCursor, projectUIMessageStream } from './ai-projector.ts'
11
+ import {
12
+ type UIMessageCursor,
13
+ projectUIMessageStream,
14
+ interruptUIMessage,
15
+ } from './ai-projector.ts'
11
16
  import {
12
17
  flattenProgressBatches,
13
18
  appendProgressBatch,
@@ -346,3 +351,124 @@ export const advanceProjection = <M extends UIMessage>({
346
351
  },
347
352
  }
348
353
  }
354
+
355
+ export const projectedTools = <M extends UIMessage>(
356
+ projection: ActiveGenerationProjection<M>,
357
+ ): ToolActivity[] =>
358
+ projection.toolEvents.reduce(
359
+ (tools, event) => reduceToolActivity(tools, event),
360
+ projection.baseTools,
361
+ )
362
+
363
+ export const restoreGenerationBase = <M extends UIMessage>(
364
+ state: AIState<M>,
365
+ generationId: string,
366
+ responseMessageId: string,
367
+ reason: 'interrupted' | 'superseded' | 'failed',
368
+ ): AIState<M> => {
369
+ const projection =
370
+ state.activeProjection?.generationId === generationId
371
+ ? state.activeProjection
372
+ : null
373
+ return {
374
+ ...state,
375
+ messages:
376
+ projection?.baseMessage === undefined
377
+ ? state.messages.filter((message) => message.id !== responseMessageId)
378
+ : upsertMessage(state.messages, projection.baseMessage),
379
+ tools:
380
+ projection === null
381
+ ? state.tools.filter(
382
+ (activity) => activity.generationId !== generationId,
383
+ )
384
+ : projection.baseTools,
385
+ pendingApprovals: state.pendingApprovals.filter(
386
+ (approval) => approval.generationId !== generationId,
387
+ ),
388
+ pendingInputs: state.pendingInputs.filter(
389
+ (input) => input.generationId !== generationId,
390
+ ),
391
+ activeGeneration: null,
392
+ activeProjection: null,
393
+ terminalGenerations: {
394
+ ...state.terminalGenerations,
395
+ [generationId]: reason,
396
+ },
397
+ compaction:
398
+ projection === null
399
+ ? state.compaction?.generationId === generationId
400
+ ? null
401
+ : state.compaction
402
+ : (projection.baseCompaction ?? null),
403
+ }
404
+ }
405
+
406
+ export const stopGenerationTools = (
407
+ tools: ToolActivity[],
408
+ generationId: string | undefined,
409
+ responseMessageId: string,
410
+ error: string,
411
+ ): ToolActivity[] =>
412
+ tools.map((activity) =>
413
+ activity.status === 'running' &&
414
+ (generationId === undefined
415
+ ? activity.messageId === responseMessageId
416
+ : activity.generationId === generationId)
417
+ ? { ...activity, status: 'failed', error, preliminary: false }
418
+ : activity,
419
+ )
420
+
421
+ export const interruptGenerationProjection = <M extends UIMessage>(
422
+ state: AIState<M>,
423
+ generationId: string,
424
+ responseMessageId: string,
425
+ cutoff: number,
426
+ ): AIState<M> | null => {
427
+ const projection =
428
+ state.activeProjection?.generationId === generationId
429
+ ? state.activeProjection
430
+ : null
431
+ if (projection === null) return null
432
+ const retained: ActiveGenerationProjection<M> = {
433
+ ...projection,
434
+ batches: progressBatches(
435
+ flattenProgressBatches(projection.batches).filter(
436
+ (batch) => batch.index <= cutoff,
437
+ ),
438
+ ),
439
+ toolEvents: projection.toolEvents.filter(
440
+ (toolEvent) => toolEvent.index <= cutoff,
441
+ ),
442
+ approvalEvents: projection.approvalEvents.filter(
443
+ (approvalEvent) => approvalEvent.index <= cutoff,
444
+ ),
445
+ }
446
+ const shouldProject =
447
+ retained.baseMessage !== undefined || retained.batches.length > 0
448
+ const restored = restoreGenerationBase(
449
+ state,
450
+ generationId,
451
+ responseMessageId,
452
+ 'interrupted',
453
+ )
454
+ return {
455
+ ...restored,
456
+ messages: shouldProject
457
+ ? upsertMessage(
458
+ state.messages,
459
+ interruptUIMessage(projectedMessage(retained)),
460
+ )
461
+ : restored.messages,
462
+ tools: stopGenerationTools(
463
+ projectedTools(retained),
464
+ generationId,
465
+ responseMessageId,
466
+ 'Tool execution was interrupted.',
467
+ ),
468
+ usage:
469
+ projection.completionIndex === undefined ||
470
+ projection.completionIndex <= cutoff
471
+ ? restored.usage
472
+ : restored.usage.filter((entry) => entry.generationId !== generationId),
473
+ }
474
+ }
package/src/ai-server.ts CHANGED
@@ -1219,6 +1219,7 @@ export function createHandlers<
1219
1219
  )
1220
1220
  if (
1221
1221
  state.active?.turnId !== request.turnId ||
1222
+ Object.values(state.pendingStops).includes(request.turnId) ||
1222
1223
  state.active.suspended ||
1223
1224
  state.active.version !== request.version ||
1224
1225
  state.coordinator.response?.failure !== undefined ||
@@ -1256,9 +1257,15 @@ export function createHandlers<
1256
1257
  snapshot.state.authority?.phase === 'pausing'
1257
1258
  )
1258
1259
  return
1259
- const coordinatorState = (
1260
- await ctx.session.state(coordinator, { through: snapshot.index })
1260
+ const controlState = (
1261
+ await ctx.session.state(control.reducer, { through: snapshot.index })
1261
1262
  ).state
1263
+ if (
1264
+ request.control !== undefined &&
1265
+ Object.values(controlState.pendingStops).includes(request.control.turnId)
1266
+ )
1267
+ return
1268
+ const coordinatorState = controlState.coordinator
1262
1269
  if (
1263
1270
  coordinatorState.closed ||
1264
1271
  coordinatorState.response?.activeRequestId !== requestId
@@ -1876,6 +1883,9 @@ export function createHandlers<
1876
1883
  'ai.generation.requested': {
1877
1884
  lane: 'a2.ai.model',
1878
1885
  abortOn: {
1886
+ 'ai.control.requested': (event, trigger) =>
1887
+ event.payload.action === 'stop' &&
1888
+ event.payload.turnId === trigger.payload.control?.turnId,
1879
1889
  'ai.control.committed': (event, trigger) => {
1880
1890
  const active = (event.payload as ControlCommit).view
1881
1891
  return (
@@ -1897,6 +1907,9 @@ export function createHandlers<
1897
1907
  },
1898
1908
  'ai.tool.execution.requested': {
1899
1909
  abortOn: {
1910
+ 'ai.control.requested': (event, trigger) =>
1911
+ event.payload.action === 'stop' &&
1912
+ event.payload.turnId === trigger.payload.turnId,
1900
1913
  'ai.generation.failed': (event, trigger) =>
1901
1914
  event.payload.generationId === trigger.payload.call.generationId,
1902
1915
  'ai.control.committed': (event, trigger) => {
package/src/ai.ts CHANGED
@@ -53,6 +53,10 @@ import {
53
53
  approvalRequestedChunk,
54
54
  toolResultChunk,
55
55
  advanceProjection,
56
+ projectedTools,
57
+ restoreGenerationBase,
58
+ stopGenerationTools,
59
+ interruptGenerationProjection,
56
60
  projectedMessage,
57
61
  reduceToolActivity,
58
62
  } from './ai-message-projection.ts'
@@ -1027,127 +1031,6 @@ const projectedResponse = <M extends UIMessage>(options: {
1027
1031
  : message
1028
1032
  }
1029
1033
 
1030
- const projectedTools = <M extends UIMessage>(
1031
- projection: ActiveGenerationProjection<M>,
1032
- ): ToolActivity[] =>
1033
- projection.toolEvents.reduce(
1034
- (tools, event) => reduceToolActivity(tools, event),
1035
- projection.baseTools,
1036
- )
1037
-
1038
- const restoreGenerationBase = <M extends UIMessage>(
1039
- state: AIState<M>,
1040
- generationId: string,
1041
- responseMessageId: string,
1042
- reason: 'interrupted' | 'superseded' | 'failed',
1043
- ): AIState<M> => {
1044
- const projection =
1045
- state.activeProjection?.generationId === generationId
1046
- ? state.activeProjection
1047
- : null
1048
- return {
1049
- ...state,
1050
- messages:
1051
- projection?.baseMessage === undefined
1052
- ? state.messages.filter((message) => message.id !== responseMessageId)
1053
- : upsertMessage(state.messages, projection.baseMessage),
1054
- tools:
1055
- projection === null
1056
- ? state.tools.filter(
1057
- (activity) => activity.generationId !== generationId,
1058
- )
1059
- : projection.baseTools,
1060
- pendingApprovals: state.pendingApprovals.filter(
1061
- (approval) => approval.generationId !== generationId,
1062
- ),
1063
- pendingInputs: state.pendingInputs.filter(
1064
- (input) => input.generationId !== generationId,
1065
- ),
1066
- activeGeneration: null,
1067
- activeProjection: null,
1068
- terminalGenerations: {
1069
- ...state.terminalGenerations,
1070
- [generationId]: reason,
1071
- },
1072
- compaction:
1073
- projection === null
1074
- ? state.compaction?.generationId === generationId
1075
- ? null
1076
- : state.compaction
1077
- : (projection.baseCompaction ?? null),
1078
- }
1079
- }
1080
-
1081
- const stopGenerationTools = (
1082
- tools: ToolActivity[],
1083
- generationId: string | undefined,
1084
- responseMessageId: string,
1085
- error: string,
1086
- ): ToolActivity[] =>
1087
- tools.map((activity) =>
1088
- activity.status === 'running' &&
1089
- (generationId === undefined
1090
- ? activity.messageId === responseMessageId
1091
- : activity.generationId === generationId)
1092
- ? { ...activity, status: 'failed', error, preliminary: false }
1093
- : activity,
1094
- )
1095
-
1096
- const interruptGenerationProjection = <M extends UIMessage>(
1097
- state: AIState<M>,
1098
- generationId: string,
1099
- responseMessageId: string,
1100
- cutoff: number,
1101
- ): AIState<M> | null => {
1102
- const projection =
1103
- state.activeProjection?.generationId === generationId
1104
- ? state.activeProjection
1105
- : null
1106
- if (projection === null) return null
1107
- const retained: ActiveGenerationProjection<M> = {
1108
- ...projection,
1109
- batches: progressBatches(
1110
- flattenProgressBatches(projection.batches).filter(
1111
- (batch) => batch.index <= cutoff,
1112
- ),
1113
- ),
1114
- toolEvents: projection.toolEvents.filter(
1115
- (toolEvent) => toolEvent.index <= cutoff,
1116
- ),
1117
- approvalEvents: projection.approvalEvents.filter(
1118
- (approvalEvent) => approvalEvent.index <= cutoff,
1119
- ),
1120
- }
1121
- const shouldProject =
1122
- retained.baseMessage !== undefined || retained.batches.length > 0
1123
- const restored = restoreGenerationBase(
1124
- state,
1125
- generationId,
1126
- responseMessageId,
1127
- 'interrupted',
1128
- )
1129
- return {
1130
- ...restored,
1131
- messages: shouldProject
1132
- ? upsertMessage(
1133
- state.messages,
1134
- interruptUIMessage(projectedMessage(retained)),
1135
- )
1136
- : restored.messages,
1137
- tools: stopGenerationTools(
1138
- projectedTools(retained),
1139
- generationId,
1140
- responseMessageId,
1141
- 'Tool execution was interrupted.',
1142
- ),
1143
- usage:
1144
- projection.completionIndex === undefined ||
1145
- projection.completionIndex <= cutoff
1146
- ? restored.usage
1147
- : restored.usage.filter((entry) => entry.generationId !== generationId),
1148
- }
1149
- }
1150
-
1151
1034
  const terminalGenerationReason = <M extends UIMessage>(
1152
1035
  state: AIState<M>,
1153
1036
  generationId: string,
@@ -2306,7 +2189,7 @@ export function createReducer<D extends EventDefs>(options: {
2306
2189
  type M = AIMessageFrom<D>
2307
2190
  const reducer = options.contract
2308
2191
  .reducer({
2309
- name: options.name ?? 'a2.ai.state.v21',
2192
+ name: options.name ?? 'a2.ai.state.v22',
2310
2193
  initialState: initialState<M>(),
2311
2194
  ...(options.stateSchema === undefined
2312
2195
  ? {}
@@ -2505,7 +2388,7 @@ export function agent<
2505
2388
  })
2506
2389
  const reducer = agentContract
2507
2390
  .reducer({
2508
- name: options.reducerName ?? 'a2.ai.state.v21',
2391
+ name: options.reducerName ?? 'a2.ai.state.v22',
2509
2392
  initialState: initialState<M>(),
2510
2393
  })
2511
2394
  .fold((state, event) => reduceAIState(state, event))