@convex-dev/agent 0.1.4-alpha.0 → 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 (71) hide show
  1. package/dist/commonjs/client/streaming.d.ts +2 -2
  2. package/dist/commonjs/client/streaming.d.ts.map +1 -1
  3. package/dist/commonjs/client/streaming.js +2 -1
  4. package/dist/commonjs/client/streaming.js.map +1 -1
  5. package/dist/commonjs/component/streams.d.ts.map +1 -1
  6. package/dist/commonjs/component/streams.js +1 -4
  7. package/dist/commonjs/component/streams.js.map +1 -1
  8. package/dist/commonjs/react/deltas.d.ts +29 -0
  9. package/dist/commonjs/react/deltas.d.ts.map +1 -0
  10. package/dist/commonjs/react/deltas.js +267 -0
  11. package/dist/commonjs/react/deltas.js.map +1 -0
  12. package/dist/commonjs/react/index.d.ts +4 -49
  13. package/dist/commonjs/react/index.d.ts.map +1 -1
  14. package/dist/commonjs/react/index.js +15 -446
  15. package/dist/commonjs/react/index.js.map +1 -1
  16. package/dist/commonjs/react/optimisticallySendMessage.d.ts +8 -0
  17. package/dist/commonjs/react/optimisticallySendMessage.d.ts.map +1 -0
  18. package/dist/commonjs/react/optimisticallySendMessage.js +40 -0
  19. package/dist/commonjs/react/optimisticallySendMessage.js.map +1 -0
  20. package/dist/commonjs/react/toUIMessages.d.ts.map +1 -1
  21. package/dist/commonjs/react/toUIMessages.js +4 -2
  22. package/dist/commonjs/react/toUIMessages.js.map +1 -1
  23. package/dist/commonjs/react/types.d.ts +26 -0
  24. package/dist/commonjs/react/types.d.ts.map +1 -0
  25. package/dist/commonjs/react/types.js +2 -0
  26. package/dist/commonjs/react/types.js.map +1 -0
  27. package/dist/commonjs/react/useSmoothText.d.ts +20 -0
  28. package/dist/commonjs/react/useSmoothText.d.ts.map +1 -0
  29. package/dist/commonjs/react/useSmoothText.js +50 -0
  30. package/dist/commonjs/react/useSmoothText.js.map +1 -0
  31. package/dist/esm/client/streaming.d.ts +2 -2
  32. package/dist/esm/client/streaming.d.ts.map +1 -1
  33. package/dist/esm/client/streaming.js +2 -1
  34. package/dist/esm/client/streaming.js.map +1 -1
  35. package/dist/esm/component/streams.d.ts.map +1 -1
  36. package/dist/esm/component/streams.js +1 -4
  37. package/dist/esm/component/streams.js.map +1 -1
  38. package/dist/esm/react/deltas.d.ts +29 -0
  39. package/dist/esm/react/deltas.d.ts.map +1 -0
  40. package/dist/esm/react/deltas.js +267 -0
  41. package/dist/esm/react/deltas.js.map +1 -0
  42. package/dist/esm/react/index.d.ts +4 -49
  43. package/dist/esm/react/index.d.ts.map +1 -1
  44. package/dist/esm/react/index.js +15 -446
  45. package/dist/esm/react/index.js.map +1 -1
  46. package/dist/esm/react/optimisticallySendMessage.d.ts +8 -0
  47. package/dist/esm/react/optimisticallySendMessage.d.ts.map +1 -0
  48. package/dist/esm/react/optimisticallySendMessage.js +40 -0
  49. package/dist/esm/react/optimisticallySendMessage.js.map +1 -0
  50. package/dist/esm/react/toUIMessages.d.ts.map +1 -1
  51. package/dist/esm/react/toUIMessages.js +4 -2
  52. package/dist/esm/react/toUIMessages.js.map +1 -1
  53. package/dist/esm/react/types.d.ts +26 -0
  54. package/dist/esm/react/types.d.ts.map +1 -0
  55. package/dist/esm/react/types.js +2 -0
  56. package/dist/esm/react/types.js.map +1 -0
  57. package/dist/esm/react/useSmoothText.d.ts +20 -0
  58. package/dist/esm/react/useSmoothText.d.ts.map +1 -0
  59. package/dist/esm/react/useSmoothText.js +50 -0
  60. package/dist/esm/react/useSmoothText.js.map +1 -0
  61. package/package.json +4 -1
  62. package/src/client/streaming.ts +3 -2
  63. package/src/component/streams.ts +3 -4
  64. package/src/react/deltas.test.ts +298 -0
  65. package/src/react/deltas.ts +340 -0
  66. package/src/react/index.ts +26 -589
  67. package/src/react/optimisticallySendMessage.ts +46 -0
  68. package/src/react/toUIMessages.test.ts +301 -0
  69. package/src/react/toUIMessages.ts +6 -2
  70. package/src/react/types.ts +52 -0
  71. package/src/react/useSmoothText.ts +76 -0
@@ -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
+ }