ai 6.0.270 → 6.0.272
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 +22 -0
- package/dist/index.d.mts +126 -49
- package/dist/index.d.ts +126 -49
- package/dist/index.js +609 -394
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +545 -328
- package/dist/index.mjs.map +1 -1
- package/dist/internal/index.js +1 -1
- package/dist/internal/index.mjs +1 -1
- package/docs/03-ai-sdk-core/15-tools-and-tool-calling.mdx +26 -1
- package/docs/07-reference/01-ai-sdk-core/02-stream-text.mdx +2 -2
- package/docs/07-reference/02-ai-sdk-ui/40-create-ui-message-stream.mdx +33 -5
- package/docs/07-reference/05-ai-sdk-errors/ai-tool-choice-violation-error.mdx +38 -0
- package/docs/07-reference/05-ai-sdk-errors/index.mdx +1 -0
- package/package.json +3 -3
- package/src/agent/create-agent-ui-stream.ts +2 -2
- package/src/agent/tool-loop-agent-settings.ts +9 -0
- package/src/error/index.ts +1 -0
- package/src/error/tool-choice-violation-error.ts +80 -0
- package/src/generate-text/generate-text.ts +25 -1
- package/src/generate-text/stream-text.ts +84 -1
- package/src/ui/direct-chat-transport.ts +2 -2
- package/src/ui/validate-ui-messages.ts +136 -55
- package/src/ui-message-stream/create-ui-message-stream.ts +47 -17
- package/src/ui-message-stream/handle-ui-message-stream-finish.ts +58 -15
- package/src/ui-message-stream/index.ts +5 -1
- package/src/ui-message-stream/ui-message-stream-on-finish-callback.ts +7 -0
- package/src/ui-message-stream/ui-message-stream-outcome.ts +12 -0
- package/src/ui-message-stream/ui-message-stream-writer.ts +15 -0
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
} from '@ai-sdk/provider';
|
|
6
6
|
import {
|
|
7
7
|
lazySchema,
|
|
8
|
+
safeValidateTypes,
|
|
8
9
|
StandardSchemaV1,
|
|
9
10
|
validateTypes,
|
|
10
11
|
zodSchema,
|
|
@@ -17,6 +18,7 @@ import { jsonValueSchema } from '../types/json-value';
|
|
|
17
18
|
import { providerMetadataSchema } from '../types/provider-metadata';
|
|
18
19
|
import type {
|
|
19
20
|
DataUIPart,
|
|
21
|
+
DynamicToolUIPart,
|
|
20
22
|
InferUIMessageData,
|
|
21
23
|
InferUIMessageTools,
|
|
22
24
|
ToolUIPart,
|
|
@@ -28,6 +30,25 @@ const toolMetadataSchema: z.ZodType<JSONObject> = z.record(
|
|
|
28
30
|
jsonValueSchema.optional(),
|
|
29
31
|
);
|
|
30
32
|
|
|
33
|
+
function isEmptyObject(value: unknown): value is Record<string, never> {
|
|
34
|
+
return (
|
|
35
|
+
value != null &&
|
|
36
|
+
typeof value === 'object' &&
|
|
37
|
+
!Array.isArray(value) &&
|
|
38
|
+
Object.keys(value).length === 0
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function asDynamicToolPart(toolPart: ToolUIPart): DynamicToolUIPart {
|
|
43
|
+
const { type, ...part } = toolPart;
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
...part,
|
|
47
|
+
type: 'dynamic-tool',
|
|
48
|
+
toolName: type.slice(5),
|
|
49
|
+
} as DynamicToolUIPart;
|
|
50
|
+
}
|
|
51
|
+
|
|
31
52
|
const uiMessagesSchema = lazySchema(() =>
|
|
32
53
|
zodSchema(
|
|
33
54
|
z
|
|
@@ -354,17 +375,7 @@ export type SafeValidateUIMessagesResult<UI_MESSAGE extends UIMessage> =
|
|
|
354
375
|
error: Error;
|
|
355
376
|
};
|
|
356
377
|
|
|
357
|
-
|
|
358
|
-
* Validates a list of UI messages like `validateUIMessages`,
|
|
359
|
-
* but instead of throwing it returns `{ success: true, data }`
|
|
360
|
-
* or `{ success: false, error }`.
|
|
361
|
-
*/
|
|
362
|
-
export async function safeValidateUIMessages<UI_MESSAGE extends UIMessage>({
|
|
363
|
-
messages,
|
|
364
|
-
metadataSchema,
|
|
365
|
-
dataSchemas,
|
|
366
|
-
tools,
|
|
367
|
-
}: {
|
|
378
|
+
type ValidateUIMessagesOptions<UI_MESSAGE extends UIMessage> = {
|
|
368
379
|
messages: unknown;
|
|
369
380
|
metadataSchema?: FlexibleSchema<UIMessage['metadata']>;
|
|
370
381
|
dataSchemas?: {
|
|
@@ -378,7 +389,21 @@ export async function safeValidateUIMessages<UI_MESSAGE extends UIMessage>({
|
|
|
378
389
|
InferUIMessageTools<UI_MESSAGE>[NAME]['output']
|
|
379
390
|
>;
|
|
380
391
|
};
|
|
381
|
-
}
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
async function safeValidateUIMessagesInternal<UI_MESSAGE extends UIMessage>(
|
|
395
|
+
{
|
|
396
|
+
messages,
|
|
397
|
+
metadataSchema,
|
|
398
|
+
dataSchemas,
|
|
399
|
+
tools,
|
|
400
|
+
}: ValidateUIMessagesOptions<UI_MESSAGE>,
|
|
401
|
+
{
|
|
402
|
+
convertMissingTerminalToolsToDynamic,
|
|
403
|
+
}: {
|
|
404
|
+
convertMissingTerminalToolsToDynamic: boolean;
|
|
405
|
+
},
|
|
406
|
+
): Promise<SafeValidateUIMessagesResult<UI_MESSAGE>> {
|
|
382
407
|
try {
|
|
383
408
|
if (messages == null) {
|
|
384
409
|
return {
|
|
@@ -409,7 +434,10 @@ export async function safeValidateUIMessages<UI_MESSAGE extends UIMessage>({
|
|
|
409
434
|
}
|
|
410
435
|
}
|
|
411
436
|
|
|
412
|
-
|
|
437
|
+
const shouldValidateToolParts =
|
|
438
|
+
tools != null || convertMissingTerminalToolsToDynamic;
|
|
439
|
+
|
|
440
|
+
if (dataSchemas || shouldValidateToolParts) {
|
|
413
441
|
for (const [msgIdx, message] of validatedMessages.entries()) {
|
|
414
442
|
for (const [partIdx, part] of message.parts.entries()) {
|
|
415
443
|
// Data part validation
|
|
@@ -445,19 +473,26 @@ export async function safeValidateUIMessages<UI_MESSAGE extends UIMessage>({
|
|
|
445
473
|
}
|
|
446
474
|
|
|
447
475
|
// Tool part validation
|
|
448
|
-
if (
|
|
476
|
+
if (shouldValidateToolParts && part.type.startsWith('tool-')) {
|
|
449
477
|
const toolPart = part as ToolUIPart<
|
|
450
478
|
InferUIMessageTools<UI_MESSAGE>
|
|
451
479
|
>;
|
|
452
480
|
const toolName = toolPart.type.slice(5);
|
|
453
|
-
const tool = tools[toolName];
|
|
481
|
+
const tool = tools?.[toolName];
|
|
482
|
+
const isTerminal =
|
|
483
|
+
toolPart.state === 'output-available' ||
|
|
484
|
+
toolPart.state === 'output-error' ||
|
|
485
|
+
toolPart.state === 'output-denied';
|
|
454
486
|
|
|
455
|
-
if (
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
487
|
+
if (!tool && isTerminal) {
|
|
488
|
+
if (tools != null || convertMissingTerminalToolsToDynamic) {
|
|
489
|
+
// Persisted terminal history can reference tools that are no
|
|
490
|
+
// longer registered. Normalize those parts so callers do not
|
|
491
|
+
// receive unvalidated values under current static tool types.
|
|
492
|
+
message.parts[partIdx] = asDynamicToolPart(
|
|
493
|
+
toolPart,
|
|
494
|
+
) as (typeof message.parts)[number];
|
|
495
|
+
}
|
|
461
496
|
continue;
|
|
462
497
|
}
|
|
463
498
|
|
|
@@ -477,19 +512,53 @@ export async function safeValidateUIMessages<UI_MESSAGE extends UIMessage>({
|
|
|
477
512
|
};
|
|
478
513
|
}
|
|
479
514
|
|
|
515
|
+
const inputValidationContext = {
|
|
516
|
+
field: `messages[${msgIdx}].parts[${partIdx}].input`,
|
|
517
|
+
entityName: toolName,
|
|
518
|
+
entityId: toolPart.toolCallId,
|
|
519
|
+
};
|
|
520
|
+
let convertToDynamic = false;
|
|
521
|
+
|
|
480
522
|
// Tool input validation
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
523
|
+
if (toolPart.state === 'output-error') {
|
|
524
|
+
// Failed calls can retain invalid input. Keep them loadable, but
|
|
525
|
+
// expose incompatible input as unknown instead of the current
|
|
526
|
+
// static tool input type.
|
|
527
|
+
if (toolPart.input !== undefined) {
|
|
528
|
+
const result = await safeValidateTypes({
|
|
529
|
+
value: toolPart.input,
|
|
530
|
+
schema: tool.inputSchema,
|
|
531
|
+
context: inputValidationContext,
|
|
532
|
+
});
|
|
533
|
+
convertToDynamic = !result.success;
|
|
534
|
+
}
|
|
535
|
+
} else if (toolPart.state === 'output-available') {
|
|
536
|
+
const result = await safeValidateTypes({
|
|
537
|
+
value: toolPart.input,
|
|
538
|
+
schema: tool.inputSchema,
|
|
539
|
+
context: inputValidationContext,
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
if (!result.success) {
|
|
543
|
+
// Empty terminal input can represent aborted or incomplete
|
|
544
|
+
// history whose input was never streamed. Preserve it without
|
|
545
|
+
// claiming that it matches the current static input type.
|
|
546
|
+
if (isEmptyObject(toolPart.input)) {
|
|
547
|
+
convertToDynamic = true;
|
|
548
|
+
} else {
|
|
549
|
+
throw result.error;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
} else if (
|
|
553
|
+
toolPart.state === 'input-available' ||
|
|
554
|
+
toolPart.state === 'approval-requested' ||
|
|
555
|
+
toolPart.state === 'approval-responded' ||
|
|
556
|
+
toolPart.state === 'output-denied'
|
|
557
|
+
) {
|
|
485
558
|
await validateTypes({
|
|
486
559
|
value: toolPart.input,
|
|
487
560
|
schema: tool.inputSchema,
|
|
488
|
-
context:
|
|
489
|
-
field: `messages[${msgIdx}].parts[${partIdx}].input`,
|
|
490
|
-
entityName: toolName,
|
|
491
|
-
entityId: toolPart.toolCallId,
|
|
492
|
-
},
|
|
561
|
+
context: inputValidationContext,
|
|
493
562
|
});
|
|
494
563
|
}
|
|
495
564
|
|
|
@@ -505,6 +574,12 @@ export async function safeValidateUIMessages<UI_MESSAGE extends UIMessage>({
|
|
|
505
574
|
},
|
|
506
575
|
});
|
|
507
576
|
}
|
|
577
|
+
|
|
578
|
+
if (convertToDynamic) {
|
|
579
|
+
message.parts[partIdx] = asDynamicToolPart(
|
|
580
|
+
toolPart,
|
|
581
|
+
) as (typeof message.parts)[number];
|
|
582
|
+
}
|
|
508
583
|
}
|
|
509
584
|
}
|
|
510
585
|
}
|
|
@@ -524,6 +599,19 @@ export async function safeValidateUIMessages<UI_MESSAGE extends UIMessage>({
|
|
|
524
599
|
}
|
|
525
600
|
}
|
|
526
601
|
|
|
602
|
+
/**
|
|
603
|
+
* Validates a list of UI messages like `validateUIMessages`,
|
|
604
|
+
* but instead of throwing it returns `{ success: true, data }`
|
|
605
|
+
* or `{ success: false, error }`.
|
|
606
|
+
*/
|
|
607
|
+
export async function safeValidateUIMessages<UI_MESSAGE extends UIMessage>(
|
|
608
|
+
options: ValidateUIMessagesOptions<UI_MESSAGE>,
|
|
609
|
+
): Promise<SafeValidateUIMessagesResult<UI_MESSAGE>> {
|
|
610
|
+
return safeValidateUIMessagesInternal(options, {
|
|
611
|
+
convertMissingTerminalToolsToDynamic: false,
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
527
615
|
/**
|
|
528
616
|
* Validates a list of UI messages.
|
|
529
617
|
*
|
|
@@ -531,31 +619,24 @@ export async function safeValidateUIMessages<UI_MESSAGE extends UIMessage>({
|
|
|
531
619
|
* the corresponding schemas are provided. Otherwise, they are assumed to be
|
|
532
620
|
* valid.
|
|
533
621
|
*/
|
|
534
|
-
export async function validateUIMessages<UI_MESSAGE extends UIMessage>(
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
};
|
|
553
|
-
}): Promise<Array<UI_MESSAGE>> {
|
|
554
|
-
const response = await safeValidateUIMessages({
|
|
555
|
-
messages,
|
|
556
|
-
metadataSchema,
|
|
557
|
-
dataSchemas,
|
|
558
|
-
tools,
|
|
622
|
+
export async function validateUIMessages<UI_MESSAGE extends UIMessage>(
|
|
623
|
+
options: ValidateUIMessagesOptions<UI_MESSAGE>,
|
|
624
|
+
): Promise<Array<UI_MESSAGE>> {
|
|
625
|
+
const response = await safeValidateUIMessages(options);
|
|
626
|
+
|
|
627
|
+
if (!response.success) throw response.error;
|
|
628
|
+
|
|
629
|
+
return response.data;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
export async function validateUIMessagesForAgent<UI_MESSAGE extends UIMessage>(
|
|
633
|
+
options: ValidateUIMessagesOptions<UI_MESSAGE>,
|
|
634
|
+
): Promise<Array<UI_MESSAGE>> {
|
|
635
|
+
const response = await safeValidateUIMessagesInternal(options, {
|
|
636
|
+
// Agent tool sets can include ephemeral tools (for example, tools from a
|
|
637
|
+
// disconnected MCP server), so terminal history is converted to dynamic
|
|
638
|
+
// tool parts when those tools are no longer registered.
|
|
639
|
+
convertMissingTerminalToolsToDynamic: true,
|
|
559
640
|
});
|
|
560
641
|
|
|
561
642
|
if (!response.success) throw response.error;
|
|
@@ -6,8 +6,9 @@ import type { UIMessage } from '../ui/ui-messages';
|
|
|
6
6
|
import { handleUIMessageStreamFinish } from './handle-ui-message-stream-finish';
|
|
7
7
|
import type { InferUIMessageChunk } from './ui-message-chunks';
|
|
8
8
|
import type { UIMessageStreamOnFinishCallback } from './ui-message-stream-on-finish-callback';
|
|
9
|
+
import type { UIMessageStreamOutcome } from './ui-message-stream-outcome';
|
|
9
10
|
import type { UIMessageStreamOnStepFinishCallback } from './ui-message-stream-on-step-finish-callback';
|
|
10
|
-
import type {
|
|
11
|
+
import type { UIMessageStreamWriterWithOutcome } from './ui-message-stream-writer';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Creates a UI message stream that can be used to send messages to the client.
|
|
@@ -31,7 +32,7 @@ export function createUIMessageStream<UI_MESSAGE extends UIMessage>({
|
|
|
31
32
|
generateId = generateIdFunc,
|
|
32
33
|
}: {
|
|
33
34
|
execute: (options: {
|
|
34
|
-
writer:
|
|
35
|
+
writer: UIMessageStreamWriterWithOutcome<UI_MESSAGE>;
|
|
35
36
|
}) => Promise<void> | void;
|
|
36
37
|
onError?: (error: unknown) => string;
|
|
37
38
|
|
|
@@ -55,6 +56,7 @@ export function createUIMessageStream<UI_MESSAGE extends UIMessage>({
|
|
|
55
56
|
>;
|
|
56
57
|
|
|
57
58
|
const ongoingStreamPromises: Promise<void>[] = [];
|
|
59
|
+
let outcome: UIMessageStreamOutcome = { status: 'unknown' };
|
|
58
60
|
|
|
59
61
|
const stream = new ReadableStream({
|
|
60
62
|
start(controllerArg) {
|
|
@@ -70,6 +72,42 @@ export function createUIMessageStream<UI_MESSAGE extends UIMessage>({
|
|
|
70
72
|
}
|
|
71
73
|
}
|
|
72
74
|
|
|
75
|
+
function setOutcome(newOutcome: UIMessageStreamOutcome) {
|
|
76
|
+
if (outcome.status === 'unknown' && newOutcome.status !== 'unknown') {
|
|
77
|
+
outcome = newOutcome;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function failOutcome(error: unknown) {
|
|
82
|
+
outcome = { status: 'failed', error };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function safeError(error: unknown) {
|
|
86
|
+
try {
|
|
87
|
+
controller.error(error);
|
|
88
|
+
} catch {
|
|
89
|
+
// suppress errors when the stream has been closed
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function handleError(error: unknown) {
|
|
94
|
+
failOutcome(error);
|
|
95
|
+
|
|
96
|
+
let errorText: string;
|
|
97
|
+
try {
|
|
98
|
+
errorText = onError(error);
|
|
99
|
+
} catch (onErrorError) {
|
|
100
|
+
failOutcome(onErrorError);
|
|
101
|
+
safeError(onErrorError);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
safeEnqueue({
|
|
106
|
+
type: 'error',
|
|
107
|
+
errorText,
|
|
108
|
+
} as InferUIMessageChunk<UI_MESSAGE>);
|
|
109
|
+
}
|
|
110
|
+
|
|
73
111
|
try {
|
|
74
112
|
const result = execute({
|
|
75
113
|
writer: {
|
|
@@ -86,13 +124,11 @@ export function createUIMessageStream<UI_MESSAGE extends UIMessage>({
|
|
|
86
124
|
safeEnqueue(value);
|
|
87
125
|
}
|
|
88
126
|
})().catch(error => {
|
|
89
|
-
|
|
90
|
-
type: 'error',
|
|
91
|
-
errorText: onError(error),
|
|
92
|
-
} as InferUIMessageChunk<UI_MESSAGE>);
|
|
127
|
+
handleError(error);
|
|
93
128
|
}),
|
|
94
129
|
);
|
|
95
130
|
},
|
|
131
|
+
setOutcome,
|
|
96
132
|
onError,
|
|
97
133
|
},
|
|
98
134
|
});
|
|
@@ -100,30 +136,23 @@ export function createUIMessageStream<UI_MESSAGE extends UIMessage>({
|
|
|
100
136
|
if (result) {
|
|
101
137
|
ongoingStreamPromises.push(
|
|
102
138
|
result.catch(error => {
|
|
103
|
-
|
|
104
|
-
type: 'error',
|
|
105
|
-
errorText: onError(error),
|
|
106
|
-
} as InferUIMessageChunk<UI_MESSAGE>);
|
|
139
|
+
handleError(error);
|
|
107
140
|
}),
|
|
108
141
|
);
|
|
109
142
|
}
|
|
110
143
|
} catch (error) {
|
|
111
|
-
|
|
112
|
-
type: 'error',
|
|
113
|
-
errorText: onError(error),
|
|
114
|
-
} as InferUIMessageChunk<UI_MESSAGE>);
|
|
144
|
+
handleError(error);
|
|
115
145
|
}
|
|
116
146
|
|
|
117
147
|
// Wait until all ongoing streams are done. This approach enables merging
|
|
118
148
|
// streams even after execute has returned, as long as there is still an
|
|
119
149
|
// open merged stream. This is important to e.g. forward new streams and
|
|
120
150
|
// from callbacks.
|
|
121
|
-
const waitForStreams: Promise<void> =
|
|
151
|
+
const waitForStreams: Promise<void> = (async () => {
|
|
122
152
|
while (ongoingStreamPromises.length > 0) {
|
|
123
153
|
await ongoingStreamPromises.shift();
|
|
124
154
|
}
|
|
125
|
-
|
|
126
|
-
});
|
|
155
|
+
})();
|
|
127
156
|
|
|
128
157
|
waitForStreams.finally(() => {
|
|
129
158
|
try {
|
|
@@ -140,5 +169,6 @@ export function createUIMessageStream<UI_MESSAGE extends UIMessage>({
|
|
|
140
169
|
onStepFinish,
|
|
141
170
|
onFinish,
|
|
142
171
|
onError,
|
|
172
|
+
getOutcome: () => outcome,
|
|
143
173
|
});
|
|
144
174
|
}
|
|
@@ -8,6 +8,7 @@ import type { UIMessage } from '../ui/ui-messages';
|
|
|
8
8
|
import type { ErrorHandler } from '../util/error-handler';
|
|
9
9
|
import type { InferUIMessageChunk, UIMessageChunk } from './ui-message-chunks';
|
|
10
10
|
import type { UIMessageStreamOnFinishCallback } from './ui-message-stream-on-finish-callback';
|
|
11
|
+
import type { UIMessageStreamOutcome } from './ui-message-stream-outcome';
|
|
11
12
|
import type { UIMessageStreamOnStepFinishCallback } from './ui-message-stream-on-step-finish-callback';
|
|
12
13
|
|
|
13
14
|
export function handleUIMessageStreamFinish<UI_MESSAGE extends UIMessage>({
|
|
@@ -17,6 +18,7 @@ export function handleUIMessageStreamFinish<UI_MESSAGE extends UIMessage>({
|
|
|
17
18
|
onFinish,
|
|
18
19
|
onError,
|
|
19
20
|
stream,
|
|
21
|
+
getOutcome,
|
|
20
22
|
}: {
|
|
21
23
|
stream: ReadableStream<InferUIMessageChunk<UI_MESSAGE>>;
|
|
22
24
|
|
|
@@ -39,6 +41,11 @@ export function handleUIMessageStreamFinish<UI_MESSAGE extends UIMessage>({
|
|
|
39
41
|
onStepFinish?: UIMessageStreamOnStepFinishCallback<UI_MESSAGE>;
|
|
40
42
|
|
|
41
43
|
onFinish?: UIMessageStreamOnFinishCallback<UI_MESSAGE>;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Returns the operation-level outcome declared by the stream owner.
|
|
47
|
+
*/
|
|
48
|
+
getOutcome?: () => UIMessageStreamOutcome;
|
|
42
49
|
}): ReadableStream<InferUIMessageChunk<UI_MESSAGE>> {
|
|
43
50
|
// last message is only relevant for assistant messages
|
|
44
51
|
let lastMessage: UI_MESSAGE | undefined =
|
|
@@ -51,6 +58,13 @@ export function handleUIMessageStreamFinish<UI_MESSAGE extends UIMessage>({
|
|
|
51
58
|
}
|
|
52
59
|
|
|
53
60
|
let isAborted = false;
|
|
61
|
+
let hasProcessingFailure = false;
|
|
62
|
+
let processingError: unknown;
|
|
63
|
+
|
|
64
|
+
const recordProcessingFailure = (error: unknown) => {
|
|
65
|
+
hasProcessingFailure = true;
|
|
66
|
+
processingError = error;
|
|
67
|
+
};
|
|
54
68
|
|
|
55
69
|
const idInjectedStream = stream.pipeThrough(
|
|
56
70
|
new TransformStream<
|
|
@@ -58,21 +72,31 @@ export function handleUIMessageStreamFinish<UI_MESSAGE extends UIMessage>({
|
|
|
58
72
|
InferUIMessageChunk<UI_MESSAGE>
|
|
59
73
|
>({
|
|
60
74
|
transform(chunk, controller) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
75
|
+
try {
|
|
76
|
+
let outputChunk = chunk;
|
|
77
|
+
|
|
78
|
+
// when there is no messageId in the start chunk,
|
|
79
|
+
// but the user checked for persistence,
|
|
80
|
+
// inject the messageId into the chunk
|
|
81
|
+
if (chunk.type === 'start') {
|
|
82
|
+
const startChunk = chunk as UIMessageChunk & { type: 'start' };
|
|
83
|
+
if (startChunk.messageId == null && messageId != null) {
|
|
84
|
+
outputChunk = {
|
|
85
|
+
...startChunk,
|
|
86
|
+
messageId,
|
|
87
|
+
} as InferUIMessageChunk<UI_MESSAGE>;
|
|
88
|
+
}
|
|
68
89
|
}
|
|
69
|
-
}
|
|
70
90
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
91
|
+
if (chunk.type === 'abort') {
|
|
92
|
+
isAborted = true;
|
|
93
|
+
}
|
|
74
94
|
|
|
75
|
-
|
|
95
|
+
controller.enqueue(outputChunk);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
recordProcessingFailure(error);
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
76
100
|
},
|
|
77
101
|
}),
|
|
78
102
|
);
|
|
@@ -95,7 +119,12 @@ export function handleUIMessageStreamFinish<UI_MESSAGE extends UIMessage>({
|
|
|
95
119
|
write: (options?: UIMessageStreamWriteOptions) => void;
|
|
96
120
|
}) => Promise<void>,
|
|
97
121
|
) => {
|
|
98
|
-
|
|
122
|
+
try {
|
|
123
|
+
await job({ state, write: () => {} });
|
|
124
|
+
} catch (error) {
|
|
125
|
+
recordProcessingFailure(error);
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
99
128
|
};
|
|
100
129
|
|
|
101
130
|
let finishCalled = false;
|
|
@@ -107,9 +136,17 @@ export function handleUIMessageStreamFinish<UI_MESSAGE extends UIMessage>({
|
|
|
107
136
|
finishCalled = true;
|
|
108
137
|
|
|
109
138
|
const isContinuation = state.message.id === lastMessage?.id;
|
|
139
|
+
const declaredOutcome = getOutcome?.() ?? { status: 'unknown' };
|
|
140
|
+
const outcome: UIMessageStreamOutcome = hasProcessingFailure
|
|
141
|
+
? { status: 'failed', error: processingError }
|
|
142
|
+
: declaredOutcome.status === 'unknown' && isAborted
|
|
143
|
+
? { status: 'aborted' }
|
|
144
|
+
: declaredOutcome;
|
|
145
|
+
|
|
110
146
|
await onFinish({
|
|
111
|
-
isAborted,
|
|
147
|
+
isAborted: isAborted || outcome.status === 'aborted',
|
|
112
148
|
isContinuation,
|
|
149
|
+
outcome,
|
|
113
150
|
responseMessage: state.message as UI_MESSAGE,
|
|
114
151
|
messages: [
|
|
115
152
|
...(isContinuation ? originalMessages.slice(0, -1) : originalMessages),
|
|
@@ -138,7 +175,13 @@ export function handleUIMessageStreamFinish<UI_MESSAGE extends UIMessage>({
|
|
|
138
175
|
] as UI_MESSAGE[],
|
|
139
176
|
});
|
|
140
177
|
} catch (error) {
|
|
141
|
-
|
|
178
|
+
try {
|
|
179
|
+
onError(error);
|
|
180
|
+
} catch (onErrorError) {
|
|
181
|
+
recordProcessingFailure(onErrorError);
|
|
182
|
+
await callOnFinish();
|
|
183
|
+
throw onErrorError;
|
|
184
|
+
}
|
|
142
185
|
}
|
|
143
186
|
};
|
|
144
187
|
|
|
@@ -10,5 +10,9 @@ export {
|
|
|
10
10
|
} from './ui-message-chunks';
|
|
11
11
|
export { UI_MESSAGE_STREAM_HEADERS } from './ui-message-stream-headers';
|
|
12
12
|
export type { UIMessageStreamOnFinishCallback } from './ui-message-stream-on-finish-callback';
|
|
13
|
+
export type { UIMessageStreamOutcome } from './ui-message-stream-outcome';
|
|
13
14
|
export type { UIMessageStreamOnStepFinishCallback } from './ui-message-stream-on-step-finish-callback';
|
|
14
|
-
export type {
|
|
15
|
+
export type {
|
|
16
|
+
UIMessageStreamWriter,
|
|
17
|
+
UIMessageStreamWriterWithOutcome,
|
|
18
|
+
} from './ui-message-stream-writer';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { FinishReason } from '../types/language-model';
|
|
2
2
|
import type { UIMessage } from '../ui/ui-messages';
|
|
3
|
+
import type { UIMessageStreamOutcome } from './ui-message-stream-outcome';
|
|
3
4
|
|
|
4
5
|
export type UIMessageStreamOnFinishCallback<UI_MESSAGE extends UIMessage> =
|
|
5
6
|
(event: {
|
|
@@ -19,6 +20,12 @@ export type UIMessageStreamOnFinishCallback<UI_MESSAGE extends UIMessage> =
|
|
|
19
20
|
*/
|
|
20
21
|
isAborted: boolean;
|
|
21
22
|
|
|
23
|
+
/**
|
|
24
|
+
* The operation-level outcome of the stream. Fatal stream-processing
|
|
25
|
+
* failures override outcomes declared by the stream owner.
|
|
26
|
+
*/
|
|
27
|
+
outcome: UIMessageStreamOutcome;
|
|
28
|
+
|
|
22
29
|
/**
|
|
23
30
|
* The message that was sent to the client as a response
|
|
24
31
|
* (including the original message if it was extended).
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The operation-level outcome of a UI message stream.
|
|
3
|
+
*
|
|
4
|
+
* This is separate from model finish reasons and individual stream chunks.
|
|
5
|
+
* Fatal stream-processing failures override outcomes declared by the stream
|
|
6
|
+
* owner.
|
|
7
|
+
*/
|
|
8
|
+
export type UIMessageStreamOutcome =
|
|
9
|
+
| { status: 'completed' }
|
|
10
|
+
| { status: 'failed'; error?: unknown }
|
|
11
|
+
| { status: 'aborted' }
|
|
12
|
+
| { status: 'unknown' };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { UIMessage } from '../ui';
|
|
2
2
|
import type { ErrorHandler } from '../util/error-handler';
|
|
3
3
|
import type { InferUIMessageChunk } from './ui-message-chunks';
|
|
4
|
+
import type { UIMessageStreamOutcome } from './ui-message-stream-outcome';
|
|
4
5
|
|
|
5
6
|
export interface UIMessageStreamWriter<
|
|
6
7
|
UI_MESSAGE extends UIMessage = UIMessage,
|
|
@@ -22,3 +23,17 @@ export interface UIMessageStreamWriter<
|
|
|
22
23
|
*/
|
|
23
24
|
onError: ErrorHandler | undefined;
|
|
24
25
|
}
|
|
26
|
+
|
|
27
|
+
export interface UIMessageStreamWriterWithOutcome<
|
|
28
|
+
UI_MESSAGE extends UIMessage = UIMessage,
|
|
29
|
+
> extends UIMessageStreamWriter<UI_MESSAGE> {
|
|
30
|
+
/**
|
|
31
|
+
* Declares the operation-level outcome of the composed stream.
|
|
32
|
+
*
|
|
33
|
+
* The first outcome declared through this method is retained. Fatal
|
|
34
|
+
* execution, merge, error-handling, or downstream processing failures
|
|
35
|
+
* override declared outcomes. Declaring an outcome does not write a chunk or
|
|
36
|
+
* close the stream.
|
|
37
|
+
*/
|
|
38
|
+
setOutcome(outcome: UIMessageStreamOutcome): void;
|
|
39
|
+
}
|