experimental-a2 0.13.0 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/CHANGELOG.md +43 -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 +1053 -513
  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 +287 -65
  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 +429 -619
  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,7 +8,10 @@
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'
14
+ import { createHash } from 'node:crypto'
12
15
  import { asSchema, convertToModelMessages } from 'ai'
13
16
  import type {
14
17
  FinishReason,
@@ -34,17 +37,13 @@ import type {
34
37
  GenerationCompletedPayload,
35
38
  GenerationFailedPayload,
36
39
  GenerationProgressPayload,
37
- GenerationRequestedPayload,
38
40
  GenerationStartedPayload,
39
41
  MessageCompletedPayload,
40
- MessageCreatedPayload,
41
- MessageInterruptedPayload,
42
42
  ToolCalledPayload,
43
43
  ToolResultPayload,
44
44
  } from './ai.ts'
45
45
  import {
46
46
  aiCoordinatorReducer,
47
- continuationReady,
48
47
  type AICoordinatorState,
49
48
  } from './ai-coordinator.ts'
50
49
  import type { AppendInput, ContractEvent, EventDefs } from './contract.ts'
@@ -67,24 +66,23 @@ import {
67
66
 
68
67
  function validateAgentIngress(context: ServerIngressContext): void {
69
68
  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'
69
+ if (event.type !== 'ai.control.requested') return true
70
+ const command = event.payload as Record<string, unknown>
71
+ if (
72
+ command['action'] === 'request-input' ||
73
+ command['action'] === 'tool-result'
74
+ )
75
+ return true
76
+ if (
77
+ command['action'] === 'send' ||
78
+ command['action'] === 'edit' ||
79
+ command['action'] === 'steer'
80
+ ) {
81
+ return (
82
+ (command['message'] as { role?: unknown } | undefined)?.role !== 'user'
76
83
  )
77
84
  }
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
- )
85
+ return false
88
86
  })
89
87
  if (rejected !== undefined) {
90
88
  throw new A2Error(
@@ -119,7 +117,7 @@ export type AgentGenerateContext<
119
117
  messages: M[]
120
118
  modelMessages: ModelMessage[]
121
119
  state: AIState<M>
122
- history: ContractEvent<D>[]
120
+ session: Pick<HandlerContext<D>['session'], 'state'>
123
121
  signal: AbortSignal
124
122
  model: LanguageModel
125
123
  tools: T
@@ -152,7 +150,7 @@ export type AgentResolverContext<
152
150
  > = {
153
151
  event: ContractEvent<D, 'ai.generation.requested'>
154
152
  state: AIState<M>
155
- history: ContractEvent<D>[]
153
+ session: Pick<HandlerContext<D>['session'], 'state'>
156
154
  signal: AbortSignal
157
155
  }
158
156
 
@@ -214,9 +212,7 @@ export type CreateHandlersOptions<
214
212
  * The custom stream owns its metadata chunks.
215
213
  */
216
214
  generate?: AgentGenerate<M, D, T>
217
- compaction?:
218
- | CompactionOptions<M, D>
219
- | ((context: AgentResolverContext<M, D>) => CompactionOptions<M, D>)
215
+ compaction?: Resolvable<CompactionOptions<M, D>, AgentResolverContext<M, D>>
220
216
  /** Progress is durably flushed at either limit, whichever is reached first. */
221
217
  progress?: { maxChunks?: number; maxDelayMs?: number }
222
218
  }
@@ -341,10 +337,38 @@ async function generateWithAISDK<
341
337
  M extends UIMessage,
342
338
  D extends AIEventDefs<M> & EventDefs,
343
339
  T extends ToolSet,
344
- >(
345
- context: AgentGenerateContext<M, D, T>,
346
- messageMetadata: AgentMessageMetadata<M, D, T> | undefined,
347
- ): Promise<GenerationSource> {
340
+ >(options: {
341
+ agentName: string
342
+ context: AgentGenerateContext<M, D, T>
343
+ messageMetadata: AgentMessageMetadata<M, D, T> | undefined
344
+ }): Promise<GenerationSource> {
345
+ const { agentName, context, messageMetadata } = options
346
+ const headers = context.generation.headers ?? {}
347
+ const generation =
348
+ gatewayModelId(context.model) === undefined
349
+ ? context.generation
350
+ : {
351
+ ...context.generation,
352
+ headers: Object.keys(headers).some(
353
+ (key) => key.toLowerCase() === 'x-session-affinity',
354
+ )
355
+ ? headers
356
+ : {
357
+ ...headers,
358
+ 'x-session-affinity': createHash('sha256')
359
+ .update(
360
+ JSON.stringify([agentName, context.request.sessionId]),
361
+ )
362
+ .digest('hex'),
363
+ },
364
+ providerOptions: {
365
+ ...context.generation.providerOptions,
366
+ gateway: {
367
+ caching: 'auto',
368
+ ...context.generation.providerOptions?.['gateway'],
369
+ },
370
+ },
371
+ }
348
372
  const step = await generateAISDKStep<M, T>({
349
373
  model: context.model,
350
374
  tools: context.tools,
@@ -355,11 +379,12 @@ async function generateWithAISDK<
355
379
  ...(context.instructions === undefined
356
380
  ? {}
357
381
  : { instructions: context.instructions }),
358
- settings: context.generation,
382
+ settings: generation,
359
383
  ...(messageMetadata === undefined
360
384
  ? {}
361
385
  : {
362
- messageMetadata: ({ part }) => messageMetadata({ ...context, part }),
386
+ messageMetadata: ({ part }) =>
387
+ messageMetadata({ ...context, generation, part }),
363
388
  }),
364
389
  })
365
390
  return {
@@ -443,12 +468,17 @@ async function* consumeGeneration(options: {
443
468
  }
444
469
  }
445
470
 
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)
471
+ const foldAIEvents = <
472
+ M extends UIMessage,
473
+ D extends AIEventDefs<M> & EventDefs,
474
+ >(options: {
475
+ agent: AgentDefinition<M, D>
476
+ state: AIState<M>
477
+ events: ContractEvent<D>[]
478
+ }): AIState<M> => {
479
+ let state = options.state
480
+ for (const event of options.events)
481
+ state = options.agent.reducer.fold(state, event)
452
482
  return state
453
483
  }
454
484
 
@@ -457,6 +487,7 @@ const summarize = async <
457
487
  D extends AIEventDefs<M> & EventDefs,
458
488
  T extends ToolSet,
459
489
  >(options: {
490
+ agentName: string
460
491
  context: AgentGenerateContext<M, D, T>
461
492
  instructions: string | undefined
462
493
  }): Promise<{ summary: string; usage?: LanguageModelUsage }> => {
@@ -498,8 +529,9 @@ const summarize = async <
498
529
  return [name, definition]
499
530
  }),
500
531
  ) as T
501
- const source = await generateWithAISDK(
502
- {
532
+ const source = await generateWithAISDK({
533
+ agentName: options.agentName,
534
+ context: {
503
535
  ...options.context,
504
536
  generation,
505
537
  tools,
@@ -509,8 +541,8 @@ const summarize = async <
509
541
  ],
510
542
  responseMessageId: `${options.context.generationId}:summary`,
511
543
  },
512
- undefined,
513
- )
544
+ messageMetadata: undefined,
545
+ })
514
546
  let summary = ''
515
547
  let calledTool = false
516
548
  let finish: AgentGenerationFinish | undefined
@@ -551,71 +583,83 @@ const activeCompaction = <M extends UIMessage>(
551
583
  : null
552
584
  }
553
585
 
554
- const contextMessages = <
586
+ const contextMessages = async <
555
587
  M extends UIMessage,
556
588
  D extends AIEventDefs<M> & EventDefs,
557
589
  >(options: {
558
590
  agent: AgentDefinition<M, D>
559
- state: AIState<M>
560
- history: ContractEvent<D>[]
561
- }): M[] => {
562
- const { agent, state, history } = options
591
+ session: HandlerContext<D>['session']
592
+ snapshot: { state: AIState<M>; index: number }
593
+ coordinator: AICoordinatorState
594
+ appended?: ContractEvent<D>[]
595
+ excluded?: ReadonlySet<string>
596
+ }): Promise<M[]> => {
597
+ const { agent, session, snapshot } = options
598
+ const appended = options.appended ?? []
599
+ const state = foldAIEvents({ agent, events: appended, state: snapshot.state })
563
600
  const compaction = activeCompaction(state)
564
- if (compaction === null) return state.messages
601
+ const queued = new Set(
602
+ options.coordinator.queued.map((item) => item.messageId),
603
+ )
604
+ const visible = (messages: M[]): M[] =>
605
+ messages.filter((message) => !queued.has(message.id))
606
+ if (compaction === null) return visible(state.messages)
565
607
  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)
608
+ const throughIndex = compaction.throughIndex
575
609
  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)
610
+ const [prefix, priorCoordinator, tail] = await Promise.all([
611
+ throughIndex === snapshot.index
612
+ ? Promise.resolve(snapshot)
613
+ : session.state(agent.reducer, { through: throughIndex }),
614
+ session.state(aiCoordinatorReducer(agent.contract), {
615
+ through: throughIndex,
616
+ }),
617
+ throughIndex >= snapshot.index
618
+ ? Promise.resolve([])
619
+ : session.history({ gte: throughIndex + 1, lte: snapshot.index }),
620
+ ])
621
+ for (const item of priorCoordinator.state.queued)
622
+ retained.add(item.messageId)
623
+ const events =
624
+ options.excluded === undefined
625
+ ? tail
626
+ : withoutGenerationLifecycle(tail, options.excluded)
627
+ const context = foldAIEvents({
628
+ agent,
629
+ events: [...events, ...appended],
630
+ state: {
631
+ ...prefix.state,
632
+ messages: [
633
+ ...compaction.messages!,
634
+ ...prefix.state.messages.filter((message) =>
635
+ retained.has(message.id),
636
+ ),
637
+ ],
638
+ activeProjection: null,
639
+ },
640
+ })
641
+ const positions = new Map<string, number>()
642
+ for (const message of [...compaction.messages!, ...state.messages]) {
643
+ if (!positions.has(message.id)) positions.set(message.id, positions.size)
591
644
  }
592
- return context.messages
645
+ return visible(
646
+ context.messages.toSorted(
647
+ (left, right) =>
648
+ (positions.get(left.id) ?? positions.size) -
649
+ (positions.get(right.id) ?? positions.size),
650
+ ),
651
+ )
593
652
  }
594
653
  const boundary = state.messages.findIndex(
595
654
  (message) => message.id === compaction.throughMessageId,
596
655
  )
597
- return [
656
+ return visible([
598
657
  ...compaction.messages!,
599
658
  ...state.messages
600
659
  .slice(0, boundary + 1)
601
660
  .filter((message) => retained.has(message.id)),
602
661
  ...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))
662
+ ])
619
663
  }
620
664
 
621
665
  const modelContext = async <M extends UIMessage>(options: {
@@ -655,51 +699,20 @@ const estimateInputTokens = async (options: {
655
699
  )
656
700
  }
657
701
 
658
- const measuredInputTokens = <D extends EventDefs>(options: {
702
+ const measuredInputTokens = (options: {
659
703
  estimate: number
660
704
  model: string
661
- history: ContractEvent<D>[]
705
+ calibration: AICoordinatorState['calibration']
662
706
  }): 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)
707
+ const previous = options.calibration
708
+ return previous !== undefined &&
709
+ previous.model === options.model &&
710
+ options.estimate >= previous.estimate
711
+ ? Math.max(
712
+ options.estimate,
713
+ Math.ceil(previous.inputTokens + options.estimate - previous.estimate),
714
+ )
715
+ : options.estimate
703
716
  }
704
717
 
705
718
  type ToolCallClassification = 'automatic' | 'approval' | 'unknown'
@@ -946,36 +959,6 @@ const lifecycleEvents = <D extends EventDefs, T extends ToolSet>(options: {
946
959
  return { events: result, pending }
947
960
  }
948
961
 
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
962
  function withoutGenerationLifecycle<D extends EventDefs>(
980
963
  history: ContractEvent<D>[],
981
964
  generationIds: ReadonlySet<string>,
@@ -1062,7 +1045,7 @@ const validateCompaction = <
1062
1045
  }
1063
1046
  if (compaction !== false && 'then' in compaction) {
1064
1047
  void Promise.resolve(compaction).catch(() => {})
1065
- throw new TypeError('compaction options must resolve synchronously')
1048
+ throw new TypeError('compaction options must be a policy or a resolver')
1066
1049
  }
1067
1050
  if (compaction !== false && !('shouldCompact' in compaction)) {
1068
1051
  if (
@@ -1150,89 +1133,10 @@ export function createHandlers<
1150
1133
  const tools = options.tools ?? ({} as T)
1151
1134
  const generation: AgentGenerationSettings<T> = options.generation ?? {}
1152
1135
  const maxSteps = options.maxSteps ?? Number.POSITIVE_INFINITY
1153
- const coordinator = aiCoordinatorReducer(options.agent.contract)
1136
+ const control = createControlRuntime({ agent: options.agent })
1137
+ const coordinator = control.coordinator
1154
1138
  const promptCache = new Map<string, Promise<ModelMessage[]>>()
1155
1139
 
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
1140
  type RuntimeTool = {
1237
1141
  execute?: (
1238
1142
  input: unknown,
@@ -1244,9 +1148,7 @@ export function createHandlers<
1244
1148
  ) => unknown
1245
1149
  }
1246
1150
 
1247
- type ToolHandlerContext =
1248
- | HandlerContext<D, 'ai.tool.called'>
1249
- | HandlerContext<D, 'ai.approval.responded'>
1151
+ type ToolHandlerContext = HandlerContext<D, 'ai.tool.execution.requested'>
1250
1152
 
1251
1153
  const resultEvent = (
1252
1154
  call: ToolCalledPayload,
@@ -1285,43 +1187,44 @@ export function createHandlers<
1285
1187
  },
1286
1188
  }) as AppendInput<D>
1287
1189
 
1288
- const promptMessages = (
1289
- sessionId: string,
1290
- call: ToolCalledPayload,
1291
- readHistory: () => Promise<ContractEvent<D>[]>,
1292
- ): Promise<ModelMessage[]> => {
1293
- const key = promptCacheKey(sessionId, call.generationId)
1190
+ const promptMessages = (input: {
1191
+ ctx: ToolHandlerContext
1192
+ call: ToolCalledPayload
1193
+ generation: GenerationStartedPayload
1194
+ }): Promise<ModelMessage[]> => {
1195
+ const { ctx, call, generation: owner } = input
1196
+ const key = promptCacheKey(ctx.event.sessionId, call.generationId)
1294
1197
  const cached = promptCache.get(key)
1295
1198
  if (cached) return cached
1296
1199
  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(
1200
+ const frontier = owner.promptThroughIndex
1201
+ if (frontier === undefined)
1202
+ throw new TypeError('AI work requires a prompt checkpoint')
1203
+ const [snapshot, atPrompt, tail] = await Promise.all([
1204
+ ctx.session.state(options.agent.reducer, { through: frontier }),
1205
+ ctx.session.state(coordinator, { through: frontier }),
1206
+ ctx.session.history({ gte: frontier + 1, lte: ctx.event.index }),
1207
+ ])
1208
+ const appended = tail.filter(
1310
1209
  (event) =>
1311
- event.index <= frontier ||
1312
- ((event.type === 'ai.generation.started' ||
1210
+ (event.type === 'ai.generation.started' ||
1313
1211
  event.type === 'ai.compaction.requested' ||
1314
1212
  event.type === 'ai.compaction.completed') &&
1315
- (event.payload as { generationId: string }).generationId ===
1316
- call.generationId),
1213
+ (event.payload as { generationId: string }).generationId ===
1214
+ call.generationId,
1317
1215
  )
1318
- const state = replay(options.agent, promptHistory)
1216
+ const state = foldAIEvents({
1217
+ agent: options.agent,
1218
+ events: appended,
1219
+ state: snapshot.state,
1220
+ })
1319
1221
  return modelContext({
1320
- messages: activeContextMessages({
1222
+ messages: await contextMessages({
1321
1223
  agent: options.agent,
1322
- state,
1323
- history: promptHistory,
1324
- coordinator: coordinatorStateAt(promptHistory),
1224
+ session: ctx.session,
1225
+ snapshot,
1226
+ coordinator: atPrompt.state,
1227
+ appended,
1325
1228
  }),
1326
1229
  state,
1327
1230
  tools,
@@ -1342,7 +1245,7 @@ export function createHandlers<
1342
1245
  const schedulerFailure = consumeSchedulerSendFailure(error)
1343
1246
  if (checkAbort(ctx.signal)) return
1344
1247
  if (schedulerFailure === 'retryable') throw error
1345
- return resultEvent(call, 'execution:error', {
1248
+ return resultEvent(call, `${ctx.event.id}:execution:error`, {
1346
1249
  error: errorMessage(error),
1347
1250
  })
1348
1251
  }
@@ -1370,7 +1273,7 @@ export function createHandlers<
1370
1273
  return toolExecutionFailure(ctx, call, error)
1371
1274
  }
1372
1275
  if (iterator === undefined) {
1373
- return resultEvent(call, 'execution:0', { output })
1276
+ return resultEvent(call, `${ctx.event.id}:execution:0`, { output })
1374
1277
  }
1375
1278
  let last: unknown
1376
1279
  let sequence = 0
@@ -1383,23 +1286,27 @@ export function createHandlers<
1383
1286
  } catch (error) {
1384
1287
  return toolExecutionFailure(ctx, call, error)
1385
1288
  }
1386
- if (checkAbort(ctx.signal)) return
1387
1289
  if (result.done) {
1290
+ if (ctx.signal.reason instanceof A2Error) throw ctx.signal.reason
1388
1291
  done = true
1389
1292
  break
1390
1293
  }
1294
+ if (checkAbort(ctx.signal)) return
1391
1295
  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
- )
1296
+ await control.append({
1297
+ ctx,
1298
+ name: `tool:${call.toolCallId}:preliminary:${sequence}`,
1299
+ events: [
1300
+ resultEvent(
1301
+ call,
1302
+ `${ctx.event.id}:execution:${ctx.attempt}:${sequence}:preliminary`,
1303
+ {
1304
+ output: result.value,
1305
+ preliminary: true,
1306
+ },
1307
+ ),
1308
+ ],
1309
+ })
1403
1310
  sequence += 1
1404
1311
  }
1405
1312
  } finally {
@@ -1413,27 +1320,25 @@ export function createHandlers<
1413
1320
  }
1414
1321
  return resultEvent(
1415
1322
  call,
1416
- `execution:${sequence}:final`,
1323
+ `${ctx.event.id}:execution:${sequence}:final`,
1417
1324
  sequence === 0 ? {} : { output: last },
1418
1325
  )
1419
1326
  }
1420
1327
 
1421
- const executeTool = async (
1422
- ctx: ToolHandlerContext,
1423
- call: ToolCalledPayload,
1424
- ): Promise<AppendInput<D> | void> => {
1328
+ const executeTool = async (input: {
1329
+ ctx: ToolHandlerContext
1330
+ call: ToolCalledPayload
1331
+ generation: GenerationStartedPayload
1332
+ }): Promise<AppendInput<D> | void> => {
1333
+ const { ctx, call } = input
1425
1334
  const tool = tools[call.toolName] as RuntimeTool | undefined
1426
1335
  const execute = tool?.execute
1427
1336
  if (execute === undefined) {
1428
- return resultEvent(call, 'execution:error', {
1337
+ return resultEvent(call, `${ctx.event.id}:execution:error`, {
1429
1338
  error: `Tool '${call.toolName}' has no server executor`,
1430
1339
  })
1431
1340
  }
1432
- const messages = await promptMessages(
1433
- ctx.event.sessionId,
1434
- call,
1435
- ctx.session.history,
1436
- )
1341
+ const messages = await promptMessages(input)
1437
1342
  if (checkAbort(ctx.signal)) return
1438
1343
  const scope: AmbientToolScope = {
1439
1344
  contract: options.agent.contract,
@@ -1446,100 +1351,77 @@ export function createHandlers<
1446
1351
  )
1447
1352
  }
1448
1353
 
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 })
1354
+ const handleToolExecution = async (
1355
+ ctx: ToolHandlerContext,
1356
+ ): Promise<AppendInput<D>> => {
1357
+ try {
1358
+ const state = (
1359
+ await ctx.session.state(control.reducer, { through: 'latest' })
1360
+ ).state
1361
+ const request = ctx.event.payload
1362
+ const call = state.coordinator.response?.calls.find(
1363
+ (candidate) => candidate.work?.id === ctx.event.id,
1364
+ )
1365
+ if (
1366
+ state.active?.turnId !== request.turnId ||
1367
+ state.active.suspended ||
1368
+ state.active.version !== request.version ||
1369
+ state.coordinator.response?.failure !== undefined ||
1370
+ !call ||
1371
+ call.terminal ||
1372
+ call.work?.settled
1373
+ )
1374
+ return control.settled({ ctx, events: undefined })
1375
+ return control.settled({
1376
+ ctx,
1377
+ events: await executeTool({
1378
+ ctx,
1379
+ call: request.call,
1380
+ generation: request.generation,
1381
+ }),
1382
+ })
1383
+ } catch (error) {
1384
+ if (control.cancelled({ error, signal: ctx.signal }))
1385
+ return control.settled({ ctx, events: undefined })
1386
+ throw error
1501
1387
  }
1502
- return executeTool(ctx, current.call)
1503
1388
  }
1504
1389
 
1505
1390
  const generationHandler = async (
1506
1391
  ctx: HandlerContext<D, 'ai.generation.requested'>,
1507
1392
  ): Promise<void | AppendInput<D> | readonly AppendInput<D>[]> => {
1508
- const history = await ctx.session.history()
1393
+ if (checkAbort(ctx.signal)) return
1509
1394
  const requestId = ctx.event.id
1510
1395
  const request = ctx.event.payload
1396
+ const snapshot = await ctx.session.state(options.agent.reducer, {
1397
+ through: 'latest',
1398
+ })
1399
+ if (
1400
+ snapshot.state.active?.phase === 'paused' ||
1401
+ snapshot.state.active?.phase === 'pausing'
1402
+ )
1403
+ return
1511
1404
  const coordinatorState = (
1512
- await ctx.session.state(coordinator, { through: 'latest' })
1405
+ await ctx.session.state(coordinator, { through: snapshot.index })
1513
1406
  ).state
1514
1407
  if (
1515
1408
  coordinatorState.closed ||
1516
1409
  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
1410
  )
1537
- const priorAttempts = previousStarts.map(
1538
- (event) => (event.payload as GenerationStartedPayload).attempt,
1411
+ return
1412
+ const response = coordinatorState.response
1413
+ const current =
1414
+ response.generation?.requestId === requestId
1415
+ ? response.generation
1416
+ : undefined
1417
+ if (
1418
+ current !== undefined &&
1419
+ (response.completion !== undefined ||
1420
+ (response.failure !== undefined &&
1421
+ response.failure.superseded !== true))
1539
1422
  )
1540
- if (priorAttempts.some((priorAttempt) => priorAttempt >= ctx.attempt)) {
1541
1423
  return
1542
- }
1424
+ if (current !== undefined && current.attempt >= ctx.attempt) return
1543
1425
  const attempt = ctx.attempt
1544
1426
  const generationId = `${requestId}:generation:${attempt}`
1545
1427
  const responseMessageId =
@@ -1547,65 +1429,55 @@ export function createHandlers<
1547
1429
  (request.reason === 'message'
1548
1430
  ? `${request.messageId}:assistant`
1549
1431
  : 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)
1432
+ const responseStepCount = response.stepCount
1592
1433
  if (
1593
- request.reason === 'retry' &&
1594
- sourceCoordinatorState.response?.failure?.generationId !== undefined
1595
- ) {
1596
- replacedGenerationIds.add(
1597
- sourceCoordinatorState.response.failure.generationId,
1598
- )
1434
+ request.reason === 'tool' &&
1435
+ (requestId !==
1436
+ `ai.generate:tools:${response.source?.generation.generationId}` ||
1437
+ response.responseMessageId !== request.messageId ||
1438
+ response.responseMessageId !== request.responseMessageId)
1439
+ )
1440
+ return
1441
+ const previous = current
1442
+ const replaced =
1443
+ previous === undefined
1444
+ ? []
1445
+ : [{ generation: previous, frontier: response.promptThroughIndex! }]
1446
+ if (request.reason === 'retry' && response.source?.failed) {
1447
+ replaced.push({
1448
+ generation: response.source.generation,
1449
+ frontier: response.source.promptThroughIndex,
1450
+ })
1599
1451
  }
1600
- const promptHistory = withoutGenerationLifecycle(
1601
- history,
1602
- replacedGenerationIds,
1452
+ const replacedGenerationIds = new Set(
1453
+ replaced.map(({ generation: owner }) => owner.generationId),
1603
1454
  )
1604
- const state = replay(options.agent, promptHistory)
1455
+ let state = snapshot.state
1456
+ if (replaced.length > 0) {
1457
+ const through = Math.min(...replaced.map(({ frontier }) => frontier))
1458
+ const baseline = await ctx.session.state(options.agent.reducer, {
1459
+ through,
1460
+ })
1461
+ const tail = await ctx.session.history({
1462
+ gte: through + 1,
1463
+ lte: snapshot.index,
1464
+ })
1465
+ state = foldAIEvents({
1466
+ agent: options.agent,
1467
+ events: withoutGenerationLifecycle(tail, replacedGenerationIds),
1468
+ state: baseline.state,
1469
+ })
1470
+ }
1605
1471
  const resolverContext: AgentResolverContext<M, D> = {
1606
1472
  event: ctx.event,
1607
1473
  state,
1608
- history,
1474
+ session: {
1475
+ state: (reducer, readOptions) =>
1476
+ ctx.session.state(reducer, {
1477
+ ...readOptions,
1478
+ through: readOptions?.through ?? snapshot.index,
1479
+ }),
1480
+ },
1609
1481
  signal: ctx.signal,
1610
1482
  }
1611
1483
 
@@ -1624,7 +1496,7 @@ export function createHandlers<
1624
1496
  const compaction =
1625
1497
  typeof configuredCompaction === 'function'
1626
1498
  ? validateCompaction({
1627
- compaction: configuredCompaction(resolverContext),
1499
+ compaction: await resolve(configuredCompaction, resolverContext),
1628
1500
  explicit: true,
1629
1501
  generation: options.generation,
1630
1502
  tools: options.tools,
@@ -1633,7 +1505,7 @@ export function createHandlers<
1633
1505
  if (checkAbort(ctx.signal)) return
1634
1506
 
1635
1507
  if (request.reason === 'tool' && responseStepCount >= maxSteps) {
1636
- const source = sourceCoordinatorState.response?.generation
1508
+ const source = response.source?.generation
1637
1509
  if (source === undefined) return
1638
1510
  return {
1639
1511
  type: 'ai.generation.failed',
@@ -1656,7 +1528,7 @@ export function createHandlers<
1656
1528
  responseMessageId,
1657
1529
  attempt,
1658
1530
  model: modelName(resolvedModel),
1659
- promptThroughIndex: history.at(-1)?.index ?? ctx.event.index,
1531
+ promptThroughIndex: snapshot.index,
1660
1532
  }
1661
1533
  const catalogModelId = gatewayModelId(resolvedModel)
1662
1534
  const gatewayOptions = options.generation?.providerOptions?.['gateway']
@@ -1681,7 +1553,7 @@ export function createHandlers<
1681
1553
  } as AppendInput<D>)
1682
1554
  }
1683
1555
  if (previous) {
1684
- const payload = previous.payload as GenerationStartedPayload
1556
+ const payload = previous
1685
1557
  const superseded: GenerationFailedPayload = {
1686
1558
  requestId,
1687
1559
  messageId: payload.messageId,
@@ -1701,33 +1573,24 @@ export function createHandlers<
1701
1573
  id: generationId,
1702
1574
  payload: started,
1703
1575
  } as AppendInput<D>)
1704
- const generationHistory = [
1705
- ...history,
1706
- ...(await ctx.session.append('generation-start', ...startEvents)),
1707
- ]
1576
+ const generationEvents = await control.append({
1577
+ ctx,
1578
+ name: 'generation-start',
1579
+ events: startEvents,
1580
+ })
1708
1581
  generationStarted = true
1709
1582
 
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
- }
1583
+ if (checkAbort(ctx.signal)) return
1720
1584
 
1721
- const promptCoordinatorState = {
1722
- ...coordinatorStateAt(history),
1723
- queued: queuedMessagesAt(history),
1724
- }
1725
- const messages = activeContextMessages({
1585
+ const promptCoordinatorState = coordinatorState
1586
+ const messages = await contextMessages({
1726
1587
  agent: options.agent,
1727
- state,
1728
- history: promptHistory,
1588
+ session: ctx.session,
1589
+ snapshot: { state, index: snapshot.index },
1729
1590
  coordinator: promptCoordinatorState,
1591
+ excluded: replacedGenerationIds,
1730
1592
  })
1593
+ let generationMessages = messages
1731
1594
  let modelMessages = await modelContext({ messages, state, tools })
1732
1595
  const baseContext: AgentGenerateContext<M, D, T> = {
1733
1596
  request: ctx.event,
@@ -1737,7 +1600,7 @@ export function createHandlers<
1737
1600
  messages,
1738
1601
  modelMessages,
1739
1602
  state,
1740
- history,
1603
+ session: resolverContext.session,
1741
1604
  signal: ctx.signal,
1742
1605
  model: resolvedModel,
1743
1606
  tools,
@@ -1761,9 +1624,7 @@ export function createHandlers<
1761
1624
  : undefined
1762
1625
  let inputTokenEstimate: number | undefined
1763
1626
  let compacted = false
1764
- const canCompact =
1765
- sourceCoordinatorState.response?.calls.every((call) => call.terminal) ??
1766
- true
1627
+ const canCompact = response.source?.canCompact ?? true
1767
1628
  if (policy && canCompact) {
1768
1629
  const compactionContext = {
1769
1630
  ...resolverContext,
@@ -1782,7 +1643,7 @@ export function createHandlers<
1782
1643
  const inputTokens = measuredInputTokens({
1783
1644
  estimate: inputTokenEstimate,
1784
1645
  model: modelName(resolvedModel),
1785
- history,
1646
+ calibration: coordinatorState.calibration,
1786
1647
  })
1787
1648
  const threshold =
1788
1649
  policy.thresholdTokens ??
@@ -1816,18 +1677,25 @@ export function createHandlers<
1816
1677
  (queued) => queued.messageId === message.id,
1817
1678
  ),
1818
1679
  )?.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 },
1680
+ const throughIndex = snapshot.index
1681
+ generationEvents.push(
1682
+ ...(await control.append({
1683
+ ctx,
1684
+ name: 'compaction-requested',
1685
+ events: [
1686
+ {
1687
+ type: 'ai.compaction.requested',
1688
+ id: `${generationId}:compaction:requested`,
1689
+ payload: { generationId, throughMessageId, throughIndex },
1690
+ } as AppendInput<D>,
1691
+ ],
1825
1692
  })),
1826
1693
  )
1827
1694
  const result = !('shouldCompact' in policy)
1828
1695
  ? {
1829
1696
  messages: [] as M[],
1830
1697
  ...(await summarize({
1698
+ agentName: options.agent.contract.name,
1831
1699
  context: baseContext,
1832
1700
  instructions: policy.instructions,
1833
1701
  })),
@@ -1847,30 +1715,55 @@ export function createHandlers<
1847
1715
  ),
1848
1716
  }),
1849
1717
  }
1850
- generationHistory.push(
1851
- ...(await ctx.session.append('compaction-completed', {
1852
- type: 'ai.compaction.completed',
1853
- id: `${generationId}:compaction:completed`,
1854
- payload: completed,
1718
+ generationEvents.push(
1719
+ ...(await control.append({
1720
+ ctx,
1721
+ name: 'compaction-completed',
1722
+ events: [
1723
+ {
1724
+ type: 'ai.compaction.completed',
1725
+ id: `${generationId}:compaction:completed`,
1726
+ payload: completed,
1727
+ } as AppendInput<D>,
1728
+ ],
1855
1729
  })),
1856
1730
  )
1731
+ const queued = new Set(
1732
+ promptCoordinatorState.queued.map((item) => item.messageId),
1733
+ )
1734
+ generationMessages = result.messages.filter(
1735
+ (message) => !queued.has(message.id),
1736
+ )
1857
1737
  compacted = true
1858
1738
  }
1859
1739
  }
1860
1740
 
1861
- const currentState = replay(options.agent, generationHistory)
1862
- const currentCoordinatorState = coordinatorStateAt(generationHistory)
1863
- const generationMessages = activeContextMessages({
1741
+ const currentState = foldAIEvents({
1864
1742
  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,
1743
+ events: generationEvents,
1744
+ state: snapshot.state,
1873
1745
  })
1746
+ if (request.reason === 'retry' || previous !== undefined) {
1747
+ generationMessages = await contextMessages({
1748
+ agent: options.agent,
1749
+ session: ctx.session,
1750
+ snapshot,
1751
+ coordinator: promptCoordinatorState,
1752
+ appended: generationEvents,
1753
+ })
1754
+ }
1755
+ if (
1756
+ (policy && 'shouldCompact' in policy) ||
1757
+ generationMessages !== messages ||
1758
+ activeCompaction(currentState)?.summary !==
1759
+ activeCompaction(state)?.summary
1760
+ ) {
1761
+ modelMessages = await modelContext({
1762
+ messages: generationMessages,
1763
+ state: currentState,
1764
+ tools,
1765
+ })
1766
+ }
1874
1767
  promptCache.set(
1875
1768
  promptCacheKey(ctx.event.sessionId, generationId),
1876
1769
  Promise.resolve(modelMessages),
@@ -1893,7 +1786,7 @@ export function createHandlers<
1893
1786
  messages: generationMessages,
1894
1787
  modelMessages,
1895
1788
  state: currentState,
1896
- history,
1789
+ session: resolverContext.session,
1897
1790
  signal: ctx.signal,
1898
1791
  model: resolvedModel,
1899
1792
  tools,
@@ -1905,7 +1798,11 @@ export function createHandlers<
1905
1798
  const custom = options.generate !== undefined
1906
1799
  const source =
1907
1800
  options.generate === undefined
1908
- ? await generateWithAISDK(generateContext, options.messageMetadata)
1801
+ ? await generateWithAISDK({
1802
+ agentName: options.agent.contract.name,
1803
+ context: generateContext,
1804
+ messageMetadata: options.messageMetadata,
1805
+ })
1909
1806
  : { stream: await options.generate(generateContext) }
1910
1807
  const updates = consumeGeneration({
1911
1808
  source,
@@ -1942,15 +1839,18 @@ export function createHandlers<
1942
1839
  custom,
1943
1840
  })
1944
1841
  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
- )
1842
+ await control.append({
1843
+ ctx,
1844
+ name: `generation-progress:${sequence}`,
1845
+ events: [
1846
+ {
1847
+ type: 'ai.generation.progress',
1848
+ id: `${generationId}:progress:${sequence}`,
1849
+ payload: progress,
1850
+ } as AppendInput<D>,
1851
+ ...lifecycle.events,
1852
+ ],
1853
+ })
1954
1854
  sequence += 1
1955
1855
  }
1956
1856
 
@@ -2004,22 +1904,12 @@ export function createHandlers<
2004
1904
  } as AppendInput<D>,
2005
1905
  ]
2006
1906
  } catch (error) {
1907
+ if (error instanceof ControlCancelled) throw error
2007
1908
  if (error instanceof A2Error) {
2008
1909
  checkAbort(ctx.signal)
2009
1910
  throw error
2010
1911
  }
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
- }
1912
+ if (checkAbort(ctx.signal)) return
2023
1913
  if (!generationStarted) throw error
2024
1914
  const failed: GenerationFailedPayload = {
2025
1915
  requestId,
@@ -2036,59 +1926,6 @@ export function createHandlers<
2036
1926
  }
2037
1927
  }
2038
1928
 
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
1929
  const clearPromptCache = (sessionId: string): void => {
2093
1930
  const prefix = `${sessionId}\u001f`
2094
1931
  for (const key of promptCache.keys()) {
@@ -2096,13 +1933,6 @@ export function createHandlers<
2096
1933
  }
2097
1934
  }
2098
1935
 
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
1936
  return {
2107
1937
  'ai.model.metadata.requested': {
2108
1938
  handler: async (ctx) => {
@@ -2118,24 +1948,23 @@ export function createHandlers<
2118
1948
  } as AppendInput<D>
2119
1949
  },
2120
1950
  },
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
- },
1951
+ 'ai.control.requested': { lane: 'a2.ai.control', handler: control.handler },
1952
+ 'ai.work.reported': { lane: 'a2.ai.control', handler: control.handler },
2127
1953
  'ai.message.completed': {
2128
- lane: 'a2.ai.turn',
2129
- handler: handleResponseEnded,
1954
+ handler: async (ctx) => {
1955
+ clearPromptCache(ctx.event.sessionId)
1956
+ },
2130
1957
  },
2131
1958
  'ai.message.interrupted': {
2132
- lane: 'a2.ai.turn',
2133
- handler: handleResponseEnded,
1959
+ handler: async (ctx) => {
1960
+ clearPromptCache(ctx.event.sessionId)
1961
+ },
2134
1962
  },
2135
1963
  'ai.session.closed': {
2136
- handler: (ctx) => {
1964
+ lane: 'a2.ai.control',
1965
+ handler: async (ctx) => {
2137
1966
  clearPromptCache(ctx.event.sessionId)
2138
- return Promise.resolve()
1967
+ return control.handler(ctx)
2139
1968
  },
2140
1969
  },
2141
1970
  'ai.generation.failed': {
@@ -2147,60 +1976,41 @@ export function createHandlers<
2147
1976
  },
2148
1977
  },
2149
1978
  'ai.generation.requested': {
2150
- lane: 'a2.ai.turn',
1979
+ lane: 'a2.ai.model',
2151
1980
  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)
1981
+ 'ai.control.committed': (event, trigger) => {
1982
+ const active = (event.payload as ControlCommit).view
2158
1983
  return (
2159
- event.payload.messageId === responseMessageId &&
2160
- (event.payload.requestId === trigger.id ||
2161
- event.payload.generationId ===
2162
- `${trigger.id}:generation:${context.attempt}`)
1984
+ active?.turnId !== trigger.payload.control?.turnId ||
1985
+ active?.version !== trigger.payload.control?.version
2163
1986
  )
2164
1987
  },
2165
1988
  'ai.session.closed': true,
2166
1989
  },
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,
1990
+ handler: async (ctx) => {
1991
+ try {
1992
+ return control.settled({ ctx, events: await generationHandler(ctx) })
1993
+ } catch (error) {
1994
+ if (control.cancelled({ error, signal: ctx.signal }))
1995
+ return control.settled({ ctx, events: undefined })
1996
+ throw error
1997
+ }
2182
1998
  },
2183
- handler: handleToolCall,
2184
1999
  },
2185
- 'ai.approval.responded': {
2000
+ 'ai.tool.execution.requested': {
2186
2001
  abortOn: {
2187
2002
  '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),
2003
+ event.payload.generationId === trigger.payload.call.generationId,
2004
+ 'ai.control.committed': (event, trigger) => {
2005
+ const active = (event.payload as ControlCommit).view
2006
+ return (
2007
+ active?.turnId !== trigger.payload.turnId ||
2008
+ active?.version !== trigger.payload.version
2009
+ )
2010
+ },
2195
2011
  'ai.session.closed': true,
2196
2012
  },
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
- },
2013
+ handler: handleToolExecution,
2204
2014
  },
2205
2015
  } as NonNullable<ServerOptions<D>['handlers']>
2206
2016
  }