@convex-dev/agent 0.5.0-alpha.1 → 0.6.0-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 (233) hide show
  1. package/README.md +32 -27
  2. package/dist/UIMessages.d.ts +46 -0
  3. package/dist/UIMessages.d.ts.map +1 -0
  4. package/dist/UIMessages.js +546 -0
  5. package/dist/UIMessages.js.map +1 -0
  6. package/dist/client/createTool.d.ts +129 -27
  7. package/dist/client/createTool.d.ts.map +1 -1
  8. package/dist/client/createTool.js +66 -12
  9. package/dist/client/createTool.js.map +1 -1
  10. package/dist/client/defaultComponent.d.ts +11 -0
  11. package/dist/client/defaultComponent.d.ts.map +1 -0
  12. package/dist/client/defaultComponent.js +7 -0
  13. package/dist/client/defaultComponent.js.map +1 -0
  14. package/dist/client/definePlaygroundAPI.d.ts +1323 -192
  15. package/dist/client/definePlaygroundAPI.d.ts.map +1 -1
  16. package/dist/client/definePlaygroundAPI.js +52 -28
  17. package/dist/client/definePlaygroundAPI.js.map +1 -1
  18. package/dist/client/files.d.ts +20 -7
  19. package/dist/client/files.d.ts.map +1 -1
  20. package/dist/client/files.js +68 -11
  21. package/dist/client/files.js.map +1 -1
  22. package/dist/client/index.d.ts +1056 -965
  23. package/dist/client/index.d.ts.map +1 -1
  24. package/dist/client/index.js +242 -748
  25. package/dist/client/index.js.map +1 -1
  26. package/dist/client/messages.d.ts +461 -0
  27. package/dist/client/messages.d.ts.map +1 -0
  28. package/dist/client/messages.js +106 -0
  29. package/dist/client/messages.js.map +1 -0
  30. package/dist/client/mockModel.d.ts +42 -0
  31. package/dist/client/mockModel.d.ts.map +1 -0
  32. package/dist/client/mockModel.js +175 -0
  33. package/dist/client/mockModel.js.map +1 -0
  34. package/dist/client/saveInputMessages.d.ts +20 -0
  35. package/dist/client/saveInputMessages.d.ts.map +1 -0
  36. package/dist/client/saveInputMessages.js +58 -0
  37. package/dist/client/saveInputMessages.js.map +1 -0
  38. package/dist/client/search.d.ts +346 -35
  39. package/dist/client/search.d.ts.map +1 -1
  40. package/dist/client/search.js +350 -39
  41. package/dist/client/search.js.map +1 -1
  42. package/dist/client/start.d.ts +84 -0
  43. package/dist/client/start.d.ts.map +1 -0
  44. package/dist/client/start.js +171 -0
  45. package/dist/client/start.js.map +1 -0
  46. package/dist/client/streamText.d.ts +46 -0
  47. package/dist/client/streamText.d.ts.map +1 -0
  48. package/dist/client/streamText.js +93 -0
  49. package/dist/client/streamText.js.map +1 -0
  50. package/dist/client/streaming.d.ts +3705 -32
  51. package/dist/client/streaming.d.ts.map +1 -1
  52. package/dist/client/streaming.js +141 -59
  53. package/dist/client/streaming.js.map +1 -1
  54. package/dist/client/threads.d.ts +46 -0
  55. package/dist/client/threads.d.ts.map +1 -0
  56. package/dist/client/threads.js +49 -0
  57. package/dist/client/threads.js.map +1 -0
  58. package/dist/client/types.d.ts +265 -128
  59. package/dist/client/types.d.ts.map +1 -1
  60. package/dist/client/utils.d.ts +4 -0
  61. package/dist/client/utils.d.ts.map +1 -0
  62. package/dist/client/utils.js +21 -0
  63. package/dist/client/utils.js.map +1 -0
  64. package/dist/component/_generated/api.d.ts +24 -2178
  65. package/dist/component/_generated/api.d.ts.map +1 -1
  66. package/dist/component/_generated/api.js +10 -1
  67. package/dist/component/_generated/api.js.map +1 -1
  68. package/dist/component/_generated/component.d.ts +3119 -0
  69. package/dist/component/_generated/component.d.ts.map +1 -0
  70. package/dist/component/_generated/component.js +11 -0
  71. package/dist/component/_generated/component.js.map +1 -0
  72. package/dist/component/_generated/dataModel.d.ts +4 -18
  73. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  74. package/dist/component/_generated/dataModel.js +11 -0
  75. package/dist/component/_generated/dataModel.js.map +1 -0
  76. package/dist/component/_generated/server.d.ts +10 -38
  77. package/dist/component/_generated/server.d.ts.map +1 -1
  78. package/dist/component/_generated/server.js +9 -5
  79. package/dist/component/_generated/server.js.map +1 -1
  80. package/dist/component/files.d.ts +16 -10
  81. package/dist/component/files.d.ts.map +1 -1
  82. package/dist/component/files.js +10 -2
  83. package/dist/component/files.js.map +1 -1
  84. package/dist/component/messages.d.ts +2553 -342
  85. package/dist/component/messages.d.ts.map +1 -1
  86. package/dist/component/messages.js +387 -154
  87. package/dist/component/messages.js.map +1 -1
  88. package/dist/component/schema.d.ts +5697 -3584
  89. package/dist/component/schema.d.ts.map +1 -1
  90. package/dist/component/schema.js +18 -41
  91. package/dist/component/schema.js.map +1 -1
  92. package/dist/component/streams.d.ts +35 -335
  93. package/dist/component/streams.d.ts.map +1 -1
  94. package/dist/component/streams.js +114 -73
  95. package/dist/component/streams.js.map +1 -1
  96. package/dist/component/threads.d.ts +16 -16
  97. package/dist/component/users.d.ts +4 -4
  98. package/dist/component/vector/index.d.ts +1 -1
  99. package/dist/component/vector/index.d.ts.map +1 -1
  100. package/dist/component/vector/index.js +1 -3
  101. package/dist/component/vector/index.js.map +1 -1
  102. package/dist/deltas.d.ts +43 -0
  103. package/dist/deltas.d.ts.map +1 -0
  104. package/dist/deltas.js +447 -0
  105. package/dist/deltas.js.map +1 -0
  106. package/dist/mapping.d.ts +20 -20
  107. package/dist/mapping.d.ts.map +1 -1
  108. package/dist/mapping.js +313 -96
  109. package/dist/mapping.js.map +1 -1
  110. package/dist/react/SmoothText.d.ts +5 -0
  111. package/dist/react/SmoothText.d.ts.map +1 -0
  112. package/dist/react/SmoothText.js +6 -0
  113. package/dist/react/SmoothText.js.map +1 -0
  114. package/dist/react/index.d.ts +5 -77
  115. package/dist/react/index.d.ts.map +1 -1
  116. package/dist/react/index.js +6 -160
  117. package/dist/react/index.js.map +1 -1
  118. package/dist/react/optimisticallySendMessage.d.ts +36 -3
  119. package/dist/react/optimisticallySendMessage.d.ts.map +1 -1
  120. package/dist/react/optimisticallySendMessage.js +35 -9
  121. package/dist/react/optimisticallySendMessage.js.map +1 -1
  122. package/dist/react/types.d.ts +4 -18
  123. package/dist/react/types.d.ts.map +1 -1
  124. package/dist/react/useDeltaStreams.d.ts +10 -0
  125. package/dist/react/useDeltaStreams.d.ts.map +1 -0
  126. package/dist/react/useDeltaStreams.js +101 -0
  127. package/dist/react/useDeltaStreams.js.map +1 -0
  128. package/dist/react/useSmoothText.d.ts +13 -12
  129. package/dist/react/useSmoothText.d.ts.map +1 -1
  130. package/dist/react/useSmoothText.js +32 -15
  131. package/dist/react/useSmoothText.js.map +1 -1
  132. package/dist/react/useStreamingUIMessages.d.ts +22 -0
  133. package/dist/react/useStreamingUIMessages.d.ts.map +1 -0
  134. package/dist/react/useStreamingUIMessages.js +92 -0
  135. package/dist/react/useStreamingUIMessages.js.map +1 -0
  136. package/dist/react/useThreadMessages.d.ts +104 -0
  137. package/dist/react/useThreadMessages.d.ts.map +1 -0
  138. package/dist/react/useThreadMessages.js +148 -0
  139. package/dist/react/useThreadMessages.js.map +1 -0
  140. package/dist/react/useUIMessages.d.ts +96 -0
  141. package/dist/react/useUIMessages.d.ts.map +1 -0
  142. package/dist/react/useUIMessages.js +108 -0
  143. package/dist/react/useUIMessages.js.map +1 -0
  144. package/dist/shared.d.ts +20 -4
  145. package/dist/shared.d.ts.map +1 -1
  146. package/dist/shared.js +45 -8
  147. package/dist/shared.js.map +1 -1
  148. package/dist/validators.d.ts +22981 -5666
  149. package/dist/validators.d.ts.map +1 -1
  150. package/dist/validators.js +245 -137
  151. package/dist/validators.js.map +1 -1
  152. package/package.json +98 -50
  153. package/src/UIMessages.combineUIMessages.test.ts +239 -0
  154. package/src/UIMessages.test.ts +273 -0
  155. package/src/UIMessages.ts +739 -0
  156. package/src/client/createTool.ts +293 -76
  157. package/src/client/defaultComponent.ts +17 -0
  158. package/src/client/definePlaygroundAPI.ts +67 -31
  159. package/src/client/files.ts +100 -20
  160. package/src/client/index.test.ts +40 -85
  161. package/src/client/index.ts +520 -1290
  162. package/src/client/messages.ts +237 -0
  163. package/src/client/mockModel.ts +245 -0
  164. package/src/client/saveInputMessages.test.ts +583 -0
  165. package/src/client/saveInputMessages.ts +101 -0
  166. package/src/client/search.test.ts +1207 -0
  167. package/src/client/search.ts +577 -70
  168. package/src/client/start.ts +310 -0
  169. package/src/client/streamText.ts +163 -0
  170. package/src/client/streaming.test.ts +186 -0
  171. package/src/client/streaming.ts +219 -97
  172. package/src/client/threads.ts +83 -0
  173. package/src/client/types.ts +368 -219
  174. package/src/client/utils.ts +27 -0
  175. package/src/component/_generated/api.ts +64 -0
  176. package/src/component/_generated/component.ts +4913 -0
  177. package/src/component/_generated/{server.d.ts → server.ts} +33 -21
  178. package/src/component/files.ts +11 -2
  179. package/src/component/messages.test.ts +195 -51
  180. package/src/component/messages.ts +490 -201
  181. package/src/component/schema.ts +20 -46
  182. package/src/component/setup.test.ts +7 -0
  183. package/src/component/streams.ts +184 -83
  184. package/src/component/users.test.ts +0 -1
  185. package/src/component/vector/index.ts +1 -3
  186. package/src/deltas.test.ts +626 -0
  187. package/src/deltas.ts +570 -0
  188. package/src/fromUIMessages.test.ts +497 -0
  189. package/src/mapping.test.ts +103 -6
  190. package/src/mapping.ts +422 -161
  191. package/src/react/SmoothText.tsx +9 -0
  192. package/src/react/index.ts +10 -230
  193. package/src/react/optimisticallySendMessage.ts +55 -12
  194. package/src/react/types.ts +6 -39
  195. package/src/react/useDeltaStreams.ts +154 -0
  196. package/src/react/useSmoothText.ts +56 -36
  197. package/src/react/useStreamingUIMessages.ts +143 -0
  198. package/src/react/useThreadMessages.ts +262 -0
  199. package/src/react/useUIMessages.test.ts +255 -0
  200. package/src/react/useUIMessages.ts +195 -0
  201. package/src/shared.ts +88 -12
  202. package/src/test.ts +18 -0
  203. package/src/toUIMessages.test.ts +1269 -0
  204. package/src/validators.test.ts +18 -19
  205. package/src/validators.ts +325 -185
  206. package/dist/client/_generated/_ignore.d.ts +0 -1
  207. package/dist/client/_generated/_ignore.d.ts.map +0 -1
  208. package/dist/client/_generated/_ignore.js +0 -3
  209. package/dist/client/_generated/_ignore.js.map +0 -1
  210. package/dist/client/listMessages.d.ts +0 -22
  211. package/dist/client/listMessages.d.ts.map +0 -1
  212. package/dist/client/listMessages.js +0 -25
  213. package/dist/client/listMessages.js.map +0 -1
  214. package/dist/package.json +0 -3
  215. package/dist/react/deltas.d.ts +0 -26
  216. package/dist/react/deltas.d.ts.map +0 -1
  217. package/dist/react/deltas.js +0 -384
  218. package/dist/react/deltas.js.map +0 -1
  219. package/dist/react/toUIMessages.d.ts +0 -15
  220. package/dist/react/toUIMessages.d.ts.map +0 -1
  221. package/dist/react/toUIMessages.js +0 -211
  222. package/dist/react/toUIMessages.js.map +0 -1
  223. package/src/client/listMessages.ts +0 -38
  224. package/src/component/_generated/api.d.ts +0 -2202
  225. package/src/component/_generated/api.js +0 -23
  226. package/src/component/_generated/server.js +0 -90
  227. package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
  228. package/src/react/deltas.test.ts +0 -315
  229. package/src/react/deltas.ts +0 -478
  230. package/src/react/toUIMessages.test.ts +0 -420
  231. package/src/react/toUIMessages.ts +0 -253
  232. package/src/vitest.config.ts +0 -7
  233. /package/src/component/_generated/{dataModel.d.ts → dataModel.ts} +0 -0
@@ -2,7 +2,20 @@ import { useEffect, useRef, useState } from "react";
2
2
 
3
3
  const FPS = 20;
4
4
  const MS_PER_FRAME = 1000 / FPS;
5
- const MAX_TIME_JUMP_MS = 250;
5
+ const INITIAL_CHARS_PER_SEC = 128;
6
+
7
+ export type SmoothTextOptions = {
8
+ /**
9
+ * The number of characters to display per second.
10
+ */
11
+ charsPerSec?: number;
12
+ /**
13
+ * Whether to initially start streaming.
14
+ * If this later turns to false, it'll continue streaming.
15
+ * This will start streaming the first value it sees.
16
+ */
17
+ startStreaming?: boolean;
18
+ };
6
19
  /**
7
20
  * A hook that smoothly displays text as it is streamed.
8
21
  *
@@ -16,28 +29,20 @@ const MAX_TIME_JUMP_MS = 250;
16
29
  export function useSmoothText(
17
30
  text: string,
18
31
  {
19
- charsPerSec = 256,
32
+ charsPerSec = INITIAL_CHARS_PER_SEC,
20
33
  startStreaming = false,
21
- }: {
22
- /**
23
- * The number of characters to display per second.
24
- */
25
- charsPerSec?: number;
26
- /**
27
- * Whether to initially start streaming.
28
- * If this later turns to false, it'll continue streaming.
29
- * This will start streaming the first value it sees.
30
- */
31
- startStreaming?: boolean;
32
- } = {},
34
+ }: SmoothTextOptions = {},
33
35
  ): [string, { cursor: number; isStreaming: boolean }] {
34
- const [visibleText, setVisibleText] = useState(startStreaming ? "" : text);
36
+ const [visibleText, setVisibleText] = useState(
37
+ startStreaming ? "" : text || "",
38
+ );
35
39
  const smoothState = useRef({
36
- tick: Date.now() + (visibleText.length * 1000) / charsPerSec,
40
+ tick: Date.now(),
37
41
  cursor: visibleText.length,
38
- start: Date.now(),
39
- initialLength: visibleText.length,
42
+ lastUpdate: Date.now(),
43
+ lastUpdateLength: text.length,
40
44
  charsPerMs: charsPerSec / 1000,
45
+ initial: true,
41
46
  });
42
47
 
43
48
  const isStreaming = smoothState.current.cursor < text.length;
@@ -46,36 +51,51 @@ export function useSmoothText(
46
51
  if (!isStreaming) {
47
52
  return;
48
53
  }
49
- const latestCharsPerMs =
50
- (text.length - smoothState.current.initialLength) /
51
- (Date.now() - smoothState.current.start);
52
- // Smooth out the charsPerSec by averaging it with the previous value.
53
- smoothState.current.charsPerMs = Math.min(
54
- (2 * latestCharsPerMs + smoothState.current.charsPerMs) / 3,
55
- smoothState.current.charsPerMs * 2,
56
- );
54
+ if (smoothState.current.lastUpdateLength !== text.length) {
55
+ const timeSinceLastUpdate = Date.now() - smoothState.current.lastUpdate;
56
+ const latestCharsPerMs =
57
+ (text.length - smoothState.current.lastUpdateLength) /
58
+ timeSinceLastUpdate;
59
+ // Is the rate increasing?
60
+ const rateError = latestCharsPerMs - smoothState.current.charsPerMs;
61
+ // Is our visible text falling behind what it could show?
62
+ const charLag =
63
+ smoothState.current.lastUpdateLength - smoothState.current.cursor;
64
+ const lagRate = charLag / timeSinceLastUpdate;
65
+ const charsPerMs =
66
+ latestCharsPerMs +
67
+ (smoothState.current.initial
68
+ ? 0
69
+ : Math.max(0, (rateError + lagRate) / 2));
70
+ smoothState.current.initial = false;
71
+ // Smooth out the charsPerSec by weighting it with the previous value.
72
+ smoothState.current.charsPerMs = Math.min(
73
+ (2 * charsPerMs + smoothState.current.charsPerMs) / 3,
74
+ smoothState.current.charsPerMs * 2,
75
+ );
76
+ }
57
77
  smoothState.current.tick = Math.max(
58
78
  smoothState.current.tick,
59
- Date.now() - 2 * MS_PER_FRAME,
79
+ Date.now() - MS_PER_FRAME,
60
80
  );
81
+ smoothState.current.lastUpdate = Date.now();
82
+ smoothState.current.lastUpdateLength = text.length;
61
83
 
62
84
  function update() {
63
85
  if (smoothState.current.cursor >= text.length) {
64
86
  return;
65
87
  }
66
88
  const now = Date.now();
67
- const timeSinceLastUpdate = Math.min(
68
- MAX_TIME_JUMP_MS,
69
- now - smoothState.current.tick,
70
- );
71
- const chars = Math.floor(
89
+ const timeSinceLastUpdate = now - smoothState.current.tick;
90
+ const charsSinceLastUpdate = Math.floor(
72
91
  timeSinceLastUpdate * smoothState.current.charsPerMs,
73
92
  );
74
- smoothState.current.cursor = Math.min(
75
- smoothState.current.cursor + chars,
76
- text.length,
93
+ const chars = Math.min(
94
+ charsSinceLastUpdate,
95
+ text.length - smoothState.current.cursor,
77
96
  );
78
- smoothState.current.tick = now;
97
+ smoothState.current.cursor += chars;
98
+ smoothState.current.tick += chars / smoothState.current.charsPerMs;
79
99
  setVisibleText(text.slice(0, smoothState.current.cursor));
80
100
  }
81
101
  update();
@@ -0,0 +1,143 @@
1
+ "use client";
2
+ import { useMemo, useState, useEffect } from "react";
3
+ import { type UIDataTypes, type UIMessageChunk, type UITools } from "ai";
4
+ import type { StreamQuery, StreamQueryArgs } from "./types.js";
5
+ import { type UIMessage } from "../UIMessages.js";
6
+ import {
7
+ blankUIMessage,
8
+ getParts,
9
+ updateFromUIMessageChunks,
10
+ deriveUIMessagesFromTextStreamParts,
11
+ } from "../deltas.js";
12
+ import { useDeltaStreams } from "./useDeltaStreams.js";
13
+
14
+ // Polyfill structuredClone to support readUIMessageStream on ReactNative
15
+ if (!("structuredClone" in globalThis)) {
16
+ void import("@ungap/structured-clone" as any).then(
17
+ ({ default: structuredClone }) =>
18
+ (globalThis.structuredClone = structuredClone),
19
+ );
20
+ }
21
+
22
+ /**
23
+ * A hook that fetches streaming messages from a thread and converts them to UIMessages
24
+ * using AI SDK's readUIMessageStream.
25
+ * This ONLY returns streaming UIMessages. To get both full and streaming messages,
26
+ * use `useUIMessages`.
27
+ *
28
+ * @param query The query to use to fetch messages.
29
+ * It must take as arguments `{ threadId, paginationOpts, streamArgs }` and
30
+ * return a `streams` object returned from `agent.syncStreams`.
31
+ * @param args The arguments to pass to the query other than `paginationOpts`
32
+ * and `streamArgs`. So `{ threadId }` at minimum, plus any other arguments that
33
+ * you want to pass to the query.
34
+ * @returns The streaming UIMessages.
35
+ */
36
+ export function useStreamingUIMessages<
37
+ METADATA = unknown,
38
+ DATA_PARTS extends UIDataTypes = UIDataTypes,
39
+ TOOLS extends UITools = UITools,
40
+ Query extends StreamQuery<any> = StreamQuery<object>,
41
+ >(
42
+ query: Query,
43
+ args: StreamQueryArgs<Query> | "skip",
44
+ options?: {
45
+ startOrder?: number;
46
+ skipStreamIds?: string[];
47
+ },
48
+ // TODO: make generic on metadata, etc.
49
+ ): UIMessage<METADATA, DATA_PARTS, TOOLS>[] | undefined {
50
+ const [messageState, setMessageState] = useState<
51
+ Record<
52
+ string,
53
+ {
54
+ uiMessage: UIMessage<METADATA, DATA_PARTS, TOOLS>;
55
+ cursor: number;
56
+ }
57
+ >
58
+ >({});
59
+
60
+ const streams = useDeltaStreams(query, args, options);
61
+
62
+ const threadId = args === "skip" ? undefined : args.threadId;
63
+
64
+ useEffect(() => {
65
+ if (!streams) return;
66
+ // return if there are no new deltas beyond the cursors
67
+ let noNewDeltas = true;
68
+ for (const stream of streams) {
69
+ const lastDelta = stream.deltas.at(-1);
70
+ const cursor = messageState[stream.streamMessage.streamId]?.cursor;
71
+ if (!cursor) {
72
+ noNewDeltas = false;
73
+ break;
74
+ }
75
+ if (lastDelta && lastDelta.start >= cursor) {
76
+ noNewDeltas = false;
77
+ break;
78
+ }
79
+ }
80
+ if (noNewDeltas) {
81
+ return;
82
+ }
83
+ const abortController = new AbortController();
84
+ void (async () => {
85
+ const newMessageState: Record<
86
+ string,
87
+ {
88
+ uiMessage: UIMessage<METADATA, DATA_PARTS, TOOLS>;
89
+ cursor: number;
90
+ }
91
+ > = Object.fromEntries(
92
+ await Promise.all(
93
+ streams.map(async ({ deltas, streamMessage }) => {
94
+ const { parts, cursor } = getParts<UIMessageChunk>(deltas, 0);
95
+ if (streamMessage.format === "UIMessageChunk") {
96
+ // Unfortunately this can't handle resuming from a UIMessage and
97
+ // adding more chunks, so we re-create it from scratch each time.
98
+ const uiMessage = await updateFromUIMessageChunks(
99
+ blankUIMessage(streamMessage, threadId),
100
+ parts,
101
+ );
102
+ return [
103
+ streamMessage.streamId,
104
+ {
105
+ uiMessage,
106
+ cursor,
107
+ },
108
+ ];
109
+ } else {
110
+ const [uiMessages] = deriveUIMessagesFromTextStreamParts(
111
+ threadId,
112
+ [streamMessage],
113
+ [],
114
+ deltas,
115
+ );
116
+ return [
117
+ streamMessage.streamId,
118
+ {
119
+ uiMessage: uiMessages[0],
120
+ cursor,
121
+ },
122
+ ];
123
+ }
124
+ }),
125
+ ),
126
+ );
127
+ if (abortController.signal.aborted) return;
128
+ setMessageState(newMessageState);
129
+ })();
130
+ return () => {
131
+ abortController.abort();
132
+ };
133
+ }, [messageState, streams, threadId]);
134
+
135
+ return useMemo(() => {
136
+ if (!streams) return undefined;
137
+ return streams
138
+ .map(
139
+ ({ streamMessage }) => messageState[streamMessage.streamId]?.uiMessage,
140
+ )
141
+ .filter((uiMessage) => uiMessage !== undefined);
142
+ }, [messageState, streams]);
143
+ }
@@ -0,0 +1,262 @@
1
+ "use client";
2
+ import {
3
+ omit,
4
+ type BetterOmit,
5
+ type ErrorMessage,
6
+ type Expand,
7
+ } from "convex-helpers";
8
+ import { usePaginatedQuery } from "convex-helpers/react";
9
+ import {
10
+ type PaginatedQueryArgs,
11
+ type UsePaginatedQueryResult,
12
+ } from "convex/react";
13
+ import type {
14
+ FunctionArgs,
15
+ FunctionReference,
16
+ PaginationOptions,
17
+ PaginationResult,
18
+ } from "convex/server";
19
+ import { useMemo, useState, useEffect, useRef } from "react";
20
+ import type { SyncStreamsReturnValue } from "../client/types.js";
21
+ import { sorted } from "../shared.js";
22
+ import { fromUIMessages } from "../UIMessages.js";
23
+ import type {
24
+ Message,
25
+ MessageDoc,
26
+ MessageStatus,
27
+ StreamArgs,
28
+ } from "../validators.js";
29
+ import type { StreamQueryArgs, StreamQuery } from "./types.js";
30
+ import { useStreamingUIMessages } from "./useStreamingUIMessages.js";
31
+
32
+ export type MessageDocLike = {
33
+ order: number;
34
+ stepOrder: number;
35
+ status: MessageStatus | "streaming";
36
+ message?: Message;
37
+ };
38
+
39
+ export type ThreadMessagesQuery<
40
+ Args = unknown,
41
+ M extends MessageDocLike = MessageDocLike,
42
+ > = FunctionReference<
43
+ "query",
44
+ "public",
45
+ {
46
+ threadId: string;
47
+ paginationOpts: PaginationOptions;
48
+ /**
49
+ * If { stream: true } is passed, it will also query for stream deltas.
50
+ * In order for this to work, the query must take as an argument streamArgs.
51
+ */
52
+ streamArgs?: StreamArgs;
53
+ } & Args,
54
+ PaginationResult<M> & { streams?: SyncStreamsReturnValue }
55
+ >;
56
+
57
+ export type ThreadMessagesArgs<
58
+ Query extends ThreadMessagesQuery<unknown, MessageDocLike>,
59
+ > =
60
+ Query extends ThreadMessagesQuery<unknown, MessageDocLike>
61
+ ? Expand<BetterOmit<FunctionArgs<Query>, "paginationOpts" | "streamArgs">>
62
+ : never;
63
+
64
+ export type ThreadMessagesResult<
65
+ Query extends ThreadMessagesQuery<unknown, MessageDocLike>,
66
+ > = Query extends ThreadMessagesQuery<unknown, infer M> ? M : never;
67
+
68
+ /**
69
+ * A hook that fetches messages from a thread.
70
+ *
71
+ * This hook is a wrapper around `usePaginatedQuery` and `useStreamingThreadMessages`.
72
+ * It will fetch both full messages and streaming messages, and merge them together.
73
+ *
74
+ * The query must take as arguments `{ threadId, paginationOpts }` and return a
75
+ * pagination result of objects that extend `MessageDoc`.
76
+ *
77
+ * For streaming, it should look like this:
78
+ * ```ts
79
+ * export const listThreadMessages = query({
80
+ * args: {
81
+ * threadId: v.string(),
82
+ * paginationOpts: paginationOptsValidator,
83
+ * streamArgs: vStreamArgs,
84
+ * ... other arguments you want
85
+ * },
86
+ * handler: async (ctx, args) => {
87
+ * // await authorizeThreadAccess(ctx, threadId);
88
+ * // NOTE: listMessages returns MessageDocs, not UIMessages.
89
+ * const paginated = await listMessages(ctx, components.agent, args);
90
+ * const streams = await syncStreams(ctx, components.agent, args);
91
+ * // Here you could filter out / modify the documents & stream deltas.
92
+ * return { ...paginated, streams };
93
+ * },
94
+ * });
95
+ * ```
96
+ *
97
+ * Then the hook can be used like this:
98
+ * ```ts
99
+ * const { results, status, loadMore } = useThreadMessages(
100
+ * api.myModule.listThreadMessages,
101
+ * { threadId },
102
+ * { initialNumItems: 10, stream: true }
103
+ * );
104
+ * ```
105
+ *
106
+ * @param query The query to use to fetch messages.
107
+ * It must take as arguments `{ threadId, paginationOpts }` and return a
108
+ * pagination result of objects that extend `MessageDoc`.
109
+ * To support streaming, it must also take in `streamArgs: vStreamArgs` and
110
+ * return a `streams` object returned from `agent.syncStreams`.
111
+ * @param args The arguments to pass to the query other than `paginationOpts`
112
+ * and `streamArgs`. So `{ threadId }` at minimum, plus any other arguments that
113
+ * you want to pass to the query.
114
+ * @param options The options for the query. Similar to usePaginatedQuery.
115
+ * To enable streaming, pass `stream: true`.
116
+ * @returns The messages. If stream is true, it will return a list of messages
117
+ * that includes both full messages and streaming messages.
118
+ */
119
+ export function useThreadMessages<Query extends ThreadMessagesQuery<any, any>>(
120
+ query: Query,
121
+ args: ThreadMessagesArgs<Query> | "skip",
122
+ options: {
123
+ initialNumItems: number;
124
+ stream?: Query extends StreamQuery
125
+ ? boolean
126
+ : ErrorMessage<"To enable streaming, your query must take in streamArgs: vStreamArgs and return a streams object returned from syncStreams. See docs.">;
127
+ },
128
+ ): UsePaginatedQueryResult<
129
+ ThreadMessagesResult<Query> & { streaming: boolean; key: string }
130
+ > {
131
+ // These are full messages
132
+ const paginated = usePaginatedQuery(
133
+ query,
134
+ args as PaginatedQueryArgs<Query> | "skip",
135
+ { initialNumItems: options.initialNumItems },
136
+ );
137
+
138
+ let startOrder = paginated.results.at(-1)?.order ?? 0;
139
+ for (let i = paginated.results.length - 1; i >= 0; i--) {
140
+ const m = paginated.results[i];
141
+ if (!m.streaming && m.status === "pending") {
142
+ // round down to the nearest 10 for some cache benefits
143
+ startOrder = m.order - (m.order % 10);
144
+ break;
145
+ }
146
+ }
147
+ // These are streaming messages that will not include full messages.
148
+ const streamMessages = useStreamingThreadMessages(
149
+ query as StreamQuery<ThreadMessagesArgs<Query>>,
150
+ !options.stream ||
151
+ args === "skip" ||
152
+ paginated.status === "LoadingFirstPage"
153
+ ? "skip"
154
+ : ({ ...args, paginationOpts: { cursor: null, numItems: 0 } } as any),
155
+ { startOrder },
156
+ );
157
+
158
+ const threadId = args === "skip" ? undefined : args.threadId;
159
+
160
+ const merged = useMemo(() => {
161
+ const streamListMessages =
162
+ streamMessages?.map((m) => ({
163
+ ...m,
164
+ streaming: !m.status || m.status === "pending",
165
+ })) ?? [];
166
+ return {
167
+ ...paginated,
168
+ results: sorted(
169
+ paginated.results
170
+ .map((m) => ({ ...m, streaming: false }))
171
+ // Note: this is intentionally after paginated results.
172
+ .concat(streamListMessages) as (MessageDocLike & {
173
+ streaming: boolean;
174
+ key: string;
175
+ })[],
176
+ ).reduce(
177
+ (msgs, msg: MessageDocLike & { streaming: boolean; key: string }) => {
178
+ msg.key = `${threadId}-${msg.order}-${msg.stepOrder}`;
179
+ const last = msgs.at(-1);
180
+ if (!last) {
181
+ return [msg];
182
+ }
183
+ if (last.order !== msg.order || last.stepOrder !== msg.stepOrder) {
184
+ return [...msgs, msg];
185
+ }
186
+ if (
187
+ last.status === "pending" &&
188
+ (msg.streaming || msg.status !== "pending")
189
+ ) {
190
+ // Let's prefer a streaming or finalized message over a pending
191
+ // one.
192
+ return [...msgs.slice(0, -1), msg];
193
+ }
194
+ // skip the new one if the previous one (listed) was finalized
195
+ return msgs;
196
+ },
197
+ [] as (MessageDocLike & { streaming: boolean; key: string })[],
198
+ ),
199
+ };
200
+ }, [paginated, streamMessages, threadId]);
201
+
202
+ return merged as ThreadMessagesResult<Query> & {
203
+ key: string;
204
+ streaming: boolean;
205
+ };
206
+ }
207
+
208
+ /**
209
+ * @deprecated FYI `useStreamingUIMessages` is likely better for you.
210
+ * A hook that fetches streaming messages from a thread.
211
+ * This ONLY returns streaming messages. To get both, use `useThreadMessages`.
212
+ *
213
+ * @param query The query to use to fetch messages.
214
+ * It must take as arguments `{ threadId, paginationOpts, streamArgs }` and
215
+ * return a `streams` object returned from `agent.syncStreams`.
216
+ * @param args The arguments to pass to the query other than `paginationOpts`
217
+ * and `streamArgs`. So `{ threadId }` at minimum, plus any other arguments that
218
+ * you want to pass to the query.
219
+ * @returns The streaming messages.
220
+ */
221
+ export function useStreamingThreadMessages<Query extends StreamQuery<any>>(
222
+ query: Query,
223
+ args:
224
+ | (StreamQueryArgs<Query> & {
225
+ /** @deprecated Pass startOrder to the next argument (third argument). */
226
+ startOrder?: number;
227
+ })
228
+ | "skip",
229
+ options?: {
230
+ startOrder?: number;
231
+ skipStreamIds?: string[];
232
+ },
233
+ ): Array<MessageDoc> | undefined {
234
+ const queryArgs =
235
+ args === "skip"
236
+ ? args
237
+ : (omit(args, ["startOrder"]) as unknown as StreamQueryArgs<Query>);
238
+ const startOrder =
239
+ args === "skip" ? undefined : (args.startOrder ?? undefined);
240
+ const queryOptions = { startOrder, ...options };
241
+ const uiMessages = useStreamingUIMessages(query, queryArgs, queryOptions);
242
+ const [messages, setMessages] = useState<Array<MessageDoc> | undefined>();
243
+ const generationRef = useRef(0);
244
+
245
+ useEffect(() => {
246
+ if (args === "skip" || !uiMessages) {
247
+ setMessages(undefined);
248
+ return;
249
+ }
250
+ const currentGeneration = ++generationRef.current;
251
+ (async () => {
252
+ const nested = await Promise.all(
253
+ uiMessages.map((m) => fromUIMessages([m], { threadId: args.threadId })),
254
+ );
255
+ if (generationRef.current === currentGeneration) {
256
+ setMessages(nested.flat());
257
+ }
258
+ })();
259
+ }, [uiMessages, args === "skip" ? undefined : args.threadId]);
260
+
261
+ return messages;
262
+ }