ai 7.0.134 → 7.0.136

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 (39) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/index.d.ts +234 -7
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +616 -57
  5. package/dist/index.js.map +1 -1
  6. package/dist/internal/index.d.ts +6 -2
  7. package/dist/internal/index.d.ts.map +1 -1
  8. package/dist/internal/index.js +1 -1
  9. package/docs/03-agents/04-loop-control.mdx +2 -0
  10. package/docs/03-ai-sdk-core/15-tools-and-tool-calling.mdx +2 -2
  11. package/docs/03-ai-sdk-core/17-mcp-events.mdx +6 -2
  12. package/docs/03-ai-sdk-core/25-settings.mdx +4 -2
  13. package/docs/03-ai-sdk-core/30-embeddings.mdx +27 -0
  14. package/docs/03-ai-sdk-core/32-decisions.mdx +3 -2
  15. package/docs/04-ai-sdk-ui/21-transport.mdx +73 -0
  16. package/docs/07-reference/01-ai-sdk-core/01-generate-text.mdx +2 -2
  17. package/docs/07-reference/01-ai-sdk-core/02-stream-text.mdx +2 -2
  18. package/docs/07-reference/01-ai-sdk-core/05-embed.mdx +14 -0
  19. package/docs/07-reference/01-ai-sdk-core/06-embed-many.mdx +14 -0
  20. package/docs/07-reference/01-ai-sdk-core/16-tool-loop-agent.mdx +5 -5
  21. package/docs/07-reference/01-ai-sdk-core/23-create-mcp-client.mdx +2 -2
  22. package/docs/07-reference/01-ai-sdk-core/24-mcp-events.mdx +123 -5
  23. package/docs/07-reference/02-ai-sdk-ui/51-websocket-chat-transport.mdx +393 -0
  24. package/docs/07-reference/02-ai-sdk-ui/index.mdx +6 -0
  25. package/package.json +12 -12
  26. package/src/agent/default-stop-condition.ts +33 -0
  27. package/src/agent/tool-loop-agent.ts +2 -2
  28. package/src/embed/embed-events.ts +6 -0
  29. package/src/embed/embed-many.ts +18 -0
  30. package/src/embed/embed.ts +16 -0
  31. package/src/embed/validate-embedding-dimensions.ts +14 -0
  32. package/src/generate-text/stream-text.ts +31 -24
  33. package/src/prompt/request-options.ts +2 -2
  34. package/src/ui/chat-transport.ts +16 -0
  35. package/src/ui/chat.ts +123 -34
  36. package/src/ui/index.ts +13 -0
  37. package/src/ui/process-ui-message-stream.ts +6 -2
  38. package/src/ui/websocket-chat-transport.ts +987 -0
  39. package/src/util/serial-job-executor.ts +22 -5
@@ -20,6 +20,7 @@ import { notify } from '../util/notify';
20
20
  import { prepareRetries } from '../util/prepare-retries';
21
21
  import { splitArray } from '../util/split-array';
22
22
  import type { EmbedEndEvent, EmbedStartEvent } from './embed-events';
23
+ import { validateEmbeddingDimensions } from './validate-embedding-dimensions';
23
24
  import type { EmbedManyResult } from './embed-many-result';
24
25
  import { VERSION } from '../version';
25
26
 
@@ -39,6 +40,8 @@ const originalGenerateCallId = createIdGenerator({
39
40
  * @param model - The embedding model to use.
40
41
  * @param values - The values that should be embedded.
41
42
  *
43
+ * @param dimensions - Requested output dimensions. Must be a positive integer. Requires provider support.
44
+ *
42
45
  * @param maxRetries - Maximum number of retries. Set to 0 to disable retries. Default: 2.
43
46
  * @param abortSignal - An optional abort signal that can be used to cancel the call.
44
47
  * @param headers - Additional HTTP headers to be sent with the request. Only applicable for HTTP-based providers.
@@ -59,6 +62,7 @@ const originalGenerateCallId = createIdGenerator({
59
62
  export async function embedMany<RUNTIME_CONTEXT extends Context = Context>({
60
63
  model: modelArg,
61
64
  values,
65
+ dimensions,
62
66
  maxParallelCalls = Infinity,
63
67
  maxRetries: maxRetriesArg,
64
68
  abortSignal,
@@ -83,6 +87,13 @@ export async function embedMany<RUNTIME_CONTEXT extends Context = Context>({
83
87
  */
84
88
  values: Array<string>;
85
89
 
90
+ /**
91
+ * The requested number of dimensions for the output embeddings.
92
+ * Must be a positive integer. Support and allowed values depend on the model
93
+ * and provider implementation.
94
+ */
95
+ dimensions?: number;
96
+
86
97
  /**
87
98
  * Maximum number of retries per embedding model call. Set to 0 to disable retries.
88
99
  *
@@ -168,6 +179,8 @@ export async function embedMany<RUNTIME_CONTEXT extends Context = Context>({
168
179
  generateCallId?: () => string;
169
180
  };
170
181
  }): Promise<EmbedManyResult> {
182
+ validateEmbeddingDimensions(dimensions);
183
+
171
184
  const model = resolveEmbeddingModel(modelArg);
172
185
 
173
186
  const { maxRetries, retry } = prepareRetries({
@@ -200,6 +213,7 @@ export async function embedMany<RUNTIME_CONTEXT extends Context = Context>({
200
213
  provider: model.provider,
201
214
  modelId: model.modelId,
202
215
  value: values,
216
+ dimensions,
203
217
  maxRetries,
204
218
  headers: headersWithUserAgent,
205
219
  providerOptions,
@@ -243,12 +257,14 @@ export async function embedMany<RUNTIME_CONTEXT extends Context = Context>({
243
257
  provider: model.provider,
244
258
  modelId: model.modelId,
245
259
  values,
260
+ dimensions,
246
261
  },
247
262
  callbacks: [telemetryDispatcher.onEmbedStart],
248
263
  });
249
264
 
250
265
  const modelResponse = await model.doEmbed({
251
266
  values,
267
+ dimensions,
252
268
  abortSignal,
253
269
  headers: headersWithUserAgent,
254
270
  providerOptions,
@@ -371,12 +387,14 @@ export async function embedMany<RUNTIME_CONTEXT extends Context = Context>({
371
387
  provider: model.provider,
372
388
  modelId: model.modelId,
373
389
  values: chunk,
390
+ dimensions,
374
391
  },
375
392
  callbacks: [telemetryDispatcher.onEmbedStart],
376
393
  });
377
394
 
378
395
  const modelResponse = await model.doEmbed({
379
396
  values: chunk,
397
+ dimensions,
380
398
  abortSignal,
381
399
  headers: headersWithUserAgent,
382
400
  providerOptions: chunkProviderOptions,
@@ -15,6 +15,7 @@ import { notify } from '../util/notify';
15
15
  import { prepareRetries } from '../util/prepare-retries';
16
16
  import { VERSION } from '../version';
17
17
  import type { EmbedEndEvent, EmbedStartEvent } from './embed-events';
18
+ import { validateEmbeddingDimensions } from './validate-embedding-dimensions';
18
19
  import type { EmbedResult } from './embed-result';
19
20
 
20
21
  const originalGenerateCallId = createIdGenerator({
@@ -28,6 +29,8 @@ const originalGenerateCallId = createIdGenerator({
28
29
  * @param model - The embedding model to use.
29
30
  * @param value - The value that should be embedded.
30
31
  *
32
+ * @param dimensions - Requested output dimensions. Must be a positive integer. Requires provider support.
33
+ *
31
34
  * @param maxRetries - Maximum number of retries. Set to 0 to disable retries. Default: 2.
32
35
  * @param abortSignal - An optional abort signal that can be used to cancel the call.
33
36
  * @param headers - Additional HTTP headers to be sent with the request. Only applicable for HTTP-based providers.
@@ -44,6 +47,7 @@ const originalGenerateCallId = createIdGenerator({
44
47
  export async function embed<RUNTIME_CONTEXT extends Context = Context>({
45
48
  model: modelArg,
46
49
  value,
50
+ dimensions,
47
51
  providerOptions,
48
52
  maxRetries: maxRetriesArg,
49
53
  abortSignal,
@@ -67,6 +71,13 @@ export async function embed<RUNTIME_CONTEXT extends Context = Context>({
67
71
  */
68
72
  value: string;
69
73
 
74
+ /**
75
+ * The requested number of dimensions for the output embeddings.
76
+ * Must be a positive integer. Support and allowed values depend on the model
77
+ * and provider implementation.
78
+ */
79
+ dimensions?: number;
80
+
70
81
  /**
71
82
  * Maximum number of retries per embedding model call. Set to 0 to disable retries.
72
83
  *
@@ -144,6 +155,8 @@ export async function embed<RUNTIME_CONTEXT extends Context = Context>({
144
155
  generateCallId?: () => string;
145
156
  };
146
157
  }): Promise<EmbedResult> {
158
+ validateEmbeddingDimensions(dimensions);
159
+
147
160
  const model = resolveEmbeddingModel(modelArg);
148
161
 
149
162
  const { maxRetries, retry } = prepareRetries({
@@ -176,6 +189,7 @@ export async function embed<RUNTIME_CONTEXT extends Context = Context>({
176
189
  provider: model.provider,
177
190
  modelId: model.modelId,
178
191
  value,
192
+ dimensions,
179
193
  maxRetries,
180
194
  headers: headersWithUserAgent,
181
195
  providerOptions,
@@ -203,12 +217,14 @@ export async function embed<RUNTIME_CONTEXT extends Context = Context>({
203
217
  provider: model.provider,
204
218
  modelId: model.modelId,
205
219
  values: [value],
220
+ dimensions,
206
221
  },
207
222
  callbacks: [telemetryDispatcher.onEmbedStart],
208
223
  });
209
224
 
210
225
  const modelResponse = await model.doEmbed({
211
226
  values: [value],
227
+ dimensions,
212
228
  abortSignal,
213
229
  headers: headersWithUserAgent,
214
230
  providerOptions,
@@ -0,0 +1,14 @@
1
+ import { InvalidArgumentError } from '../error/invalid-argument-error';
2
+
3
+ export function validateEmbeddingDimensions(dimensions: number | undefined) {
4
+ if (
5
+ dimensions !== undefined &&
6
+ (!Number.isInteger(dimensions) || dimensions <= 0)
7
+ ) {
8
+ throw new InvalidArgumentError({
9
+ parameter: 'dimensions',
10
+ value: dimensions,
11
+ message: 'dimensions must be a positive integer',
12
+ });
13
+ }
14
+ }
@@ -2275,6 +2275,9 @@ class DefaultStreamTextResult<
2275
2275
  if (chunkTimeoutId != null) {
2276
2276
  clearTimeout(chunkTimeoutId);
2277
2277
  }
2278
+ if (abortSignal?.aborted) {
2279
+ return;
2280
+ }
2278
2281
  chunkTimeoutId = setAbortTimeout({
2279
2282
  abortController: chunkAbortController,
2280
2283
  label: 'Chunk',
@@ -2289,6 +2292,11 @@ class DefaultStreamTextResult<
2289
2292
  }
2290
2293
  }
2291
2294
 
2295
+ function clearModelOutputTimeouts() {
2296
+ clearFirstChunkTimeout();
2297
+ clearChunkTimeout();
2298
+ }
2299
+
2292
2300
  function clearStepTimeout() {
2293
2301
  if (stepTimeoutId != null) {
2294
2302
  clearTimeout(stepTimeoutId);
@@ -2297,8 +2305,7 @@ class DefaultStreamTextResult<
2297
2305
 
2298
2306
  function clearStepTimeouts() {
2299
2307
  clearStepTimeout();
2300
- clearFirstChunkTimeout();
2301
- clearChunkTimeout();
2308
+ clearModelOutputTimeouts();
2302
2309
  }
2303
2310
 
2304
2311
  function cleanupStepTimeouts() {
@@ -2509,7 +2516,6 @@ class DefaultStreamTextResult<
2509
2516
  let automaticStreamRetryCount = 0;
2510
2517
  let callbackStreamRetryCount = 0;
2511
2518
  let bufferedAttemptParts: LanguageModelStreamPart<TOOLS>[] = [];
2512
- const outputChunksHandledBeforeBuffering = new WeakSet<object>();
2513
2519
  const openTextParts = new Set<string>();
2514
2520
  const openReasoningParts = new Set<string>();
2515
2521
  let enqueueStreamRetryAttemptBoundary = false;
@@ -2561,7 +2567,16 @@ class DefaultStreamTextResult<
2561
2567
  };
2562
2568
 
2563
2569
  while (true) {
2564
- const { done, value } = await languageModelStreamReader.read();
2570
+ let result: ReadableStreamReadResult<
2571
+ LanguageModelStreamPart<TOOLS>
2572
+ >;
2573
+ try {
2574
+ result = await languageModelStreamReader.read();
2575
+ } catch (error) {
2576
+ clearModelOutputTimeouts();
2577
+ throw error;
2578
+ }
2579
+ const { done, value } = result;
2565
2580
 
2566
2581
  if (enqueueStreamRetryAttemptBoundary) {
2567
2582
  controller.enqueue(
@@ -2576,6 +2591,7 @@ class DefaultStreamTextResult<
2576
2591
  }
2577
2592
 
2578
2593
  if (done) {
2594
+ clearModelOutputTimeouts();
2579
2595
  flushBufferedAttemptParts();
2580
2596
  controller.close();
2581
2597
  return;
@@ -2592,22 +2608,24 @@ class DefaultStreamTextResult<
2592
2608
  value.type === 'tool-error';
2593
2609
 
2594
2610
  if (value.type === 'model-call-end') {
2611
+ clearModelOutputTimeouts();
2595
2612
  flushBufferedAttemptParts();
2596
2613
  enqueueAttemptPart(value);
2597
2614
  return;
2598
2615
  }
2599
2616
 
2617
+ // Observe model output before buffering and tool processing.
2618
+ // Replaying buffered parts must not re-arm finished timers.
2619
+ if (isOutputChunk(value)) {
2620
+ clearFirstChunkTimeout();
2621
+ resetChunkTimeout();
2622
+ }
2623
+
2600
2624
  if (
2601
2625
  shouldBufferToolParts &&
2602
2626
  value.type !== 'error' &&
2603
2627
  (isToolPart || bufferedAttemptParts.length > 0)
2604
2628
  ) {
2605
- if (isOutputChunk(value)) {
2606
- clearFirstChunkTimeout();
2607
- resetChunkTimeout();
2608
- outputChunksHandledBeforeBuffering.add(value);
2609
- }
2610
-
2611
2629
  bufferedAttemptParts.push(value);
2612
2630
  continue;
2613
2631
  }
@@ -2617,6 +2635,7 @@ class DefaultStreamTextResult<
2617
2635
  return;
2618
2636
  }
2619
2637
 
2638
+ clearModelOutputTimeouts();
2620
2639
  await notify({
2621
2640
  event: { chunk: value },
2622
2641
  callbacks: onChunk,
@@ -2677,10 +2696,12 @@ class DefaultStreamTextResult<
2677
2696
  response = retryLanguageModelCall.response;
2678
2697
  languageModelStreamReader =
2679
2698
  retryLanguageModelCall.stream.getReader();
2699
+ startFirstChunkTimeout();
2680
2700
  enqueueStreamRetryAttemptBoundary = true;
2681
2701
  }
2682
2702
  },
2683
2703
  cancel(reason) {
2704
+ clearModelOutputTimeouts();
2684
2705
  return languageModelStreamReader.cancel(reason);
2685
2706
  },
2686
2707
  });
@@ -2857,21 +2878,7 @@ class DefaultStreamTextResult<
2857
2878
  const chunkType = chunk.type;
2858
2879
 
2859
2880
  if (isOutputChunk(chunk)) {
2860
- const timeoutHandledBeforeBuffering =
2861
- outputChunksHandledBeforeBuffering.has(chunk);
2862
-
2863
- if (
2864
- !hasReceivedOutputChunk &&
2865
- !timeoutHandledBeforeBuffering
2866
- ) {
2867
- // Clear before forwarding the first output so a timeout
2868
- // cannot race with already-visible generated content.
2869
- clearFirstChunkTimeout();
2870
- }
2871
2881
  hasReceivedOutputChunk = true;
2872
- if (!timeoutHandledBeforeBuffering) {
2873
- resetChunkTimeout();
2874
- }
2875
2882
  }
2876
2883
 
2877
2884
  switch (chunkType) {
@@ -4,9 +4,9 @@ import type { ToolSet } from '@ai-sdk/provider-utils';
4
4
  * Timeout configuration for API calls. Can be specified as:
5
5
  * - A number representing milliseconds
6
6
  * - An object with `totalMs` property for the total timeout in milliseconds
7
- * - An object with `stepMs` property for the timeout of each step in milliseconds
7
+ * - An object with `stepMs` property for the timeout of each complete step, including model generation, retries, and local tool execution, in milliseconds
8
8
  * - An object with `firstChunkMs` property for the timeout until the first content chunk of each step (streaming only)
9
- * - An object with `chunkMs` property for the timeout between content chunks (streaming only)
9
+ * - An object with `chunkMs` property for the timeout between meaningful model output chunks, starting with the first output and ending when the model response ends (streaming only)
10
10
  * - An object with `toolMs` property for the default timeout for all tool executions
11
11
  * - An object with `tools` property for per-tool timeout overrides using `{toolName}Ms` keys
12
12
  */
@@ -13,6 +13,22 @@ import type { UIMessage } from './ui-messages';
13
13
  * @template UI_MESSAGE - The UI message type extending UIMessage
14
14
  */
15
15
  export interface ChatTransport<UI_MESSAGE extends UIMessage> {
16
+ /**
17
+ * Whether reconnecting returns the complete response from its beginning.
18
+ * Complete replays must reset the streaming message state before processing
19
+ * the first start chunk.
20
+ */
21
+ readonly resumeStreamIsReplay?: boolean;
22
+
23
+ /**
24
+ * Releases persistent resources owned by the transport.
25
+ *
26
+ * The caller that creates a transport owns its lifecycle and should call
27
+ * this method when no consumers remain. `AbstractChat.dispose()` calls it
28
+ * for directly managed chat instances.
29
+ */
30
+ close?: () => void;
31
+
16
32
  /**
17
33
  * Sends messages to the chat API endpoint and returns a streaming response.
18
34
  *
package/src/ui/chat.ts CHANGED
@@ -8,6 +8,7 @@ import { InvalidArgumentError } from '../error/invalid-argument-error';
8
8
  import type { FinishReason } from '../types/language-model';
9
9
  import type { UIMessageChunk } from '../ui-message-stream/ui-message-chunks';
10
10
  import { consumeStream } from '../util/consume-stream';
11
+ import { createResolvablePromise } from '../util/create-resolvable-promise';
11
12
  import { SerialJobExecutor } from '../util/serial-job-executor';
12
13
  import type { ChatTransport } from './chat-transport';
13
14
  import { convertFileListToFileUIParts } from './convert-file-list-to-file-ui-parts';
@@ -135,12 +136,19 @@ export type ChatStatus = 'submitted' | 'streaming' | 'ready' | 'error';
135
136
  type ActiveResponse<UI_MESSAGE extends UIMessage> = {
136
137
  state: StreamingUIMessageState<UI_MESSAGE>;
137
138
  abortController: AbortController;
139
+ completionPromise: Promise<void>;
138
140
  };
139
141
 
140
142
  type ActiveResumeRequest = {
141
143
  abortController: AbortController;
144
+ completionPromise: Promise<void>;
142
145
  };
143
146
 
147
+ type MakeRequestOptions = {
148
+ trigger: 'submit-message' | 'resume-stream' | 'regenerate-message';
149
+ messageId?: string;
150
+ } & ChatRequestOptions;
151
+
144
152
  export interface ChatState<UI_MESSAGE extends UIMessage> {
145
153
  status: ChatStatus;
146
154
 
@@ -267,6 +275,8 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
267
275
  | StreamingUIMessageState<UI_MESSAGE>
268
276
  | undefined = undefined;
269
277
  private jobExecutor = new SerialJobExecutor();
278
+ private activeStopCount = 0;
279
+ private stopGeneration = 0;
270
280
 
271
281
  constructor({
272
282
  generateId = generateIdFunc,
@@ -591,6 +601,7 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
591
601
 
592
602
  // automatically send the message if the sendAutomaticallyWhen function returns true
593
603
  if (
604
+ messageIndex !== -1 &&
594
605
  this.status !== 'streaming' &&
595
606
  this.status !== 'submitted' &&
596
607
  this.sendAutomaticallyWhen
@@ -599,16 +610,18 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
599
610
 
600
611
  if (shouldSend) {
601
612
  // no await to avoid deadlocking
602
- const messageId =
603
- messageIndex === -1
604
- ? this.lastMessage?.id
605
- : messages[messageIndex].id;
606
-
607
- this.makeRequestForToolApproval({
608
- messageId,
609
- messageIndex,
610
- ...options,
611
- });
613
+ void this.runAutomaticRequest(() => {
614
+ const messageId =
615
+ messageIndex === -1
616
+ ? this.lastMessage?.id
617
+ : messages[messageIndex].id;
618
+
619
+ return this.makeRequestForToolApproval({
620
+ messageId,
621
+ messageIndex,
622
+ ...options,
623
+ });
624
+ }, shouldSend);
612
625
  }
613
626
  }
614
627
  });
@@ -684,11 +697,15 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
684
697
 
685
698
  if (shouldSend) {
686
699
  // no await to avoid deadlocking
687
- this.makeRequest({
688
- trigger: 'submit-message',
689
- messageId: this.lastMessage?.id,
690
- ...options,
691
- });
700
+ void this.runAutomaticRequest(
701
+ () =>
702
+ this.makeRequest({
703
+ trigger: 'submit-message',
704
+ messageId: this.lastMessage?.id,
705
+ ...options,
706
+ }),
707
+ shouldSend,
708
+ );
692
709
  }
693
710
  }
694
711
  });
@@ -697,14 +714,42 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
697
714
  addToolResult = this.addToolOutput;
698
715
 
699
716
  /**
700
- * Abort the current request immediately, keep the generated tokens if any.
717
+ * Abort the current request, keep the generated tokens if any, and wait for
718
+ * the request pipeline to finish.
701
719
  */
702
720
  stop = async () => {
703
- for (const controller of this.pendingMessagePreparations) {
704
- controller.abort();
721
+ this.activeStopCount++;
722
+ this.stopGeneration++;
723
+
724
+ try {
725
+ const activeResumeRequest = this.activeResumeRequest;
726
+ const activeResponse = this.activeResponse;
727
+
728
+ for (const controller of this.pendingMessagePreparations) {
729
+ controller.abort();
730
+ }
731
+ activeResumeRequest?.abortController.abort();
732
+ activeResponse?.abortController.abort();
733
+
734
+ await Promise.all([
735
+ activeResumeRequest?.completionPromise,
736
+ activeResponse?.completionPromise,
737
+ ]);
738
+
739
+ // Stream cancellation can complete while a processing job is still
740
+ // blocked in onToolCall. Drain that job and any message update it queued.
741
+ await this.jobExecutor.waitForIdle();
742
+ } finally {
743
+ this.activeStopCount--;
705
744
  }
706
- this.activeResumeRequest?.abortController.abort();
707
- this.activeResponse?.abortController.abort();
745
+ };
746
+
747
+ /**
748
+ * Stops active work and releases resources owned by the chat transport.
749
+ */
750
+ dispose = async (): Promise<void> => {
751
+ await this.stop();
752
+ this.transport.close?.();
708
753
  };
709
754
 
710
755
  private async shouldSendAutomatically(): Promise<boolean> {
@@ -722,6 +767,30 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
722
767
  return result as boolean;
723
768
  }
724
769
 
770
+ private async runAutomaticRequest(
771
+ request: () => Promise<void>,
772
+ shouldSend?: boolean,
773
+ ): Promise<void> {
774
+ const stopGeneration = this.stopGeneration;
775
+ const startedWhileStopping = this.activeStopCount > 0;
776
+
777
+ if (!(shouldSend ?? (await this.shouldSendAutomatically()))) {
778
+ return;
779
+ }
780
+
781
+ // A stop invalidates automatic requests that were pending when it started,
782
+ // including requests queued by callback-driven tool updates.
783
+ if (
784
+ startedWhileStopping ||
785
+ stopGeneration !== this.stopGeneration ||
786
+ this.activeStopCount > 0
787
+ ) {
788
+ return;
789
+ }
790
+
791
+ await request();
792
+ }
793
+
725
794
  private async makeRequestForToolApproval({
726
795
  messageId,
727
796
  messageIndex,
@@ -752,23 +821,38 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
752
821
  }
753
822
  }
754
823
 
755
- private async makeRequest({
824
+ private async makeRequest(options: MakeRequestOptions) {
825
+ const completion = createResolvablePromise<void>();
826
+
827
+ try {
828
+ await this.makeRequestImpl({
829
+ ...options,
830
+ completionPromise: completion.promise,
831
+ });
832
+ } finally {
833
+ completion.resolve();
834
+ }
835
+ }
836
+
837
+ private async makeRequestImpl({
756
838
  trigger,
757
839
  metadata,
758
840
  headers,
759
841
  body,
760
842
  messageId,
761
- }: {
762
- trigger: 'submit-message' | 'resume-stream' | 'regenerate-message';
763
- messageId?: string;
764
- } & ChatRequestOptions) {
843
+ completionPromise,
844
+ }: MakeRequestOptions & {
845
+ completionPromise: Promise<void>;
846
+ }) {
765
847
  if (trigger !== 'resume-stream') {
766
848
  this.resumableStreamState = undefined;
767
849
  }
768
850
 
769
851
  const abortController = new AbortController();
770
852
  const activeResumeRequest =
771
- trigger === 'resume-stream' ? { abortController } : undefined;
853
+ trigger === 'resume-stream'
854
+ ? { abortController, completionPromise }
855
+ : undefined;
772
856
 
773
857
  if (activeResumeRequest) {
774
858
  this.activeResumeRequest?.abortController.abort();
@@ -893,6 +977,7 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
893
977
  messageId: this.generateId(),
894
978
  }),
895
979
  abortController,
980
+ completionPromise,
896
981
  } as ActiveResponse<UI_MESSAGE>;
897
982
 
898
983
  activeResponse = response;
@@ -964,6 +1049,8 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
964
1049
  stream: processUIMessageStream({
965
1050
  stream,
966
1051
  resetStateOnMessageIdChange: trigger === 'resume-stream',
1052
+ resetStateOnFirstMessageStart:
1053
+ trigger === 'resume-stream' && this.transport.resumeStreamIsReplay,
967
1054
  onToolCall: this.onToolCall,
968
1055
  onData: this.onData,
969
1056
  messageMetadataSchema: this.messageMetadataSchema,
@@ -1059,14 +1146,16 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
1059
1146
  }
1060
1147
 
1061
1148
  // automatically send the message if the sendAutomaticallyWhen function returns true
1062
- if (!isError && (await this.shouldSendAutomatically())) {
1063
- await this.makeRequest({
1064
- trigger: 'submit-message',
1065
- messageId: this.lastMessage?.id,
1066
- metadata,
1067
- headers,
1068
- body,
1069
- });
1149
+ if (!isAbort && !isError) {
1150
+ await this.runAutomaticRequest(() =>
1151
+ this.makeRequest({
1152
+ trigger: 'submit-message',
1153
+ messageId: this.lastMessage?.id,
1154
+ metadata,
1155
+ headers,
1156
+ body,
1157
+ }),
1158
+ );
1070
1159
  }
1071
1160
  }
1072
1161
  }
package/src/ui/index.ts CHANGED
@@ -32,6 +32,19 @@ export {
32
32
  export { lastAssistantMessageIsCompleteWithApprovalResponses } from './last-assistant-message-is-complete-with-approval-responses';
33
33
  export { lastAssistantMessageIsCompleteWithToolCalls } from './last-assistant-message-is-complete-with-tool-calls';
34
34
  export { TextStreamChatTransport } from './text-stream-chat-transport';
35
+ export {
36
+ safeValidateWebSocketChatTransportRequest,
37
+ WebSocketChatTransport,
38
+ type PrepareWebSocketChatTransportReconnectToStreamRequest,
39
+ type PrepareWebSocketChatTransportSendMessagesRequest,
40
+ type SafeValidateWebSocketChatTransportRequestResult,
41
+ type WebSocketChatTransportAbortRequest,
42
+ type WebSocketChatTransportInitOptions,
43
+ type WebSocketChatTransportRequest,
44
+ type WebSocketChatTransportResponse,
45
+ type WebSocketChatTransportResumeRequest,
46
+ type WebSocketChatTransportSendRequest,
47
+ } from './websocket-chat-transport';
35
48
  export {
36
49
  getStaticToolName,
37
50
  getToolName,
@@ -121,6 +121,7 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
121
121
  onToolCall,
122
122
  onData,
123
123
  resetStateOnMessageIdChange = false,
124
+ resetStateOnFirstMessageStart = false,
124
125
  }: {
125
126
  // input stream is not fully typed yet:
126
127
  stream: ReadableStream<UIMessageChunk>;
@@ -139,6 +140,8 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
139
140
  onError: ErrorHandler;
140
141
  // During resume, a different message ID identifies a separate response.
141
142
  resetStateOnMessageIdChange?: boolean;
143
+ // A transport can replay a response from the beginning using the same ID.
144
+ resetStateOnFirstMessageStart?: boolean;
142
145
  }): ReadableStream<InferUIMessageChunk<UI_MESSAGE>> {
143
146
  return stream.pipeThrough(
144
147
  new TransformStream<UIMessageChunk, InferUIMessageChunk<UI_MESSAGE>>({
@@ -957,9 +960,10 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
957
960
 
958
961
  case 'start': {
959
962
  if (
960
- resetStateOnMessageIdChange &&
961
963
  chunk.messageId != null &&
962
- chunk.messageId !== state.message.id
964
+ (resetStateOnFirstMessageStart ||
965
+ (resetStateOnMessageIdChange &&
966
+ chunk.messageId !== state.message.id))
963
967
  ) {
964
968
  // Start the separate response with empty parts and metadata.
965
969
  // Reset the active-part maps too, so earlier chunks stay with