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.
- package/CHANGELOG.md +47 -0
- package/dist/{actor-DJi3RsNu.d.ts → actor-BfQSE0KC.d.ts} +4 -4
- package/dist/{actor-DJi3RsNu.d.ts.map → actor-BfQSE0KC.d.ts.map} +1 -1
- package/dist/actor-client.d.ts +1 -1
- package/dist/actor-client.js +1 -1
- package/dist/actor-react.d.ts +3 -3
- package/dist/actor-react.js +2 -2
- package/dist/{actor-shared-DI7J5upy.js → actor-shared-B5tJfzt-.js} +2 -2
- package/dist/{actor-shared-DI7J5upy.js.map → actor-shared-B5tJfzt-.js.map} +1 -1
- package/dist/actor.d.ts +1 -1
- package/dist/actor.js +3 -3
- package/dist/ai-Cai-lCbj.d.ts +580 -0
- package/dist/ai-Cai-lCbj.d.ts.map +1 -0
- package/dist/ai-control-CcD4hh3y.js +119 -0
- package/dist/ai-control-CcD4hh3y.js.map +1 -0
- package/dist/ai-server.d.ts +16 -8
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +1322 -475
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +2 -334
- package/dist/ai.js +838 -85
- package/dist/ai.js.map +1 -1
- package/dist/{client-P_NNNRM-.d.ts → client-BAEABRZB.d.ts} +2 -2
- package/dist/{client-P_NNNRM-.d.ts.map → client-BAEABRZB.d.ts.map} +1 -1
- package/dist/{client-Bf6uSEAk.js → client-BYzHjkwU.js} +21 -6
- package/dist/client-BYzHjkwU.js.map +1 -0
- package/dist/client.d.ts +1 -1
- package/dist/client.js +1 -1
- package/dist/{contract-48bUMgcL.js → contract-CKRg_E4q.js} +3 -26
- package/dist/contract-CKRg_E4q.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/react.d.ts +2 -2
- package/dist/react.js +1 -1
- package/dist/{reducer-DJKWm3cp.d.ts → reducer-BcS9VDKC.d.ts} +4 -1
- package/dist/{reducer-DJKWm3cp.d.ts.map → reducer-BcS9VDKC.d.ts.map} +1 -1
- package/dist/reducer-DEMjEY_O.js +29 -0
- package/dist/reducer-DEMjEY_O.js.map +1 -0
- package/dist/scheduler-qstash.d.ts +2 -2
- package/dist/scheduler-qstash.js +1 -1
- package/dist/scheduler-vercel.d.ts +2 -2
- package/dist/scheduler-vercel.js +1 -1
- package/dist/{server-DjZZa1wr.d.ts → server-Bp5Nd1pF.d.ts} +3 -3
- package/dist/{server-DjZZa1wr.d.ts.map → server-Bp5Nd1pF.d.ts.map} +1 -1
- package/dist/{server-BeNADlCI.js → server-CjJSGcF7.js} +4 -3
- package/dist/server-CjJSGcF7.js.map +1 -0
- package/dist/server.d.ts +3 -3
- package/dist/server.js +1 -1
- package/dist/{store-DtDOWLSn.d.ts → store-D_yhNdPz.d.ts} +7 -2
- package/dist/{store-DtDOWLSn.d.ts.map → store-D_yhNdPz.d.ts.map} +1 -1
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-postgres.d.ts +1 -1
- package/dist/store-postgres.js +19 -0
- package/dist/store-postgres.js.map +1 -1
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +1 -1
- package/dist/{store-redis-notify-BUCyXOn0.js → store-redis-notify-D2EI6gwX.js} +27 -2
- package/dist/store-redis-notify-D2EI6gwX.js.map +1 -0
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.js +1 -1
- package/dist/store-sqlite.d.ts +1 -1
- package/docs/guides/06-ai-agents.mdx +361 -76
- package/docs/reference/01-api.mdx +159 -27
- package/examples/playground/app/agent/[agentId]/agent-client.tsx +145 -33
- package/examples/playground/app/agent/[agentId]/agent-queue.tsx +289 -0
- package/examples/playground/app/agent/[agentId]/compaction/route.ts +14 -0
- package/examples/playground/app/agent/[agentId]/compaction-event.tsx +38 -0
- package/examples/playground/app/agent/[agentId]/compaction-panel.tsx +294 -0
- package/examples/playground/app/agent/compaction-settings.test.ts +144 -0
- package/examples/playground/app/agent/compaction-settings.ts +49 -0
- package/examples/playground/app/agent/compaction-timeline.test.ts +337 -0
- package/examples/playground/app/agent/compaction-timeline.ts +198 -0
- package/examples/playground/app/agent/model.ts +56 -1
- package/examples/playground/app/agent/server.ts +9 -2
- package/examples/playground/app/chat/[chatId]/chat-client.tsx +3 -13
- package/examples/playground/app/chat/model.ts +2 -2
- package/examples/playground/app/chat/server.ts +24 -17
- package/examples/playground/app/globals.css +333 -0
- package/examples/playground/package.json +1 -1
- package/package.json +1 -1
- package/src/ai-client-state.ts +185 -0
- package/src/ai-control-server.ts +829 -0
- package/src/ai-control-state.ts +152 -0
- package/src/ai-control.ts +139 -0
- package/src/ai-coordinator.ts +99 -32
- package/src/ai-model-metadata.ts +108 -0
- package/src/ai-progress-batches.ts +68 -0
- package/src/ai-projector.ts +76 -15
- package/src/ai-sdk-step.ts +5 -2
- package/src/ai-server.ts +920 -638
- package/src/ai.ts +650 -110
- package/src/client.ts +31 -9
- package/src/licenses/Apache-2.0.txt +55 -0
- package/src/parse-partial-json.ts +441 -0
- package/src/reducer.ts +6 -0
- package/src/server.ts +8 -4
- package/src/store-postgres.ts +27 -0
- package/src/store-redis-core.ts +53 -1
- package/src/store-redis-notify.ts +1 -0
- package/src/store.ts +6 -0
- package/dist/ai.d.ts.map +0 -1
- package/dist/client-Bf6uSEAk.js.map +0 -1
- package/dist/contract-48bUMgcL.js.map +0 -1
- package/dist/server-BeNADlCI.js.map +0 -1
- 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 {
|
|
18
|
-
import {
|
|
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
|
|
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:
|
|
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.
|
|
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
|
-
|
|
290
|
-
|
|
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
|
|
1110
|
+
const projectedMessageStream = <M extends UIMessage>(
|
|
830
1111
|
projection: ActiveGenerationProjection<M>,
|
|
831
|
-
): M => {
|
|
832
|
-
const projectionEvents
|
|
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
|
|
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:
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
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:
|
|
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 =
|
|
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(
|
|
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(
|
|
1871
|
-
|
|
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: {
|
|
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
|
-
|
|
2488
|
+
const reducer = options.contract
|
|
1970
2489
|
.reducer({
|
|
1971
|
-
name: options.name ?? 'a2.ai.state.
|
|
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
|
|
2497
|
+
type CommandInput<C> = { type: 'ai.control.requested'; payload: C; id?: string }
|
|
2498
|
+
type ControlInput<
|
|
1978
2499
|
M extends UIMessage,
|
|
1979
|
-
|
|
1980
|
-
> =
|
|
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
|
-
):
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
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
|
-
):
|
|
1996
|
-
|
|
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
|
-
):
|
|
2004
|
-
|
|
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
|
-
|
|
2011
|
-
):
|
|
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
|
-
):
|
|
2560
|
+
): ControlInput<M, 'send'>[] => [
|
|
2018
2561
|
{
|
|
2019
|
-
type: 'ai.
|
|
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
|
-
|
|
2038
|
-
{
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
}
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
{
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
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.
|
|
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
|
}
|