@convex-dev/agent 0.2.6-alpha.0 → 0.2.6

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 (98) hide show
  1. package/dist/client/definePlaygroundAPI.d.ts +6 -4
  2. package/dist/client/definePlaygroundAPI.d.ts.map +1 -1
  3. package/dist/client/definePlaygroundAPI.js +15 -6
  4. package/dist/client/definePlaygroundAPI.js.map +1 -1
  5. package/dist/client/index.d.ts +26 -120
  6. package/dist/client/index.d.ts.map +1 -1
  7. package/dist/client/index.js +48 -373
  8. package/dist/client/index.js.map +1 -1
  9. package/dist/client/messages.d.ts +1 -1
  10. package/dist/client/messages.d.ts.map +1 -1
  11. package/dist/client/mockModel.d.ts +3 -3
  12. package/dist/client/mockModel.d.ts.map +1 -1
  13. package/dist/client/mockModel.js +22 -17
  14. package/dist/client/mockModel.js.map +1 -1
  15. package/dist/client/saveInputMessages.d.ts +20 -0
  16. package/dist/client/saveInputMessages.d.ts.map +1 -0
  17. package/dist/client/saveInputMessages.js +57 -0
  18. package/dist/client/saveInputMessages.js.map +1 -0
  19. package/dist/client/search.d.ts +110 -9
  20. package/dist/client/search.d.ts.map +1 -1
  21. package/dist/client/search.js +271 -39
  22. package/dist/client/search.js.map +1 -1
  23. package/dist/client/start.d.ts +83 -0
  24. package/dist/client/start.d.ts.map +1 -0
  25. package/dist/client/start.js +171 -0
  26. package/dist/client/start.js.map +1 -0
  27. package/dist/client/streaming.d.ts +8 -8
  28. package/dist/client/streaming.d.ts.map +1 -1
  29. package/dist/client/streaming.js +2 -1
  30. package/dist/client/streaming.js.map +1 -1
  31. package/dist/client/textStreamParts.d.ts.map +1 -1
  32. package/dist/client/textStreamParts.js +2 -9
  33. package/dist/client/textStreamParts.js.map +1 -1
  34. package/dist/client/threads.d.ts +1 -1
  35. package/dist/client/threads.d.ts.map +1 -1
  36. package/dist/client/types.d.ts +137 -5
  37. package/dist/client/types.d.ts.map +1 -1
  38. package/dist/component/_generated/api.d.ts +11 -3
  39. package/dist/component/messages.d.ts +13 -4
  40. package/dist/component/messages.d.ts.map +1 -1
  41. package/dist/component/messages.js +67 -25
  42. package/dist/component/messages.js.map +1 -1
  43. package/dist/component/schema.d.ts +2 -1643
  44. package/dist/component/schema.d.ts.map +1 -1
  45. package/dist/component/schema.js +0 -24
  46. package/dist/component/schema.js.map +1 -1
  47. package/dist/mapping.d.ts +7 -9
  48. package/dist/mapping.d.ts.map +1 -1
  49. package/dist/mapping.js +73 -7
  50. package/dist/mapping.js.map +1 -1
  51. package/dist/react/deltas.d.ts.map +1 -1
  52. package/dist/react/deltas.js +15 -5
  53. package/dist/react/deltas.js.map +1 -1
  54. package/dist/react/fromUIMessages.d.ts +13 -0
  55. package/dist/react/fromUIMessages.d.ts.map +1 -0
  56. package/dist/react/fromUIMessages.js +70 -0
  57. package/dist/react/fromUIMessages.js.map +1 -0
  58. package/dist/react/toUIMessages.d.ts +5 -2
  59. package/dist/react/toUIMessages.d.ts.map +1 -1
  60. package/dist/react/toUIMessages.js +3 -0
  61. package/dist/react/toUIMessages.js.map +1 -1
  62. package/dist/shared.d.ts +10 -0
  63. package/dist/shared.d.ts.map +1 -1
  64. package/dist/shared.js +26 -0
  65. package/dist/shared.js.map +1 -1
  66. package/dist/validators.d.ts +1640 -0
  67. package/dist/validators.d.ts.map +1 -1
  68. package/dist/validators.js +41 -0
  69. package/dist/validators.js.map +1 -1
  70. package/package.json +1 -1
  71. package/src/client/definePlaygroundAPI.ts +16 -7
  72. package/src/client/index.test.ts +11 -46
  73. package/src/client/index.ts +99 -558
  74. package/src/client/messages.ts +1 -1
  75. package/src/client/mock.json +68 -0
  76. package/src/client/mockModel.ts +34 -23
  77. package/src/client/saveInputMessages.test.ts +576 -0
  78. package/src/client/saveInputMessages.ts +100 -0
  79. package/src/client/search.test.ts +1017 -0
  80. package/src/client/search.ts +446 -68
  81. package/src/client/start.ts +313 -0
  82. package/src/client/stream.json +48 -0
  83. package/src/client/streaming.ts +3 -3
  84. package/src/client/textStreamParts.ts +2 -11
  85. package/src/client/threads.ts +1 -1
  86. package/src/client/types.ts +143 -3
  87. package/src/component/_generated/api.d.ts +11 -3
  88. package/src/component/messages.ts +73 -27
  89. package/src/component/schema.ts +1 -29
  90. package/src/mapping.ts +84 -7
  91. package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  92. package/src/react/deltas.ts +18 -5
  93. package/src/react/fromUIMessages.test.ts +427 -0
  94. package/src/react/fromUIMessages.ts +85 -0
  95. package/src/react/toUIMessages.ts +21 -13
  96. package/src/shared.ts +33 -0
  97. package/src/validators.test.ts +13 -2
  98. package/src/validators.ts +48 -0
@@ -0,0 +1,427 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { toUIMessages } from "./toUIMessages.js";
3
+ import { fromUIMessages } from "./fromUIMessages.js";
4
+ import type { MessageDoc } from "../client/index.js";
5
+ import type { UIMessage } from "./toUIMessages.js";
6
+
7
+ // Helper to create a base message doc
8
+ function baseMessageDoc<T = unknown>(overrides: Partial<MessageDoc & { streaming?: boolean; metadata?: T }> = {}): MessageDoc & { streaming?: boolean; metadata?: T } {
9
+ return {
10
+ _id: "msg1",
11
+ _creationTime: Date.now(),
12
+ order: 0,
13
+ stepOrder: 0,
14
+ status: "success",
15
+ threadId: "thread1",
16
+ tool: false,
17
+ ...overrides,
18
+ };
19
+ }
20
+
21
+ describe("fromUIMessages round-trip tests", () => {
22
+ it("preserves essential data for simple user message", () => {
23
+ const originalMessages = [
24
+ baseMessageDoc({
25
+ message: {
26
+ role: "user",
27
+ content: "Hello world!",
28
+ },
29
+ text: "Hello world!",
30
+ }),
31
+ ];
32
+
33
+ const uiMessages = toUIMessages(originalMessages);
34
+ const backToMessageDocs = fromUIMessages("thread1", uiMessages);
35
+
36
+ expect(uiMessages).toHaveLength(1);
37
+ expect(uiMessages[0].role).toBe("user");
38
+ expect(uiMessages[0].text).toBe("Hello world!");
39
+
40
+ expect(backToMessageDocs).toHaveLength(1);
41
+ expect(backToMessageDocs[0].text).toBe("Hello world!");
42
+ expect(backToMessageDocs[0].threadId).toBe("thread1");
43
+
44
+ // Content gets normalized to array format
45
+ expect(Array.isArray(backToMessageDocs[0].message?.content)).toBe(true);
46
+ if (Array.isArray(backToMessageDocs[0].message?.content)) {
47
+ expect(backToMessageDocs[0].message.content[0]).toMatchObject({
48
+ type: "text",
49
+ text: "Hello world!",
50
+ });
51
+ }
52
+ });
53
+
54
+ it("preserves essential data for assistant message", () => {
55
+ const originalMessages = [
56
+ baseMessageDoc({
57
+ message: {
58
+ role: "assistant",
59
+ content: "Hi there! How can I help?",
60
+ },
61
+ text: "Hi there! How can I help?",
62
+ }),
63
+ ];
64
+
65
+ const uiMessages = toUIMessages(originalMessages);
66
+ const backToMessageDocs = fromUIMessages("thread1", uiMessages);
67
+
68
+ expect(uiMessages).toHaveLength(1);
69
+ expect(uiMessages[0].role).toBe("assistant");
70
+ expect(uiMessages[0].text).toBe("Hi there! How can I help?");
71
+
72
+ expect(backToMessageDocs).toHaveLength(1);
73
+ expect(backToMessageDocs[0].text).toBe("Hi there! How can I help?");
74
+ });
75
+
76
+ it("preserves system messages correctly", () => {
77
+ const originalMessages = [
78
+ baseMessageDoc({
79
+ message: {
80
+ role: "system",
81
+ content: "You are a helpful assistant.",
82
+ },
83
+ text: "You are a helpful assistant.",
84
+ }),
85
+ ];
86
+
87
+ const uiMessages = toUIMessages(originalMessages);
88
+ const backToMessageDocs = fromUIMessages("thread1", uiMessages);
89
+
90
+ expect(uiMessages).toHaveLength(1);
91
+ expect(uiMessages[0].role).toBe("system");
92
+ expect(uiMessages[0].text).toBe("You are a helpful assistant.");
93
+
94
+ expect(backToMessageDocs).toHaveLength(1);
95
+ expect(backToMessageDocs[0].text).toBe("You are a helpful assistant.");
96
+ expect(backToMessageDocs[0].message?.role).toBe("system");
97
+
98
+ // System content stays as string
99
+ expect(backToMessageDocs[0].message?.content).toBe(
100
+ "You are a helpful assistant.",
101
+ );
102
+ });
103
+
104
+ it("preserves reasoning in assistant messages", () => {
105
+ const originalMessages = [
106
+ baseMessageDoc({
107
+ message: {
108
+ role: "assistant",
109
+ content: [
110
+ {
111
+ type: "reasoning",
112
+ text: "Let me think about this...",
113
+ },
114
+ {
115
+ type: "text",
116
+ text: "Here's my response.",
117
+ },
118
+ ],
119
+ },
120
+ text: "Here's my response.",
121
+ reasoning: "Let me think about this...",
122
+ }),
123
+ ];
124
+
125
+ const uiMessages = toUIMessages(originalMessages);
126
+ const backToMessageDocs = fromUIMessages("thread1", uiMessages);
127
+
128
+ expect(uiMessages).toHaveLength(1);
129
+ expect(uiMessages[0].text).toBe("Here's my response.");
130
+
131
+ // Check that reasoning parts are preserved in UI message
132
+ const reasoningParts = uiMessages[0].parts.filter(
133
+ (part) => part.type === "reasoning",
134
+ );
135
+ expect(reasoningParts).toHaveLength(1);
136
+ expect(reasoningParts[0]).toMatchObject({
137
+ type: "reasoning",
138
+ text: "Let me think about this...",
139
+ });
140
+
141
+ expect(backToMessageDocs).toHaveLength(1);
142
+ expect(backToMessageDocs[0].text).toBe("Here's my response.");
143
+ expect(backToMessageDocs[0].reasoning).toBe("Let me think about this...");
144
+ });
145
+
146
+ it("handles tool calls and groups them correctly", () => {
147
+ // Tool calls get grouped into single UI message but expanded back to multiple message docs
148
+ const originalMessages = [
149
+ baseMessageDoc({
150
+ _id: "msg1",
151
+ order: 1,
152
+ stepOrder: 1,
153
+ message: {
154
+ role: "assistant",
155
+ content: [
156
+ {
157
+ type: "tool-call",
158
+ toolName: "calculator",
159
+ toolCallId: "call1",
160
+ args: { operation: "add", a: 2, b: 3 },
161
+ },
162
+ ],
163
+ },
164
+ tool: true,
165
+ }),
166
+ baseMessageDoc({
167
+ _id: "msg2",
168
+ order: 1,
169
+ stepOrder: 2,
170
+ message: {
171
+ role: "tool",
172
+ content: [
173
+ {
174
+ type: "tool-result",
175
+ toolCallId: "call1",
176
+ toolName: "calculator",
177
+ output: {
178
+ type: "json",
179
+ value: { result: 5 },
180
+ },
181
+ },
182
+ ],
183
+ },
184
+ tool: true,
185
+ }),
186
+ ];
187
+
188
+ const uiMessages = toUIMessages(originalMessages);
189
+ const backToMessageDocs = fromUIMessages("thread1", uiMessages);
190
+
191
+ // Should be grouped into single UI message
192
+ expect(uiMessages).toHaveLength(1);
193
+ expect(uiMessages[0].role).toBe("assistant");
194
+
195
+ // Check tool parts exist
196
+ const toolParts = uiMessages[0].parts.filter(
197
+ (part) => part.type === "tool-calculator",
198
+ );
199
+ expect(toolParts).toHaveLength(1);
200
+ expect(toolParts[0]).toMatchObject({
201
+ type: "tool-calculator",
202
+ toolCallId: "call1",
203
+ state: "output-available",
204
+ input: { operation: "add", a: 2, b: 3 },
205
+ output: { result: 5 },
206
+ });
207
+
208
+ // Should expand back to multiple message docs
209
+ expect(backToMessageDocs.length).toBeGreaterThanOrEqual(1);
210
+
211
+ // Check that tool information is preserved
212
+ const toolMessages = backToMessageDocs.filter((msg) => msg.tool);
213
+ expect(toolMessages.length).toBeGreaterThan(0);
214
+ expect(toolMessages[0].stepOrder).toBe(1);
215
+ expect(toolMessages[1].stepOrder).toBe(2);
216
+ });
217
+
218
+ it("preserves file attachments in user messages", () => {
219
+ const originalMessages = [
220
+ baseMessageDoc({
221
+ message: {
222
+ role: "user",
223
+ content: [
224
+ {
225
+ type: "text",
226
+ text: "What's in this image?",
227
+ },
228
+ {
229
+ type: "file",
230
+ mimeType: "image/png",
231
+ data: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
232
+ },
233
+ ],
234
+ },
235
+ text: "What's in this image?",
236
+ }),
237
+ ];
238
+
239
+ const uiMessages = toUIMessages(originalMessages);
240
+ const backToMessageDocs = fromUIMessages("thread1", uiMessages);
241
+
242
+ expect(uiMessages).toHaveLength(1);
243
+ expect(uiMessages[0].role).toBe("user");
244
+ expect(uiMessages[0].text).toBe("What's in this image?");
245
+
246
+ // Check file parts exist in UI message
247
+ const fileParts = uiMessages[0].parts.filter(
248
+ (part) => part.type === "file",
249
+ );
250
+ expect(fileParts).toHaveLength(1);
251
+
252
+ expect(backToMessageDocs).toHaveLength(1);
253
+ expect(backToMessageDocs[0].text).toBe("What's in this image?");
254
+
255
+ // Check file content is preserved
256
+ const content = backToMessageDocs[0].message?.content;
257
+ expect(Array.isArray(content)).toBe(true);
258
+ if (Array.isArray(content)) {
259
+ const fileContent = content.find((c) => c.type === "file");
260
+ expect(fileContent).toBeDefined();
261
+ expect(fileContent).toMatchObject({
262
+ type: "file",
263
+ mimeType: "image/png",
264
+ });
265
+ }
266
+ });
267
+
268
+ it("preserves sources correctly", () => {
269
+ const originalMessages = [
270
+ baseMessageDoc({
271
+ message: {
272
+ role: "assistant",
273
+ content: [
274
+ {
275
+ type: "text",
276
+ text: "I found some relevant sources.",
277
+ },
278
+ ],
279
+ },
280
+ text: "I found some relevant sources.",
281
+ sources: [
282
+ {
283
+ type: "source",
284
+ sourceType: "url",
285
+ id: "source1",
286
+ url: "https://example.com",
287
+ title: "Example Source",
288
+ },
289
+ {
290
+ type: "source",
291
+ sourceType: "document",
292
+ id: "source2",
293
+ mediaType: "application/pdf",
294
+ title: "Document Source",
295
+ },
296
+ ],
297
+ }),
298
+ ];
299
+
300
+ const uiMessages = toUIMessages(originalMessages);
301
+ const backToMessageDocs = fromUIMessages("thread1", uiMessages);
302
+
303
+ expect(uiMessages).toHaveLength(1);
304
+
305
+ // Check source parts exist in UI message
306
+ const sourceParts = uiMessages[0].parts.filter(
307
+ (part) => part.type === "source-url" || part.type === "source-document",
308
+ );
309
+ expect(sourceParts).toHaveLength(2);
310
+
311
+ expect(backToMessageDocs).toHaveLength(1);
312
+ expect(backToMessageDocs[0].sources).toHaveLength(2);
313
+ expect(backToMessageDocs[0].sources![0]).toMatchObject({
314
+ type: "source",
315
+ sourceType: "url",
316
+ id: "source1",
317
+ url: "https://example.com",
318
+ title: "Example Source",
319
+ });
320
+ });
321
+
322
+ it("preserves metadata when provided", () => {
323
+ const testMetadata = {
324
+ customField: "customValue",
325
+ timestamp: Date.now(),
326
+ };
327
+
328
+ const originalMessages = [
329
+ baseMessageDoc({
330
+ message: {
331
+ role: "user",
332
+ content: "Test message",
333
+ },
334
+ text: "Test message",
335
+ metadata: testMetadata,
336
+ }),
337
+ ];
338
+
339
+ const uiMessages = toUIMessages(originalMessages);
340
+ const backToMessageDocs = fromUIMessages("thread1", uiMessages);
341
+
342
+ expect(uiMessages).toHaveLength(1);
343
+ expect(uiMessages[0].metadata).toEqual(testMetadata);
344
+
345
+ expect(backToMessageDocs).toHaveLength(1);
346
+ expect(backToMessageDocs[0].metadata).toEqual(testMetadata);
347
+ });
348
+
349
+ it("handles streaming status correctly", () => {
350
+ const originalMessages = [
351
+ baseMessageDoc({
352
+ message: {
353
+ role: "assistant",
354
+ content: "Streaming response...",
355
+ },
356
+ text: "Streaming response...",
357
+ streaming: true,
358
+ status: "pending",
359
+ }),
360
+ ];
361
+
362
+ const uiMessages = toUIMessages(originalMessages);
363
+ const backToMessageDocs = fromUIMessages("thread1", uiMessages);
364
+
365
+ expect(uiMessages).toHaveLength(1);
366
+ expect(uiMessages[0].status).toBe("streaming");
367
+
368
+ expect(backToMessageDocs).toHaveLength(1);
369
+ expect(backToMessageDocs[0].streaming).toBe(true);
370
+ expect(backToMessageDocs[0].status).toBe("pending");
371
+ });
372
+ });
373
+
374
+ describe("fromUIMessages functionality tests", () => {
375
+ it("handles empty messages array", () => {
376
+ const uiMessages: UIMessage[] = [];
377
+ const result = fromUIMessages("thread1", uiMessages);
378
+ expect(result).toHaveLength(0);
379
+ });
380
+
381
+ it("correctly assigns thread ID", () => {
382
+ const uiMessage: UIMessage = {
383
+ id: "test-id",
384
+ _creationTime: Date.now(),
385
+ order: 0,
386
+ stepOrder: 0,
387
+ status: "success",
388
+ key: "test-key",
389
+ text: "Hello",
390
+ role: "user",
391
+ parts: [{ type: "text", text: "Hello" }],
392
+ };
393
+
394
+ const result = fromUIMessages("custom-thread-id", [uiMessage]);
395
+ expect(result).toHaveLength(1);
396
+ expect(result[0].threadId).toBe("custom-thread-id");
397
+ });
398
+
399
+ it("correctly determines tool status", () => {
400
+ const toolUIMessage: UIMessage = {
401
+ id: "tool-id",
402
+ _creationTime: Date.now(),
403
+ order: 0,
404
+ stepOrder: 0,
405
+ status: "success",
406
+ key: "tool-key",
407
+ text: "",
408
+ role: "assistant",
409
+ parts: [
410
+ {
411
+ type: "tool-calculator",
412
+ toolCallId: "call1",
413
+ input: { a: 1, b: 2 },
414
+ state: "output-available",
415
+ output: { result: 3 },
416
+ },
417
+ ],
418
+ };
419
+
420
+ const result = fromUIMessages("thread1", [toolUIMessage]);
421
+ expect(result.length).toBeGreaterThan(0);
422
+
423
+ // Should have tool messages
424
+ const toolMessages = result.filter((msg) => msg.tool);
425
+ expect(toolMessages.length).toBeGreaterThan(0);
426
+ });
427
+ });
@@ -0,0 +1,85 @@
1
+ import { convertToModelMessages } from "ai";
2
+ import { extractReasoning, extractText, isTool } from "../shared.js";
3
+ import type { MessageDoc, vSource } from "../validators.js";
4
+ import type { UIMessage } from "./toUIMessages.js";
5
+ import type { ProviderOptions } from "@ai-sdk/provider-utils";
6
+ import { omit } from "convex-helpers";
7
+ import { fromModelMessage } from "../mapping.js";
8
+ import type { Infer } from "convex/values";
9
+
10
+ export function fromUIMessages<METADATA = unknown>(
11
+ threadId: string,
12
+ messages: (UIMessage<METADATA> & {
13
+ userId?: string;
14
+ model?: string;
15
+ provider?: string;
16
+ providerOptions?: ProviderOptions;
17
+ })[],
18
+ ): (MessageDoc & { streaming: boolean; metadata?: METADATA })[] {
19
+ return messages.flatMap((uiMessage) => {
20
+ const stepOrder = uiMessage.stepOrder;
21
+ const commonFields = {
22
+ _id: uiMessage.id,
23
+ ...omit(uiMessage, ["parts", "role", "key", "text"]),
24
+ threadId,
25
+ status: uiMessage.status === "streaming" ? "pending" : "success",
26
+ streaming: uiMessage.status === "streaming",
27
+ // to override
28
+ tool: false,
29
+ } satisfies MessageDoc & { streaming: boolean; metadata?: METADATA };
30
+ const modelMessages = convertToModelMessages([uiMessage]);
31
+ return modelMessages.map((modelMessage, i) => {
32
+ const message = fromModelMessage(modelMessage);
33
+ const tool = isTool(message);
34
+ const doc: MessageDoc & { streaming: boolean; metadata?: METADATA } = {
35
+ ...commonFields,
36
+ stepOrder: stepOrder + i,
37
+ message,
38
+ tool,
39
+ text: extractText(message),
40
+ reasoning: extractReasoning(message),
41
+ finishReason: tool ? "tool-calls" : "stop",
42
+ sources: fromSourceParts(uiMessage.parts),
43
+ };
44
+ if (Array.isArray(modelMessage.content)) {
45
+ const providerOptions = modelMessage.content.find(
46
+ (c) => c.providerOptions,
47
+ )?.providerOptions;
48
+ if (providerOptions) {
49
+ // convertToModelMessages changes providerMetadata to providerOptions
50
+ doc.providerMetadata = providerOptions;
51
+ doc.providerOptions ??= providerOptions;
52
+ }
53
+ }
54
+ return doc;
55
+ });
56
+ });
57
+ }
58
+
59
+ function fromSourceParts(parts: UIMessage["parts"]): Infer<typeof vSource>[] {
60
+ return parts
61
+ .map((part) => {
62
+ if (part.type === "source-url") {
63
+ return {
64
+ type: "source",
65
+ sourceType: "url",
66
+ url: part.url,
67
+ id: part.sourceId,
68
+ providerMetadata: part.providerMetadata,
69
+ title: part.title,
70
+ } satisfies Infer<typeof vSource>;
71
+ }
72
+ if (part.type === "source-document") {
73
+ return {
74
+ type: "source",
75
+ sourceType: "document",
76
+ mediaType: part.mediaType,
77
+ id: part.sourceId,
78
+ providerMetadata: part.providerMetadata,
79
+ title: part.title,
80
+ } satisfies Infer<typeof vSource>;
81
+ }
82
+ return undefined;
83
+ })
84
+ .filter((p) => p !== undefined);
85
+ }
@@ -30,12 +30,17 @@ export type UIMessage<
30
30
  _creationTime: number;
31
31
  };
32
32
 
33
+ type ExtraFields<METADATA = unknown> = {
34
+ streaming?: boolean;
35
+ metadata?: METADATA;
36
+ };
37
+
33
38
  export function toUIMessages<
34
39
  METADATA = unknown,
35
40
  DATA_PARTS extends UIDataTypes = UIDataTypes,
36
41
  TOOLS extends UITools = UITools,
37
42
  >(
38
- messages: (MessageDoc & { streaming?: boolean })[],
43
+ messages: (MessageDoc & ExtraFields<METADATA>)[],
39
44
  ): UIMessage<METADATA, DATA_PARTS, TOOLS>[] {
40
45
  // Group assistant and tool messages together
41
46
  const assistantGroups = groupAssistantMessages(messages);
@@ -55,29 +60,29 @@ export function toUIMessages<
55
60
  return uiMessages;
56
61
  }
57
62
 
58
- type Group =
63
+ type Group<METADATA = unknown> =
59
64
  | {
60
65
  role: "user";
61
- message: MessageDoc & { streaming?: boolean };
66
+ message: MessageDoc & ExtraFields<METADATA>;
62
67
  }
63
68
  | {
64
69
  role: "system";
65
- message: MessageDoc & { streaming?: boolean };
70
+ message: MessageDoc & ExtraFields<METADATA>;
66
71
  }
67
72
  | {
68
73
  role: "assistant";
69
- messages: (MessageDoc & { streaming?: boolean })[];
74
+ messages: (MessageDoc & ExtraFields<METADATA>)[];
70
75
  };
71
76
 
72
- function groupAssistantMessages(
73
- messages: (MessageDoc & { streaming?: boolean })[],
74
- ): Group[] {
75
- const groups: Group[] = [];
77
+ function groupAssistantMessages<METADATA = unknown>(
78
+ messages: (MessageDoc & ExtraFields<METADATA>)[],
79
+ ): Group<METADATA>[] {
80
+ const groups: Group<METADATA>[] = [];
76
81
 
77
82
  // Sort messages by order and stepOrder first to handle out-of-order arrivals
78
83
  const sortedMessages = sorted(messages);
79
84
 
80
- let currentAssistantGroup: (MessageDoc & { streaming?: boolean })[] = [];
85
+ let currentAssistantGroup: (MessageDoc & ExtraFields<METADATA>)[] = [];
81
86
  let currentOrder: number | undefined;
82
87
 
83
88
  for (const message of sortedMessages) {
@@ -145,7 +150,7 @@ function createSystemUIMessage<
145
150
  DATA_PARTS extends UIDataTypes = UIDataTypes,
146
151
  TOOLS extends UITools = UITools,
147
152
  >(
148
- message: MessageDoc & { streaming?: boolean },
153
+ message: MessageDoc & ExtraFields<METADATA>,
149
154
  ): UIMessage<METADATA, DATA_PARTS, TOOLS> {
150
155
  const text = extractTextFromMessageDoc(message);
151
156
  const partCommon = {
@@ -166,6 +171,7 @@ function createSystemUIMessage<
166
171
  role: "system",
167
172
  agentName: message.agentName,
168
173
  parts: [{ type: "text", text, ...partCommon } satisfies TextUIPart],
174
+ metadata: message.metadata,
169
175
  };
170
176
  }
171
177
 
@@ -180,7 +186,7 @@ function createUserUIMessage<
180
186
  DATA_PARTS extends UIDataTypes = UIDataTypes,
181
187
  TOOLS extends UITools = UITools,
182
188
  >(
183
- message: MessageDoc & { streaming?: boolean },
189
+ message: MessageDoc & ExtraFields<METADATA>,
184
190
  ): UIMessage<METADATA, DATA_PARTS, TOOLS> {
185
191
  const text = extractTextFromMessageDoc(message);
186
192
  const coreMessage = deserializeMessage(message.message!);
@@ -224,6 +230,7 @@ function createUserUIMessage<
224
230
  text,
225
231
  role: "user",
226
232
  parts,
233
+ metadata: message.metadata,
227
234
  };
228
235
  }
229
236
 
@@ -232,7 +239,7 @@ function createAssistantUIMessage<
232
239
  DATA_PARTS extends UIDataTypes = UIDataTypes,
233
240
  TOOLS extends UITools = UITools,
234
241
  >(
235
- groupUnordered: (MessageDoc & { streaming?: boolean })[],
242
+ groupUnordered: (MessageDoc & ExtraFields<METADATA>)[],
236
243
  ): UIMessage<METADATA, DATA_PARTS, TOOLS> {
237
244
  const group = sorted(groupUnordered);
238
245
  const firstMessage = group[0];
@@ -415,6 +422,7 @@ function createAssistantUIMessage<
415
422
  text: allText,
416
423
  status,
417
424
  parts: allParts,
425
+ metadata: group.find((m) => m.metadata)?.metadata,
418
426
  };
419
427
  }
420
428
 
package/src/shared.ts CHANGED
@@ -37,6 +37,16 @@ export function extractText(message: Message | ModelMessage) {
37
37
  return undefined;
38
38
  }
39
39
 
40
+ export function extractReasoning(message: Message | ModelMessage) {
41
+ if (typeof message.content === "string") {
42
+ return undefined;
43
+ }
44
+ return message.content
45
+ .filter((c) => c.type === "reasoning")
46
+ .map((c) => c.text)
47
+ .join("");
48
+ }
49
+
40
50
  export const DEFAULT_MESSAGE_RANGE = { before: 2, after: 1 };
41
51
 
42
52
  export function sorted<T extends { order: number; stepOrder: number }>(
@@ -49,3 +59,26 @@ export function sorted<T extends { order: number; stepOrder: number }>(
49
59
  : (a, b) => b.order - a.order || b.stepOrder - a.stepOrder,
50
60
  );
51
61
  }
62
+
63
+ export type ModelOrMetadata =
64
+ | string
65
+ | ({ provider: string } & ({ modelId: string } | { model: string }));
66
+
67
+ export function getModelName(embeddingModel: ModelOrMetadata): string {
68
+ if (typeof embeddingModel === "string") {
69
+ if (embeddingModel.includes("/")) {
70
+ return embeddingModel.split("/").slice(1).join("/");
71
+ }
72
+ return embeddingModel;
73
+ }
74
+ return "modelId" in embeddingModel
75
+ ? embeddingModel.modelId
76
+ : embeddingModel.model;
77
+ }
78
+
79
+ export function getProviderName(embeddingModel: ModelOrMetadata): string {
80
+ if (typeof embeddingModel === "string") {
81
+ return embeddingModel.split("/").at(0)!;
82
+ }
83
+ return embeddingModel.provider;
84
+ }
@@ -1,7 +1,12 @@
1
1
  import type { Infer } from "convex/values";
2
2
  import { expectTypeOf, test } from "vitest";
3
- import type { ContextOptions, StorageOptions } from "./client/types.js";
4
- import { vContextOptions, vStorageOptions } from "./validators.js";
3
+ import type {
4
+ ContextOptions,
5
+ OpaqueIds,
6
+ StorageOptions,
7
+ } from "./client/types.js";
8
+ import { vContextOptions, vMessageDoc, vStorageOptions } from "./validators.js";
9
+ import type { Doc } from "./component/_generated/dataModel.js";
5
10
 
6
11
  expectTypeOf<Infer<typeof vContextOptions>>().toExtend<ContextOptions>();
7
12
  expectTypeOf<ContextOptions>().toExtend<Infer<typeof vContextOptions>>();
@@ -9,4 +14,10 @@ expectTypeOf<ContextOptions>().toExtend<Infer<typeof vContextOptions>>();
9
14
  expectTypeOf<Infer<typeof vStorageOptions>>().toExtend<StorageOptions>();
10
15
  expectTypeOf<StorageOptions>().toExtend<Infer<typeof vStorageOptions>>();
11
16
 
17
+ type MessageBasedOnSchema = OpaqueIds<
18
+ Omit<Doc<"messages">, "files" | "stepId" | "parentMessageId">
19
+ >;
20
+ expectTypeOf<Infer<typeof vMessageDoc>>().toEqualTypeOf<MessageBasedOnSchema>();
21
+ expectTypeOf<MessageBasedOnSchema>().toEqualTypeOf<Infer<typeof vMessageDoc>>();
22
+
12
23
  test("noop", () => {});