ai 6.0.302 → 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.
- package/CHANGELOG.md +24 -0
- package/dist/index.d.mts +34 -5
- package/dist/index.d.ts +34 -5
- package/dist/index.js +174 -85
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +174 -85
- package/dist/index.mjs.map +1 -1
- package/dist/internal/index.js +1 -33
- package/dist/internal/index.js.map +1 -1
- package/dist/internal/index.mjs +1 -33
- package/dist/internal/index.mjs.map +1 -1
- package/docs/04-ai-sdk-ui/03-chatbot-resume-streams.mdx +23 -1
- package/docs/04-ai-sdk-ui/21-error-handling.mdx +34 -0
- package/docs/07-reference/02-ai-sdk-ui/01-use-chat.mdx +8 -1
- package/package.json +2 -2
- package/src/global.ts +23 -4
- package/src/prompt/convert-to-language-model-prompt.ts +0 -42
- package/src/ui/chat-transport.ts +7 -0
- package/src/ui/chat.ts +176 -45
- package/src/ui/default-chat-transport.ts +14 -1
- package/src/ui/process-ui-message-stream.ts +35 -1
- package/src/ui-message-stream/read-ui-message-stream.ts +1 -0
- package/src/util/serial-job-executor.ts +22 -5
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
|
-
//
|
|
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:
|
|
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:
|
|
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:
|
|
52
|
+
var AI_SDK_TELEMETRY_INTEGRATIONS:
|
|
53
|
+
| Array<AISDKGlobalTypes[keyof AISDKGlobalTypes]['telemetry']>
|
|
54
|
+
| undefined;
|
|
36
55
|
}
|
|
@@ -52,38 +52,6 @@ export async function convertToLanguageModelPrompt({
|
|
|
52
52
|
supportedUrls,
|
|
53
53
|
);
|
|
54
54
|
|
|
55
|
-
const approvalIdToToolCallId = new Map<string, string>();
|
|
56
|
-
for (const message of prompt.messages) {
|
|
57
|
-
if (message.role === 'assistant' && Array.isArray(message.content)) {
|
|
58
|
-
for (const part of message.content) {
|
|
59
|
-
if (
|
|
60
|
-
part.type === 'tool-approval-request' &&
|
|
61
|
-
'approvalId' in part &&
|
|
62
|
-
'toolCallId' in part
|
|
63
|
-
) {
|
|
64
|
-
approvalIdToToolCallId.set(
|
|
65
|
-
part.approvalId as string,
|
|
66
|
-
part.toolCallId as string,
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const approvedToolCallIds = new Set<string>();
|
|
74
|
-
for (const message of prompt.messages) {
|
|
75
|
-
if (message.role === 'tool') {
|
|
76
|
-
for (const part of message.content) {
|
|
77
|
-
if (part.type === 'tool-approval-response') {
|
|
78
|
-
const toolCallId = approvalIdToToolCallId.get(part.approvalId);
|
|
79
|
-
if (toolCallId) {
|
|
80
|
-
approvedToolCallIds.add(toolCallId);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
55
|
const messages = [
|
|
88
56
|
...(prompt.system != null
|
|
89
57
|
? typeof prompt.system === 'string'
|
|
@@ -149,11 +117,6 @@ export async function convertToLanguageModelPrompt({
|
|
|
149
117
|
}
|
|
150
118
|
case 'user':
|
|
151
119
|
case 'system':
|
|
152
|
-
// remove approved tool calls from the set before checking:
|
|
153
|
-
for (const id of approvedToolCallIds) {
|
|
154
|
-
toolCallIds.delete(id);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
120
|
if (toolCallIds.size > 0) {
|
|
158
121
|
throw new MissingToolResultsError({
|
|
159
122
|
toolCallIds: Array.from(toolCallIds),
|
|
@@ -163,11 +126,6 @@ export async function convertToLanguageModelPrompt({
|
|
|
163
126
|
}
|
|
164
127
|
}
|
|
165
128
|
|
|
166
|
-
// remove approved tool calls from the set before checking:
|
|
167
|
-
for (const id of approvedToolCallIds) {
|
|
168
|
-
toolCallIds.delete(id);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
129
|
if (toolCallIds.size > 0) {
|
|
172
130
|
throw new MissingToolResultsError({ toolCallIds: Array.from(toolCallIds) });
|
|
173
131
|
}
|
package/src/ui/chat-transport.ts
CHANGED
|
@@ -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
|
@@ -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,
|
|
@@ -508,6 +518,33 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
|
|
|
508
518
|
await this.makeRequest({ trigger: 'resume-stream', ...options });
|
|
509
519
|
};
|
|
510
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
|
+
|
|
511
548
|
/**
|
|
512
549
|
* Clear the error state and set the status to ready if the chat is in an error state.
|
|
513
550
|
*/
|
|
@@ -572,21 +609,23 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
|
|
|
572
609
|
this.status !== 'submitted' &&
|
|
573
610
|
this.sendAutomaticallyWhen
|
|
574
611
|
) {
|
|
575
|
-
this.shouldSendAutomatically()
|
|
576
|
-
|
|
577
|
-
|
|
612
|
+
const shouldSend = await this.shouldSendAutomatically();
|
|
613
|
+
|
|
614
|
+
if (shouldSend) {
|
|
615
|
+
// no await to avoid deadlocking
|
|
616
|
+
void this.runAutomaticRequest(() => {
|
|
578
617
|
const messageId =
|
|
579
618
|
messageIndex === -1
|
|
580
619
|
? this.lastMessage?.id
|
|
581
620
|
: messages[messageIndex].id;
|
|
582
621
|
|
|
583
|
-
this.makeRequestForToolApproval({
|
|
622
|
+
return this.makeRequestForToolApproval({
|
|
584
623
|
messageId,
|
|
585
624
|
messageIndex,
|
|
586
625
|
...options,
|
|
587
626
|
});
|
|
588
|
-
}
|
|
589
|
-
}
|
|
627
|
+
}, shouldSend);
|
|
628
|
+
}
|
|
590
629
|
}
|
|
591
630
|
});
|
|
592
631
|
|
|
@@ -599,7 +638,6 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
|
|
|
599
638
|
}) =>
|
|
600
639
|
this.jobExecutor.run(async () => {
|
|
601
640
|
const messages = this.state.messages;
|
|
602
|
-
const lastMessage = messages[messages.length - 1];
|
|
603
641
|
|
|
604
642
|
const updatePart = (
|
|
605
643
|
part: UIMessagePart<UIDataTypes, UITools>,
|
|
@@ -608,11 +646,21 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
|
|
|
608
646
|
? ({ ...part, state, output, errorText } as typeof part)
|
|
609
647
|
: part;
|
|
610
648
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
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
|
+
}
|
|
616
664
|
|
|
617
665
|
// update the active response if it exists
|
|
618
666
|
if (this.activeResponse) {
|
|
@@ -622,20 +670,25 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
|
|
|
622
670
|
|
|
623
671
|
// automatically send the message if the sendAutomaticallyWhen function returns true
|
|
624
672
|
if (
|
|
673
|
+
messageIndex !== -1 &&
|
|
625
674
|
this.status !== 'streaming' &&
|
|
626
675
|
this.status !== 'submitted' &&
|
|
627
676
|
this.sendAutomaticallyWhen
|
|
628
677
|
) {
|
|
629
|
-
this.shouldSendAutomatically()
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
678
|
+
const shouldSend = await this.shouldSendAutomatically();
|
|
679
|
+
|
|
680
|
+
if (shouldSend) {
|
|
681
|
+
// no await to avoid deadlocking
|
|
682
|
+
void this.runAutomaticRequest(
|
|
683
|
+
() =>
|
|
684
|
+
this.makeRequest({
|
|
685
|
+
trigger: 'submit-message',
|
|
686
|
+
messageId: messages[messageIndex].id,
|
|
687
|
+
...options,
|
|
688
|
+
}),
|
|
689
|
+
shouldSend,
|
|
690
|
+
);
|
|
691
|
+
}
|
|
639
692
|
}
|
|
640
693
|
});
|
|
641
694
|
|
|
@@ -643,11 +696,31 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
|
|
|
643
696
|
addToolResult = this.addToolOutput;
|
|
644
697
|
|
|
645
698
|
/**
|
|
646
|
-
* Abort the current request
|
|
699
|
+
* Abort the current request, keep the generated tokens if any, and wait for
|
|
700
|
+
* the request pipeline to finish.
|
|
647
701
|
*/
|
|
648
702
|
stop = async () => {
|
|
649
|
-
this.
|
|
650
|
-
this.
|
|
703
|
+
this.activeStopCount++;
|
|
704
|
+
this.stopGeneration++;
|
|
705
|
+
|
|
706
|
+
try {
|
|
707
|
+
const activeResumeRequest = this.activeResumeRequest;
|
|
708
|
+
const activeResponse = this.activeResponse;
|
|
709
|
+
|
|
710
|
+
activeResumeRequest?.abortController.abort();
|
|
711
|
+
activeResponse?.abortController.abort();
|
|
712
|
+
|
|
713
|
+
await Promise.all([
|
|
714
|
+
activeResumeRequest?.completionPromise,
|
|
715
|
+
activeResponse?.completionPromise,
|
|
716
|
+
]);
|
|
717
|
+
|
|
718
|
+
// Stream cancellation can complete while a processing job is still
|
|
719
|
+
// blocked in onToolCall. Drain that job and any message update it queued.
|
|
720
|
+
await this.jobExecutor.waitForIdle();
|
|
721
|
+
} finally {
|
|
722
|
+
this.activeStopCount--;
|
|
723
|
+
}
|
|
651
724
|
};
|
|
652
725
|
|
|
653
726
|
private async shouldSendAutomatically(): Promise<boolean> {
|
|
@@ -665,6 +738,30 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
|
|
|
665
738
|
return result as boolean;
|
|
666
739
|
}
|
|
667
740
|
|
|
741
|
+
private async runAutomaticRequest(
|
|
742
|
+
request: () => Promise<void>,
|
|
743
|
+
shouldSend?: boolean,
|
|
744
|
+
): Promise<void> {
|
|
745
|
+
const stopGeneration = this.stopGeneration;
|
|
746
|
+
const startedWhileStopping = this.activeStopCount > 0;
|
|
747
|
+
|
|
748
|
+
if (!(shouldSend ?? (await this.shouldSendAutomatically()))) {
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// A stop invalidates automatic requests that were pending when it started,
|
|
753
|
+
// including requests queued by callback-driven tool updates.
|
|
754
|
+
if (
|
|
755
|
+
startedWhileStopping ||
|
|
756
|
+
stopGeneration !== this.stopGeneration ||
|
|
757
|
+
this.activeStopCount > 0
|
|
758
|
+
) {
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
await request();
|
|
763
|
+
}
|
|
764
|
+
|
|
668
765
|
private async makeRequestForToolApproval({
|
|
669
766
|
messageId,
|
|
670
767
|
messageIndex,
|
|
@@ -695,23 +792,38 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
|
|
|
695
792
|
}
|
|
696
793
|
}
|
|
697
794
|
|
|
698
|
-
private async makeRequest({
|
|
795
|
+
private async makeRequest(options: MakeRequestOptions) {
|
|
796
|
+
const completion = createResolvablePromise<void>();
|
|
797
|
+
|
|
798
|
+
try {
|
|
799
|
+
await this.makeRequestImpl({
|
|
800
|
+
...options,
|
|
801
|
+
completionPromise: completion.promise,
|
|
802
|
+
});
|
|
803
|
+
} finally {
|
|
804
|
+
completion.resolve();
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
private async makeRequestImpl({
|
|
699
809
|
trigger,
|
|
700
810
|
metadata,
|
|
701
811
|
headers,
|
|
702
812
|
body,
|
|
703
813
|
messageId,
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
}
|
|
814
|
+
completionPromise,
|
|
815
|
+
}: MakeRequestOptions & {
|
|
816
|
+
completionPromise: Promise<void>;
|
|
817
|
+
}) {
|
|
708
818
|
if (trigger !== 'resume-stream') {
|
|
709
819
|
this.resumableStreamState = undefined;
|
|
710
820
|
}
|
|
711
821
|
|
|
712
822
|
const abortController = new AbortController();
|
|
713
823
|
const activeResumeRequest =
|
|
714
|
-
trigger === 'resume-stream'
|
|
824
|
+
trigger === 'resume-stream'
|
|
825
|
+
? { abortController, completionPromise }
|
|
826
|
+
: undefined;
|
|
715
827
|
|
|
716
828
|
if (activeResumeRequest) {
|
|
717
829
|
this.activeResumeRequest?.abortController.abort();
|
|
@@ -797,6 +909,7 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
|
|
|
797
909
|
responseMessageIndex === -1
|
|
798
910
|
? lastMessage
|
|
799
911
|
: this.state.messages[responseMessageIndex];
|
|
912
|
+
const originalResponseMessageId = responseMessage?.id;
|
|
800
913
|
const resumableResponseMessage =
|
|
801
914
|
trigger === 'resume-stream' &&
|
|
802
915
|
responseMessage?.role === 'assistant' &&
|
|
@@ -830,6 +943,7 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
|
|
|
830
943
|
messageId: this.generateId(),
|
|
831
944
|
}),
|
|
832
945
|
abortController,
|
|
946
|
+
completionPromise,
|
|
833
947
|
} as ActiveResponse<UI_MESSAGE>;
|
|
834
948
|
|
|
835
949
|
activeResponse = response;
|
|
@@ -880,16 +994,28 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
|
|
|
880
994
|
this.setStatus({ status: 'streaming' });
|
|
881
995
|
}
|
|
882
996
|
|
|
883
|
-
|
|
997
|
+
const existingMessageIndex = this.state.messages.findLastIndex(
|
|
998
|
+
message => message.id === response.state.message.id,
|
|
999
|
+
);
|
|
1000
|
+
|
|
1001
|
+
if (existingMessageIndex !== -1) {
|
|
884
1002
|
this.state.replaceMessage(
|
|
885
|
-
|
|
1003
|
+
existingMessageIndex,
|
|
886
1004
|
response.state.message,
|
|
887
1005
|
);
|
|
888
|
-
} else if (
|
|
889
|
-
this.state.
|
|
890
|
-
|
|
891
|
-
response.state.message,
|
|
1006
|
+
} else if (usesEarlierAssistantMessage) {
|
|
1007
|
+
const originalMessageIndex = this.state.messages.findLastIndex(
|
|
1008
|
+
message => message.id === originalResponseMessageId,
|
|
892
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
|
+
}
|
|
893
1019
|
} else {
|
|
894
1020
|
this.state.pushMessage(response.state.message);
|
|
895
1021
|
}
|
|
@@ -900,6 +1026,9 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
|
|
|
900
1026
|
await consumeStream({
|
|
901
1027
|
stream: processUIMessageStream({
|
|
902
1028
|
stream,
|
|
1029
|
+
resetStateOnMessageIdChange: trigger === 'resume-stream',
|
|
1030
|
+
resetStateOnFirstMessageStart:
|
|
1031
|
+
trigger === 'resume-stream' && this.transport.resumeStreamIsReplay,
|
|
903
1032
|
onToolCall: this.onToolCall,
|
|
904
1033
|
onData: this.onData,
|
|
905
1034
|
messageMetadataSchema: this.messageMetadataSchema,
|
|
@@ -992,14 +1121,16 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
|
|
|
992
1121
|
}
|
|
993
1122
|
|
|
994
1123
|
// automatically send the message if the sendAutomaticallyWhen function returns true
|
|
995
|
-
if (!
|
|
996
|
-
await this.
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1124
|
+
if (!isAbort && !isError) {
|
|
1125
|
+
await this.runAutomaticRequest(() =>
|
|
1126
|
+
this.makeRequest({
|
|
1127
|
+
trigger: 'submit-message',
|
|
1128
|
+
messageId: this.lastMessage?.id,
|
|
1129
|
+
metadata,
|
|
1130
|
+
headers,
|
|
1131
|
+
body,
|
|
1132
|
+
}),
|
|
1133
|
+
);
|
|
1003
1134
|
}
|
|
1004
1135
|
}
|
|
1005
1136
|
}
|
|
@@ -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
|
-
|
|
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(
|
|
@@ -120,6 +120,8 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
|
|
|
120
120
|
onError,
|
|
121
121
|
onToolCall,
|
|
122
122
|
onData,
|
|
123
|
+
resetStateOnMessageIdChange = false,
|
|
124
|
+
resetStateOnFirstMessageStart = false,
|
|
123
125
|
}: {
|
|
124
126
|
// input stream is not fully typed yet:
|
|
125
127
|
stream: ReadableStream<UIMessageChunk>;
|
|
@@ -136,7 +138,13 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
|
|
|
136
138
|
}) => Promise<void>,
|
|
137
139
|
) => Promise<void>;
|
|
138
140
|
onError: ErrorHandler;
|
|
141
|
+
// During resume, a different message ID identifies a separate response.
|
|
142
|
+
resetStateOnMessageIdChange?: boolean;
|
|
143
|
+
// A transport can replay a response from the beginning using the same ID.
|
|
144
|
+
resetStateOnFirstMessageStart?: boolean;
|
|
139
145
|
}): ReadableStream<InferUIMessageChunk<UI_MESSAGE>> {
|
|
146
|
+
let isFirstMessageStart = true;
|
|
147
|
+
|
|
140
148
|
return stream.pipeThrough(
|
|
141
149
|
new TransformStream<UIMessageChunk, InferUIMessageChunk<UI_MESSAGE>>({
|
|
142
150
|
async transform(chunk, controller) {
|
|
@@ -878,13 +886,39 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
|
|
|
878
886
|
}
|
|
879
887
|
|
|
880
888
|
case 'start': {
|
|
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.
|
|
901
|
+
Object.assign(
|
|
902
|
+
state,
|
|
903
|
+
createStreamingUIMessageState({
|
|
904
|
+
lastMessage: undefined,
|
|
905
|
+
messageId: chunk.messageId ?? state.message.id,
|
|
906
|
+
}),
|
|
907
|
+
{ finishReason: undefined },
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
|
|
881
911
|
if (chunk.messageId != null) {
|
|
882
912
|
state.message.id = chunk.messageId;
|
|
883
913
|
}
|
|
884
914
|
|
|
885
915
|
await updateMessageMetadata(chunk.messageMetadata);
|
|
886
916
|
|
|
887
|
-
if (
|
|
917
|
+
if (
|
|
918
|
+
shouldReset ||
|
|
919
|
+
chunk.messageId != null ||
|
|
920
|
+
chunk.messageMetadata != null
|
|
921
|
+
) {
|
|
888
922
|
write({ updateStatus: false });
|
|
889
923
|
}
|
|
890
924
|
break;
|
|
@@ -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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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
|
}
|