ai 7.0.134 → 7.0.136
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.ts +234 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +616 -57
- package/dist/index.js.map +1 -1
- package/dist/internal/index.d.ts +6 -2
- package/dist/internal/index.d.ts.map +1 -1
- package/dist/internal/index.js +1 -1
- package/docs/03-agents/04-loop-control.mdx +2 -0
- package/docs/03-ai-sdk-core/15-tools-and-tool-calling.mdx +2 -2
- package/docs/03-ai-sdk-core/17-mcp-events.mdx +6 -2
- package/docs/03-ai-sdk-core/25-settings.mdx +4 -2
- package/docs/03-ai-sdk-core/30-embeddings.mdx +27 -0
- package/docs/03-ai-sdk-core/32-decisions.mdx +3 -2
- package/docs/04-ai-sdk-ui/21-transport.mdx +73 -0
- package/docs/07-reference/01-ai-sdk-core/01-generate-text.mdx +2 -2
- package/docs/07-reference/01-ai-sdk-core/02-stream-text.mdx +2 -2
- package/docs/07-reference/01-ai-sdk-core/05-embed.mdx +14 -0
- package/docs/07-reference/01-ai-sdk-core/06-embed-many.mdx +14 -0
- package/docs/07-reference/01-ai-sdk-core/16-tool-loop-agent.mdx +5 -5
- package/docs/07-reference/01-ai-sdk-core/23-create-mcp-client.mdx +2 -2
- package/docs/07-reference/01-ai-sdk-core/24-mcp-events.mdx +123 -5
- package/docs/07-reference/02-ai-sdk-ui/51-websocket-chat-transport.mdx +393 -0
- package/docs/07-reference/02-ai-sdk-ui/index.mdx +6 -0
- package/package.json +12 -12
- package/src/agent/default-stop-condition.ts +33 -0
- package/src/agent/tool-loop-agent.ts +2 -2
- package/src/embed/embed-events.ts +6 -0
- package/src/embed/embed-many.ts +18 -0
- package/src/embed/embed.ts +16 -0
- package/src/embed/validate-embedding-dimensions.ts +14 -0
- package/src/generate-text/stream-text.ts +31 -24
- package/src/prompt/request-options.ts +2 -2
- package/src/ui/chat-transport.ts +16 -0
- package/src/ui/chat.ts +123 -34
- package/src/ui/index.ts +13 -0
- package/src/ui/process-ui-message-stream.ts +6 -2
- package/src/ui/websocket-chat-transport.ts +987 -0
- package/src/util/serial-job-executor.ts +22 -5
package/src/embed/embed-many.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { notify } from '../util/notify';
|
|
|
20
20
|
import { prepareRetries } from '../util/prepare-retries';
|
|
21
21
|
import { splitArray } from '../util/split-array';
|
|
22
22
|
import type { EmbedEndEvent, EmbedStartEvent } from './embed-events';
|
|
23
|
+
import { validateEmbeddingDimensions } from './validate-embedding-dimensions';
|
|
23
24
|
import type { EmbedManyResult } from './embed-many-result';
|
|
24
25
|
import { VERSION } from '../version';
|
|
25
26
|
|
|
@@ -39,6 +40,8 @@ const originalGenerateCallId = createIdGenerator({
|
|
|
39
40
|
* @param model - The embedding model to use.
|
|
40
41
|
* @param values - The values that should be embedded.
|
|
41
42
|
*
|
|
43
|
+
* @param dimensions - Requested output dimensions. Must be a positive integer. Requires provider support.
|
|
44
|
+
*
|
|
42
45
|
* @param maxRetries - Maximum number of retries. Set to 0 to disable retries. Default: 2.
|
|
43
46
|
* @param abortSignal - An optional abort signal that can be used to cancel the call.
|
|
44
47
|
* @param headers - Additional HTTP headers to be sent with the request. Only applicable for HTTP-based providers.
|
|
@@ -59,6 +62,7 @@ const originalGenerateCallId = createIdGenerator({
|
|
|
59
62
|
export async function embedMany<RUNTIME_CONTEXT extends Context = Context>({
|
|
60
63
|
model: modelArg,
|
|
61
64
|
values,
|
|
65
|
+
dimensions,
|
|
62
66
|
maxParallelCalls = Infinity,
|
|
63
67
|
maxRetries: maxRetriesArg,
|
|
64
68
|
abortSignal,
|
|
@@ -83,6 +87,13 @@ export async function embedMany<RUNTIME_CONTEXT extends Context = Context>({
|
|
|
83
87
|
*/
|
|
84
88
|
values: Array<string>;
|
|
85
89
|
|
|
90
|
+
/**
|
|
91
|
+
* The requested number of dimensions for the output embeddings.
|
|
92
|
+
* Must be a positive integer. Support and allowed values depend on the model
|
|
93
|
+
* and provider implementation.
|
|
94
|
+
*/
|
|
95
|
+
dimensions?: number;
|
|
96
|
+
|
|
86
97
|
/**
|
|
87
98
|
* Maximum number of retries per embedding model call. Set to 0 to disable retries.
|
|
88
99
|
*
|
|
@@ -168,6 +179,8 @@ export async function embedMany<RUNTIME_CONTEXT extends Context = Context>({
|
|
|
168
179
|
generateCallId?: () => string;
|
|
169
180
|
};
|
|
170
181
|
}): Promise<EmbedManyResult> {
|
|
182
|
+
validateEmbeddingDimensions(dimensions);
|
|
183
|
+
|
|
171
184
|
const model = resolveEmbeddingModel(modelArg);
|
|
172
185
|
|
|
173
186
|
const { maxRetries, retry } = prepareRetries({
|
|
@@ -200,6 +213,7 @@ export async function embedMany<RUNTIME_CONTEXT extends Context = Context>({
|
|
|
200
213
|
provider: model.provider,
|
|
201
214
|
modelId: model.modelId,
|
|
202
215
|
value: values,
|
|
216
|
+
dimensions,
|
|
203
217
|
maxRetries,
|
|
204
218
|
headers: headersWithUserAgent,
|
|
205
219
|
providerOptions,
|
|
@@ -243,12 +257,14 @@ export async function embedMany<RUNTIME_CONTEXT extends Context = Context>({
|
|
|
243
257
|
provider: model.provider,
|
|
244
258
|
modelId: model.modelId,
|
|
245
259
|
values,
|
|
260
|
+
dimensions,
|
|
246
261
|
},
|
|
247
262
|
callbacks: [telemetryDispatcher.onEmbedStart],
|
|
248
263
|
});
|
|
249
264
|
|
|
250
265
|
const modelResponse = await model.doEmbed({
|
|
251
266
|
values,
|
|
267
|
+
dimensions,
|
|
252
268
|
abortSignal,
|
|
253
269
|
headers: headersWithUserAgent,
|
|
254
270
|
providerOptions,
|
|
@@ -371,12 +387,14 @@ export async function embedMany<RUNTIME_CONTEXT extends Context = Context>({
|
|
|
371
387
|
provider: model.provider,
|
|
372
388
|
modelId: model.modelId,
|
|
373
389
|
values: chunk,
|
|
390
|
+
dimensions,
|
|
374
391
|
},
|
|
375
392
|
callbacks: [telemetryDispatcher.onEmbedStart],
|
|
376
393
|
});
|
|
377
394
|
|
|
378
395
|
const modelResponse = await model.doEmbed({
|
|
379
396
|
values: chunk,
|
|
397
|
+
dimensions,
|
|
380
398
|
abortSignal,
|
|
381
399
|
headers: headersWithUserAgent,
|
|
382
400
|
providerOptions: chunkProviderOptions,
|
package/src/embed/embed.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { notify } from '../util/notify';
|
|
|
15
15
|
import { prepareRetries } from '../util/prepare-retries';
|
|
16
16
|
import { VERSION } from '../version';
|
|
17
17
|
import type { EmbedEndEvent, EmbedStartEvent } from './embed-events';
|
|
18
|
+
import { validateEmbeddingDimensions } from './validate-embedding-dimensions';
|
|
18
19
|
import type { EmbedResult } from './embed-result';
|
|
19
20
|
|
|
20
21
|
const originalGenerateCallId = createIdGenerator({
|
|
@@ -28,6 +29,8 @@ const originalGenerateCallId = createIdGenerator({
|
|
|
28
29
|
* @param model - The embedding model to use.
|
|
29
30
|
* @param value - The value that should be embedded.
|
|
30
31
|
*
|
|
32
|
+
* @param dimensions - Requested output dimensions. Must be a positive integer. Requires provider support.
|
|
33
|
+
*
|
|
31
34
|
* @param maxRetries - Maximum number of retries. Set to 0 to disable retries. Default: 2.
|
|
32
35
|
* @param abortSignal - An optional abort signal that can be used to cancel the call.
|
|
33
36
|
* @param headers - Additional HTTP headers to be sent with the request. Only applicable for HTTP-based providers.
|
|
@@ -44,6 +47,7 @@ const originalGenerateCallId = createIdGenerator({
|
|
|
44
47
|
export async function embed<RUNTIME_CONTEXT extends Context = Context>({
|
|
45
48
|
model: modelArg,
|
|
46
49
|
value,
|
|
50
|
+
dimensions,
|
|
47
51
|
providerOptions,
|
|
48
52
|
maxRetries: maxRetriesArg,
|
|
49
53
|
abortSignal,
|
|
@@ -67,6 +71,13 @@ export async function embed<RUNTIME_CONTEXT extends Context = Context>({
|
|
|
67
71
|
*/
|
|
68
72
|
value: string;
|
|
69
73
|
|
|
74
|
+
/**
|
|
75
|
+
* The requested number of dimensions for the output embeddings.
|
|
76
|
+
* Must be a positive integer. Support and allowed values depend on the model
|
|
77
|
+
* and provider implementation.
|
|
78
|
+
*/
|
|
79
|
+
dimensions?: number;
|
|
80
|
+
|
|
70
81
|
/**
|
|
71
82
|
* Maximum number of retries per embedding model call. Set to 0 to disable retries.
|
|
72
83
|
*
|
|
@@ -144,6 +155,8 @@ export async function embed<RUNTIME_CONTEXT extends Context = Context>({
|
|
|
144
155
|
generateCallId?: () => string;
|
|
145
156
|
};
|
|
146
157
|
}): Promise<EmbedResult> {
|
|
158
|
+
validateEmbeddingDimensions(dimensions);
|
|
159
|
+
|
|
147
160
|
const model = resolveEmbeddingModel(modelArg);
|
|
148
161
|
|
|
149
162
|
const { maxRetries, retry } = prepareRetries({
|
|
@@ -176,6 +189,7 @@ export async function embed<RUNTIME_CONTEXT extends Context = Context>({
|
|
|
176
189
|
provider: model.provider,
|
|
177
190
|
modelId: model.modelId,
|
|
178
191
|
value,
|
|
192
|
+
dimensions,
|
|
179
193
|
maxRetries,
|
|
180
194
|
headers: headersWithUserAgent,
|
|
181
195
|
providerOptions,
|
|
@@ -203,12 +217,14 @@ export async function embed<RUNTIME_CONTEXT extends Context = Context>({
|
|
|
203
217
|
provider: model.provider,
|
|
204
218
|
modelId: model.modelId,
|
|
205
219
|
values: [value],
|
|
220
|
+
dimensions,
|
|
206
221
|
},
|
|
207
222
|
callbacks: [telemetryDispatcher.onEmbedStart],
|
|
208
223
|
});
|
|
209
224
|
|
|
210
225
|
const modelResponse = await model.doEmbed({
|
|
211
226
|
values: [value],
|
|
227
|
+
dimensions,
|
|
212
228
|
abortSignal,
|
|
213
229
|
headers: headersWithUserAgent,
|
|
214
230
|
providerOptions,
|
|
@@ -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
|
+
}
|
|
@@ -2275,6 +2275,9 @@ class DefaultStreamTextResult<
|
|
|
2275
2275
|
if (chunkTimeoutId != null) {
|
|
2276
2276
|
clearTimeout(chunkTimeoutId);
|
|
2277
2277
|
}
|
|
2278
|
+
if (abortSignal?.aborted) {
|
|
2279
|
+
return;
|
|
2280
|
+
}
|
|
2278
2281
|
chunkTimeoutId = setAbortTimeout({
|
|
2279
2282
|
abortController: chunkAbortController,
|
|
2280
2283
|
label: 'Chunk',
|
|
@@ -2289,6 +2292,11 @@ class DefaultStreamTextResult<
|
|
|
2289
2292
|
}
|
|
2290
2293
|
}
|
|
2291
2294
|
|
|
2295
|
+
function clearModelOutputTimeouts() {
|
|
2296
|
+
clearFirstChunkTimeout();
|
|
2297
|
+
clearChunkTimeout();
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2292
2300
|
function clearStepTimeout() {
|
|
2293
2301
|
if (stepTimeoutId != null) {
|
|
2294
2302
|
clearTimeout(stepTimeoutId);
|
|
@@ -2297,8 +2305,7 @@ class DefaultStreamTextResult<
|
|
|
2297
2305
|
|
|
2298
2306
|
function clearStepTimeouts() {
|
|
2299
2307
|
clearStepTimeout();
|
|
2300
|
-
|
|
2301
|
-
clearChunkTimeout();
|
|
2308
|
+
clearModelOutputTimeouts();
|
|
2302
2309
|
}
|
|
2303
2310
|
|
|
2304
2311
|
function cleanupStepTimeouts() {
|
|
@@ -2509,7 +2516,6 @@ class DefaultStreamTextResult<
|
|
|
2509
2516
|
let automaticStreamRetryCount = 0;
|
|
2510
2517
|
let callbackStreamRetryCount = 0;
|
|
2511
2518
|
let bufferedAttemptParts: LanguageModelStreamPart<TOOLS>[] = [];
|
|
2512
|
-
const outputChunksHandledBeforeBuffering = new WeakSet<object>();
|
|
2513
2519
|
const openTextParts = new Set<string>();
|
|
2514
2520
|
const openReasoningParts = new Set<string>();
|
|
2515
2521
|
let enqueueStreamRetryAttemptBoundary = false;
|
|
@@ -2561,7 +2567,16 @@ class DefaultStreamTextResult<
|
|
|
2561
2567
|
};
|
|
2562
2568
|
|
|
2563
2569
|
while (true) {
|
|
2564
|
-
|
|
2570
|
+
let result: ReadableStreamReadResult<
|
|
2571
|
+
LanguageModelStreamPart<TOOLS>
|
|
2572
|
+
>;
|
|
2573
|
+
try {
|
|
2574
|
+
result = await languageModelStreamReader.read();
|
|
2575
|
+
} catch (error) {
|
|
2576
|
+
clearModelOutputTimeouts();
|
|
2577
|
+
throw error;
|
|
2578
|
+
}
|
|
2579
|
+
const { done, value } = result;
|
|
2565
2580
|
|
|
2566
2581
|
if (enqueueStreamRetryAttemptBoundary) {
|
|
2567
2582
|
controller.enqueue(
|
|
@@ -2576,6 +2591,7 @@ class DefaultStreamTextResult<
|
|
|
2576
2591
|
}
|
|
2577
2592
|
|
|
2578
2593
|
if (done) {
|
|
2594
|
+
clearModelOutputTimeouts();
|
|
2579
2595
|
flushBufferedAttemptParts();
|
|
2580
2596
|
controller.close();
|
|
2581
2597
|
return;
|
|
@@ -2592,22 +2608,24 @@ class DefaultStreamTextResult<
|
|
|
2592
2608
|
value.type === 'tool-error';
|
|
2593
2609
|
|
|
2594
2610
|
if (value.type === 'model-call-end') {
|
|
2611
|
+
clearModelOutputTimeouts();
|
|
2595
2612
|
flushBufferedAttemptParts();
|
|
2596
2613
|
enqueueAttemptPart(value);
|
|
2597
2614
|
return;
|
|
2598
2615
|
}
|
|
2599
2616
|
|
|
2617
|
+
// Observe model output before buffering and tool processing.
|
|
2618
|
+
// Replaying buffered parts must not re-arm finished timers.
|
|
2619
|
+
if (isOutputChunk(value)) {
|
|
2620
|
+
clearFirstChunkTimeout();
|
|
2621
|
+
resetChunkTimeout();
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2600
2624
|
if (
|
|
2601
2625
|
shouldBufferToolParts &&
|
|
2602
2626
|
value.type !== 'error' &&
|
|
2603
2627
|
(isToolPart || bufferedAttemptParts.length > 0)
|
|
2604
2628
|
) {
|
|
2605
|
-
if (isOutputChunk(value)) {
|
|
2606
|
-
clearFirstChunkTimeout();
|
|
2607
|
-
resetChunkTimeout();
|
|
2608
|
-
outputChunksHandledBeforeBuffering.add(value);
|
|
2609
|
-
}
|
|
2610
|
-
|
|
2611
2629
|
bufferedAttemptParts.push(value);
|
|
2612
2630
|
continue;
|
|
2613
2631
|
}
|
|
@@ -2617,6 +2635,7 @@ class DefaultStreamTextResult<
|
|
|
2617
2635
|
return;
|
|
2618
2636
|
}
|
|
2619
2637
|
|
|
2638
|
+
clearModelOutputTimeouts();
|
|
2620
2639
|
await notify({
|
|
2621
2640
|
event: { chunk: value },
|
|
2622
2641
|
callbacks: onChunk,
|
|
@@ -2677,10 +2696,12 @@ class DefaultStreamTextResult<
|
|
|
2677
2696
|
response = retryLanguageModelCall.response;
|
|
2678
2697
|
languageModelStreamReader =
|
|
2679
2698
|
retryLanguageModelCall.stream.getReader();
|
|
2699
|
+
startFirstChunkTimeout();
|
|
2680
2700
|
enqueueStreamRetryAttemptBoundary = true;
|
|
2681
2701
|
}
|
|
2682
2702
|
},
|
|
2683
2703
|
cancel(reason) {
|
|
2704
|
+
clearModelOutputTimeouts();
|
|
2684
2705
|
return languageModelStreamReader.cancel(reason);
|
|
2685
2706
|
},
|
|
2686
2707
|
});
|
|
@@ -2857,21 +2878,7 @@ class DefaultStreamTextResult<
|
|
|
2857
2878
|
const chunkType = chunk.type;
|
|
2858
2879
|
|
|
2859
2880
|
if (isOutputChunk(chunk)) {
|
|
2860
|
-
const timeoutHandledBeforeBuffering =
|
|
2861
|
-
outputChunksHandledBeforeBuffering.has(chunk);
|
|
2862
|
-
|
|
2863
|
-
if (
|
|
2864
|
-
!hasReceivedOutputChunk &&
|
|
2865
|
-
!timeoutHandledBeforeBuffering
|
|
2866
|
-
) {
|
|
2867
|
-
// Clear before forwarding the first output so a timeout
|
|
2868
|
-
// cannot race with already-visible generated content.
|
|
2869
|
-
clearFirstChunkTimeout();
|
|
2870
|
-
}
|
|
2871
2881
|
hasReceivedOutputChunk = true;
|
|
2872
|
-
if (!timeoutHandledBeforeBuffering) {
|
|
2873
|
-
resetChunkTimeout();
|
|
2874
|
-
}
|
|
2875
2882
|
}
|
|
2876
2883
|
|
|
2877
2884
|
switch (chunkType) {
|
|
@@ -4,9 +4,9 @@ import type { ToolSet } from '@ai-sdk/provider-utils';
|
|
|
4
4
|
* Timeout configuration for API calls. Can be specified as:
|
|
5
5
|
* - A number representing milliseconds
|
|
6
6
|
* - An object with `totalMs` property for the total timeout in milliseconds
|
|
7
|
-
* - An object with `stepMs` property for the timeout of each step in milliseconds
|
|
7
|
+
* - An object with `stepMs` property for the timeout of each complete step, including model generation, retries, and local tool execution, in milliseconds
|
|
8
8
|
* - An object with `firstChunkMs` property for the timeout until the first content chunk of each step (streaming only)
|
|
9
|
-
* - An object with `chunkMs` property for the timeout between
|
|
9
|
+
* - An object with `chunkMs` property for the timeout between meaningful model output chunks, starting with the first output and ending when the model response ends (streaming only)
|
|
10
10
|
* - An object with `toolMs` property for the default timeout for all tool executions
|
|
11
11
|
* - An object with `tools` property for per-tool timeout overrides using `{toolName}Ms` keys
|
|
12
12
|
*/
|
package/src/ui/chat-transport.ts
CHANGED
|
@@ -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
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
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.
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
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
|
|
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
|
-
|
|
704
|
-
|
|
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
|
-
|
|
707
|
-
|
|
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
|
-
|
|
763
|
-
|
|
764
|
-
}
|
|
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'
|
|
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 (!
|
|
1063
|
-
await this.
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
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
|
-
|
|
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
|