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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/dist/UIMessages.d.ts +43 -0
  2. package/dist/UIMessages.d.ts.map +1 -0
  3. package/dist/{react/toUIMessages.js → UIMessages.js} +97 -4
  4. package/dist/UIMessages.js.map +1 -0
  5. package/dist/client/createTool.js +1 -1
  6. package/dist/client/createTool.js.map +1 -1
  7. package/dist/client/index.d.ts +39 -19
  8. package/dist/client/index.d.ts.map +1 -1
  9. package/dist/client/index.js +22 -19
  10. package/dist/client/index.js.map +1 -1
  11. package/dist/client/mockModel.d.ts +2 -0
  12. package/dist/client/mockModel.d.ts.map +1 -1
  13. package/dist/client/mockModel.js +11 -7
  14. package/dist/client/mockModel.js.map +1 -1
  15. package/dist/client/search.d.ts.map +1 -1
  16. package/dist/client/search.js +2 -0
  17. package/dist/client/search.js.map +1 -1
  18. package/dist/client/start.d.ts +1 -0
  19. package/dist/client/start.d.ts.map +1 -1
  20. package/dist/client/start.js +0 -1
  21. package/dist/client/start.js.map +1 -1
  22. package/dist/client/streaming.d.ts +23 -4
  23. package/dist/client/streaming.d.ts.map +1 -1
  24. package/dist/client/streaming.js +32 -6
  25. package/dist/client/streaming.js.map +1 -1
  26. package/dist/client/types.d.ts +19 -13
  27. package/dist/client/types.d.ts.map +1 -1
  28. package/dist/component/_generated/api.d.ts +2 -0
  29. package/dist/component/messages.js +1 -1
  30. package/dist/component/messages.js.map +1 -1
  31. package/dist/component/schema.d.ts +10 -4
  32. package/dist/component/schema.d.ts.map +1 -1
  33. package/dist/component/schema.js +4 -0
  34. package/dist/component/schema.js.map +1 -1
  35. package/dist/component/streams.d.ts +2 -4
  36. package/dist/component/streams.d.ts.map +1 -1
  37. package/dist/component/streams.js +42 -43
  38. package/dist/component/streams.js.map +1 -1
  39. package/dist/deltas.d.ts +48 -0
  40. package/dist/deltas.d.ts.map +1 -0
  41. package/dist/deltas.js +468 -0
  42. package/dist/deltas.js.map +1 -0
  43. package/dist/react/index.d.ts +4 -77
  44. package/dist/react/index.d.ts.map +1 -1
  45. package/dist/react/index.js +4 -178
  46. package/dist/react/index.js.map +1 -1
  47. package/dist/react/optimisticallySendMessage.d.ts +36 -3
  48. package/dist/react/optimisticallySendMessage.d.ts.map +1 -1
  49. package/dist/react/optimisticallySendMessage.js +37 -5
  50. package/dist/react/optimisticallySendMessage.js.map +1 -1
  51. package/dist/react/types.d.ts +4 -18
  52. package/dist/react/types.d.ts.map +1 -1
  53. package/dist/react/useStreamingUIMessages.d.ts +22 -0
  54. package/dist/react/useStreamingUIMessages.d.ts.map +1 -0
  55. package/dist/react/useStreamingUIMessages.js +204 -0
  56. package/dist/react/useStreamingUIMessages.js.map +1 -0
  57. package/dist/react/useThreadMessages.d.ts +103 -0
  58. package/dist/react/useThreadMessages.d.ts.map +1 -0
  59. package/dist/react/useThreadMessages.js +139 -0
  60. package/dist/react/useThreadMessages.js.map +1 -0
  61. package/dist/react/useUIMessages.d.ts +91 -0
  62. package/dist/react/useUIMessages.d.ts.map +1 -0
  63. package/dist/react/useUIMessages.js +115 -0
  64. package/dist/react/useUIMessages.js.map +1 -0
  65. package/dist/validators.d.ts +3 -1
  66. package/dist/validators.d.ts.map +1 -1
  67. package/dist/validators.js +1 -0
  68. package/dist/validators.js.map +1 -1
  69. package/package.json +4 -4
  70. package/src/{react/toUIMessages.ts → UIMessages.ts} +132 -15
  71. package/src/client/createTool.ts +1 -1
  72. package/src/client/index.ts +60 -41
  73. package/src/client/mockModel.ts +13 -8
  74. package/src/client/search.ts +2 -0
  75. package/src/client/start.ts +2 -1
  76. package/src/client/streaming.test.ts +186 -0
  77. package/src/client/streaming.ts +50 -9
  78. package/src/client/types.ts +19 -21
  79. package/src/component/_generated/api.d.ts +2 -0
  80. package/src/component/messages.ts +1 -1
  81. package/src/component/schema.ts +6 -0
  82. package/src/component/streams.ts +49 -66
  83. package/src/{react/deltas.test.ts → deltas.test.ts} +57 -46
  84. package/src/deltas.ts +589 -0
  85. package/src/{react/fromUIMessages.test.ts → fromUIMessages.test.ts} +108 -43
  86. package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  87. package/src/react/index.ts +8 -250
  88. package/src/react/optimisticallySendMessage.ts +57 -8
  89. package/src/react/types.ts +6 -39
  90. package/src/react/useStreamingUIMessages.ts +294 -0
  91. package/src/react/useThreadMessages.ts +251 -0
  92. package/src/react/useUIMessages.ts +206 -0
  93. package/src/{react/toUIMessages.test.ts → toUIMessages.test.ts} +3 -3
  94. package/src/validators.ts +3 -0
  95. package/dist/client/textStreamParts.d.ts +0 -5
  96. package/dist/client/textStreamParts.d.ts.map +0 -1
  97. package/dist/client/textStreamParts.js +0 -44
  98. package/dist/client/textStreamParts.js.map +0 -1
  99. package/dist/react/deltas.d.ts +0 -27
  100. package/dist/react/deltas.d.ts.map +0 -1
  101. package/dist/react/deltas.js +0 -627
  102. package/dist/react/deltas.js.map +0 -1
  103. package/dist/react/fromUIMessages.d.ts +0 -13
  104. package/dist/react/fromUIMessages.d.ts.map +0 -1
  105. package/dist/react/fromUIMessages.js +0 -70
  106. package/dist/react/fromUIMessages.js.map +0 -1
  107. package/dist/react/toUIMessages.d.ts +0 -19
  108. package/dist/react/toUIMessages.d.ts.map +0 -1
  109. package/dist/react/toUIMessages.js.map +0 -1
  110. package/src/client/textStreamParts.ts +0 -62
  111. package/src/react/deltas.ts +0 -774
  112. package/src/react/fromUIMessages.ts +0 -85
@@ -1,11 +1,12 @@
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";
1
+ import { describe, expect, it } from "vitest";
2
+ import type { MessageDoc } from "./client/index.js";
3
+ import type { UIMessage } from "./UIMessages.js";
4
+ import { fromUIMessages, toUIMessages } from "./UIMessages.js";
6
5
 
7
6
  // Helper to create a base message doc
8
- function baseMessageDoc<T = unknown>(overrides: Partial<MessageDoc & { streaming?: boolean; metadata?: T }> = {}): MessageDoc & { streaming?: boolean; metadata?: T } {
7
+ function baseMessageDoc<T = unknown>(
8
+ overrides: Partial<MessageDoc & { streaming?: boolean; metadata?: T }> = {},
9
+ ): MessageDoc & { streaming?: boolean; metadata?: T } {
9
10
  return {
10
11
  _id: "msg1",
11
12
  _creationTime: Date.now(),
@@ -31,7 +32,9 @@ describe("fromUIMessages round-trip tests", () => {
31
32
  ];
32
33
 
33
34
  const uiMessages = toUIMessages(originalMessages);
34
- const backToMessageDocs = fromUIMessages("thread1", uiMessages);
35
+ const backToMessageDocs = fromUIMessages(uiMessages, {
36
+ threadId: "thread1",
37
+ });
35
38
 
36
39
  expect(uiMessages).toHaveLength(1);
37
40
  expect(uiMessages[0].role).toBe("user");
@@ -63,7 +66,9 @@ describe("fromUIMessages round-trip tests", () => {
63
66
  ];
64
67
 
65
68
  const uiMessages = toUIMessages(originalMessages);
66
- const backToMessageDocs = fromUIMessages("thread1", uiMessages);
69
+ const backToMessageDocs = fromUIMessages(uiMessages, {
70
+ threadId: "thread1",
71
+ });
67
72
 
68
73
  expect(uiMessages).toHaveLength(1);
69
74
  expect(uiMessages[0].role).toBe("assistant");
@@ -85,7 +90,9 @@ describe("fromUIMessages round-trip tests", () => {
85
90
  ];
86
91
 
87
92
  const uiMessages = toUIMessages(originalMessages);
88
- const backToMessageDocs = fromUIMessages("thread1", uiMessages);
93
+ const backToMessageDocs = fromUIMessages(uiMessages, {
94
+ threadId: "thread1",
95
+ });
89
96
 
90
97
  expect(uiMessages).toHaveLength(1);
91
98
  expect(uiMessages[0].role).toBe("system");
@@ -123,7 +130,9 @@ describe("fromUIMessages round-trip tests", () => {
123
130
  ];
124
131
 
125
132
  const uiMessages = toUIMessages(originalMessages);
126
- const backToMessageDocs = fromUIMessages("thread1", uiMessages);
133
+ const backToMessageDocs = fromUIMessages(uiMessages, {
134
+ threadId: "thread1",
135
+ });
127
136
 
128
137
  expect(uiMessages).toHaveLength(1);
129
138
  expect(uiMessages[0].text).toBe("Here's my response.");
@@ -184,35 +193,41 @@ describe("fromUIMessages round-trip tests", () => {
184
193
  tool: true,
185
194
  }),
186
195
  ];
196
+ const toTest = [originalMessages, [...originalMessages].reverse()];
197
+ for (const messages of toTest) {
198
+ const uiMessages = toUIMessages(messages);
199
+ const backToMessageDocs = fromUIMessages(uiMessages, {
200
+ threadId: "thread1",
201
+ });
187
202
 
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
- });
203
+ // Should be grouped into single UI message
204
+ expect(uiMessages).toHaveLength(1);
205
+ const uiMessage = uiMessages[0];
206
+ expect(uiMessage.role).toBe("assistant");
207
+ expect(uiMessage.id).toBe("msg1");
208
+
209
+ // Check tool parts exist
210
+ const toolParts = uiMessage.parts.filter(
211
+ (part) => part.type === "tool-calculator",
212
+ );
213
+ expect(toolParts).toHaveLength(1);
214
+ expect(toolParts[0]).toMatchObject({
215
+ type: "tool-calculator",
216
+ toolCallId: "call1",
217
+ state: "output-available",
218
+ input: { operation: "add", a: 2, b: 3 },
219
+ output: { result: 5 },
220
+ });
207
221
 
208
- // Should expand back to multiple message docs
209
- expect(backToMessageDocs.length).toBeGreaterThanOrEqual(1);
222
+ // Should expand back to multiple message docs
223
+ expect(backToMessageDocs.length).toBeGreaterThanOrEqual(1);
210
224
 
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);
225
+ // Check that tool information is preserved
226
+ const toolMessages = backToMessageDocs.filter((msg) => msg.tool);
227
+ expect(toolMessages.length).toBeGreaterThan(0);
228
+ expect(toolMessages[0].stepOrder).toBe(1);
229
+ expect(toolMessages[1].stepOrder).toBe(2);
230
+ }
216
231
  });
217
232
 
218
233
  it("preserves file attachments in user messages", () => {
@@ -237,7 +252,9 @@ describe("fromUIMessages round-trip tests", () => {
237
252
  ];
238
253
 
239
254
  const uiMessages = toUIMessages(originalMessages);
240
- const backToMessageDocs = fromUIMessages("thread1", uiMessages);
255
+ const backToMessageDocs = fromUIMessages(uiMessages, {
256
+ threadId: "thread1",
257
+ });
241
258
 
242
259
  expect(uiMessages).toHaveLength(1);
243
260
  expect(uiMessages[0].role).toBe("user");
@@ -298,7 +315,9 @@ describe("fromUIMessages round-trip tests", () => {
298
315
  ];
299
316
 
300
317
  const uiMessages = toUIMessages(originalMessages);
301
- const backToMessageDocs = fromUIMessages("thread1", uiMessages);
318
+ const backToMessageDocs = fromUIMessages(uiMessages, {
319
+ threadId: "thread1",
320
+ });
302
321
 
303
322
  expect(uiMessages).toHaveLength(1);
304
323
 
@@ -337,7 +356,9 @@ describe("fromUIMessages round-trip tests", () => {
337
356
  ];
338
357
 
339
358
  const uiMessages = toUIMessages(originalMessages);
340
- const backToMessageDocs = fromUIMessages("thread1", uiMessages);
359
+ const backToMessageDocs = fromUIMessages(uiMessages, {
360
+ threadId: "thread1",
361
+ });
341
362
 
342
363
  expect(uiMessages).toHaveLength(1);
343
364
  expect(uiMessages[0].metadata).toEqual(testMetadata);
@@ -360,7 +381,9 @@ describe("fromUIMessages round-trip tests", () => {
360
381
  ];
361
382
 
362
383
  const uiMessages = toUIMessages(originalMessages);
363
- const backToMessageDocs = fromUIMessages("thread1", uiMessages);
384
+ const backToMessageDocs = fromUIMessages(uiMessages, {
385
+ threadId: "thread1",
386
+ });
364
387
 
365
388
  expect(uiMessages).toHaveLength(1);
366
389
  expect(uiMessages[0].status).toBe("streaming");
@@ -374,7 +397,7 @@ describe("fromUIMessages round-trip tests", () => {
374
397
  describe("fromUIMessages functionality tests", () => {
375
398
  it("handles empty messages array", () => {
376
399
  const uiMessages: UIMessage[] = [];
377
- const result = fromUIMessages("thread1", uiMessages);
400
+ const result = fromUIMessages(uiMessages, { threadId: "thread1" });
378
401
  expect(result).toHaveLength(0);
379
402
  });
380
403
 
@@ -391,7 +414,9 @@ describe("fromUIMessages functionality tests", () => {
391
414
  parts: [{ type: "text", text: "Hello" }],
392
415
  };
393
416
 
394
- const result = fromUIMessages("custom-thread-id", [uiMessage]);
417
+ const result = fromUIMessages([uiMessage], {
418
+ threadId: "custom-thread-id",
419
+ });
395
420
  expect(result).toHaveLength(1);
396
421
  expect(result[0].threadId).toBe("custom-thread-id");
397
422
  });
@@ -417,11 +442,51 @@ describe("fromUIMessages functionality tests", () => {
417
442
  ],
418
443
  };
419
444
 
420
- const result = fromUIMessages("thread1", [toolUIMessage]);
445
+ const result = fromUIMessages([toolUIMessage], { threadId: "thread1" });
421
446
  expect(result.length).toBeGreaterThan(0);
422
447
 
423
448
  // Should have tool messages
424
449
  const toolMessages = result.filter((msg) => msg.tool);
425
450
  expect(toolMessages.length).toBeGreaterThan(0);
426
451
  });
452
+
453
+ it("handles tool calls without responses", () => {
454
+ const toolUIMessage: UIMessage = {
455
+ id: "tool-id",
456
+ _creationTime: Date.now(),
457
+ order: 0,
458
+ stepOrder: 0,
459
+ status: "success",
460
+ key: "tool-key",
461
+ text: "",
462
+ role: "assistant",
463
+ parts: [
464
+ { type: "text", text: "Tool call" },
465
+ {
466
+ type: "tool-calculator",
467
+ toolCallId: "call1",
468
+ input: { a: 1, b: 2 },
469
+ state: "input-available",
470
+ },
471
+ ],
472
+ };
473
+
474
+ const result = fromUIMessages([toolUIMessage], { threadId: "thread1" });
475
+ expect(result.length).toBeGreaterThan(0);
476
+
477
+ // Should have tool messages
478
+ const toolMessages = result.filter((msg) => msg.tool);
479
+ expect(toolMessages.length).toBe(1);
480
+ expect(toolMessages[0].message?.role).toBe("assistant");
481
+ expect(toolMessages[0].message?.content[0]).toMatchObject({
482
+ type: "text",
483
+ text: "Tool call",
484
+ });
485
+ expect(toolMessages[0].message?.content[1]).toMatchObject({
486
+ args: { a: 1, b: 2 },
487
+ toolCallId: "call1",
488
+ toolName: "calculator",
489
+ type: "tool-call",
490
+ });
491
+ });
427
492
  });
@@ -1 +1 @@
1
- {"version":"3.2.4","results":[[":client/index.test.ts",{"duration":129.9694999999997,"failed":false}],[":component/messages.test.ts",{"duration":191.73391600000014,"failed":false}],[":component/users.test.ts",{"duration":545.7272500000001,"failed":false}],[":mapping.test.ts",{"duration":9.238791999999876,"failed":false}],[":react/deltas.test.ts",{"duration":5.8497500000000855,"failed":false}],[":react/toUIMessages.test.ts",{"duration":6.495208000000048,"failed":false}],[":validators.test.ts",{"duration":1.0037499999999682,"failed":false}],[":deltas.test.ts",{"duration":0,"failed":false}]]}
1
+ {"version":"3.2.4","results":[[":client/index.test.ts",{"duration":129.9694999999997,"failed":false}],[":component/messages.test.ts",{"duration":191.73391600000014,"failed":false}],[":component/users.test.ts",{"duration":545.7272500000001,"failed":false}],[":mapping.test.ts",{"duration":9.238791999999876,"failed":false}],[":react/deltas.test.ts",{"duration":5.8497500000000855,"failed":false}],[":react/toUIMessages.test.ts",{"duration":6.495208000000048,"failed":false}],[":validators.test.ts",{"duration":1.0037499999999682,"failed":false}],[":deltas.test.ts",{"duration":0,"failed":false}],[":client/streaming.test.ts",{"duration":177.87866699695587,"failed":false}]]}
@@ -1,259 +1,17 @@
1
1
  "use client";
2
- import { omit, type ErrorMessage } from "convex-helpers";
3
- import {
4
- type PaginatedQueryArgs,
5
- type UsePaginatedQueryResult,
6
- useQuery,
7
- } from "convex/react";
8
- import { usePaginatedQuery } from "convex-helpers/react";
9
- import type { FunctionArgs } from "convex/server";
10
2
  import { useMemo, useState } from "react";
11
- import type { MessageDoc } from "../client/index.js";
12
- import type { SyncStreamsReturnValue } from "../client/types.js";
13
- import type { StreamArgs } from "../validators.js";
14
- import type { UIMessage } from "./toUIMessages.js";
15
- import { toUIMessages } from "./toUIMessages.js";
16
- import { mergeDeltas } from "./deltas.js";
17
- import type {
18
- ThreadQuery,
19
- ThreadStreamQuery,
20
- ThreadMessagesArgs,
21
- ThreadMessagesResult,
22
- } from "./types.js";
3
+ export { toUIMessages, type UIMessage } from "../UIMessages.js";
23
4
 
24
5
  export { optimisticallySendMessage } from "./optimisticallySendMessage.js";
25
6
  export { useSmoothText } from "./useSmoothText.js";
26
7
  export { SmoothText } from "./SmoothText.js";
27
- export { toUIMessages, type UIMessage, type ThreadQuery };
28
-
29
- /**
30
- * A hook that fetches messages from a thread.
31
- *
32
- * This hook is a wrapper around `usePaginatedQuery` and `useStreamingThreadMessages`.
33
- * It will fetch both full messages and streaming messages, and merge them together.
34
- *
35
- * The query must take as arguments `{ threadId, paginationOpts }` and return a
36
- * pagination result of objects that extend `MessageDoc`.
37
- *
38
- * For streaming, it should look like this:
39
- * ```ts
40
- * export const listThreadMessages = query({
41
- * args: {
42
- * threadId: v.string(),
43
- * paginationOpts: paginationOptsValidator,
44
- * streamArgs: vStreamArgs,
45
- * ... other arguments you want
46
- * },
47
- * handler: async (ctx, { threadId, paginationOpts, streamArgs }) => {
48
- * // await authorizeThreadAccess(ctx, threadId);
49
- * const paginated = await agent.listMessages(ctx, { threadId, paginationOpts });
50
- * const streams = await agent.syncStreams(ctx, { threadId, streamArgs });
51
- * // Here you could filter out / modify the documents & stream deltas.
52
- * return { ...paginated, streams };
53
- * },
54
- * });
55
- * ```
56
- *
57
- * Then the hook can be used like this:
58
- * ```ts
59
- * const messages = useThreadMessages(
60
- * api.myModule.listThreadMessages,
61
- * { threadId },
62
- * { initialNumItems: 10, stream: true }
63
- * );
64
- * ```
65
- *
66
- * @param query The query to use to fetch messages.
67
- * It must take as arguments `{ threadId, paginationOpts }` and return a
68
- * pagination result of objects that extend `MessageDoc`.
69
- * To support streaming, it must also take in `streamArgs: vStreamArgs` and
70
- * return a `streams` object returned from `agent.syncStreams`.
71
- * @param args The arguments to pass to the query other than `paginationOpts`
72
- * and `streamArgs`. So `{ threadId }` at minimum, plus any other arguments that
73
- * you want to pass to the query.
74
- * @param options The options for the query. Similar to usePaginatedQuery.
75
- * To enable streaming, pass `stream: true`.
76
- * @returns The messages. If stream is true, it will return a list of messages
77
- * that includes both full messages and streaming messages.
78
- */
79
- export function useThreadMessages<
80
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
81
- Query extends ThreadQuery<any, any>,
82
- >(
83
- query: Query,
84
- args: ThreadMessagesArgs<Query> | "skip",
85
- options: {
86
- initialNumItems: number;
87
- stream?: Query extends ThreadStreamQuery
88
- ? boolean
89
- : ErrorMessage<"To enable streaming, your query must take in streamArgs: vStreamArgs and return a streams object returned from agent.syncStreams. See docs.">;
90
- },
91
- ): UsePaginatedQueryResult<
92
- ThreadMessagesResult<Query> & { streaming?: boolean }
93
- > {
94
- // These are full messages
95
- const paginated = usePaginatedQuery(
96
- query,
97
- args as PaginatedQueryArgs<Query> | "skip",
98
- { initialNumItems: options.initialNumItems },
99
- );
100
-
101
- let startOrder = paginated.results.at(-1)?.order ?? 0;
102
- for (let i = paginated.results.length - 1; i >= 0; i--) {
103
- const m = paginated.results[i];
104
- if (!m.streaming && m.status === "pending") {
105
- // round down to the nearest 10 for some cache benefits
106
- startOrder = m.order - (m.order % 10);
107
- break;
108
- }
109
- }
110
- // These are streaming messages that will not include full messages.
111
- const streamMessages = useStreamingThreadMessages(
112
- query as ThreadStreamQuery<
113
- ThreadMessagesArgs<Query>,
114
- ThreadMessagesResult<Query>
115
- >,
116
- !options.stream ||
117
- args === "skip" ||
118
- paginated.status === "LoadingFirstPage"
119
- ? "skip"
120
- : { ...args, startOrder },
121
- );
122
-
123
- const merged = useMemo(() => {
124
- const streamListMessages =
125
- streamMessages?.map((m) => ({
126
- ...m,
127
- streaming: !m.status || m.status === "pending",
128
- })) ?? [];
129
- return {
130
- ...paginated,
131
- results: paginated.results
132
- .map((m) => ({ ...m, streaming: false }))
133
- // Note: this is intentionally after paginated results.
134
- .concat(streamListMessages)
135
- .sort((a, b) =>
136
- a.order === b.order ? a.stepOrder - b.stepOrder : a.order - b.order,
137
- )
138
- .reduce(
139
- (msgs, msg) => {
140
- const last = msgs.at(-1);
141
- if (!last) {
142
- return [msg];
143
- }
144
- if (last.order !== msg.order || last.stepOrder !== msg.stepOrder) {
145
- return [...msgs, msg];
146
- }
147
- if (
148
- last.status === "pending" &&
149
- (msg.streaming || msg.status !== "pending")
150
- ) {
151
- // Let's prefer a streaming or finalized message over a pending
152
- // one.
153
- return [...msgs.slice(0, -1), msg];
154
- }
155
- // skip the new one if the previous one (listed) was finalized
156
- return msgs;
157
- },
158
- [] as (ThreadMessagesResult<Query> & { streaming: boolean })[],
159
- ),
160
- };
161
- }, [paginated, streamMessages]);
162
-
163
- return merged as ThreadMessagesResult<Query>;
164
- }
165
-
166
- /**
167
- * A hook that fetches streaming messages from a thread.
168
- * This ONLY returns streaming messages. To get both, use `useThreadMessages`.
169
- *
170
- * @param query The query to use to fetch messages.
171
- * It must take as arguments `{ threadId, paginationOpts, streamArgs }` and
172
- * return a `streams` object returned from `agent.syncStreams`.
173
- * @param args The arguments to pass to the query other than `paginationOpts`
174
- * and `streamArgs`. So `{ threadId }` at minimum, plus any other arguments that
175
- * you want to pass to the query.
176
- * @returns The streaming messages.
177
- */
178
- export function useStreamingThreadMessages<
179
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
180
- Query extends ThreadStreamQuery<any, any>,
181
- >(
182
- query: Query,
183
- args: (ThreadMessagesArgs<Query> & { startOrder?: number }) | "skip",
184
- ): Array<ThreadMessagesResult<Query>> | undefined {
185
- // Invariant: streamMessages[streamId] is comprised of all deltas up to the
186
- // cursor. There can be multiple messages in the same stream, e.g. for tool
187
- // calls.
188
- const [streams, setStreams] = useState<
189
- Array<{ streamId: string; cursor: number; messages: MessageDoc[] }>
190
- >([]);
191
- const queryArgs = args === "skip" ? args : omit(args, ["startOrder"]);
192
- // Get all the active streams
193
- const streamList = useQuery(
194
- query,
195
- queryArgs === "skip"
196
- ? queryArgs
197
- : ({
198
- ...queryArgs,
199
- paginationOpts: { cursor: null, numItems: 0 },
200
- streamArgs: {
201
- kind: "list",
202
- startOrder: queryArgs.startOrder ?? 0,
203
- } as StreamArgs,
204
- } as FunctionArgs<Query>),
205
- ) as
206
- | { streams: Extract<SyncStreamsReturnValue, { kind: "list" }> }
207
- | undefined;
208
- // Get the cursors for all the active streams
209
- const cursors = useMemo(() => {
210
- if (!streamList?.streams) return [];
211
- if (streamList.streams.kind !== "list") {
212
- throw new Error("Expected list streams");
213
- }
214
- return streamList.streams.messages.map(({ streamId }) => {
215
- const stream = streams.find((s) => s.streamId === streamId);
216
- const cursor = stream?.cursor ?? 0;
217
- return { streamId, cursor };
218
- });
219
- }, [streamList, streams]);
220
- // Get the deltas for all the active streams, if any.
221
- const cursorQuery = useQuery(
222
- query,
223
- queryArgs === "skip" || !streamList
224
- ? ("skip" as const)
225
- : ({
226
- ...queryArgs,
227
- paginationOpts: { cursor: null, numItems: 0 },
228
- streamArgs: { kind: "deltas", cursors } as StreamArgs,
229
- } as FunctionArgs<Query>),
230
- ) as
231
- | { streams: Extract<SyncStreamsReturnValue, { kind: "deltas" }> }
232
- | undefined;
233
- // Merge any deltas into the streamChunks, keeping it unmodified if unchanged.
234
- const threadId = args === "skip" ? undefined : args.threadId;
235
- const [messages, newStreams, changed] = useMemo(() => {
236
- if (!threadId) return [undefined, [], false];
237
- if (!streamList) return [undefined, [], false];
238
- if (cursorQuery && cursorQuery.streams?.kind !== "deltas") {
239
- throw new Error("Expected deltas streams");
240
- }
241
- return mergeDeltas(
242
- threadId,
243
- streamList.streams.messages,
244
- streams,
245
- cursorQuery?.streams?.deltas ?? [],
246
- );
247
- }, [threadId, cursorQuery, streams, streamList]);
248
- // Now assemble the chunks into messages
249
- if (!threadId) {
250
- return undefined;
251
- }
252
- if (changed) {
253
- setStreams(newStreams);
254
- }
255
- return messages as ThreadMessagesResult<Query>[] | undefined;
256
- }
8
+ export {
9
+ type ThreadMessagesQuery,
10
+ useThreadMessages,
11
+ useStreamingThreadMessages,
12
+ } from "./useThreadMessages.js";
13
+ export { type UIMessagesQuery, useUIMessages } from "./useUIMessages.js";
14
+ export { useStreamingUIMessages } from "./useStreamingUIMessages.js";
257
15
 
258
16
  /**
259
17
  * @deprecated use useThreadMessages or useStreamingThreadMessages instead
@@ -1,10 +1,52 @@
1
1
  import { insertAtTop } from "convex/react";
2
- import type { MessageDoc } from "../client/index.js";
2
+ import type { MessageDoc, StreamArgs } from "../validators.js";
3
3
  import type { OptimisticLocalStore } from "convex/browser";
4
- import type { ThreadQuery } from "./types.js";
4
+ import type { UIMessage } from "../UIMessages.js";
5
+ import type {
6
+ FunctionReference,
7
+ PaginationOptions,
8
+ PaginationResult,
9
+ } from "convex/server";
10
+ import type { SyncStreamsReturnValue } from "@convex-dev/agent";
5
11
 
12
+ /**
13
+ * Adds a sent message to the end of a list of messages, so it shows up until
14
+ * the message is saved on the server and arrives in the query.
15
+ * It generates a message with fields that match both MessageDoc and UIMessage,
16
+ * for convenience. It will not include any other fields you might have in your
17
+ * regular query, however.
18
+ *
19
+ * @param query The query used to fetch messages, typically with
20
+ * useThreadMessages or useUIMessages.
21
+ * @returns A function that can be used to optimistically send a message.
22
+ * If your mutation takes different arguments than { threadId, prompt }, you can
23
+ * use it as a helper function in your optimistic update:
24
+ * ```ts
25
+ * const sendMessage = useMutation(
26
+ * api.chatStreaming.streamStoryAsynchronously,
27
+ * ).withOptimisticUpdate(
28
+ * (store, args) => {
29
+ * optimisticallySendMessage(api.chatStreaming.listThreadMessages)(store, {
30
+ * threadId:
31
+ * prompt: whatever you would have passed to the mutation,
32
+ * })
33
+ * }
34
+ * );
35
+ * ```
36
+ */
6
37
  export function optimisticallySendMessage(
7
- query: ThreadQuery<unknown, MessageDoc>,
38
+ query: FunctionReference<
39
+ "query",
40
+ "public",
41
+ {
42
+ threadId: string;
43
+ paginationOpts: PaginationOptions;
44
+ streamArgs?: StreamArgs;
45
+ },
46
+ PaginationResult<MessageDoc | UIMessage> & {
47
+ streams?: SyncStreamsReturnValue;
48
+ }
49
+ >,
8
50
  ): (
9
51
  store: OptimisticLocalStore,
10
52
  args: { threadId: string; prompt: string },
@@ -20,19 +62,26 @@ export function optimisticallySendMessage(
20
62
  }
21
63
  }
22
64
  const order = maxOrder + 1;
65
+ const stepOrder = 0;
66
+ const id = randomUUID();
67
+ const { prompt, ...rest } = args;
23
68
  insertAtTop({
24
69
  paginatedQuery: query,
25
70
  argsToMatch: { threadId: args.threadId, streamArgs: undefined },
26
71
  item: {
72
+ ...rest,
27
73
  _creationTime: Date.now(),
28
- _id: randomUUID(),
74
+ _id: id,
75
+ id,
76
+ key: `${args.threadId}-${order}-${stepOrder}`,
29
77
  order,
30
- stepOrder: 0,
78
+ stepOrder,
31
79
  status: "pending",
32
- threadId: args.threadId,
33
80
  tool: false,
34
- message: { role: "user", content: args.prompt },
35
- text: args.prompt,
81
+ message: { role: "user", content: prompt },
82
+ parts: [{ type: "text", text: prompt }],
83
+ role: "user",
84
+ text: prompt,
36
85
  },
37
86
  localQueryStore: store,
38
87
  });
@@ -1,52 +1,19 @@
1
1
  import type { BetterOmit, Expand } from "convex-helpers";
2
- import type {
3
- FunctionArgs,
4
- FunctionReference,
5
- PaginationOptions,
6
- PaginationResult,
7
- } from "convex/server";
8
- import type { MessageDoc } from "../client/index.js";
2
+ import type { FunctionArgs, FunctionReference } from "convex/server";
9
3
  import type { SyncStreamsReturnValue } from "../client/types.js";
10
4
  import type { StreamArgs } from "../validators.js";
11
5
 
12
- export type ThreadQuery<
13
- Args = unknown,
14
- M extends MessageDoc = MessageDoc,
15
- > = FunctionReference<
6
+ export type StreamQuery<Args = Record<string, unknown>> = FunctionReference<
16
7
  "query",
17
8
  "public",
18
9
  {
19
10
  threadId: string;
20
- paginationOpts: PaginationOptions;
21
- // TODO: will this allow passing a function that doesn't have this param?
22
- /**
23
- * If { stream: true } is passed, it will also query for stream deltas.
24
- * In order for this to work, the query must take as an argument streamArgs.
25
- */
26
- streamArgs?: StreamArgs;
27
- } & Args,
28
- PaginationResult<M> & { streams?: SyncStreamsReturnValue }
29
- >;
30
-
31
- export type ThreadStreamQuery<
32
- Args = Record<string, unknown>,
33
- M extends MessageDoc = MessageDoc,
34
- > = FunctionReference<
35
- "query",
36
- "public",
37
- {
38
- threadId: string;
39
- paginationOpts: PaginationOptions;
40
11
  streamArgs?: StreamArgs; // required for stream query
41
12
  } & Args,
42
- PaginationResult<M> & { streams: SyncStreamsReturnValue }
13
+ { streams: SyncStreamsReturnValue }
43
14
  >;
44
15
 
45
- export type ThreadMessagesArgs<Query extends ThreadQuery<unknown, MessageDoc>> =
46
- Query extends ThreadQuery<unknown, MessageDoc>
47
- ? Expand<BetterOmit<FunctionArgs<Query>, "paginationOpts" | "streamArgs">>
16
+ export type StreamMessagesArgs<Query extends StreamQuery<unknown>> =
17
+ Query extends StreamQuery<unknown>
18
+ ? Expand<BetterOmit<FunctionArgs<Query>, "streamArgs">>
48
19
  : never;
49
-
50
- export type ThreadMessagesResult<
51
- Query extends ThreadQuery<unknown, MessageDoc>,
52
- > = Query extends ThreadQuery<unknown, infer M> ? M : never;