ai 7.0.82 → 7.0.83
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 +12 -0
- package/dist/index.d.ts +45 -19
- package/dist/index.js +272 -79
- package/dist/index.js.map +1 -1
- package/dist/internal/index.js +1 -1
- package/docs/07-reference/01-ai-sdk-core/02-stream-text.mdx +3 -3
- package/docs/07-reference/02-ai-sdk-ui/40-create-ui-message-stream.mdx +35 -6
- package/package.json +5 -5
- package/src/agent/create-agent-ui-stream.ts +2 -2
- package/src/ui/direct-chat-transport.ts +2 -2
- package/src/ui/last-assistant-message-is-complete-with-approval-responses.ts +1 -0
- 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 +51 -14
- package/src/ui-message-stream/index.ts +5 -1
- package/src/ui-message-stream/to-ui-message-stream.ts +108 -26
- package/src/ui-message-stream/ui-message-stream-on-end-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
- package/src/util/create-stitchable-stream.ts +31 -1
package/dist/internal/index.js
CHANGED
|
@@ -4034,13 +4034,13 @@ To see `streamText` in action, check out [these examples](#examples).
|
|
|
4034
4034
|
},
|
|
4035
4035
|
{
|
|
4036
4036
|
name: 'onEnd',
|
|
4037
|
-
type: '(options: { messages: UIMessage[]; isContinuation: boolean; responseMessage: UIMessage; isAborted: boolean; }) => void',
|
|
4037
|
+
type: '(options: { messages: UIMessage[]; isContinuation: boolean; responseMessage: UIMessage; isAborted: boolean; outcome: UIMessageStreamOutcome; finishReason?: FinishReason; }) => PromiseLike<void> | void',
|
|
4038
4038
|
isOptional: true,
|
|
4039
|
-
description: 'Callback function called when the stream ends. Provides the updated
|
|
4039
|
+
description: 'Callback function called when the stream ends. Provides the updated messages, continuation and abort state, model finish reason, and operation-level outcome.',
|
|
4040
4040
|
},
|
|
4041
4041
|
{
|
|
4042
4042
|
name: 'onFinish',
|
|
4043
|
-
type: '(options: { messages: UIMessage[]; isContinuation: boolean; responseMessage: UIMessage; isAborted: boolean; }) => void',
|
|
4043
|
+
type: '(options: { messages: UIMessage[]; isContinuation: boolean; responseMessage: UIMessage; isAborted: boolean; outcome: UIMessageStreamOutcome; finishReason?: FinishReason; }) => PromiseLike<void> | void',
|
|
4044
4044
|
isOptional: true,
|
|
4045
4045
|
description: 'Deprecated alias for `onEnd`.',
|
|
4046
4046
|
},
|
|
@@ -47,16 +47,33 @@ const stream = createUIMessageStream({
|
|
|
47
47
|
prompt: 'Write a haiku about AI',
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
-
writer.merge(
|
|
50
|
+
writer.merge(
|
|
51
|
+
toUIMessageStream({
|
|
52
|
+
stream: result.stream,
|
|
53
|
+
onEnd: ({ outcome }) => {
|
|
54
|
+
// The composer decides that the model stream outcome is also the
|
|
55
|
+
// aggregate stream outcome.
|
|
56
|
+
writer.setOutcome(outcome);
|
|
57
|
+
},
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
51
60
|
},
|
|
52
61
|
onError: error => `Custom error: ${error.message}`,
|
|
53
62
|
originalMessages: existingMessages,
|
|
54
|
-
onEnd: ({ messages, isContinuation, responseMessage }) => {
|
|
63
|
+
onEnd: ({ messages, isContinuation, outcome, responseMessage }) => {
|
|
55
64
|
console.log('Stream ended with messages:', messages);
|
|
65
|
+
console.log('Stream outcome:', outcome.status);
|
|
56
66
|
},
|
|
57
67
|
});
|
|
58
68
|
```
|
|
59
69
|
|
|
70
|
+
`setOutcome` records the composer's policy without writing a chunk or closing
|
|
71
|
+
the stream. The first outcome declared through `setOutcome` is retained, but a
|
|
72
|
+
fatal execution, merge, error-handling, or downstream processing failure makes
|
|
73
|
+
the final `onEnd` outcome `failed`. Individual `error` chunks do not change the
|
|
74
|
+
outcome by themselves. When merging multiple child streams, aggregate their
|
|
75
|
+
outcomes and call `setOutcome` once.
|
|
76
|
+
|
|
60
77
|
## API Signature
|
|
61
78
|
|
|
62
79
|
### Parameters
|
|
@@ -65,12 +82,12 @@ const stream = createUIMessageStream({
|
|
|
65
82
|
content={[
|
|
66
83
|
{
|
|
67
84
|
name: 'execute',
|
|
68
|
-
type: '(options: { writer:
|
|
85
|
+
type: '(options: { writer: UIMessageStreamWriterWithOutcome }) => Promise<void> | void',
|
|
69
86
|
description:
|
|
70
87
|
'A function that receives a writer instance and can use it to write UI message chunks to the stream.',
|
|
71
88
|
properties: [
|
|
72
89
|
{
|
|
73
|
-
type: '
|
|
90
|
+
type: 'UIMessageStreamWriterWithOutcome',
|
|
74
91
|
parameters: [
|
|
75
92
|
{
|
|
76
93
|
name: 'write',
|
|
@@ -83,6 +100,12 @@ const stream = createUIMessageStream({
|
|
|
83
100
|
description:
|
|
84
101
|
'Merges the contents of another UI message stream into this stream.',
|
|
85
102
|
},
|
|
103
|
+
{
|
|
104
|
+
name: 'setOutcome',
|
|
105
|
+
type: '(outcome: UIMessageStreamOutcome) => void',
|
|
106
|
+
description:
|
|
107
|
+
"Declares the operation-level outcome of the composed stream. The first outcome declared through this method is retained, while fatal execution, merge, error-handling, or downstream processing failures override declarations. Supported statuses are 'completed', 'failed', 'aborted', and 'unknown'. Declaring an outcome does not write a chunk or close the stream.",
|
|
108
|
+
},
|
|
86
109
|
{
|
|
87
110
|
name: 'onError',
|
|
88
111
|
type: '(error: unknown) => string',
|
|
@@ -107,7 +130,7 @@ const stream = createUIMessageStream({
|
|
|
107
130
|
},
|
|
108
131
|
{
|
|
109
132
|
name: 'onEnd',
|
|
110
|
-
type: '(options: { messages: UIMessage[]; isContinuation: boolean; isAborted: boolean; responseMessage: UIMessage; finishReason?: FinishReason }) => PromiseLike<void> | void',
|
|
133
|
+
type: '(options: { messages: UIMessage[]; isContinuation: boolean; isAborted: boolean; outcome: UIMessageStreamOutcome; responseMessage: UIMessage; finishReason?: FinishReason }) => PromiseLike<void> | void',
|
|
111
134
|
description: 'A callback function that is called when the stream ends.',
|
|
112
135
|
properties: [
|
|
113
136
|
{
|
|
@@ -129,6 +152,12 @@ const stream = createUIMessageStream({
|
|
|
129
152
|
type: 'boolean',
|
|
130
153
|
description: 'Indicates whether the stream was aborted.',
|
|
131
154
|
},
|
|
155
|
+
{
|
|
156
|
+
name: 'outcome',
|
|
157
|
+
type: "UIMessageStreamOutcome = { status: 'completed' } | { status: 'failed'; error?: unknown } | { status: 'aborted' } | { status: 'unknown' }",
|
|
158
|
+
description:
|
|
159
|
+
'The operation-level outcome of the stream. It reflects the stream owner declaration unless a fatal stream-processing failure occurs, and is separate from model finish reasons and individual error chunks.',
|
|
160
|
+
},
|
|
132
161
|
{
|
|
133
162
|
name: 'responseMessage',
|
|
134
163
|
type: 'UIMessage',
|
|
@@ -147,7 +176,7 @@ const stream = createUIMessageStream({
|
|
|
147
176
|
},
|
|
148
177
|
{
|
|
149
178
|
name: 'onFinish',
|
|
150
|
-
type: '(options: { messages: UIMessage[]; isContinuation: boolean; isAborted: boolean; responseMessage: UIMessage; finishReason?: FinishReason }) => PromiseLike<void> | void',
|
|
179
|
+
type: '(options: { messages: UIMessage[]; isContinuation: boolean; isAborted: boolean; outcome: UIMessageStreamOutcome; responseMessage: UIMessage; finishReason?: FinishReason }) => PromiseLike<void> | void',
|
|
151
180
|
description: 'Deprecated alias for `onEnd`.',
|
|
152
181
|
},
|
|
153
182
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.83",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AI SDK by Vercel - build apps like ChatGPT, Claude, Gemini, and more with a single interface for any model using the Vercel AI Gateway or go direct to OpenAI, Anthropic, Google, or any other model provider.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -47,14 +47,14 @@
|
|
|
47
47
|
"@ai-sdk/provider-utils": "5.0.32"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@ai-sdk/amazon-bedrock": "5.0.
|
|
51
|
-
"@ai-sdk/deepseek": "3.0.
|
|
52
|
-
"@ai-sdk/google": "4.0.
|
|
50
|
+
"@ai-sdk/amazon-bedrock": "5.0.66",
|
|
51
|
+
"@ai-sdk/deepseek": "3.0.35",
|
|
52
|
+
"@ai-sdk/google": "4.0.54",
|
|
53
53
|
"@ai-sdk/groq": "4.0.33",
|
|
54
54
|
"@ai-sdk/huggingface": "2.0.39",
|
|
55
55
|
"@ai-sdk/moonshotai": "3.0.41",
|
|
56
56
|
"@ai-sdk/open-responses": "2.0.34",
|
|
57
|
-
"@ai-sdk/openai": "4.0.
|
|
57
|
+
"@ai-sdk/openai": "4.0.50",
|
|
58
58
|
"@ai-sdk/test-server": "2.0.1",
|
|
59
59
|
"@ai-sdk/xai": "4.0.47",
|
|
60
60
|
"@edge-runtime/vm": "^5.0.0",
|
|
@@ -21,7 +21,7 @@ import type {
|
|
|
21
21
|
InferUITools,
|
|
22
22
|
UIMessage,
|
|
23
23
|
} from '../ui/ui-messages';
|
|
24
|
-
import {
|
|
24
|
+
import { validateUIMessagesForAgent } from '../ui/validate-ui-messages';
|
|
25
25
|
import {
|
|
26
26
|
createAsyncIterableStream,
|
|
27
27
|
type AsyncIterableStream,
|
|
@@ -77,7 +77,7 @@ export async function createAgentUIStream<
|
|
|
77
77
|
} & UIMessageStreamOptions<UI_MESSAGE>): Promise<
|
|
78
78
|
AsyncIterableStream<InferUIMessageChunk<UI_MESSAGE>>
|
|
79
79
|
> {
|
|
80
|
-
const validatedMessages = await
|
|
80
|
+
const validatedMessages = await validateUIMessagesForAgent<UI_MESSAGE>({
|
|
81
81
|
messages: uiMessages,
|
|
82
82
|
// tools are compatible; the casting is required because the context param is
|
|
83
83
|
// not available in ui messages
|
|
@@ -11,7 +11,7 @@ import type {
|
|
|
11
11
|
InferUITools,
|
|
12
12
|
UIMessage,
|
|
13
13
|
} from './ui-messages';
|
|
14
|
-
import {
|
|
14
|
+
import { validateUIMessagesForAgent } from './validate-ui-messages';
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Options for the `DirectChatTransport` class.
|
|
@@ -93,7 +93,7 @@ export class DirectChatTransport<
|
|
|
93
93
|
ReadableStream<UIMessageChunk>
|
|
94
94
|
> {
|
|
95
95
|
// Validate the incoming UI messages
|
|
96
|
-
const validatedMessages = await
|
|
96
|
+
const validatedMessages = await validateUIMessagesForAgent<UI_MESSAGE>({
|
|
97
97
|
messages,
|
|
98
98
|
// tools are compatible; the casting is required because the context param is
|
|
99
99
|
// not available in ui messages
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { TypeValidationError, type JSONObject } from '@ai-sdk/provider';
|
|
2
2
|
import {
|
|
3
3
|
lazySchema,
|
|
4
|
+
safeValidateTypes,
|
|
4
5
|
validateTypes,
|
|
5
6
|
zodSchema,
|
|
6
7
|
type FlexibleSchema,
|
|
@@ -13,6 +14,7 @@ import { providerMetadataSchema } from '../types/provider-metadata';
|
|
|
13
14
|
import { z, type ZodType } from '../util/zod';
|
|
14
15
|
import type {
|
|
15
16
|
DataUIPart,
|
|
17
|
+
DynamicToolUIPart,
|
|
16
18
|
InferUIMessageData,
|
|
17
19
|
InferUIMessageTools,
|
|
18
20
|
ToolUIPart,
|
|
@@ -26,6 +28,25 @@ const toolMetadataSchema: ZodType<JSONObject> = z.record(
|
|
|
26
28
|
|
|
27
29
|
const providerReferenceSchema = z.record(z.string(), z.string());
|
|
28
30
|
|
|
31
|
+
function isEmptyObject(value: unknown): value is Record<string, never> {
|
|
32
|
+
return (
|
|
33
|
+
value != null &&
|
|
34
|
+
typeof value === 'object' &&
|
|
35
|
+
!Array.isArray(value) &&
|
|
36
|
+
Object.keys(value).length === 0
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function asDynamicToolPart(toolPart: ToolUIPart): DynamicToolUIPart {
|
|
41
|
+
const { type, ...part } = toolPart;
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
...part,
|
|
45
|
+
type: 'dynamic-tool',
|
|
46
|
+
toolName: type.slice(5),
|
|
47
|
+
} as DynamicToolUIPart;
|
|
48
|
+
}
|
|
49
|
+
|
|
29
50
|
const uiMessagesSchema = lazySchema(() =>
|
|
30
51
|
zodSchema(
|
|
31
52
|
z
|
|
@@ -384,17 +405,7 @@ export type SafeValidateUIMessagesResult<UI_MESSAGE extends UIMessage> =
|
|
|
384
405
|
error: Error;
|
|
385
406
|
};
|
|
386
407
|
|
|
387
|
-
|
|
388
|
-
* Validates a list of UI messages like `validateUIMessages`,
|
|
389
|
-
* but instead of throwing it returns `{ success: true, data }`
|
|
390
|
-
* or `{ success: false, error }`.
|
|
391
|
-
*/
|
|
392
|
-
export async function safeValidateUIMessages<UI_MESSAGE extends UIMessage>({
|
|
393
|
-
messages,
|
|
394
|
-
metadataSchema,
|
|
395
|
-
dataSchemas,
|
|
396
|
-
tools,
|
|
397
|
-
}: {
|
|
408
|
+
type ValidateUIMessagesOptions<UI_MESSAGE extends UIMessage> = {
|
|
398
409
|
messages: unknown;
|
|
399
410
|
metadataSchema?: FlexibleSchema<UIMessage['metadata']>;
|
|
400
411
|
dataSchemas?: {
|
|
@@ -408,7 +419,21 @@ export async function safeValidateUIMessages<UI_MESSAGE extends UIMessage>({
|
|
|
408
419
|
InferUIMessageTools<UI_MESSAGE>[NAME]['output']
|
|
409
420
|
>;
|
|
410
421
|
};
|
|
411
|
-
}
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
async function safeValidateUIMessagesInternal<UI_MESSAGE extends UIMessage>(
|
|
425
|
+
{
|
|
426
|
+
messages,
|
|
427
|
+
metadataSchema,
|
|
428
|
+
dataSchemas,
|
|
429
|
+
tools,
|
|
430
|
+
}: ValidateUIMessagesOptions<UI_MESSAGE>,
|
|
431
|
+
{
|
|
432
|
+
convertMissingTerminalToolsToDynamic,
|
|
433
|
+
}: {
|
|
434
|
+
convertMissingTerminalToolsToDynamic: boolean;
|
|
435
|
+
},
|
|
436
|
+
): Promise<SafeValidateUIMessagesResult<UI_MESSAGE>> {
|
|
412
437
|
try {
|
|
413
438
|
if (messages == null) {
|
|
414
439
|
return {
|
|
@@ -439,7 +464,10 @@ export async function safeValidateUIMessages<UI_MESSAGE extends UIMessage>({
|
|
|
439
464
|
}
|
|
440
465
|
}
|
|
441
466
|
|
|
442
|
-
|
|
467
|
+
const shouldValidateToolParts =
|
|
468
|
+
tools != null || convertMissingTerminalToolsToDynamic;
|
|
469
|
+
|
|
470
|
+
if (dataSchemas || shouldValidateToolParts) {
|
|
443
471
|
for (const [msgIdx, message] of validatedMessages.entries()) {
|
|
444
472
|
for (const [partIdx, part] of message.parts.entries()) {
|
|
445
473
|
// Data part validation
|
|
@@ -475,19 +503,26 @@ export async function safeValidateUIMessages<UI_MESSAGE extends UIMessage>({
|
|
|
475
503
|
}
|
|
476
504
|
|
|
477
505
|
// Tool part validation
|
|
478
|
-
if (
|
|
506
|
+
if (shouldValidateToolParts && part.type.startsWith('tool-')) {
|
|
479
507
|
const toolPart = part as ToolUIPart<
|
|
480
508
|
InferUIMessageTools<UI_MESSAGE>
|
|
481
509
|
>;
|
|
482
510
|
const toolName = toolPart.type.slice(5);
|
|
483
|
-
const tool = getOwn(tools, toolName);
|
|
511
|
+
const tool = tools == null ? undefined : getOwn(tools, toolName);
|
|
512
|
+
const isTerminal =
|
|
513
|
+
toolPart.state === 'output-available' ||
|
|
514
|
+
toolPart.state === 'output-error' ||
|
|
515
|
+
toolPart.state === 'output-denied';
|
|
484
516
|
|
|
485
|
-
if (
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
517
|
+
if (!tool && isTerminal) {
|
|
518
|
+
if (tools != null || convertMissingTerminalToolsToDynamic) {
|
|
519
|
+
// Persisted terminal history can reference tools that are no
|
|
520
|
+
// longer registered. Normalize those parts so callers do not
|
|
521
|
+
// receive unvalidated values under current static tool types.
|
|
522
|
+
message.parts[partIdx] = asDynamicToolPart(
|
|
523
|
+
toolPart,
|
|
524
|
+
) as (typeof message.parts)[number];
|
|
525
|
+
}
|
|
491
526
|
continue;
|
|
492
527
|
}
|
|
493
528
|
|
|
@@ -507,19 +542,53 @@ export async function safeValidateUIMessages<UI_MESSAGE extends UIMessage>({
|
|
|
507
542
|
};
|
|
508
543
|
}
|
|
509
544
|
|
|
545
|
+
const inputValidationContext = {
|
|
546
|
+
field: `messages[${msgIdx}].parts[${partIdx}].input`,
|
|
547
|
+
entityName: toolName,
|
|
548
|
+
entityId: toolPart.toolCallId,
|
|
549
|
+
};
|
|
550
|
+
let convertToDynamic = false;
|
|
551
|
+
|
|
510
552
|
// Tool input validation
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
553
|
+
if (toolPart.state === 'output-error') {
|
|
554
|
+
// Failed calls can retain invalid input. Keep them loadable, but
|
|
555
|
+
// expose incompatible input as unknown instead of the current
|
|
556
|
+
// static tool input type.
|
|
557
|
+
if (toolPart.input !== undefined) {
|
|
558
|
+
const result = await safeValidateTypes({
|
|
559
|
+
value: toolPart.input,
|
|
560
|
+
schema: tool.inputSchema,
|
|
561
|
+
context: inputValidationContext,
|
|
562
|
+
});
|
|
563
|
+
convertToDynamic = !result.success;
|
|
564
|
+
}
|
|
565
|
+
} else if (toolPart.state === 'output-available') {
|
|
566
|
+
const result = await safeValidateTypes({
|
|
567
|
+
value: toolPart.input,
|
|
568
|
+
schema: tool.inputSchema,
|
|
569
|
+
context: inputValidationContext,
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
if (!result.success) {
|
|
573
|
+
// Empty terminal input can represent aborted or incomplete
|
|
574
|
+
// history whose input was never streamed. Preserve it without
|
|
575
|
+
// claiming that it matches the current static input type.
|
|
576
|
+
if (isEmptyObject(toolPart.input)) {
|
|
577
|
+
convertToDynamic = true;
|
|
578
|
+
} else {
|
|
579
|
+
throw result.error;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
} else if (
|
|
583
|
+
toolPart.state === 'input-available' ||
|
|
584
|
+
toolPart.state === 'approval-requested' ||
|
|
585
|
+
toolPart.state === 'approval-responded' ||
|
|
586
|
+
toolPart.state === 'output-denied'
|
|
587
|
+
) {
|
|
515
588
|
await validateTypes({
|
|
516
589
|
value: toolPart.input,
|
|
517
590
|
schema: tool.inputSchema,
|
|
518
|
-
context:
|
|
519
|
-
field: `messages[${msgIdx}].parts[${partIdx}].input`,
|
|
520
|
-
entityName: toolName,
|
|
521
|
-
entityId: toolPart.toolCallId,
|
|
522
|
-
},
|
|
591
|
+
context: inputValidationContext,
|
|
523
592
|
});
|
|
524
593
|
}
|
|
525
594
|
|
|
@@ -535,6 +604,12 @@ export async function safeValidateUIMessages<UI_MESSAGE extends UIMessage>({
|
|
|
535
604
|
},
|
|
536
605
|
});
|
|
537
606
|
}
|
|
607
|
+
|
|
608
|
+
if (convertToDynamic) {
|
|
609
|
+
message.parts[partIdx] = asDynamicToolPart(
|
|
610
|
+
toolPart,
|
|
611
|
+
) as (typeof message.parts)[number];
|
|
612
|
+
}
|
|
538
613
|
}
|
|
539
614
|
}
|
|
540
615
|
}
|
|
@@ -554,6 +629,19 @@ export async function safeValidateUIMessages<UI_MESSAGE extends UIMessage>({
|
|
|
554
629
|
}
|
|
555
630
|
}
|
|
556
631
|
|
|
632
|
+
/**
|
|
633
|
+
* Validates a list of UI messages like `validateUIMessages`,
|
|
634
|
+
* but instead of throwing it returns `{ success: true, data }`
|
|
635
|
+
* or `{ success: false, error }`.
|
|
636
|
+
*/
|
|
637
|
+
export async function safeValidateUIMessages<UI_MESSAGE extends UIMessage>(
|
|
638
|
+
options: ValidateUIMessagesOptions<UI_MESSAGE>,
|
|
639
|
+
): Promise<SafeValidateUIMessagesResult<UI_MESSAGE>> {
|
|
640
|
+
return safeValidateUIMessagesInternal(options, {
|
|
641
|
+
convertMissingTerminalToolsToDynamic: false,
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
|
|
557
645
|
/**
|
|
558
646
|
* Validates a list of UI messages.
|
|
559
647
|
*
|
|
@@ -561,31 +649,24 @@ export async function safeValidateUIMessages<UI_MESSAGE extends UIMessage>({
|
|
|
561
649
|
* the corresponding schemas are provided. Otherwise, they are assumed to be
|
|
562
650
|
* valid.
|
|
563
651
|
*/
|
|
564
|
-
export async function validateUIMessages<UI_MESSAGE extends UIMessage>(
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
};
|
|
583
|
-
}): Promise<Array<UI_MESSAGE>> {
|
|
584
|
-
const response = await safeValidateUIMessages({
|
|
585
|
-
messages,
|
|
586
|
-
metadataSchema,
|
|
587
|
-
dataSchemas,
|
|
588
|
-
tools,
|
|
652
|
+
export async function validateUIMessages<UI_MESSAGE extends UIMessage>(
|
|
653
|
+
options: ValidateUIMessagesOptions<UI_MESSAGE>,
|
|
654
|
+
): Promise<Array<UI_MESSAGE>> {
|
|
655
|
+
const response = await safeValidateUIMessages(options);
|
|
656
|
+
|
|
657
|
+
if (!response.success) throw response.error;
|
|
658
|
+
|
|
659
|
+
return response.data;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
export async function validateUIMessagesForAgent<UI_MESSAGE extends UIMessage>(
|
|
663
|
+
options: ValidateUIMessagesOptions<UI_MESSAGE>,
|
|
664
|
+
): Promise<Array<UI_MESSAGE>> {
|
|
665
|
+
const response = await safeValidateUIMessagesInternal(options, {
|
|
666
|
+
// Agent tool sets can include ephemeral tools (for example, tools from a
|
|
667
|
+
// disconnected MCP server), so terminal history is converted to dynamic
|
|
668
|
+
// tool parts when those tools are no longer registered.
|
|
669
|
+
convertMissingTerminalToolsToDynamic: true,
|
|
589
670
|
});
|
|
590
671
|
|
|
591
672
|
if (!response.success) throw response.error;
|
|
@@ -6,9 +6,10 @@ 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 { UIMessageStreamOnEndCallback } from './ui-message-stream-on-end-callback';
|
|
9
|
+
import type { UIMessageStreamOutcome } from './ui-message-stream-outcome';
|
|
9
10
|
import type { UIMessageStreamOnStepEndCallback } from './ui-message-stream-on-step-end-callback';
|
|
10
11
|
import type { UIMessageStreamOnStepFinishCallback } from './ui-message-stream-on-step-finish-callback';
|
|
11
|
-
import type {
|
|
12
|
+
import type { UIMessageStreamWriterWithOutcome } from './ui-message-stream-writer';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Creates a UI message stream that can be used to send messages to the client.
|
|
@@ -36,7 +37,7 @@ export function createUIMessageStream<UI_MESSAGE extends UIMessage>({
|
|
|
36
37
|
generateId = generateIdFunc,
|
|
37
38
|
}: {
|
|
38
39
|
execute: (options: {
|
|
39
|
-
writer:
|
|
40
|
+
writer: UIMessageStreamWriterWithOutcome<UI_MESSAGE>;
|
|
40
41
|
}) => Promise<void> | void;
|
|
41
42
|
onError?: (error: unknown) => string;
|
|
42
43
|
|
|
@@ -72,6 +73,7 @@ export function createUIMessageStream<UI_MESSAGE extends UIMessage>({
|
|
|
72
73
|
>;
|
|
73
74
|
|
|
74
75
|
const ongoingStreamPromises: Promise<void>[] = [];
|
|
76
|
+
let outcome: UIMessageStreamOutcome = { status: 'unknown' };
|
|
75
77
|
|
|
76
78
|
const stream = new ReadableStream({
|
|
77
79
|
start(controllerArg) {
|
|
@@ -87,6 +89,42 @@ export function createUIMessageStream<UI_MESSAGE extends UIMessage>({
|
|
|
87
89
|
}
|
|
88
90
|
}
|
|
89
91
|
|
|
92
|
+
function setOutcome(newOutcome: UIMessageStreamOutcome) {
|
|
93
|
+
if (outcome.status === 'unknown' && newOutcome.status !== 'unknown') {
|
|
94
|
+
outcome = newOutcome;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function failOutcome(error: unknown) {
|
|
99
|
+
outcome = { status: 'failed', error };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function safeError(error: unknown) {
|
|
103
|
+
try {
|
|
104
|
+
controller.error(error);
|
|
105
|
+
} catch {
|
|
106
|
+
// suppress errors when the stream has been closed
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function handleError(error: unknown) {
|
|
111
|
+
failOutcome(error);
|
|
112
|
+
|
|
113
|
+
let errorText: string;
|
|
114
|
+
try {
|
|
115
|
+
errorText = onError(error);
|
|
116
|
+
} catch (onErrorError) {
|
|
117
|
+
failOutcome(onErrorError);
|
|
118
|
+
safeError(onErrorError);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
safeEnqueue({
|
|
123
|
+
type: 'error',
|
|
124
|
+
errorText,
|
|
125
|
+
} as InferUIMessageChunk<UI_MESSAGE>);
|
|
126
|
+
}
|
|
127
|
+
|
|
90
128
|
try {
|
|
91
129
|
const result = execute({
|
|
92
130
|
writer: {
|
|
@@ -103,13 +141,11 @@ export function createUIMessageStream<UI_MESSAGE extends UIMessage>({
|
|
|
103
141
|
safeEnqueue(value);
|
|
104
142
|
}
|
|
105
143
|
})().catch(error => {
|
|
106
|
-
|
|
107
|
-
type: 'error',
|
|
108
|
-
errorText: onError(error),
|
|
109
|
-
} as InferUIMessageChunk<UI_MESSAGE>);
|
|
144
|
+
handleError(error);
|
|
110
145
|
}),
|
|
111
146
|
);
|
|
112
147
|
},
|
|
148
|
+
setOutcome,
|
|
113
149
|
onError,
|
|
114
150
|
},
|
|
115
151
|
});
|
|
@@ -117,30 +153,23 @@ export function createUIMessageStream<UI_MESSAGE extends UIMessage>({
|
|
|
117
153
|
if (result) {
|
|
118
154
|
ongoingStreamPromises.push(
|
|
119
155
|
result.catch(error => {
|
|
120
|
-
|
|
121
|
-
type: 'error',
|
|
122
|
-
errorText: onError(error),
|
|
123
|
-
} as InferUIMessageChunk<UI_MESSAGE>);
|
|
156
|
+
handleError(error);
|
|
124
157
|
}),
|
|
125
158
|
);
|
|
126
159
|
}
|
|
127
160
|
} catch (error) {
|
|
128
|
-
|
|
129
|
-
type: 'error',
|
|
130
|
-
errorText: onError(error),
|
|
131
|
-
} as InferUIMessageChunk<UI_MESSAGE>);
|
|
161
|
+
handleError(error);
|
|
132
162
|
}
|
|
133
163
|
|
|
134
164
|
// Wait until all ongoing streams are done. This approach enables merging
|
|
135
165
|
// streams even after execute has returned, as long as there is still an
|
|
136
166
|
// open merged stream. This is important to e.g. forward new streams and
|
|
137
167
|
// from callbacks.
|
|
138
|
-
const waitForStreams: Promise<void> =
|
|
168
|
+
const waitForStreams: Promise<void> = (async () => {
|
|
139
169
|
while (ongoingStreamPromises.length > 0) {
|
|
140
170
|
await ongoingStreamPromises.shift();
|
|
141
171
|
}
|
|
142
|
-
|
|
143
|
-
});
|
|
172
|
+
})();
|
|
144
173
|
|
|
145
174
|
waitForStreams.finally(() => {
|
|
146
175
|
try {
|
|
@@ -157,5 +186,6 @@ export function createUIMessageStream<UI_MESSAGE extends UIMessage>({
|
|
|
157
186
|
onStepEnd: onStepEnd ?? onStepFinish,
|
|
158
187
|
onEnd: onEnd ?? onFinish,
|
|
159
188
|
onError,
|
|
189
|
+
getOutcome: () => outcome,
|
|
160
190
|
});
|
|
161
191
|
}
|