ai 7.0.82 → 7.0.84

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.
@@ -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
  );
@@ -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
- }): 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>> {
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
- if (dataSchemas || tools) {
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 (tools && part.type.startsWith('tool-')) {
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
- !tool &&
487
- (toolPart.state === 'output-available' ||
488
- toolPart.state === 'output-error' ||
489
- toolPart.state === 'output-denied')
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
- // Note: input is intentionally not re-validated for terminal states.
512
- // Terminal tool calls can keep invalid or incomplete input, and
513
- // re-validating it on replay would crash follow-up messages.
514
- 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
+ ) {
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
- messages,
566
- metadataSchema,
567
- dataSchemas,
568
- tools,
569
- }: {
570
- messages: unknown;
571
- metadataSchema?: FlexibleSchema<UIMessage['metadata']>;
572
- dataSchemas?: {
573
- [NAME in keyof InferUIMessageData<UI_MESSAGE> & string]?: FlexibleSchema<
574
- InferUIMessageData<UI_MESSAGE>[NAME]
575
- >;
576
- };
577
- tools?: {
578
- [NAME in keyof InferUIMessageTools<UI_MESSAGE> & string]?: Tool<
579
- InferUIMessageTools<UI_MESSAGE>[NAME]['input'],
580
- InferUIMessageTools<UI_MESSAGE>[NAME]['output']
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 { UIMessageStreamWriter } from './ui-message-stream-writer';
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: UIMessageStreamWriter<UI_MESSAGE>;
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
- safeEnqueue({
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
- safeEnqueue({
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
- safeEnqueue({
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> = new Promise(async resolve => {
168
+ const waitForStreams: Promise<void> = (async () => {
139
169
  while (ongoingStreamPromises.length > 0) {
140
170
  await ongoingStreamPromises.shift();
141
171
  }
142
- resolve();
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
  }
@@ -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 { UIMessageStreamOnEndCallback } from './ui-message-stream-on-end-callback';
11
+ import type { UIMessageStreamOutcome } from './ui-message-stream-outcome';
11
12
  import type { UIMessageStreamOnStepEndCallback } from './ui-message-stream-on-step-end-callback';
12
13
  import type { UIMessageStreamOnStepFinishCallback } from './ui-message-stream-on-step-finish-callback';
13
14
 
@@ -20,6 +21,7 @@ export function handleUIMessageStreamFinish<UI_MESSAGE extends UIMessage>({
20
21
  onFinish,
21
22
  onError,
22
23
  stream,
24
+ getOutcome,
23
25
  }: {
24
26
  stream: ReadableStream<InferUIMessageChunk<UI_MESSAGE>>;
25
27
 
@@ -54,6 +56,11 @@ export function handleUIMessageStreamFinish<UI_MESSAGE extends UIMessage>({
54
56
  * @deprecated Use `onEnd` instead.
55
57
  */
56
58
  onFinish?: UIMessageStreamOnEndCallback<UI_MESSAGE>;
59
+
60
+ /**
61
+ * Returns the operation-level outcome declared by the stream owner.
62
+ */
63
+ getOutcome?: () => UIMessageStreamOutcome;
57
64
  }): ReadableStream<InferUIMessageChunk<UI_MESSAGE>> {
58
65
  // last message is only relevant for assistant messages
59
66
  let lastMessage: UI_MESSAGE | undefined =
@@ -66,6 +73,13 @@ export function handleUIMessageStreamFinish<UI_MESSAGE extends UIMessage>({
66
73
  }
67
74
 
68
75
  let isAborted = false;
76
+ let hasProcessingFailure = false;
77
+ let processingError: unknown;
78
+
79
+ const recordProcessingFailure = (error: unknown) => {
80
+ hasProcessingFailure = true;
81
+ processingError = error;
82
+ };
69
83
 
70
84
  const idInjectedStream = stream.pipeThrough(
71
85
  new TransformStream<
@@ -73,21 +87,31 @@ export function handleUIMessageStreamFinish<UI_MESSAGE extends UIMessage>({
73
87
  InferUIMessageChunk<UI_MESSAGE>
74
88
  >({
75
89
  transform(chunk, controller) {
76
- // when there is no messageId in the start chunk,
77
- // but the user checked for persistence,
78
- // inject the messageId into the chunk
79
- if (chunk.type === 'start') {
80
- const startChunk = chunk as UIMessageChunk & { type: 'start' };
81
- if (startChunk.messageId == null && messageId != null) {
82
- startChunk.messageId = messageId;
90
+ try {
91
+ let outputChunk = chunk;
92
+
93
+ // when there is no messageId in the start chunk,
94
+ // but the user checked for persistence,
95
+ // inject the messageId into the chunk
96
+ if (chunk.type === 'start') {
97
+ const startChunk = chunk as UIMessageChunk & { type: 'start' };
98
+ if (startChunk.messageId == null && messageId != null) {
99
+ outputChunk = {
100
+ ...startChunk,
101
+ messageId,
102
+ } as InferUIMessageChunk<UI_MESSAGE>;
103
+ }
83
104
  }
84
- }
85
105
 
86
- if (chunk.type === 'abort') {
87
- isAborted = true;
88
- }
106
+ if (chunk.type === 'abort') {
107
+ isAborted = true;
108
+ }
89
109
 
90
- controller.enqueue(chunk);
110
+ controller.enqueue(outputChunk);
111
+ } catch (error) {
112
+ recordProcessingFailure(error);
113
+ throw error;
114
+ }
91
115
  },
92
116
  }),
93
117
  );
@@ -113,7 +137,12 @@ export function handleUIMessageStreamFinish<UI_MESSAGE extends UIMessage>({
113
137
  write: (options?: UIMessageStreamWriteOptions) => void;
114
138
  }) => Promise<void>,
115
139
  ) => {
116
- await job({ state, write: () => {} });
140
+ try {
141
+ await job({ state, write: () => {} });
142
+ } catch (error) {
143
+ recordProcessingFailure(error);
144
+ throw error;
145
+ }
117
146
  };
118
147
 
119
148
  let finishCalled = false;
@@ -125,9 +154,17 @@ export function handleUIMessageStreamFinish<UI_MESSAGE extends UIMessage>({
125
154
  finishCalled = true;
126
155
 
127
156
  const isContinuation = state.message.id === lastMessage?.id;
157
+ const declaredOutcome = getOutcome?.() ?? { status: 'unknown' };
158
+ const outcome: UIMessageStreamOutcome = hasProcessingFailure
159
+ ? { status: 'failed', error: processingError }
160
+ : declaredOutcome.status === 'unknown' && isAborted
161
+ ? { status: 'aborted' }
162
+ : declaredOutcome;
163
+
128
164
  await resolvedOnEnd({
129
- isAborted,
165
+ isAborted: isAborted || outcome.status === 'aborted',
130
166
  isContinuation,
167
+ outcome,
131
168
  responseMessage: state.message as UI_MESSAGE,
132
169
  messages: [
133
170
  ...(isContinuation ? originalMessages.slice(0, -1) : originalMessages),
@@ -16,6 +16,10 @@ export {
16
16
  export { UI_MESSAGE_STREAM_HEADERS } from './ui-message-stream-headers';
17
17
  export type { UIMessageStreamOnEndCallback } from './ui-message-stream-on-end-callback';
18
18
  export type { UIMessageStreamOnFinishCallback } from './ui-message-stream-on-finish-callback';
19
+ export type { UIMessageStreamOutcome } from './ui-message-stream-outcome';
19
20
  export type { UIMessageStreamOnStepEndCallback } from './ui-message-stream-on-step-end-callback';
20
21
  export type { UIMessageStreamOnStepFinishCallback } from './ui-message-stream-on-step-finish-callback';
21
- export type { UIMessageStreamWriter } from './ui-message-stream-writer';
22
+ export type {
23
+ UIMessageStreamWriter,
24
+ UIMessageStreamWriterWithOutcome,
25
+ } from './ui-message-stream-writer';