ai 7.0.82 → 7.0.83

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -92,7 +92,7 @@ import {
92
92
  } from "@ai-sdk/provider-utils";
93
93
 
94
94
  // src/version.ts
95
- var VERSION = true ? "7.0.82" : "0.0.0-test";
95
+ var VERSION = true ? "7.0.83" : "0.0.0-test";
96
96
 
97
97
  // src/util/download/download.ts
98
98
  var download = async ({
@@ -4034,13 +4034,13 @@ To see `streamText` in action, check out [these examples](#examples).
4034
4034
  },
4035
4035
  {
4036
4036
  name: 'onEnd',
4037
- type: '(options: { messages: UIMessage[]; isContinuation: boolean; responseMessage: UIMessage; isAborted: boolean; }) => void',
4037
+ type: '(options: { messages: UIMessage[]; isContinuation: boolean; responseMessage: UIMessage; isAborted: boolean; outcome: UIMessageStreamOutcome; finishReason?: FinishReason; }) => PromiseLike<void> | void',
4038
4038
  isOptional: true,
4039
- description: 'Callback function called when the stream ends. Provides the updated list of UI messages, whether the response is a continuation, the response message, and whether the stream was aborted.',
4039
+ description: 'Callback function called when the stream ends. Provides the updated messages, continuation and abort state, model finish reason, and operation-level outcome.',
4040
4040
  },
4041
4041
  {
4042
4042
  name: 'onFinish',
4043
- type: '(options: { messages: UIMessage[]; isContinuation: boolean; responseMessage: UIMessage; isAborted: boolean; }) => void',
4043
+ type: '(options: { messages: UIMessage[]; isContinuation: boolean; responseMessage: UIMessage; isAborted: boolean; outcome: UIMessageStreamOutcome; finishReason?: FinishReason; }) => PromiseLike<void> | void',
4044
4044
  isOptional: true,
4045
4045
  description: 'Deprecated alias for `onEnd`.',
4046
4046
  },
@@ -47,16 +47,33 @@ const stream = createUIMessageStream({
47
47
  prompt: 'Write a haiku about AI',
48
48
  });
49
49
 
50
- writer.merge(toUIMessageStream({ stream: result.stream }));
50
+ writer.merge(
51
+ toUIMessageStream({
52
+ stream: result.stream,
53
+ onEnd: ({ outcome }) => {
54
+ // The composer decides that the model stream outcome is also the
55
+ // aggregate stream outcome.
56
+ writer.setOutcome(outcome);
57
+ },
58
+ }),
59
+ );
51
60
  },
52
61
  onError: error => `Custom error: ${error.message}`,
53
62
  originalMessages: existingMessages,
54
- onEnd: ({ messages, isContinuation, responseMessage }) => {
63
+ onEnd: ({ messages, isContinuation, outcome, responseMessage }) => {
55
64
  console.log('Stream ended with messages:', messages);
65
+ console.log('Stream outcome:', outcome.status);
56
66
  },
57
67
  });
58
68
  ```
59
69
 
70
+ `setOutcome` records the composer's policy without writing a chunk or closing
71
+ the stream. The first outcome declared through `setOutcome` is retained, but a
72
+ fatal execution, merge, error-handling, or downstream processing failure makes
73
+ the final `onEnd` outcome `failed`. Individual `error` chunks do not change the
74
+ outcome by themselves. When merging multiple child streams, aggregate their
75
+ outcomes and call `setOutcome` once.
76
+
60
77
  ## API Signature
61
78
 
62
79
  ### Parameters
@@ -65,12 +82,12 @@ const stream = createUIMessageStream({
65
82
  content={[
66
83
  {
67
84
  name: 'execute',
68
- type: '(options: { writer: UIMessageStreamWriter }) => Promise<void> | void',
85
+ type: '(options: { writer: UIMessageStreamWriterWithOutcome }) => Promise<void> | void',
69
86
  description:
70
87
  'A function that receives a writer instance and can use it to write UI message chunks to the stream.',
71
88
  properties: [
72
89
  {
73
- type: 'UIMessageStreamWriter',
90
+ type: 'UIMessageStreamWriterWithOutcome',
74
91
  parameters: [
75
92
  {
76
93
  name: 'write',
@@ -83,6 +100,12 @@ const stream = createUIMessageStream({
83
100
  description:
84
101
  'Merges the contents of another UI message stream into this stream.',
85
102
  },
103
+ {
104
+ name: 'setOutcome',
105
+ type: '(outcome: UIMessageStreamOutcome) => void',
106
+ description:
107
+ "Declares the operation-level outcome of the composed stream. The first outcome declared through this method is retained, while fatal execution, merge, error-handling, or downstream processing failures override declarations. Supported statuses are 'completed', 'failed', 'aborted', and 'unknown'. Declaring an outcome does not write a chunk or close the stream.",
108
+ },
86
109
  {
87
110
  name: 'onError',
88
111
  type: '(error: unknown) => string',
@@ -107,7 +130,7 @@ const stream = createUIMessageStream({
107
130
  },
108
131
  {
109
132
  name: 'onEnd',
110
- type: '(options: { messages: UIMessage[]; isContinuation: boolean; isAborted: boolean; responseMessage: UIMessage; finishReason?: FinishReason }) => PromiseLike<void> | void',
133
+ type: '(options: { messages: UIMessage[]; isContinuation: boolean; isAborted: boolean; outcome: UIMessageStreamOutcome; responseMessage: UIMessage; finishReason?: FinishReason }) => PromiseLike<void> | void',
111
134
  description: 'A callback function that is called when the stream ends.',
112
135
  properties: [
113
136
  {
@@ -129,6 +152,12 @@ const stream = createUIMessageStream({
129
152
  type: 'boolean',
130
153
  description: 'Indicates whether the stream was aborted.',
131
154
  },
155
+ {
156
+ name: 'outcome',
157
+ type: "UIMessageStreamOutcome = { status: 'completed' } | { status: 'failed'; error?: unknown } | { status: 'aborted' } | { status: 'unknown' }",
158
+ description:
159
+ 'The operation-level outcome of the stream. It reflects the stream owner declaration unless a fatal stream-processing failure occurs, and is separate from model finish reasons and individual error chunks.',
160
+ },
132
161
  {
133
162
  name: 'responseMessage',
134
163
  type: 'UIMessage',
@@ -147,7 +176,7 @@ const stream = createUIMessageStream({
147
176
  },
148
177
  {
149
178
  name: 'onFinish',
150
- type: '(options: { messages: UIMessage[]; isContinuation: boolean; isAborted: boolean; responseMessage: UIMessage; finishReason?: FinishReason }) => PromiseLike<void> | void',
179
+ type: '(options: { messages: UIMessage[]; isContinuation: boolean; isAborted: boolean; outcome: UIMessageStreamOutcome; responseMessage: UIMessage; finishReason?: FinishReason }) => PromiseLike<void> | void',
151
180
  description: 'Deprecated alias for `onEnd`.',
152
181
  },
153
182
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai",
3
- "version": "7.0.82",
3
+ "version": "7.0.83",
4
4
  "type": "module",
5
5
  "description": "AI SDK by Vercel - build apps like ChatGPT, Claude, Gemini, and more with a single interface for any model using the Vercel AI Gateway or go direct to OpenAI, Anthropic, Google, or any other model provider.",
6
6
  "license": "Apache-2.0",
@@ -47,14 +47,14 @@
47
47
  "@ai-sdk/provider-utils": "5.0.32"
48
48
  },
49
49
  "devDependencies": {
50
- "@ai-sdk/amazon-bedrock": "5.0.65",
51
- "@ai-sdk/deepseek": "3.0.34",
52
- "@ai-sdk/google": "4.0.53",
50
+ "@ai-sdk/amazon-bedrock": "5.0.66",
51
+ "@ai-sdk/deepseek": "3.0.35",
52
+ "@ai-sdk/google": "4.0.54",
53
53
  "@ai-sdk/groq": "4.0.33",
54
54
  "@ai-sdk/huggingface": "2.0.39",
55
55
  "@ai-sdk/moonshotai": "3.0.41",
56
56
  "@ai-sdk/open-responses": "2.0.34",
57
- "@ai-sdk/openai": "4.0.49",
57
+ "@ai-sdk/openai": "4.0.50",
58
58
  "@ai-sdk/test-server": "2.0.1",
59
59
  "@ai-sdk/xai": "4.0.47",
60
60
  "@edge-runtime/vm": "^5.0.0",
@@ -21,7 +21,7 @@ import type {
21
21
  InferUITools,
22
22
  UIMessage,
23
23
  } from '../ui/ui-messages';
24
- import { validateUIMessages } from '../ui/validate-ui-messages';
24
+ import { validateUIMessagesForAgent } from '../ui/validate-ui-messages';
25
25
  import {
26
26
  createAsyncIterableStream,
27
27
  type AsyncIterableStream,
@@ -77,7 +77,7 @@ export async function createAgentUIStream<
77
77
  } & UIMessageStreamOptions<UI_MESSAGE>): Promise<
78
78
  AsyncIterableStream<InferUIMessageChunk<UI_MESSAGE>>
79
79
  > {
80
- const validatedMessages = await validateUIMessages<UI_MESSAGE>({
80
+ const validatedMessages = await validateUIMessagesForAgent<UI_MESSAGE>({
81
81
  messages: uiMessages,
82
82
  // tools are compatible; the casting is required because the context param is
83
83
  // not available in ui messages
@@ -11,7 +11,7 @@ import type {
11
11
  InferUITools,
12
12
  UIMessage,
13
13
  } from './ui-messages';
14
- import { 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
  );
@@ -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
  }