ai 7.0.133 → 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,25 +601,28 @@ 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
597
608
  ) {
598
- this.shouldSendAutomatically().then(shouldSend => {
599
- if (shouldSend) {
600
- // no await to avoid deadlocking
609
+ const shouldSend = await this.shouldSendAutomatically();
610
+
611
+ if (shouldSend) {
612
+ // no await to avoid deadlocking
613
+ void this.runAutomaticRequest(() => {
601
614
  const messageId =
602
615
  messageIndex === -1
603
616
  ? this.lastMessage?.id
604
617
  : messages[messageIndex].id;
605
618
 
606
- this.makeRequestForToolApproval({
619
+ return this.makeRequestForToolApproval({
607
620
  messageId,
608
621
  messageIndex,
609
622
  ...options,
610
623
  });
611
- }
612
- });
624
+ }, shouldSend);
625
+ }
613
626
  }
614
627
  });
615
628
 
@@ -680,16 +693,20 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
680
693
  this.status !== 'submitted' &&
681
694
  this.sendAutomaticallyWhen
682
695
  ) {
683
- this.shouldSendAutomatically().then(shouldSend => {
684
- if (shouldSend) {
685
- // no await to avoid deadlocking
686
- this.makeRequest({
687
- trigger: 'submit-message',
688
- messageId: this.lastMessage?.id,
689
- ...options,
690
- });
691
- }
692
- });
696
+ const shouldSend = await this.shouldSendAutomatically();
697
+
698
+ if (shouldSend) {
699
+ // no await to avoid deadlocking
700
+ void this.runAutomaticRequest(
701
+ () =>
702
+ this.makeRequest({
703
+ trigger: 'submit-message',
704
+ messageId: this.lastMessage?.id,
705
+ ...options,
706
+ }),
707
+ shouldSend,
708
+ );
709
+ }
693
710
  }
694
711
  });
695
712
 
@@ -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();
@@ -854,10 +938,8 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
854
938
  responseMessageIndex === -1
855
939
  ? lastMessage
856
940
  : this.state.messages[responseMessageIndex];
857
- // the continued stream can start with either
858
- // 1) input deltas
859
- // 2) or the result of an answered tool approval request
860
- // Keep the tool part so in case 2, those chunks can find their tool call
941
+ // The continued stream can start with input deltas or a tool result.
942
+ // Keep unfinished tool parts so result chunks can find their tool call.
861
943
  const resumableResponseMessage =
862
944
  trigger === 'resume-stream' &&
863
945
  responseMessage?.role === 'assistant' &&
@@ -865,6 +947,7 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
865
947
  part =>
866
948
  isToolUIPart(part) &&
867
949
  (part.state === 'input-streaming' ||
950
+ (part.state === 'input-available' && part.providerExecuted) ||
868
951
  part.state === 'approval-responded'),
869
952
  )
870
953
  ? this.state.snapshot(responseMessage)
@@ -894,6 +977,7 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
894
977
  messageId: this.generateId(),
895
978
  }),
896
979
  abortController,
980
+ completionPromise,
897
981
  } as ActiveResponse<UI_MESSAGE>;
898
982
 
899
983
  activeResponse = response;
@@ -965,6 +1049,8 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
965
1049
  stream: processUIMessageStream({
966
1050
  stream,
967
1051
  resetStateOnMessageIdChange: trigger === 'resume-stream',
1052
+ resetStateOnFirstMessageStart:
1053
+ trigger === 'resume-stream' && this.transport.resumeStreamIsReplay,
968
1054
  onToolCall: this.onToolCall,
969
1055
  onData: this.onData,
970
1056
  messageMetadataSchema: this.messageMetadataSchema,
@@ -1060,14 +1146,16 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
1060
1146
  }
1061
1147
 
1062
1148
  // automatically send the message if the sendAutomaticallyWhen function returns true
1063
- if (!isError && (await this.shouldSendAutomatically())) {
1064
- await this.makeRequest({
1065
- trigger: 'submit-message',
1066
- messageId: this.lastMessage?.id,
1067
- metadata,
1068
- headers,
1069
- body,
1070
- });
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
+ );
1071
1159
  }
1072
1160
  }
1073
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