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
@@ -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';
@@ -254,6 +254,7 @@ export function toUIMessageChunk<
254
254
  type: 'tool-approval-request',
255
255
  approvalId: part.approvalId,
256
256
  toolCallId: part.toolCall.toolCallId,
257
+ ...(part.reason != null ? { reason: part.reason } : {}),
257
258
  ...(part.isAutomatic != null ? { isAutomatic: part.isAutomatic } : {}),
258
259
  ...(part.signature != null ? { signature: part.signature } : {}),
259
260
  };
@@ -7,6 +7,7 @@ import type { UIMessage } from '../ui/ui-messages';
7
7
  import { getResponseUIMessageId } from './get-response-ui-message-id';
8
8
  import { handleUIMessageStreamFinish } from './handle-ui-message-stream-finish';
9
9
  import type { InferUIMessageChunk } from './ui-message-chunks';
10
+ import type { UIMessageStreamOutcome } from './ui-message-stream-outcome';
10
11
  import { toUIMessageChunk } from './to-ui-message-chunk';
11
12
 
12
13
  /**
@@ -37,6 +38,26 @@ export function toUIMessageStream<
37
38
  } & UIMessageStreamOptions<UI_MESSAGE>): ReadableStream<
38
39
  InferUIMessageChunk<UI_MESSAGE>
39
40
  > {
41
+ let outcome: UIMessageStreamOutcome = { status: 'unknown' };
42
+ let hasFatalFailure = false;
43
+
44
+ const setSourceOutcome = (newOutcome: UIMessageStreamOutcome) => {
45
+ if (
46
+ !hasFatalFailure &&
47
+ outcome.status !== 'completed' &&
48
+ outcome.status !== 'aborted' &&
49
+ newOutcome.status !== 'unknown' &&
50
+ (outcome.status === 'unknown' || newOutcome.status !== 'failed')
51
+ ) {
52
+ outcome = newOutcome;
53
+ }
54
+ };
55
+
56
+ const failOutcome = (error: unknown) => {
57
+ hasFatalFailure = true;
58
+ outcome = { status: 'failed', error };
59
+ };
60
+
40
61
  const responseMessageId =
41
62
  generateMessageId != null
42
63
  ? getResponseUIMessageId({
@@ -45,37 +66,97 @@ export function toUIMessageStream<
45
66
  })
46
67
  : undefined;
47
68
 
48
- const uiMessageChunkStream = stream.pipeThrough(
69
+ const sourceReader = stream.getReader();
70
+ let sourceReaderReleased = false;
71
+ let sourceStreamCancelled = false;
72
+
73
+ const releaseSourceReader = () => {
74
+ if (!sourceReaderReleased) {
75
+ sourceReader.releaseLock();
76
+ sourceReaderReleased = true;
77
+ }
78
+ };
79
+
80
+ const sourceStream = new ReadableStream<TextStreamPart<TOOLS>>({
81
+ async pull(controller) {
82
+ try {
83
+ const { done, value } = await sourceReader.read();
84
+
85
+ if (done) {
86
+ releaseSourceReader();
87
+ if (!sourceStreamCancelled) {
88
+ controller.close();
89
+ }
90
+ } else {
91
+ controller.enqueue(value);
92
+ }
93
+ } catch (error) {
94
+ releaseSourceReader();
95
+ if (!sourceStreamCancelled) {
96
+ failOutcome(error);
97
+ controller.error(error);
98
+ }
99
+ }
100
+ },
101
+
102
+ async cancel(reason) {
103
+ sourceStreamCancelled = true;
104
+ if (sourceReaderReleased) {
105
+ return;
106
+ }
107
+
108
+ try {
109
+ await sourceReader.cancel(reason);
110
+ } finally {
111
+ releaseSourceReader();
112
+ }
113
+ },
114
+ });
115
+
116
+ const uiMessageChunkStream = sourceStream.pipeThrough(
49
117
  new TransformStream({
50
118
  transform: async (part, controller) => {
51
- const messageMetadataValue = messageMetadata?.({ part });
52
-
53
- const uiMessageChunk = toUIMessageChunk(part, {
54
- tools,
55
- sendReasoning,
56
- sendSources,
57
- sendStart,
58
- sendFinish,
59
- onError,
60
- messageMetadata: messageMetadataValue,
61
- responseMessageId,
62
- });
63
-
64
- if (uiMessageChunk != null) {
65
- controller.enqueue(uiMessageChunk);
66
- }
119
+ try {
120
+ const messageMetadataValue = messageMetadata?.({ part });
67
121
 
68
- // start and finish events already include metadata in the converted
69
- // chunk; for other part types emit a separate message-metadata chunk
70
- if (
71
- messageMetadataValue != null &&
72
- part.type !== 'start' &&
73
- part.type !== 'finish'
74
- ) {
75
- controller.enqueue({
76
- type: 'message-metadata',
122
+ const uiMessageChunk = toUIMessageChunk(part, {
123
+ tools,
124
+ sendReasoning,
125
+ sendSources,
126
+ sendStart,
127
+ sendFinish,
128
+ onError,
77
129
  messageMetadata: messageMetadataValue,
130
+ responseMessageId,
78
131
  });
132
+
133
+ if (uiMessageChunk != null) {
134
+ controller.enqueue(uiMessageChunk);
135
+ }
136
+
137
+ // start and finish events already include metadata in the converted
138
+ // chunk; for other part types emit a separate message-metadata chunk
139
+ if (
140
+ messageMetadataValue != null &&
141
+ part.type !== 'start' &&
142
+ part.type !== 'finish'
143
+ ) {
144
+ controller.enqueue({
145
+ type: 'message-metadata',
146
+ messageMetadata: messageMetadataValue,
147
+ });
148
+ }
149
+
150
+ if (part.type === 'finish') {
151
+ setSourceOutcome({ status: 'completed' });
152
+ } else if (part.type === 'abort') {
153
+ setSourceOutcome({ status: 'aborted' });
154
+ } else if (part.type === 'error') {
155
+ setSourceOutcome({ status: 'failed', error: part.error });
156
+ }
157
+ } catch (error) {
158
+ failOutcome(error);
159
+ throw error;
79
160
  }
80
161
  },
81
162
  }),
@@ -87,5 +168,6 @@ export function toUIMessageStream<
87
168
  originalMessages,
88
169
  onEnd: onEnd ?? onFinish,
89
170
  onError,
171
+ getOutcome: () => outcome,
90
172
  });
91
173
  }
@@ -85,6 +85,7 @@ export const uiMessageChunkSchema = lazySchema(() =>
85
85
  type: z.literal('tool-approval-request'),
86
86
  approvalId: z.string(),
87
87
  toolCallId: z.string(),
88
+ reason: z.string().optional(),
88
89
  isAutomatic: z.boolean().optional(),
89
90
  signature: z.string().optional(),
90
91
  }),
@@ -298,6 +299,7 @@ export type UIMessageChunk<
298
299
  type: 'tool-approval-request';
299
300
  approvalId: string;
300
301
  toolCallId: string;
302
+ reason?: string;
301
303
  isAutomatic?: boolean;
302
304
  signature?: string;
303
305
  }
@@ -1,5 +1,6 @@
1
1
  import type { FinishReason } from '../types/language-model';
2
2
  import type { UIMessage } from '../ui/ui-messages';
3
+ import type { UIMessageStreamOutcome } from './ui-message-stream-outcome';
3
4
 
4
5
  export type UIMessageStreamOnEndCallback<UI_MESSAGE extends UIMessage> =
5
6
  (event: {
@@ -19,6 +20,12 @@ export type UIMessageStreamOnEndCallback<UI_MESSAGE extends UIMessage> =
19
20
  */
20
21
  isAborted: boolean;
21
22
 
23
+ /**
24
+ * The operation-level outcome of the stream. Fatal stream-processing
25
+ * failures override outcomes declared by the stream owner.
26
+ */
27
+ outcome: UIMessageStreamOutcome;
28
+
22
29
  /**
23
30
  * The message that was sent to the client as a response
24
31
  * (including the original message if it was extended).
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The operation-level outcome of a UI message stream.
3
+ *
4
+ * This is separate from model finish reasons and individual stream chunks.
5
+ * Fatal stream-processing failures override outcomes declared by the stream
6
+ * owner.
7
+ */
8
+ export type UIMessageStreamOutcome =
9
+ | { status: 'completed' }
10
+ | { status: 'failed'; error?: unknown }
11
+ | { status: 'aborted' }
12
+ | { status: 'unknown' };
@@ -1,6 +1,7 @@
1
1
  import type { UIMessage } from '../ui';
2
2
  import type { ErrorHandler } from '../util/error-handler';
3
3
  import type { InferUIMessageChunk } from './ui-message-chunks';
4
+ import type { UIMessageStreamOutcome } from './ui-message-stream-outcome';
4
5
 
5
6
  export interface UIMessageStreamWriter<
6
7
  UI_MESSAGE extends UIMessage = UIMessage,
@@ -22,3 +23,17 @@ export interface UIMessageStreamWriter<
22
23
  */
23
24
  onError: ErrorHandler | undefined;
24
25
  }
26
+
27
+ export interface UIMessageStreamWriterWithOutcome<
28
+ UI_MESSAGE extends UIMessage = UIMessage,
29
+ > extends UIMessageStreamWriter<UI_MESSAGE> {
30
+ /**
31
+ * Declares the operation-level outcome of the composed stream.
32
+ *
33
+ * The first outcome declared through this method is retained. Fatal
34
+ * execution, merge, error-handling, or downstream processing failures
35
+ * override declared outcomes. Declaring an outcome does not write a chunk or
36
+ * close the stream.
37
+ */
38
+ setOutcome(outcome: UIMessageStreamOutcome): void;
39
+ }
@@ -25,9 +25,14 @@ export function createStitchableStream<T>(): {
25
25
  }> = [];
26
26
  let controller: ReadableStreamDefaultController<T> | null = null;
27
27
  let isClosed = false;
28
+ let isCancelled = false;
28
29
  let waitForNewStream = createResolvablePromise<void>();
29
30
 
30
31
  const terminate = () => {
32
+ if (isCancelled) {
33
+ return;
34
+ }
35
+
31
36
  isClosed = true;
32
37
  waitForNewStream.resolve();
33
38
 
@@ -40,6 +45,10 @@ export function createStitchableStream<T>(): {
40
45
  };
41
46
 
42
47
  const processPull = async () => {
48
+ if (isCancelled) {
49
+ return;
50
+ }
51
+
43
52
  // Case 1: Outer stream is closed and no more inner streams
44
53
  if (isClosed && innerStreams.length === 0) {
45
54
  controller?.close();
@@ -59,6 +68,10 @@ export function createStitchableStream<T>(): {
59
68
  try {
60
69
  const { value, done } = await currentStream.reader.read();
61
70
 
71
+ if (isCancelled) {
72
+ return;
73
+ }
74
+
62
75
  if (done) {
63
76
  // Case 3: Current inner stream is done
64
77
  innerStreams.shift(); // Remove the finished stream
@@ -75,6 +88,10 @@ export function createStitchableStream<T>(): {
75
88
  controller?.enqueue(value);
76
89
  }
77
90
  } catch (error) {
91
+ if (isCancelled) {
92
+ return;
93
+ }
94
+
78
95
  // Case 5: Current inner stream throws an error
79
96
  currentStream.onError?.(error);
80
97
  controller?.error(error);
@@ -90,12 +107,15 @@ export function createStitchableStream<T>(): {
90
107
  },
91
108
  pull: processPull,
92
109
  async cancel() {
110
+ isCancelled = true;
111
+ isClosed = true;
112
+ waitForNewStream.resolve();
113
+
93
114
  for (const { reader, onCancel } of innerStreams) {
94
115
  onCancel?.();
95
116
  await reader.cancel();
96
117
  }
97
118
  innerStreams = [];
98
- isClosed = true;
99
119
  },
100
120
  }),
101
121
  addStream: (
@@ -105,6 +125,12 @@ export function createStitchableStream<T>(): {
105
125
  onCancel?: () => void;
106
126
  },
107
127
  ) => {
128
+ if (isCancelled) {
129
+ callbacks?.onCancel?.();
130
+ void innerStream.cancel().catch(() => {});
131
+ return;
132
+ }
133
+
108
134
  if (isClosed) {
109
135
  throw new Error('Cannot add inner stream: outer stream is closed');
110
136
  }
@@ -121,6 +147,10 @@ export function createStitchableStream<T>(): {
121
147
  * finish processing and then close the outer stream.
122
148
  */
123
149
  close: () => {
150
+ if (isCancelled) {
151
+ return;
152
+ }
153
+
124
154
  isClosed = true;
125
155
  waitForNewStream.resolve();
126
156