ai 6.0.270 → 6.0.271
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 +10 -0
- package/dist/index.d.mts +45 -19
- package/dist/index.d.ts +45 -19
- package/dist/index.js +228 -56
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +233 -60
- package/dist/index.mjs.map +1 -1
- package/dist/internal/index.js +1 -1
- package/dist/internal/index.mjs +1 -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/package.json +1 -1
- package/src/agent/create-agent-ui-stream.ts +2 -2
- 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
package/dist/internal/index.js
CHANGED
|
@@ -164,7 +164,7 @@ function detectMediaType({
|
|
|
164
164
|
var import_provider_utils2 = require("@ai-sdk/provider-utils");
|
|
165
165
|
|
|
166
166
|
// src/version.ts
|
|
167
|
-
var VERSION = true ? "6.0.
|
|
167
|
+
var VERSION = true ? "6.0.271" : "0.0.0-test";
|
|
168
168
|
|
|
169
169
|
// src/util/download/download.ts
|
|
170
170
|
var download = async ({
|
package/dist/internal/index.mjs
CHANGED
|
@@ -3509,9 +3509,9 @@ To see `streamText` in action, check out [these examples](#examples).
|
|
|
3509
3509
|
},
|
|
3510
3510
|
{
|
|
3511
3511
|
name: 'onFinish',
|
|
3512
|
-
type: '(options: { messages: UIMessage[]; isContinuation: boolean; responseMessage: UIMessage; isAborted: boolean; }) => void',
|
|
3512
|
+
type: '(options: { messages: UIMessage[]; isContinuation: boolean; responseMessage: UIMessage; isAborted: boolean; outcome: UIMessageStreamOutcome; finishReason?: FinishReason; }) => PromiseLike<void> | void',
|
|
3513
3513
|
isOptional: true,
|
|
3514
|
-
description: 'Callback function called when the stream finishes. Provides the updated
|
|
3514
|
+
description: 'Callback function called when the stream finishes. Provides the updated messages, continuation and abort state, model finish reason, and operation-level outcome.',
|
|
3515
3515
|
},
|
|
3516
3516
|
{
|
|
3517
3517
|
name: 'messageMetadata',
|
|
@@ -47,16 +47,32 @@ const stream = createUIMessageStream({
|
|
|
47
47
|
prompt: 'Write a haiku about AI',
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
-
writer.merge(
|
|
50
|
+
writer.merge(
|
|
51
|
+
result.toUIMessageStream({
|
|
52
|
+
onFinish: ({ outcome }) => {
|
|
53
|
+
// The composer decides that the model stream outcome is also the
|
|
54
|
+
// aggregate stream outcome.
|
|
55
|
+
writer.setOutcome(outcome);
|
|
56
|
+
},
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
51
59
|
},
|
|
52
60
|
onError: error => `Custom error: ${error.message}`,
|
|
53
61
|
originalMessages: existingMessages,
|
|
54
|
-
onFinish: ({ messages, isContinuation, responseMessage }) => {
|
|
62
|
+
onFinish: ({ messages, isContinuation, outcome, responseMessage }) => {
|
|
55
63
|
console.log('Stream finished with messages:', messages);
|
|
64
|
+
console.log('Stream outcome:', outcome.status);
|
|
56
65
|
},
|
|
57
66
|
});
|
|
58
67
|
```
|
|
59
68
|
|
|
69
|
+
`setOutcome` records the composer's policy without writing a chunk or closing
|
|
70
|
+
the stream. The first outcome declared through `setOutcome` is retained, but a
|
|
71
|
+
fatal execution, merge, error-handling, or downstream processing failure makes
|
|
72
|
+
the final `onFinish` outcome `failed`. Individual `error` chunks do not change the
|
|
73
|
+
outcome by themselves. When merging multiple child streams, aggregate their
|
|
74
|
+
outcomes and call `setOutcome` once.
|
|
75
|
+
|
|
60
76
|
## API Signature
|
|
61
77
|
|
|
62
78
|
### Parameters
|
|
@@ -65,12 +81,12 @@ const stream = createUIMessageStream({
|
|
|
65
81
|
content={[
|
|
66
82
|
{
|
|
67
83
|
name: 'execute',
|
|
68
|
-
type: '(options: { writer:
|
|
84
|
+
type: '(options: { writer: UIMessageStreamWriterWithOutcome }) => Promise<void> | void',
|
|
69
85
|
description:
|
|
70
86
|
'A function that receives a writer instance and can use it to write UI message chunks to the stream.',
|
|
71
87
|
properties: [
|
|
72
88
|
{
|
|
73
|
-
type: '
|
|
89
|
+
type: 'UIMessageStreamWriterWithOutcome',
|
|
74
90
|
parameters: [
|
|
75
91
|
{
|
|
76
92
|
name: 'write',
|
|
@@ -83,6 +99,12 @@ const stream = createUIMessageStream({
|
|
|
83
99
|
description:
|
|
84
100
|
'Merges the contents of another UI message stream into this stream.',
|
|
85
101
|
},
|
|
102
|
+
{
|
|
103
|
+
name: 'setOutcome',
|
|
104
|
+
type: '(outcome: UIMessageStreamOutcome) => void',
|
|
105
|
+
description:
|
|
106
|
+
"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.",
|
|
107
|
+
},
|
|
86
108
|
{
|
|
87
109
|
name: 'onError',
|
|
88
110
|
type: '(error: unknown) => string',
|
|
@@ -107,7 +129,7 @@ const stream = createUIMessageStream({
|
|
|
107
129
|
},
|
|
108
130
|
{
|
|
109
131
|
name: 'onFinish',
|
|
110
|
-
type: '(options: { messages: UIMessage[]; isContinuation: boolean; isAborted: boolean; responseMessage: UIMessage; finishReason?: FinishReason }) => PromiseLike<void> | void',
|
|
132
|
+
type: '(options: { messages: UIMessage[]; isContinuation: boolean; isAborted: boolean; outcome: UIMessageStreamOutcome; responseMessage: UIMessage; finishReason?: FinishReason }) => PromiseLike<void> | void',
|
|
111
133
|
description:
|
|
112
134
|
'A callback function that is called when the stream finishes.',
|
|
113
135
|
properties: [
|
|
@@ -130,6 +152,12 @@ const stream = createUIMessageStream({
|
|
|
130
152
|
type: 'boolean',
|
|
131
153
|
description: 'Indicates whether the stream was aborted.',
|
|
132
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
|
+
},
|
|
133
161
|
{
|
|
134
162
|
name: 'responseMessage',
|
|
135
163
|
type: 'UIMessage',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai",
|
|
3
|
-
"version": "6.0.
|
|
3
|
+
"version": "6.0.271",
|
|
4
4
|
"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.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -8,7 +8,7 @@ import type { TimeoutConfiguration } from '../prompt/call-settings';
|
|
|
8
8
|
import type { InferUIMessageChunk } from '../ui-message-stream';
|
|
9
9
|
import { convertToModelMessages } from '../ui/convert-to-model-messages';
|
|
10
10
|
import type { InferUITools, UIMessage } from '../ui/ui-messages';
|
|
11
|
-
import {
|
|
11
|
+
import { validateUIMessagesForAgent } from '../ui/validate-ui-messages';
|
|
12
12
|
import type { AsyncIterableStream } from '../util/async-iterable-stream';
|
|
13
13
|
import type { Agent } from './agent';
|
|
14
14
|
import type { ToolLoopAgentOnStepFinishCallback } from './tool-loop-agent-settings';
|
|
@@ -58,7 +58,7 @@ export async function createAgentUIStream<
|
|
|
58
58
|
InferUIMessageChunk<UIMessage<MESSAGE_METADATA, never, InferUITools<TOOLS>>>
|
|
59
59
|
>
|
|
60
60
|
> {
|
|
61
|
-
const validatedMessages = await
|
|
61
|
+
const validatedMessages = await validateUIMessagesForAgent<
|
|
62
62
|
UIMessage<MESSAGE_METADATA, never, InferUITools<TOOLS>>
|
|
63
63
|
>({
|
|
64
64
|
messages: uiMessages,
|
|
@@ -64,6 +64,7 @@ import type {
|
|
|
64
64
|
InferUIMessageChunk,
|
|
65
65
|
UIMessageChunk,
|
|
66
66
|
} from '../ui-message-stream/ui-message-chunks';
|
|
67
|
+
import type { UIMessageStreamOutcome } from '../ui-message-stream/ui-message-stream-outcome';
|
|
67
68
|
import type { UIMessageStreamResponseInit } from '../ui-message-stream/ui-message-stream-response-init';
|
|
68
69
|
import type {
|
|
69
70
|
InferUIMessageData,
|
|
@@ -2660,6 +2661,26 @@ class DefaultStreamTextResult<
|
|
|
2660
2661
|
}: UIMessageStreamOptions<UI_MESSAGE> = {}): AsyncIterableStream<
|
|
2661
2662
|
InferUIMessageChunk<UI_MESSAGE>
|
|
2662
2663
|
> {
|
|
2664
|
+
let outcome: UIMessageStreamOutcome = { status: 'unknown' };
|
|
2665
|
+
let hasFatalFailure = false;
|
|
2666
|
+
|
|
2667
|
+
const setSourceOutcome = (newOutcome: UIMessageStreamOutcome) => {
|
|
2668
|
+
if (
|
|
2669
|
+
!hasFatalFailure &&
|
|
2670
|
+
outcome.status !== 'completed' &&
|
|
2671
|
+
outcome.status !== 'aborted' &&
|
|
2672
|
+
newOutcome.status !== 'unknown' &&
|
|
2673
|
+
(outcome.status === 'unknown' || newOutcome.status !== 'failed')
|
|
2674
|
+
) {
|
|
2675
|
+
outcome = newOutcome;
|
|
2676
|
+
}
|
|
2677
|
+
};
|
|
2678
|
+
|
|
2679
|
+
const failOutcome = (error: unknown) => {
|
|
2680
|
+
hasFatalFailure = true;
|
|
2681
|
+
outcome = { status: 'failed', error };
|
|
2682
|
+
};
|
|
2683
|
+
|
|
2663
2684
|
const responseMessageId =
|
|
2664
2685
|
generateMessageId != null
|
|
2665
2686
|
? getResponseUIMessageId({
|
|
@@ -2680,7 +2701,58 @@ class DefaultStreamTextResult<
|
|
|
2680
2701
|
return tool?.type === 'dynamic' ? true : undefined;
|
|
2681
2702
|
};
|
|
2682
2703
|
|
|
2683
|
-
const
|
|
2704
|
+
const trackFatalFailures = <T>(stream: ReadableStream<T>) => {
|
|
2705
|
+
const reader = stream.getReader();
|
|
2706
|
+
let readerReleased = false;
|
|
2707
|
+
let streamCancelled = false;
|
|
2708
|
+
|
|
2709
|
+
const releaseReader = () => {
|
|
2710
|
+
if (!readerReleased) {
|
|
2711
|
+
reader.releaseLock();
|
|
2712
|
+
readerReleased = true;
|
|
2713
|
+
}
|
|
2714
|
+
};
|
|
2715
|
+
|
|
2716
|
+
return new ReadableStream<T>({
|
|
2717
|
+
async pull(controller) {
|
|
2718
|
+
try {
|
|
2719
|
+
const { done, value } = await reader.read();
|
|
2720
|
+
|
|
2721
|
+
if (done) {
|
|
2722
|
+
releaseReader();
|
|
2723
|
+
if (!streamCancelled) {
|
|
2724
|
+
controller.close();
|
|
2725
|
+
}
|
|
2726
|
+
} else {
|
|
2727
|
+
controller.enqueue(value);
|
|
2728
|
+
}
|
|
2729
|
+
} catch (error) {
|
|
2730
|
+
releaseReader();
|
|
2731
|
+
if (!streamCancelled) {
|
|
2732
|
+
failOutcome(error);
|
|
2733
|
+
controller.error(error);
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
},
|
|
2737
|
+
|
|
2738
|
+
async cancel(reason) {
|
|
2739
|
+
streamCancelled = true;
|
|
2740
|
+
if (readerReleased) {
|
|
2741
|
+
return;
|
|
2742
|
+
}
|
|
2743
|
+
|
|
2744
|
+
try {
|
|
2745
|
+
await reader.cancel(reason);
|
|
2746
|
+
} finally {
|
|
2747
|
+
releaseReader();
|
|
2748
|
+
}
|
|
2749
|
+
},
|
|
2750
|
+
});
|
|
2751
|
+
};
|
|
2752
|
+
|
|
2753
|
+
const sourceStream = trackFatalFailures(this.fullStream);
|
|
2754
|
+
|
|
2755
|
+
const convertedStream = sourceStream.pipeThrough(
|
|
2684
2756
|
new TransformStream<
|
|
2685
2757
|
TextStreamPart<TOOLS>,
|
|
2686
2758
|
UIMessageChunk<
|
|
@@ -3021,10 +3093,20 @@ class DefaultStreamTextResult<
|
|
|
3021
3093
|
messageMetadata: messageMetadataValue,
|
|
3022
3094
|
});
|
|
3023
3095
|
}
|
|
3096
|
+
|
|
3097
|
+
if (part.type === 'finish') {
|
|
3098
|
+
setSourceOutcome({ status: 'completed' });
|
|
3099
|
+
} else if (part.type === 'abort') {
|
|
3100
|
+
setSourceOutcome({ status: 'aborted' });
|
|
3101
|
+
} else if (part.type === 'error') {
|
|
3102
|
+
setSourceOutcome({ status: 'failed', error: part.error });
|
|
3103
|
+
}
|
|
3024
3104
|
},
|
|
3025
3105
|
}),
|
|
3026
3106
|
);
|
|
3027
3107
|
|
|
3108
|
+
const baseStream = trackFatalFailures(convertedStream);
|
|
3109
|
+
|
|
3028
3110
|
return createAsyncIterableStream(
|
|
3029
3111
|
handleUIMessageStreamFinish<UI_MESSAGE>({
|
|
3030
3112
|
stream: baseStream,
|
|
@@ -3032,6 +3114,7 @@ class DefaultStreamTextResult<
|
|
|
3032
3114
|
originalMessages,
|
|
3033
3115
|
onFinish,
|
|
3034
3116
|
onError,
|
|
3117
|
+
getOutcome: () => outcome,
|
|
3035
3118
|
}),
|
|
3036
3119
|
);
|
|
3037
3120
|
}
|
|
@@ -6,7 +6,7 @@ import type { Agent } from '../agent/agent';
|
|
|
6
6
|
import type { ChatTransport } from './chat-transport';
|
|
7
7
|
import { convertToModelMessages } from './convert-to-model-messages';
|
|
8
8
|
import type { InferUITools, UIMessage } from './ui-messages';
|
|
9
|
-
import {
|
|
9
|
+
import { validateUIMessagesForAgent } from './validate-ui-messages';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Options for the `DirectChatTransport` class.
|
|
@@ -80,7 +80,7 @@ export class DirectChatTransport<
|
|
|
80
80
|
ReadableStream<UIMessageChunk>
|
|
81
81
|
> {
|
|
82
82
|
// Validate the incoming UI messages
|
|
83
|
-
const validatedMessages = await
|
|
83
|
+
const validatedMessages = await validateUIMessagesForAgent<UI_MESSAGE>({
|
|
84
84
|
messages,
|
|
85
85
|
tools: this.agent.tools,
|
|
86
86
|
});
|
|
@@ -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;
|