experimental-a2 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/dist/actor-client.d.ts +1 -1
  3. package/dist/{actor-BfQSE0KC.d.ts → actor-ohPC-81x.d.ts} +5 -5
  4. package/dist/{actor-BfQSE0KC.d.ts.map → actor-ohPC-81x.d.ts.map} +1 -1
  5. package/dist/actor-react.d.ts +3 -3
  6. package/dist/actor.d.ts +1 -1
  7. package/dist/actor.js +1 -1
  8. package/dist/ai-CrLYwNEx.js +2431 -0
  9. package/dist/ai-CrLYwNEx.js.map +1 -0
  10. package/dist/{ai-Cai-lCbj.d.ts → ai-HJ9fHfYI.d.ts} +72 -13
  11. package/dist/ai-HJ9fHfYI.d.ts.map +1 -0
  12. package/dist/ai-server.d.ts +4 -5
  13. package/dist/ai-server.d.ts.map +1 -1
  14. package/dist/ai-server.js +755 -290
  15. package/dist/ai-server.js.map +1 -1
  16. package/dist/ai.d.ts +2 -2
  17. package/dist/ai.js +1 -1999
  18. package/dist/client-BYzHjkwU.js.map +1 -1
  19. package/dist/{client-BAEABRZB.d.ts → client-CzyacQpJ.d.ts} +9 -9
  20. package/dist/client-CzyacQpJ.d.ts.map +1 -0
  21. package/dist/client.d.ts +1 -1
  22. package/dist/idempotent-id-BRJVeylj.js +13 -0
  23. package/dist/idempotent-id-BRJVeylj.js.map +1 -0
  24. package/dist/index.d.ts +3 -3
  25. package/dist/index.js +1 -12
  26. package/dist/otel.d.ts +1 -1
  27. package/dist/react.d.ts +13 -13
  28. package/dist/react.d.ts.map +1 -1
  29. package/dist/react.js.map +1 -1
  30. package/dist/reducer-DEMjEY_O.js.map +1 -1
  31. package/dist/{reducer-BcS9VDKC.d.ts → reducer-otzHjuJj.d.ts} +3 -3
  32. package/dist/{reducer-BcS9VDKC.d.ts.map → reducer-otzHjuJj.d.ts.map} +1 -1
  33. package/dist/scheduler-qstash.d.ts +2 -2
  34. package/dist/scheduler-qstash.js +1 -1
  35. package/dist/scheduler-vercel.d.ts +2 -2
  36. package/dist/scheduler-vercel.js +1 -1
  37. package/dist/{server-CjJSGcF7.js → server-BD5ckxZb.js} +39 -26
  38. package/dist/server-BD5ckxZb.js.map +1 -0
  39. package/dist/{server-Bp5Nd1pF.d.ts → server-DbD7IJVK.d.ts} +5 -5
  40. package/dist/{server-Bp5Nd1pF.d.ts.map → server-DbD7IJVK.d.ts.map} +1 -1
  41. package/dist/server.d.ts +3 -3
  42. package/dist/server.js +1 -1
  43. package/dist/{store-D_yhNdPz.d.ts → store-DyZM6fS5.d.ts} +3 -2
  44. package/dist/{store-D_yhNdPz.d.ts.map → store-DyZM6fS5.d.ts.map} +1 -1
  45. package/dist/store-N8PXxDAS.js.map +1 -1
  46. package/dist/store-memory.d.ts +1 -1
  47. package/dist/store-postgres.d.ts +1 -1
  48. package/dist/store-redis-http.d.ts +1 -1
  49. package/dist/store-redis-http.js +1 -1
  50. package/dist/{store-redis-notify-D2EI6gwX.js → store-redis-notify-BVLUfI3j.js} +14 -13
  51. package/dist/store-redis-notify-BVLUfI3j.js.map +1 -0
  52. package/dist/store-redis.d.ts +1 -1
  53. package/dist/store-redis.js +1 -1
  54. package/dist/store-sqlite.d.ts +1 -1
  55. package/dist/{telemetry-CpeclqB2.d.ts → telemetry-B5jzpy6y.d.ts} +2 -2
  56. package/dist/telemetry-B5jzpy6y.d.ts.map +1 -0
  57. package/docs/concepts/04-state.mdx +9 -4
  58. package/docs/guides/06-ai-agents.mdx +134 -43
  59. package/docs/reference/01-api.mdx +104 -27
  60. package/examples/playground/app/agent/[agentId]/agent-client.tsx +20 -28
  61. package/examples/playground/app/agent/[agentId]/agent-queue.test.tsx +123 -0
  62. package/examples/playground/app/agent/[agentId]/agent-queue.tsx +37 -43
  63. package/examples/playground/app/agent/[agentId]/compaction-panel.tsx +44 -24
  64. package/examples/playground/app/agent/compaction-timeline.test.ts +47 -7
  65. package/examples/playground/app/agent/compaction-timeline.ts +13 -3
  66. package/examples/playground/app/chat/[chatId]/chat-client.tsx +1 -5
  67. package/examples/playground/app/chat/[chatId]/session.ts +17 -2
  68. package/examples/playground/app/chat/model.test.ts +4 -4
  69. package/examples/playground/app/chat/model.ts +18 -6
  70. package/examples/playground/app/chat/server.ts +6 -3
  71. package/examples/playground/package.json +3 -1
  72. package/package.json +1 -1
  73. package/src/ai-client-state.ts +0 -2
  74. package/src/ai-context-schema.ts +296 -0
  75. package/src/ai-context.ts +680 -0
  76. package/src/ai-control-server.ts +160 -131
  77. package/src/ai-control-state.ts +6 -0
  78. package/src/ai-control.ts +6 -7
  79. package/src/ai-coordinator.ts +92 -38
  80. package/src/ai-id.ts +6 -0
  81. package/src/ai-input-tokens.ts +75 -0
  82. package/src/ai-message-projection.ts +348 -0
  83. package/src/ai-server.ts +232 -334
  84. package/src/ai-stored-state.ts +484 -0
  85. package/src/ai.ts +252 -418
  86. package/src/client.ts +21 -15
  87. package/src/react.ts +25 -19
  88. package/src/reducer.ts +2 -1
  89. package/src/server.ts +73 -56
  90. package/src/store-redis-core.ts +15 -17
  91. package/src/store.ts +1 -0
  92. package/src/telemetry.ts +2 -1
  93. package/dist/ai-Cai-lCbj.d.ts.map +0 -1
  94. package/dist/ai-control-CcD4hh3y.js +0 -119
  95. package/dist/ai-control-CcD4hh3y.js.map +0 -1
  96. package/dist/ai.js.map +0 -1
  97. package/dist/client-BAEABRZB.d.ts.map +0 -1
  98. package/dist/index.js.map +0 -1
  99. package/dist/server-CjJSGcF7.js.map +0 -1
  100. package/dist/store-redis-notify-D2EI6gwX.js.map +0 -1
  101. package/dist/telemetry-CpeclqB2.d.ts.map +0 -1
package/src/ai-server.ts CHANGED
@@ -9,10 +9,20 @@
9
9
  // ordered; parallel consumption would corrupt progress sequence and state.
10
10
 
11
11
  import { createControlRuntime, ControlCancelled } from './ai-control-server.ts'
12
- import type { ControlCommit } from './ai-control.ts'
12
+ import {
13
+ createContextReducer,
14
+ contextForAttempt,
15
+ generationBase,
16
+ type AIContextState,
17
+ type AIModelContext,
18
+ } from './ai-context.ts'
19
+ import type { Reducer } from './reducer.ts'
20
+ import { aiStateReducer } from './ai-stored-state.ts'
21
+ import { aiEventId, AI_EVENT_ID_PREFIX } from './ai-id.ts'
22
+ import type { ControlCommit, ControlCommand } from './ai-control.ts'
13
23
  import { AsyncLocalStorage } from 'node:async_hooks'
14
24
  import { createHash } from 'node:crypto'
15
- import { asSchema, convertToModelMessages } from 'ai'
25
+ import { convertToModelMessages, isToolUIPart } from 'ai'
16
26
  import type {
17
27
  FinishReason,
18
28
  Instructions,
@@ -26,14 +36,15 @@ import type {
26
36
  } from 'ai'
27
37
  import { generateAISDKStep, type AISDKStepSettings } from './ai-sdk-step.ts'
28
38
  import { gatewayModelId, readModelLimits } from './ai-model-metadata.ts'
39
+ import { estimateInputTokens, measuredInputTokens } from './ai-input-tokens.ts'
29
40
  import type {
30
41
  AIEventDefs,
31
- AIState,
32
42
  AgentDefinition,
33
43
  AgentToolContext,
34
44
  AgentToolEventDefs,
35
45
  ApprovalRequestedPayload,
36
46
  CompactionCompletedPayload,
47
+ CompactionRequestedPayload,
37
48
  GenerationCompletedPayload,
38
49
  GenerationFailedPayload,
39
50
  GenerationProgressPayload,
@@ -42,10 +53,7 @@ import type {
42
53
  ToolCalledPayload,
43
54
  ToolResultPayload,
44
55
  } from './ai.ts'
45
- import {
46
- aiCoordinatorReducer,
47
- type AICoordinatorState,
48
- } from './ai-coordinator.ts'
56
+ import type { AICoordinatorState } from './ai-coordinator.ts'
49
57
  import type { AppendInput, ContractEvent, EventDefs } from './contract.ts'
50
58
  import { A2Error } from './errors.ts'
51
59
  import {
@@ -116,7 +124,6 @@ export type AgentGenerateContext<
116
124
  responseMessageId: string
117
125
  messages: M[]
118
126
  modelMessages: ModelMessage[]
119
- state: AIState<M>
120
127
  session: Pick<HandlerContext<D>['session'], 'state'>
121
128
  signal: AbortSignal
122
129
  model: LanguageModel
@@ -149,7 +156,7 @@ export type AgentResolverContext<
149
156
  D extends AIEventDefs<M> & EventDefs,
150
157
  > = {
151
158
  event: ContractEvent<D, 'ai.generation.requested'>
152
- state: AIState<M>
159
+ messages: M[]
153
160
  session: Pick<HandlerContext<D>['session'], 'state'>
154
161
  signal: AbortSignal
155
162
  }
@@ -472,13 +479,12 @@ const foldAIEvents = <
472
479
  M extends UIMessage,
473
480
  D extends AIEventDefs<M> & EventDefs,
474
481
  >(options: {
475
- agent: AgentDefinition<M, D>
476
- state: AIState<M>
482
+ reducer: Reducer<D, AIContextState<M>>
483
+ state: AIContextState<M>
477
484
  events: ContractEvent<D>[]
478
- }): AIState<M> => {
485
+ }): AIContextState<M> => {
479
486
  let state = options.state
480
- for (const event of options.events)
481
- state = options.agent.reducer.fold(state, event)
487
+ for (const event of options.events) state = options.reducer.fold(state, event)
482
488
  return state
483
489
  }
484
490
 
@@ -539,7 +545,10 @@ const summarize = async <
539
545
  ...options.context.modelMessages,
540
546
  { role: 'user', content: instruction },
541
547
  ],
542
- responseMessageId: `${options.context.generationId}:summary`,
548
+ responseMessageId: await aiEventId(
549
+ 'a2.ai.summary',
550
+ options.context.generationId,
551
+ ),
543
552
  },
544
553
  messageMetadata: undefined,
545
554
  })
@@ -573,98 +582,26 @@ const summarize = async <
573
582
  }
574
583
 
575
584
  const activeCompaction = <M extends UIMessage>(
576
- state: AIState<M>,
577
- ): AIState<M>['compaction'] => {
585
+ state: AIModelContext<M>,
586
+ ): AIModelContext<M>['compaction'] => {
578
587
  const compaction = state.compaction
579
- return compaction?.status === 'completed' &&
580
- compaction.messages !== undefined &&
581
- state.messages.some((message) => message.id === compaction.throughMessageId)
582
- ? compaction
583
- : null
588
+ return compaction?.status === 'completed' ? compaction : null
584
589
  }
585
590
 
586
- const contextMessages = async <
587
- M extends UIMessage,
588
- D extends AIEventDefs<M> & EventDefs,
589
- >(options: {
590
- agent: AgentDefinition<M, D>
591
- session: HandlerContext<D>['session']
592
- snapshot: { state: AIState<M>; index: number }
591
+ const contextMessages = <M extends UIMessage>({
592
+ state,
593
+ coordinator,
594
+ }: {
595
+ state: AIModelContext<M>
593
596
  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 })
600
- const compaction = activeCompaction(state)
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)
607
- const retained = new Set(compaction.retainedMessageIds ?? [])
608
- const throughIndex = compaction.throughIndex
609
- if (throughIndex !== undefined) {
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)
644
- }
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
- )
652
- }
653
- const boundary = state.messages.findIndex(
654
- (message) => message.id === compaction.throughMessageId,
655
- )
656
- return visible([
657
- ...compaction.messages!,
658
- ...state.messages
659
- .slice(0, boundary + 1)
660
- .filter((message) => retained.has(message.id)),
661
- ...state.messages.slice(boundary + 1),
662
- ])
597
+ }): M[] => {
598
+ const queued = new Set(coordinator.queued.map((item) => item.messageId))
599
+ return state.messages.filter((message) => !queued.has(message.id))
663
600
  }
664
601
 
665
602
  const modelContext = async <M extends UIMessage>(options: {
666
603
  messages: M[]
667
- state: AIState<M>
604
+ state: AIModelContext<M>
668
605
  tools: ToolSet
669
606
  }): Promise<ModelMessage[]> => {
670
607
  const messages = await convertToModelMessages(options.messages, {
@@ -676,45 +613,6 @@ const modelContext = async <M extends UIMessage>(options: {
676
613
  : [{ role: 'user', content: summary }, ...messages]
677
614
  }
678
615
 
679
- const estimateInputTokens = async (options: {
680
- messages: ModelMessage[]
681
- instructions: Instructions | undefined
682
- tools: ToolSet
683
- }): Promise<number> => {
684
- const tools = await Promise.all(
685
- Object.entries(options.tools).map(async ([name, tool]) => ({
686
- name,
687
- description: tool.description,
688
- inputSchema: await asSchema(tool.inputSchema).jsonSchema,
689
- })),
690
- )
691
- return Math.ceil(
692
- new TextEncoder().encode(
693
- JSON.stringify({
694
- messages: options.messages,
695
- instructions: options.instructions,
696
- tools,
697
- }),
698
- ).byteLength / 4,
699
- )
700
- }
701
-
702
- const measuredInputTokens = (options: {
703
- estimate: number
704
- model: string
705
- calibration: AICoordinatorState['calibration']
706
- }): number => {
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
716
- }
717
-
718
616
  type ToolCallClassification = 'automatic' | 'approval' | 'unknown'
719
617
 
720
618
  type PendingToolCall = {
@@ -722,12 +620,16 @@ type PendingToolCall = {
722
620
  classification: ToolCallClassification
723
621
  }
724
622
 
725
- const toolCalledEvent = <D extends EventDefs>(
623
+ const toolCalledEvent = async <D extends EventDefs>(
726
624
  payload: ToolCalledPayload,
727
- ): AppendInput<D> =>
625
+ ): Promise<AppendInput<D>> =>
728
626
  ({
729
627
  type: 'ai.tool.called',
730
- id: `${payload.generationId}:tool:${payload.toolCallId}:called`,
628
+ id: await aiEventId(
629
+ 'a2.ai.tool.called',
630
+ payload.generationId,
631
+ payload.toolCallId,
632
+ ),
731
633
  payload,
732
634
  }) as AppendInput<D>
733
635
 
@@ -757,7 +659,10 @@ const approvalClassification = <T extends ToolSet>(
757
659
  return tool?.needsApproval === true ? 'approval' : 'automatic'
758
660
  }
759
661
 
760
- const lifecycleEvents = <D extends EventDefs, T extends ToolSet>(options: {
662
+ const lifecycleEvents = async <
663
+ D extends EventDefs,
664
+ T extends ToolSet,
665
+ >(options: {
761
666
  requestId: string
762
667
  messageId: string
763
668
  generationId: string
@@ -767,7 +672,7 @@ const lifecycleEvents = <D extends EventDefs, T extends ToolSet>(options: {
767
672
  tools: T
768
673
  generation: AgentGenerationSettings<T>
769
674
  custom: boolean
770
- }): { events: AppendInput<D>[]; pending: PendingToolCall[] } => {
675
+ }): Promise<{ events: AppendInput<D>[]; pending: PendingToolCall[] }> => {
771
676
  const result: AppendInput<D>[] = []
772
677
  const pending = [...options.pending]
773
678
  const take = (toolCallId: string): PendingToolCall | undefined => {
@@ -777,12 +682,18 @@ const lifecycleEvents = <D extends EventDefs, T extends ToolSet>(options: {
777
682
  if (index === -1) return undefined
778
683
  return pending.splice(index, 1)[0]
779
684
  }
780
- const flush = (toolCallId: string): void => {
685
+ const flush = async (toolCallId: string): Promise<void> => {
781
686
  const pendingCall = take(toolCallId)
782
- if (pendingCall) result.push(toolCalledEvent<D>(pendingCall.call))
687
+ if (pendingCall) result.push(await toolCalledEvent<D>(pendingCall.call))
783
688
  }
784
689
  for (const [chunkIndex, chunk] of options.chunks.entries()) {
785
- const resultId = `${options.generationId}:tool-result:${options.sequence}:${chunkIndex}`
690
+ const resultId = (): Promise<string> =>
691
+ aiEventId(
692
+ 'a2.ai.tool.stream-result',
693
+ options.generationId,
694
+ String(options.sequence),
695
+ String(chunkIndex),
696
+ )
786
697
  if (chunk.type === 'tool-input-available') {
787
698
  const payload: ToolCalledPayload = {
788
699
  requestId: options.requestId,
@@ -816,7 +727,7 @@ const lifecycleEvents = <D extends EventDefs, T extends ToolSet>(options: {
816
727
  chunk.toolName,
817
728
  )
818
729
  if (classification === 'automatic') {
819
- result.push(toolCalledEvent<D>(payload))
730
+ result.push(await toolCalledEvent<D>(payload))
820
731
  } else {
821
732
  pending.push({ call: payload, classification })
822
733
  }
@@ -850,13 +761,13 @@ const lifecycleEvents = <D extends EventDefs, T extends ToolSet>(options: {
850
761
  input: chunk.input,
851
762
  }
852
763
  take(chunk.toolCallId)
853
- result.push(toolCalledEvent<D>(called), {
764
+ result.push(await toolCalledEvent<D>(called), {
854
765
  type: 'ai.tool.result',
855
- id: resultId,
766
+ id: await resultId(),
856
767
  payload,
857
768
  } as AppendInput<D>)
858
769
  } else if (chunk.type === 'tool-output-available') {
859
- flush(chunk.toolCallId)
770
+ await flush(chunk.toolCallId)
860
771
  const payload: ToolResultPayload = {
861
772
  requestId: options.requestId,
862
773
  messageId: options.messageId,
@@ -880,11 +791,11 @@ const lifecycleEvents = <D extends EventDefs, T extends ToolSet>(options: {
880
791
  }
881
792
  result.push({
882
793
  type: 'ai.tool.result',
883
- id: resultId,
794
+ id: await resultId(),
884
795
  payload,
885
796
  } as AppendInput<D>)
886
797
  } else if (chunk.type === 'tool-output-error') {
887
- flush(chunk.toolCallId)
798
+ await flush(chunk.toolCallId)
888
799
  const payload: ToolResultPayload = {
889
800
  requestId: options.requestId,
890
801
  messageId: options.messageId,
@@ -905,11 +816,11 @@ const lifecycleEvents = <D extends EventDefs, T extends ToolSet>(options: {
905
816
  }
906
817
  result.push({
907
818
  type: 'ai.tool.result',
908
- id: resultId,
819
+ id: await resultId(),
909
820
  payload,
910
821
  } as AppendInput<D>)
911
822
  } else if (chunk.type === 'tool-output-denied') {
912
- flush(chunk.toolCallId)
823
+ await flush(chunk.toolCallId)
913
824
  const payload: ToolResultPayload = {
914
825
  requestId: options.requestId,
915
826
  messageId: options.messageId,
@@ -920,12 +831,13 @@ const lifecycleEvents = <D extends EventDefs, T extends ToolSet>(options: {
920
831
  }
921
832
  result.push({
922
833
  type: 'ai.tool.result',
923
- id: resultId,
834
+ id: await resultId(),
924
835
  payload,
925
836
  } as AppendInput<D>)
926
837
  } else if (chunk.type === 'tool-approval-request') {
927
- flush(chunk.toolCallId)
838
+ await flush(chunk.toolCallId)
928
839
  const payload: ApprovalRequestedPayload = {
840
+ requestId: options.requestId,
929
841
  messageId: options.messageId,
930
842
  generationId: options.generationId,
931
843
  approvalId: chunk.approvalId,
@@ -939,14 +851,23 @@ const lifecycleEvents = <D extends EventDefs, T extends ToolSet>(options: {
939
851
  }
940
852
  result.push({
941
853
  type: 'ai.approval.requested',
942
- id: `${options.generationId}:approval:${chunk.approvalId}`,
854
+ id: await aiEventId(
855
+ 'a2.ai.approval.requested',
856
+ options.generationId,
857
+ chunk.approvalId,
858
+ ),
943
859
  payload,
944
860
  } as AppendInput<D>)
945
861
  } else if (chunk.type === 'tool-approval-response') {
946
862
  result.push({
947
863
  type: 'ai.approval.responded',
948
- id: `${options.generationId}:approval:${chunk.approvalId}:automatic-response`,
864
+ id: await aiEventId(
865
+ 'a2.ai.approval.automatic-response',
866
+ options.generationId,
867
+ chunk.approvalId,
868
+ ),
949
869
  payload: {
870
+ requestId: options.requestId,
950
871
  messageId: options.messageId,
951
872
  generationId: options.generationId,
952
873
  approvalId: chunk.approvalId,
@@ -959,73 +880,6 @@ const lifecycleEvents = <D extends EventDefs, T extends ToolSet>(options: {
959
880
  return { events: result, pending }
960
881
  }
961
882
 
962
- function withoutGenerationLifecycle<D extends EventDefs>(
963
- history: ContractEvent<D>[],
964
- generationIds: ReadonlySet<string>,
965
- ): ContractEvent<D>[] {
966
- if (generationIds.size === 0) return history
967
- const approvalKeys = new Set<string>()
968
- const inputKeys = new Set<string>()
969
- for (const event of history) {
970
- const payload = event.payload as {
971
- generationId?: unknown
972
- messageId?: unknown
973
- approvalId?: unknown
974
- inputId?: unknown
975
- }
976
- if (
977
- typeof payload.generationId !== 'string' ||
978
- !generationIds.has(payload.generationId) ||
979
- typeof payload.messageId !== 'string'
980
- ) {
981
- continue
982
- }
983
- if (
984
- event.type === 'ai.approval.requested' &&
985
- typeof payload.approvalId === 'string'
986
- ) {
987
- approvalKeys.add(`${payload.messageId}\u001f${payload.approvalId}`)
988
- }
989
- if (
990
- event.type === 'ai.input.requested' &&
991
- typeof payload.inputId === 'string'
992
- ) {
993
- inputKeys.add(`${payload.messageId}\u001f${payload.inputId}`)
994
- }
995
- }
996
- return history.filter((event) => {
997
- const payload = event.payload as {
998
- generationId?: unknown
999
- messageId?: unknown
1000
- approvalId?: unknown
1001
- inputId?: unknown
1002
- }
1003
- if (
1004
- typeof payload.generationId === 'string' &&
1005
- generationIds.has(payload.generationId)
1006
- ) {
1007
- return false
1008
- }
1009
- if (
1010
- event.type === 'ai.approval.responded' &&
1011
- typeof payload.messageId === 'string' &&
1012
- typeof payload.approvalId === 'string'
1013
- ) {
1014
- return !approvalKeys.has(
1015
- `${payload.messageId}\u001f${payload.approvalId}`,
1016
- )
1017
- }
1018
- if (
1019
- event.type === 'ai.input.responded' &&
1020
- typeof payload.messageId === 'string' &&
1021
- typeof payload.inputId === 'string'
1022
- ) {
1023
- return !inputKeys.has(`${payload.messageId}\u001f${payload.inputId}`)
1024
- }
1025
- return true
1026
- })
1027
- }
1028
-
1029
883
  const validateCompaction = <
1030
884
  M extends UIMessage,
1031
885
  D extends AIEventDefs<M> & EventDefs,
@@ -1135,6 +989,14 @@ export function createHandlers<
1135
989
  const maxSteps = options.maxSteps ?? Number.POSITIVE_INFINITY
1136
990
  const control = createControlRuntime({ agent: options.agent })
1137
991
  const coordinator = control.coordinator
992
+ const contextReducer = createContextReducer({ agent: options.agent })
993
+ const readContext = async (
994
+ session: HandlerContext<D>['session'],
995
+ through: number | 'latest',
996
+ ) => {
997
+ const snapshot = await session.state(contextReducer, { through })
998
+ return { state: contextReducer.view(snapshot.state), index: snapshot.index }
999
+ }
1138
1000
  const promptCache = new Map<string, Promise<ModelMessage[]>>()
1139
1001
 
1140
1002
  type RuntimeTool = {
@@ -1150,7 +1012,7 @@ export function createHandlers<
1150
1012
 
1151
1013
  type ToolHandlerContext = HandlerContext<D, 'ai.tool.execution.requested'>
1152
1014
 
1153
- const resultEvent = (
1015
+ const resultEvent = async (
1154
1016
  call: ToolCalledPayload,
1155
1017
  suffix: string,
1156
1018
  result: {
@@ -1159,10 +1021,15 @@ export function createHandlers<
1159
1021
  denied?: boolean
1160
1022
  preliminary?: boolean
1161
1023
  },
1162
- ): AppendInput<D> =>
1024
+ ): Promise<AppendInput<D>> =>
1163
1025
  ({
1164
1026
  type: 'ai.tool.result',
1165
- id: `${call.generationId}:tool:${call.toolCallId}:${suffix}`,
1027
+ id: await aiEventId(
1028
+ 'a2.ai.tool.result',
1029
+ call.generationId,
1030
+ call.toolCallId,
1031
+ suffix,
1032
+ ),
1166
1033
  payload: {
1167
1034
  requestId: call.requestId,
1168
1035
  messageId: call.messageId,
@@ -1200,32 +1067,20 @@ export function createHandlers<
1200
1067
  const frontier = owner.promptThroughIndex
1201
1068
  if (frontier === undefined)
1202
1069
  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(
1209
- (event) =>
1210
- (event.type === 'ai.generation.started' ||
1211
- event.type === 'ai.compaction.requested' ||
1212
- event.type === 'ai.compaction.completed') &&
1213
- (event.payload as { generationId: string }).generationId ===
1214
- call.generationId,
1070
+ const snapshot = await readContext(ctx.session, ctx.event.index)
1071
+ if (
1072
+ snapshot.state.generation?.owner.generationId !== owner.generationId ||
1073
+ snapshot.state.generation.owner.requestId !== owner.requestId ||
1074
+ snapshot.state.activeProjection?.generationId !== owner.generationId
1215
1075
  )
1216
- const state = foldAIEvents({
1217
- agent: options.agent,
1218
- events: appended,
1219
- state: snapshot.state,
1220
- })
1076
+ throw new TypeError(
1077
+ 'Tool prompt checkpoint does not own its generation',
1078
+ )
1079
+ const state = generationBase(snapshot.state)
1221
1080
  return modelContext({
1222
- messages: await contextMessages({
1223
- agent: options.agent,
1224
- session: ctx.session,
1225
- snapshot,
1226
- coordinator: atPrompt.state,
1227
- appended,
1228
- }),
1081
+ messages: state.messages.filter(
1082
+ (message) => state.messageIndexes[message.id]! <= frontier,
1083
+ ),
1229
1084
  state,
1230
1085
  tools,
1231
1086
  })
@@ -1237,11 +1092,11 @@ export function createHandlers<
1237
1092
  return computation
1238
1093
  }
1239
1094
 
1240
- const toolExecutionFailure = (
1095
+ const toolExecutionFailure = async (
1241
1096
  ctx: ToolHandlerContext,
1242
1097
  call: ToolCalledPayload,
1243
1098
  error: unknown,
1244
- ): AppendInput<D> | void => {
1099
+ ): Promise<AppendInput<D> | void> => {
1245
1100
  const schedulerFailure = consumeSchedulerSendFailure(error)
1246
1101
  if (checkAbort(ctx.signal)) return
1247
1102
  if (schedulerFailure === 'retryable') throw error
@@ -1297,7 +1152,7 @@ export function createHandlers<
1297
1152
  ctx,
1298
1153
  name: `tool:${call.toolCallId}:preliminary:${sequence}`,
1299
1154
  events: [
1300
- resultEvent(
1155
+ await resultEvent(
1301
1156
  call,
1302
1157
  `${ctx.event.id}:execution:${ctx.attempt}:${sequence}:preliminary`,
1303
1158
  {
@@ -1393,12 +1248,12 @@ export function createHandlers<
1393
1248
  if (checkAbort(ctx.signal)) return
1394
1249
  const requestId = ctx.event.id
1395
1250
  const request = ctx.event.payload
1396
- const snapshot = await ctx.session.state(options.agent.reducer, {
1397
- through: 'latest',
1398
- })
1251
+ const snapshot = await readContext(ctx.session, 'latest')
1399
1252
  if (
1400
- snapshot.state.active?.phase === 'paused' ||
1401
- snapshot.state.active?.phase === 'pausing'
1253
+ snapshot.state.closed ||
1254
+ snapshot.state.authority?.requestId !== requestId ||
1255
+ snapshot.state.authority?.phase === 'paused' ||
1256
+ snapshot.state.authority?.phase === 'pausing'
1402
1257
  )
1403
1258
  return
1404
1259
  const coordinatorState = (
@@ -1423,54 +1278,29 @@ export function createHandlers<
1423
1278
  return
1424
1279
  if (current !== undefined && current.attempt >= ctx.attempt) return
1425
1280
  const attempt = ctx.attempt
1426
- const generationId = `${requestId}:generation:${attempt}`
1427
- const responseMessageId =
1428
- request.responseMessageId ??
1429
- (request.reason === 'message'
1430
- ? `${request.messageId}:assistant`
1431
- : request.messageId)
1281
+ const generationId = await aiEventId(
1282
+ 'a2.ai.generation',
1283
+ requestId,
1284
+ String(attempt),
1285
+ )
1286
+ const responseMessageId = request.responseMessageId
1432
1287
  const responseStepCount = response.stepCount
1433
1288
  if (
1434
1289
  request.reason === 'tool' &&
1435
- (requestId !==
1436
- `ai.generate:tools:${response.source?.generation.generationId}` ||
1290
+ (request.sourceGenerationId !==
1291
+ response.source?.generation.generationId ||
1437
1292
  response.responseMessageId !== request.messageId ||
1438
1293
  response.responseMessageId !== request.responseMessageId)
1439
1294
  )
1440
1295
  return
1441
1296
  const previous = current
1442
- const replaced =
1297
+ const state =
1443
1298
  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
- })
1451
- }
1452
- const replacedGenerationIds = new Set(
1453
- replaced.map(({ generation: owner }) => owner.generationId),
1454
- )
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
- }
1299
+ ? snapshot.state
1300
+ : contextForAttempt(snapshot.state)
1471
1301
  const resolverContext: AgentResolverContext<M, D> = {
1472
1302
  event: ctx.event,
1473
- state,
1303
+ messages: contextMessages({ state, coordinator: coordinatorState }),
1474
1304
  session: {
1475
1305
  state: (reducer, readOptions) =>
1476
1306
  ctx.session.state(reducer, {
@@ -1509,7 +1339,7 @@ export function createHandlers<
1509
1339
  if (source === undefined) return
1510
1340
  return {
1511
1341
  type: 'ai.generation.failed',
1512
- id: `${requestId}:step-limit`,
1342
+ id: await aiEventId('a2.ai.step-limit', requestId),
1513
1343
  payload: {
1514
1344
  requestId: source.requestId,
1515
1345
  messageId: source.messageId,
@@ -1548,7 +1378,12 @@ export function createHandlers<
1548
1378
  ) {
1549
1379
  startEvents.push({
1550
1380
  type: 'ai.model.metadata.requested',
1551
- id: `ai.model.metadata:${encodeURIComponent(options.agent.contract.name)}:${encodeURIComponent(ctx.event.sessionId)}:${encodeURIComponent(catalogModelId)}`,
1381
+ id: await aiEventId(
1382
+ 'a2.ai.model.metadata',
1383
+ options.agent.contract.name,
1384
+ ctx.event.sessionId,
1385
+ catalogModelId,
1386
+ ),
1552
1387
  payload: { modelId: catalogModelId },
1553
1388
  } as AppendInput<D>)
1554
1389
  }
@@ -1564,7 +1399,7 @@ export function createHandlers<
1564
1399
  }
1565
1400
  startEvents.push({
1566
1401
  type: 'ai.generation.failed',
1567
- id: `${payload.generationId}:superseded`,
1402
+ id: await aiEventId('a2.ai.superseded', payload.generationId),
1568
1403
  payload: superseded,
1569
1404
  } as AppendInput<D>)
1570
1405
  }
@@ -1583,12 +1418,9 @@ export function createHandlers<
1583
1418
  if (checkAbort(ctx.signal)) return
1584
1419
 
1585
1420
  const promptCoordinatorState = coordinatorState
1586
- const messages = await contextMessages({
1587
- agent: options.agent,
1588
- session: ctx.session,
1589
- snapshot: { state, index: snapshot.index },
1421
+ const messages = contextMessages({
1422
+ state,
1590
1423
  coordinator: promptCoordinatorState,
1591
- excluded: replacedGenerationIds,
1592
1424
  })
1593
1425
  let generationMessages = messages
1594
1426
  let modelMessages = await modelContext({ messages, state, tools })
@@ -1599,7 +1431,6 @@ export function createHandlers<
1599
1431
  responseMessageId,
1600
1432
  messages,
1601
1433
  modelMessages,
1602
- state,
1603
1434
  session: resolverContext.session,
1604
1435
  signal: ctx.signal,
1605
1436
  model: resolvedModel,
@@ -1624,8 +1455,24 @@ export function createHandlers<
1624
1455
  : undefined
1625
1456
  let inputTokenEstimate: number | undefined
1626
1457
  let compacted = false
1627
- const canCompact = response.source?.canCompact ?? true
1628
- if (policy && canCompact) {
1458
+ let compactionEstimate:
1459
+ | Pick<
1460
+ CompactionRequestedPayload,
1461
+ 'inputTokenEstimate' | 'inputTokens' | 'thresholdTokens'
1462
+ >
1463
+ | undefined
1464
+ const hasPendingTools = messages.some((message) =>
1465
+ message.parts.some(
1466
+ (part) =>
1467
+ isToolUIPart(part) &&
1468
+ !(
1469
+ part.state === 'output-error' ||
1470
+ part.state === 'output-denied' ||
1471
+ (part.state === 'output-available' && part.preliminary !== true)
1472
+ ),
1473
+ ),
1474
+ )
1475
+ if (policy && !hasPendingTools) {
1629
1476
  const compactionContext = {
1630
1477
  ...resolverContext,
1631
1478
  messages,
@@ -1665,10 +1512,24 @@ export function createHandlers<
1665
1512
  )
1666
1513
  }
1667
1514
  shouldCompact = threshold !== undefined && inputTokens >= threshold
1515
+ if (threshold !== undefined && shouldCompact) {
1516
+ compactionEstimate = {
1517
+ inputTokenEstimate,
1518
+ inputTokens,
1519
+ thresholdTokens: threshold,
1520
+ }
1521
+ }
1668
1522
  } else {
1669
1523
  shouldCompact = false
1670
1524
  }
1671
1525
  if (checkAbort(ctx.signal)) return
1526
+ if (shouldCompact) {
1527
+ const latest = (
1528
+ await ctx.session.state(coordinator, { through: 'latest' })
1529
+ ).state
1530
+ shouldCompact = Object.keys(latest.unsettledTools ?? {}).length === 0
1531
+ }
1532
+ if (checkAbort(ctx.signal)) return
1672
1533
  if (shouldCompact) {
1673
1534
  const throughMessageId =
1674
1535
  state.messages.findLast(
@@ -1685,8 +1546,16 @@ export function createHandlers<
1685
1546
  events: [
1686
1547
  {
1687
1548
  type: 'ai.compaction.requested',
1688
- id: `${generationId}:compaction:requested`,
1689
- payload: { generationId, throughMessageId, throughIndex },
1549
+ id: await aiEventId(
1550
+ 'a2.ai.compaction.requested',
1551
+ generationId,
1552
+ ),
1553
+ payload: {
1554
+ generationId,
1555
+ throughMessageId,
1556
+ throughIndex,
1557
+ ...compactionEstimate,
1558
+ },
1690
1559
  } as AppendInput<D>,
1691
1560
  ],
1692
1561
  })),
@@ -1722,7 +1591,10 @@ export function createHandlers<
1722
1591
  events: [
1723
1592
  {
1724
1593
  type: 'ai.compaction.completed',
1725
- id: `${generationId}:compaction:completed`,
1594
+ id: await aiEventId(
1595
+ 'a2.ai.compaction.completed',
1596
+ generationId,
1597
+ ),
1726
1598
  payload: completed,
1727
1599
  } as AppendInput<D>,
1728
1600
  ],
@@ -1739,17 +1611,14 @@ export function createHandlers<
1739
1611
  }
1740
1612
 
1741
1613
  const currentState = foldAIEvents({
1742
- agent: options.agent,
1614
+ reducer: contextReducer[aiStateReducer],
1743
1615
  events: generationEvents,
1744
- state: snapshot.state,
1616
+ state,
1745
1617
  })
1746
1618
  if (request.reason === 'retry' || previous !== undefined) {
1747
- generationMessages = await contextMessages({
1748
- agent: options.agent,
1749
- session: ctx.session,
1750
- snapshot,
1619
+ generationMessages = contextMessages({
1620
+ state: currentState,
1751
1621
  coordinator: promptCoordinatorState,
1752
- appended: generationEvents,
1753
1622
  })
1754
1623
  }
1755
1624
  if (
@@ -1785,7 +1654,6 @@ export function createHandlers<
1785
1654
  responseMessageId,
1786
1655
  messages: generationMessages,
1787
1656
  modelMessages,
1788
- state: currentState,
1789
1657
  session: resolverContext.session,
1790
1658
  signal: ctx.signal,
1791
1659
  model: resolvedModel,
@@ -1827,7 +1695,7 @@ export function createHandlers<
1827
1695
  sequence,
1828
1696
  chunks: update.chunks,
1829
1697
  }
1830
- const lifecycle = lifecycleEvents<D, T>({
1698
+ const lifecycle = await lifecycleEvents<D, T>({
1831
1699
  requestId,
1832
1700
  messageId: responseMessageId,
1833
1701
  generationId,
@@ -1845,7 +1713,11 @@ export function createHandlers<
1845
1713
  events: [
1846
1714
  {
1847
1715
  type: 'ai.generation.progress',
1848
- id: `${generationId}:progress:${sequence}`,
1716
+ id: await aiEventId(
1717
+ 'a2.ai.progress',
1718
+ generationId,
1719
+ String(sequence),
1720
+ ),
1849
1721
  payload: progress,
1850
1722
  } as AppendInput<D>,
1851
1723
  ...lifecycle.events,
@@ -1868,7 +1740,7 @@ export function createHandlers<
1868
1740
  }
1869
1741
  const completionEvent = {
1870
1742
  type: 'ai.generation.completed',
1871
- id: `${generationId}:completed`,
1743
+ id: await aiEventId('a2.ai.completed', generationId),
1872
1744
  payload: completed,
1873
1745
  } as AppendInput<D>
1874
1746
  const unresolved = options.generate
@@ -1881,14 +1753,16 @@ export function createHandlers<
1881
1753
  `generation ended with unresolved tool authorization for '${unresolved[0]!.call.toolName}'`,
1882
1754
  )
1883
1755
  }
1884
- const pendingEvents = pendingToolCalls
1885
- .filter(
1886
- (pending) =>
1887
- pending.classification === 'automatic' ||
1888
- (options.generate === undefined &&
1889
- pending.classification === 'unknown'),
1890
- )
1891
- .map((pending) => toolCalledEvent<D>(pending.call))
1756
+ const pendingEvents = await Promise.all(
1757
+ pendingToolCalls
1758
+ .filter(
1759
+ (pending) =>
1760
+ pending.classification === 'automatic' ||
1761
+ (options.generate === undefined &&
1762
+ pending.classification === 'unknown'),
1763
+ )
1764
+ .map((pending) => toolCalledEvent<D>(pending.call)),
1765
+ )
1892
1766
  if (finish.finishReason === 'tool-calls') {
1893
1767
  return [...pendingEvents, completionEvent]
1894
1768
  }
@@ -1897,7 +1771,7 @@ export function createHandlers<
1897
1771
  completionEvent,
1898
1772
  {
1899
1773
  type: 'ai.message.completed',
1900
- id: `${generationId}:message:completed`,
1774
+ id: await aiEventId('a2.ai.message.completed', generationId),
1901
1775
  payload: {
1902
1776
  messageId: responseMessageId,
1903
1777
  } satisfies MessageCompletedPayload,
@@ -1920,7 +1794,7 @@ export function createHandlers<
1920
1794
  }
1921
1795
  return {
1922
1796
  type: 'ai.generation.failed',
1923
- id: `${generationId}:failed`,
1797
+ id: await aiEventId('a2.ai.failed', generationId),
1924
1798
  payload: failed,
1925
1799
  } as AppendInput<D>
1926
1800
  }
@@ -1943,12 +1817,36 @@ export function createHandlers<
1943
1817
  ctx.signal.throwIfAborted()
1944
1818
  return {
1945
1819
  type: 'ai.model.metadata.resolved',
1946
- id: `${ctx.event.id}:resolved`,
1820
+ id: await aiEventId('a2.ai.model.metadata.resolved', ctx.event.id),
1947
1821
  payload: { modelId: ctx.event.payload.modelId, limits },
1948
1822
  } as AppendInput<D>
1949
1823
  },
1950
1824
  },
1951
- 'ai.control.requested': { lane: 'a2.ai.control', handler: control.handler },
1825
+ 'ai.control.requested': {
1826
+ lane: ({ event }) => {
1827
+ const command = event.payload as ControlCommand<M>
1828
+ if (event.id === '')
1829
+ throw new A2Error(
1830
+ 'INVALID_PAYLOAD',
1831
+ 'AI command IDs must be nonempty',
1832
+ )
1833
+ if (event.id?.startsWith(AI_EVENT_ID_PREFIX))
1834
+ throw new A2Error(
1835
+ 'INVALID_PAYLOAD',
1836
+ `AI command IDs cannot use the reserved '${AI_EVENT_ID_PREFIX}' prefix`,
1837
+ )
1838
+ if (
1839
+ (command.action === 'send' || command.action === 'steer') &&
1840
+ event.id !== command.message.id
1841
+ )
1842
+ throw new A2Error(
1843
+ 'INVALID_PAYLOAD',
1844
+ 'AI input event ID must equal message.id',
1845
+ )
1846
+ return 'a2.ai.control'
1847
+ },
1848
+ handler: control.handler,
1849
+ },
1952
1850
  'ai.work.reported': { lane: 'a2.ai.control', handler: control.handler },
1953
1851
  'ai.message.completed': {
1954
1852
  handler: async (ctx) => {