experimental-a2 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/CHANGELOG.md +47 -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 +16 -8
  17. package/dist/ai-server.d.ts.map +1 -1
  18. package/dist/ai-server.js +1322 -475
  19. package/dist/ai-server.js.map +1 -1
  20. package/dist/ai.d.ts +2 -334
  21. package/dist/ai.js +838 -85
  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 +361 -76
  64. package/docs/reference/01-api.mdx +159 -27
  65. package/examples/playground/app/agent/[agentId]/agent-client.tsx +145 -33
  66. package/examples/playground/app/agent/[agentId]/agent-queue.tsx +289 -0
  67. package/examples/playground/app/agent/[agentId]/compaction/route.ts +14 -0
  68. package/examples/playground/app/agent/[agentId]/compaction-event.tsx +38 -0
  69. package/examples/playground/app/agent/[agentId]/compaction-panel.tsx +294 -0
  70. package/examples/playground/app/agent/compaction-settings.test.ts +144 -0
  71. package/examples/playground/app/agent/compaction-settings.ts +49 -0
  72. package/examples/playground/app/agent/compaction-timeline.test.ts +337 -0
  73. package/examples/playground/app/agent/compaction-timeline.ts +198 -0
  74. package/examples/playground/app/agent/model.ts +56 -1
  75. package/examples/playground/app/agent/server.ts +9 -2
  76. package/examples/playground/app/chat/[chatId]/chat-client.tsx +3 -13
  77. package/examples/playground/app/chat/model.ts +2 -2
  78. package/examples/playground/app/chat/server.ts +24 -17
  79. package/examples/playground/app/globals.css +333 -0
  80. package/examples/playground/package.json +1 -1
  81. package/package.json +1 -1
  82. package/src/ai-client-state.ts +185 -0
  83. package/src/ai-control-server.ts +829 -0
  84. package/src/ai-control-state.ts +152 -0
  85. package/src/ai-control.ts +139 -0
  86. package/src/ai-coordinator.ts +99 -32
  87. package/src/ai-model-metadata.ts +108 -0
  88. package/src/ai-progress-batches.ts +68 -0
  89. package/src/ai-projector.ts +76 -15
  90. package/src/ai-sdk-step.ts +5 -2
  91. package/src/ai-server.ts +920 -638
  92. package/src/ai.ts +650 -110
  93. package/src/client.ts +31 -9
  94. package/src/licenses/Apache-2.0.txt +55 -0
  95. package/src/parse-partial-json.ts +441 -0
  96. package/src/reducer.ts +6 -0
  97. package/src/server.ts +8 -4
  98. package/src/store-postgres.ts +27 -0
  99. package/src/store-redis-core.ts +53 -1
  100. package/src/store-redis-notify.ts +1 -0
  101. package/src/store.ts +6 -0
  102. package/dist/ai.d.ts.map +0 -1
  103. package/dist/client-Bf6uSEAk.js.map +0 -1
  104. package/dist/contract-48bUMgcL.js.map +0 -1
  105. package/dist/server-BeNADlCI.js.map +0 -1
  106. package/dist/store-redis-notify-BUCyXOn0.js.map +0 -1
package/src/ai.ts CHANGED
@@ -13,19 +13,54 @@ import type {
13
13
  UIMessage,
14
14
  UIMessageChunk,
15
15
  } from 'ai'
16
+ import {
17
+ isQueueCommand,
18
+ projectAIClientState,
19
+ type PendingQueueCommand,
20
+ } from './ai-client-state.ts'
21
+ export type { PendingQueueCommand } from './ai-client-state.ts'
22
+ import { applyControlChanges } from './ai-control-state.ts'
23
+ import { initialControlState } from './ai-control.ts'
24
+ import type {
25
+ ControlCommit,
26
+ ActiveTurn,
27
+ ControlCommand,
28
+ ControlReceipt,
29
+ InboxItem,
30
+ ToolExecutionRequest,
31
+ WorkReport,
32
+ } from './ai-control.ts'
33
+ export type {
34
+ ActiveTurn,
35
+ ControlCommand,
36
+ ControlReceipt,
37
+ InboxItem,
38
+ } from './ai-control.ts'
16
39
  import { contract as createContract } from './contract.ts'
17
- import type { AppendInput, Contract, EventDefs } from './contract.ts'
18
- import { interruptUIMessage, projectUIMessage } from './ai-projector.ts'
40
+ import type { Contract, EventDefs } from './contract.ts'
41
+ import {
42
+ interruptUIMessage,
43
+ pauseUIMessage,
44
+ projectUIMessage,
45
+ projectUIMessageStream,
46
+ type UIMessageCursor,
47
+ } from './ai-projector.ts'
19
48
  import { ambientToolScopeStorage } from './internal.ts'
20
- import type { Reducer } from './reducer.ts'
49
+ import { clientStateProjection, type Reducer } from './reducer.ts'
50
+ import {
51
+ appendProgressBatch,
52
+ flattenProgressBatches,
53
+ progressBatches,
54
+ type ProgressBatches,
55
+ } from './ai-progress-batches.ts'
21
56
  import type { HandlerContext } from './server.ts'
22
57
  import type { StandardSchemaV1 } from './standard-schema.ts'
23
58
  import { assertSyncSchema, validateSync } from './validate.ts'
24
59
 
25
60
  export type AgentStatus =
26
- 'idle' | 'generating' | 'waiting' | 'failed' | 'closed'
61
+ 'idle' | 'generating' | 'waiting' | 'failed' | 'closed' | 'paused'
27
62
 
28
- export type GenerationReason = 'message' | 'tool' | 'input' | 'retry'
63
+ export type GenerationReason = 'message' | 'tool' | 'input' | 'retry' | 'resume'
29
64
 
30
65
  export type SessionCreatedPayload = { metadata?: JSONValue }
31
66
  export type SessionClosedPayload = { reason?: string }
@@ -47,6 +82,7 @@ export type MessageInterruptedPayload = MessageInterruption &
47
82
  | { requestId: string; generationId?: never }
48
83
  )
49
84
  export type GenerationRequestedPayload = {
85
+ control?: { turnId: string; version: number }
50
86
  messageId: string
51
87
  reason: GenerationReason
52
88
  responseMessageId?: string
@@ -64,6 +100,7 @@ export type GenerationStartedPayload = {
64
100
  responseMessageId: string
65
101
  attempt: number
66
102
  model: string
103
+ promptThroughIndex?: number
67
104
  }
68
105
  export type GenerationProgressPayload = {
69
106
  requestId: string
@@ -80,6 +117,7 @@ export type GenerationCompletedPayload = {
80
117
  responseMessageId: string
81
118
  finishReason?: FinishReason
82
119
  usage?: LanguageModelUsage
120
+ inputTokenEstimate?: number
83
121
  }
84
122
  export type GenerationFailedPayload = {
85
123
  requestId: string
@@ -152,18 +190,45 @@ export type InputRespondedPayload = {
152
190
  name: string
153
191
  value: JSONValue
154
192
  }
193
+ export type ModelLimits = {
194
+ contextWindow: number
195
+ maxOutputTokens: number
196
+ }
197
+ export type ModelMetadataRequestedPayload = { modelId: string }
198
+ export type ModelMetadataResolvedPayload = {
199
+ modelId: string
200
+ limits: ModelLimits | null
201
+ }
202
+ export type ModelMetadataState =
203
+ | { status: 'pending' }
204
+ | { status: 'resolved'; limits: ModelLimits }
205
+ | { status: 'unavailable' }
206
+
155
207
  export type CompactionRequestedPayload = {
156
208
  generationId: string
157
209
  throughMessageId: string
210
+ throughIndex?: number
158
211
  }
159
212
  export type CompactionCompletedPayload<M extends UIMessage> = {
160
213
  generationId: string
161
214
  throughMessageId: string
215
+ throughIndex?: number
162
216
  messages: M[]
163
217
  retainedMessageIds?: string[]
218
+ summary?: string
219
+ usage?: LanguageModelUsage
164
220
  }
165
221
 
166
222
  export type AIEventDefs<M extends UIMessage = UIMessage> = {
223
+ 'ai.control.requested': StandardSchemaV1<ControlCommand<M>>
224
+ 'ai.control.committed': StandardSchemaV1<ControlCommit>
225
+ 'ai.control.decided': StandardSchemaV1<ControlReceipt>
226
+ 'ai.work.reported': StandardSchemaV1<WorkReport>
227
+ 'ai.tool.execution.requested': StandardSchemaV1<ToolExecutionRequest>
228
+ 'ai.generation.paused': StandardSchemaV1<{
229
+ generationId: string
230
+ messageId: string
231
+ }>
167
232
  'ai.session.created': StandardSchemaV1<SessionCreatedPayload>
168
233
  'ai.session.closed': StandardSchemaV1<SessionClosedPayload>
169
234
  'ai.message.created': StandardSchemaV1<MessageCreatedPayload<M>>
@@ -183,6 +248,8 @@ export type AIEventDefs<M extends UIMessage = UIMessage> = {
183
248
  'ai.input.responded': StandardSchemaV1<InputRespondedPayload>
184
249
  'ai.compaction.requested': StandardSchemaV1<CompactionRequestedPayload>
185
250
  'ai.compaction.completed': StandardSchemaV1<CompactionCompletedPayload<M>>
251
+ 'ai.model.metadata.requested': StandardSchemaV1<ModelMetadataRequestedPayload>
252
+ 'ai.model.metadata.resolved': StandardSchemaV1<ModelMetadataResolvedPayload>
186
253
  }
187
254
 
188
255
  export type ActiveGeneration = GenerationStartedPayload
@@ -193,8 +260,10 @@ export type ActiveGenerationProjection<M extends UIMessage = UIMessage> = {
193
260
  /** Log index of the completed model step while its response remains interruptible. */
194
261
  completionIndex?: number
195
262
  baseMessage?: M
263
+ promptThroughIndex?: number
264
+ cursor?: { throughIndex: number; stream: UIMessageCursor } | null
196
265
  baseCompaction?: CompactionState<M> | null
197
- batches: Array<{ index: number; chunks: UIMessageChunk[] }>
266
+ batches: ProgressBatches
198
267
  baseTools: ToolActivity[]
199
268
  toolEvents: Array<
200
269
  | { index: number; type: 'ai.tool.called'; payload: ToolCalledPayload }
@@ -242,8 +311,11 @@ export type CompactionState<M extends UIMessage> = {
242
311
  status: 'running' | 'completed'
243
312
  generationId: string
244
313
  throughMessageId: string
314
+ throughIndex?: number
245
315
  messages?: M[]
246
316
  retainedMessageIds?: string[]
317
+ summary?: string
318
+ usage?: LanguageModelUsage
247
319
  }
248
320
 
249
321
  export type GenerationUsage = {
@@ -252,6 +324,12 @@ export type GenerationUsage = {
252
324
  }
253
325
 
254
326
  export type AIState<M extends UIMessage = UIMessage> = {
327
+ inbox: { paused: boolean; items: InboxItem<M>[] }
328
+ pendingQueueCommands?: PendingQueueCommand<M>[]
329
+ active: ActiveTurn<M> | null
330
+ starting: boolean
331
+ receipt: ControlReceipt | null
332
+ pausedGenerationIds?: Record<string, true>
255
333
  session: { created: boolean; metadata?: JSONValue }
256
334
  messages: M[]
257
335
  status: AgentStatus
@@ -269,6 +347,7 @@ export type AIState<M extends UIMessage = UIMessage> = {
269
347
  pendingInputs: PendingInput[]
270
348
  tools: ToolActivity[]
271
349
  compaction: CompactionState<M> | null
350
+ modelMetadata: Record<string, ModelMetadataState>
272
351
  usage: GenerationUsage[]
273
352
  error: string | null
274
353
  }
@@ -282,12 +361,13 @@ export type AgentDefinition<
282
361
  }
283
362
 
284
363
  export type AgentToolEventDefs = EventDefs &
285
- Pick<AIEventDefs, 'ai.tool.called' | 'ai.approval.responded'>
364
+ Pick<AIEventDefs, 'ai.tool.execution.requested'>
286
365
 
287
366
  /** The durable handler context supplied when A2 executes a local AI tool. */
288
- export type AgentToolContext<D extends AgentToolEventDefs> =
289
- | HandlerContext<D, 'ai.tool.called'>
290
- | HandlerContext<D, 'ai.approval.responded'>
367
+ export type AgentToolContext<D extends AgentToolEventDefs> = HandlerContext<
368
+ D,
369
+ 'ai.tool.execution.requested'
370
+ >
291
371
 
292
372
  export type AgentOptions<M extends UIMessage, E extends EventDefs> = {
293
373
  name: string
@@ -373,6 +453,15 @@ const jsonObject = <T extends object>(
373
453
  const stringField = (value: Record<string, unknown>, key: string): boolean =>
374
454
  typeof value[key] === 'string' && value[key].length > 0
375
455
 
456
+ const optionalIndexField = (
457
+ value: Record<string, unknown>,
458
+ key: string,
459
+ ): boolean =>
460
+ value[key] === undefined ||
461
+ (typeof value[key] === 'number' &&
462
+ Number.isSafeInteger(value[key]) &&
463
+ value[key] >= 0)
464
+
376
465
  const optionalStringField = (
377
466
  value: Record<string, unknown>,
378
467
  key: string,
@@ -449,6 +538,7 @@ const completedPayloadSchema: StandardSchemaV1<GenerationCompletedPayload> =
449
538
  !stringField(value, 'messageId') ||
450
539
  !stringField(value, 'generationId') ||
451
540
  !stringField(value, 'responseMessageId') ||
541
+ !optionalIndexField(value, 'inputTokenEstimate') ||
452
542
  !optionalStringField(value, 'finishReason')
453
543
  ) {
454
544
  return issue('invalid ai.generation.completed payload')
@@ -465,6 +555,8 @@ const compactionPayloadSchema = <M extends UIMessage>(
465
555
  !isJSONCompatible(value) ||
466
556
  !stringField(value, 'generationId') ||
467
557
  !stringField(value, 'throughMessageId') ||
558
+ !optionalIndexField(value, 'throughIndex') ||
559
+ !optionalStringField(value, 'summary') ||
468
560
  !Array.isArray(value['messages']) ||
469
561
  (value['retainedMessageIds'] !== undefined &&
470
562
  (!Array.isArray(value['retainedMessageIds']) ||
@@ -484,7 +576,16 @@ const compactionPayloadSchema = <M extends UIMessage>(
484
576
  value: {
485
577
  generationId: value['generationId'] as string,
486
578
  throughMessageId: value['throughMessageId'] as string,
579
+ ...(value['throughIndex'] === undefined
580
+ ? {}
581
+ : { throughIndex: value['throughIndex'] as number }),
487
582
  messages,
583
+ ...(value['summary'] === undefined
584
+ ? {}
585
+ : { summary: value['summary'] as string }),
586
+ ...(value['usage'] === undefined
587
+ ? {}
588
+ : { usage: value['usage'] as LanguageModelUsage }),
488
589
  ...(value['retainedMessageIds'] === undefined
489
590
  ? {}
490
591
  : { retainedMessageIds: value['retainedMessageIds'] as string[] }),
@@ -498,6 +599,143 @@ export function createEvents<M extends UIMessage = UIMessage>(options?: {
498
599
  const messageSchema = options?.messageSchema
499
600
  if (messageSchema) assertSyncSchema(messageSchema, 'the AI message schema')
500
601
  const definition: AIEventDefs<M> = {
602
+ 'ai.control.requested': schema('ai.control.requested', (value) => {
603
+ if (!isRecord(value) || !isJSONCompatible(value))
604
+ return issue('invalid AI command')
605
+ const action = value['action']
606
+ const forwarded = {
607
+ approval: ['ai.approval.responded', 'response'],
608
+ 'tool-result': ['ai.tool.result', 'result'],
609
+ input: ['ai.input.responded', 'response'],
610
+ 'request-input': ['ai.input.requested', 'request'],
611
+ retry: ['ai.retry.requested', 'request'],
612
+ interrupt: ['ai.message.interrupted', 'request'],
613
+ } as const
614
+ if (typeof action === 'string' && Object.hasOwn(forwarded, action)) {
615
+ const [type, key] = forwarded[action as keyof typeof forwarded]
616
+ const payload = validateSync(
617
+ definition[type] as StandardSchemaV1<unknown>,
618
+ value[key],
619
+ 'AI command',
620
+ )
621
+ if (payload.issues) return { issues: payload.issues }
622
+ return { value: { action, [key]: payload.value } as ControlCommand<M> }
623
+ }
624
+ if (action === 'send' || action === 'edit' || action === 'steer') {
625
+ const parsed = validateMessage(value['message'], messageSchema)
626
+ if (parsed.issues) return parsed
627
+ if (
628
+ (action === 'edit' &&
629
+ (!Number.isSafeInteger(value['expectedRevision']) ||
630
+ Number(value['expectedRevision']) < 0)) ||
631
+ (action === 'steer' &&
632
+ (!stringField(value, 'turnId') ||
633
+ !optionalIndexField(value, 'lastSeenIndex'))) ||
634
+ (action === 'send' &&
635
+ value['generate'] !== undefined &&
636
+ typeof value['generate'] !== 'boolean')
637
+ )
638
+ return issue('invalid AI command')
639
+ return {
640
+ value: { ...value, message: parsed.value } as ControlCommand<M>,
641
+ }
642
+ }
643
+ if (
644
+ (action === 'remove' && stringField(value, 'inputId')) ||
645
+ (action === 'send-now' &&
646
+ stringField(value, 'inputId') &&
647
+ (value['turnId'] === null || stringField(value, 'turnId')) &&
648
+ optionalIndexField(value, 'lastSeenIndex')) ||
649
+ (action === 'move' &&
650
+ stringField(value, 'inputId') &&
651
+ (value['beforeId'] === null || stringField(value, 'beforeId'))) ||
652
+ (action === 'stop' &&
653
+ stringField(value, 'turnId') &&
654
+ optionalIndexField(value, 'lastSeenIndex')) ||
655
+ (action === 'pause' &&
656
+ (value['when'] === 'now' || value['when'] === 'after-turn')) ||
657
+ action === 'resume'
658
+ )
659
+ return { value: value as ControlCommand<M> }
660
+ return issue('invalid AI command')
661
+ }),
662
+ 'ai.control.committed': jsonObject(
663
+ 'ai.control.committed',
664
+ (value) => Array.isArray(value['changes']) && isRecord(value['view']),
665
+ ),
666
+ 'ai.control.decided': jsonObject(
667
+ 'ai.control.decided',
668
+ (value) =>
669
+ stringField(value, 'commandId') &&
670
+ (value['outcome'] === 'applied' || value['outcome'] === 'rejected'),
671
+ ),
672
+ 'ai.work.reported': schema('ai.work.reported', (value) => {
673
+ if (
674
+ !isRecord(value) ||
675
+ !isJSONCompatible(value) ||
676
+ !stringField(value, 'workId') ||
677
+ !stringField(value, 'turnId') ||
678
+ typeof value['version'] !== 'number' ||
679
+ !optionalIndexField(value, 'version') ||
680
+ typeof value['attempt'] !== 'number' ||
681
+ !Number.isSafeInteger(value['attempt']) ||
682
+ value['attempt'] < 1 ||
683
+ typeof value['settled'] !== 'boolean' ||
684
+ (value['kind'] !== 'model' && value['kind'] !== 'tool') ||
685
+ !Array.isArray(value['events'])
686
+ )
687
+ return issue('invalid AI work report')
688
+ const allowed = new Set([
689
+ 'ai.generation.started',
690
+ 'ai.generation.completed',
691
+ 'ai.generation.failed',
692
+ 'ai.tool.called',
693
+ 'ai.tool.result',
694
+ 'ai.approval.requested',
695
+ 'ai.approval.responded',
696
+ 'ai.compaction.requested',
697
+ 'ai.compaction.completed',
698
+ 'ai.model.metadata.requested',
699
+ ])
700
+ const events: WorkReport['events'] = []
701
+ for (const event of value['events']) {
702
+ if (
703
+ !isRecord(event) ||
704
+ !stringField(event, 'id') ||
705
+ typeof event['type'] !== 'string' ||
706
+ !allowed.has(event['type']) ||
707
+ (value['kind'] === 'tool' && event['type'] !== 'ai.tool.result')
708
+ )
709
+ return issue('invalid AI work fact')
710
+ const parsed = validateSync(
711
+ definition[
712
+ event['type'] as keyof AIEventDefs<M>
713
+ ] as StandardSchemaV1<unknown>,
714
+ event['payload'],
715
+ 'AI work fact',
716
+ )
717
+ if (parsed.issues) return { issues: parsed.issues }
718
+ events.push({
719
+ id: event['id'] as string,
720
+ type: event['type'],
721
+ payload: parsed.value,
722
+ })
723
+ }
724
+ return { value: { ...value, events } as WorkReport }
725
+ }),
726
+ 'ai.tool.execution.requested': jsonObject(
727
+ 'ai.tool.execution.requested',
728
+ (value) =>
729
+ stringField(value, 'turnId') &&
730
+ optionalIndexField(value, 'version') &&
731
+ isRecord(value['call']) &&
732
+ isRecord(value['generation']),
733
+ ),
734
+ 'ai.generation.paused': jsonObject(
735
+ 'ai.generation.paused',
736
+ (value) =>
737
+ stringField(value, 'generationId') && stringField(value, 'messageId'),
738
+ ),
501
739
  'ai.session.created': jsonObject('ai.session.created', () => true),
502
740
  'ai.session.closed': jsonObject('ai.session.closed', (value) =>
503
741
  optionalStringField(value, 'reason'),
@@ -528,7 +766,8 @@ export function createEvents<M extends UIMessage = UIMessage>(options?: {
528
766
  (value['reason'] === 'message' ||
529
767
  value['reason'] === 'tool' ||
530
768
  value['reason'] === 'input' ||
531
- value['reason'] === 'retry'),
769
+ value['reason'] === 'retry' ||
770
+ value['reason'] === 'resume'),
532
771
  ),
533
772
  'ai.retry.requested': jsonObject(
534
773
  'ai.retry.requested',
@@ -547,7 +786,8 @@ export function createEvents<M extends UIMessage = UIMessage>(options?: {
547
786
  typeof value['attempt'] === 'number' &&
548
787
  Number.isInteger(value['attempt']) &&
549
788
  value['attempt'] > 0 &&
550
- stringField(value, 'model'),
789
+ stringField(value, 'model') &&
790
+ optionalIndexField(value, 'promptThroughIndex'),
551
791
  ),
552
792
  'ai.generation.progress': progressPayloadSchema,
553
793
  'ai.generation.completed': completedPayloadSchema,
@@ -641,9 +881,27 @@ export function createEvents<M extends UIMessage = UIMessage>(options?: {
641
881
  'ai.compaction.requested',
642
882
  (value) =>
643
883
  stringField(value, 'generationId') &&
644
- stringField(value, 'throughMessageId'),
884
+ stringField(value, 'throughMessageId') &&
885
+ optionalIndexField(value, 'throughIndex'),
645
886
  ),
646
887
  'ai.compaction.completed': compactionPayloadSchema(messageSchema),
888
+ 'ai.model.metadata.requested': jsonObject(
889
+ 'ai.model.metadata.requested',
890
+ (value) => stringField(value, 'modelId'),
891
+ ),
892
+ 'ai.model.metadata.resolved': jsonObject(
893
+ 'ai.model.metadata.resolved',
894
+ (value) =>
895
+ stringField(value, 'modelId') &&
896
+ (value['limits'] === null ||
897
+ (isRecord(value['limits']) &&
898
+ typeof value['limits']['contextWindow'] === 'number' &&
899
+ Number.isSafeInteger(value['limits']['contextWindow']) &&
900
+ value['limits']['contextWindow'] > 0 &&
901
+ typeof value['limits']['maxOutputTokens'] === 'number' &&
902
+ Number.isSafeInteger(value['limits']['maxOutputTokens']) &&
903
+ value['limits']['maxOutputTokens'] > 0)),
904
+ ),
647
905
  }
648
906
  return definition
649
907
  }
@@ -651,6 +909,11 @@ export function createEvents<M extends UIMessage = UIMessage>(options?: {
651
909
  export const events: AIEventDefs<UIMessage> = createEvents()
652
910
 
653
911
  const initialState = <M extends UIMessage>(): AIState<M> => ({
912
+ inbox: initialControlState<M>().inbox,
913
+ pendingQueueCommands: [],
914
+ active: null,
915
+ starting: false,
916
+ receipt: null,
654
917
  session: { created: false },
655
918
  messages: [],
656
919
  status: 'idle',
@@ -663,6 +926,7 @@ const initialState = <M extends UIMessage>(): AIState<M> => ({
663
926
  pendingInputs: [],
664
927
  tools: [],
665
928
  compaction: null,
929
+ modelMetadata: {},
666
930
  usage: [],
667
931
  error: null,
668
932
  })
@@ -675,6 +939,23 @@ const upsertMessage = <M extends UIMessage>(messages: M[], message: M): M[] => {
675
939
  return next
676
940
  }
677
941
 
942
+ const upsertResponse = <M extends UIMessage>(options: {
943
+ messages: M[]
944
+ message: M
945
+ inputId: string
946
+ }): M[] => {
947
+ const { messages, message, inputId } = options
948
+ if (messages.some((item) => item.id === message.id))
949
+ return upsertMessage(messages, message)
950
+ const inputIndex = messages.findIndex((item) => item.id === inputId)
951
+ if (inputIndex === -1) return [...messages, message]
952
+ return [
953
+ ...messages.slice(0, inputIndex + 1),
954
+ message,
955
+ ...messages.slice(inputIndex + 1),
956
+ ]
957
+ }
958
+
678
959
  const answerApproval = <M extends UIMessage>(
679
960
  messages: M[],
680
961
  response: ApprovalRespondedPayload,
@@ -826,12 +1107,10 @@ const isStaleGeneration = <M extends UIMessage>(
826
1107
  return current !== undefined && current !== generationId
827
1108
  }
828
1109
 
829
- const projectedMessage = <M extends UIMessage>(
1110
+ const projectedMessageStream = <M extends UIMessage>(
830
1111
  projection: ActiveGenerationProjection<M>,
831
- ): M => {
832
- const projectionEvents: Array<{ index: number; chunks: UIMessageChunk[] }> = [
833
- ...projection.batches,
834
- ]
1112
+ ): { message: M; cursor: UIMessageCursor } => {
1113
+ const projectionEvents = flattenProgressBatches(projection.batches)
835
1114
  for (const event of projection.toolEvents) {
836
1115
  if (event.type === 'ai.tool.result') {
837
1116
  projectionEvents.push({
@@ -850,7 +1129,7 @@ const projectedMessage = <M extends UIMessage>(
850
1129
  ],
851
1130
  })
852
1131
  }
853
- return projectUIMessage({
1132
+ return projectUIMessageStream({
854
1133
  messageId: projection.responseMessageId,
855
1134
  ...(projection.baseMessage === undefined
856
1135
  ? {}
@@ -861,6 +1140,25 @@ const projectedMessage = <M extends UIMessage>(
861
1140
  })
862
1141
  }
863
1142
 
1143
+ const projectedMessage = <M extends UIMessage>(
1144
+ projection: ActiveGenerationProjection<M>,
1145
+ ): M => projectedMessageStream(projection).message
1146
+
1147
+ const projectedResponse = <M extends UIMessage>(options: {
1148
+ state: AIState<M>
1149
+ projection: ActiveGenerationProjection<M>
1150
+ }): M => {
1151
+ const message = projectedMessage(options.projection)
1152
+ return options.state.pausedGenerationIds?.[options.projection.generationId]
1153
+ ? pauseUIMessage({
1154
+ message,
1155
+ toolCallIds: new Set(
1156
+ options.state.tools.map((tool) => tool.toolCallId),
1157
+ ),
1158
+ })
1159
+ : message
1160
+ }
1161
+
864
1162
  const reduceToolActivity = (
865
1163
  tools: ToolActivity[],
866
1164
  event:
@@ -1020,7 +1318,11 @@ const interruptGenerationProjection = <M extends UIMessage>(
1020
1318
  if (projection === null) return null
1021
1319
  const retained: ActiveGenerationProjection<M> = {
1022
1320
  ...projection,
1023
- batches: projection.batches.filter((batch) => batch.index <= cutoff),
1321
+ batches: progressBatches(
1322
+ flattenProgressBatches(projection.batches).filter(
1323
+ (batch) => batch.index <= cutoff,
1324
+ ),
1325
+ ),
1024
1326
  toolEvents: projection.toolEvents.filter(
1025
1327
  (toolEvent) => toolEvent.index <= cutoff,
1026
1328
  ),
@@ -1040,7 +1342,7 @@ const interruptGenerationProjection = <M extends UIMessage>(
1040
1342
  ...restored,
1041
1343
  messages: shouldProject
1042
1344
  ? upsertMessage(
1043
- restored.messages,
1345
+ state.messages,
1044
1346
  interruptUIMessage(projectedMessage(retained)),
1045
1347
  )
1046
1348
  : restored.messages,
@@ -1084,6 +1386,141 @@ export function reduceAIState<M extends UIMessage>(
1084
1386
  ): AIState<M> {
1085
1387
  if (state.status === 'closed') return state
1086
1388
  switch (event.type) {
1389
+ case 'ai.control.requested': {
1390
+ const command = event.payload as ControlCommand<M>
1391
+ if (!event.id || !isQueueCommand(command)) return state
1392
+ const pending = state.pendingQueueCommands ?? []
1393
+ if (pending.some((item) => item.commandId === event.id)) return state
1394
+ if (
1395
+ (command.action === 'send' || command.action === 'steer') &&
1396
+ (state.messages.some((message) => message.id === command.message.id) ||
1397
+ state.inbox.items.some((item) => item.id === command.message.id))
1398
+ )
1399
+ return state
1400
+ return {
1401
+ ...state,
1402
+ pendingQueueCommands: [...pending, { commandId: event.id, command }],
1403
+ }
1404
+ }
1405
+ case 'ai.control.committed': {
1406
+ const commit = event.payload as ControlCommit
1407
+ const visible = applyControlChanges({
1408
+ state: { inbox: state.inbox, active: state.active },
1409
+ changes: commit.changes.filter(
1410
+ (change) => change.path[0] === 'inbox' || change.path[0] === 'active',
1411
+ ),
1412
+ })
1413
+ const { phase } = commit.view
1414
+ const status: AgentStatus = commit.view.closed
1415
+ ? 'closed'
1416
+ : phase === 'paused' ||
1417
+ phase === 'pausing' ||
1418
+ (commit.view.paused && phase === null)
1419
+ ? 'paused'
1420
+ : phase === 'failed'
1421
+ ? 'failed'
1422
+ : phase === 'starting' || phase === 'generating'
1423
+ ? 'generating'
1424
+ : phase
1425
+ ? 'waiting'
1426
+ : 'idle'
1427
+ return {
1428
+ ...state,
1429
+ ...visible,
1430
+ status,
1431
+ activeRequestId: commit.view.requestId,
1432
+ activeResponseMessageId: commit.view.responseMessageId,
1433
+ }
1434
+ }
1435
+ case 'ai.control.decided': {
1436
+ const receipt = event.payload as ControlReceipt
1437
+ if (receipt.work) return state
1438
+ const pending = state.pendingQueueCommands ?? []
1439
+ const settled = pending.find(
1440
+ (item) => item.commandId === receipt.commandId,
1441
+ )
1442
+ return {
1443
+ ...state,
1444
+ receipt,
1445
+ ...(settled === undefined
1446
+ ? {}
1447
+ : {
1448
+ pendingQueueCommands: pending.filter(
1449
+ (item) => item.commandId !== receipt.commandId,
1450
+ ),
1451
+ }),
1452
+ }
1453
+ }
1454
+ case 'ai.generation.paused': {
1455
+ const payload = event.payload as {
1456
+ generationId: string
1457
+ messageId: string
1458
+ }
1459
+ const toolCallIds = new Set(
1460
+ state.tools
1461
+ .filter((tool) => tool.messageId === payload.messageId)
1462
+ .map((tool) => tool.toolCallId),
1463
+ )
1464
+ const projection = state.activeProjection
1465
+ return {
1466
+ ...state,
1467
+ activeGeneration: null,
1468
+ activeProjection:
1469
+ projection?.generationId === payload.generationId
1470
+ ? {
1471
+ ...projection,
1472
+ cursor: null,
1473
+ batches: progressBatches(
1474
+ flattenProgressBatches(projection.batches).map((batch) => ({
1475
+ index: batch.index,
1476
+ chunks: batch.chunks.filter(
1477
+ (chunk) =>
1478
+ !('toolCallId' in chunk) ||
1479
+ toolCallIds.has(chunk.toolCallId),
1480
+ ),
1481
+ })),
1482
+ ),
1483
+ }
1484
+ : projection,
1485
+ compaction:
1486
+ state.compaction?.status === 'running'
1487
+ ? (projection?.baseCompaction ?? null)
1488
+ : state.compaction,
1489
+ messages: state.messages.map((message) =>
1490
+ message.id === payload.messageId
1491
+ ? pauseUIMessage({ message, toolCallIds })
1492
+ : message,
1493
+ ),
1494
+ pausedGenerationIds: {
1495
+ ...state.pausedGenerationIds,
1496
+ [payload.generationId]: true,
1497
+ },
1498
+ }
1499
+ }
1500
+ case 'ai.model.metadata.requested': {
1501
+ const { modelId } = event.payload as ModelMetadataRequestedPayload
1502
+ if (Object.hasOwn(state.modelMetadata, modelId)) return state
1503
+ return {
1504
+ ...state,
1505
+ modelMetadata: {
1506
+ ...state.modelMetadata,
1507
+ [modelId]: { status: 'pending' },
1508
+ },
1509
+ }
1510
+ }
1511
+ case 'ai.model.metadata.resolved': {
1512
+ const { modelId, limits } = event.payload as ModelMetadataResolvedPayload
1513
+ return {
1514
+ ...state,
1515
+ modelMetadata: {
1516
+ ...state.modelMetadata,
1517
+ [modelId]:
1518
+ limits === null
1519
+ ? { status: 'unavailable' }
1520
+ : { status: 'resolved', limits },
1521
+ },
1522
+ }
1523
+ }
1087
1524
  case 'ai.session.created': {
1088
1525
  const payload = event.payload as SessionCreatedPayload
1089
1526
  return {
@@ -1101,7 +1538,14 @@ export function reduceAIState<M extends UIMessage>(
1101
1538
  case 'ai.session.closed':
1102
1539
  return {
1103
1540
  ...state,
1541
+ active: null,
1542
+ inbox: { paused: true, items: [] },
1543
+ pendingQueueCommands: [],
1104
1544
  status: 'closed',
1545
+ compaction:
1546
+ state.compaction?.status === 'running'
1547
+ ? (state.activeProjection?.baseCompaction ?? null)
1548
+ : state.compaction,
1105
1549
  activeGeneration: null,
1106
1550
  activeRequestId: null,
1107
1551
  activeResponseMessageId: null,
@@ -1109,7 +1553,14 @@ export function reduceAIState<M extends UIMessage>(
1109
1553
  }
1110
1554
  case 'ai.message.created': {
1111
1555
  const { message } = event.payload as MessageCreatedPayload<M>
1112
- return { ...state, messages: upsertMessage(state.messages, message) }
1556
+ return {
1557
+ ...state,
1558
+ messages: upsertMessage(state.messages, message),
1559
+ activeProjection:
1560
+ state.activeProjection?.responseMessageId === message.id
1561
+ ? { ...state.activeProjection, cursor: null }
1562
+ : state.activeProjection,
1563
+ }
1113
1564
  }
1114
1565
  case 'ai.message.completed': {
1115
1566
  const { messageId } = event.payload as MessageCompletedPayload
@@ -1166,6 +1617,7 @@ export function reduceAIState<M extends UIMessage>(
1166
1617
  return state
1167
1618
  }
1168
1619
  const acceptsRequest =
1620
+ (payload.control !== undefined && state.activeRequestId === event.id) ||
1169
1621
  payload.reason === 'message' ||
1170
1622
  (payload.reason === 'tool'
1171
1623
  ? ownerGenerationId !== undefined &&
@@ -1266,9 +1718,10 @@ export function reduceAIState<M extends UIMessage>(
1266
1718
  activeProjection: {
1267
1719
  generationId: payload.generationId,
1268
1720
  responseMessageId: payload.responseMessageId,
1721
+ promptThroughIndex: payload.promptThroughIndex ?? event.index - 1,
1269
1722
  ...(baseMessage === undefined ? {} : { baseMessage }),
1270
1723
  baseCompaction: baseState.compaction,
1271
- batches: [],
1724
+ batches: progressBatches(),
1272
1725
  baseTools: baseState.tools,
1273
1726
  toolEvents: [],
1274
1727
  approvalEvents: [],
@@ -1278,6 +1731,7 @@ export function reduceAIState<M extends UIMessage>(
1278
1731
  }
1279
1732
  case 'ai.generation.progress': {
1280
1733
  const payload = event.payload as GenerationProgressPayload
1734
+ if (state.pausedGenerationIds?.[payload.generationId]) return state
1281
1735
  if (terminalGenerationReason(state, payload.generationId) !== undefined) {
1282
1736
  return state
1283
1737
  }
@@ -1288,18 +1742,63 @@ export function reduceAIState<M extends UIMessage>(
1288
1742
  ) {
1289
1743
  return state
1290
1744
  }
1745
+ const ordered =
1746
+ event.index > (activeProjection.batches.tail.at(-1)?.index ?? -1)
1291
1747
  const projection: ActiveGenerationProjection<M> = {
1292
1748
  ...activeProjection,
1293
- batches: [
1294
- ...activeProjection.batches.filter(
1295
- (batch) => batch.index !== event.index,
1296
- ),
1297
- { index: event.index, chunks: payload.chunks },
1298
- ].toSorted((left, right) => left.index - right.index),
1749
+ batches: ordered
1750
+ ? appendProgressBatch({
1751
+ batches: activeProjection.batches,
1752
+ batch: { index: event.index, chunks: payload.chunks },
1753
+ })
1754
+ : progressBatches(
1755
+ [
1756
+ ...flattenProgressBatches(activeProjection.batches).filter(
1757
+ (batch) => batch.index !== event.index,
1758
+ ),
1759
+ { index: event.index, chunks: payload.chunks },
1760
+ ].toSorted((left, right) => left.index - right.index),
1761
+ ),
1762
+ }
1763
+ const base = state.messages.find(
1764
+ (message) => message.id === projection.responseMessageId,
1765
+ )
1766
+ const incremental =
1767
+ base !== undefined &&
1768
+ activeProjection.cursor != null &&
1769
+ event.index > activeProjection.cursor.throughIndex
1770
+ const projected = incremental
1771
+ ? projectUIMessageStream({
1772
+ messageId: projection.responseMessageId,
1773
+ base,
1774
+ cursor: activeProjection.cursor!.stream,
1775
+ chunks: payload.chunks,
1776
+ })
1777
+ : projectedMessageStream(projection)
1778
+ projection.cursor = {
1779
+ throughIndex: incremental
1780
+ ? event.index
1781
+ : Math.max(
1782
+ event.index,
1783
+ projection.batches.tail.at(-1)?.index ?? 0,
1784
+ projection.toolEvents.reduce(
1785
+ (index, entry) => Math.max(index, entry.index),
1786
+ 0,
1787
+ ),
1788
+ projection.approvalEvents.reduce(
1789
+ (index, entry) => Math.max(index, entry.index),
1790
+ 0,
1791
+ ),
1792
+ ),
1793
+ stream: projected.cursor,
1299
1794
  }
1300
1795
  return {
1301
1796
  ...state,
1302
- messages: upsertMessage(state.messages, projectedMessage(projection)),
1797
+ messages: upsertResponse({
1798
+ messages: state.messages,
1799
+ message: projected.message,
1800
+ inputId: state.activeGeneration.messageId,
1801
+ }),
1303
1802
  activeProjection: projection,
1304
1803
  status: 'generating',
1305
1804
  }
@@ -1727,12 +2226,16 @@ export function reduceAIState<M extends UIMessage>(
1727
2226
  }
1728
2227
  const activeProjection: ActiveGenerationProjection<M> = {
1729
2228
  ...projection,
2229
+ cursor: null,
1730
2230
  toolEvents: [
1731
2231
  ...projection.toolEvents,
1732
2232
  { index: event.index, type: 'ai.tool.result', payload },
1733
2233
  ],
1734
2234
  }
1735
- const projected = projectedMessage(activeProjection)
2235
+ const projected = projectedResponse({
2236
+ state,
2237
+ projection: activeProjection,
2238
+ })
1736
2239
  const tools = projectedTools(activeProjection)
1737
2240
  const failureError = state.error ?? 'Generation failed.'
1738
2241
  const next = {
@@ -1783,6 +2286,7 @@ export function reduceAIState<M extends UIMessage>(
1783
2286
  projection?.generationId === payload.generationId
1784
2287
  ? {
1785
2288
  ...projection,
2289
+ cursor: null,
1786
2290
  approvalEvents: [
1787
2291
  ...projection.approvalEvents,
1788
2292
  {
@@ -1800,7 +2304,10 @@ export function reduceAIState<M extends UIMessage>(
1800
2304
  ? projectMessageChunks(state.messages, payload.messageId, [
1801
2305
  approvalRequestedChunk(payload),
1802
2306
  ])
1803
- : upsertMessage(state.messages, projectedMessage(activeProjection)),
2307
+ : upsertMessage(
2308
+ state.messages,
2309
+ projectedResponse({ state, projection: activeProjection }),
2310
+ ),
1804
2311
  activeProjection,
1805
2312
  pendingApprovals:
1806
2313
  payload.isAutomatic === true
@@ -1852,6 +2359,7 @@ export function reduceAIState<M extends UIMessage>(
1852
2359
  projection !== null && activeRequest !== undefined
1853
2360
  ? {
1854
2361
  ...projection,
2362
+ cursor: null,
1855
2363
  approvalEvents: [
1856
2364
  ...projection.approvalEvents,
1857
2365
  {
@@ -1867,8 +2375,14 @@ export function reduceAIState<M extends UIMessage>(
1867
2375
  messages:
1868
2376
  activeProjection === null || activeProjection === projection
1869
2377
  ? answerApproval(state.messages, payload)
1870
- : upsertMessage(state.messages, projectedMessage(activeProjection)),
1871
- activeProjection,
2378
+ : upsertMessage(
2379
+ state.messages,
2380
+ projectedResponse({ state, projection: activeProjection }),
2381
+ ),
2382
+ activeProjection:
2383
+ activeProjection === null
2384
+ ? null
2385
+ : { ...activeProjection, cursor: null },
1872
2386
  pendingApprovals: state.pendingApprovals.filter(
1873
2387
  (approval) =>
1874
2388
  approval.approvalId !== payload.approvalId ||
@@ -1943,7 +2457,12 @@ export function reduceAIState<M extends UIMessage>(
1943
2457
  }
1944
2458
  return {
1945
2459
  ...state,
1946
- compaction: { status: 'completed', ...payload },
2460
+ compaction: {
2461
+ status: 'completed',
2462
+ ...payload,
2463
+ throughIndex:
2464
+ payload.throughIndex ?? state.activeProjection!.promptThroughIndex!,
2465
+ },
1947
2466
  }
1948
2467
  }
1949
2468
  default:
@@ -1966,59 +2485,84 @@ export function createReducer<D extends EventDefs>(options: {
1966
2485
  name?: string
1967
2486
  }): Reducer<D, AIState<AIMessageFrom<D>>> {
1968
2487
  type M = AIMessageFrom<D>
1969
- return options.contract
2488
+ const reducer = options.contract
1970
2489
  .reducer({
1971
- name: options.name ?? 'a2.ai.state.v8',
2490
+ name: options.name ?? 'a2.ai.state.v17',
1972
2491
  initialState: initialState<M>(),
1973
2492
  })
1974
2493
  .fold((state, event) => reduceAIState(state, event))
2494
+ return { ...reducer, [clientStateProjection]: projectAIClientState }
1975
2495
  }
1976
2496
 
1977
- type ProtocolInput<
2497
+ type CommandInput<C> = { type: 'ai.control.requested'; payload: C; id?: string }
2498
+ type ControlInput<
1978
2499
  M extends UIMessage,
1979
- K extends keyof AIEventDefs<M> & string,
1980
- > = AppendInput<Pick<AIEventDefs<M>, K>>
2500
+ A extends ControlCommand<M>['action'],
2501
+ > = CommandInput<Extract<ControlCommand<M>, { action: A }>>
1981
2502
 
1982
2503
  export type AIInputs = {
1983
- /** Record a message. User messages schedule unless generate is false. */
1984
2504
  message<M extends UIMessage>(
1985
2505
  message: M,
1986
2506
  options?: { generate?: boolean },
1987
- ): ProtocolInput<M, 'ai.message.created'>[]
1988
- /** Record a message without requesting a generation. */
1989
- seed<M extends UIMessage>(
1990
- message: M,
1991
- ): ProtocolInput<M, 'ai.message.created'>[]
1992
- /** Answer an AI SDK tool approval. The tool barrier owns continuation. */
2507
+ ): ControlInput<M, 'send'>[]
2508
+ seed<M extends UIMessage>(message: M): ControlInput<M, 'send'>[]
2509
+ queue: {
2510
+ remove(options: { inputId: string }): ControlInput<UIMessage, 'remove'>[]
2511
+ sendNow(options: {
2512
+ inputId: string
2513
+ turnId: string | null
2514
+ lastSeenIndex?: number
2515
+ }): ControlInput<UIMessage, 'send-now'>[]
2516
+ edit<M extends UIMessage>(options: {
2517
+ message: M
2518
+ expectedRevision: number
2519
+ }): ControlInput<M, 'edit'>[]
2520
+ move(options: {
2521
+ inputId: string
2522
+ beforeId: string | null
2523
+ }): ControlInput<UIMessage, 'move'>[]
2524
+ }
2525
+ stop(options: {
2526
+ turnId: string
2527
+ lastSeenIndex?: number
2528
+ }): ControlInput<UIMessage, 'stop'>[]
2529
+ steer<M extends UIMessage>(options: {
2530
+ turnId: string
2531
+ message: M
2532
+ lastSeenIndex?: number
2533
+ }): ControlInput<M, 'steer'>[]
2534
+ pause(options: {
2535
+ when: 'now' | 'after-turn'
2536
+ }): ControlInput<UIMessage, 'pause'>[]
2537
+ resume(): ControlInput<UIMessage, 'resume'>[]
1993
2538
  approval(
1994
2539
  response: ApprovalRespondedPayload,
1995
- ): ProtocolInput<UIMessage, 'ai.approval.responded'>[]
1996
- /** Answer an application-defined input request. */
1997
- input(
1998
- response: InputRespondedPayload,
1999
- ): ProtocolInput<UIMessage, 'ai.input.responded'>[]
2000
- /** Record an application-defined request for input. */
2540
+ ): ControlInput<UIMessage, 'approval'>[]
2541
+ input(response: InputRespondedPayload): ControlInput<UIMessage, 'input'>[]
2001
2542
  requestInput(
2002
2543
  request: InputRequestedPayload,
2003
- ): ProtocolInput<UIMessage, 'ai.input.requested'>[]
2004
- /** Request a fresh attempt for a failed response. */
2005
- retry(
2006
- options: RetryGenerationOptions,
2007
- ): ProtocolInput<UIMessage, 'ai.retry.requested'>[]
2008
- /** Interrupt the active response for a message. */
2544
+ ): ControlInput<UIMessage, 'request-input'>[]
2545
+ retry(request: RetryGenerationOptions): ControlInput<UIMessage, 'retry'>[]
2009
2546
  interrupt(
2010
- interruption: MessageInterruptedPayload,
2011
- ): ProtocolInput<UIMessage, 'ai.message.interrupted'>[]
2547
+ request: MessageInterruptedPayload,
2548
+ ): ControlInput<UIMessage, 'interrupt'>[]
2549
+ toolResult(
2550
+ result: ToolResultPayload,
2551
+ ): ControlInput<UIMessage, 'tool-result'>[]
2012
2552
  }
2013
2553
 
2554
+ const commandInput = <C extends ControlCommand<UIMessage>>(
2555
+ payload: C,
2556
+ ): CommandInput<C>[] => [{ type: 'ai.control.requested', payload }]
2014
2557
  const messageInput = <M extends UIMessage>(
2015
2558
  message: M,
2016
2559
  generate?: boolean,
2017
- ): ProtocolInput<M, 'ai.message.created'>[] => [
2560
+ ): ControlInput<M, 'send'>[] => [
2018
2561
  {
2019
- type: 'ai.message.created',
2562
+ type: 'ai.control.requested',
2020
2563
  id: `ai.message:${message.id}`,
2021
2564
  payload: {
2565
+ action: 'send',
2022
2566
  message,
2023
2567
  ...(generate === false ? { generate: false } : {}),
2024
2568
  },
@@ -2027,57 +2571,53 @@ const messageInput = <M extends UIMessage>(
2027
2571
 
2028
2572
  /** Pure append inputs for the built-in AI protocol. */
2029
2573
  export const inputs: AIInputs = {
2574
+ toolResult: (result) => commandInput({ action: 'tool-result', result }),
2575
+ queue: {
2576
+ remove: (options) => commandInput({ action: 'remove', ...options }),
2577
+ sendNow: (options) => commandInput({ action: 'send-now', ...options }),
2578
+ edit: (options) => commandInput({ action: 'edit', ...options }),
2579
+ move: (options) => commandInput({ action: 'move', ...options }),
2580
+ },
2581
+ stop: (options) => commandInput({ action: 'stop', ...options }),
2582
+ steer: (options) => commandInput({ action: 'steer', ...options }),
2583
+ pause: (options) => commandInput({ action: 'pause', ...options }),
2584
+ resume: () => commandInput({ action: 'resume' }),
2030
2585
  message<M extends UIMessage>(message: M, options?: { generate?: boolean }) {
2031
2586
  return messageInput(message, options?.generate)
2032
2587
  },
2033
2588
  seed<M extends UIMessage>(message: M) {
2034
2589
  return messageInput(message, false)
2035
2590
  },
2036
- approval(response) {
2037
- return [
2038
- {
2039
- type: 'ai.approval.responded',
2040
- id: `ai.approval:${encodeURIComponent(response.messageId)}:${encodeURIComponent(response.generationId)}:${encodeURIComponent(response.approvalId)}:response`,
2041
- payload: response,
2042
- },
2043
- ]
2044
- },
2045
- input(response) {
2046
- return [
2047
- {
2048
- type: 'ai.input.responded',
2049
- id: `ai.input:${encodeURIComponent(response.messageId)}:${encodeURIComponent(response.generationId)}:${encodeURIComponent(response.inputId)}:response`,
2050
- payload: response,
2051
- },
2052
- ]
2053
- },
2054
- requestInput(request) {
2055
- return [
2056
- {
2057
- type: 'ai.input.requested',
2058
- id: `ai.input:${encodeURIComponent(request.messageId)}:${encodeURIComponent(request.generationId)}:${encodeURIComponent(request.inputId)}:request`,
2059
- payload: request,
2060
- },
2061
- ]
2062
- },
2063
- retry(retryOptions) {
2064
- return [
2065
- {
2066
- type: 'ai.retry.requested',
2067
- id: `ai.retry:${retryOptions.retryId}`,
2068
- payload: retryOptions,
2069
- },
2070
- ]
2071
- },
2072
- interrupt(interruption) {
2073
- return [
2074
- {
2075
- type: 'ai.message.interrupted',
2076
- id: `ai.interrupt:${interruption.generationId ?? interruption.requestId}`,
2077
- payload: interruption,
2078
- },
2079
- ]
2080
- },
2591
+ approval: (response) => [
2592
+ {
2593
+ ...commandInput({ action: 'approval', response })[0]!,
2594
+ id: `ai.approval:${encodeURIComponent(response.messageId)}:${encodeURIComponent(response.generationId)}:${encodeURIComponent(response.approvalId)}:response`,
2595
+ },
2596
+ ],
2597
+ input: (response) => [
2598
+ {
2599
+ ...commandInput({ action: 'input', response })[0]!,
2600
+ id: `ai.input:${encodeURIComponent(response.messageId)}:${encodeURIComponent(response.generationId)}:${encodeURIComponent(response.inputId)}:response`,
2601
+ },
2602
+ ],
2603
+ requestInput: (request) => [
2604
+ {
2605
+ ...commandInput({ action: 'request-input', request })[0]!,
2606
+ id: `ai.input:${encodeURIComponent(request.messageId)}:${encodeURIComponent(request.generationId)}:${encodeURIComponent(request.inputId)}:request`,
2607
+ },
2608
+ ],
2609
+ retry: (request) => [
2610
+ {
2611
+ ...commandInput({ action: 'retry', request })[0]!,
2612
+ id: `ai.retry:${request.retryId}`,
2613
+ },
2614
+ ],
2615
+ interrupt: (request) => [
2616
+ {
2617
+ ...commandInput({ action: 'interrupt', request })[0]!,
2618
+ id: `ai.interrupt:${request.generationId ?? request.requestId}`,
2619
+ },
2620
+ ],
2081
2621
  }
2082
2622
 
2083
2623
  /**
@@ -2134,14 +2674,14 @@ export function agent<
2134
2674
  })
2135
2675
  const reducer = agentContract
2136
2676
  .reducer({
2137
- name: options.reducerName ?? 'a2.ai.state.v8',
2677
+ name: options.reducerName ?? 'a2.ai.state.v17',
2138
2678
  initialState: initialState<M>(),
2139
2679
  })
2140
2680
  .fold((state, event) => reduceAIState(state, event))
2141
2681
 
2142
2682
  const definition = {
2143
2683
  contract: agentContract,
2144
- reducer,
2684
+ reducer: { ...reducer, [clientStateProjection]: projectAIClientState<M> },
2145
2685
  }
2146
2686
  return definition as unknown as AgentDefinition<M, AIEventDefs<M> & E>
2147
2687
  }