ai 7.0.79 → 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.
Files changed (59) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/index.d.ts +139 -53
  3. package/dist/index.js +872 -496
  4. package/dist/index.js.map +1 -1
  5. package/dist/internal/index.d.ts +9 -1
  6. package/dist/internal/index.js +4 -2
  7. package/dist/internal/index.js.map +1 -1
  8. package/dist/test/index.d.ts +4 -1
  9. package/dist/test/index.js +4 -0
  10. package/dist/test/index.js.map +1 -1
  11. package/docs/03-agents/06-policy-tool-approvals.mdx +1 -1
  12. package/docs/03-agents/06-tool-approvals.mdx +6 -0
  13. package/docs/03-ai-sdk-core/15-tools-and-tool-calling.mdx +6 -4
  14. package/docs/03-ai-sdk-core/50-error-handling.mdx +17 -2
  15. package/docs/04-ai-sdk-ui/03-chatbot-tool-usage.mdx +6 -0
  16. package/docs/04-ai-sdk-ui/50-stream-protocol.mdx +2 -2
  17. package/docs/07-reference/01-ai-sdk-core/01-generate-text.mdx +1 -1
  18. package/docs/07-reference/01-ai-sdk-core/02-stream-text.mdx +8 -7
  19. package/docs/07-reference/01-ai-sdk-core/06-embed-many.mdx +6 -2
  20. package/docs/07-reference/01-ai-sdk-core/16-tool-loop-agent.mdx +1 -1
  21. package/docs/07-reference/02-ai-sdk-ui/31-convert-to-model-messages.mdx +2 -2
  22. package/docs/07-reference/02-ai-sdk-ui/40-create-ui-message-stream.mdx +35 -6
  23. package/docs/07-reference/04-ai-sdk-workflow/03-generate-video.mdx +157 -0
  24. package/docs/07-reference/04-ai-sdk-workflow/index.mdx +6 -0
  25. package/docs/07-reference/05-ai-sdk-errors/ai-stream-provider-error.mdx +52 -0
  26. package/docs/07-reference/05-ai-sdk-errors/index.mdx +1 -0
  27. package/package.json +14 -3
  28. package/src/agent/create-agent-ui-stream.ts +2 -2
  29. package/src/embed/embed-many.ts +78 -8
  30. package/src/error/index.ts +1 -0
  31. package/src/error/stream-provider-error.ts +78 -0
  32. package/src/generate-text/execute-tools-from-stream.ts +3 -0
  33. package/src/generate-text/generate-text-result.ts +2 -1
  34. package/src/generate-text/generate-text.ts +6 -3
  35. package/src/generate-text/stream-language-model-call.ts +8 -0
  36. package/src/generate-text/to-response-messages.ts +1 -0
  37. package/src/generate-text/tool-approval-configuration.ts +5 -1
  38. package/src/generate-text/tool-approval-request-output.ts +5 -0
  39. package/src/middleware/wrap-embedding-model.ts +11 -2
  40. package/src/model/get-embedding-model-max-input-bytes-per-call.ts +15 -0
  41. package/src/prompt/content-part.ts +1 -0
  42. package/src/prompt/normalize-stream-provider-error.ts +131 -0
  43. package/src/test/mock-embedding-model-v4.ts +9 -0
  44. package/src/ui/convert-to-model-messages.ts +3 -0
  45. package/src/ui/direct-chat-transport.ts +2 -2
  46. package/src/ui/last-assistant-message-is-complete-with-approval-responses.ts +1 -0
  47. package/src/ui/process-ui-message-stream.ts +6 -0
  48. package/src/ui/ui-messages.ts +10 -0
  49. package/src/ui/validate-ui-messages.ts +146 -55
  50. package/src/ui-message-stream/create-ui-message-stream.ts +47 -17
  51. package/src/ui-message-stream/handle-ui-message-stream-finish.ts +51 -14
  52. package/src/ui-message-stream/index.ts +5 -1
  53. package/src/ui-message-stream/to-ui-message-chunk.ts +1 -0
  54. package/src/ui-message-stream/to-ui-message-stream.ts +108 -26
  55. package/src/ui-message-stream/ui-message-chunks.ts +2 -0
  56. package/src/ui-message-stream/ui-message-stream-on-end-callback.ts +7 -0
  57. package/src/ui-message-stream/ui-message-stream-outcome.ts +12 -0
  58. package/src/ui-message-stream/ui-message-stream-writer.ts +15 -0
  59. package/src/util/create-stitchable-stream.ts +31 -1
@@ -0,0 +1,131 @@
1
+ import { AISDKError } from '@ai-sdk/provider';
2
+ import { isProviderStreamError } from '@ai-sdk/provider-utils';
3
+ import { StreamProviderError } from '../error/stream-provider-error';
4
+
5
+ /**
6
+ * Normalizes well-formed provider error payloads without changing existing
7
+ * Error instances or malformed/unknown values.
8
+ */
9
+ export function normalizeStreamProviderError(error: unknown): unknown {
10
+ if (
11
+ isError(error) ||
12
+ AISDKError.isInstance(error) ||
13
+ StreamProviderError.isInstance(error)
14
+ ) {
15
+ return error;
16
+ }
17
+
18
+ const outer = asRecord(error);
19
+ if (outer == null) {
20
+ return error;
21
+ }
22
+
23
+ const providerStreamError = isProviderStreamError(error);
24
+ const details = providerStreamError
25
+ ? outer
26
+ : (asRecord(asRecord(outer.response)?.error) ??
27
+ asRecord(outer.error) ??
28
+ outer);
29
+
30
+ if (typeof details.message !== 'string') {
31
+ return error;
32
+ }
33
+
34
+ const type = getString(details.type) ?? getString(outer.type);
35
+ const code = getStringOrNumber(details.code) ?? getStringOrNumber(outer.code);
36
+ const explicitStatusCode =
37
+ getHttpStatusCode(details.statusCode) ??
38
+ getHttpStatusCode(outer.statusCode) ??
39
+ getHttpStatusCode(details.status_code) ??
40
+ getHttpStatusCode(outer.status_code) ??
41
+ getHttpStatusCode(details.status) ??
42
+ getHttpStatusCode(outer.status) ??
43
+ getHttpStatusCode(details.code) ??
44
+ getHttpStatusCode(outer.code);
45
+ const messageMetadata = inferExactMessageMetadata(details.message);
46
+ const statusCode = explicitStatusCode ?? messageMetadata?.statusCode;
47
+ const explicitRetryability =
48
+ getBoolean(details.isRetryable) ??
49
+ getBoolean(outer.isRetryable) ??
50
+ getBoolean(details.is_retryable) ??
51
+ getBoolean(outer.is_retryable);
52
+
53
+ return new StreamProviderError({
54
+ message: details.message,
55
+ type,
56
+ code,
57
+ statusCode,
58
+ isRetryable:
59
+ explicitRetryability ??
60
+ messageMetadata?.isRetryable ??
61
+ isRetryableStatusCode(statusCode),
62
+ data: providerStreamError ? error.data : error,
63
+ });
64
+ }
65
+
66
+ function inferExactMessageMetadata(
67
+ message: string,
68
+ ): { statusCode: number; isRetryable: true } | undefined {
69
+ switch (message.trim().toLowerCase()) {
70
+ case 'overloaded':
71
+ case 'overloaded error':
72
+ case 'model overloaded':
73
+ return { statusCode: 503, isRetryable: true };
74
+ case 'internal server error':
75
+ return { statusCode: 500, isRetryable: true };
76
+ case 'service unavailable':
77
+ return { statusCode: 503, isRetryable: true };
78
+ default:
79
+ return undefined;
80
+ }
81
+ }
82
+
83
+ function isRetryableStatusCode(statusCode: number | undefined): boolean {
84
+ return (
85
+ statusCode != null &&
86
+ (statusCode === 408 ||
87
+ statusCode === 409 ||
88
+ statusCode === 429 ||
89
+ statusCode >= 500)
90
+ );
91
+ }
92
+
93
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
94
+ return typeof value === 'object' && value != null
95
+ ? (value as Record<string, unknown>)
96
+ : undefined;
97
+ }
98
+
99
+ // `instanceof` misses Error instances created in another JavaScript realm.
100
+ function isError(value: unknown): value is Error {
101
+ return (
102
+ value instanceof Error ||
103
+ Object.prototype.toString.call(value) === '[object Error]'
104
+ );
105
+ }
106
+
107
+ function getString(value: unknown): string | undefined {
108
+ return typeof value === 'string' ? value : undefined;
109
+ }
110
+
111
+ function getStringOrNumber(value: unknown): string | number | undefined {
112
+ return typeof value === 'string' || typeof value === 'number'
113
+ ? value
114
+ : undefined;
115
+ }
116
+
117
+ function getBoolean(value: unknown): boolean | undefined {
118
+ return typeof value === 'boolean' ? value : undefined;
119
+ }
120
+
121
+ function getHttpStatusCode(value: unknown): number | undefined {
122
+ const statusCode =
123
+ typeof value === 'string' && /^\d{3}$/.test(value) ? Number(value) : value;
124
+
125
+ return typeof statusCode === 'number' &&
126
+ Number.isInteger(statusCode) &&
127
+ statusCode >= 400 &&
128
+ statusCode <= 599
129
+ ? statusCode
130
+ : undefined;
131
+ }
@@ -1,4 +1,5 @@
1
1
  import type { EmbeddingModelV4 } from '@ai-sdk/provider';
2
+ import { EXPERIMENTAL_EMBEDDING_MODEL_MAX_INPUT_BYTES_PER_CALL } from '@ai-sdk/provider-utils';
2
3
  import { notImplemented } from './not-implemented';
3
4
 
4
5
  export class MockEmbeddingModelV4 implements EmbeddingModelV4 {
@@ -7,6 +8,10 @@ export class MockEmbeddingModelV4 implements EmbeddingModelV4 {
7
8
  readonly provider: EmbeddingModelV4['provider'];
8
9
  readonly modelId: EmbeddingModelV4['modelId'];
9
10
  readonly maxEmbeddingsPerCall: EmbeddingModelV4['maxEmbeddingsPerCall'];
11
+ readonly [EXPERIMENTAL_EMBEDDING_MODEL_MAX_INPUT_BYTES_PER_CALL]:
12
+ | PromiseLike<number | undefined>
13
+ | number
14
+ | undefined;
10
15
  readonly supportsParallelCalls: EmbeddingModelV4['supportsParallelCalls'];
11
16
 
12
17
  doEmbed: EmbeddingModelV4['doEmbed'];
@@ -17,12 +22,14 @@ export class MockEmbeddingModelV4 implements EmbeddingModelV4 {
17
22
  provider = 'mock-provider',
18
23
  modelId = 'mock-model-id',
19
24
  maxEmbeddingsPerCall = 1,
25
+ maxInputBytesPerCall,
20
26
  supportsParallelCalls = false,
21
27
  doEmbed = notImplemented,
22
28
  }: {
23
29
  provider?: EmbeddingModelV4['provider'];
24
30
  modelId?: EmbeddingModelV4['modelId'];
25
31
  maxEmbeddingsPerCall?: EmbeddingModelV4['maxEmbeddingsPerCall'] | null;
32
+ maxInputBytesPerCall?: PromiseLike<number | undefined> | number | undefined;
26
33
  supportsParallelCalls?: EmbeddingModelV4['supportsParallelCalls'];
27
34
  doEmbed?:
28
35
  | EmbeddingModelV4['doEmbed']
@@ -32,6 +39,8 @@ export class MockEmbeddingModelV4 implements EmbeddingModelV4 {
32
39
  this.provider = provider;
33
40
  this.modelId = modelId;
34
41
  this.maxEmbeddingsPerCall = maxEmbeddingsPerCall ?? undefined;
42
+ this[EXPERIMENTAL_EMBEDDING_MODEL_MAX_INPUT_BYTES_PER_CALL] =
43
+ maxInputBytesPerCall;
35
44
  this.supportsParallelCalls = supportsParallelCalls;
36
45
  this.doEmbed = async options => {
37
46
  this.doEmbedCalls.push(options);
@@ -233,6 +233,9 @@ export async function convertToModelMessages<UI_MESSAGE extends UIMessage>(
233
233
  approvalId: part.approval.id,
234
234
  toolCallId: part.toolCallId,
235
235
  isAutomatic: part.approval.isAutomatic,
236
+ ...(part.approval.requestReason != null
237
+ ? { reason: part.approval.requestReason }
238
+ : {}),
236
239
  ...(part.approval.signature != null
237
240
  ? { signature: part.approval.signature }
238
241
  : {}),
@@ -11,7 +11,7 @@ import type {
11
11
  InferUITools,
12
12
  UIMessage,
13
13
  } from './ui-messages';
14
- import { validateUIMessages } from './validate-ui-messages';
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 validateUIMessages<UI_MESSAGE>({
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
@@ -36,6 +36,7 @@ export function lastAssistantMessageIsCompleteWithApprovalResponses({
36
36
  part =>
37
37
  part.state === 'output-available' ||
38
38
  part.state === 'output-error' ||
39
+ part.state === 'output-denied' ||
39
40
  part.state === 'approval-responded',
40
41
  )
41
42
  );
@@ -748,6 +748,9 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
748
748
  toolInvocation.state = 'approval-requested';
749
749
  toolInvocation.approval = {
750
750
  id: chunk.approvalId,
751
+ ...(chunk.reason != null
752
+ ? { requestReason: chunk.reason }
753
+ : {}),
751
754
  ...(chunk.isAutomatic === true ? { isAutomatic: true } : {}),
752
755
  ...(chunk.signature != null
753
756
  ? { signature: chunk.signature }
@@ -770,6 +773,9 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
770
773
  toolInvocation.approval = {
771
774
  id: chunk.approvalId,
772
775
  approved: chunk.approved,
776
+ ...(approval.requestReason != null
777
+ ? { requestReason: approval.requestReason }
778
+ : {}),
773
779
  ...(chunk.reason != null ? { reason: chunk.reason } : {}),
774
780
  ...(approval.isAutomatic === true ? { isAutomatic: true } : {}),
775
781
  ...(approval.signature != null
@@ -319,6 +319,7 @@ export type UIToolInvocation<TOOL extends UITool | Tool> = {
319
319
  approval: {
320
320
  id: string;
321
321
  approved?: never;
322
+ requestReason?: string;
322
323
  reason?: never;
323
324
  isAutomatic?: boolean;
324
325
  signature?: string;
@@ -333,6 +334,7 @@ export type UIToolInvocation<TOOL extends UITool | Tool> = {
333
334
  approval: {
334
335
  id: string;
335
336
  approved: boolean;
337
+ requestReason?: string;
336
338
  reason?: string;
337
339
  isAutomatic?: boolean;
338
340
  signature?: string;
@@ -349,6 +351,7 @@ export type UIToolInvocation<TOOL extends UITool | Tool> = {
349
351
  approval?: {
350
352
  id: string;
351
353
  approved: true;
354
+ requestReason?: string;
352
355
  reason?: string;
353
356
  isAutomatic?: boolean;
354
357
  signature?: string;
@@ -365,6 +368,7 @@ export type UIToolInvocation<TOOL extends UITool | Tool> = {
365
368
  approval?: {
366
369
  id: string;
367
370
  approved: true;
371
+ requestReason?: string;
368
372
  reason?: string;
369
373
  isAutomatic?: boolean;
370
374
  signature?: string;
@@ -379,6 +383,7 @@ export type UIToolInvocation<TOOL extends UITool | Tool> = {
379
383
  approval: {
380
384
  id: string;
381
385
  approved: false;
386
+ requestReason?: string;
382
387
  reason?: string;
383
388
  isAutomatic?: boolean;
384
389
  signature?: string;
@@ -437,6 +442,7 @@ export type DynamicToolUIPart = {
437
442
  approval: {
438
443
  id: string;
439
444
  approved?: never;
445
+ requestReason?: string;
440
446
  reason?: never;
441
447
  isAutomatic?: boolean;
442
448
  signature?: string;
@@ -451,6 +457,7 @@ export type DynamicToolUIPart = {
451
457
  approval: {
452
458
  id: string;
453
459
  approved: boolean;
460
+ requestReason?: string;
454
461
  reason?: string;
455
462
  isAutomatic?: boolean;
456
463
  signature?: string;
@@ -467,6 +474,7 @@ export type DynamicToolUIPart = {
467
474
  approval?: {
468
475
  id: string;
469
476
  approved: true;
477
+ requestReason?: string;
470
478
  reason?: string;
471
479
  isAutomatic?: boolean;
472
480
  signature?: string;
@@ -482,6 +490,7 @@ export type DynamicToolUIPart = {
482
490
  approval?: {
483
491
  id: string;
484
492
  approved: true;
493
+ requestReason?: string;
485
494
  reason?: string;
486
495
  isAutomatic?: boolean;
487
496
  signature?: string;
@@ -496,6 +505,7 @@ export type DynamicToolUIPart = {
496
505
  approval: {
497
506
  id: string;
498
507
  approved: false;
508
+ requestReason?: string;
499
509
  reason?: string;
500
510
  isAutomatic?: boolean;
501
511
  signature?: string;
@@ -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
@@ -132,6 +153,7 @@ const uiMessagesSchema = lazySchema(() =>
132
153
  approval: z.object({
133
154
  id: z.string(),
134
155
  approved: z.never().optional(),
156
+ requestReason: z.string().optional(),
135
157
  reason: z.never().optional(),
136
158
  isAutomatic: z.boolean().optional(),
137
159
  signature: z.string().optional(),
@@ -151,6 +173,7 @@ const uiMessagesSchema = lazySchema(() =>
151
173
  approval: z.object({
152
174
  id: z.string(),
153
175
  approved: z.boolean(),
176
+ requestReason: z.string().optional(),
154
177
  reason: z.string().optional(),
155
178
  isAutomatic: z.boolean().optional(),
156
179
  signature: z.string().optional(),
@@ -173,6 +196,7 @@ const uiMessagesSchema = lazySchema(() =>
173
196
  .object({
174
197
  id: z.string(),
175
198
  approved: z.literal(true),
199
+ requestReason: z.string().optional(),
176
200
  reason: z.string().optional(),
177
201
  isAutomatic: z.boolean().optional(),
178
202
  signature: z.string().optional(),
@@ -196,6 +220,7 @@ const uiMessagesSchema = lazySchema(() =>
196
220
  .object({
197
221
  id: z.string(),
198
222
  approved: z.literal(true),
223
+ requestReason: z.string().optional(),
199
224
  reason: z.string().optional(),
200
225
  isAutomatic: z.boolean().optional(),
201
226
  signature: z.string().optional(),
@@ -216,6 +241,7 @@ const uiMessagesSchema = lazySchema(() =>
216
241
  approval: z.object({
217
242
  id: z.string(),
218
243
  approved: z.literal(false),
244
+ requestReason: z.string().optional(),
219
245
  reason: z.string().optional(),
220
246
  isAutomatic: z.boolean().optional(),
221
247
  signature: z.string().optional(),
@@ -258,6 +284,7 @@ const uiMessagesSchema = lazySchema(() =>
258
284
  approval: z.object({
259
285
  id: z.string(),
260
286
  approved: z.never().optional(),
287
+ requestReason: z.string().optional(),
261
288
  reason: z.never().optional(),
262
289
  isAutomatic: z.boolean().optional(),
263
290
  signature: z.string().optional(),
@@ -276,6 +303,7 @@ const uiMessagesSchema = lazySchema(() =>
276
303
  approval: z.object({
277
304
  id: z.string(),
278
305
  approved: z.boolean(),
306
+ requestReason: z.string().optional(),
279
307
  reason: z.string().optional(),
280
308
  isAutomatic: z.boolean().optional(),
281
309
  signature: z.string().optional(),
@@ -297,6 +325,7 @@ const uiMessagesSchema = lazySchema(() =>
297
325
  .object({
298
326
  id: z.string(),
299
327
  approved: z.literal(true),
328
+ requestReason: z.string().optional(),
300
329
  reason: z.string().optional(),
301
330
  isAutomatic: z.boolean().optional(),
302
331
  signature: z.string().optional(),
@@ -319,6 +348,7 @@ const uiMessagesSchema = lazySchema(() =>
319
348
  .object({
320
349
  id: z.string(),
321
350
  approved: z.literal(true),
351
+ requestReason: z.string().optional(),
322
352
  reason: z.string().optional(),
323
353
  isAutomatic: z.boolean().optional(),
324
354
  signature: z.string().optional(),
@@ -338,6 +368,7 @@ const uiMessagesSchema = lazySchema(() =>
338
368
  approval: z.object({
339
369
  id: z.string(),
340
370
  approved: z.literal(false),
371
+ requestReason: z.string().optional(),
341
372
  reason: z.string().optional(),
342
373
  isAutomatic: z.boolean().optional(),
343
374
  signature: z.string().optional(),
@@ -374,17 +405,7 @@ export type SafeValidateUIMessagesResult<UI_MESSAGE extends UIMessage> =
374
405
  error: Error;
375
406
  };
376
407
 
377
- /**
378
- * Validates a list of UI messages like `validateUIMessages`,
379
- * but instead of throwing it returns `{ success: true, data }`
380
- * or `{ success: false, error }`.
381
- */
382
- export async function safeValidateUIMessages<UI_MESSAGE extends UIMessage>({
383
- messages,
384
- metadataSchema,
385
- dataSchemas,
386
- tools,
387
- }: {
408
+ type ValidateUIMessagesOptions<UI_MESSAGE extends UIMessage> = {
388
409
  messages: unknown;
389
410
  metadataSchema?: FlexibleSchema<UIMessage['metadata']>;
390
411
  dataSchemas?: {
@@ -398,7 +419,21 @@ export async function safeValidateUIMessages<UI_MESSAGE extends UIMessage>({
398
419
  InferUIMessageTools<UI_MESSAGE>[NAME]['output']
399
420
  >;
400
421
  };
401
- }): Promise<SafeValidateUIMessagesResult<UI_MESSAGE>> {
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>> {
402
437
  try {
403
438
  if (messages == null) {
404
439
  return {
@@ -429,7 +464,10 @@ export async function safeValidateUIMessages<UI_MESSAGE extends UIMessage>({
429
464
  }
430
465
  }
431
466
 
432
- if (dataSchemas || tools) {
467
+ const shouldValidateToolParts =
468
+ tools != null || convertMissingTerminalToolsToDynamic;
469
+
470
+ if (dataSchemas || shouldValidateToolParts) {
433
471
  for (const [msgIdx, message] of validatedMessages.entries()) {
434
472
  for (const [partIdx, part] of message.parts.entries()) {
435
473
  // Data part validation
@@ -465,19 +503,26 @@ export async function safeValidateUIMessages<UI_MESSAGE extends UIMessage>({
465
503
  }
466
504
 
467
505
  // Tool part validation
468
- if (tools && part.type.startsWith('tool-')) {
506
+ if (shouldValidateToolParts && part.type.startsWith('tool-')) {
469
507
  const toolPart = part as ToolUIPart<
470
508
  InferUIMessageTools<UI_MESSAGE>
471
509
  >;
472
510
  const toolName = toolPart.type.slice(5);
473
- 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';
474
516
 
475
- if (
476
- !tool &&
477
- (toolPart.state === 'output-available' ||
478
- toolPart.state === 'output-error' ||
479
- toolPart.state === 'output-denied')
480
- ) {
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
+ }
481
526
  continue;
482
527
  }
483
528
 
@@ -497,19 +542,53 @@ export async function safeValidateUIMessages<UI_MESSAGE extends UIMessage>({
497
542
  };
498
543
  }
499
544
 
545
+ const inputValidationContext = {
546
+ field: `messages[${msgIdx}].parts[${partIdx}].input`,
547
+ entityName: toolName,
548
+ entityId: toolPart.toolCallId,
549
+ };
550
+ let convertToDynamic = false;
551
+
500
552
  // Tool input validation
501
- // Note: input is intentionally not re-validated for terminal states.
502
- // Terminal tool calls can keep invalid or incomplete input, and
503
- // re-validating it on replay would crash follow-up messages.
504
- if (toolPart.state === 'input-available') {
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
+ ) {
505
588
  await validateTypes({
506
589
  value: toolPart.input,
507
590
  schema: tool.inputSchema,
508
- context: {
509
- field: `messages[${msgIdx}].parts[${partIdx}].input`,
510
- entityName: toolName,
511
- entityId: toolPart.toolCallId,
512
- },
591
+ context: inputValidationContext,
513
592
  });
514
593
  }
515
594
 
@@ -525,6 +604,12 @@ export async function safeValidateUIMessages<UI_MESSAGE extends UIMessage>({
525
604
  },
526
605
  });
527
606
  }
607
+
608
+ if (convertToDynamic) {
609
+ message.parts[partIdx] = asDynamicToolPart(
610
+ toolPart,
611
+ ) as (typeof message.parts)[number];
612
+ }
528
613
  }
529
614
  }
530
615
  }
@@ -544,6 +629,19 @@ export async function safeValidateUIMessages<UI_MESSAGE extends UIMessage>({
544
629
  }
545
630
  }
546
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
+
547
645
  /**
548
646
  * Validates a list of UI messages.
549
647
  *
@@ -551,31 +649,24 @@ export async function safeValidateUIMessages<UI_MESSAGE extends UIMessage>({
551
649
  * the corresponding schemas are provided. Otherwise, they are assumed to be
552
650
  * valid.
553
651
  */
554
- export async function validateUIMessages<UI_MESSAGE extends UIMessage>({
555
- messages,
556
- metadataSchema,
557
- dataSchemas,
558
- tools,
559
- }: {
560
- messages: unknown;
561
- metadataSchema?: FlexibleSchema<UIMessage['metadata']>;
562
- dataSchemas?: {
563
- [NAME in keyof InferUIMessageData<UI_MESSAGE> & string]?: FlexibleSchema<
564
- InferUIMessageData<UI_MESSAGE>[NAME]
565
- >;
566
- };
567
- tools?: {
568
- [NAME in keyof InferUIMessageTools<UI_MESSAGE> & string]?: Tool<
569
- InferUIMessageTools<UI_MESSAGE>[NAME]['input'],
570
- InferUIMessageTools<UI_MESSAGE>[NAME]['output']
571
- >;
572
- };
573
- }): Promise<Array<UI_MESSAGE>> {
574
- const response = await safeValidateUIMessages({
575
- messages,
576
- metadataSchema,
577
- dataSchemas,
578
- 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,
579
670
  });
580
671
 
581
672
  if (!response.success) throw response.error;