@convex-dev/agent 0.2.7 → 0.2.8-alpha.0
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/dist/UIMessages.d.ts +43 -0
- package/dist/UIMessages.d.ts.map +1 -0
- package/dist/{react/toUIMessages.js → UIMessages.js} +97 -4
- package/dist/UIMessages.js.map +1 -0
- package/dist/client/index.d.ts +9 -9
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +22 -19
- package/dist/client/index.js.map +1 -1
- package/dist/client/search.d.ts.map +1 -1
- package/dist/client/search.js +2 -0
- package/dist/client/search.js.map +1 -1
- package/dist/client/start.d.ts.map +1 -1
- package/dist/client/start.js +0 -1
- package/dist/client/start.js.map +1 -1
- package/dist/client/streaming.d.ts +23 -4
- package/dist/client/streaming.d.ts.map +1 -1
- package/dist/client/streaming.js +32 -6
- package/dist/client/streaming.js.map +1 -1
- package/dist/component/_generated/api.d.ts +2 -0
- package/dist/component/messages.js +1 -1
- package/dist/component/messages.js.map +1 -1
- package/dist/component/schema.d.ts +10 -4
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +4 -0
- package/dist/component/schema.js.map +1 -1
- package/dist/component/streams.d.ts +2 -4
- package/dist/component/streams.d.ts.map +1 -1
- package/dist/component/streams.js +42 -43
- package/dist/component/streams.js.map +1 -1
- package/dist/deltas.d.ts +48 -0
- package/dist/deltas.d.ts.map +1 -0
- package/dist/deltas.js +468 -0
- package/dist/deltas.js.map +1 -0
- package/dist/react/index.d.ts +4 -77
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +4 -178
- package/dist/react/index.js.map +1 -1
- package/dist/react/optimisticallySendMessage.d.ts +36 -3
- package/dist/react/optimisticallySendMessage.d.ts.map +1 -1
- package/dist/react/optimisticallySendMessage.js +37 -5
- package/dist/react/optimisticallySendMessage.js.map +1 -1
- package/dist/react/types.d.ts +4 -18
- package/dist/react/types.d.ts.map +1 -1
- package/dist/react/useStreamingUIMessages.d.ts +22 -0
- package/dist/react/useStreamingUIMessages.d.ts.map +1 -0
- package/dist/react/useStreamingUIMessages.js +204 -0
- package/dist/react/useStreamingUIMessages.js.map +1 -0
- package/dist/react/useThreadMessages.d.ts +103 -0
- package/dist/react/useThreadMessages.d.ts.map +1 -0
- package/dist/react/useThreadMessages.js +139 -0
- package/dist/react/useThreadMessages.js.map +1 -0
- package/dist/react/useUIMessages.d.ts +91 -0
- package/dist/react/useUIMessages.d.ts.map +1 -0
- package/dist/react/useUIMessages.js +115 -0
- package/dist/react/useUIMessages.js.map +1 -0
- package/dist/validators.d.ts +3 -1
- package/dist/validators.d.ts.map +1 -1
- package/dist/validators.js +1 -0
- package/dist/validators.js.map +1 -1
- package/package.json +1 -1
- package/src/{react/toUIMessages.ts → UIMessages.ts} +132 -15
- package/src/client/index.ts +30 -24
- package/src/client/search.ts +2 -0
- package/src/client/start.ts +0 -1
- package/src/client/streaming.test.ts +186 -0
- package/src/client/streaming.ts +50 -9
- package/src/component/_generated/api.d.ts +2 -0
- package/src/component/messages.ts +1 -1
- package/src/component/schema.ts +6 -0
- package/src/component/streams.ts +49 -66
- package/src/{react/deltas.test.ts → deltas.test.ts} +57 -46
- package/src/deltas.ts +589 -0
- package/src/{react/fromUIMessages.test.ts → fromUIMessages.test.ts} +69 -44
- package/src/react/index.ts +8 -250
- package/src/react/optimisticallySendMessage.ts +57 -8
- package/src/react/types.ts +6 -39
- package/src/react/useStreamingUIMessages.ts +294 -0
- package/src/react/useThreadMessages.ts +251 -0
- package/src/react/useUIMessages.ts +206 -0
- package/src/{react/toUIMessages.test.ts → toUIMessages.test.ts} +3 -3
- package/src/validators.ts +3 -0
- package/dist/client/textStreamParts.d.ts +0 -5
- package/dist/client/textStreamParts.d.ts.map +0 -1
- package/dist/client/textStreamParts.js +0 -44
- package/dist/client/textStreamParts.js.map +0 -1
- package/dist/react/deltas.d.ts +0 -27
- package/dist/react/deltas.d.ts.map +0 -1
- package/dist/react/deltas.js +0 -627
- package/dist/react/deltas.js.map +0 -1
- package/dist/react/fromUIMessages.d.ts +0 -13
- package/dist/react/fromUIMessages.d.ts.map +0 -1
- package/dist/react/fromUIMessages.js +0 -75
- package/dist/react/fromUIMessages.js.map +0 -1
- package/dist/react/toUIMessages.d.ts +0 -19
- package/dist/react/toUIMessages.d.ts.map +0 -1
- package/dist/react/toUIMessages.js.map +0 -1
- package/src/client/textStreamParts.ts +0 -62
- package/src/react/deltas.ts +0 -774
- package/src/react/fromUIMessages.ts +0 -90
package/src/client/streaming.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type ChunkDetector,
|
|
3
|
+
type AsyncIterableStream,
|
|
4
|
+
type StreamTextTransform,
|
|
5
|
+
type ToolSet,
|
|
6
|
+
smoothStream,
|
|
7
|
+
} from "ai";
|
|
2
8
|
import {
|
|
3
9
|
vStreamDelta,
|
|
4
10
|
vStreamMessage,
|
|
@@ -152,6 +158,36 @@ export const DEFAULT_STREAMING_OPTIONS = {
|
|
|
152
158
|
returnImmediately: false,
|
|
153
159
|
} satisfies StreamingOptions;
|
|
154
160
|
|
|
161
|
+
/**
|
|
162
|
+
*
|
|
163
|
+
* @param options The options passed to `agent.streamText` to decide whether to
|
|
164
|
+
* save deltas while streaming.
|
|
165
|
+
* @param existing The transforms passed to `agent.streamText` to merge with.
|
|
166
|
+
* @returns The merged transforms to pass to the underlying `streamText` call.
|
|
167
|
+
*/
|
|
168
|
+
export function mergeTransforms<TOOLS extends ToolSet>(
|
|
169
|
+
options: { chunking?: StreamingOptions["chunking"] } | boolean | undefined,
|
|
170
|
+
existing:
|
|
171
|
+
| StreamTextTransform<TOOLS>
|
|
172
|
+
| Array<StreamTextTransform<TOOLS>>
|
|
173
|
+
| undefined,
|
|
174
|
+
) {
|
|
175
|
+
if (!options) {
|
|
176
|
+
return existing;
|
|
177
|
+
}
|
|
178
|
+
const chunking =
|
|
179
|
+
typeof options === "boolean"
|
|
180
|
+
? DEFAULT_STREAMING_OPTIONS.chunking
|
|
181
|
+
: options.chunking;
|
|
182
|
+
const transforms = Array.isArray(existing)
|
|
183
|
+
? existing
|
|
184
|
+
: existing
|
|
185
|
+
? [existing]
|
|
186
|
+
: [];
|
|
187
|
+
transforms.push(smoothStream({ delayInMs: null, chunking }));
|
|
188
|
+
return transforms;
|
|
189
|
+
}
|
|
190
|
+
|
|
155
191
|
/**
|
|
156
192
|
* DeltaStreamer can be used to save a stream of "parts" by writing
|
|
157
193
|
* batches of them in "deltas" to the database so clients can subscribe
|
|
@@ -162,7 +198,7 @@ export const DEFAULT_STREAMING_OPTIONS = {
|
|
|
162
198
|
export class DeltaStreamer<T> {
|
|
163
199
|
public streamId: string | undefined;
|
|
164
200
|
public readonly config: {
|
|
165
|
-
|
|
201
|
+
throttleMs: number;
|
|
166
202
|
onAsyncAbort: (reason: string) => Promise<void>;
|
|
167
203
|
compress: ((parts: T[]) => T[]) | null;
|
|
168
204
|
};
|
|
@@ -176,7 +212,7 @@ export class DeltaStreamer<T> {
|
|
|
176
212
|
public readonly component: AgentComponent,
|
|
177
213
|
public readonly ctx: RunMutationCtx,
|
|
178
214
|
config: {
|
|
179
|
-
|
|
215
|
+
throttleMs: number | undefined;
|
|
180
216
|
onAsyncAbort: (reason: string) => Promise<void>;
|
|
181
217
|
abortSignal: AbortSignal | undefined;
|
|
182
218
|
compress: ((parts: T[]) => T[]) | null;
|
|
@@ -190,13 +226,11 @@ export class DeltaStreamer<T> {
|
|
|
190
226
|
model?: string;
|
|
191
227
|
provider?: string;
|
|
192
228
|
providerOptions?: ProviderOptions;
|
|
229
|
+
format: "UIMessageChunk" | "TextStreamPart" | undefined;
|
|
193
230
|
},
|
|
194
231
|
) {
|
|
195
232
|
this.config = {
|
|
196
|
-
|
|
197
|
-
config.stream === true
|
|
198
|
-
? DEFAULT_STREAMING_OPTIONS
|
|
199
|
-
: { ...DEFAULT_STREAMING_OPTIONS, ...config },
|
|
233
|
+
throttleMs: config.throttleMs ?? DEFAULT_STREAMING_OPTIONS.throttleMs,
|
|
200
234
|
onAsyncAbort: config.onAsyncAbort,
|
|
201
235
|
compress: config.compress,
|
|
202
236
|
};
|
|
@@ -232,12 +266,19 @@ export class DeltaStreamer<T> {
|
|
|
232
266
|
this.#nextParts.push(...parts);
|
|
233
267
|
if (
|
|
234
268
|
!this.#ongoingWrite &&
|
|
235
|
-
Date.now() - this.#latestWrite >= this.config.
|
|
269
|
+
Date.now() - this.#latestWrite >= this.config.throttleMs
|
|
236
270
|
) {
|
|
237
271
|
this.#ongoingWrite = this.#sendDelta();
|
|
238
272
|
}
|
|
239
273
|
}
|
|
240
274
|
|
|
275
|
+
public async consumeStream(stream: AsyncIterableStream<T>) {
|
|
276
|
+
for await (const chunk of stream) {
|
|
277
|
+
await this.addParts([chunk]);
|
|
278
|
+
}
|
|
279
|
+
await this.finish();
|
|
280
|
+
}
|
|
281
|
+
|
|
241
282
|
async #sendDelta() {
|
|
242
283
|
if (this.abortController.signal.aborted) {
|
|
243
284
|
return;
|
|
@@ -267,7 +308,7 @@ export class DeltaStreamer<T> {
|
|
|
267
308
|
// Now that we've sent the delta, check if we need to send another one.
|
|
268
309
|
if (
|
|
269
310
|
this.#nextParts.length > 0 &&
|
|
270
|
-
Date.now() - this.#latestWrite >= this.config.
|
|
311
|
+
Date.now() - this.#latestWrite >= this.config.throttleMs
|
|
271
312
|
) {
|
|
272
313
|
// We send again immediately with the accumulated deltas.
|
|
273
314
|
this.#ongoingWrite = this.#sendDelta();
|
|
@@ -2189,6 +2189,7 @@ export type Mounts = {
|
|
|
2189
2189
|
"public",
|
|
2190
2190
|
{
|
|
2191
2191
|
agentName?: string;
|
|
2192
|
+
format?: "UIMessageChunk" | "TextStreamPart";
|
|
2192
2193
|
model?: string;
|
|
2193
2194
|
order: number;
|
|
2194
2195
|
provider?: string;
|
|
@@ -2253,6 +2254,7 @@ export type Mounts = {
|
|
|
2253
2254
|
},
|
|
2254
2255
|
Array<{
|
|
2255
2256
|
agentName?: string;
|
|
2257
|
+
format?: "UIMessageChunk" | "TextStreamPart";
|
|
2256
2258
|
model?: string;
|
|
2257
2259
|
order: number;
|
|
2258
2260
|
provider?: string;
|
|
@@ -344,7 +344,7 @@ export const finalizeMessage = mutation({
|
|
|
344
344
|
const message = await ctx.db.get(messageId);
|
|
345
345
|
assert(message, `Message ${messageId} not found`);
|
|
346
346
|
if (message.status !== "pending") {
|
|
347
|
-
console.
|
|
347
|
+
console.debug(
|
|
348
348
|
"Trying to finalize a message that's already",
|
|
349
349
|
message.status,
|
|
350
350
|
);
|
package/src/component/schema.ts
CHANGED
|
@@ -91,6 +91,12 @@ export const schema = defineSchema({
|
|
|
91
91
|
model: v.optional(v.string()),
|
|
92
92
|
provider: v.optional(v.string()),
|
|
93
93
|
providerOptions: v.optional(vProviderOptions), // Sent to model
|
|
94
|
+
// The data format for the deltas. By default, we use UIMessageChunks.
|
|
95
|
+
// This format dictates how the messages are materialized for capturing
|
|
96
|
+
// partial messages during failure, as well as on the client side.
|
|
97
|
+
format: v.optional(
|
|
98
|
+
v.union(v.literal("UIMessageChunk"), v.literal("TextStreamPart")),
|
|
99
|
+
),
|
|
94
100
|
|
|
95
101
|
threadId: v.id("threads"),
|
|
96
102
|
order: v.number(),
|
package/src/component/streams.ts
CHANGED
|
@@ -20,8 +20,8 @@ import { stream } from "convex-helpers/server/stream";
|
|
|
20
20
|
import { mergedStream } from "convex-helpers/server/stream";
|
|
21
21
|
import { paginator } from "convex-helpers/server/pagination";
|
|
22
22
|
import type { WithoutSystemFields } from "convex/server";
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
23
|
+
import { deriveUIMessagesFromDeltas } from "../deltas.js";
|
|
24
|
+
import { fromUIMessages } from "../UIMessages.js";
|
|
25
25
|
|
|
26
26
|
const SECOND = 1000;
|
|
27
27
|
const MINUTE = 60 * SECOND;
|
|
@@ -140,6 +140,7 @@ export const list = query({
|
|
|
140
140
|
streamId: m._id,
|
|
141
141
|
status: m.state.kind,
|
|
142
142
|
...pick(m, [
|
|
143
|
+
"format",
|
|
143
144
|
"order",
|
|
144
145
|
"stepOrder",
|
|
145
146
|
"userId",
|
|
@@ -516,72 +517,54 @@ export async function getStreamingMessagesWithMetadata(
|
|
|
516
517
|
);
|
|
517
518
|
const messages = (
|
|
518
519
|
await Promise.all(
|
|
519
|
-
streamingMessages.map((
|
|
520
|
-
|
|
521
|
-
|
|
520
|
+
streamingMessages.map(async (streamingMessage) => {
|
|
521
|
+
const deltas = await ctx.db
|
|
522
|
+
.query("streamDeltas")
|
|
523
|
+
.withIndex("streamId_start_end", (q) =>
|
|
524
|
+
q.eq("streamId", streamingMessage._id),
|
|
525
|
+
)
|
|
526
|
+
.take(1000);
|
|
527
|
+
const uiMessages = await deriveUIMessagesFromDeltas(
|
|
522
528
|
threadId,
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
529
|
+
[
|
|
530
|
+
{
|
|
531
|
+
...streamingMessage,
|
|
532
|
+
status: "streaming",
|
|
533
|
+
streamId: streamingMessage._id,
|
|
534
|
+
},
|
|
535
|
+
],
|
|
536
|
+
deltas,
|
|
537
|
+
);
|
|
538
|
+
// We don't save messages that have already been saved
|
|
539
|
+
const numToSkip = stepOrder - streamingMessage.stepOrder;
|
|
540
|
+
const messages = await Promise.all(
|
|
541
|
+
fromUIMessages(uiMessages, streamingMessage)
|
|
542
|
+
.slice(numToSkip)
|
|
543
|
+
.filter((m) => m.message !== undefined)
|
|
544
|
+
.map(async (msg) => {
|
|
545
|
+
return {
|
|
546
|
+
...pick(msg, [
|
|
547
|
+
"message",
|
|
548
|
+
"fileIds",
|
|
549
|
+
"status",
|
|
550
|
+
"finishReason",
|
|
551
|
+
"model",
|
|
552
|
+
"provider",
|
|
553
|
+
"providerMetadata",
|
|
554
|
+
"sources",
|
|
555
|
+
"reasoning",
|
|
556
|
+
"reasoningDetails",
|
|
557
|
+
"usage",
|
|
558
|
+
"warnings",
|
|
559
|
+
"error",
|
|
560
|
+
]),
|
|
561
|
+
...metadata,
|
|
562
|
+
} as MessageWithMetadataInternal;
|
|
563
|
+
}),
|
|
564
|
+
);
|
|
565
|
+
return messages;
|
|
566
|
+
}),
|
|
528
567
|
)
|
|
529
568
|
).flat();
|
|
530
569
|
return messages;
|
|
531
570
|
}
|
|
532
|
-
|
|
533
|
-
export async function getMessagesWithMetadataForStreamingMessage(
|
|
534
|
-
ctx: MutationCtx,
|
|
535
|
-
threadId: Id<"threads">,
|
|
536
|
-
stepOrder: number,
|
|
537
|
-
streamingMessage: Doc<"streamingMessages">,
|
|
538
|
-
metadata: { status: "success" | "failed"; error?: string },
|
|
539
|
-
): Promise<MessageWithMetadataInternal[]> {
|
|
540
|
-
const deltas = await ctx.db
|
|
541
|
-
.query("streamDeltas")
|
|
542
|
-
.withIndex("streamId_start_end", (q) =>
|
|
543
|
-
q.eq("streamId", streamingMessage._id),
|
|
544
|
-
)
|
|
545
|
-
.take(1000);
|
|
546
|
-
const [messageDocs] = mergeDeltas(
|
|
547
|
-
threadId,
|
|
548
|
-
[
|
|
549
|
-
{
|
|
550
|
-
...streamingMessage,
|
|
551
|
-
status: "streaming",
|
|
552
|
-
streamId: streamingMessage._id,
|
|
553
|
-
},
|
|
554
|
-
],
|
|
555
|
-
[],
|
|
556
|
-
deltas,
|
|
557
|
-
);
|
|
558
|
-
// We don't save messages that have already been saved
|
|
559
|
-
const numToSkip = stepOrder - streamingMessage.stepOrder;
|
|
560
|
-
const messages = await Promise.all(
|
|
561
|
-
messageDocs
|
|
562
|
-
.slice(numToSkip)
|
|
563
|
-
.filter((m) => m.message !== undefined)
|
|
564
|
-
.map(async (msg) => {
|
|
565
|
-
const message = await serializeOrThrow(msg.message!);
|
|
566
|
-
return {
|
|
567
|
-
message,
|
|
568
|
-
...pick(msg, [
|
|
569
|
-
"fileIds",
|
|
570
|
-
"status",
|
|
571
|
-
"finishReason",
|
|
572
|
-
"model",
|
|
573
|
-
"provider",
|
|
574
|
-
"providerMetadata",
|
|
575
|
-
"sources",
|
|
576
|
-
"reasoning",
|
|
577
|
-
"reasoningDetails",
|
|
578
|
-
"usage",
|
|
579
|
-
"warnings",
|
|
580
|
-
"error",
|
|
581
|
-
]),
|
|
582
|
-
...metadata,
|
|
583
|
-
} as MessageWithMetadataInternal;
|
|
584
|
-
}),
|
|
585
|
-
);
|
|
586
|
-
return messages;
|
|
587
|
-
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
deriveUIMessagesFromTextStreamParts,
|
|
4
|
+
updateFromTextStreamParts,
|
|
5
|
+
} from "./deltas.js";
|
|
6
|
+
import type { StreamMessage, StreamDelta } from "./validators.js";
|
|
4
7
|
import { omit } from "convex-helpers";
|
|
5
|
-
import type { TextStreamPart, ToolSet } from "ai";
|
|
8
|
+
import type { TextStreamPart, ToolSet, ToolUIPart } from "ai";
|
|
6
9
|
|
|
7
10
|
function makeStreamMessage(
|
|
8
11
|
streamId: string,
|
|
@@ -30,7 +33,7 @@ describe("mergeDeltas", () => {
|
|
|
30
33
|
{ type: "text-delta", id: "1", text: "Hello" },
|
|
31
34
|
]),
|
|
32
35
|
];
|
|
33
|
-
const [messages, newStreams, changed] =
|
|
36
|
+
const [messages, newStreams, changed] = deriveUIMessagesFromTextStreamParts(
|
|
34
37
|
"thread1",
|
|
35
38
|
streamMessages,
|
|
36
39
|
[],
|
|
@@ -38,7 +41,7 @@ describe("mergeDeltas", () => {
|
|
|
38
41
|
);
|
|
39
42
|
expect(messages).toHaveLength(1);
|
|
40
43
|
expect(messages[0].text).toBe("Hello");
|
|
41
|
-
expect(messages[0].
|
|
44
|
+
expect(messages[0].role).toBe("assistant");
|
|
42
45
|
expect(changed).toBe(true);
|
|
43
46
|
expect(newStreams[0].cursor).toBe(5);
|
|
44
47
|
});
|
|
@@ -54,7 +57,7 @@ describe("mergeDeltas", () => {
|
|
|
54
57
|
{ type: "text-delta", id: "2", text: " World!" },
|
|
55
58
|
]),
|
|
56
59
|
];
|
|
57
|
-
const [messages, newStreams, changed] =
|
|
60
|
+
const [messages, newStreams, changed] = deriveUIMessagesFromTextStreamParts(
|
|
58
61
|
"thread1",
|
|
59
62
|
streamMessages,
|
|
60
63
|
[],
|
|
@@ -75,7 +78,7 @@ describe("mergeDeltas", () => {
|
|
|
75
78
|
type: "tool-call",
|
|
76
79
|
toolCallId: "call1",
|
|
77
80
|
toolName: "myTool",
|
|
78
|
-
input: "",
|
|
81
|
+
input: "What's the meaning of life?",
|
|
79
82
|
},
|
|
80
83
|
]),
|
|
81
84
|
makeDelta(streamId, 1, 2, [
|
|
@@ -88,28 +91,23 @@ describe("mergeDeltas", () => {
|
|
|
88
91
|
},
|
|
89
92
|
]),
|
|
90
93
|
];
|
|
91
|
-
const [
|
|
94
|
+
const [[message], _, changed] = deriveUIMessagesFromTextStreamParts(
|
|
92
95
|
"thread1",
|
|
93
96
|
streamMessages,
|
|
94
97
|
[],
|
|
95
98
|
deltas,
|
|
96
99
|
);
|
|
97
|
-
expect(
|
|
98
|
-
expect(
|
|
99
|
-
|
|
100
|
-
const content = messages[0].message?.content;
|
|
100
|
+
expect(message).toBeDefined();
|
|
101
|
+
expect(message.role).toBe("assistant");
|
|
102
|
+
const content = message.parts;
|
|
101
103
|
expect(content).toEqual([
|
|
102
|
-
{ type: "tool-call", toolCallId: "call1", toolName: "myTool", args: "" },
|
|
103
|
-
]);
|
|
104
|
-
expect(messages[1].message?.role).toBe("tool");
|
|
105
|
-
expect(messages[1].tool).toBe(true);
|
|
106
|
-
expect(messages[1].message?.content).toEqual([
|
|
107
104
|
{
|
|
108
|
-
type: "tool-
|
|
105
|
+
type: "tool-myTool",
|
|
109
106
|
toolCallId: "call1",
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
107
|
+
input: "What's the meaning of life?",
|
|
108
|
+
output: "42",
|
|
109
|
+
state: "output-available",
|
|
110
|
+
} satisfies ToolUIPart,
|
|
113
111
|
]);
|
|
114
112
|
expect(changed).toBe(true);
|
|
115
113
|
});
|
|
@@ -118,13 +116,12 @@ describe("mergeDeltas", () => {
|
|
|
118
116
|
const streamId = "s3";
|
|
119
117
|
const streamMessages = [makeStreamMessage(streamId, 3, 0)];
|
|
120
118
|
const deltas: StreamDelta[] = [];
|
|
121
|
-
const [
|
|
119
|
+
const [, newStreams, changed] = deriveUIMessagesFromTextStreamParts(
|
|
122
120
|
"thread1",
|
|
123
121
|
streamMessages,
|
|
124
122
|
[],
|
|
125
123
|
deltas,
|
|
126
124
|
);
|
|
127
|
-
expect(messages).toHaveLength(0);
|
|
128
125
|
expect(changed).toBe(false);
|
|
129
126
|
expect(newStreams[0].cursor).toBe(0);
|
|
130
127
|
});
|
|
@@ -136,7 +133,12 @@ describe("mergeDeltas", () => {
|
|
|
136
133
|
makeDelta("s2", 0, 3, [{ type: "text-delta", id: "1", text: "B" }]),
|
|
137
134
|
makeDelta("s1", 0, 3, [{ type: "text-delta", id: "2", text: "A" }]),
|
|
138
135
|
];
|
|
139
|
-
const [messages, _, changed] =
|
|
136
|
+
const [messages, _, changed] = deriveUIMessagesFromTextStreamParts(
|
|
137
|
+
"thread1",
|
|
138
|
+
[s2, s1],
|
|
139
|
+
[],
|
|
140
|
+
deltas,
|
|
141
|
+
);
|
|
140
142
|
expect(messages).toHaveLength(2);
|
|
141
143
|
expect(messages[0].text).toBe("A");
|
|
142
144
|
expect(messages[1].text).toBe("B");
|
|
@@ -158,11 +160,16 @@ describe("mergeDeltas", () => {
|
|
|
158
160
|
]),
|
|
159
161
|
makeDelta(streamId, 11, 12, [{ type: "text-delta", id: "3", text: "!" }]),
|
|
160
162
|
];
|
|
161
|
-
const [messages] =
|
|
163
|
+
const [messages] = deriveUIMessagesFromTextStreamParts(
|
|
164
|
+
"thread1",
|
|
165
|
+
streamMessages,
|
|
166
|
+
[],
|
|
167
|
+
deltas,
|
|
168
|
+
);
|
|
162
169
|
expect(messages).toHaveLength(1);
|
|
163
170
|
expect(messages[0].text).toBe("Hello World!!");
|
|
164
171
|
// There should only be one text part per message
|
|
165
|
-
const content = messages[0].
|
|
172
|
+
const content = messages[0].parts;
|
|
166
173
|
if (Array.isArray(content)) {
|
|
167
174
|
const textParts = content.filter((p) => p.type === "text");
|
|
168
175
|
expect(textParts).toHaveLength(1);
|
|
@@ -175,20 +182,28 @@ describe("mergeDeltas", () => {
|
|
|
175
182
|
const streamMessages = [makeStreamMessage(streamId, 6, 0)];
|
|
176
183
|
const deltas = [
|
|
177
184
|
makeDelta(streamId, 0, 1, [
|
|
185
|
+
{ type: "reasoning-start", id: "1" },
|
|
178
186
|
{ type: "reasoning-delta", id: "1", text: "I'm thinking..." },
|
|
179
187
|
]),
|
|
180
188
|
makeDelta(streamId, 1, 2, [
|
|
181
|
-
{ type: "reasoning-delta", id: "
|
|
189
|
+
{ type: "reasoning-delta", id: "1", text: " Still thinking..." },
|
|
182
190
|
]),
|
|
191
|
+
makeDelta(streamId, 2, 3, [{ type: "reasoning-end", id: "1" }]),
|
|
183
192
|
];
|
|
184
|
-
const [messages] =
|
|
193
|
+
const [messages] = deriveUIMessagesFromTextStreamParts(
|
|
194
|
+
"thread1",
|
|
195
|
+
streamMessages,
|
|
196
|
+
[],
|
|
197
|
+
deltas,
|
|
198
|
+
);
|
|
185
199
|
expect(messages).toHaveLength(1);
|
|
186
|
-
if (Array.isArray(messages[0].
|
|
187
|
-
const reasoningParts = messages[0].
|
|
200
|
+
if (Array.isArray(messages[0].parts)) {
|
|
201
|
+
const reasoningParts = messages[0].parts.filter(
|
|
188
202
|
(p) => p.type === "reasoning",
|
|
189
203
|
);
|
|
190
204
|
expect(reasoningParts).toHaveLength(1);
|
|
191
205
|
expect(reasoningParts[0].text).toBe("I'm thinking... Still thinking...");
|
|
206
|
+
expect(reasoningParts[0].state).toBe("done");
|
|
192
207
|
}
|
|
193
208
|
});
|
|
194
209
|
|
|
@@ -204,47 +219,43 @@ describe("mergeDeltas", () => {
|
|
|
204
219
|
]),
|
|
205
220
|
];
|
|
206
221
|
// First call: apply both deltas
|
|
207
|
-
let [result, changed] =
|
|
222
|
+
let [result, changed] = updateFromTextStreamParts(
|
|
208
223
|
"thread1",
|
|
209
224
|
streamMessage,
|
|
210
225
|
undefined,
|
|
211
226
|
deltas,
|
|
212
227
|
);
|
|
213
|
-
expect(result.
|
|
214
|
-
expect(result.messages[0].text).toBe("Hello World!");
|
|
228
|
+
expect(result.message.text).toBe("Hello World!");
|
|
215
229
|
// Second call: re-apply the same deltas (should not duplicate)
|
|
216
|
-
[result, changed] =
|
|
230
|
+
[result, changed] = updateFromTextStreamParts(
|
|
217
231
|
"thread1",
|
|
218
232
|
streamMessage,
|
|
219
233
|
result,
|
|
220
234
|
deltas,
|
|
221
235
|
);
|
|
222
|
-
expect(result.
|
|
223
|
-
expect(result.messages[0].text).toBe("Hello World!");
|
|
236
|
+
expect(result.message.text).toBe("Hello World!");
|
|
224
237
|
// Third call: add a new delta
|
|
225
238
|
const moreDeltas = [
|
|
226
239
|
...deltas,
|
|
227
240
|
makeDelta(streamId, 11, 12, [{ type: "text-delta", id: "3", text: "!" }]),
|
|
228
241
|
];
|
|
229
|
-
[result, changed] =
|
|
242
|
+
[result, changed] = updateFromTextStreamParts(
|
|
230
243
|
"thread1",
|
|
231
244
|
streamMessage,
|
|
232
245
|
result,
|
|
233
246
|
moreDeltas,
|
|
234
247
|
);
|
|
235
248
|
expect(changed).toBe(true);
|
|
236
|
-
expect(result.
|
|
237
|
-
expect(result.messages[0].text).toBe("Hello World!!");
|
|
249
|
+
expect(result.message.text).toBe("Hello World!!");
|
|
238
250
|
// Re-apply all deltas again (should still not duplicate)
|
|
239
|
-
[result, changed] =
|
|
251
|
+
[result, changed] = updateFromTextStreamParts(
|
|
240
252
|
"thread1",
|
|
241
253
|
streamMessage,
|
|
242
254
|
result,
|
|
243
255
|
moreDeltas,
|
|
244
256
|
);
|
|
245
257
|
expect(changed).toBe(false);
|
|
246
|
-
expect(result.
|
|
247
|
-
expect(result.messages[0].text).toBe("Hello World!!");
|
|
258
|
+
expect(result.message.text).toBe("Hello World!!");
|
|
248
259
|
});
|
|
249
260
|
|
|
250
261
|
it("mergeDeltas is pure and does not mutate inputs", () => {
|
|
@@ -270,13 +281,13 @@ describe("mergeDeltas", () => {
|
|
|
270
281
|
}
|
|
271
282
|
deepFreeze(streamMessages);
|
|
272
283
|
deepFreeze(deltas);
|
|
273
|
-
const [messages1, streams1, changed1] =
|
|
284
|
+
const [messages1, streams1, changed1] = deriveUIMessagesFromTextStreamParts(
|
|
274
285
|
"thread1",
|
|
275
286
|
streamMessages,
|
|
276
287
|
[],
|
|
277
288
|
deltas,
|
|
278
289
|
);
|
|
279
|
-
const [messages2, streams2, changed2] =
|
|
290
|
+
const [messages2, streams2, changed2] = deriveUIMessagesFromTextStreamParts(
|
|
280
291
|
"thread1",
|
|
281
292
|
streamMessages,
|
|
282
293
|
[],
|
|
@@ -288,12 +299,12 @@ describe("mergeDeltas", () => {
|
|
|
288
299
|
expect(
|
|
289
300
|
streams1.map((s) => ({
|
|
290
301
|
...s,
|
|
291
|
-
|
|
302
|
+
message: omit(s.message, ["_creationTime"]),
|
|
292
303
|
})),
|
|
293
304
|
).toEqual(
|
|
294
305
|
streams2.map((s) => ({
|
|
295
306
|
...s,
|
|
296
|
-
|
|
307
|
+
message: omit(s.message, ["_creationTime"]),
|
|
297
308
|
})),
|
|
298
309
|
);
|
|
299
310
|
expect(changed1).toBe(changed2);
|