@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.
Files changed (99) hide show
  1. package/dist/UIMessages.d.ts +43 -0
  2. package/dist/UIMessages.d.ts.map +1 -0
  3. package/dist/{react/toUIMessages.js → UIMessages.js} +97 -4
  4. package/dist/UIMessages.js.map +1 -0
  5. package/dist/client/index.d.ts +9 -9
  6. package/dist/client/index.d.ts.map +1 -1
  7. package/dist/client/index.js +22 -19
  8. package/dist/client/index.js.map +1 -1
  9. package/dist/client/search.d.ts.map +1 -1
  10. package/dist/client/search.js +2 -0
  11. package/dist/client/search.js.map +1 -1
  12. package/dist/client/start.d.ts.map +1 -1
  13. package/dist/client/start.js +0 -1
  14. package/dist/client/start.js.map +1 -1
  15. package/dist/client/streaming.d.ts +23 -4
  16. package/dist/client/streaming.d.ts.map +1 -1
  17. package/dist/client/streaming.js +32 -6
  18. package/dist/client/streaming.js.map +1 -1
  19. package/dist/component/_generated/api.d.ts +2 -0
  20. package/dist/component/messages.js +1 -1
  21. package/dist/component/messages.js.map +1 -1
  22. package/dist/component/schema.d.ts +10 -4
  23. package/dist/component/schema.d.ts.map +1 -1
  24. package/dist/component/schema.js +4 -0
  25. package/dist/component/schema.js.map +1 -1
  26. package/dist/component/streams.d.ts +2 -4
  27. package/dist/component/streams.d.ts.map +1 -1
  28. package/dist/component/streams.js +42 -43
  29. package/dist/component/streams.js.map +1 -1
  30. package/dist/deltas.d.ts +48 -0
  31. package/dist/deltas.d.ts.map +1 -0
  32. package/dist/deltas.js +468 -0
  33. package/dist/deltas.js.map +1 -0
  34. package/dist/react/index.d.ts +4 -77
  35. package/dist/react/index.d.ts.map +1 -1
  36. package/dist/react/index.js +4 -178
  37. package/dist/react/index.js.map +1 -1
  38. package/dist/react/optimisticallySendMessage.d.ts +36 -3
  39. package/dist/react/optimisticallySendMessage.d.ts.map +1 -1
  40. package/dist/react/optimisticallySendMessage.js +37 -5
  41. package/dist/react/optimisticallySendMessage.js.map +1 -1
  42. package/dist/react/types.d.ts +4 -18
  43. package/dist/react/types.d.ts.map +1 -1
  44. package/dist/react/useStreamingUIMessages.d.ts +22 -0
  45. package/dist/react/useStreamingUIMessages.d.ts.map +1 -0
  46. package/dist/react/useStreamingUIMessages.js +204 -0
  47. package/dist/react/useStreamingUIMessages.js.map +1 -0
  48. package/dist/react/useThreadMessages.d.ts +103 -0
  49. package/dist/react/useThreadMessages.d.ts.map +1 -0
  50. package/dist/react/useThreadMessages.js +139 -0
  51. package/dist/react/useThreadMessages.js.map +1 -0
  52. package/dist/react/useUIMessages.d.ts +91 -0
  53. package/dist/react/useUIMessages.d.ts.map +1 -0
  54. package/dist/react/useUIMessages.js +115 -0
  55. package/dist/react/useUIMessages.js.map +1 -0
  56. package/dist/validators.d.ts +3 -1
  57. package/dist/validators.d.ts.map +1 -1
  58. package/dist/validators.js +1 -0
  59. package/dist/validators.js.map +1 -1
  60. package/package.json +1 -1
  61. package/src/{react/toUIMessages.ts → UIMessages.ts} +132 -15
  62. package/src/client/index.ts +30 -24
  63. package/src/client/search.ts +2 -0
  64. package/src/client/start.ts +0 -1
  65. package/src/client/streaming.test.ts +186 -0
  66. package/src/client/streaming.ts +50 -9
  67. package/src/component/_generated/api.d.ts +2 -0
  68. package/src/component/messages.ts +1 -1
  69. package/src/component/schema.ts +6 -0
  70. package/src/component/streams.ts +49 -66
  71. package/src/{react/deltas.test.ts → deltas.test.ts} +57 -46
  72. package/src/deltas.ts +589 -0
  73. package/src/{react/fromUIMessages.test.ts → fromUIMessages.test.ts} +69 -44
  74. package/src/react/index.ts +8 -250
  75. package/src/react/optimisticallySendMessage.ts +57 -8
  76. package/src/react/types.ts +6 -39
  77. package/src/react/useStreamingUIMessages.ts +294 -0
  78. package/src/react/useThreadMessages.ts +251 -0
  79. package/src/react/useUIMessages.ts +206 -0
  80. package/src/{react/toUIMessages.test.ts → toUIMessages.test.ts} +3 -3
  81. package/src/validators.ts +3 -0
  82. package/dist/client/textStreamParts.d.ts +0 -5
  83. package/dist/client/textStreamParts.d.ts.map +0 -1
  84. package/dist/client/textStreamParts.js +0 -44
  85. package/dist/client/textStreamParts.js.map +0 -1
  86. package/dist/react/deltas.d.ts +0 -27
  87. package/dist/react/deltas.d.ts.map +0 -1
  88. package/dist/react/deltas.js +0 -627
  89. package/dist/react/deltas.js.map +0 -1
  90. package/dist/react/fromUIMessages.d.ts +0 -13
  91. package/dist/react/fromUIMessages.d.ts.map +0 -1
  92. package/dist/react/fromUIMessages.js +0 -75
  93. package/dist/react/fromUIMessages.js.map +0 -1
  94. package/dist/react/toUIMessages.d.ts +0 -19
  95. package/dist/react/toUIMessages.d.ts.map +0 -1
  96. package/dist/react/toUIMessages.js.map +0 -1
  97. package/src/client/textStreamParts.ts +0 -62
  98. package/src/react/deltas.ts +0 -774
  99. package/src/react/fromUIMessages.ts +0 -90
@@ -1,4 +1,10 @@
1
- import { type ChunkDetector } from "ai";
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
- stream: Required<StreamingOptions>;
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
- stream: true | StreamingOptions;
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
- stream:
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.stream.throttleMs
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.stream.throttleMs
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.log(
347
+ console.debug(
348
348
  "Trying to finalize a message that's already",
349
349
  message.status,
350
350
  );
@@ -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(),
@@ -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 { mergeDeltas } from "../react/deltas.js";
24
- import { serializeOrThrow } from "../mapping.js";
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((m) =>
520
- getMessagesWithMetadataForStreamingMessage(
521
- ctx,
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
- stepOrder,
524
- m,
525
- metadata,
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 { mergeDeltas, applyDeltasToStreamMessage } from "./deltas.js";
3
- import type { StreamMessage, StreamDelta } from "../validators.js";
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] = mergeDeltas(
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].message?.role).toBe("assistant");
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] = mergeDeltas(
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 [messages, _, changed] = mergeDeltas(
94
+ const [[message], _, changed] = deriveUIMessagesFromTextStreamParts(
92
95
  "thread1",
93
96
  streamMessages,
94
97
  [],
95
98
  deltas,
96
99
  );
97
- expect(messages).toHaveLength(2);
98
- expect(messages[0].message?.role).toBe("assistant");
99
- expect(messages[0].tool).toBe(true);
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-result",
105
+ type: "tool-myTool",
109
106
  toolCallId: "call1",
110
- toolName: "myTool",
111
- result: "42",
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 [messages, newStreams, changed] = mergeDeltas(
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] = mergeDeltas("thread1", [s2, s1], [], deltas);
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] = mergeDeltas("thread1", streamMessages, [], deltas);
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].message?.content;
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: "2", text: " Still thinking..." },
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] = mergeDeltas("thread1", streamMessages, [], deltas);
193
+ const [messages] = deriveUIMessagesFromTextStreamParts(
194
+ "thread1",
195
+ streamMessages,
196
+ [],
197
+ deltas,
198
+ );
185
199
  expect(messages).toHaveLength(1);
186
- if (Array.isArray(messages[0].message?.content)) {
187
- const reasoningParts = messages[0].message.content.filter(
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] = applyDeltasToStreamMessage(
222
+ let [result, changed] = updateFromTextStreamParts(
208
223
  "thread1",
209
224
  streamMessage,
210
225
  undefined,
211
226
  deltas,
212
227
  );
213
- expect(result.messages).toHaveLength(1);
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] = applyDeltasToStreamMessage(
230
+ [result, changed] = updateFromTextStreamParts(
217
231
  "thread1",
218
232
  streamMessage,
219
233
  result,
220
234
  deltas,
221
235
  );
222
- expect(result.messages).toHaveLength(1);
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] = applyDeltasToStreamMessage(
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.messages).toHaveLength(1);
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] = applyDeltasToStreamMessage(
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.messages).toHaveLength(1);
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] = mergeDeltas(
284
+ const [messages1, streams1, changed1] = deriveUIMessagesFromTextStreamParts(
274
285
  "thread1",
275
286
  streamMessages,
276
287
  [],
277
288
  deltas,
278
289
  );
279
- const [messages2, streams2, changed2] = mergeDeltas(
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
- messages: s.messages.map((m) => omit(m, ["_creationTime"])),
302
+ message: omit(s.message, ["_creationTime"]),
292
303
  })),
293
304
  ).toEqual(
294
305
  streams2.map((s) => ({
295
306
  ...s,
296
- messages: s.messages.map((m) => omit(m, ["_creationTime"])),
307
+ message: omit(s.message, ["_creationTime"]),
297
308
  })),
298
309
  );
299
310
  expect(changed1).toBe(changed2);