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.
@@ -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
  }
@@ -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