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
@@ -0,0 +1,152 @@
1
+ import type {
2
+ AIControlState,
3
+ ControlCommit,
4
+ ControlChange,
5
+ } from './ai-control.ts'
6
+ import type { UIMessage } from 'ai'
7
+
8
+ type RecordValue = Record<string, unknown> | unknown[]
9
+ const container = (value: unknown): value is RecordValue =>
10
+ typeof value === 'object' && value !== null
11
+ const same = (left: unknown, right: unknown): boolean =>
12
+ JSON.stringify(left) === JSON.stringify(right)
13
+
14
+ export const controlChanges = ({
15
+ before,
16
+ after,
17
+ }: {
18
+ before: unknown
19
+ after: unknown
20
+ }): ControlChange[] => {
21
+ const changes: ControlChange[] = []
22
+ const visit = (
23
+ left: unknown,
24
+ right: unknown,
25
+ path: (string | number)[],
26
+ ): void => {
27
+ if (same(left, right)) return
28
+ if (Array.isArray(left) && Array.isArray(right)) {
29
+ let prefix = 0
30
+ while (
31
+ prefix < Math.min(left.length, right.length) &&
32
+ same(left[prefix], right[prefix])
33
+ )
34
+ prefix += 1
35
+ let suffix = 0
36
+ while (
37
+ suffix < Math.min(left.length, right.length) - prefix &&
38
+ same(left[left.length - 1 - suffix], right[right.length - 1 - suffix])
39
+ )
40
+ suffix += 1
41
+ if (left.length === right.length) {
42
+ for (let index = prefix; index < left.length - suffix; index += 1)
43
+ visit(left[index], right[index], [...path, index])
44
+ } else
45
+ changes.push({
46
+ path,
47
+ splice: {
48
+ index: prefix,
49
+ deleteCount: left.length - prefix - suffix,
50
+ items: right.slice(prefix, right.length - suffix),
51
+ },
52
+ })
53
+ return
54
+ }
55
+ if (
56
+ container(left) &&
57
+ container(right) &&
58
+ !Array.isArray(left) &&
59
+ !Array.isArray(right)
60
+ ) {
61
+ for (const key of Object.keys(left))
62
+ if (!Object.hasOwn(right, key))
63
+ changes.push({ path: [...path, key], remove: true })
64
+ for (const key of Object.keys(right))
65
+ visit(Object.hasOwn(left, key) ? left[key] : undefined, right[key], [
66
+ ...path,
67
+ key,
68
+ ])
69
+ return
70
+ }
71
+ changes.push({ path, value: right })
72
+ }
73
+ visit(before, after, [])
74
+ return changes
75
+ }
76
+
77
+ export const applyControlChanges = <T>({
78
+ state,
79
+ changes,
80
+ }: {
81
+ state: T
82
+ changes: readonly ControlChange[]
83
+ }): T => {
84
+ if (changes.length === 0) return state
85
+ let next = structuredClone(state)
86
+ for (const change of changes) {
87
+ const path = change.path
88
+ if (path.length === 0) {
89
+ next = structuredClone(change.value) as T
90
+ continue
91
+ }
92
+ let target: unknown = next
93
+ for (const key of path.slice(0, -1)) {
94
+ if (!container(target) || !Object.hasOwn(target, key))
95
+ throw new TypeError('Invalid AI control change path')
96
+ target = (target as Record<string | number, unknown>)[key]
97
+ }
98
+ if (!container(target))
99
+ throw new TypeError('Invalid AI control change target')
100
+ const parent = target as Record<string | number, unknown>
101
+ const key = path.at(-1)!
102
+ if (change.remove) delete parent[key]
103
+ else if (change.splice) {
104
+ const items = parent[key] as unknown[]
105
+ Object.defineProperty(parent, key, {
106
+ value: [
107
+ ...items.slice(0, change.splice.index),
108
+ ...structuredClone(change.splice.items),
109
+ ...items.slice(change.splice.index + change.splice.deleteCount),
110
+ ],
111
+ enumerable: true,
112
+ configurable: true,
113
+ writable: true,
114
+ })
115
+ } else if (change.move) {
116
+ const items = parent[key] as unknown[]
117
+ const [item] = items.splice(change.move.from, 1)
118
+ items.splice(change.move.to, 0, item)
119
+ } else
120
+ Object.defineProperty(parent, key, {
121
+ value: structuredClone(change.value),
122
+ enumerable: true,
123
+ configurable: true,
124
+ writable: true,
125
+ })
126
+ }
127
+ return next
128
+ }
129
+
130
+ export const controlCommit = <M extends UIMessage>(options: {
131
+ before: AIControlState<M>
132
+ after: AIControlState<M>
133
+ moves: ControlChange[]
134
+ }): ControlCommit => {
135
+ const active = options.after.active
136
+ const response = options.after.coordinator.response
137
+ return {
138
+ changes: [
139
+ ...options.moves,
140
+ ...controlChanges({ before: options.before, after: options.after }),
141
+ ],
142
+ view: {
143
+ turnId: active?.turnId ?? null,
144
+ version: active?.version ?? null,
145
+ phase: active?.phase ?? null,
146
+ paused: options.after.inbox.paused,
147
+ closed: options.after.coordinator.closed,
148
+ requestId: response?.activeRequestId ?? null,
149
+ responseMessageId: response?.responseMessageId ?? null,
150
+ },
151
+ }
152
+ }
@@ -0,0 +1,139 @@
1
+ import type { UIMessage } from 'ai'
2
+ import type { AICoordinatorState } from './ai-coordinator.ts'
3
+ import type {
4
+ ApprovalRespondedPayload,
5
+ GenerationStartedPayload,
6
+ InputRequestedPayload,
7
+ InputRespondedPayload,
8
+ MessageInterruptedPayload,
9
+ RetryRequestedPayload,
10
+ ToolCalledPayload,
11
+ ToolResultPayload,
12
+ } from './ai.ts'
13
+
14
+ export type InboxItem<M extends UIMessage = UIMessage> = {
15
+ id: string
16
+ revision: number
17
+ message: M
18
+ generate: boolean
19
+ afterStepOf?: string
20
+ }
21
+
22
+ export type ControlCommand<M extends UIMessage = UIMessage> =
23
+ | { action: 'send'; message: M; generate?: boolean }
24
+ | { action: 'edit'; message: M; expectedRevision: number }
25
+ | { action: 'move'; inputId: string; beforeId: string | null }
26
+ | { action: 'remove'; inputId: string }
27
+ | {
28
+ action: 'send-now'
29
+ inputId: string
30
+ turnId: string | null
31
+ lastSeenIndex?: number
32
+ }
33
+ | { action: 'stop'; turnId: string; lastSeenIndex?: number }
34
+ | { action: 'steer'; turnId: string; message: M; lastSeenIndex?: number }
35
+ | { action: 'pause'; when: 'now' | 'after-turn' }
36
+ | { action: 'resume' }
37
+ | { action: 'tool-result'; result: ToolResultPayload }
38
+ | { action: 'approval'; response: ApprovalRespondedPayload }
39
+ | { action: 'input'; response: InputRespondedPayload }
40
+ | { action: 'request-input'; request: InputRequestedPayload }
41
+ | { action: 'retry'; request: RetryRequestedPayload }
42
+ | { action: 'interrupt'; request: MessageInterruptedPayload }
43
+
44
+ export type ControlReceipt = {
45
+ commandId: string
46
+ work?: true
47
+ outcome: 'applied' | 'rejected'
48
+ reason?:
49
+ | 'already-active'
50
+ | 'revision-conflict'
51
+ | 'stale-turn'
52
+ | 'not-found'
53
+ | 'duplicate-input'
54
+ | 'closed'
55
+ | 'stale-work'
56
+ | 'not-user-input'
57
+ }
58
+
59
+ export type ActiveTurn<M extends UIMessage = UIMessage> = {
60
+ turnId: string
61
+ input: InboxItem<M>
62
+ phase:
63
+ | 'starting'
64
+ | 'generating'
65
+ | 'tools'
66
+ | 'waiting'
67
+ | 'failed'
68
+ | 'pausing'
69
+ | 'paused'
70
+ }
71
+
72
+ export type ControlWork = {
73
+ id: string
74
+ version: number
75
+ settled: boolean
76
+ attempt?: number
77
+ facts?: string[]
78
+ }
79
+
80
+ export type AIControlState<M extends UIMessage = UIMessage> = {
81
+ reservedIds: Record<string, true | 'removed'>
82
+ inbox: { paused: boolean; items: InboxItem<M>[] }
83
+ active:
84
+ | (ActiveTurn<M> & {
85
+ version: number
86
+ suspended: boolean
87
+ model: ControlWork | null
88
+ })
89
+ | null
90
+ coordinator: AICoordinatorState
91
+ }
92
+
93
+ export type WorkFact = { type: string; id: string; payload: unknown }
94
+ export type WorkReport = {
95
+ attempt: number
96
+ workId: string
97
+ turnId: string
98
+ version: number
99
+ kind: 'model' | 'tool'
100
+ settled: boolean
101
+ events: WorkFact[]
102
+ }
103
+
104
+ export type ToolExecutionRequest = {
105
+ turnId: string
106
+ version: number
107
+ call: ToolCalledPayload
108
+ generation: GenerationStartedPayload
109
+ }
110
+
111
+ export const initialControlState = <
112
+ M extends UIMessage,
113
+ >(): AIControlState<M> => ({
114
+ reservedIds: {},
115
+ inbox: { paused: false, items: [] },
116
+ active: null,
117
+ coordinator: { closed: false, queued: [] },
118
+ })
119
+
120
+ export type ControlChange = {
121
+ path: (string | number)[]
122
+ value?: unknown
123
+ remove?: true
124
+ splice?: { index: number; deleteCount: number; items: unknown[] }
125
+ move?: { from: number; to: number }
126
+ }
127
+
128
+ export type ControlCommit = {
129
+ changes: ControlChange[]
130
+ view: {
131
+ turnId: string | null
132
+ version: number | null
133
+ phase: ActiveTurn['phase'] | null
134
+ paused: boolean
135
+ closed: boolean
136
+ requestId: string | null
137
+ responseMessageId: string | null
138
+ }
139
+ }
@@ -1,4 +1,6 @@
1
1
  import type { UIMessage } from 'ai'
2
+ import { applyControlChanges } from './ai-control-state.ts'
3
+ import type { ControlCommit, ControlWork } from './ai-control.ts'
2
4
  import type {
3
5
  AIEventDefs,
4
6
  ApprovalRequestedPayload,
@@ -17,26 +19,32 @@ import type { Contract, ContractEvent, EventDefs } from './contract.ts'
17
19
  import type { Reducer } from './reducer.ts'
18
20
 
19
21
  export type CoordinatorCall = {
20
- index: number
21
22
  call: ToolCalledPayload
22
23
  approval?: ApprovalRequestedPayload
23
24
  response?: ApprovalRespondedPayload
24
- responseIndex?: number
25
25
  terminal: boolean
26
+ work?: ControlWork
26
27
  }
27
28
 
28
29
  export type CoordinatorResponse = {
29
30
  rootMessageId: string
30
31
  responseMessageId: string
31
32
  status: 'requested' | 'generating' | 'waiting' | 'failed'
33
+ stepCount: number
32
34
  activeRequestId?: string
33
35
  generation?: GenerationStartedPayload
36
+ promptThroughIndex?: number
37
+ source?: {
38
+ generation: GenerationStartedPayload
39
+ promptThroughIndex: number
40
+ failed: boolean
41
+ canCompact: boolean
42
+ }
34
43
  completion?: GenerationCompletedPayload
35
44
  failure?: GenerationFailedPayload
36
45
  calls: CoordinatorCall[]
37
46
  inputs: InputRequestedPayload[]
38
47
  inputResponse?: {
39
- index: number
40
48
  generationId: string
41
49
  inputId: string
42
50
  name: string
@@ -45,8 +53,9 @@ export type CoordinatorResponse = {
45
53
 
46
54
  export type AICoordinatorState = {
47
55
  closed: boolean
48
- queued: Array<{ index: number; messageId: string; generate: boolean }>
56
+ queued: Array<{ messageId: string; generate: boolean }>
49
57
  response?: CoordinatorResponse
58
+ calibration?: { model: string; inputTokens: number; estimate: number }
50
59
  }
51
60
 
52
61
  const updateResponse = (
@@ -57,13 +66,18 @@ const updateResponse = (
57
66
  ? state
58
67
  : { ...state, response: update(state.response) }
59
68
 
60
- const foldCoordinator = <M extends UIMessage, D extends EventDefs>(
69
+ export const foldCoordinator = <M extends UIMessage, D extends EventDefs>(
61
70
  state: AICoordinatorState,
62
71
  event: ContractEvent<D>,
63
72
  ): AICoordinatorState => {
64
73
  switch (event.type) {
65
74
  case 'ai.session.closed':
66
75
  return { closed: true, queued: [] }
76
+ case 'ai.retry.requested':
77
+ case 'ai.compaction.completed': {
78
+ const { calibration: _, ...rest } = state
79
+ return rest
80
+ }
67
81
  case 'ai.message.created': {
68
82
  const payload = event.payload as MessageCreatedPayload<M>
69
83
  const message = payload.message
@@ -73,11 +87,10 @@ const foldCoordinator = <M extends UIMessage, D extends EventDefs>(
73
87
  queued: [
74
88
  ...state.queued.filter((item) => item.messageId !== message.id),
75
89
  {
76
- index: event.index,
77
90
  messageId: message.id,
78
91
  generate: payload.generate !== false,
79
92
  },
80
- ].toSorted((left, right) => left.index - right.index),
93
+ ],
81
94
  }
82
95
  }
83
96
  case 'ai.generation.requested': {
@@ -85,21 +98,22 @@ const foldCoordinator = <M extends UIMessage, D extends EventDefs>(
85
98
  if (request.reason === 'message') {
86
99
  const responseMessageId =
87
100
  request.responseMessageId ?? `${request.messageId}:assistant`
88
- const requested = state.queued.find(
101
+ const requested = state.queued.findIndex(
89
102
  (item) => item.messageId === request.messageId,
90
103
  )
91
104
  return {
92
105
  ...state,
93
106
  queued:
94
- requested === undefined
107
+ requested < 0
95
108
  ? state.queued.filter(
96
109
  (item) => item.messageId !== request.messageId,
97
110
  )
98
- : state.queued.filter((item) => item.index > requested.index),
111
+ : state.queued.slice(requested + 1),
99
112
  response: {
100
113
  rootMessageId: request.messageId,
101
114
  responseMessageId,
102
115
  status: 'requested',
116
+ stepCount: 0,
103
117
  activeRequestId: event.id,
104
118
  calls: [],
105
119
  inputs: [],
@@ -123,14 +137,31 @@ const foldCoordinator = <M extends UIMessage, D extends EventDefs>(
123
137
  ) {
124
138
  return state
125
139
  }
126
- return updateResponse(state, (response) => ({
127
- rootMessageId: response.rootMessageId,
128
- responseMessageId: response.responseMessageId,
129
- status: 'requested',
130
- activeRequestId: event.id,
131
- calls: [],
132
- inputs: [],
133
- }))
140
+ return updateResponse(state, (response) => {
141
+ const generation = response.generation ?? response.source?.generation
142
+ const promptThroughIndex =
143
+ response.promptThroughIndex ?? response.source?.promptThroughIndex
144
+ return {
145
+ rootMessageId: response.rootMessageId,
146
+ responseMessageId: response.responseMessageId,
147
+ status: 'requested',
148
+ stepCount: response.stepCount,
149
+ activeRequestId: event.id,
150
+ ...(generation === undefined || promptThroughIndex === undefined
151
+ ? {}
152
+ : {
153
+ source: {
154
+ generation,
155
+ promptThroughIndex,
156
+ failed:
157
+ response.failure?.generationId === generation.generationId,
158
+ canCompact: response.calls.every((call) => call.terminal),
159
+ },
160
+ }),
161
+ calls: [],
162
+ inputs: [],
163
+ }
164
+ })
134
165
  }
135
166
  case 'ai.generation.started': {
136
167
  const generation = event.payload as GenerationStartedPayload
@@ -146,7 +177,12 @@ const foldCoordinator = <M extends UIMessage, D extends EventDefs>(
146
177
  rootMessageId: response.rootMessageId,
147
178
  responseMessageId: response.responseMessageId,
148
179
  status: 'generating',
180
+ stepCount: response.stepCount,
149
181
  generation,
182
+ ...(generation.promptThroughIndex === undefined
183
+ ? {}
184
+ : { promptThroughIndex: generation.promptThroughIndex }),
185
+ ...(response.source === undefined ? {} : { source: response.source }),
150
186
  activeRequestId: generation.requestId,
151
187
  calls: [],
152
188
  inputs: [],
@@ -159,11 +195,29 @@ const foldCoordinator = <M extends UIMessage, D extends EventDefs>(
159
195
  ) {
160
196
  return state
161
197
  }
162
- return updateResponse(state, (response) => ({
198
+ const next = updateResponse(state, (response) => ({
163
199
  ...response,
164
200
  status: 'waiting',
165
201
  completion,
202
+ stepCount:
203
+ response.stepCount + (response.completion === undefined ? 1 : 0),
166
204
  }))
205
+ const { calibration: _, ...uncalibrated } = next
206
+ const inputTokens = completion.usage?.inputTokens
207
+ const estimate = completion.inputTokenEstimate
208
+ return inputTokens !== undefined &&
209
+ Number.isFinite(inputTokens) &&
210
+ inputTokens >= 0 &&
211
+ estimate !== undefined
212
+ ? {
213
+ ...uncalibrated,
214
+ calibration: {
215
+ model: state.response.generation.model,
216
+ inputTokens,
217
+ estimate,
218
+ },
219
+ }
220
+ : uncalibrated
167
221
  }
168
222
  case 'ai.generation.failed': {
169
223
  const failure = event.payload as GenerationFailedPayload
@@ -186,18 +240,21 @@ const foldCoordinator = <M extends UIMessage, D extends EventDefs>(
186
240
  }
187
241
  case 'ai.message.completed': {
188
242
  const messageId = (event.payload as { messageId: string }).messageId
189
- return state.response?.responseMessageId === messageId
190
- ? { closed: state.closed, queued: state.queued }
191
- : state
243
+ if (state.response?.responseMessageId !== messageId) return state
244
+ const { response: _, ...rest } = state
245
+ return rest
192
246
  }
193
247
  case 'ai.message.interrupted': {
194
248
  const interruption = event.payload as MessageInterruptedPayload
195
249
  const response = state.response
196
- return response?.responseMessageId === interruption.messageId &&
197
- (interruption.generationId === response.generation?.generationId ||
198
- interruption.requestId === response.activeRequestId)
199
- ? { closed: state.closed, queued: state.queued }
200
- : state
250
+ if (
251
+ response?.responseMessageId !== interruption.messageId ||
252
+ (interruption.generationId !== response.generation?.generationId &&
253
+ interruption.requestId !== response.activeRequestId)
254
+ )
255
+ return state
256
+ const { response: _, ...rest } = state
257
+ return rest
201
258
  }
202
259
  case 'ai.tool.called': {
203
260
  const call = event.payload as ToolCalledPayload
@@ -210,7 +267,7 @@ const foldCoordinator = <M extends UIMessage, D extends EventDefs>(
210
267
  ...response.calls.filter(
211
268
  (candidate) => candidate.call.toolCallId !== call.toolCallId,
212
269
  ),
213
- { index: event.index, call, terminal: false },
270
+ { call, terminal: false },
214
271
  ],
215
272
  }))
216
273
  }
@@ -240,7 +297,7 @@ const foldCoordinator = <M extends UIMessage, D extends EventDefs>(
240
297
  ...response,
241
298
  calls: response.calls.map((candidate) =>
242
299
  candidate.approval?.approvalId === approval.approvalId
243
- ? { ...candidate, response: approval, responseIndex: event.index }
300
+ ? { ...candidate, response: approval }
244
301
  : candidate,
245
302
  ),
246
303
  }))
@@ -310,7 +367,6 @@ const foldCoordinator = <M extends UIMessage, D extends EventDefs>(
310
367
  (candidate) => candidate.inputId !== input.inputId,
311
368
  ),
312
369
  inputResponse: {
313
- index: event.index,
314
370
  generationId: input.generationId,
315
371
  inputId: input.inputId,
316
372
  name: requested.name,
@@ -330,13 +386,24 @@ export const aiCoordinatorReducer = <
330
386
  ): Reducer<D, AICoordinatorState> =>
331
387
  contract
332
388
  .reducer({
333
- name: 'a2.ai.coordinator.v3',
389
+ name: 'a2.ai.coordinator.v5',
334
390
  initialState: {
335
391
  closed: false,
336
392
  queued: [],
337
393
  } as AICoordinatorState,
338
394
  })
339
- .fold((state, event) => foldCoordinator<M, D>(state, event))
395
+ .fold((state, event) =>
396
+ event.type === 'ai.control.committed'
397
+ ? applyControlChanges({
398
+ state,
399
+ changes: (event.payload as ControlCommit).changes
400
+ .filter((change) => change.path[0] === 'coordinator')
401
+ .map((change) =>
402
+ Object.assign({}, change, { path: change.path.slice(1) }),
403
+ ),
404
+ })
405
+ : state,
406
+ )
340
407
 
341
408
  export const continuationReady = (state: AICoordinatorState): boolean => {
342
409
  const response = state.response
@@ -0,0 +1,68 @@
1
+ import type { UIMessageChunk } from 'ai'
2
+
3
+ export type ProgressBatch = { index: number; chunks: UIMessageChunk[] }
4
+ type BatchBlock = ProgressBatch[] | { left: BatchBlock; right: BatchBlock }
5
+
6
+ export type ProgressBatches = {
7
+ length: number
8
+ tail: ProgressBatch[]
9
+ blocks: Array<BatchBlock | null>
10
+ }
11
+
12
+ const PAGE_SIZE = 64
13
+
14
+ export const appendProgressBatch = ({
15
+ batches,
16
+ batch,
17
+ }: {
18
+ batches: ProgressBatches
19
+ batch: ProgressBatch
20
+ }): ProgressBatches => {
21
+ if (batches.tail.length < PAGE_SIZE)
22
+ return {
23
+ ...batches,
24
+ length: batches.length + 1,
25
+ tail: [...batches.tail, batch],
26
+ }
27
+
28
+ const blocks = [...batches.blocks]
29
+ let block: BatchBlock = batches.tail
30
+ let level = 0
31
+ for (;;) {
32
+ const left = blocks[level]
33
+ if (left == null) break
34
+ block = { left, right: block }
35
+ blocks[level] = null
36
+ level += 1
37
+ }
38
+ blocks[level] = block
39
+ return { length: batches.length + 1, tail: [batch], blocks }
40
+ }
41
+
42
+ export const progressBatches = (
43
+ items: readonly ProgressBatch[] = [],
44
+ ): ProgressBatches => {
45
+ let batches: ProgressBatches = { length: 0, tail: [], blocks: [] }
46
+ for (const batch of items) batches = appendProgressBatch({ batches, batch })
47
+ return batches
48
+ }
49
+
50
+ export const flattenProgressBatches = (
51
+ batches: ProgressBatches,
52
+ ): ProgressBatch[] => {
53
+ const items: ProgressBatch[] = []
54
+ const visit = (block: BatchBlock): void => {
55
+ if (Array.isArray(block)) items.push(...block)
56
+ else {
57
+ visit(block.left)
58
+ visit(block.right)
59
+ }
60
+ }
61
+ // Level n holds 2^n completed pages; higher levels contain older batches.
62
+ for (let level = batches.blocks.length - 1; level >= 0; level -= 1) {
63
+ const block = batches.blocks[level]
64
+ if (block) visit(block)
65
+ }
66
+ items.push(...batches.tail)
67
+ return items
68
+ }