@convex-dev/agent 0.1.3 → 0.1.4

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 (108) hide show
  1. package/dist/commonjs/client/index.d.ts +6 -5
  2. package/dist/commonjs/client/index.d.ts.map +1 -1
  3. package/dist/commonjs/client/index.js +37 -11
  4. package/dist/commonjs/client/index.js.map +1 -1
  5. package/dist/commonjs/client/streaming.d.ts +2 -2
  6. package/dist/commonjs/client/streaming.d.ts.map +1 -1
  7. package/dist/commonjs/client/streaming.js +2 -1
  8. package/dist/commonjs/client/streaming.js.map +1 -1
  9. package/dist/commonjs/component/messages.d.ts +3 -3
  10. package/dist/commonjs/component/messages.d.ts.map +1 -1
  11. package/dist/commonjs/component/messages.js +16 -14
  12. package/dist/commonjs/component/messages.js.map +1 -1
  13. package/dist/commonjs/component/streams.d.ts.map +1 -1
  14. package/dist/commonjs/component/streams.js +1 -4
  15. package/dist/commonjs/component/streams.js.map +1 -1
  16. package/dist/commonjs/component/users.d.ts +3 -0
  17. package/dist/commonjs/component/users.d.ts.map +1 -1
  18. package/dist/commonjs/component/users.js +11 -1
  19. package/dist/commonjs/component/users.js.map +1 -1
  20. package/dist/commonjs/component/vector/index.d.ts +3 -1
  21. package/dist/commonjs/component/vector/index.d.ts.map +1 -1
  22. package/dist/commonjs/component/vector/index.js +6 -2
  23. package/dist/commonjs/component/vector/index.js.map +1 -1
  24. package/dist/commonjs/react/deltas.d.ts +29 -0
  25. package/dist/commonjs/react/deltas.d.ts.map +1 -0
  26. package/dist/commonjs/react/deltas.js +267 -0
  27. package/dist/commonjs/react/deltas.js.map +1 -0
  28. package/dist/commonjs/react/index.d.ts +4 -49
  29. package/dist/commonjs/react/index.d.ts.map +1 -1
  30. package/dist/commonjs/react/index.js +15 -446
  31. package/dist/commonjs/react/index.js.map +1 -1
  32. package/dist/commonjs/react/optimisticallySendMessage.d.ts +8 -0
  33. package/dist/commonjs/react/optimisticallySendMessage.d.ts.map +1 -0
  34. package/dist/commonjs/react/optimisticallySendMessage.js +40 -0
  35. package/dist/commonjs/react/optimisticallySendMessage.js.map +1 -0
  36. package/dist/commonjs/react/toUIMessages.d.ts.map +1 -1
  37. package/dist/commonjs/react/toUIMessages.js +4 -2
  38. package/dist/commonjs/react/toUIMessages.js.map +1 -1
  39. package/dist/commonjs/react/types.d.ts +26 -0
  40. package/dist/commonjs/react/types.d.ts.map +1 -0
  41. package/dist/commonjs/react/types.js +2 -0
  42. package/dist/commonjs/react/types.js.map +1 -0
  43. package/dist/commonjs/react/useSmoothText.d.ts +20 -0
  44. package/dist/commonjs/react/useSmoothText.d.ts.map +1 -0
  45. package/dist/commonjs/react/useSmoothText.js +50 -0
  46. package/dist/commonjs/react/useSmoothText.js.map +1 -0
  47. package/dist/esm/client/index.d.ts +6 -5
  48. package/dist/esm/client/index.d.ts.map +1 -1
  49. package/dist/esm/client/index.js +37 -11
  50. package/dist/esm/client/index.js.map +1 -1
  51. package/dist/esm/client/streaming.d.ts +2 -2
  52. package/dist/esm/client/streaming.d.ts.map +1 -1
  53. package/dist/esm/client/streaming.js +2 -1
  54. package/dist/esm/client/streaming.js.map +1 -1
  55. package/dist/esm/component/messages.d.ts +3 -3
  56. package/dist/esm/component/messages.d.ts.map +1 -1
  57. package/dist/esm/component/messages.js +16 -14
  58. package/dist/esm/component/messages.js.map +1 -1
  59. package/dist/esm/component/streams.d.ts.map +1 -1
  60. package/dist/esm/component/streams.js +1 -4
  61. package/dist/esm/component/streams.js.map +1 -1
  62. package/dist/esm/component/users.d.ts +3 -0
  63. package/dist/esm/component/users.d.ts.map +1 -1
  64. package/dist/esm/component/users.js +11 -1
  65. package/dist/esm/component/users.js.map +1 -1
  66. package/dist/esm/component/vector/index.d.ts +3 -1
  67. package/dist/esm/component/vector/index.d.ts.map +1 -1
  68. package/dist/esm/component/vector/index.js +6 -2
  69. package/dist/esm/component/vector/index.js.map +1 -1
  70. package/dist/esm/react/deltas.d.ts +29 -0
  71. package/dist/esm/react/deltas.d.ts.map +1 -0
  72. package/dist/esm/react/deltas.js +267 -0
  73. package/dist/esm/react/deltas.js.map +1 -0
  74. package/dist/esm/react/index.d.ts +4 -49
  75. package/dist/esm/react/index.d.ts.map +1 -1
  76. package/dist/esm/react/index.js +15 -446
  77. package/dist/esm/react/index.js.map +1 -1
  78. package/dist/esm/react/optimisticallySendMessage.d.ts +8 -0
  79. package/dist/esm/react/optimisticallySendMessage.d.ts.map +1 -0
  80. package/dist/esm/react/optimisticallySendMessage.js +40 -0
  81. package/dist/esm/react/optimisticallySendMessage.js.map +1 -0
  82. package/dist/esm/react/toUIMessages.d.ts.map +1 -1
  83. package/dist/esm/react/toUIMessages.js +4 -2
  84. package/dist/esm/react/toUIMessages.js.map +1 -1
  85. package/dist/esm/react/types.d.ts +26 -0
  86. package/dist/esm/react/types.d.ts.map +1 -0
  87. package/dist/esm/react/types.js +2 -0
  88. package/dist/esm/react/types.js.map +1 -0
  89. package/dist/esm/react/useSmoothText.d.ts +20 -0
  90. package/dist/esm/react/useSmoothText.d.ts.map +1 -0
  91. package/dist/esm/react/useSmoothText.js +50 -0
  92. package/dist/esm/react/useSmoothText.js.map +1 -0
  93. package/package.json +4 -1
  94. package/src/client/index.ts +41 -23
  95. package/src/client/streaming.ts +3 -2
  96. package/src/component/_generated/api.d.ts +2 -2
  97. package/src/component/messages.ts +26 -15
  98. package/src/component/streams.ts +3 -4
  99. package/src/component/users.ts +12 -0
  100. package/src/component/vector/index.ts +9 -3
  101. package/src/react/deltas.test.ts +298 -0
  102. package/src/react/deltas.ts +340 -0
  103. package/src/react/index.ts +26 -589
  104. package/src/react/optimisticallySendMessage.ts +46 -0
  105. package/src/react/toUIMessages.test.ts +301 -0
  106. package/src/react/toUIMessages.ts +6 -2
  107. package/src/react/types.ts +52 -0
  108. package/src/react/useSmoothText.ts +76 -0
@@ -0,0 +1,298 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { mergeDeltas, applyDeltasToStreamMessage } from "./deltas";
3
+ import type { StreamMessage, StreamDelta, TextStreamPart } from "../validators";
4
+
5
+ function makeStreamMessage(
6
+ streamId: string,
7
+ order: number,
8
+ stepOrder: number
9
+ ): StreamMessage {
10
+ return {
11
+ streamId,
12
+ order,
13
+ stepOrder,
14
+ } as StreamMessage;
15
+ }
16
+
17
+ function makeDelta(
18
+ streamId: string,
19
+ start: number,
20
+ end: number,
21
+ parts: TextStreamPart[]
22
+ ): StreamDelta {
23
+ return {
24
+ streamId,
25
+ start,
26
+ end,
27
+ parts,
28
+ };
29
+ }
30
+
31
+ describe("mergeDeltas", () => {
32
+ it("merges a single text-delta into a message", () => {
33
+ const streamId = "s1";
34
+ const streamMessages = [makeStreamMessage(streamId, 1, 0)];
35
+ const deltas = [
36
+ makeDelta(streamId, 0, 5, [{ type: "text-delta", textDelta: "Hello" }]),
37
+ ];
38
+ const [messages, newStreams, changed] = mergeDeltas(
39
+ "thread1",
40
+ streamMessages,
41
+ [],
42
+ deltas
43
+ );
44
+ expect(messages).toHaveLength(1);
45
+ expect(messages[0].text).toBe("Hello");
46
+ expect(messages[0].message?.role).toBe("assistant");
47
+ expect(changed).toBe(true);
48
+ expect(newStreams[0].cursor).toBe(5);
49
+ });
50
+
51
+ it("merges multiple deltas for the same stream", () => {
52
+ const streamId = "s1";
53
+ const streamMessages = [makeStreamMessage(streamId, 1, 0)];
54
+ const deltas = [
55
+ makeDelta(streamId, 0, 5, [{ type: "text-delta", textDelta: "Hello" }]),
56
+ makeDelta(streamId, 5, 11, [
57
+ { type: "text-delta", textDelta: " World!" },
58
+ ]),
59
+ ];
60
+ const [messages, newStreams, changed] = mergeDeltas(
61
+ "thread1",
62
+ streamMessages,
63
+ [],
64
+ deltas
65
+ );
66
+ expect(messages).toHaveLength(1);
67
+ expect(messages[0].text).toBe("Hello World!");
68
+ expect(changed).toBe(true);
69
+ expect(newStreams[0].cursor).toBe(11);
70
+ });
71
+
72
+ it("handles tool-call and tool-result parts", () => {
73
+ const streamId = "s2";
74
+ const streamMessages = [makeStreamMessage(streamId, 2, 0)];
75
+ const deltas = [
76
+ makeDelta(streamId, 0, 1, [
77
+ {
78
+ type: "tool-call",
79
+ toolCallId: "call1",
80
+ toolName: "myTool",
81
+ args: "",
82
+ },
83
+ ]),
84
+ makeDelta(streamId, 1, 2, [
85
+ {
86
+ type: "tool-result",
87
+ toolCallId: "call1",
88
+ toolName: "myTool",
89
+ result: "42",
90
+ },
91
+ ]),
92
+ ];
93
+ const [messages, _, changed] = mergeDeltas(
94
+ "thread1",
95
+ streamMessages,
96
+ [],
97
+ deltas
98
+ );
99
+ expect(messages).toHaveLength(2);
100
+ expect(messages[0].message?.role).toBe("assistant");
101
+ expect(messages[0].tool).toBe(true);
102
+ const content = messages[0].message?.content;
103
+ expect(content).toEqual([
104
+ {
105
+ type: "tool-call",
106
+ toolCallId: "call1",
107
+ toolName: "myTool",
108
+ args: "",
109
+ },
110
+ ]);
111
+ expect(messages[1].message?.role).toBe("tool");
112
+ expect(messages[1].tool).toBe(true);
113
+ expect(messages[1].message?.content).toEqual([
114
+ {
115
+ type: "tool-result",
116
+ toolCallId: "call1",
117
+ toolName: "myTool",
118
+ result: "42",
119
+ },
120
+ ]);
121
+ expect(changed).toBe(true);
122
+ });
123
+
124
+ it("returns changed=false if no new deltas", () => {
125
+ const streamId = "s3";
126
+ const streamMessages = [makeStreamMessage(streamId, 3, 0)];
127
+ const deltas: StreamDelta[] = [];
128
+ const [messages, newStreams, changed] = mergeDeltas(
129
+ "thread1",
130
+ streamMessages,
131
+ [],
132
+ deltas
133
+ );
134
+ expect(messages).toHaveLength(0);
135
+ expect(changed).toBe(false);
136
+ expect(newStreams[0].cursor).toBe(0);
137
+ });
138
+
139
+ it("handles multiple streams and sorts by order/stepOrder", () => {
140
+ const s1 = makeStreamMessage("s1", 1, 0);
141
+ const s2 = makeStreamMessage("s2", 2, 0);
142
+ const deltas = [
143
+ makeDelta("s2", 0, 3, [{ type: "text-delta", textDelta: "B" }]),
144
+ makeDelta("s1", 0, 3, [{ type: "text-delta", textDelta: "A" }]),
145
+ ];
146
+ const [messages, _, changed] = mergeDeltas("thread1", [s2, s1], [], deltas);
147
+ expect(messages).toHaveLength(2);
148
+ expect(messages[0].text).toBe("A");
149
+ expect(messages[1].text).toBe("B");
150
+ expect(changed).toBe(true);
151
+ // Sorted by order
152
+ expect(messages[0].order).toBe(1);
153
+ expect(messages[1].order).toBe(2);
154
+ });
155
+
156
+ it("does not duplicate text content when merging sequential text-deltas", () => {
157
+ const streamId = "s4";
158
+ const streamMessages = [makeStreamMessage(streamId, 4, 0)];
159
+ const deltas = [
160
+ makeDelta(streamId, 0, 5, [{ type: "text-delta", textDelta: "Hello" }]),
161
+ makeDelta(streamId, 5, 11, [
162
+ { type: "text-delta", textDelta: " World!" },
163
+ ]),
164
+ makeDelta(streamId, 11, 12, [{ type: "text-delta", textDelta: "!" }]),
165
+ ];
166
+ const [messages] = mergeDeltas("thread1", streamMessages, [], deltas);
167
+ expect(messages).toHaveLength(1);
168
+ expect(messages[0].text).toBe("Hello World!!");
169
+ // There should only be one text part per message
170
+ const content = messages[0].message?.content;
171
+ if (Array.isArray(content)) {
172
+ const textParts = content.filter((p) => p.type === "text");
173
+ expect(textParts).toHaveLength(1);
174
+ expect(textParts[0].text).toBe("Hello World!!");
175
+ }
176
+ });
177
+
178
+ it("does not duplicate reasoning parts", () => {
179
+ const streamId = "s6";
180
+ const streamMessages = [makeStreamMessage(streamId, 6, 0)];
181
+ const deltas = [
182
+ makeDelta(streamId, 0, 1, [
183
+ { type: "reasoning", textDelta: "I'm thinking..." },
184
+ ]),
185
+ makeDelta(streamId, 1, 2, [
186
+ { type: "reasoning", textDelta: " Still thinking..." },
187
+ ]),
188
+ ];
189
+ const [messages] = mergeDeltas("thread1", streamMessages, [], deltas);
190
+ expect(messages).toHaveLength(1);
191
+ if (Array.isArray(messages[0].message?.content)) {
192
+ const reasoningParts = messages[0].message.content.filter(
193
+ (p) => p.type === "reasoning"
194
+ );
195
+ expect(reasoningParts).toHaveLength(1);
196
+ expect(reasoningParts[0].text).toBe("I'm thinking... Still thinking...");
197
+ }
198
+ });
199
+
200
+ it("applyDeltasToStreamMessage is idempotent and does not duplicate content", () => {
201
+ const streamId = "s7";
202
+ const streamMessage = makeStreamMessage(streamId, 7, 0);
203
+ const deltas = [
204
+ makeDelta(streamId, 0, 5, [{ type: "text-delta", textDelta: "Hello" }]),
205
+ makeDelta(streamId, 5, 11, [
206
+ { type: "text-delta", textDelta: " World!" },
207
+ ]),
208
+ ];
209
+ // First call: apply both deltas
210
+ let [result, changed] = applyDeltasToStreamMessage(
211
+ "thread1",
212
+ streamMessage,
213
+ undefined,
214
+ deltas
215
+ );
216
+ expect(result.messages).toHaveLength(1);
217
+ expect(result.messages[0].text).toBe("Hello World!");
218
+ // Second call: re-apply the same deltas (should not duplicate)
219
+ [result, changed] = applyDeltasToStreamMessage(
220
+ "thread1",
221
+ streamMessage,
222
+ result,
223
+ deltas
224
+ );
225
+ expect(result.messages).toHaveLength(1);
226
+ expect(result.messages[0].text).toBe("Hello World!");
227
+ // Third call: add a new delta
228
+ const moreDeltas = [
229
+ ...deltas,
230
+ makeDelta(streamId, 11, 12, [{ type: "text-delta", textDelta: "!" }]),
231
+ ];
232
+ [result, changed] = applyDeltasToStreamMessage(
233
+ "thread1",
234
+ streamMessage,
235
+ result,
236
+ moreDeltas
237
+ );
238
+ expect(changed).toBe(true);
239
+ expect(result.messages).toHaveLength(1);
240
+ expect(result.messages[0].text).toBe("Hello World!!");
241
+ // Re-apply all deltas again (should still not duplicate)
242
+ [result, changed] = applyDeltasToStreamMessage(
243
+ "thread1",
244
+ streamMessage,
245
+ result,
246
+ moreDeltas
247
+ );
248
+ expect(changed).toBe(false);
249
+ expect(result.messages).toHaveLength(1);
250
+ expect(result.messages[0].text).toBe("Hello World!!");
251
+ });
252
+
253
+ it("mergeDeltas is pure and does not mutate inputs", () => {
254
+ const streamId = "s8";
255
+ const streamMessages = [makeStreamMessage(streamId, 8, 0)];
256
+ const deltas = [
257
+ makeDelta(streamId, 0, 5, [{ type: "text-delta", textDelta: "Hello" }]),
258
+ makeDelta(streamId, 5, 11, [
259
+ { type: "text-delta", textDelta: " World!" },
260
+ ]),
261
+ ];
262
+ // Deep freeze inputs to catch mutation
263
+ function deepFreeze(obj: unknown): unknown {
264
+ if (obj && typeof obj === "object" && !Object.isFrozen(obj)) {
265
+ Object.freeze(obj);
266
+ for (const key of Object.keys(obj)) {
267
+ deepFreeze((obj as Record<string, unknown>)[key]);
268
+ }
269
+ }
270
+ return obj;
271
+ }
272
+ deepFreeze(streamMessages);
273
+ deepFreeze(deltas);
274
+ const [messages1, streams1, changed1] = mergeDeltas(
275
+ "thread1",
276
+ streamMessages,
277
+ [],
278
+ deltas
279
+ );
280
+ const [messages2, streams2, changed2] = mergeDeltas(
281
+ "thread1",
282
+ streamMessages,
283
+ [],
284
+ deltas
285
+ );
286
+ expect(messages1).toEqual(messages2);
287
+ expect(streams1).toEqual(streams2);
288
+ expect(changed1).toBe(changed2);
289
+ // Inputs should remain unchanged
290
+ expect(streamMessages).toEqual([makeStreamMessage(streamId, 8, 0)]);
291
+ expect(deltas).toEqual([
292
+ makeDelta(streamId, 0, 5, [{ type: "text-delta", textDelta: "Hello" }]),
293
+ makeDelta(streamId, 5, 11, [
294
+ { type: "text-delta", textDelta: " World!" },
295
+ ]),
296
+ ]);
297
+ });
298
+ });
@@ -0,0 +1,340 @@
1
+ import type { TextPart, ToolCallPart, ToolResultPart } from "ai";
2
+ import type { MessageDoc } from "../client";
3
+ import type {
4
+ Message,
5
+ StreamDelta,
6
+ StreamMessage,
7
+ TextStreamPart,
8
+ } from "../validators";
9
+ import type { UIMessage } from "./toUIMessages";
10
+ import { toUIMessages } from "./toUIMessages";
11
+
12
+ export { toUIMessages, type UIMessage };
13
+
14
+ export function mergeDeltas(
15
+ threadId: string,
16
+ streamMessages: StreamMessage[],
17
+ existingStreams: Array<{
18
+ streamId: string;
19
+ cursor: number;
20
+ messages: MessageDoc[];
21
+ }>,
22
+ allDeltas: StreamDelta[]
23
+ ): [
24
+ MessageDoc[],
25
+ Array<{ streamId: string; cursor: number; messages: MessageDoc[] }>,
26
+ boolean,
27
+ ] {
28
+ const newStreams: Array<{
29
+ streamId: string;
30
+ cursor: number;
31
+ messages: MessageDoc[];
32
+ }> = [];
33
+ // Seed the existing chunks
34
+ let changed = false;
35
+ for (const streamMessage of streamMessages) {
36
+ const deltas = allDeltas.filter(
37
+ (d) => d.streamId === streamMessage.streamId
38
+ );
39
+ const existing = existingStreams.find(
40
+ (s) => s.streamId === streamMessage.streamId
41
+ );
42
+ const [newStream, messageChanged] = applyDeltasToStreamMessage(
43
+ threadId,
44
+ streamMessage,
45
+ existing,
46
+ deltas
47
+ );
48
+ newStreams.push(newStream);
49
+ if (messageChanged) changed = true;
50
+ }
51
+ for (const { streamId } of existingStreams) {
52
+ if (!newStreams.find((s) => s.streamId === streamId)) {
53
+ // There's a stream that's no longer active.
54
+ changed = true;
55
+ }
56
+ }
57
+ const messages = newStreams
58
+ .map((s) => s.messages)
59
+ .flat()
60
+ .sort((a, b) => a.order - b.order || a.stepOrder - b.stepOrder);
61
+ return [messages, newStreams, changed];
62
+ }
63
+
64
+ // exported for testing
65
+ export function applyDeltasToStreamMessage(
66
+ threadId: string,
67
+ streamMessage: StreamMessage,
68
+ existing:
69
+ | { streamId: string; cursor: number; messages: MessageDoc[] }
70
+ | undefined,
71
+ deltas: StreamDelta[]
72
+ ): [{ streamId: string; cursor: number; messages: MessageDoc[] }, boolean] {
73
+ let changed = false;
74
+ let cursor = existing?.cursor ?? 0;
75
+ let parts: TextStreamPart[] = [];
76
+ for (const delta of deltas.sort((a, b) => a.start - b.start)) {
77
+ if (delta.parts.length === 0) {
78
+ console.warn(`Got delta with no parts: ${JSON.stringify(delta)}`);
79
+ continue;
80
+ }
81
+ if (cursor !== delta.start) {
82
+ if (cursor >= delta.end) {
83
+ console.debug(
84
+ `Got duplicate delta for stream ${delta.streamId} at ${delta.start}`
85
+ );
86
+ continue;
87
+ } else if (cursor < delta.start) {
88
+ console.warn(
89
+ `Got delta for stream ${delta.streamId} that has a gap ${cursor} -> ${delta.start}`
90
+ );
91
+ continue;
92
+ } else {
93
+ throw new Error(
94
+ `Got unexpected delta for stream ${delta.streamId}: delta: ${delta.start} -> ${delta.end} existing cursor: ${cursor}`
95
+ );
96
+ }
97
+ }
98
+ changed = true;
99
+ cursor = delta.end;
100
+ parts.push(...delta.parts);
101
+ }
102
+ if (!changed) {
103
+ return [
104
+ existing ?? { streamId: streamMessage.streamId, cursor, messages: [] },
105
+ false,
106
+ ];
107
+ }
108
+
109
+ const existingMessages = existing?.messages ?? [];
110
+
111
+ let currentMessage: MessageDoc;
112
+ if (existingMessages.length > 0) {
113
+ // replace the last message with a new one
114
+ const lastMessage = existingMessages.at(-1)!;
115
+ currentMessage = {
116
+ ...lastMessage,
117
+ message: cloneMessageAndContent(lastMessage.message),
118
+ };
119
+ } else {
120
+ const newMessage = createStreamingMessage(
121
+ threadId,
122
+ streamMessage,
123
+ parts[0]!,
124
+ existingMessages.length
125
+ );
126
+ parts = parts.slice(1);
127
+ currentMessage = newMessage;
128
+ }
129
+ const newStream = {
130
+ streamId: streamMessage.streamId,
131
+ cursor,
132
+ messages: [...existingMessages.slice(0, -1), currentMessage],
133
+ };
134
+ let lastContent = getLastContent(currentMessage);
135
+ for (const part of parts) {
136
+ let contentToAdd:
137
+ | TextPart
138
+ | ToolCallPart
139
+ | { type: "reasoning"; text: string }
140
+ | ToolResultPart
141
+ | undefined;
142
+ const isToolRole = part.type === "source" || part.type === "tool-result";
143
+ if (isToolRole !== (currentMessage.message!.role === "tool")) {
144
+ currentMessage = createStreamingMessage(
145
+ threadId,
146
+ streamMessage,
147
+ part,
148
+ newStream.messages.length
149
+ );
150
+ lastContent = getLastContent(currentMessage);
151
+ newStream.messages.push(currentMessage);
152
+ continue;
153
+ }
154
+ switch (part.type) {
155
+ case "text-delta":
156
+ currentMessage.text += part.textDelta;
157
+ if (lastContent?.type === "text") {
158
+ lastContent.text += part.textDelta;
159
+ } else {
160
+ contentToAdd = {
161
+ type: "text",
162
+ text: part.textDelta,
163
+ };
164
+ }
165
+ break;
166
+ case "tool-call-streaming-start":
167
+ currentMessage.tool = true;
168
+ contentToAdd = {
169
+ type: "tool-call",
170
+ toolCallId: part.toolCallId,
171
+ toolName: part.toolName,
172
+ args: "",
173
+ };
174
+ break;
175
+ case "tool-call-delta":
176
+ {
177
+ currentMessage.tool = true;
178
+ if (lastContent?.type !== "tool-call") {
179
+ throw new Error("Expected last content to be a tool call");
180
+ }
181
+ if (typeof lastContent.args !== "string") {
182
+ throw new Error("Expected args to be a string");
183
+ }
184
+ lastContent.args += part.argsTextDelta;
185
+ }
186
+ break;
187
+ case "tool-call":
188
+ currentMessage.tool = true;
189
+ contentToAdd = part;
190
+ break;
191
+ case "reasoning":
192
+ if (lastContent?.type === "reasoning") {
193
+ lastContent.text += part.textDelta;
194
+ } else {
195
+ contentToAdd = {
196
+ type: "reasoning",
197
+ text: part.textDelta,
198
+ };
199
+ }
200
+ break;
201
+ case "source":
202
+ if (!currentMessage.sources) {
203
+ currentMessage.sources = [];
204
+ }
205
+ currentMessage.sources.push(part.source);
206
+ break;
207
+ case "tool-result":
208
+ contentToAdd = part;
209
+ break;
210
+ default:
211
+ console.warn(`Received unexpected part: ${JSON.stringify(part)}`);
212
+ break;
213
+ }
214
+ if (contentToAdd) {
215
+ if (!currentMessage.message!.content) {
216
+ currentMessage.message!.content = [];
217
+ }
218
+ if (!Array.isArray(currentMessage.message?.content)) {
219
+ throw new Error("Expected message content to be an array");
220
+ }
221
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
222
+ currentMessage.message.content.push(contentToAdd as any);
223
+ lastContent = contentToAdd;
224
+ }
225
+ }
226
+ return [newStream, true];
227
+ }
228
+
229
+ function cloneMessageAndContent(
230
+ message: Message | undefined
231
+ ): Message | undefined {
232
+ return (
233
+ message &&
234
+ ({
235
+ ...message,
236
+ content: Array.isArray(message.content)
237
+ ? [...message.content]
238
+ : message.content,
239
+ } as typeof message)
240
+ );
241
+ }
242
+
243
+ function getLastContent(message: MessageDoc) {
244
+ if (Array.isArray(message.message?.content)) {
245
+ return message.message.content.at(-1);
246
+ }
247
+ return undefined;
248
+ }
249
+
250
+ export function createStreamingMessage(
251
+ threadId: string,
252
+ message: StreamMessage,
253
+ part: TextStreamPart,
254
+ index: number
255
+ ): MessageDoc {
256
+ const { streamId, ...rest } = message;
257
+ const metadata: MessageDoc = {
258
+ _id: `${streamId}-${index}`,
259
+ _creationTime: Date.now(),
260
+ status: "pending",
261
+ threadId,
262
+ tool: false,
263
+ ...rest,
264
+ };
265
+ switch (part.type) {
266
+ case "text-delta":
267
+ return {
268
+ ...metadata,
269
+ message: {
270
+ role: "assistant",
271
+ content: [{ type: "text", text: part.textDelta }],
272
+ },
273
+ text: part.textDelta,
274
+ };
275
+ case "tool-call-streaming-start":
276
+ return {
277
+ ...metadata,
278
+ tool: true,
279
+ message: {
280
+ role: "assistant",
281
+ content: [
282
+ {
283
+ type: "tool-call",
284
+ toolName: part.toolName,
285
+ toolCallId: part.toolCallId,
286
+ args: "", // when it's a string, it's a partial call
287
+ },
288
+ ],
289
+ },
290
+ };
291
+ case "reasoning":
292
+ return {
293
+ ...metadata,
294
+ message: {
295
+ role: "assistant",
296
+ content: [{ type: "reasoning", text: part.textDelta }],
297
+ },
298
+ reasoning: part.textDelta,
299
+ };
300
+ case "source":
301
+ console.warn("Received source part first??");
302
+ return {
303
+ ...metadata,
304
+ tool: true,
305
+ message: { role: "tool", content: [] },
306
+ sources: [part.source],
307
+ };
308
+ case "tool-call":
309
+ return {
310
+ ...metadata,
311
+ tool: true,
312
+ message: { role: "assistant", content: [part] },
313
+ };
314
+ case "tool-call-delta":
315
+ console.warn("Received tool call delta part first??");
316
+ return {
317
+ ...metadata,
318
+ tool: true,
319
+ message: {
320
+ role: "assistant",
321
+ content: [
322
+ {
323
+ type: "tool-call",
324
+ toolCallId: part.toolCallId,
325
+ toolName: part.toolName,
326
+ args: part.argsTextDelta,
327
+ },
328
+ ],
329
+ },
330
+ };
331
+ case "tool-result":
332
+ return {
333
+ ...metadata,
334
+ tool: true,
335
+ message: { role: "tool", content: [part] },
336
+ };
337
+ default:
338
+ throw new Error(`Unexpected part type: ${JSON.stringify(part)}`);
339
+ }
340
+ }