@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.
- package/dist/commonjs/client/index.d.ts +6 -5
- package/dist/commonjs/client/index.d.ts.map +1 -1
- package/dist/commonjs/client/index.js +37 -11
- package/dist/commonjs/client/index.js.map +1 -1
- package/dist/commonjs/client/streaming.d.ts +2 -2
- package/dist/commonjs/client/streaming.d.ts.map +1 -1
- package/dist/commonjs/client/streaming.js +2 -1
- package/dist/commonjs/client/streaming.js.map +1 -1
- package/dist/commonjs/component/messages.d.ts +3 -3
- package/dist/commonjs/component/messages.d.ts.map +1 -1
- package/dist/commonjs/component/messages.js +16 -14
- package/dist/commonjs/component/messages.js.map +1 -1
- package/dist/commonjs/component/streams.d.ts.map +1 -1
- package/dist/commonjs/component/streams.js +1 -4
- package/dist/commonjs/component/streams.js.map +1 -1
- package/dist/commonjs/component/users.d.ts +3 -0
- package/dist/commonjs/component/users.d.ts.map +1 -1
- package/dist/commonjs/component/users.js +11 -1
- package/dist/commonjs/component/users.js.map +1 -1
- package/dist/commonjs/component/vector/index.d.ts +3 -1
- package/dist/commonjs/component/vector/index.d.ts.map +1 -1
- package/dist/commonjs/component/vector/index.js +6 -2
- package/dist/commonjs/component/vector/index.js.map +1 -1
- package/dist/commonjs/react/deltas.d.ts +29 -0
- package/dist/commonjs/react/deltas.d.ts.map +1 -0
- package/dist/commonjs/react/deltas.js +267 -0
- package/dist/commonjs/react/deltas.js.map +1 -0
- package/dist/commonjs/react/index.d.ts +4 -49
- package/dist/commonjs/react/index.d.ts.map +1 -1
- package/dist/commonjs/react/index.js +15 -446
- package/dist/commonjs/react/index.js.map +1 -1
- package/dist/commonjs/react/optimisticallySendMessage.d.ts +8 -0
- package/dist/commonjs/react/optimisticallySendMessage.d.ts.map +1 -0
- package/dist/commonjs/react/optimisticallySendMessage.js +40 -0
- package/dist/commonjs/react/optimisticallySendMessage.js.map +1 -0
- package/dist/commonjs/react/toUIMessages.d.ts.map +1 -1
- package/dist/commonjs/react/toUIMessages.js +4 -2
- package/dist/commonjs/react/toUIMessages.js.map +1 -1
- package/dist/commonjs/react/types.d.ts +26 -0
- package/dist/commonjs/react/types.d.ts.map +1 -0
- package/dist/commonjs/react/types.js +2 -0
- package/dist/commonjs/react/types.js.map +1 -0
- package/dist/commonjs/react/useSmoothText.d.ts +20 -0
- package/dist/commonjs/react/useSmoothText.d.ts.map +1 -0
- package/dist/commonjs/react/useSmoothText.js +50 -0
- package/dist/commonjs/react/useSmoothText.js.map +1 -0
- package/dist/esm/client/index.d.ts +6 -5
- package/dist/esm/client/index.d.ts.map +1 -1
- package/dist/esm/client/index.js +37 -11
- package/dist/esm/client/index.js.map +1 -1
- package/dist/esm/client/streaming.d.ts +2 -2
- package/dist/esm/client/streaming.d.ts.map +1 -1
- package/dist/esm/client/streaming.js +2 -1
- package/dist/esm/client/streaming.js.map +1 -1
- package/dist/esm/component/messages.d.ts +3 -3
- package/dist/esm/component/messages.d.ts.map +1 -1
- package/dist/esm/component/messages.js +16 -14
- package/dist/esm/component/messages.js.map +1 -1
- package/dist/esm/component/streams.d.ts.map +1 -1
- package/dist/esm/component/streams.js +1 -4
- package/dist/esm/component/streams.js.map +1 -1
- package/dist/esm/component/users.d.ts +3 -0
- package/dist/esm/component/users.d.ts.map +1 -1
- package/dist/esm/component/users.js +11 -1
- package/dist/esm/component/users.js.map +1 -1
- package/dist/esm/component/vector/index.d.ts +3 -1
- package/dist/esm/component/vector/index.d.ts.map +1 -1
- package/dist/esm/component/vector/index.js +6 -2
- package/dist/esm/component/vector/index.js.map +1 -1
- package/dist/esm/react/deltas.d.ts +29 -0
- package/dist/esm/react/deltas.d.ts.map +1 -0
- package/dist/esm/react/deltas.js +267 -0
- package/dist/esm/react/deltas.js.map +1 -0
- package/dist/esm/react/index.d.ts +4 -49
- package/dist/esm/react/index.d.ts.map +1 -1
- package/dist/esm/react/index.js +15 -446
- package/dist/esm/react/index.js.map +1 -1
- package/dist/esm/react/optimisticallySendMessage.d.ts +8 -0
- package/dist/esm/react/optimisticallySendMessage.d.ts.map +1 -0
- package/dist/esm/react/optimisticallySendMessage.js +40 -0
- package/dist/esm/react/optimisticallySendMessage.js.map +1 -0
- package/dist/esm/react/toUIMessages.d.ts.map +1 -1
- package/dist/esm/react/toUIMessages.js +4 -2
- package/dist/esm/react/toUIMessages.js.map +1 -1
- package/dist/esm/react/types.d.ts +26 -0
- package/dist/esm/react/types.d.ts.map +1 -0
- package/dist/esm/react/types.js +2 -0
- package/dist/esm/react/types.js.map +1 -0
- package/dist/esm/react/useSmoothText.d.ts +20 -0
- package/dist/esm/react/useSmoothText.d.ts.map +1 -0
- package/dist/esm/react/useSmoothText.js +50 -0
- package/dist/esm/react/useSmoothText.js.map +1 -0
- package/package.json +4 -1
- package/src/client/index.ts +41 -23
- package/src/client/streaming.ts +3 -2
- package/src/component/_generated/api.d.ts +2 -2
- package/src/component/messages.ts +26 -15
- package/src/component/streams.ts +3 -4
- package/src/component/users.ts +12 -0
- package/src/component/vector/index.ts +9 -3
- package/src/react/deltas.test.ts +298 -0
- package/src/react/deltas.ts +340 -0
- package/src/react/index.ts +26 -589
- package/src/react/optimisticallySendMessage.ts +46 -0
- package/src/react/toUIMessages.test.ts +301 -0
- package/src/react/toUIMessages.ts +6 -2
- package/src/react/types.ts +52 -0
- 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
|
+
}
|