experimental-a2 0.18.0 → 0.19.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 +39 -0
- package/dist/{actor-Bjce9_5U.d.ts → actor-C7b-1arn.d.ts} +3 -3
- package/dist/{actor-Bjce9_5U.d.ts.map → actor-C7b-1arn.d.ts.map} +1 -1
- package/dist/actor-client.d.ts +1 -1
- package/dist/actor-react.d.ts +1 -1
- package/dist/actor-react.js +1 -1
- package/dist/actor.d.ts +1 -1
- package/dist/actor.js +2 -2
- package/dist/{ai-DrUaMyRt.d.ts → ai-Cz3hqVhk.d.ts} +34 -17
- package/dist/ai-Cz3hqVhk.d.ts.map +1 -0
- package/dist/ai-client.d.ts +1 -1
- package/dist/ai-client.js +2 -2
- package/dist/ai-control-CvLbW10B.js +46 -0
- package/dist/ai-control-CvLbW10B.js.map +1 -0
- package/dist/ai-fx.d.ts +1 -1
- package/dist/ai-fx.d.ts.map +1 -1
- package/dist/ai-fx.js +122 -44
- package/dist/ai-fx.js.map +1 -1
- package/dist/{ai-harness-BuLh_EYi.js → ai-harness-BuuC-gJ3.js} +79 -15
- package/dist/ai-harness-BuuC-gJ3.js.map +1 -0
- package/dist/{ai-harness-DvsPeJeB.d.ts → ai-harness-CV6rXbB3.d.ts} +2 -2
- package/dist/ai-harness-CV6rXbB3.d.ts.map +1 -0
- package/dist/{ai-BpOGm-zB.js → ai-mMiWul1T.js} +117 -78
- package/dist/ai-mMiWul1T.js.map +1 -0
- package/dist/ai-pi.d.ts +1 -1
- package/dist/ai-pi.d.ts.map +1 -1
- package/dist/ai-pi.js +114 -23
- package/dist/ai-pi.js.map +1 -1
- package/dist/ai-react.d.ts +1 -1
- package/dist/ai-react.js +2 -2
- package/dist/ai-server.d.ts +3 -3
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +127 -55
- package/dist/ai-server.js.map +1 -1
- package/dist/ai-tool-input-DmSqMUQi.js +40 -0
- package/dist/ai-tool-input-DmSqMUQi.js.map +1 -0
- package/dist/ai.d.ts +2 -2
- package/dist/ai.js +1 -1
- package/dist/{client-BReKqwZC.js → client-BhHLoNKF.js} +3 -3
- package/dist/{client-BReKqwZC.js.map → client-BhHLoNKF.js.map} +1 -1
- package/dist/client.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/{internal-B2T3U4Rq.js → internal-CscSY7K_.js} +4 -2
- package/dist/{internal-B2T3U4Rq.js.map → internal-CscSY7K_.js.map} +1 -1
- package/dist/react.js +1 -1
- package/dist/scheduler-qstash.d.ts +2 -2
- package/dist/scheduler-qstash.js +3 -3
- package/dist/scheduler-qstash.js.map +1 -1
- package/dist/scheduler-vercel.d.ts +7 -7
- package/dist/scheduler-vercel.d.ts.map +1 -1
- package/dist/scheduler-vercel.js +10 -14
- package/dist/scheduler-vercel.js.map +1 -1
- package/dist/{server-Dw_k_7kO.js → server-D-Y4pqwu.js} +37 -11
- package/dist/server-D-Y4pqwu.js.map +1 -0
- package/dist/{server-En7cbi3v.d.ts → server-DYAATFuT.d.ts} +11 -5
- package/dist/server-DYAATFuT.d.ts.map +1 -0
- package/dist/server.d.ts +2 -2
- package/dist/server.js +1 -1
- package/dist/{session-clear-Bo-XTFEy.d.ts → session-clear-C-QwTXV1.d.ts} +2 -2
- package/dist/session-clear-C-QwTXV1.d.ts.map +1 -0
- package/dist/{store-Djdv0bnZ.d.ts → store-CyLUy86v.d.ts} +23 -4
- package/dist/store-CyLUy86v.d.ts.map +1 -0
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-memory.js +25 -6
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-B5FcwJfo.js → store-polling-7WJV0YWP.js} +2 -2
- package/dist/{store-polling-B5FcwJfo.js.map → store-polling-7WJV0YWP.js.map} +1 -1
- package/dist/store-postgres.d.ts +1 -1
- package/dist/store-postgres.d.ts.map +1 -1
- package/dist/store-postgres.js +55 -25
- package/dist/store-postgres.js.map +1 -1
- package/dist/{store-presence-polling-BAwS8RWn.js → store-presence-polling-B1ixeqIS.js} +3 -3
- package/dist/{store-presence-polling-BAwS8RWn.js.map → store-presence-polling-B1ixeqIS.js.map} +1 -1
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +3 -3
- package/dist/{store-redis-notify-BRITti_o.js → store-redis-notify-CM0AfLY0.js} +88 -37
- package/dist/store-redis-notify-CM0AfLY0.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/dist/store-sqlite.js +35 -20
- package/dist/store-sqlite.js.map +1 -1
- package/dist/{wire-rSUglYo8.js → wire-BKxH1vbP.js} +2 -2
- package/dist/{wire-rSUglYo8.js.map → wire-BKxH1vbP.js.map} +1 -1
- package/docs/agents/03-conversations.mdx +27 -15
- package/docs/agents/04-harnesses.mdx +51 -5
- package/docs/contract/03-sessions.mdx +1 -1
- package/docs/index.mdx +3 -3
- package/docs/reference/02-server.mdx +12 -7
- package/docs/reference/04-ai.mdx +41 -23
- package/docs/reference/05-adapters.mdx +6 -6
- package/docs/server/01-handlers.mdx +1 -1
- package/docs/server/02-timers.mdx +1 -1
- package/docs/server/03-cancellation.mdx +3 -2
- package/docs/server/05-production.mdx +4 -3
- package/docs/server/06-guarantees.mdx +20 -11
- package/examples/playground/DEPLOY.md +11 -4
- package/examples/playground/app/a0/[appId]/page.tsx +2 -8
- package/examples/playground/app/a0/a0.css +244 -5
- package/examples/playground/app/a0/app-view.tsx +2 -1
- package/examples/playground/app/a0/attachments.ts +133 -0
- package/examples/playground/app/a0/chat.test.tsx +504 -0
- package/examples/playground/app/a0/chat.tsx +623 -63
- package/examples/playground/app/a0/context-menu.tsx +38 -11
- package/examples/playground/app/a0/devtools.tsx +2 -8
- package/examples/playground/app/a0/model.test.ts +28 -0
- package/examples/playground/app/a0/model.ts +30 -0
- package/examples/playground/app/a0/server.ts +13 -19
- package/examples/playground/app/a0/uploads/route.ts +29 -0
- package/examples/playground/app/agent/[agentId]/agent-client.tsx +2 -1
- package/examples/playground/app/agent/[agentId]/agent-queue.test.tsx +1 -0
- package/examples/playground/app/agent/model.test.ts +12 -3
- package/examples/playground/app/chat/[chatId]/agent-state.test.ts +1 -5
- package/examples/playground/next.config.ts +7 -4
- package/examples/playground/package.json +3 -1
- package/package.json +1 -1
- package/src/ai-agent.ts +14 -3
- package/src/ai-client-state.ts +21 -9
- package/src/ai-context-schema.ts +28 -4
- package/src/ai-context.ts +83 -3
- package/src/ai-control-server.ts +70 -43
- package/src/ai-control-state.ts +3 -4
- package/src/ai-control.ts +59 -10
- package/src/ai-coordinator.ts +7 -6
- package/src/ai-fx.ts +184 -67
- package/src/ai-harness.ts +153 -29
- package/src/ai-message-shape.ts +41 -0
- package/src/ai-pi.ts +161 -46
- package/src/ai-stored-state.ts +1 -3
- package/src/ai-tool-input.ts +45 -0
- package/src/ai.ts +116 -77
- package/src/internal.ts +6 -0
- package/src/libfx.d.ts +8 -1
- package/src/scheduler-qstash.ts +5 -5
- package/src/scheduler-vercel.ts +15 -27
- package/src/server.ts +58 -12
- package/src/store-memory.ts +50 -9
- package/src/store-postgres.ts +79 -28
- package/src/store-redis-core.ts +112 -31
- package/src/store-sqlite.ts +61 -28
- package/src/store.ts +22 -3
- package/dist/ai-BpOGm-zB.js.map +0 -1
- package/dist/ai-DrUaMyRt.d.ts.map +0 -1
- package/dist/ai-harness-BuLh_EYi.js.map +0 -1
- package/dist/ai-harness-DvsPeJeB.d.ts.map +0 -1
- package/dist/server-Dw_k_7kO.js.map +0 -1
- package/dist/server-En7cbi3v.d.ts.map +0 -1
- package/dist/session-clear-Bo-XTFEy.d.ts.map +0 -1
- package/dist/store-Djdv0bnZ.d.ts.map +0 -1
- package/dist/store-redis-notify-BRITti_o.js.map +0 -1
- package/examples/playground/app/a0/no-photon.cjs +0 -4
package/src/ai.ts
CHANGED
|
@@ -23,7 +23,8 @@ import {
|
|
|
23
23
|
} from './ai-client-state.ts'
|
|
24
24
|
export type { PendingQueueCommand } from './ai-client-state.ts'
|
|
25
25
|
import { applyControlChanges } from './ai-control-state.ts'
|
|
26
|
-
import {
|
|
26
|
+
import { STEERED_REASON, unexplainedAdmission } from './ai-control.ts'
|
|
27
|
+
import { isJSONCompatible, isRecord, isUIMessage } from './ai-message-shape.ts'
|
|
27
28
|
import { AI_EVENT_ID_PREFIX } from './ai-id.ts'
|
|
28
29
|
import type {
|
|
29
30
|
ControlCommit,
|
|
@@ -31,6 +32,7 @@ import type {
|
|
|
31
32
|
ControlCommand,
|
|
32
33
|
ControlReceipt,
|
|
33
34
|
InboxItem,
|
|
35
|
+
InputRef,
|
|
34
36
|
ToolExecutionRequest,
|
|
35
37
|
WorkReport,
|
|
36
38
|
} from './ai-control.ts'
|
|
@@ -39,6 +41,7 @@ export type {
|
|
|
39
41
|
ControlCommand,
|
|
40
42
|
ControlReceipt,
|
|
41
43
|
InboxItem,
|
|
44
|
+
InputRef,
|
|
42
45
|
} from './ai-control.ts'
|
|
43
46
|
import { contract as createContract } from './contract.ts'
|
|
44
47
|
import type { Contract, ContractEvent, EventDefs } from './contract.ts'
|
|
@@ -139,10 +142,15 @@ export type TranscriptMessage<M extends UIMessage> = Omit<M, 'parts'> & {
|
|
|
139
142
|
|
|
140
143
|
export type SessionCreatedPayload = { metadata?: JSONValue }
|
|
141
144
|
export type SessionClosedPayload = { reason?: string }
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
145
|
+
/**
|
|
146
|
+
* A message entering the transcript. Its content is the command's that brought
|
|
147
|
+
* it in, which every reader of the transcript has already folded.
|
|
148
|
+
*/
|
|
149
|
+
export type MessageCreatedPayload = {
|
|
150
|
+
messageId: string
|
|
151
|
+
role: UIMessage['role']
|
|
152
|
+
/** For user messages, false records passive context. Omission schedules. */
|
|
153
|
+
generate?: false
|
|
146
154
|
}
|
|
147
155
|
export type MessageCompletedPayload = { messageId: string }
|
|
148
156
|
type MessageInterruption = {
|
|
@@ -312,7 +320,7 @@ export type AIEventDefs<M extends UIMessage = UIMessage> = {
|
|
|
312
320
|
}>
|
|
313
321
|
'ai.session.created': StandardSchemaV1<SessionCreatedPayload>
|
|
314
322
|
'ai.session.closed': StandardSchemaV1<SessionClosedPayload>
|
|
315
|
-
'ai.message.created': StandardSchemaV1<MessageCreatedPayload
|
|
323
|
+
'ai.message.created': StandardSchemaV1<MessageCreatedPayload>
|
|
316
324
|
'ai.message.completed': StandardSchemaV1<MessageCompletedPayload>
|
|
317
325
|
'ai.message.interrupted': StandardSchemaV1<MessageInterruptedPayload>
|
|
318
326
|
'ai.generation.requested': StandardSchemaV1<GenerationRequestedPayload>
|
|
@@ -402,7 +410,7 @@ export type GenerationUsage = {
|
|
|
402
410
|
export type AIState<M extends UIMessage = UIMessage> = {
|
|
403
411
|
inbox: { paused: boolean; items: InboxItem<M>[] }
|
|
404
412
|
pendingQueueCommands?: PendingQueueCommand<M>[]
|
|
405
|
-
active: ActiveTurn
|
|
413
|
+
active: ActiveTurn | null
|
|
406
414
|
starting: boolean
|
|
407
415
|
receipt: ControlReceipt | null
|
|
408
416
|
pausedGenerationIds?: Record<string, true>
|
|
@@ -433,13 +441,13 @@ export type AIState<M extends UIMessage = UIMessage> = {
|
|
|
433
441
|
error: string | null
|
|
434
442
|
}
|
|
435
443
|
|
|
436
|
-
/** The `UIMessage` type a contract's
|
|
444
|
+
/** The `UIMessage` type a contract's input commands carry. */
|
|
437
445
|
export type AIMessageOf<D> = D extends {
|
|
438
|
-
readonly 'ai.
|
|
446
|
+
readonly 'ai.control.requested': infer S extends StandardSchemaV1
|
|
439
447
|
}
|
|
440
|
-
? StandardSchemaV1.InferOutput<S> extends
|
|
441
|
-
infer M extends UIMessage
|
|
442
|
-
|
|
448
|
+
? Extract<StandardSchemaV1.InferOutput<S>, { action: 'send' }> extends {
|
|
449
|
+
message: infer M extends UIMessage
|
|
450
|
+
}
|
|
443
451
|
? M
|
|
444
452
|
: UIMessage
|
|
445
453
|
: UIMessage
|
|
@@ -529,33 +537,6 @@ const schema = <T>(
|
|
|
529
537
|
},
|
|
530
538
|
})
|
|
531
539
|
|
|
532
|
-
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
533
|
-
typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
534
|
-
|
|
535
|
-
const isJSONCompatible = (
|
|
536
|
-
value: unknown,
|
|
537
|
-
seen: Set<object> = new Set(),
|
|
538
|
-
): boolean => {
|
|
539
|
-
if (
|
|
540
|
-
value === null ||
|
|
541
|
-
typeof value === 'string' ||
|
|
542
|
-
typeof value === 'boolean'
|
|
543
|
-
) {
|
|
544
|
-
return true
|
|
545
|
-
}
|
|
546
|
-
if (typeof value === 'number') return Number.isFinite(value)
|
|
547
|
-
if (typeof value === 'undefined') return true
|
|
548
|
-
if (typeof value !== 'object') return false
|
|
549
|
-
if (seen.has(value)) return false
|
|
550
|
-
seen.add(value)
|
|
551
|
-
const valid = Array.isArray(value)
|
|
552
|
-
? value.every((item) => isJSONCompatible(item, seen))
|
|
553
|
-
: Object.getPrototypeOf(value) === Object.prototype &&
|
|
554
|
-
Object.values(value).every((item) => isJSONCompatible(item, seen))
|
|
555
|
-
seen.delete(value)
|
|
556
|
-
return valid
|
|
557
|
-
}
|
|
558
|
-
|
|
559
540
|
const jsonObject = <T extends object>(
|
|
560
541
|
label: string,
|
|
561
542
|
check: (value: Record<string, unknown>) => boolean,
|
|
@@ -584,45 +565,39 @@ const optionalStringField = (
|
|
|
584
565
|
key: string,
|
|
585
566
|
): boolean => value[key] === undefined || typeof value[key] === 'string'
|
|
586
567
|
|
|
587
|
-
const messageValue = (value: unknown): value is UIMessage =>
|
|
588
|
-
isRecord(value) &&
|
|
589
|
-
typeof value['id'] === 'string' &&
|
|
590
|
-
(value['role'] === 'user' ||
|
|
591
|
-
value['role'] === 'assistant' ||
|
|
592
|
-
value['role'] === 'system') &&
|
|
593
|
-
Array.isArray(value['parts']) &&
|
|
594
|
-
value['parts'].every(
|
|
595
|
-
(part) => isRecord(part) && typeof part['type'] === 'string',
|
|
596
|
-
) &&
|
|
597
|
-
isJSONCompatible(value)
|
|
598
|
-
|
|
599
568
|
const validateMessage = <M extends UIMessage>(
|
|
600
569
|
value: unknown,
|
|
601
570
|
messageSchema?: StandardSchemaV1<M>,
|
|
602
571
|
): SchemaResult<M> => {
|
|
603
|
-
if (!
|
|
572
|
+
if (!isUIMessage(value)) return issue('invalid AI SDK UIMessage')
|
|
604
573
|
if (!messageSchema) return { value: value as M }
|
|
605
574
|
return validateSync(messageSchema, value, 'the AI message')
|
|
606
575
|
}
|
|
607
576
|
|
|
608
|
-
const
|
|
609
|
-
|
|
610
|
-
)
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
577
|
+
const messageCreatedSchema: StandardSchemaV1<MessageCreatedPayload> = schema(
|
|
578
|
+
'ai.message.created',
|
|
579
|
+
(value) => {
|
|
580
|
+
if (
|
|
581
|
+
!isRecord(value) ||
|
|
582
|
+
Object.keys(value).some(
|
|
583
|
+
(key) => key !== 'messageId' && key !== 'role' && key !== 'generate',
|
|
584
|
+
) ||
|
|
585
|
+
!stringField(value, 'messageId') ||
|
|
586
|
+
(value['role'] !== 'user' &&
|
|
587
|
+
value['role'] !== 'assistant' &&
|
|
588
|
+
value['role'] !== 'system') ||
|
|
589
|
+
(value['generate'] !== undefined && value['generate'] !== false)
|
|
590
|
+
)
|
|
591
|
+
return issue('invalid ai.message.created payload')
|
|
619
592
|
return {
|
|
620
593
|
value: {
|
|
621
|
-
|
|
622
|
-
|
|
594
|
+
messageId: value['messageId'] as string,
|
|
595
|
+
role: value['role'],
|
|
596
|
+
...(value['generate'] === false ? { generate: false as const } : {}),
|
|
623
597
|
},
|
|
624
598
|
}
|
|
625
|
-
}
|
|
599
|
+
},
|
|
600
|
+
)
|
|
626
601
|
|
|
627
602
|
const progressPayloadSchema: StandardSchemaV1<GenerationProgressPayload> =
|
|
628
603
|
schema('ai.generation.progress', (value) => {
|
|
@@ -731,6 +706,14 @@ export function createEvents<M extends UIMessage = UIMessage>(options?: {
|
|
|
731
706
|
'AI command',
|
|
732
707
|
)
|
|
733
708
|
if (payload.issues) return { issues: payload.issues }
|
|
709
|
+
if (
|
|
710
|
+
action === 'interrupt' &&
|
|
711
|
+
isRecord(payload.value) &&
|
|
712
|
+
payload.value['reason'] === STEERED_REASON
|
|
713
|
+
)
|
|
714
|
+
return issue(
|
|
715
|
+
`the interruption reason "${STEERED_REASON}" is reserved for steering`,
|
|
716
|
+
)
|
|
734
717
|
return { value: { action, [key]: payload.value } as ControlCommand<M> }
|
|
735
718
|
}
|
|
736
719
|
if (action === 'send' || action === 'edit' || action === 'steer') {
|
|
@@ -857,7 +840,7 @@ export function createEvents<M extends UIMessage = UIMessage>(options?: {
|
|
|
857
840
|
'ai.session.closed': jsonObject('ai.session.closed', (value) =>
|
|
858
841
|
optionalStringField(value, 'reason'),
|
|
859
842
|
),
|
|
860
|
-
'ai.message.created':
|
|
843
|
+
'ai.message.created': messageCreatedSchema,
|
|
861
844
|
'ai.message.completed': jsonObject('ai.message.completed', (value) =>
|
|
862
845
|
stringField(value, 'messageId'),
|
|
863
846
|
),
|
|
@@ -1036,7 +1019,7 @@ export function createEvents<M extends UIMessage = UIMessage>(options?: {
|
|
|
1036
1019
|
export const events: AIEventDefs<UIMessage> = createEvents()
|
|
1037
1020
|
|
|
1038
1021
|
const initialState = <M extends UIMessage>(): AIState<M> => ({
|
|
1039
|
-
inbox:
|
|
1022
|
+
inbox: { paused: false, items: [] },
|
|
1040
1023
|
pendingQueueCommands: [],
|
|
1041
1024
|
active: null,
|
|
1042
1025
|
starting: false,
|
|
@@ -1153,6 +1136,22 @@ export function reduceAIState<M extends UIMessage>(
|
|
|
1153
1136
|
)
|
|
1154
1137
|
}
|
|
1155
1138
|
|
|
1139
|
+
/**
|
|
1140
|
+
* An input's content, while it is queued or about to enter the transcript:
|
|
1141
|
+
* its inbox item's, or its pending send's or steer's, whose command id is the
|
|
1142
|
+
* input's id.
|
|
1143
|
+
*/
|
|
1144
|
+
function inputContent<M extends UIMessage>(state: AIState<M>, id: string): M {
|
|
1145
|
+
const queued = state.inbox.items.find((item) => item.id === id)?.message
|
|
1146
|
+
if (queued !== undefined) return queued
|
|
1147
|
+
const command = state.pendingQueueCommands?.find(
|
|
1148
|
+
(item) => item.commandId === id,
|
|
1149
|
+
)?.command
|
|
1150
|
+
if (command?.action === 'send' || command?.action === 'steer')
|
|
1151
|
+
return command.message
|
|
1152
|
+
throw unexplainedAdmission(id)
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1156
1155
|
function reduceAIEvent<M extends UIMessage>(
|
|
1157
1156
|
state: AIState<M>,
|
|
1158
1157
|
event: { type: string; payload: unknown; index: number; id?: string },
|
|
@@ -1177,12 +1176,35 @@ function reduceAIEvent<M extends UIMessage>(
|
|
|
1177
1176
|
}
|
|
1178
1177
|
case 'ai.control.committed': {
|
|
1179
1178
|
const commit = event.payload as ControlCommit
|
|
1180
|
-
const
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1179
|
+
const changes = commit.changes.filter(
|
|
1180
|
+
(change) => change.path[0] === 'inbox' || change.path[0] === 'active',
|
|
1181
|
+
)
|
|
1182
|
+
let { inbox, active } = state
|
|
1183
|
+
if (changes.length > 0) {
|
|
1184
|
+
// A commit holds inputs by reference; their content is the commands'.
|
|
1185
|
+
const visible = applyControlChanges({
|
|
1186
|
+
state: {
|
|
1187
|
+
inbox: {
|
|
1188
|
+
...inbox,
|
|
1189
|
+
items: inbox.items.map(
|
|
1190
|
+
({ message: _, ...input }): InputRef => input,
|
|
1191
|
+
),
|
|
1192
|
+
},
|
|
1193
|
+
active,
|
|
1194
|
+
},
|
|
1195
|
+
changes,
|
|
1196
|
+
})
|
|
1197
|
+
const queued = new Map(inbox.items.map((item) => [item.id, item]))
|
|
1198
|
+
inbox = {
|
|
1199
|
+
...visible.inbox,
|
|
1200
|
+
items: visible.inbox.items.map((input) => ({
|
|
1201
|
+
...input,
|
|
1202
|
+
message:
|
|
1203
|
+
queued.get(input.id)?.message ?? inputContent(state, input.id),
|
|
1204
|
+
})),
|
|
1205
|
+
}
|
|
1206
|
+
active = visible.active
|
|
1207
|
+
}
|
|
1186
1208
|
const { phase } = commit.view
|
|
1187
1209
|
const status: AgentStatus = commit.view.closed
|
|
1188
1210
|
? 'closed'
|
|
@@ -1199,7 +1221,8 @@ function reduceAIEvent<M extends UIMessage>(
|
|
|
1199
1221
|
: 'idle'
|
|
1200
1222
|
return {
|
|
1201
1223
|
...state,
|
|
1202
|
-
|
|
1224
|
+
inbox,
|
|
1225
|
+
active,
|
|
1203
1226
|
status,
|
|
1204
1227
|
activeRequestId: commit.view.requestId,
|
|
1205
1228
|
activeRequestReason: commit.view.requestReason,
|
|
@@ -1214,9 +1237,24 @@ function reduceAIEvent<M extends UIMessage>(
|
|
|
1214
1237
|
const settled = pending.find(
|
|
1215
1238
|
(item) => item.commandId === receipt.commandId,
|
|
1216
1239
|
)
|
|
1240
|
+
// An edit's content takes effect at its receipt.
|
|
1241
|
+
const edited =
|
|
1242
|
+
receipt.outcome === 'applied' && settled?.command.action === 'edit'
|
|
1243
|
+
? settled.command.message
|
|
1244
|
+
: undefined
|
|
1217
1245
|
return {
|
|
1218
1246
|
...state,
|
|
1219
1247
|
receipt,
|
|
1248
|
+
...(edited === undefined
|
|
1249
|
+
? {}
|
|
1250
|
+
: {
|
|
1251
|
+
inbox: {
|
|
1252
|
+
...state.inbox,
|
|
1253
|
+
items: state.inbox.items.map((item) =>
|
|
1254
|
+
item.id === edited.id ? { ...item, message: edited } : item,
|
|
1255
|
+
),
|
|
1256
|
+
},
|
|
1257
|
+
}),
|
|
1220
1258
|
...(settled === undefined
|
|
1221
1259
|
? {}
|
|
1222
1260
|
: {
|
|
@@ -1329,7 +1367,8 @@ function reduceAIEvent<M extends UIMessage>(
|
|
|
1329
1367
|
activeProjection: null,
|
|
1330
1368
|
}
|
|
1331
1369
|
case 'ai.message.created': {
|
|
1332
|
-
const {
|
|
1370
|
+
const { messageId } = event.payload as MessageCreatedPayload
|
|
1371
|
+
const message = inputContent(state, messageId)
|
|
1333
1372
|
return {
|
|
1334
1373
|
...state,
|
|
1335
1374
|
messages: upsertMessage(state.messages, message),
|
|
@@ -2516,7 +2555,7 @@ export function handlerContext<D extends AgentToolEventDefs = AIEventDefs>(
|
|
|
2516
2555
|
}
|
|
2517
2556
|
|
|
2518
2557
|
/** The built-in AI reducer's name, and so its checkpoint identity. */
|
|
2519
|
-
export const AI_STATE_REDUCER = 'a2.ai.state.
|
|
2558
|
+
export const AI_STATE_REDUCER = 'a2.ai.state.v27'
|
|
2520
2559
|
|
|
2521
2560
|
/**
|
|
2522
2561
|
* The built-in AI protocol plus application events, as a core contract whose
|
package/src/internal.ts
CHANGED
|
@@ -274,12 +274,18 @@ export const DRAIN_TIMINGS: {
|
|
|
274
274
|
watchdogGraceMs: number
|
|
275
275
|
/** Maximum time a top-level append waits for its initial scheduler arm. */
|
|
276
276
|
schedulerArmTimeoutMs: number
|
|
277
|
+
/** First retry delay after a caught failure, doubling per failure. */
|
|
278
|
+
failureRetryBaseMs: number
|
|
279
|
+
/** Ceiling of the caught-failure retry delay. */
|
|
280
|
+
failureRetryMaxMs: number
|
|
277
281
|
} = {
|
|
278
282
|
claimTtlMs: 5_000,
|
|
279
283
|
claimHeartbeatMs: 2_000,
|
|
280
284
|
renewRetryMs: 400,
|
|
281
285
|
watchdogGraceMs: 1_000,
|
|
282
286
|
schedulerArmTimeoutMs: 2_000,
|
|
287
|
+
failureRetryBaseMs: 1_000,
|
|
288
|
+
failureRetryMaxMs: 30_000,
|
|
283
289
|
}
|
|
284
290
|
|
|
285
291
|
/**
|
package/src/libfx.d.ts
CHANGED
|
@@ -14,8 +14,15 @@ declare module 'libfx' {
|
|
|
14
14
|
cancel(): void
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
export type FxPromptBlock =
|
|
18
|
+
| { type: 'text'; text: string }
|
|
19
|
+
| { type: 'image'; data: string; mimeType: string }
|
|
20
|
+
|
|
17
21
|
export type FxAgent = {
|
|
18
|
-
prompt(
|
|
22
|
+
prompt(
|
|
23
|
+
input: string | FxPromptBlock[],
|
|
24
|
+
options?: { signal?: AbortSignal },
|
|
25
|
+
): FxTurn
|
|
19
26
|
checkpoint(): Promise<Uint8Array>
|
|
20
27
|
close(): Promise<void>
|
|
21
28
|
}
|
package/src/scheduler-qstash.ts
CHANGED
|
@@ -912,7 +912,7 @@ export function qstash(options: QStashSchedulerOptions = {}): A2Scheduler {
|
|
|
912
912
|
}
|
|
913
913
|
if (result.settled) return response.ok()
|
|
914
914
|
|
|
915
|
-
if (result.outcome === 'busy') {
|
|
915
|
+
if (result.outcome === 'busy' || result.outcome === 'stalled') {
|
|
916
916
|
if (result.schedulerArm) await result.schedulerArm
|
|
917
917
|
else {
|
|
918
918
|
await sendTask({
|
|
@@ -926,10 +926,10 @@ export function qstash(options: QStashSchedulerOptions = {}): A2Scheduler {
|
|
|
926
926
|
return response.ok()
|
|
927
927
|
}
|
|
928
928
|
|
|
929
|
-
//
|
|
930
|
-
//
|
|
931
|
-
//
|
|
932
|
-
//
|
|
929
|
+
// A drain whose outcome is unknown hands its work to a fresh durable
|
|
930
|
+
// message: QStash's per-message retry default can be lower than A2's
|
|
931
|
+
// failure budget. If this publish fails, the throw keeps the current
|
|
932
|
+
// delivery retryable.
|
|
933
933
|
await sendTask({
|
|
934
934
|
version: 1,
|
|
935
935
|
kind: 'drain',
|
package/src/scheduler-vercel.ts
CHANGED
|
@@ -14,8 +14,7 @@
|
|
|
14
14
|
* the configured deployment scope for sending and receiving.
|
|
15
15
|
* - `handler(...servers)` is the delivery route: look up the server for
|
|
16
16
|
* the contract named in the message and drain it. Settled drains ack;
|
|
17
|
-
* contention
|
|
18
|
-
* the current message and redelivers with backoff.
|
|
17
|
+
* contention and handler failure arm a later watchdog before acking.
|
|
19
18
|
*
|
|
20
19
|
* `@vercel/queue` is an optional peer dependency, imported lazily. The
|
|
21
20
|
* `transport` option is the injection seam — tests run a real little
|
|
@@ -44,12 +43,12 @@ import {
|
|
|
44
43
|
} from './server.ts'
|
|
45
44
|
|
|
46
45
|
/**
|
|
47
|
-
* The "not settled yet" signal for a
|
|
48
|
-
* not a transport failure. The default transport classifies
|
|
49
|
-
* `@vercel/queue`'s `retry` option and reschedules the delivery quietly
|
|
46
|
+
* The "not settled yet" signal for a drain whose outcome is unknown — an
|
|
47
|
+
* expected state, not a transport failure. The default transport classifies
|
|
48
|
+
* it in `@vercel/queue`'s `retry` option and reschedules the delivery quietly
|
|
50
49
|
* (`{ afterSeconds }` → visibility change → 200 response, no error log)
|
|
51
|
-
* instead of letting it surface as a thrown 500. Busy
|
|
52
|
-
* fresh watchdog instead. Custom transports should treat this class the
|
|
50
|
+
* instead of letting it surface as a thrown 500. Busy and stalled deliveries
|
|
51
|
+
* use a fresh watchdog instead. Custom transports should treat this class the
|
|
53
52
|
* same way when their protocol has a polite reschedule.
|
|
54
53
|
*/
|
|
55
54
|
export class DrainUnsettled extends Error {}
|
|
@@ -101,12 +100,9 @@ export type VercelQueuesOptions = {
|
|
|
101
100
|
/** Vercel Queues rejects shorter visibility changes. */
|
|
102
101
|
const MIN_VISIBILITY_SECONDS = 30
|
|
103
102
|
|
|
104
|
-
/** Vercel Queues' maximum message retention
|
|
103
|
+
/** Vercel Queues' maximum message retention; its default is a day. */
|
|
105
104
|
const MAX_RETENTION_SECONDS = 7 * 24 * 60 * 60
|
|
106
105
|
|
|
107
|
-
/** Leave the final retention day for delivery and retries. */
|
|
108
|
-
const MAX_DELAY_SECONDS = 6 * 24 * 60 * 60
|
|
109
|
-
|
|
110
106
|
const TOPIC_PATTERN = /^[A-Za-z0-9_-]+$/
|
|
111
107
|
const RETRYABLE_HTTP_CLIENT_STATUSES = new Set([408, 409, 425, 429])
|
|
112
108
|
/** The SDK's send path maps these statuses to name-only error instances. */
|
|
@@ -258,14 +254,6 @@ export function vercelQueues(options: VercelQueuesOptions = {}): A2Scheduler {
|
|
|
258
254
|
0,
|
|
259
255
|
Math.ceil((deliveryTargetAt - nowMs) / 1_000),
|
|
260
256
|
)
|
|
261
|
-
if (deliveryDelaySeconds > MAX_DELAY_SECONDS) {
|
|
262
|
-
throw classifySchedulerSendFailure(
|
|
263
|
-
new RangeError(
|
|
264
|
-
'vercelQueues() tasks cannot be delayed more than 6 days',
|
|
265
|
-
),
|
|
266
|
-
'terminal',
|
|
267
|
-
)
|
|
268
|
-
}
|
|
269
257
|
const message: SchedulerTask = { ...task, dueAt }
|
|
270
258
|
const idempotencyKey =
|
|
271
259
|
task.kind === 'drain'
|
|
@@ -354,11 +342,12 @@ export function vercelQueues(options: VercelQueuesOptions = {}): A2Scheduler {
|
|
|
354
342
|
}
|
|
355
343
|
if (result.settled) return
|
|
356
344
|
|
|
357
|
-
if (result.outcome === 'busy') {
|
|
358
|
-
// The live owner
|
|
359
|
-
// Ensure a later slot here
|
|
360
|
-
// durable delivery. If the queue is down, the
|
|
361
|
-
// current message unacked for visibility-timeout
|
|
345
|
+
if (result.outcome === 'busy' || result.outcome === 'stalled') {
|
|
346
|
+
// The live owner arms its own claim windows, and the store holds
|
|
347
|
+
// a failed event until its retry time. Ensure a later slot here
|
|
348
|
+
// as well before relinquishing this durable delivery. If the queue is down, the
|
|
349
|
+
// throw leaves the current message unacked for visibility-timeout
|
|
350
|
+
// redelivery.
|
|
362
351
|
if (result.schedulerArm) await result.schedulerArm
|
|
363
352
|
else {
|
|
364
353
|
await sendTask({
|
|
@@ -372,9 +361,8 @@ export function vercelQueues(options: VercelQueuesOptions = {}): A2Scheduler {
|
|
|
372
361
|
return
|
|
373
362
|
}
|
|
374
363
|
|
|
375
|
-
//
|
|
376
|
-
// transport retry path.
|
|
377
|
-
// above, so retries stop there.
|
|
364
|
+
// A drain whose outcome is unknown keeps the current durable message
|
|
365
|
+
// and uses the transport retry path.
|
|
378
366
|
throw new DrainUnsettled(
|
|
379
367
|
`a2 scheduler: session '${task.sessionId}' of '${task.contract}' not settled yet (${result.outcome}) — rescheduled for redelivery`,
|
|
380
368
|
)
|
package/src/server.ts
CHANGED
|
@@ -205,9 +205,15 @@ export type DeadLetter<D extends EventDefs> = ContractEvent<D> & {
|
|
|
205
205
|
lane: string | null
|
|
206
206
|
/** Durable dispatch claims, including claims abandoned by hard kills. */
|
|
207
207
|
attemptCount: number
|
|
208
|
-
/**
|
|
208
|
+
/**
|
|
209
|
+
* Failed attempts since the event was appended or last retried: caught
|
|
210
|
+
* handler failures, and attempts that ended before they finished.
|
|
211
|
+
*/
|
|
209
212
|
failureCount: number
|
|
210
|
-
/**
|
|
213
|
+
/**
|
|
214
|
+
* The last error a handler threw, stringified; null when every failed
|
|
215
|
+
* attempt ended before it finished.
|
|
216
|
+
*/
|
|
211
217
|
lastError: string | null
|
|
212
218
|
failedAt: Date
|
|
213
219
|
}
|
|
@@ -499,6 +505,13 @@ export type ServerOptions<
|
|
|
499
505
|
|
|
500
506
|
const MAX_FAILURES = 10
|
|
501
507
|
|
|
508
|
+
/** How long an event waits after its `failureCount`th failed attempt. */
|
|
509
|
+
const failureRetryDelayMs = (failureCount: number): number =>
|
|
510
|
+
Math.min(
|
|
511
|
+
DRAIN_TIMINGS.failureRetryBaseMs * 2 ** Math.max(failureCount - 1, 0),
|
|
512
|
+
DRAIN_TIMINGS.failureRetryMaxMs,
|
|
513
|
+
)
|
|
514
|
+
|
|
502
515
|
/** Abort a lost attempt's handler and release its drain slot. */
|
|
503
516
|
function endSuperseded(
|
|
504
517
|
entry: {
|
|
@@ -527,6 +540,7 @@ type ClaimWindow = {
|
|
|
527
540
|
expiresAtMs: number
|
|
528
541
|
watchdogAtMs: number
|
|
529
542
|
deadlineCapped: boolean
|
|
543
|
+
pastDeadline: boolean
|
|
530
544
|
}
|
|
531
545
|
|
|
532
546
|
type DrainSignal = {
|
|
@@ -601,9 +615,11 @@ function pruneAbortHub(hub: AbortHub): void {
|
|
|
601
615
|
}
|
|
602
616
|
|
|
603
617
|
/**
|
|
604
|
-
* Deadlines tighten the claim/watchdog window
|
|
605
|
-
*
|
|
606
|
-
*
|
|
618
|
+
* Deadlines tighten the claim/watchdog window, and a passed deadline ends
|
|
619
|
+
* claiming: a claim then could not run, only lapse and be claimed again until
|
|
620
|
+
* the platform ends the process. A platform timeout otherwise follows the
|
|
621
|
+
* same path as any other process death: the in-flight event remains pending
|
|
622
|
+
* and a fresh invocation retries it.
|
|
607
623
|
*/
|
|
608
624
|
function nextClaimWindow(): ClaimWindow {
|
|
609
625
|
const nowMs = Date.now()
|
|
@@ -619,6 +635,7 @@ function nextClaimWindow(): ClaimWindow {
|
|
|
619
635
|
expiresAtMs,
|
|
620
636
|
watchdogAtMs: expiresAtMs + DRAIN_TIMINGS.watchdogGraceMs,
|
|
621
637
|
deadlineCapped,
|
|
638
|
+
pastDeadline: deadlineMs !== null && deadlineTtlMs <= 0,
|
|
622
639
|
}
|
|
623
640
|
}
|
|
624
641
|
|
|
@@ -3068,6 +3085,25 @@ export function createServer<
|
|
|
3068
3085
|
return tracked
|
|
3069
3086
|
}
|
|
3070
3087
|
|
|
3088
|
+
/** Records an event the store dead-lettered on takeover; it never runs. */
|
|
3089
|
+
const reportExhausted = (event: StoredEvent): Promise<void> =>
|
|
3090
|
+
telemetry.span(
|
|
3091
|
+
'a2.event',
|
|
3092
|
+
{
|
|
3093
|
+
'a2.contract': name,
|
|
3094
|
+
'a2.session_id': sessionId,
|
|
3095
|
+
'a2.event.type': event.type,
|
|
3096
|
+
'a2.event.index': event.index,
|
|
3097
|
+
'a2.event.id': event.id,
|
|
3098
|
+
'a2.event.attempt': event.attemptCount,
|
|
3099
|
+
'a2.event.handled': handlers.has(event.type),
|
|
3100
|
+
...(event.lane === null ? {} : { 'a2.event.lane': event.lane }),
|
|
3101
|
+
},
|
|
3102
|
+
async (eventSpan) => {
|
|
3103
|
+
eventSpan.setAttribute('a2.event.outcome', 'dead_lettered')
|
|
3104
|
+
},
|
|
3105
|
+
)
|
|
3106
|
+
|
|
3071
3107
|
const runEvent = async (
|
|
3072
3108
|
event: StoredEvent,
|
|
3073
3109
|
supersedeSignal: AbortSignal,
|
|
@@ -3127,9 +3163,8 @@ export function createServer<
|
|
|
3127
3163
|
if (supersedeSignal.aborted && err === supersedeSignal.reason) {
|
|
3128
3164
|
// The handler surrendered to its own abort — identity, not
|
|
3129
3165
|
// code, so it cannot arrive from another context. A lapse
|
|
3130
|
-
// is not a handler failure: no completion
|
|
3131
|
-
//
|
|
3132
|
-
// retries.
|
|
3166
|
+
// is not a handler failure: no completion; the claim simply
|
|
3167
|
+
// expires, and the claim that takes it over counts it.
|
|
3133
3168
|
eventSpan.setAttribute('a2.event.outcome', 'surrendered')
|
|
3134
3169
|
return {
|
|
3135
3170
|
status: 'surrendered',
|
|
@@ -3162,6 +3197,7 @@ export function createServer<
|
|
|
3162
3197
|
// budget before dead-lettering.
|
|
3163
3198
|
maxFailures:
|
|
3164
3199
|
err instanceof NonRetriableError ? 1 : MAX_FAILURES,
|
|
3200
|
+
retryAfterMs: failureRetryDelayMs(event.failureCount + 1),
|
|
3165
3201
|
})
|
|
3166
3202
|
commitSpan.setAttribute('a2.commit.outcome', result.outcome)
|
|
3167
3203
|
return result
|
|
@@ -3293,6 +3329,10 @@ export function createServer<
|
|
|
3293
3329
|
let claim: StoreClaimAvailableResult | undefined
|
|
3294
3330
|
if (shouldClaim) {
|
|
3295
3331
|
const window = nextClaimWindow()
|
|
3332
|
+
if (window.pastDeadline) {
|
|
3333
|
+
outcome = 'busy'
|
|
3334
|
+
break
|
|
3335
|
+
}
|
|
3296
3336
|
claim = await telemetry.span(
|
|
3297
3337
|
'a2.claim',
|
|
3298
3338
|
{ 'a2.contract': name, 'a2.session_id': sessionId },
|
|
@@ -3301,6 +3341,7 @@ export function createServer<
|
|
|
3301
3341
|
sessionId: ns,
|
|
3302
3342
|
holder,
|
|
3303
3343
|
ttlMs: window.ttlMs,
|
|
3344
|
+
maxFailures: MAX_FAILURES,
|
|
3304
3345
|
...(window.deadlineCapped
|
|
3305
3346
|
? { expiresAtMs: window.expiresAtMs }
|
|
3306
3347
|
: {}),
|
|
@@ -3317,11 +3358,16 @@ export function createServer<
|
|
|
3317
3358
|
},
|
|
3318
3359
|
)
|
|
3319
3360
|
if (claim.outcome === 'claimed') {
|
|
3320
|
-
|
|
3361
|
+
// A takeover that spent the last failed attempt comes back
|
|
3362
|
+
// dead-lettered instead of claimed.
|
|
3363
|
+
const runnable = claim.events.filter(
|
|
3364
|
+
(event) => event.failedAt === null,
|
|
3365
|
+
)
|
|
3366
|
+
for (const event of claim.events)
|
|
3367
|
+
if (event.failedAt !== null) await reportExhausted(event)
|
|
3368
|
+
if (runnable.length > 0) requestWatchdog(window.watchdogAtMs)
|
|
3321
3369
|
if (window.deadlineCapped) stopRenewing = true
|
|
3322
|
-
for (const event of
|
|
3323
|
-
start(event, window.expiresAtMs)
|
|
3324
|
-
}
|
|
3370
|
+
for (const event of runnable) start(event, window.expiresAtMs)
|
|
3325
3371
|
shouldClaim = false
|
|
3326
3372
|
}
|
|
3327
3373
|
}
|