ai 7.0.134 → 7.0.135

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