experimental-a2 0.13.0 → 0.14.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 (99) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/{actor-DJi3RsNu.d.ts → actor-BfQSE0KC.d.ts} +4 -4
  3. package/dist/{actor-DJi3RsNu.d.ts.map → actor-BfQSE0KC.d.ts.map} +1 -1
  4. package/dist/actor-client.d.ts +1 -1
  5. package/dist/actor-client.js +1 -1
  6. package/dist/actor-react.d.ts +3 -3
  7. package/dist/actor-react.js +2 -2
  8. package/dist/{actor-shared-DI7J5upy.js → actor-shared-B5tJfzt-.js} +2 -2
  9. package/dist/{actor-shared-DI7J5upy.js.map → actor-shared-B5tJfzt-.js.map} +1 -1
  10. package/dist/actor.d.ts +1 -1
  11. package/dist/actor.js +3 -3
  12. package/dist/ai-Cai-lCbj.d.ts +580 -0
  13. package/dist/ai-Cai-lCbj.d.ts.map +1 -0
  14. package/dist/ai-control-CcD4hh3y.js +119 -0
  15. package/dist/ai-control-CcD4hh3y.js.map +1 -0
  16. package/dist/ai-server.d.ts +6 -6
  17. package/dist/ai-server.d.ts.map +1 -1
  18. package/dist/ai-server.js +1014 -501
  19. package/dist/ai-server.js.map +1 -1
  20. package/dist/ai.d.ts +2 -365
  21. package/dist/ai.js +801 -80
  22. package/dist/ai.js.map +1 -1
  23. package/dist/{client-P_NNNRM-.d.ts → client-BAEABRZB.d.ts} +2 -2
  24. package/dist/{client-P_NNNRM-.d.ts.map → client-BAEABRZB.d.ts.map} +1 -1
  25. package/dist/{client-Bf6uSEAk.js → client-BYzHjkwU.js} +21 -6
  26. package/dist/client-BYzHjkwU.js.map +1 -0
  27. package/dist/client.d.ts +1 -1
  28. package/dist/client.js +1 -1
  29. package/dist/{contract-48bUMgcL.js → contract-CKRg_E4q.js} +3 -26
  30. package/dist/contract-CKRg_E4q.js.map +1 -0
  31. package/dist/index.d.ts +2 -2
  32. package/dist/index.js +1 -1
  33. package/dist/react.d.ts +2 -2
  34. package/dist/react.js +1 -1
  35. package/dist/{reducer-DJKWm3cp.d.ts → reducer-BcS9VDKC.d.ts} +4 -1
  36. package/dist/{reducer-DJKWm3cp.d.ts.map → reducer-BcS9VDKC.d.ts.map} +1 -1
  37. package/dist/reducer-DEMjEY_O.js +29 -0
  38. package/dist/reducer-DEMjEY_O.js.map +1 -0
  39. package/dist/scheduler-qstash.d.ts +2 -2
  40. package/dist/scheduler-qstash.js +1 -1
  41. package/dist/scheduler-vercel.d.ts +2 -2
  42. package/dist/scheduler-vercel.js +1 -1
  43. package/dist/{server-DjZZa1wr.d.ts → server-Bp5Nd1pF.d.ts} +3 -3
  44. package/dist/{server-DjZZa1wr.d.ts.map → server-Bp5Nd1pF.d.ts.map} +1 -1
  45. package/dist/{server-BeNADlCI.js → server-CjJSGcF7.js} +4 -3
  46. package/dist/server-CjJSGcF7.js.map +1 -0
  47. package/dist/server.d.ts +3 -3
  48. package/dist/server.js +1 -1
  49. package/dist/{store-DtDOWLSn.d.ts → store-D_yhNdPz.d.ts} +7 -2
  50. package/dist/{store-DtDOWLSn.d.ts.map → store-D_yhNdPz.d.ts.map} +1 -1
  51. package/dist/store-N8PXxDAS.js.map +1 -1
  52. package/dist/store-memory.d.ts +1 -1
  53. package/dist/store-postgres.d.ts +1 -1
  54. package/dist/store-postgres.js +19 -0
  55. package/dist/store-postgres.js.map +1 -1
  56. package/dist/store-redis-http.d.ts +1 -1
  57. package/dist/store-redis-http.js +1 -1
  58. package/dist/{store-redis-notify-BUCyXOn0.js → store-redis-notify-D2EI6gwX.js} +27 -2
  59. package/dist/store-redis-notify-D2EI6gwX.js.map +1 -0
  60. package/dist/store-redis.d.ts +1 -1
  61. package/dist/store-redis.js +1 -1
  62. package/dist/store-sqlite.d.ts +1 -1
  63. package/docs/guides/06-ai-agents.mdx +265 -62
  64. package/docs/reference/01-api.mdx +122 -27
  65. package/examples/playground/app/agent/[agentId]/agent-client.tsx +118 -21
  66. package/examples/playground/app/agent/[agentId]/agent-queue.tsx +289 -0
  67. package/examples/playground/app/agent/compaction-settings.test.ts +22 -6
  68. package/examples/playground/app/agent/model.ts +11 -2
  69. package/examples/playground/app/agent/server.ts +8 -2
  70. package/examples/playground/app/chat/[chatId]/chat-client.tsx +3 -13
  71. package/examples/playground/app/chat/model.ts +2 -2
  72. package/examples/playground/app/chat/server.ts +24 -17
  73. package/examples/playground/app/globals.css +179 -0
  74. package/examples/playground/package.json +1 -1
  75. package/package.json +1 -1
  76. package/src/ai-client-state.ts +185 -0
  77. package/src/ai-control-server.ts +829 -0
  78. package/src/ai-control-state.ts +152 -0
  79. package/src/ai-control.ts +139 -0
  80. package/src/ai-coordinator.ts +99 -32
  81. package/src/ai-progress-batches.ts +68 -0
  82. package/src/ai-projector.ts +76 -15
  83. package/src/ai-sdk-step.ts +0 -1
  84. package/src/ai-server.ts +381 -608
  85. package/src/ai.ts +553 -108
  86. package/src/client.ts +31 -9
  87. package/src/licenses/Apache-2.0.txt +55 -0
  88. package/src/parse-partial-json.ts +441 -0
  89. package/src/reducer.ts +6 -0
  90. package/src/server.ts +8 -4
  91. package/src/store-postgres.ts +27 -0
  92. package/src/store-redis-core.ts +53 -1
  93. package/src/store-redis-notify.ts +1 -0
  94. package/src/store.ts +6 -0
  95. package/dist/ai.d.ts.map +0 -1
  96. package/dist/client-Bf6uSEAk.js.map +0 -1
  97. package/dist/contract-48bUMgcL.js.map +0 -1
  98. package/dist/server-BeNADlCI.js.map +0 -1
  99. package/dist/store-redis-notify-BUCyXOn0.js.map +0 -1
package/src/ai-server.ts CHANGED
@@ -8,6 +8,8 @@
8
8
  // oxlint-disable no-await-in-loop -- stream chunks and durable appends are
9
9
  // ordered; parallel consumption would corrupt progress sequence and state.
10
10
 
11
+ import { createControlRuntime, ControlCancelled } from './ai-control-server.ts'
12
+ import type { ControlCommit } from './ai-control.ts'
11
13
  import { AsyncLocalStorage } from 'node:async_hooks'
12
14
  import { asSchema, convertToModelMessages } from 'ai'
13
15
  import type {
@@ -34,17 +36,13 @@ import type {
34
36
  GenerationCompletedPayload,
35
37
  GenerationFailedPayload,
36
38
  GenerationProgressPayload,
37
- GenerationRequestedPayload,
38
39
  GenerationStartedPayload,
39
40
  MessageCompletedPayload,
40
- MessageCreatedPayload,
41
- MessageInterruptedPayload,
42
41
  ToolCalledPayload,
43
42
  ToolResultPayload,
44
43
  } from './ai.ts'
45
44
  import {
46
45
  aiCoordinatorReducer,
47
- continuationReady,
48
46
  type AICoordinatorState,
49
47
  } from './ai-coordinator.ts'
50
48
  import type { AppendInput, ContractEvent, EventDefs } from './contract.ts'
@@ -67,24 +65,23 @@ import {
67
65
 
68
66
  function validateAgentIngress(context: ServerIngressContext): void {
69
67
  const rejected = context.events.find((event) => {
70
- if (event.type !== 'ai.message.created') {
71
- return !(
72
- event.type === 'ai.approval.responded' ||
73
- event.type === 'ai.input.responded' ||
74
- event.type === 'ai.message.interrupted' ||
75
- event.type === 'ai.retry.requested'
68
+ if (event.type !== 'ai.control.requested') return true
69
+ const command = event.payload as Record<string, unknown>
70
+ if (
71
+ command['action'] === 'request-input' ||
72
+ command['action'] === 'tool-result'
73
+ )
74
+ return true
75
+ if (
76
+ command['action'] === 'send' ||
77
+ command['action'] === 'edit' ||
78
+ command['action'] === 'steer'
79
+ ) {
80
+ return (
81
+ (command['message'] as { role?: unknown } | undefined)?.role !== 'user'
76
82
  )
77
83
  }
78
- const payload = event.payload
79
- return (
80
- typeof payload !== 'object' ||
81
- payload === null ||
82
- !('message' in payload) ||
83
- typeof payload.message !== 'object' ||
84
- payload.message === null ||
85
- !('role' in payload.message) ||
86
- payload.message.role !== 'user'
87
- )
84
+ return false
88
85
  })
89
86
  if (rejected !== undefined) {
90
87
  throw new A2Error(
@@ -119,7 +116,7 @@ export type AgentGenerateContext<
119
116
  messages: M[]
120
117
  modelMessages: ModelMessage[]
121
118
  state: AIState<M>
122
- history: ContractEvent<D>[]
119
+ session: Pick<HandlerContext<D>['session'], 'state'>
123
120
  signal: AbortSignal
124
121
  model: LanguageModel
125
122
  tools: T
@@ -152,7 +149,7 @@ export type AgentResolverContext<
152
149
  > = {
153
150
  event: ContractEvent<D, 'ai.generation.requested'>
154
151
  state: AIState<M>
155
- history: ContractEvent<D>[]
152
+ session: Pick<HandlerContext<D>['session'], 'state'>
156
153
  signal: AbortSignal
157
154
  }
158
155
 
@@ -214,9 +211,7 @@ export type CreateHandlersOptions<
214
211
  * The custom stream owns its metadata chunks.
215
212
  */
216
213
  generate?: AgentGenerate<M, D, T>
217
- compaction?:
218
- | CompactionOptions<M, D>
219
- | ((context: AgentResolverContext<M, D>) => CompactionOptions<M, D>)
214
+ compaction?: Resolvable<CompactionOptions<M, D>, AgentResolverContext<M, D>>
220
215
  /** Progress is durably flushed at either limit, whichever is reached first. */
221
216
  progress?: { maxChunks?: number; maxDelayMs?: number }
222
217
  }
@@ -443,12 +438,17 @@ async function* consumeGeneration(options: {
443
438
  }
444
439
  }
445
440
 
446
- const replay = <M extends UIMessage, D extends AIEventDefs<M> & EventDefs>(
447
- definition: AgentDefinition<M, D>,
448
- history: ContractEvent<D>[],
449
- ): AIState<M> => {
450
- let state = definition.reducer.initialState
451
- for (const event of history) state = definition.reducer.fold(state, event)
441
+ const foldAIEvents = <
442
+ M extends UIMessage,
443
+ D extends AIEventDefs<M> & EventDefs,
444
+ >(options: {
445
+ agent: AgentDefinition<M, D>
446
+ state: AIState<M>
447
+ events: ContractEvent<D>[]
448
+ }): AIState<M> => {
449
+ let state = options.state
450
+ for (const event of options.events)
451
+ state = options.agent.reducer.fold(state, event)
452
452
  return state
453
453
  }
454
454
 
@@ -551,71 +551,83 @@ const activeCompaction = <M extends UIMessage>(
551
551
  : null
552
552
  }
553
553
 
554
- const contextMessages = <
554
+ const contextMessages = async <
555
555
  M extends UIMessage,
556
556
  D extends AIEventDefs<M> & EventDefs,
557
557
  >(options: {
558
558
  agent: AgentDefinition<M, D>
559
- state: AIState<M>
560
- history: ContractEvent<D>[]
561
- }): M[] => {
562
- const { agent, state, history } = options
559
+ session: HandlerContext<D>['session']
560
+ snapshot: { state: AIState<M>; index: number }
561
+ coordinator: AICoordinatorState
562
+ appended?: ContractEvent<D>[]
563
+ excluded?: ReadonlySet<string>
564
+ }): Promise<M[]> => {
565
+ const { agent, session, snapshot } = options
566
+ const appended = options.appended ?? []
567
+ const state = foldAIEvents({ agent, events: appended, state: snapshot.state })
563
568
  const compaction = activeCompaction(state)
564
- if (compaction === null) return state.messages
569
+ const queued = new Set(
570
+ options.coordinator.queued.map((item) => item.messageId),
571
+ )
572
+ const visible = (messages: M[]): M[] =>
573
+ messages.filter((message) => !queued.has(message.id))
574
+ if (compaction === null) return visible(state.messages)
565
575
  const retained = new Set(compaction.retainedMessageIds ?? [])
566
- const startedIndex = history.find(
567
- (event) =>
568
- event.type === 'ai.generation.started' &&
569
- (event.payload as GenerationStartedPayload).generationId ===
570
- compaction.generationId,
571
- )?.index
572
- const throughIndex =
573
- compaction.throughIndex ??
574
- (startedIndex === undefined ? undefined : startedIndex - 1)
576
+ const throughIndex = compaction.throughIndex
575
577
  if (throughIndex !== undefined) {
576
- const prefix = history.filter((event) => event.index <= throughIndex)
577
- for (const queued of queuedMessagesAt(prefix))
578
- retained.add(queued.messageId)
579
- let context = replay(agent, prefix)
580
- context = {
581
- ...context,
582
- messages: [
583
- ...compaction.messages!,
584
- ...context.messages.filter((message) => retained.has(message.id)),
585
- ],
586
- activeProjection: null,
587
- }
588
- for (const event of history) {
589
- if (event.index > throughIndex)
590
- context = agent.reducer.fold(context, event)
578
+ const [prefix, priorCoordinator, tail] = await Promise.all([
579
+ throughIndex === snapshot.index
580
+ ? Promise.resolve(snapshot)
581
+ : session.state(agent.reducer, { through: throughIndex }),
582
+ session.state(aiCoordinatorReducer(agent.contract), {
583
+ through: throughIndex,
584
+ }),
585
+ throughIndex >= snapshot.index
586
+ ? Promise.resolve([])
587
+ : session.history({ gte: throughIndex + 1, lte: snapshot.index }),
588
+ ])
589
+ for (const item of priorCoordinator.state.queued)
590
+ retained.add(item.messageId)
591
+ const events =
592
+ options.excluded === undefined
593
+ ? tail
594
+ : withoutGenerationLifecycle(tail, options.excluded)
595
+ const context = foldAIEvents({
596
+ agent,
597
+ events: [...events, ...appended],
598
+ state: {
599
+ ...prefix.state,
600
+ messages: [
601
+ ...compaction.messages!,
602
+ ...prefix.state.messages.filter((message) =>
603
+ retained.has(message.id),
604
+ ),
605
+ ],
606
+ activeProjection: null,
607
+ },
608
+ })
609
+ const positions = new Map<string, number>()
610
+ for (const message of [...compaction.messages!, ...state.messages]) {
611
+ if (!positions.has(message.id)) positions.set(message.id, positions.size)
591
612
  }
592
- return context.messages
613
+ return visible(
614
+ context.messages.toSorted(
615
+ (left, right) =>
616
+ (positions.get(left.id) ?? positions.size) -
617
+ (positions.get(right.id) ?? positions.size),
618
+ ),
619
+ )
593
620
  }
594
621
  const boundary = state.messages.findIndex(
595
622
  (message) => message.id === compaction.throughMessageId,
596
623
  )
597
- return [
624
+ return visible([
598
625
  ...compaction.messages!,
599
626
  ...state.messages
600
627
  .slice(0, boundary + 1)
601
628
  .filter((message) => retained.has(message.id)),
602
629
  ...state.messages.slice(boundary + 1),
603
- ]
604
- }
605
-
606
- const activeContextMessages = <
607
- M extends UIMessage,
608
- D extends AIEventDefs<M> & EventDefs,
609
- >(options: {
610
- agent: AgentDefinition<M, D>
611
- state: AIState<M>
612
- history: ContractEvent<D>[]
613
- coordinator: AICoordinatorState
614
- }): M[] => {
615
- const queued = new Set(
616
- options.coordinator.queued.map((item) => item.messageId),
617
- )
618
- return contextMessages(options).filter((message) => !queued.has(message.id))
630
+ ])
619
631
  }
620
632
 
621
633
  const modelContext = async <M extends UIMessage>(options: {
@@ -655,51 +667,20 @@ const estimateInputTokens = async (options: {
655
667
  )
656
668
  }
657
669
 
658
- const measuredInputTokens = <D extends EventDefs>(options: {
670
+ const measuredInputTokens = (options: {
659
671
  estimate: number
660
672
  model: string
661
- history: ContractEvent<D>[]
673
+ calibration: AICoordinatorState['calibration']
662
674
  }): number => {
663
- for (const event of options.history.toReversed()) {
664
- if (
665
- event.type === 'ai.compaction.completed' ||
666
- event.type === 'ai.retry.requested'
667
- )
668
- break
669
- if (event.type !== 'ai.generation.completed') continue
670
- const completed = event.payload as GenerationCompletedPayload
671
- const started = options.history.find(
672
- (candidate) =>
673
- candidate.type === 'ai.generation.started' &&
674
- (candidate.payload as GenerationStartedPayload).generationId ===
675
- completed.generationId,
676
- )
677
- const input = completed.usage?.inputTokens
678
- const estimate = completed.inputTokenEstimate
679
- if (
680
- (started?.payload as GenerationStartedPayload | undefined)?.model !==
681
- options.model
682
- )
683
- break
684
- if (
685
- input === undefined ||
686
- estimate === undefined ||
687
- !Number.isFinite(input) ||
688
- input < 0 ||
689
- options.estimate < estimate
690
- )
691
- break
692
- return Math.max(
693
- options.estimate,
694
- Math.ceil(input + options.estimate - estimate),
695
- )
696
- }
697
- return options.estimate
698
- }
699
-
700
- const generationRequestId = (generationId: string): string | undefined => {
701
- const markerIndex = generationId.lastIndexOf(':generation:')
702
- return markerIndex === -1 ? undefined : generationId.slice(0, markerIndex)
675
+ const previous = options.calibration
676
+ return previous !== undefined &&
677
+ previous.model === options.model &&
678
+ options.estimate >= previous.estimate
679
+ ? Math.max(
680
+ options.estimate,
681
+ Math.ceil(previous.inputTokens + options.estimate - previous.estimate),
682
+ )
683
+ : options.estimate
703
684
  }
704
685
 
705
686
  type ToolCallClassification = 'automatic' | 'approval' | 'unknown'
@@ -946,36 +927,6 @@ const lifecycleEvents = <D extends EventDefs, T extends ToolSet>(options: {
946
927
  return { events: result, pending }
947
928
  }
948
929
 
949
- function queuedMessagesAt<D extends EventDefs>(
950
- history: ContractEvent<D>[],
951
- ): AICoordinatorState['queued'] {
952
- const queued: AICoordinatorState['queued'] = []
953
- for (const event of history) {
954
- if (event.type === 'ai.message.created') {
955
- const payload = event.payload as MessageCreatedPayload<UIMessage>
956
- if (payload.message.role !== 'user') continue
957
- const duplicate = queued.findIndex(
958
- (item) => item.messageId === payload.message.id,
959
- )
960
- if (duplicate !== -1) queued.splice(duplicate, 1)
961
- queued.push({
962
- index: event.index,
963
- messageId: payload.message.id,
964
- generate: payload.generate !== false,
965
- })
966
- continue
967
- }
968
- if (event.type !== 'ai.generation.requested') continue
969
- const request = event.payload as GenerationRequestedPayload
970
- if (request.reason !== 'message') continue
971
- const requested = queued.findIndex(
972
- (item) => item.messageId === request.messageId,
973
- )
974
- if (requested !== -1) queued.splice(0, requested + 1)
975
- }
976
- return queued
977
- }
978
-
979
930
  function withoutGenerationLifecycle<D extends EventDefs>(
980
931
  history: ContractEvent<D>[],
981
932
  generationIds: ReadonlySet<string>,
@@ -1062,7 +1013,7 @@ const validateCompaction = <
1062
1013
  }
1063
1014
  if (compaction !== false && 'then' in compaction) {
1064
1015
  void Promise.resolve(compaction).catch(() => {})
1065
- throw new TypeError('compaction options must resolve synchronously')
1016
+ throw new TypeError('compaction options must be a policy or a resolver')
1066
1017
  }
1067
1018
  if (compaction !== false && !('shouldCompact' in compaction)) {
1068
1019
  if (
@@ -1150,89 +1101,10 @@ export function createHandlers<
1150
1101
  const tools = options.tools ?? ({} as T)
1151
1102
  const generation: AgentGenerationSettings<T> = options.generation ?? {}
1152
1103
  const maxSteps = options.maxSteps ?? Number.POSITIVE_INFINITY
1153
- const coordinator = aiCoordinatorReducer(options.agent.contract)
1104
+ const control = createControlRuntime({ agent: options.agent })
1105
+ const coordinator = control.coordinator
1154
1106
  const promptCache = new Map<string, Promise<ModelMessage[]>>()
1155
1107
 
1156
- const coordinatorStateAt = (
1157
- history: ContractEvent<D>[],
1158
- frontier = Number.POSITIVE_INFINITY,
1159
- ): AICoordinatorState => {
1160
- let state = coordinator.initialState
1161
- for (const event of history) {
1162
- if (event.index >= frontier) break
1163
- state = coordinator.fold(state, event)
1164
- }
1165
- return state
1166
- }
1167
-
1168
- const requestForNextMessage = (
1169
- state: AICoordinatorState,
1170
- ): AppendInput<D> | undefined => {
1171
- if (state.closed || state.response !== undefined) return undefined
1172
- const next = state.queued.find((item) => item.generate !== false)
1173
- if (next === undefined) return undefined
1174
- return {
1175
- type: 'ai.generation.requested',
1176
- id: `ai.generate:message:${next.messageId}`,
1177
- payload: { messageId: next.messageId, reason: 'message' },
1178
- } as AppendInput<D>
1179
- }
1180
-
1181
- const scheduleNext = async (
1182
- ctx: Pick<HandlerContext<D>, 'session'>,
1183
- ): Promise<AppendInput<D> | void> =>
1184
- requestForNextMessage(
1185
- (await ctx.session.state(coordinator, { through: 'latest' })).state,
1186
- )
1187
-
1188
- const continueIfReady = async (
1189
- ctx: Pick<HandlerContext<D>, 'session'>,
1190
- generationId: string,
1191
- ): Promise<void> => {
1192
- const state = (await ctx.session.state(coordinator, { through: 'latest' }))
1193
- .state
1194
- const response = state.response
1195
- if (response?.generation?.generationId !== generationId) {
1196
- return
1197
- }
1198
- const input = response.inputResponse
1199
- const inputReady =
1200
- response.completion?.generationId === generationId &&
1201
- response.failure === undefined &&
1202
- input !== undefined &&
1203
- response.inputs.length === 0 &&
1204
- response.calls.every(
1205
- (call) =>
1206
- call.terminal ||
1207
- (call.call.providerExecuted === true &&
1208
- call.call.supportsDeferredResults !== true &&
1209
- call.approval !== undefined &&
1210
- call.response !== undefined),
1211
- )
1212
- if (inputReady) {
1213
- await ctx.session.append('continue-after-input', {
1214
- type: 'ai.generation.requested',
1215
- id: `ai.generate:input:${encodeURIComponent(response.responseMessageId)}:${encodeURIComponent(input.generationId)}:${encodeURIComponent(input.inputId)}`,
1216
- payload: {
1217
- messageId: response.responseMessageId,
1218
- responseMessageId: response.responseMessageId,
1219
- reason: 'input',
1220
- },
1221
- } as AppendInput<D>)
1222
- return
1223
- }
1224
- if (!continuationReady(state)) return
1225
- await ctx.session.append('continue-after-tools', {
1226
- type: 'ai.generation.requested',
1227
- id: `ai.generate:tools:${generationId}`,
1228
- payload: {
1229
- messageId: response.responseMessageId,
1230
- responseMessageId: response.responseMessageId,
1231
- reason: 'tool',
1232
- },
1233
- } as AppendInput<D>)
1234
- }
1235
-
1236
1108
  type RuntimeTool = {
1237
1109
  execute?: (
1238
1110
  input: unknown,
@@ -1244,9 +1116,7 @@ export function createHandlers<
1244
1116
  ) => unknown
1245
1117
  }
1246
1118
 
1247
- type ToolHandlerContext =
1248
- | HandlerContext<D, 'ai.tool.called'>
1249
- | HandlerContext<D, 'ai.approval.responded'>
1119
+ type ToolHandlerContext = HandlerContext<D, 'ai.tool.execution.requested'>
1250
1120
 
1251
1121
  const resultEvent = (
1252
1122
  call: ToolCalledPayload,
@@ -1285,43 +1155,44 @@ export function createHandlers<
1285
1155
  },
1286
1156
  }) as AppendInput<D>
1287
1157
 
1288
- const promptMessages = (
1289
- sessionId: string,
1290
- call: ToolCalledPayload,
1291
- readHistory: () => Promise<ContractEvent<D>[]>,
1292
- ): Promise<ModelMessage[]> => {
1293
- const key = promptCacheKey(sessionId, call.generationId)
1158
+ const promptMessages = (input: {
1159
+ ctx: ToolHandlerContext
1160
+ call: ToolCalledPayload
1161
+ generation: GenerationStartedPayload
1162
+ }): Promise<ModelMessage[]> => {
1163
+ const { ctx, call, generation: owner } = input
1164
+ const key = promptCacheKey(ctx.event.sessionId, call.generationId)
1294
1165
  const cached = promptCache.get(key)
1295
1166
  if (cached) return cached
1296
1167
  const computation = (async () => {
1297
- const history = await readHistory()
1298
- const started = history.find(
1299
- (event) =>
1300
- event.type === 'ai.generation.started' &&
1301
- (event.payload as GenerationStartedPayload).generationId ===
1302
- call.generationId,
1303
- )
1304
- const startedPayload = started?.payload as
1305
- GenerationStartedPayload | undefined
1306
- const frontier =
1307
- startedPayload?.promptThroughIndex ??
1308
- (started?.index ?? Number.POSITIVE_INFINITY) - 1
1309
- const promptHistory = history.filter(
1168
+ const frontier = owner.promptThroughIndex
1169
+ if (frontier === undefined)
1170
+ throw new TypeError('AI work requires a prompt checkpoint')
1171
+ const [snapshot, atPrompt, tail] = await Promise.all([
1172
+ ctx.session.state(options.agent.reducer, { through: frontier }),
1173
+ ctx.session.state(coordinator, { through: frontier }),
1174
+ ctx.session.history({ gte: frontier + 1, lte: ctx.event.index }),
1175
+ ])
1176
+ const appended = tail.filter(
1310
1177
  (event) =>
1311
- event.index <= frontier ||
1312
- ((event.type === 'ai.generation.started' ||
1178
+ (event.type === 'ai.generation.started' ||
1313
1179
  event.type === 'ai.compaction.requested' ||
1314
1180
  event.type === 'ai.compaction.completed') &&
1315
- (event.payload as { generationId: string }).generationId ===
1316
- call.generationId),
1181
+ (event.payload as { generationId: string }).generationId ===
1182
+ call.generationId,
1317
1183
  )
1318
- const state = replay(options.agent, promptHistory)
1184
+ const state = foldAIEvents({
1185
+ agent: options.agent,
1186
+ events: appended,
1187
+ state: snapshot.state,
1188
+ })
1319
1189
  return modelContext({
1320
- messages: activeContextMessages({
1190
+ messages: await contextMessages({
1321
1191
  agent: options.agent,
1322
- state,
1323
- history: promptHistory,
1324
- coordinator: coordinatorStateAt(promptHistory),
1192
+ session: ctx.session,
1193
+ snapshot,
1194
+ coordinator: atPrompt.state,
1195
+ appended,
1325
1196
  }),
1326
1197
  state,
1327
1198
  tools,
@@ -1342,7 +1213,7 @@ export function createHandlers<
1342
1213
  const schedulerFailure = consumeSchedulerSendFailure(error)
1343
1214
  if (checkAbort(ctx.signal)) return
1344
1215
  if (schedulerFailure === 'retryable') throw error
1345
- return resultEvent(call, 'execution:error', {
1216
+ return resultEvent(call, `${ctx.event.id}:execution:error`, {
1346
1217
  error: errorMessage(error),
1347
1218
  })
1348
1219
  }
@@ -1370,7 +1241,7 @@ export function createHandlers<
1370
1241
  return toolExecutionFailure(ctx, call, error)
1371
1242
  }
1372
1243
  if (iterator === undefined) {
1373
- return resultEvent(call, 'execution:0', { output })
1244
+ return resultEvent(call, `${ctx.event.id}:execution:0`, { output })
1374
1245
  }
1375
1246
  let last: unknown
1376
1247
  let sequence = 0
@@ -1383,23 +1254,27 @@ export function createHandlers<
1383
1254
  } catch (error) {
1384
1255
  return toolExecutionFailure(ctx, call, error)
1385
1256
  }
1386
- if (checkAbort(ctx.signal)) return
1387
1257
  if (result.done) {
1258
+ if (ctx.signal.reason instanceof A2Error) throw ctx.signal.reason
1388
1259
  done = true
1389
1260
  break
1390
1261
  }
1262
+ if (checkAbort(ctx.signal)) return
1391
1263
  last = result.value
1392
- await ctx.session.append(
1393
- `tool:${call.toolCallId}:preliminary:${sequence}`,
1394
- resultEvent(
1395
- call,
1396
- `execution:${ctx.attempt}:${sequence}:preliminary`,
1397
- {
1398
- output: result.value,
1399
- preliminary: true,
1400
- },
1401
- ),
1402
- )
1264
+ await control.append({
1265
+ ctx,
1266
+ name: `tool:${call.toolCallId}:preliminary:${sequence}`,
1267
+ events: [
1268
+ resultEvent(
1269
+ call,
1270
+ `${ctx.event.id}:execution:${ctx.attempt}:${sequence}:preliminary`,
1271
+ {
1272
+ output: result.value,
1273
+ preliminary: true,
1274
+ },
1275
+ ),
1276
+ ],
1277
+ })
1403
1278
  sequence += 1
1404
1279
  }
1405
1280
  } finally {
@@ -1413,27 +1288,25 @@ export function createHandlers<
1413
1288
  }
1414
1289
  return resultEvent(
1415
1290
  call,
1416
- `execution:${sequence}:final`,
1291
+ `${ctx.event.id}:execution:${sequence}:final`,
1417
1292
  sequence === 0 ? {} : { output: last },
1418
1293
  )
1419
1294
  }
1420
1295
 
1421
- const executeTool = async (
1422
- ctx: ToolHandlerContext,
1423
- call: ToolCalledPayload,
1424
- ): Promise<AppendInput<D> | void> => {
1296
+ const executeTool = async (input: {
1297
+ ctx: ToolHandlerContext
1298
+ call: ToolCalledPayload
1299
+ generation: GenerationStartedPayload
1300
+ }): Promise<AppendInput<D> | void> => {
1301
+ const { ctx, call } = input
1425
1302
  const tool = tools[call.toolName] as RuntimeTool | undefined
1426
1303
  const execute = tool?.execute
1427
1304
  if (execute === undefined) {
1428
- return resultEvent(call, 'execution:error', {
1305
+ return resultEvent(call, `${ctx.event.id}:execution:error`, {
1429
1306
  error: `Tool '${call.toolName}' has no server executor`,
1430
1307
  })
1431
1308
  }
1432
- const messages = await promptMessages(
1433
- ctx.event.sessionId,
1434
- call,
1435
- ctx.session.history,
1436
- )
1309
+ const messages = await promptMessages(input)
1437
1310
  if (checkAbort(ctx.signal)) return
1438
1311
  const scope: AmbientToolScope = {
1439
1312
  contract: options.agent.contract,
@@ -1446,100 +1319,77 @@ export function createHandlers<
1446
1319
  )
1447
1320
  }
1448
1321
 
1449
- const handleToolCall = async (
1450
- ctx: HandlerContext<D, 'ai.tool.called'>,
1451
- ): Promise<AppendInput<D> | void> => {
1452
- const state = (await ctx.session.state(coordinator, { through: 'latest' }))
1453
- .state
1454
- const response = state.response
1455
- const current = response?.calls.find(
1456
- (candidate) => candidate.index === ctx.event.index,
1457
- )
1458
- if (
1459
- current === undefined ||
1460
- response?.generation?.generationId !== ctx.event.payload.generationId ||
1461
- response.failure !== undefined ||
1462
- current.terminal ||
1463
- current.approval !== undefined
1464
- ) {
1465
- return
1466
- }
1467
- if (current.call.providerExecuted === true) {
1468
- await continueIfReady(ctx, current.call.generationId)
1469
- return
1470
- }
1471
- return executeTool(ctx, current.call)
1472
- }
1473
-
1474
- const handleApproval = async (
1475
- ctx: HandlerContext<D, 'ai.approval.responded'>,
1476
- ): Promise<AppendInput<D> | void> => {
1477
- const state = (await ctx.session.state(coordinator, { through: 'latest' }))
1478
- .state
1479
- const response = state.response
1480
- const current = response?.calls.find(
1481
- (candidate) =>
1482
- candidate.approval?.approvalId === ctx.event.payload.approvalId &&
1483
- candidate.approval.messageId === ctx.event.payload.messageId &&
1484
- candidate.approval.generationId === ctx.event.payload.generationId &&
1485
- candidate.responseIndex === ctx.event.index,
1486
- )
1487
- if (
1488
- current === undefined ||
1489
- response?.generation?.generationId !== current.call.generationId ||
1490
- response.failure !== undefined ||
1491
- current.terminal
1492
- ) {
1493
- return
1494
- }
1495
- if (current.call.providerExecuted === true) {
1496
- await continueIfReady(ctx, current.call.generationId)
1497
- return
1498
- }
1499
- if (!ctx.event.payload.approved) {
1500
- return resultEvent(current.call, 'execution:denied', { denied: true })
1322
+ const handleToolExecution = async (
1323
+ ctx: ToolHandlerContext,
1324
+ ): Promise<AppendInput<D>> => {
1325
+ try {
1326
+ const state = (
1327
+ await ctx.session.state(control.reducer, { through: 'latest' })
1328
+ ).state
1329
+ const request = ctx.event.payload
1330
+ const call = state.coordinator.response?.calls.find(
1331
+ (candidate) => candidate.work?.id === ctx.event.id,
1332
+ )
1333
+ if (
1334
+ state.active?.turnId !== request.turnId ||
1335
+ state.active.suspended ||
1336
+ state.active.version !== request.version ||
1337
+ state.coordinator.response?.failure !== undefined ||
1338
+ !call ||
1339
+ call.terminal ||
1340
+ call.work?.settled
1341
+ )
1342
+ return control.settled({ ctx, events: undefined })
1343
+ return control.settled({
1344
+ ctx,
1345
+ events: await executeTool({
1346
+ ctx,
1347
+ call: request.call,
1348
+ generation: request.generation,
1349
+ }),
1350
+ })
1351
+ } catch (error) {
1352
+ if (control.cancelled({ error, signal: ctx.signal }))
1353
+ return control.settled({ ctx, events: undefined })
1354
+ throw error
1501
1355
  }
1502
- return executeTool(ctx, current.call)
1503
1356
  }
1504
1357
 
1505
1358
  const generationHandler = async (
1506
1359
  ctx: HandlerContext<D, 'ai.generation.requested'>,
1507
1360
  ): Promise<void | AppendInput<D> | readonly AppendInput<D>[]> => {
1508
- const history = await ctx.session.history()
1361
+ if (checkAbort(ctx.signal)) return
1509
1362
  const requestId = ctx.event.id
1510
1363
  const request = ctx.event.payload
1364
+ const snapshot = await ctx.session.state(options.agent.reducer, {
1365
+ through: 'latest',
1366
+ })
1367
+ if (
1368
+ snapshot.state.active?.phase === 'paused' ||
1369
+ snapshot.state.active?.phase === 'pausing'
1370
+ )
1371
+ return
1511
1372
  const coordinatorState = (
1512
- await ctx.session.state(coordinator, { through: 'latest' })
1373
+ await ctx.session.state(coordinator, { through: snapshot.index })
1513
1374
  ).state
1514
1375
  if (
1515
1376
  coordinatorState.closed ||
1516
1377
  coordinatorState.response?.activeRequestId !== requestId
1517
- ) {
1518
- return
1519
- }
1520
- const terminal = history.some((event) => {
1521
- if (event.type === 'ai.generation.completed') {
1522
- return (
1523
- (event.payload as GenerationCompletedPayload).requestId === requestId
1524
- )
1525
- }
1526
- if (event.type !== 'ai.generation.failed') return false
1527
- const payload = event.payload as GenerationFailedPayload
1528
- return payload.requestId === requestId && payload.superseded !== true
1529
- })
1530
- if (terminal) return
1531
-
1532
- const previousStarts = history.filter(
1533
- (event) =>
1534
- event.type === 'ai.generation.started' &&
1535
- (event.payload as GenerationStartedPayload).requestId === requestId,
1536
1378
  )
1537
- const priorAttempts = previousStarts.map(
1538
- (event) => (event.payload as GenerationStartedPayload).attempt,
1379
+ return
1380
+ const response = coordinatorState.response
1381
+ const current =
1382
+ response.generation?.requestId === requestId
1383
+ ? response.generation
1384
+ : undefined
1385
+ if (
1386
+ current !== undefined &&
1387
+ (response.completion !== undefined ||
1388
+ (response.failure !== undefined &&
1389
+ response.failure.superseded !== true))
1539
1390
  )
1540
- if (priorAttempts.some((priorAttempt) => priorAttempt >= ctx.attempt)) {
1541
1391
  return
1542
- }
1392
+ if (current !== undefined && current.attempt >= ctx.attempt) return
1543
1393
  const attempt = ctx.attempt
1544
1394
  const generationId = `${requestId}:generation:${attempt}`
1545
1395
  const responseMessageId =
@@ -1547,65 +1397,55 @@ export function createHandlers<
1547
1397
  (request.reason === 'message'
1548
1398
  ? `${request.messageId}:assistant`
1549
1399
  : request.messageId)
1550
-
1551
- const responseStepCount = history.filter(
1552
- (event) =>
1553
- event.type === 'ai.generation.completed' &&
1554
- (event.payload as GenerationCompletedPayload).responseMessageId ===
1555
- responseMessageId,
1556
- ).length
1557
-
1558
- const sourceCoordinatorState = coordinatorStateAt(history, ctx.event.index)
1559
- if (request.reason === 'tool') {
1560
- const prefix = 'ai.generate:tools:'
1561
- if (!requestId.startsWith(prefix)) return
1562
- const sourceGenerationId = requestId.slice(prefix.length)
1563
- if (
1564
- !continuationReady(sourceCoordinatorState) ||
1565
- sourceCoordinatorState.response?.generation?.generationId !==
1566
- sourceGenerationId ||
1567
- sourceCoordinatorState.response.responseMessageId !==
1568
- request.messageId ||
1569
- sourceCoordinatorState.response.responseMessageId !==
1570
- request.responseMessageId
1571
- ) {
1572
- return
1573
- }
1574
- }
1575
-
1576
- const previous = previousStarts
1577
- .filter(
1578
- (event) =>
1579
- (event.payload as GenerationStartedPayload).attempt < attempt,
1580
- )
1581
- .toSorted(
1582
- (left, right) =>
1583
- (right.payload as GenerationStartedPayload).attempt -
1584
- (left.payload as GenerationStartedPayload).attempt,
1585
- )[0]
1586
-
1587
- const incompleteId = previous
1588
- ? (previous.payload as GenerationStartedPayload).generationId
1589
- : undefined
1590
- const replacedGenerationIds = new Set<string>()
1591
- if (incompleteId !== undefined) replacedGenerationIds.add(incompleteId)
1400
+ const responseStepCount = response.stepCount
1592
1401
  if (
1593
- request.reason === 'retry' &&
1594
- sourceCoordinatorState.response?.failure?.generationId !== undefined
1595
- ) {
1596
- replacedGenerationIds.add(
1597
- sourceCoordinatorState.response.failure.generationId,
1598
- )
1402
+ request.reason === 'tool' &&
1403
+ (requestId !==
1404
+ `ai.generate:tools:${response.source?.generation.generationId}` ||
1405
+ response.responseMessageId !== request.messageId ||
1406
+ response.responseMessageId !== request.responseMessageId)
1407
+ )
1408
+ return
1409
+ const previous = current
1410
+ const replaced =
1411
+ previous === undefined
1412
+ ? []
1413
+ : [{ generation: previous, frontier: response.promptThroughIndex! }]
1414
+ if (request.reason === 'retry' && response.source?.failed) {
1415
+ replaced.push({
1416
+ generation: response.source.generation,
1417
+ frontier: response.source.promptThroughIndex,
1418
+ })
1599
1419
  }
1600
- const promptHistory = withoutGenerationLifecycle(
1601
- history,
1602
- replacedGenerationIds,
1420
+ const replacedGenerationIds = new Set(
1421
+ replaced.map(({ generation: owner }) => owner.generationId),
1603
1422
  )
1604
- const state = replay(options.agent, promptHistory)
1423
+ let state = snapshot.state
1424
+ if (replaced.length > 0) {
1425
+ const through = Math.min(...replaced.map(({ frontier }) => frontier))
1426
+ const baseline = await ctx.session.state(options.agent.reducer, {
1427
+ through,
1428
+ })
1429
+ const tail = await ctx.session.history({
1430
+ gte: through + 1,
1431
+ lte: snapshot.index,
1432
+ })
1433
+ state = foldAIEvents({
1434
+ agent: options.agent,
1435
+ events: withoutGenerationLifecycle(tail, replacedGenerationIds),
1436
+ state: baseline.state,
1437
+ })
1438
+ }
1605
1439
  const resolverContext: AgentResolverContext<M, D> = {
1606
1440
  event: ctx.event,
1607
1441
  state,
1608
- history,
1442
+ session: {
1443
+ state: (reducer, readOptions) =>
1444
+ ctx.session.state(reducer, {
1445
+ ...readOptions,
1446
+ through: readOptions?.through ?? snapshot.index,
1447
+ }),
1448
+ },
1609
1449
  signal: ctx.signal,
1610
1450
  }
1611
1451
 
@@ -1624,7 +1464,7 @@ export function createHandlers<
1624
1464
  const compaction =
1625
1465
  typeof configuredCompaction === 'function'
1626
1466
  ? validateCompaction({
1627
- compaction: configuredCompaction(resolverContext),
1467
+ compaction: await resolve(configuredCompaction, resolverContext),
1628
1468
  explicit: true,
1629
1469
  generation: options.generation,
1630
1470
  tools: options.tools,
@@ -1633,7 +1473,7 @@ export function createHandlers<
1633
1473
  if (checkAbort(ctx.signal)) return
1634
1474
 
1635
1475
  if (request.reason === 'tool' && responseStepCount >= maxSteps) {
1636
- const source = sourceCoordinatorState.response?.generation
1476
+ const source = response.source?.generation
1637
1477
  if (source === undefined) return
1638
1478
  return {
1639
1479
  type: 'ai.generation.failed',
@@ -1656,7 +1496,7 @@ export function createHandlers<
1656
1496
  responseMessageId,
1657
1497
  attempt,
1658
1498
  model: modelName(resolvedModel),
1659
- promptThroughIndex: history.at(-1)?.index ?? ctx.event.index,
1499
+ promptThroughIndex: snapshot.index,
1660
1500
  }
1661
1501
  const catalogModelId = gatewayModelId(resolvedModel)
1662
1502
  const gatewayOptions = options.generation?.providerOptions?.['gateway']
@@ -1681,7 +1521,7 @@ export function createHandlers<
1681
1521
  } as AppendInput<D>)
1682
1522
  }
1683
1523
  if (previous) {
1684
- const payload = previous.payload as GenerationStartedPayload
1524
+ const payload = previous
1685
1525
  const superseded: GenerationFailedPayload = {
1686
1526
  requestId,
1687
1527
  messageId: payload.messageId,
@@ -1701,33 +1541,24 @@ export function createHandlers<
1701
1541
  id: generationId,
1702
1542
  payload: started,
1703
1543
  } as AppendInput<D>)
1704
- const generationHistory = [
1705
- ...history,
1706
- ...(await ctx.session.append('generation-start', ...startEvents)),
1707
- ]
1544
+ const generationEvents = await control.append({
1545
+ ctx,
1546
+ name: 'generation-start',
1547
+ events: startEvents,
1548
+ })
1708
1549
  generationStarted = true
1709
1550
 
1710
- const startedCoordinatorState = (
1711
- await ctx.session.state(coordinator, { through: 'latest' })
1712
- ).state
1713
- if (
1714
- startedCoordinatorState.response?.activeRequestId !== requestId ||
1715
- startedCoordinatorState.response.generation?.generationId !==
1716
- generationId
1717
- ) {
1718
- return
1719
- }
1551
+ if (checkAbort(ctx.signal)) return
1720
1552
 
1721
- const promptCoordinatorState = {
1722
- ...coordinatorStateAt(history),
1723
- queued: queuedMessagesAt(history),
1724
- }
1725
- const messages = activeContextMessages({
1553
+ const promptCoordinatorState = coordinatorState
1554
+ const messages = await contextMessages({
1726
1555
  agent: options.agent,
1727
- state,
1728
- history: promptHistory,
1556
+ session: ctx.session,
1557
+ snapshot: { state, index: snapshot.index },
1729
1558
  coordinator: promptCoordinatorState,
1559
+ excluded: replacedGenerationIds,
1730
1560
  })
1561
+ let generationMessages = messages
1731
1562
  let modelMessages = await modelContext({ messages, state, tools })
1732
1563
  const baseContext: AgentGenerateContext<M, D, T> = {
1733
1564
  request: ctx.event,
@@ -1737,7 +1568,7 @@ export function createHandlers<
1737
1568
  messages,
1738
1569
  modelMessages,
1739
1570
  state,
1740
- history,
1571
+ session: resolverContext.session,
1741
1572
  signal: ctx.signal,
1742
1573
  model: resolvedModel,
1743
1574
  tools,
@@ -1761,9 +1592,7 @@ export function createHandlers<
1761
1592
  : undefined
1762
1593
  let inputTokenEstimate: number | undefined
1763
1594
  let compacted = false
1764
- const canCompact =
1765
- sourceCoordinatorState.response?.calls.every((call) => call.terminal) ??
1766
- true
1595
+ const canCompact = response.source?.canCompact ?? true
1767
1596
  if (policy && canCompact) {
1768
1597
  const compactionContext = {
1769
1598
  ...resolverContext,
@@ -1782,7 +1611,7 @@ export function createHandlers<
1782
1611
  const inputTokens = measuredInputTokens({
1783
1612
  estimate: inputTokenEstimate,
1784
1613
  model: modelName(resolvedModel),
1785
- history,
1614
+ calibration: coordinatorState.calibration,
1786
1615
  })
1787
1616
  const threshold =
1788
1617
  policy.thresholdTokens ??
@@ -1816,12 +1645,18 @@ export function createHandlers<
1816
1645
  (queued) => queued.messageId === message.id,
1817
1646
  ),
1818
1647
  )?.id ?? request.messageId
1819
- const throughIndex = history.at(-1)?.index ?? ctx.event.index
1820
- generationHistory.push(
1821
- ...(await ctx.session.append('compaction-requested', {
1822
- type: 'ai.compaction.requested',
1823
- id: `${generationId}:compaction:requested`,
1824
- payload: { generationId, throughMessageId, throughIndex },
1648
+ const throughIndex = snapshot.index
1649
+ generationEvents.push(
1650
+ ...(await control.append({
1651
+ ctx,
1652
+ name: 'compaction-requested',
1653
+ events: [
1654
+ {
1655
+ type: 'ai.compaction.requested',
1656
+ id: `${generationId}:compaction:requested`,
1657
+ payload: { generationId, throughMessageId, throughIndex },
1658
+ } as AppendInput<D>,
1659
+ ],
1825
1660
  })),
1826
1661
  )
1827
1662
  const result = !('shouldCompact' in policy)
@@ -1847,30 +1682,55 @@ export function createHandlers<
1847
1682
  ),
1848
1683
  }),
1849
1684
  }
1850
- generationHistory.push(
1851
- ...(await ctx.session.append('compaction-completed', {
1852
- type: 'ai.compaction.completed',
1853
- id: `${generationId}:compaction:completed`,
1854
- payload: completed,
1685
+ generationEvents.push(
1686
+ ...(await control.append({
1687
+ ctx,
1688
+ name: 'compaction-completed',
1689
+ events: [
1690
+ {
1691
+ type: 'ai.compaction.completed',
1692
+ id: `${generationId}:compaction:completed`,
1693
+ payload: completed,
1694
+ } as AppendInput<D>,
1695
+ ],
1855
1696
  })),
1856
1697
  )
1698
+ const queued = new Set(
1699
+ promptCoordinatorState.queued.map((item) => item.messageId),
1700
+ )
1701
+ generationMessages = result.messages.filter(
1702
+ (message) => !queued.has(message.id),
1703
+ )
1857
1704
  compacted = true
1858
1705
  }
1859
1706
  }
1860
1707
 
1861
- const currentState = replay(options.agent, generationHistory)
1862
- const currentCoordinatorState = coordinatorStateAt(generationHistory)
1863
- const generationMessages = activeContextMessages({
1708
+ const currentState = foldAIEvents({
1864
1709
  agent: options.agent,
1865
- state: currentState,
1866
- history: generationHistory,
1867
- coordinator: currentCoordinatorState,
1868
- })
1869
- modelMessages = await modelContext({
1870
- messages: generationMessages,
1871
- state: currentState,
1872
- tools,
1710
+ events: generationEvents,
1711
+ state: snapshot.state,
1873
1712
  })
1713
+ if (request.reason === 'retry' || previous !== undefined) {
1714
+ generationMessages = await contextMessages({
1715
+ agent: options.agent,
1716
+ session: ctx.session,
1717
+ snapshot,
1718
+ coordinator: promptCoordinatorState,
1719
+ appended: generationEvents,
1720
+ })
1721
+ }
1722
+ if (
1723
+ (policy && 'shouldCompact' in policy) ||
1724
+ generationMessages !== messages ||
1725
+ activeCompaction(currentState)?.summary !==
1726
+ activeCompaction(state)?.summary
1727
+ ) {
1728
+ modelMessages = await modelContext({
1729
+ messages: generationMessages,
1730
+ state: currentState,
1731
+ tools,
1732
+ })
1733
+ }
1874
1734
  promptCache.set(
1875
1735
  promptCacheKey(ctx.event.sessionId, generationId),
1876
1736
  Promise.resolve(modelMessages),
@@ -1893,7 +1753,7 @@ export function createHandlers<
1893
1753
  messages: generationMessages,
1894
1754
  modelMessages,
1895
1755
  state: currentState,
1896
- history,
1756
+ session: resolverContext.session,
1897
1757
  signal: ctx.signal,
1898
1758
  model: resolvedModel,
1899
1759
  tools,
@@ -1942,15 +1802,18 @@ export function createHandlers<
1942
1802
  custom,
1943
1803
  })
1944
1804
  pendingToolCalls = lifecycle.pending
1945
- await ctx.session.append(
1946
- `generation-progress:${sequence}`,
1947
- {
1948
- type: 'ai.generation.progress',
1949
- id: `${generationId}:progress:${sequence}`,
1950
- payload: progress,
1951
- },
1952
- ...lifecycle.events,
1953
- )
1805
+ await control.append({
1806
+ ctx,
1807
+ name: `generation-progress:${sequence}`,
1808
+ events: [
1809
+ {
1810
+ type: 'ai.generation.progress',
1811
+ id: `${generationId}:progress:${sequence}`,
1812
+ payload: progress,
1813
+ } as AppendInput<D>,
1814
+ ...lifecycle.events,
1815
+ ],
1816
+ })
1954
1817
  sequence += 1
1955
1818
  }
1956
1819
 
@@ -2004,22 +1867,12 @@ export function createHandlers<
2004
1867
  } as AppendInput<D>,
2005
1868
  ]
2006
1869
  } catch (error) {
1870
+ if (error instanceof ControlCancelled) throw error
2007
1871
  if (error instanceof A2Error) {
2008
1872
  checkAbort(ctx.signal)
2009
1873
  throw error
2010
1874
  }
2011
- if (checkAbort(ctx.signal)) {
2012
- const interrupted: MessageInterruptedPayload = {
2013
- messageId: responseMessageId,
2014
- generationId,
2015
- reason: 'aborted',
2016
- }
2017
- return {
2018
- type: 'ai.message.interrupted',
2019
- id: `${generationId}:interrupted`,
2020
- payload: interrupted,
2021
- } as AppendInput<D>
2022
- }
1875
+ if (checkAbort(ctx.signal)) return
2023
1876
  if (!generationStarted) throw error
2024
1877
  const failed: GenerationFailedPayload = {
2025
1878
  requestId,
@@ -2036,59 +1889,6 @@ export function createHandlers<
2036
1889
  }
2037
1890
  }
2038
1891
 
2039
- const handleMessageCreated = async (
2040
- ctx: HandlerContext<D, 'ai.message.created'>,
2041
- ): Promise<AppendInput<D> | void> => {
2042
- if (
2043
- ctx.event.payload.message.role !== 'user' ||
2044
- ctx.event.payload.generate === false
2045
- ) {
2046
- return
2047
- }
2048
- return scheduleNext(ctx)
2049
- }
2050
-
2051
- const handleRetry = async (
2052
- ctx: HandlerContext<D, 'ai.retry.requested'>,
2053
- ): Promise<AppendInput<D> | void> => {
2054
- const state = (await ctx.session.state(coordinator, { through: 'latest' }))
2055
- .state
2056
- const response = state.response
2057
- if (
2058
- response?.status !== 'failed' ||
2059
- response.rootMessageId !== ctx.event.payload.messageId ||
2060
- response.responseMessageId !== ctx.event.payload.responseMessageId
2061
- ) {
2062
- return
2063
- }
2064
- return {
2065
- type: 'ai.generation.requested',
2066
- id: `ai.generate:retry:${ctx.event.payload.retryId}`,
2067
- payload: {
2068
- messageId: response.rootMessageId,
2069
- responseMessageId: response.responseMessageId,
2070
- reason: 'retry',
2071
- },
2072
- } as AppendInput<D>
2073
- }
2074
-
2075
- const handleInputResponse = async (
2076
- ctx: HandlerContext<D, 'ai.input.responded'>,
2077
- ): Promise<void> => {
2078
- const state = (await ctx.session.state(coordinator, { through: 'latest' }))
2079
- .state
2080
- const response = state.response
2081
- if (
2082
- response?.responseMessageId !== ctx.event.payload.messageId ||
2083
- response.inputResponse?.index !== ctx.event.index ||
2084
- response.inputResponse.generationId !== ctx.event.payload.generationId ||
2085
- response.inputResponse.inputId !== ctx.event.payload.inputId
2086
- ) {
2087
- return
2088
- }
2089
- await continueIfReady(ctx, ctx.event.payload.generationId)
2090
- }
2091
-
2092
1892
  const clearPromptCache = (sessionId: string): void => {
2093
1893
  const prefix = `${sessionId}\u001f`
2094
1894
  for (const key of promptCache.keys()) {
@@ -2096,13 +1896,6 @@ export function createHandlers<
2096
1896
  }
2097
1897
  }
2098
1898
 
2099
- const handleResponseEnded = async (
2100
- ctx: HandlerContext<D, 'ai.message.completed' | 'ai.message.interrupted'>,
2101
- ): Promise<AppendInput<D> | void> => {
2102
- clearPromptCache(ctx.event.sessionId)
2103
- return scheduleNext(ctx)
2104
- }
2105
-
2106
1899
  return {
2107
1900
  'ai.model.metadata.requested': {
2108
1901
  handler: async (ctx) => {
@@ -2118,24 +1911,23 @@ export function createHandlers<
2118
1911
  } as AppendInput<D>
2119
1912
  },
2120
1913
  },
2121
- 'ai.message.created': { lane: 'a2.ai.turn', handler: handleMessageCreated },
2122
- 'ai.retry.requested': { lane: 'a2.ai.turn', handler: handleRetry },
2123
- 'ai.input.responded': {
2124
- lane: 'a2.ai.turn',
2125
- handler: handleInputResponse,
2126
- },
1914
+ 'ai.control.requested': { lane: 'a2.ai.control', handler: control.handler },
1915
+ 'ai.work.reported': { lane: 'a2.ai.control', handler: control.handler },
2127
1916
  'ai.message.completed': {
2128
- lane: 'a2.ai.turn',
2129
- handler: handleResponseEnded,
1917
+ handler: async (ctx) => {
1918
+ clearPromptCache(ctx.event.sessionId)
1919
+ },
2130
1920
  },
2131
1921
  'ai.message.interrupted': {
2132
- lane: 'a2.ai.turn',
2133
- handler: handleResponseEnded,
1922
+ handler: async (ctx) => {
1923
+ clearPromptCache(ctx.event.sessionId)
1924
+ },
2134
1925
  },
2135
1926
  'ai.session.closed': {
2136
- handler: (ctx) => {
1927
+ lane: 'a2.ai.control',
1928
+ handler: async (ctx) => {
2137
1929
  clearPromptCache(ctx.event.sessionId)
2138
- return Promise.resolve()
1930
+ return control.handler(ctx)
2139
1931
  },
2140
1932
  },
2141
1933
  'ai.generation.failed': {
@@ -2147,60 +1939,41 @@ export function createHandlers<
2147
1939
  },
2148
1940
  },
2149
1941
  'ai.generation.requested': {
2150
- lane: 'a2.ai.turn',
1942
+ lane: 'a2.ai.model',
2151
1943
  abortOn: {
2152
- 'ai.message.interrupted': (event, trigger, context) => {
2153
- const responseMessageId =
2154
- trigger.payload.responseMessageId ??
2155
- (trigger.payload.reason === 'message'
2156
- ? `${trigger.payload.messageId}:assistant`
2157
- : trigger.payload.messageId)
1944
+ 'ai.control.committed': (event, trigger) => {
1945
+ const active = (event.payload as ControlCommit).view
2158
1946
  return (
2159
- event.payload.messageId === responseMessageId &&
2160
- (event.payload.requestId === trigger.id ||
2161
- event.payload.generationId ===
2162
- `${trigger.id}:generation:${context.attempt}`)
1947
+ active?.turnId !== trigger.payload.control?.turnId ||
1948
+ active?.version !== trigger.payload.control?.version
2163
1949
  )
2164
1950
  },
2165
1951
  'ai.session.closed': true,
2166
1952
  },
2167
- handler: generationHandler,
2168
- },
2169
- 'ai.generation.completed': {
2170
- handler: async (ctx) =>
2171
- continueIfReady(ctx, ctx.event.payload.generationId),
2172
- },
2173
- 'ai.tool.called': {
2174
- abortOn: {
2175
- 'ai.generation.failed': (event, trigger) =>
2176
- event.payload.generationId === trigger.payload.generationId,
2177
- 'ai.message.interrupted': (event, trigger) =>
2178
- event.payload.messageId === trigger.payload.messageId &&
2179
- (event.payload.requestId === trigger.payload.requestId ||
2180
- event.payload.generationId === trigger.payload.generationId),
2181
- 'ai.session.closed': true,
1953
+ handler: async (ctx) => {
1954
+ try {
1955
+ return control.settled({ ctx, events: await generationHandler(ctx) })
1956
+ } catch (error) {
1957
+ if (control.cancelled({ error, signal: ctx.signal }))
1958
+ return control.settled({ ctx, events: undefined })
1959
+ throw error
1960
+ }
2182
1961
  },
2183
- handler: handleToolCall,
2184
1962
  },
2185
- 'ai.approval.responded': {
1963
+ 'ai.tool.execution.requested': {
2186
1964
  abortOn: {
2187
1965
  'ai.generation.failed': (event, trigger) =>
2188
- event.payload.responseMessageId === trigger.payload.messageId &&
2189
- event.payload.generationId === trigger.payload.generationId,
2190
- 'ai.message.interrupted': (event, trigger) =>
2191
- event.payload.messageId === trigger.payload.messageId &&
2192
- (event.payload.requestId ===
2193
- generationRequestId(trigger.payload.generationId) ||
2194
- event.payload.generationId === trigger.payload.generationId),
1966
+ event.payload.generationId === trigger.payload.call.generationId,
1967
+ 'ai.control.committed': (event, trigger) => {
1968
+ const active = (event.payload as ControlCommit).view
1969
+ return (
1970
+ active?.turnId !== trigger.payload.turnId ||
1971
+ active?.version !== trigger.payload.version
1972
+ )
1973
+ },
2195
1974
  'ai.session.closed': true,
2196
1975
  },
2197
- handler: handleApproval,
2198
- },
2199
- 'ai.tool.result': {
2200
- handler: async (ctx) => {
2201
- if (ctx.event.payload.preliminary === true) return
2202
- await continueIfReady(ctx, ctx.event.payload.generationId)
2203
- },
1976
+ handler: handleToolExecution,
2204
1977
  },
2205
1978
  } as NonNullable<ServerOptions<D>['handlers']>
2206
1979
  }