ai 6.0.268 → 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.
Files changed (35) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/index.d.mts +48 -21
  3. package/dist/index.d.ts +48 -21
  4. package/dist/index.js +378 -130
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +317 -68
  7. package/dist/index.mjs.map +1 -1
  8. package/dist/internal/index.js +1 -1
  9. package/dist/internal/index.mjs +1 -1
  10. package/dist/test/index.d.mts +4 -1
  11. package/dist/test/index.d.ts +4 -1
  12. package/dist/test/index.js +6 -0
  13. package/dist/test/index.js.map +1 -1
  14. package/dist/test/index.mjs +6 -0
  15. package/dist/test/index.mjs.map +1 -1
  16. package/docs/07-reference/01-ai-sdk-core/02-stream-text.mdx +2 -2
  17. package/docs/07-reference/01-ai-sdk-core/06-embed-many.mdx +6 -2
  18. package/docs/07-reference/02-ai-sdk-ui/40-create-ui-message-stream.mdx +33 -5
  19. package/package.json +3 -3
  20. package/src/agent/create-agent-ui-stream.ts +2 -2
  21. package/src/embed/embed-many.ts +75 -6
  22. package/src/generate-text/stream-text.ts +84 -1
  23. package/src/middleware/wrap-embedding-model.ts +8 -1
  24. package/src/model/get-embedding-model-max-input-bytes-per-call.ts +15 -0
  25. package/src/test/mock-embedding-model-v3.ts +9 -0
  26. package/src/ui/direct-chat-transport.ts +2 -2
  27. package/src/ui/last-assistant-message-is-complete-with-approval-responses.ts +1 -0
  28. package/src/ui/validate-ui-messages.ts +136 -55
  29. package/src/ui-message-stream/create-ui-message-stream.ts +47 -17
  30. package/src/ui-message-stream/handle-ui-message-stream-finish.ts +58 -15
  31. package/src/ui-message-stream/index.ts +5 -1
  32. package/src/ui-message-stream/ui-message-stream-on-finish-callback.ts +7 -0
  33. package/src/ui-message-stream/ui-message-stream-outcome.ts +12 -0
  34. package/src/ui-message-stream/ui-message-stream-writer.ts +15 -0
  35. package/src/util/create-stitchable-stream.ts +35 -3
@@ -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 baseStream = this.fullStream.pipeThrough(
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
  }
@@ -2,6 +2,11 @@ import type {
2
2
  EmbeddingModelV3,
3
3
  EmbeddingModelV3CallOptions,
4
4
  } from '@ai-sdk/provider';
5
+ import { EXPERIMENTAL_EMBEDDING_MODEL_MAX_INPUT_BYTES_PER_CALL } from '@ai-sdk/provider-utils';
6
+ import {
7
+ getEmbeddingModelMaxInputBytesPerCall,
8
+ type EmbeddingModelWithMaxInputBytesPerCall,
9
+ } from '../model/get-embedding-model-max-input-bytes-per-call';
5
10
  import type { EmbeddingModelMiddleware } from '../types';
6
11
  import { asArray } from '../util/as-array';
7
12
 
@@ -52,7 +57,7 @@ const doWrap = ({
52
57
  middleware: EmbeddingModelMiddleware;
53
58
  modelId?: string;
54
59
  providerId?: string;
55
- }): EmbeddingModelV3 => {
60
+ }): EmbeddingModelWithMaxInputBytesPerCall => {
56
61
  async function doTransform({
57
62
  params,
58
63
  }: {
@@ -67,6 +72,8 @@ const doWrap = ({
67
72
  modelId: modelId ?? overrideModelId?.({ model }) ?? model.modelId,
68
73
  maxEmbeddingsPerCall:
69
74
  overrideMaxEmbeddingsPerCall?.({ model }) ?? model.maxEmbeddingsPerCall,
75
+ [EXPERIMENTAL_EMBEDDING_MODEL_MAX_INPUT_BYTES_PER_CALL]:
76
+ getEmbeddingModelMaxInputBytesPerCall(model),
70
77
  supportsParallelCalls:
71
78
  overrideSupportsParallelCalls?.({ model }) ?? model.supportsParallelCalls,
72
79
  async doEmbed(
@@ -0,0 +1,15 @@
1
+ import type { EmbeddingModelV3 } from '@ai-sdk/provider';
2
+ import { EXPERIMENTAL_EMBEDDING_MODEL_MAX_INPUT_BYTES_PER_CALL } from '@ai-sdk/provider-utils';
3
+
4
+ export type EmbeddingModelWithMaxInputBytesPerCall = EmbeddingModelV3 & {
5
+ readonly [EXPERIMENTAL_EMBEDDING_MODEL_MAX_INPUT_BYTES_PER_CALL]?:
6
+ | PromiseLike<number | undefined>
7
+ | number
8
+ | undefined;
9
+ };
10
+
11
+ export function getEmbeddingModelMaxInputBytesPerCall(model: EmbeddingModelV3) {
12
+ return (model as EmbeddingModelWithMaxInputBytesPerCall)[
13
+ EXPERIMENTAL_EMBEDDING_MODEL_MAX_INPUT_BYTES_PER_CALL
14
+ ];
15
+ }
@@ -1,4 +1,5 @@
1
1
  import type { EmbeddingModelV3 } 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 MockEmbeddingModelV3 implements EmbeddingModelV3 {
@@ -7,6 +8,10 @@ export class MockEmbeddingModelV3 implements EmbeddingModelV3 {
7
8
  readonly provider: EmbeddingModelV3['provider'];
8
9
  readonly modelId: EmbeddingModelV3['modelId'];
9
10
  readonly maxEmbeddingsPerCall: EmbeddingModelV3['maxEmbeddingsPerCall'];
11
+ readonly [EXPERIMENTAL_EMBEDDING_MODEL_MAX_INPUT_BYTES_PER_CALL]:
12
+ | PromiseLike<number | undefined>
13
+ | number
14
+ | undefined;
10
15
  readonly supportsParallelCalls: EmbeddingModelV3['supportsParallelCalls'];
11
16
 
12
17
  doEmbed: EmbeddingModelV3['doEmbed'];
@@ -17,12 +22,14 @@ export class MockEmbeddingModelV3 implements EmbeddingModelV3 {
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?: EmbeddingModelV3['provider'];
24
30
  modelId?: EmbeddingModelV3['modelId'];
25
31
  maxEmbeddingsPerCall?: EmbeddingModelV3['maxEmbeddingsPerCall'] | null;
32
+ maxInputBytesPerCall?: PromiseLike<number | undefined> | number | undefined;
26
33
  supportsParallelCalls?: EmbeddingModelV3['supportsParallelCalls'];
27
34
  doEmbed?:
28
35
  | EmbeddingModelV3['doEmbed']
@@ -32,6 +39,8 @@ export class MockEmbeddingModelV3 implements EmbeddingModelV3 {
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);
@@ -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 { validateUIMessages } from './validate-ui-messages';
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 validateUIMessages<UI_MESSAGE>({
83
+ const validatedMessages = await validateUIMessagesForAgent<UI_MESSAGE>({
84
84
  messages,
85
85
  tools: this.agent.tools,
86
86
  });
@@ -37,6 +37,7 @@ export function lastAssistantMessageIsCompleteWithApprovalResponses({
37
37
  part =>
38
38
  part.state === 'output-available' ||
39
39
  part.state === 'output-error' ||
40
+ part.state === 'output-denied' ||
40
41
  part.state === 'approval-responded',
41
42
  )
42
43
  );
@@ -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
- }): Promise<SafeValidateUIMessagesResult<UI_MESSAGE>> {
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
- if (dataSchemas || tools) {
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 (tools && part.type.startsWith('tool-')) {
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
- !tool &&
457
- (toolPart.state === 'output-available' ||
458
- toolPart.state === 'output-error' ||
459
- toolPart.state === 'output-denied')
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
- // Note: input is intentionally not re-validated for terminal states.
482
- // Terminal tool calls can keep invalid or incomplete input, and
483
- // re-validating it on replay would crash follow-up messages.
484
- if (toolPart.state === 'input-available') {
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
- messages,
536
- metadataSchema,
537
- dataSchemas,
538
- tools,
539
- }: {
540
- messages: unknown;
541
- metadataSchema?: FlexibleSchema<UIMessage['metadata']>;
542
- dataSchemas?: {
543
- [NAME in keyof InferUIMessageData<UI_MESSAGE> & string]?: FlexibleSchema<
544
- InferUIMessageData<UI_MESSAGE>[NAME]
545
- >;
546
- };
547
- tools?: {
548
- [NAME in keyof InferUIMessageTools<UI_MESSAGE> & string]?: Tool<
549
- InferUIMessageTools<UI_MESSAGE>[NAME]['input'],
550
- InferUIMessageTools<UI_MESSAGE>[NAME]['output']
551
- >;
552
- };
553
- }): Promise<Array<UI_MESSAGE>> {
554
- const response = await safeValidateUIMessages({
555
- messages,
556
- metadataSchema,
557
- dataSchemas,
558
- tools,
622
+ export async function validateUIMessages<UI_MESSAGE extends UIMessage>(
623
+ options: ValidateUIMessagesOptions<UI_MESSAGE>,
624
+ ): Promise<Array<UI_MESSAGE>> {
625
+ const response = await safeValidateUIMessages(options);
626
+
627
+ if (!response.success) throw response.error;
628
+
629
+ return response.data;
630
+ }
631
+
632
+ export async function validateUIMessagesForAgent<UI_MESSAGE extends UIMessage>(
633
+ options: ValidateUIMessagesOptions<UI_MESSAGE>,
634
+ ): Promise<Array<UI_MESSAGE>> {
635
+ const response = await safeValidateUIMessagesInternal(options, {
636
+ // Agent tool sets can include ephemeral tools (for example, tools from a
637
+ // disconnected MCP server), so terminal history is converted to dynamic
638
+ // tool parts when those tools are no longer registered.
639
+ convertMissingTerminalToolsToDynamic: true,
559
640
  });
560
641
 
561
642
  if (!response.success) throw response.error;
@@ -6,8 +6,9 @@ import type { UIMessage } from '../ui/ui-messages';
6
6
  import { handleUIMessageStreamFinish } from './handle-ui-message-stream-finish';
7
7
  import type { InferUIMessageChunk } from './ui-message-chunks';
8
8
  import type { UIMessageStreamOnFinishCallback } from './ui-message-stream-on-finish-callback';
9
+ import type { UIMessageStreamOutcome } from './ui-message-stream-outcome';
9
10
  import type { UIMessageStreamOnStepFinishCallback } from './ui-message-stream-on-step-finish-callback';
10
- import type { UIMessageStreamWriter } from './ui-message-stream-writer';
11
+ import type { UIMessageStreamWriterWithOutcome } from './ui-message-stream-writer';
11
12
 
12
13
  /**
13
14
  * Creates a UI message stream that can be used to send messages to the client.
@@ -31,7 +32,7 @@ export function createUIMessageStream<UI_MESSAGE extends UIMessage>({
31
32
  generateId = generateIdFunc,
32
33
  }: {
33
34
  execute: (options: {
34
- writer: UIMessageStreamWriter<UI_MESSAGE>;
35
+ writer: UIMessageStreamWriterWithOutcome<UI_MESSAGE>;
35
36
  }) => Promise<void> | void;
36
37
  onError?: (error: unknown) => string;
37
38
 
@@ -55,6 +56,7 @@ export function createUIMessageStream<UI_MESSAGE extends UIMessage>({
55
56
  >;
56
57
 
57
58
  const ongoingStreamPromises: Promise<void>[] = [];
59
+ let outcome: UIMessageStreamOutcome = { status: 'unknown' };
58
60
 
59
61
  const stream = new ReadableStream({
60
62
  start(controllerArg) {
@@ -70,6 +72,42 @@ export function createUIMessageStream<UI_MESSAGE extends UIMessage>({
70
72
  }
71
73
  }
72
74
 
75
+ function setOutcome(newOutcome: UIMessageStreamOutcome) {
76
+ if (outcome.status === 'unknown' && newOutcome.status !== 'unknown') {
77
+ outcome = newOutcome;
78
+ }
79
+ }
80
+
81
+ function failOutcome(error: unknown) {
82
+ outcome = { status: 'failed', error };
83
+ }
84
+
85
+ function safeError(error: unknown) {
86
+ try {
87
+ controller.error(error);
88
+ } catch {
89
+ // suppress errors when the stream has been closed
90
+ }
91
+ }
92
+
93
+ function handleError(error: unknown) {
94
+ failOutcome(error);
95
+
96
+ let errorText: string;
97
+ try {
98
+ errorText = onError(error);
99
+ } catch (onErrorError) {
100
+ failOutcome(onErrorError);
101
+ safeError(onErrorError);
102
+ return;
103
+ }
104
+
105
+ safeEnqueue({
106
+ type: 'error',
107
+ errorText,
108
+ } as InferUIMessageChunk<UI_MESSAGE>);
109
+ }
110
+
73
111
  try {
74
112
  const result = execute({
75
113
  writer: {
@@ -86,13 +124,11 @@ export function createUIMessageStream<UI_MESSAGE extends UIMessage>({
86
124
  safeEnqueue(value);
87
125
  }
88
126
  })().catch(error => {
89
- safeEnqueue({
90
- type: 'error',
91
- errorText: onError(error),
92
- } as InferUIMessageChunk<UI_MESSAGE>);
127
+ handleError(error);
93
128
  }),
94
129
  );
95
130
  },
131
+ setOutcome,
96
132
  onError,
97
133
  },
98
134
  });
@@ -100,30 +136,23 @@ export function createUIMessageStream<UI_MESSAGE extends UIMessage>({
100
136
  if (result) {
101
137
  ongoingStreamPromises.push(
102
138
  result.catch(error => {
103
- safeEnqueue({
104
- type: 'error',
105
- errorText: onError(error),
106
- } as InferUIMessageChunk<UI_MESSAGE>);
139
+ handleError(error);
107
140
  }),
108
141
  );
109
142
  }
110
143
  } catch (error) {
111
- safeEnqueue({
112
- type: 'error',
113
- errorText: onError(error),
114
- } as InferUIMessageChunk<UI_MESSAGE>);
144
+ handleError(error);
115
145
  }
116
146
 
117
147
  // Wait until all ongoing streams are done. This approach enables merging
118
148
  // streams even after execute has returned, as long as there is still an
119
149
  // open merged stream. This is important to e.g. forward new streams and
120
150
  // from callbacks.
121
- const waitForStreams: Promise<void> = new Promise(async resolve => {
151
+ const waitForStreams: Promise<void> = (async () => {
122
152
  while (ongoingStreamPromises.length > 0) {
123
153
  await ongoingStreamPromises.shift();
124
154
  }
125
- resolve();
126
- });
155
+ })();
127
156
 
128
157
  waitForStreams.finally(() => {
129
158
  try {
@@ -140,5 +169,6 @@ export function createUIMessageStream<UI_MESSAGE extends UIMessage>({
140
169
  onStepFinish,
141
170
  onFinish,
142
171
  onError,
172
+ getOutcome: () => outcome,
143
173
  });
144
174
  }