ai 6.0.303 → 6.0.304

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.
@@ -13,6 +13,13 @@ 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
+
16
23
  /**
17
24
  * Sends messages to the chat API endpoint and returns a streaming response.
18
25
  *
package/src/ui/chat.ts CHANGED
@@ -518,6 +518,33 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
518
518
  await this.makeRequest({ trigger: 'resume-stream', ...options });
519
519
  };
520
520
 
521
+ /**
522
+ * Resume the stream if the latest response ended with a resumable network
523
+ * disconnect. If a response is still active, wait for it to settle first so
524
+ * disconnects reported shortly after the caller's trigger are handled.
525
+ *
526
+ * @internal
527
+ */
528
+ '~resumeStreamIfDisconnected' = async ({
529
+ shouldResume = () => true,
530
+ }: {
531
+ shouldResume?: () => boolean;
532
+ } = {}): Promise<void> => {
533
+ const activeResponse = this.activeResponse;
534
+
535
+ if (this.resumableStreamState == null && activeResponse != null) {
536
+ await activeResponse.completionPromise;
537
+
538
+ if (this.resumableStreamState !== activeResponse.state) {
539
+ return;
540
+ }
541
+ }
542
+
543
+ if (this.resumableStreamState != null && shouldResume()) {
544
+ await this.resumeStream();
545
+ }
546
+ };
547
+
521
548
  /**
522
549
  * Clear the error state and set the status to ready if the chat is in an error state.
523
550
  */
@@ -611,7 +638,6 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
611
638
  }) =>
612
639
  this.jobExecutor.run(async () => {
613
640
  const messages = this.state.messages;
614
- const lastMessage = messages[messages.length - 1];
615
641
 
616
642
  const updatePart = (
617
643
  part: UIMessagePart<UIDataTypes, UITools>,
@@ -620,11 +646,21 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
620
646
  ? ({ ...part, state, output, errorText } as typeof part)
621
647
  : part;
622
648
 
623
- // update the message to trigger an immediate UI update
624
- this.state.replaceMessage(messages.length - 1, {
625
- ...lastMessage,
626
- parts: lastMessage.parts.map(updatePart),
627
- });
649
+ const messageIndex = messages.findIndex(message =>
650
+ message.parts.some(
651
+ part => isToolUIPart(part) && part.toolCallId === toolCallId,
652
+ ),
653
+ );
654
+
655
+ if (messageIndex !== -1) {
656
+ const message = messages[messageIndex];
657
+
658
+ // update the message to trigger an immediate UI update
659
+ this.state.replaceMessage(messageIndex, {
660
+ ...message,
661
+ parts: message.parts.map(updatePart),
662
+ });
663
+ }
628
664
 
629
665
  // update the active response if it exists
630
666
  if (this.activeResponse) {
@@ -634,6 +670,7 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
634
670
 
635
671
  // automatically send the message if the sendAutomaticallyWhen function returns true
636
672
  if (
673
+ messageIndex !== -1 &&
637
674
  this.status !== 'streaming' &&
638
675
  this.status !== 'submitted' &&
639
676
  this.sendAutomaticallyWhen
@@ -646,7 +683,7 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
646
683
  () =>
647
684
  this.makeRequest({
648
685
  trigger: 'submit-message',
649
- messageId: this.lastMessage?.id,
686
+ messageId: messages[messageIndex].id,
650
687
  ...options,
651
688
  }),
652
689
  shouldSend,
@@ -872,6 +909,7 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
872
909
  responseMessageIndex === -1
873
910
  ? lastMessage
874
911
  : this.state.messages[responseMessageIndex];
912
+ const originalResponseMessageId = responseMessage?.id;
875
913
  const resumableResponseMessage =
876
914
  trigger === 'resume-stream' &&
877
915
  responseMessage?.role === 'assistant' &&
@@ -956,16 +994,28 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
956
994
  this.setStatus({ status: 'streaming' });
957
995
  }
958
996
 
959
- if (usesEarlierAssistantMessage) {
997
+ const existingMessageIndex = this.state.messages.findLastIndex(
998
+ message => message.id === response.state.message.id,
999
+ );
1000
+
1001
+ if (existingMessageIndex !== -1) {
960
1002
  this.state.replaceMessage(
961
- responseMessageIndex,
1003
+ existingMessageIndex,
962
1004
  response.state.message,
963
1005
  );
964
- } else if (response.state.message.id === this.lastMessage?.id) {
965
- this.state.replaceMessage(
966
- this.state.messages.length - 1,
967
- response.state.message,
1006
+ } else if (usesEarlierAssistantMessage) {
1007
+ const originalMessageIndex = this.state.messages.findLastIndex(
1008
+ message => message.id === originalResponseMessageId,
968
1009
  );
1010
+
1011
+ if (originalMessageIndex !== -1) {
1012
+ this.state.replaceMessage(
1013
+ originalMessageIndex,
1014
+ response.state.message,
1015
+ );
1016
+ } else {
1017
+ this.state.pushMessage(response.state.message);
1018
+ }
969
1019
  } else {
970
1020
  this.state.pushMessage(response.state.message);
971
1021
  }
@@ -976,6 +1026,9 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
976
1026
  await consumeStream({
977
1027
  stream: processUIMessageStream({
978
1028
  stream,
1029
+ resetStateOnMessageIdChange: trigger === 'resume-stream',
1030
+ resetStateOnFirstMessageStart:
1031
+ trigger === 'resume-stream' && this.transport.resumeStreamIsReplay,
979
1032
  onToolCall: this.onToolCall,
980
1033
  onData: this.onData,
981
1034
  messageMetadataSchema: this.messageMetadataSchema,
@@ -12,8 +12,21 @@ import type { UIMessage } from './ui-messages';
12
12
  export class DefaultChatTransport<
13
13
  UI_MESSAGE extends UIMessage,
14
14
  > extends HttpChatTransport<UI_MESSAGE> {
15
- constructor(options: HttpChatTransportInitOptions<UI_MESSAGE> = {}) {
15
+ readonly resumeStreamIsReplay: boolean;
16
+
17
+ constructor({
18
+ resumeStreamIsReplay = true,
19
+ ...options
20
+ }: HttpChatTransportInitOptions<UI_MESSAGE> & {
21
+ /**
22
+ * Whether resumed streams replay the response from the beginning.
23
+ * Defaults to true. Set to false for endpoints that only continue the
24
+ * interrupted response, even when they emit a start chunk.
25
+ */
26
+ resumeStreamIsReplay?: boolean;
27
+ } = {}) {
16
28
  super(options);
29
+ this.resumeStreamIsReplay = resumeStreamIsReplay;
17
30
  }
18
31
 
19
32
  protected processResponseStream(
@@ -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,7 +140,11 @@ 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>> {
146
+ let isFirstMessageStart = true;
147
+
143
148
  return stream.pipeThrough(
144
149
  new TransformStream<UIMessageChunk, InferUIMessageChunk<UI_MESSAGE>>({
145
150
  async transform(chunk, controller) {
@@ -881,19 +886,23 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
881
886
  }
882
887
 
883
888
  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.
889
+ const shouldReset =
890
+ (resetStateOnFirstMessageStart && isFirstMessageStart) ||
891
+ (resetStateOnMessageIdChange &&
892
+ chunk.messageId != null &&
893
+ chunk.messageId !== state.message.id);
894
+ isFirstMessageStart = false;
895
+
896
+ if (shouldReset) {
897
+ // Rebuild a replay or separate response from empty state. Only
898
+ // reset a replay once: merged streams can emit more starts.
899
+ // Keep the state object for its callers and preserve the ID
900
+ // when the replay does not include one.
892
901
  Object.assign(
893
902
  state,
894
903
  createStreamingUIMessageState({
895
904
  lastMessage: undefined,
896
- messageId: chunk.messageId,
905
+ messageId: chunk.messageId ?? state.message.id,
897
906
  }),
898
907
  { finishReason: undefined },
899
908
  );
@@ -905,7 +914,11 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
905
914
 
906
915
  await updateMessageMetadata(chunk.messageMetadata);
907
916
 
908
- if (chunk.messageId != null || chunk.messageMetadata != null) {
917
+ if (
918
+ shouldReset ||
919
+ chunk.messageId != null ||
920
+ chunk.messageMetadata != null
921
+ ) {
909
922
  write({ updateStatus: false });
910
923
  }
911
924
  break;