ai 6.0.301 → 6.0.303

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.
package/src/global.ts CHANGED
@@ -2,8 +2,20 @@ import type { ProviderV3 } from '@ai-sdk/provider';
2
2
  import type { LogWarningsFunction } from './logger/log-warnings';
3
3
  import type { TelemetryIntegration } from './telemetry/telemetry-integration';
4
4
 
5
- // add AI SDK default provider to the globalThis object
5
+ // Each installed copy contributes its own types without conflicting with other
6
+ // versions. This is a type-only key; no symbol is created at runtime.
7
+ declare const aiSdkGlobalsKey: unique symbol;
8
+
6
9
  declare global {
10
+ interface AISDKGlobalTypes {
11
+ [aiSdkGlobalsKey]: {
12
+ provider: ProviderV3;
13
+ logWarnings: LogWarningsFunction;
14
+ telemetry: TelemetryIntegration;
15
+ };
16
+ }
17
+
18
+ // Keep these registry-based declarations identical across major versions.
7
19
  /**
8
20
  * The default provider to use for the AI SDK.
9
21
  * String model ids are resolved to the default provider and model id.
@@ -12,7 +24,9 @@ declare global {
12
24
  *
13
25
  * @see https://ai-sdk.dev/docs/ai-sdk-core/provider-management#global-provider-configuration
14
26
  */
15
- var AI_SDK_DEFAULT_PROVIDER: ProviderV3 | undefined;
27
+ var AI_SDK_DEFAULT_PROVIDER:
28
+ | AISDKGlobalTypes[keyof AISDKGlobalTypes]['provider']
29
+ | undefined;
16
30
 
17
31
  /**
18
32
  * The warning logger to use for the AI SDK.
@@ -21,7 +35,10 @@ declare global {
21
35
  *
22
36
  * If set to false, no warnings are logged.
23
37
  */
24
- var AI_SDK_LOG_WARNINGS: LogWarningsFunction | undefined | false;
38
+ var AI_SDK_LOG_WARNINGS:
39
+ | AISDKGlobalTypes[keyof AISDKGlobalTypes]['logWarnings']
40
+ | undefined
41
+ | false;
25
42
 
26
43
  /**
27
44
  * Globally registered telemetry integrations for the AI SDK.
@@ -32,5 +49,7 @@ declare global {
32
49
  * Prefer using `registerTelemetryIntegration()` from `'ai'` instead of
33
50
  * assigning this directly.
34
51
  */
35
- var AI_SDK_TELEMETRY_INTEGRATIONS: TelemetryIntegration[] | undefined;
52
+ var AI_SDK_TELEMETRY_INTEGRATIONS:
53
+ | Array<AISDKGlobalTypes[keyof AISDKGlobalTypes]['telemetry']>
54
+ | undefined;
36
55
  }
@@ -99,5 +99,9 @@ function convertV2UsageToV3(usage: LanguageModelV2Usage): LanguageModelV3Usage {
99
99
  text: undefined,
100
100
  reasoning: usage.reasoningTokens,
101
101
  },
102
+ raw:
103
+ usage.totalTokens == null
104
+ ? undefined
105
+ : { totalTokens: usage.totalTokens },
102
106
  };
103
107
  }
@@ -135,6 +135,12 @@ export function addLanguageModelUsage(
135
135
  usage1: LanguageModelUsage,
136
136
  usage2: LanguageModelUsage,
137
137
  ): LanguageModelUsage {
138
+ const raw = isNullLanguageModelUsage(usage1)
139
+ ? usage2.raw
140
+ : isNullLanguageModelUsage(usage2)
141
+ ? usage1.raw
142
+ : undefined;
143
+
138
144
  return {
139
145
  inputTokens: addTokenCounts(usage1.inputTokens, usage2.inputTokens),
140
146
  inputTokenDetails: {
@@ -171,9 +177,24 @@ export function addLanguageModelUsage(
171
177
  usage1.cachedInputTokens,
172
178
  usage2.cachedInputTokens,
173
179
  ),
180
+ ...(raw == null ? {} : { raw }),
174
181
  };
175
182
  }
176
183
 
184
+ function isNullLanguageModelUsage(usage: LanguageModelUsage): boolean {
185
+ return (
186
+ usage.inputTokens == null &&
187
+ usage.inputTokenDetails?.noCacheTokens == null &&
188
+ usage.inputTokenDetails?.cacheReadTokens == null &&
189
+ usage.inputTokenDetails?.cacheWriteTokens == null &&
190
+ usage.outputTokens == null &&
191
+ usage.outputTokenDetails?.textTokens == null &&
192
+ usage.outputTokenDetails?.reasoningTokens == null &&
193
+ usage.totalTokens == null &&
194
+ usage.raw == null
195
+ );
196
+ }
197
+
177
198
  function addTokenCounts(
178
199
  tokenCount1: number | undefined,
179
200
  tokenCount2: number | undefined,
package/src/ui/chat.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  import type { FinishReason } from '../types/language-model';
8
8
  import type { UIMessageChunk } from '../ui-message-stream/ui-message-chunks';
9
9
  import { consumeStream } from '../util/consume-stream';
10
+ import { createResolvablePromise } from '../util/create-resolvable-promise';
10
11
  import { SerialJobExecutor } from '../util/serial-job-executor';
11
12
  import type { ChatTransport } from './chat-transport';
12
13
  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
 
@@ -266,6 +274,8 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
266
274
  | StreamingUIMessageState<UI_MESSAGE>
267
275
  | undefined = undefined;
268
276
  private jobExecutor = new SerialJobExecutor();
277
+ private activeStopCount = 0;
278
+ private stopGeneration = 0;
269
279
 
270
280
  constructor({
271
281
  generateId = generateIdFunc,
@@ -314,7 +324,12 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
314
324
  status: ChatStatus;
315
325
  error?: Error;
316
326
  }) {
317
- if (this.status === status) return;
327
+ if (this.status === status) {
328
+ if (this.error !== error) {
329
+ this.state.error = error;
330
+ }
331
+ return;
332
+ }
318
333
 
319
334
  this.state.status = status;
320
335
  this.state.error = error;
@@ -567,21 +582,23 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
567
582
  this.status !== 'submitted' &&
568
583
  this.sendAutomaticallyWhen
569
584
  ) {
570
- this.shouldSendAutomatically().then(shouldSend => {
571
- if (shouldSend) {
572
- // no await to avoid deadlocking
585
+ const shouldSend = await this.shouldSendAutomatically();
586
+
587
+ if (shouldSend) {
588
+ // no await to avoid deadlocking
589
+ void this.runAutomaticRequest(() => {
573
590
  const messageId =
574
591
  messageIndex === -1
575
592
  ? this.lastMessage?.id
576
593
  : messages[messageIndex].id;
577
594
 
578
- this.makeRequestForToolApproval({
595
+ return this.makeRequestForToolApproval({
579
596
  messageId,
580
597
  messageIndex,
581
598
  ...options,
582
599
  });
583
- }
584
- });
600
+ }, shouldSend);
601
+ }
585
602
  }
586
603
  });
587
604
 
@@ -621,16 +638,20 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
621
638
  this.status !== 'submitted' &&
622
639
  this.sendAutomaticallyWhen
623
640
  ) {
624
- this.shouldSendAutomatically().then(shouldSend => {
625
- if (shouldSend) {
626
- // no await to avoid deadlocking
627
- this.makeRequest({
628
- trigger: 'submit-message',
629
- messageId: this.lastMessage?.id,
630
- ...options,
631
- });
632
- }
633
- });
641
+ const shouldSend = await this.shouldSendAutomatically();
642
+
643
+ if (shouldSend) {
644
+ // no await to avoid deadlocking
645
+ void this.runAutomaticRequest(
646
+ () =>
647
+ this.makeRequest({
648
+ trigger: 'submit-message',
649
+ messageId: this.lastMessage?.id,
650
+ ...options,
651
+ }),
652
+ shouldSend,
653
+ );
654
+ }
634
655
  }
635
656
  });
636
657
 
@@ -638,11 +659,31 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
638
659
  addToolResult = this.addToolOutput;
639
660
 
640
661
  /**
641
- * Abort the current request immediately, keep the generated tokens if any.
662
+ * Abort the current request, keep the generated tokens if any, and wait for
663
+ * the request pipeline to finish.
642
664
  */
643
665
  stop = async () => {
644
- this.activeResumeRequest?.abortController.abort();
645
- this.activeResponse?.abortController.abort();
666
+ this.activeStopCount++;
667
+ this.stopGeneration++;
668
+
669
+ try {
670
+ const activeResumeRequest = this.activeResumeRequest;
671
+ const activeResponse = this.activeResponse;
672
+
673
+ activeResumeRequest?.abortController.abort();
674
+ activeResponse?.abortController.abort();
675
+
676
+ await Promise.all([
677
+ activeResumeRequest?.completionPromise,
678
+ activeResponse?.completionPromise,
679
+ ]);
680
+
681
+ // Stream cancellation can complete while a processing job is still
682
+ // blocked in onToolCall. Drain that job and any message update it queued.
683
+ await this.jobExecutor.waitForIdle();
684
+ } finally {
685
+ this.activeStopCount--;
686
+ }
646
687
  };
647
688
 
648
689
  private async shouldSendAutomatically(): Promise<boolean> {
@@ -660,6 +701,30 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
660
701
  return result as boolean;
661
702
  }
662
703
 
704
+ private async runAutomaticRequest(
705
+ request: () => Promise<void>,
706
+ shouldSend?: boolean,
707
+ ): Promise<void> {
708
+ const stopGeneration = this.stopGeneration;
709
+ const startedWhileStopping = this.activeStopCount > 0;
710
+
711
+ if (!(shouldSend ?? (await this.shouldSendAutomatically()))) {
712
+ return;
713
+ }
714
+
715
+ // A stop invalidates automatic requests that were pending when it started,
716
+ // including requests queued by callback-driven tool updates.
717
+ if (
718
+ startedWhileStopping ||
719
+ stopGeneration !== this.stopGeneration ||
720
+ this.activeStopCount > 0
721
+ ) {
722
+ return;
723
+ }
724
+
725
+ await request();
726
+ }
727
+
663
728
  private async makeRequestForToolApproval({
664
729
  messageId,
665
730
  messageIndex,
@@ -690,23 +755,38 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
690
755
  }
691
756
  }
692
757
 
693
- private async makeRequest({
758
+ private async makeRequest(options: MakeRequestOptions) {
759
+ const completion = createResolvablePromise<void>();
760
+
761
+ try {
762
+ await this.makeRequestImpl({
763
+ ...options,
764
+ completionPromise: completion.promise,
765
+ });
766
+ } finally {
767
+ completion.resolve();
768
+ }
769
+ }
770
+
771
+ private async makeRequestImpl({
694
772
  trigger,
695
773
  metadata,
696
774
  headers,
697
775
  body,
698
776
  messageId,
699
- }: {
700
- trigger: 'submit-message' | 'resume-stream' | 'regenerate-message';
701
- messageId?: string;
702
- } & ChatRequestOptions) {
777
+ completionPromise,
778
+ }: MakeRequestOptions & {
779
+ completionPromise: Promise<void>;
780
+ }) {
703
781
  if (trigger !== 'resume-stream') {
704
782
  this.resumableStreamState = undefined;
705
783
  }
706
784
 
707
785
  const abortController = new AbortController();
708
786
  const activeResumeRequest =
709
- trigger === 'resume-stream' ? { abortController } : undefined;
787
+ trigger === 'resume-stream'
788
+ ? { abortController, completionPromise }
789
+ : undefined;
710
790
 
711
791
  if (activeResumeRequest) {
712
792
  this.activeResumeRequest?.abortController.abort();
@@ -825,6 +905,7 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
825
905
  messageId: this.generateId(),
826
906
  }),
827
907
  abortController,
908
+ completionPromise,
828
909
  } as ActiveResponse<UI_MESSAGE>;
829
910
 
830
911
  activeResponse = response;
@@ -987,14 +1068,16 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
987
1068
  }
988
1069
 
989
1070
  // automatically send the message if the sendAutomaticallyWhen function returns true
990
- if (!isError && (await this.shouldSendAutomatically())) {
991
- await this.makeRequest({
992
- trigger: 'submit-message',
993
- messageId: this.lastMessage?.id,
994
- metadata,
995
- headers,
996
- body,
997
- });
1071
+ if (!isAbort && !isError) {
1072
+ await this.runAutomaticRequest(() =>
1073
+ this.makeRequest({
1074
+ trigger: 'submit-message',
1075
+ messageId: this.lastMessage?.id,
1076
+ metadata,
1077
+ headers,
1078
+ body,
1079
+ }),
1080
+ );
998
1081
  }
999
1082
  }
1000
1083
  }
@@ -10,6 +10,7 @@ import {
10
10
  import type { ToolSet } from '../generate-text/tool-set';
11
11
  import { createToolModelOutput } from '../prompt/create-tool-model-output';
12
12
  import { MessageConversionError } from '../prompt/message-conversion-error';
13
+ import { getOwn } from '../util/get-own';
13
14
  import {
14
15
  getToolName,
15
16
  isDataUIPart,
@@ -27,6 +28,7 @@ import {
27
28
  type ToolUIPart,
28
29
  type UIMessage,
29
30
  } from './ui-messages';
31
+ import { isToolPartFromUnavailableTool } from './unavailable-tool';
30
32
 
31
33
  /**
32
34
  * Converts an array of UI messages from useChat into an array of ModelMessages that can be used
@@ -65,6 +67,39 @@ export async function convertToModelMessages<UI_MESSAGE extends UIMessage>(
65
67
  }));
66
68
  }
67
69
 
70
+ async function createModelOutput({
71
+ toolPart,
72
+ toolName,
73
+ output,
74
+ errorMode,
75
+ }: {
76
+ toolPart: ToolUIPart<InferUIMessageTools<UI_MESSAGE>> | DynamicToolUIPart;
77
+ toolName: string;
78
+ output: unknown;
79
+ errorMode: 'none' | 'text' | 'json';
80
+ }) {
81
+ const tool = getOwn(options?.tools, toolName);
82
+
83
+ if (
84
+ errorMode === 'none' &&
85
+ tool == null &&
86
+ isToolPartFromUnavailableTool(toolPart)
87
+ ) {
88
+ return {
89
+ type: 'text' as const,
90
+ value: 'Tool output omitted because the tool is no longer available.',
91
+ };
92
+ }
93
+
94
+ return createToolModelOutput({
95
+ toolCallId: toolPart.toolCallId,
96
+ input: toolPart.input,
97
+ output,
98
+ tool,
99
+ errorMode,
100
+ });
101
+ }
102
+
68
103
  for (const message of messages) {
69
104
  switch (message.role) {
70
105
  case 'system': {
@@ -231,14 +266,13 @@ export async function convertToModelMessages<UI_MESSAGE extends UIMessage>(
231
266
  type: 'tool-result',
232
267
  toolCallId: part.toolCallId,
233
268
  toolName,
234
- output: await createToolModelOutput({
235
- toolCallId: part.toolCallId,
236
- input: part.input,
269
+ output: await createModelOutput({
270
+ toolPart: part,
271
+ toolName,
237
272
  output:
238
273
  part.state === 'output-error'
239
274
  ? part.errorText
240
275
  : part.output,
241
- tool: options?.tools?.[toolName],
242
276
  errorMode:
243
277
  part.state === 'output-error' ? 'json' : 'none',
244
278
  }),
@@ -331,14 +365,13 @@ export async function convertToModelMessages<UI_MESSAGE extends UIMessage>(
331
365
  type: 'tool-result',
332
366
  toolCallId: toolPart.toolCallId,
333
367
  toolName,
334
- output: await createToolModelOutput({
335
- toolCallId: toolPart.toolCallId,
336
- input: toolPart.input,
368
+ output: await createModelOutput({
369
+ toolPart,
370
+ toolName,
337
371
  output:
338
372
  toolPart.state === 'output-error'
339
373
  ? toolPart.errorText
340
374
  : toolPart.output,
341
- tool: options?.tools?.[toolName],
342
375
  errorMode:
343
376
  toolPart.state === 'output-error' ? 'text' : 'none',
344
377
  }),
@@ -11,10 +11,12 @@ import type { UIMessage } from './ui-messages';
11
11
 
12
12
  function appendPathToUrl(url: string, path: string): string {
13
13
  const queryOrFragmentStart = url.search(/[?#]/);
14
+ const urlPath =
15
+ queryOrFragmentStart === -1 ? url : url.slice(0, queryOrFragmentStart);
16
+ const suffix =
17
+ queryOrFragmentStart === -1 ? '' : url.slice(queryOrFragmentStart);
14
18
 
15
- return queryOrFragmentStart === -1
16
- ? `${url}${path}`
17
- : `${url.slice(0, queryOrFragmentStart)}${path}${url.slice(queryOrFragmentStart)}`;
19
+ return `${urlPath.endsWith('/') && path.startsWith('/') ? urlPath.slice(0, -1) : urlPath}${path}${suffix}`;
18
20
  }
19
21
 
20
22
  export type PrepareSendMessagesRequest<UI_MESSAGE extends UIMessage> = (
@@ -120,6 +120,7 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
120
120
  onError,
121
121
  onToolCall,
122
122
  onData,
123
+ resetStateOnMessageIdChange = false,
123
124
  }: {
124
125
  // input stream is not fully typed yet:
125
126
  stream: ReadableStream<UIMessageChunk>;
@@ -136,6 +137,8 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
136
137
  }) => Promise<void>,
137
138
  ) => Promise<void>;
138
139
  onError: ErrorHandler;
140
+ // During resume, a different message ID identifies a separate response.
141
+ resetStateOnMessageIdChange?: boolean;
139
142
  }): ReadableStream<InferUIMessageChunk<UI_MESSAGE>> {
140
143
  return stream.pipeThrough(
141
144
  new TransformStream<UIMessageChunk, InferUIMessageChunk<UI_MESSAGE>>({
@@ -878,6 +881,24 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
878
881
  }
879
882
 
880
883
  case 'start': {
884
+ if (
885
+ resetStateOnMessageIdChange &&
886
+ chunk.messageId != null &&
887
+ chunk.messageId !== state.message.id
888
+ ) {
889
+ // Start the separate response with empty parts and metadata.
890
+ // Reset the active-part maps too, so earlier chunks stay with
891
+ // the previous response. Keep the state object for its callers.
892
+ Object.assign(
893
+ state,
894
+ createStreamingUIMessageState({
895
+ lastMessage: undefined,
896
+ messageId: chunk.messageId,
897
+ }),
898
+ { finishReason: undefined },
899
+ );
900
+ }
901
+
881
902
  if (chunk.messageId != null) {
882
903
  state.message.id = chunk.messageId;
883
904
  }
@@ -348,6 +348,14 @@ export type ToolUIPart<TOOLS extends UITools = UITools> = ValueOf<{
348
348
  export type DynamicToolUIPart = {
349
349
  type: 'dynamic-tool';
350
350
 
351
+ /**
352
+ * `false` when a static tool part was normalized to a dynamic tool part
353
+ * because its current tool schema was unavailable or incompatible.
354
+ *
355
+ * This preserves its static origin across persistence boundaries.
356
+ */
357
+ dynamic?: false;
358
+
351
359
  /**
352
360
  * Name of the tool that is being called.
353
361
  */
@@ -0,0 +1,17 @@
1
+ import type { DynamicToolUIPart, ToolUIPart } from './ui-messages';
2
+
3
+ export function markToolPartAsUnavailable(
4
+ part: DynamicToolUIPart,
5
+ ): DynamicToolUIPart {
6
+ // Preserve the static tool's provenance across persistence boundaries so
7
+ // model-message conversion never falls back to its unfiltered raw output.
8
+ part.dynamic = false;
9
+
10
+ return part;
11
+ }
12
+
13
+ export function isToolPartFromUnavailableTool(
14
+ part: ToolUIPart | DynamicToolUIPart,
15
+ ): boolean {
16
+ return part.type === 'dynamic-tool' && part.dynamic === false;
17
+ }
@@ -15,6 +15,7 @@ import {
15
15
  import { z } from 'zod/v4';
16
16
  import { InvalidArgumentError } from '../error';
17
17
  import { jsonValueSchema } from '../types/json-value';
18
+ import { getOwn } from '../util/get-own';
18
19
  import { isDeepEqualData } from '../util/is-deep-equal-data';
19
20
  import { providerMetadataSchema } from '../types/provider-metadata';
20
21
  import type {
@@ -25,6 +26,7 @@ import type {
25
26
  ToolUIPart,
26
27
  UIMessage,
27
28
  } from './ui-messages';
29
+ import { markToolPartAsUnavailable } from './unavailable-tool';
28
30
 
29
31
  const toolMetadataSchema: z.ZodType<JSONObject> = z.record(
30
32
  z.string(),
@@ -43,11 +45,11 @@ function isEmptyObject(value: unknown): value is Record<string, never> {
43
45
  function asDynamicToolPart(toolPart: ToolUIPart): DynamicToolUIPart {
44
46
  const { type, ...part } = toolPart;
45
47
 
46
- return {
48
+ return markToolPartAsUnavailable({
47
49
  ...part,
48
50
  type: 'dynamic-tool',
49
51
  toolName: type.slice(5),
50
- } as DynamicToolUIPart;
52
+ } as DynamicToolUIPart);
51
53
  }
52
54
 
53
55
  function getToolPartInputSchemaInput(
@@ -134,6 +136,7 @@ const uiMessagesSchema = lazySchema(() => {
134
136
  }),
135
137
  z.object({
136
138
  type: z.literal('dynamic-tool'),
139
+ dynamic: z.literal(false).optional(),
137
140
  toolName: z.string(),
138
141
  toolCallId: z.string(),
139
142
  toolMetadata: toolMetadataSchema.optional(),
@@ -148,6 +151,7 @@ const uiMessagesSchema = lazySchema(() => {
148
151
  }),
149
152
  z.object({
150
153
  type: z.literal('dynamic-tool'),
154
+ dynamic: z.literal(false).optional(),
151
155
  toolName: z.string(),
152
156
  toolCallId: z.string(),
153
157
  toolMetadata: toolMetadataSchema.optional(),
@@ -161,6 +165,7 @@ const uiMessagesSchema = lazySchema(() => {
161
165
  }),
162
166
  z.object({
163
167
  type: z.literal('dynamic-tool'),
168
+ dynamic: z.literal(false).optional(),
164
169
  toolName: z.string(),
165
170
  toolCallId: z.string(),
166
171
  toolMetadata: toolMetadataSchema.optional(),
@@ -174,6 +179,7 @@ const uiMessagesSchema = lazySchema(() => {
174
179
  }),
175
180
  z.object({
176
181
  type: z.literal('dynamic-tool'),
182
+ dynamic: z.literal(false).optional(),
177
183
  toolName: z.string(),
178
184
  toolCallId: z.string(),
179
185
  toolMetadata: toolMetadataSchema.optional(),
@@ -187,6 +193,7 @@ const uiMessagesSchema = lazySchema(() => {
187
193
  }),
188
194
  z.object({
189
195
  type: z.literal('dynamic-tool'),
196
+ dynamic: z.literal(false).optional(),
190
197
  toolName: z.string(),
191
198
  toolCallId: z.string(),
192
199
  toolMetadata: toolMetadataSchema.optional(),
@@ -202,6 +209,7 @@ const uiMessagesSchema = lazySchema(() => {
202
209
  }),
203
210
  z.object({
204
211
  type: z.literal('dynamic-tool'),
212
+ dynamic: z.literal(false).optional(),
205
213
  toolName: z.string(),
206
214
  toolCallId: z.string(),
207
215
  toolMetadata: toolMetadataSchema.optional(),
@@ -217,6 +225,7 @@ const uiMessagesSchema = lazySchema(() => {
217
225
  }),
218
226
  z.object({
219
227
  type: z.literal('dynamic-tool'),
228
+ dynamic: z.literal(false).optional(),
220
229
  toolName: z.string(),
221
230
  toolCallId: z.string(),
222
231
  toolMetadata: toolMetadataSchema.optional(),
@@ -451,7 +460,7 @@ async function safeValidateUIMessagesInternal<UI_MESSAGE extends UIMessage>(
451
460
  InferUIMessageTools<UI_MESSAGE>
452
461
  >;
453
462
  const toolName = toolPart.type.slice(5);
454
- const tool = tools?.[toolName];
463
+ const tool = getOwn(tools, toolName);
455
464
  const isTerminal =
456
465
  toolPart.state === 'output-available' ||
457
466
  toolPart.state === 'output-error' ||
@@ -95,6 +95,7 @@ export function readUIMessageStream<UI_MESSAGE extends UIMessage>({
95
95
  consumeStream({
96
96
  stream: processUIMessageStream({
97
97
  stream,
98
+ resetStateOnMessageIdChange: true,
98
99
  runUpdateMessageJob(
99
100
  job: (options: {
100
101
  state: StreamingUIMessageState<UI_MESSAGE>;
@@ -3,6 +3,7 @@ import type { Job } from './job';
3
3
  export class SerialJobExecutor {
4
4
  private queue: Array<Job> = [];
5
5
  private isProcessing = false;
6
+ private idleWaiters: Array<() => void> = [];
6
7
 
7
8
  private async processQueue() {
8
9
  if (this.isProcessing) {
@@ -11,12 +12,18 @@ export class SerialJobExecutor {
11
12
 
12
13
  this.isProcessing = true;
13
14
 
14
- while (this.queue.length > 0) {
15
- await this.queue[0]();
16
- this.queue.shift();
17
- }
15
+ try {
16
+ while (this.queue.length > 0) {
17
+ await this.queue[0]();
18
+ this.queue.shift();
19
+ }
20
+ } finally {
21
+ this.isProcessing = false;
18
22
 
19
- this.isProcessing = false;
23
+ for (const resolve of this.idleWaiters.splice(0)) {
24
+ resolve();
25
+ }
26
+ }
20
27
  }
21
28
 
22
29
  async run(job: Job): Promise<void> {
@@ -33,4 +40,14 @@ export class SerialJobExecutor {
33
40
  void this.processQueue();
34
41
  });
35
42
  }
43
+
44
+ async waitForIdle(): Promise<void> {
45
+ if (!this.isProcessing && this.queue.length === 0) {
46
+ return;
47
+ }
48
+
49
+ await new Promise<void>(resolve => {
50
+ this.idleWaiters.push(resolve);
51
+ });
52
+ }
36
53
  }