@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
@@ -0,0 +1,9 @@
1
+ import { useSmoothText, type SmoothTextOptions } from "./useSmoothText.js";
2
+
3
+ export function SmoothText({
4
+ text,
5
+ ...options
6
+ }: { text: string } & SmoothTextOptions) {
7
+ const [visibleText] = useSmoothText(text, options);
8
+ return visibleText;
9
+ }
@@ -1,237 +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
- import { useMemo, useRef, 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";
2
+ import { useMemo, useState } from "react";
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
- export { toUIMessages, type UIMessage, type ThreadQuery };
27
-
28
- /**
29
- * A hook that fetches messages from a thread.
30
- *
31
- * This hook is a wrapper around `usePaginatedQuery` and `useStreamingThreadMessages`.
32
- * It will fetch both full messages and streaming messages, and merge them together.
33
- *
34
- * The query must take as arguments `{ threadId, paginationOpts }` and return a
35
- * pagination result of objects that extend `MessageDoc`.
36
- *
37
- * For streaming, it should look like this:
38
- * ```ts
39
- * export const listThreadMessages = query({
40
- * args: {
41
- * threadId: v.string(),
42
- * paginationOpts: paginationOptsValidator,
43
- * streamArgs: vStreamArgs,
44
- * ... other arguments you want
45
- * },
46
- * handler: async (ctx, { threadId, paginationOpts, streamArgs }) => {
47
- * // await authorizeThreadAccess(ctx, threadId);
48
- * const paginated = await agent.listMessages(ctx, { threadId, paginationOpts });
49
- * const streams = await agent.syncStreams(ctx, { threadId, streamArgs });
50
- * // Here you could filter out / modify the documents & stream deltas.
51
- * return { ...paginated, streams };
52
- * },
53
- * });
54
- * ```
55
- *
56
- * Then the hook can be used like this:
57
- * ```ts
58
- * const messages = useThreadMessages(
59
- * api.myModule.listThreadMessages,
60
- * { threadId },
61
- * { initialNumItems: 10, stream: true }
62
- * );
63
- * ```
64
- *
65
- * @param query The query to use to fetch messages.
66
- * It must take as arguments `{ threadId, paginationOpts }` and return a
67
- * pagination result of objects that extend `MessageDoc`.
68
- * To support streaming, it must also take in `streamArgs: vStreamArgs` and
69
- * return a `streams` object returned from `agent.syncStreams`.
70
- * @param args The arguments to pass to the query other than `paginationOpts`
71
- * and `streamArgs`. So `{ threadId }` at minimum, plus any other arguments that
72
- * you want to pass to the query.
73
- * @param options The options for the query. Similar to usePaginatedQuery.
74
- * To enable streaming, pass `stream: true`.
75
- * @returns The messages. If stream is true, it will return a list of messages
76
- * that includes both full messages and streaming messages.
77
- */
78
- export function useThreadMessages<
79
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
80
- Query extends ThreadQuery<any, any>,
81
- >(
82
- query: Query,
83
- args: ThreadMessagesArgs<Query> | "skip",
84
- options: {
85
- initialNumItems: number;
86
- stream?: Query extends ThreadStreamQuery
87
- ? boolean
88
- : ErrorMessage<"To enable streaming, your query must take in streamArgs: vStreamArgs and return a streams object returned from agent.syncStreams. See docs.">;
89
- },
90
- ): UsePaginatedQueryResult<
91
- ThreadMessagesResult<Query> & { streaming?: boolean }
92
- > {
93
- // These are full messages
94
- const paginated = usePaginatedQuery(
95
- query,
96
- args as PaginatedQueryArgs<Query> | "skip",
97
- { initialNumItems: options.initialNumItems },
98
- );
99
-
100
- // These are streaming messages that will not include full messages.
101
- const streamMessages = useStreamingThreadMessages(
102
- query as ThreadStreamQuery<
103
- ThreadMessagesArgs<Query>,
104
- ThreadMessagesResult<Query>
105
- >,
106
- !options.stream ||
107
- args === "skip" ||
108
- paginated.status === "LoadingFirstPage"
109
- ? "skip"
110
- : { ...args, startOrder: paginated.results.at(-1)?.order },
111
- );
112
-
113
- const merged = useMemo(() => {
114
- const streamListMessages =
115
- streamMessages?.map((m) => ({
116
- ...m,
117
- streaming: !m.status || m.status === "pending",
118
- })) ?? [];
119
- return {
120
- ...paginated,
121
- results: paginated.results
122
- .map((m) => ({ ...m, streaming: false }))
123
- .concat(streamListMessages)
124
- .sort((a, b) =>
125
- a.order === b.order ? a.stepOrder - b.stepOrder : a.order - b.order,
126
- )
127
- // They shouldn't overlap, but check for duplicates just in case.
128
- .filter(
129
- (m, i, arr) =>
130
- !arr[i - 1] ||
131
- m.order !== arr[i - 1].order ||
132
- m.stepOrder !== arr[i - 1].stepOrder,
133
- ),
134
- };
135
- }, [paginated, streamMessages]);
136
-
137
- return merged as ThreadMessagesResult<Query>;
138
- }
139
-
140
- /**
141
- * A hook that fetches streaming messages from a thread.
142
- * This ONLY returns streaming messages. To get both, use `useThreadMessages`.
143
- *
144
- * @param query The query to use to fetch messages.
145
- * It must take as arguments `{ threadId, paginationOpts, streamArgs }` and
146
- * return a `streams` object returned from `agent.syncStreams`.
147
- * @param args The arguments to pass to the query other than `paginationOpts`
148
- * and `streamArgs`. So `{ threadId }` at minimum, plus any other arguments that
149
- * you want to pass to the query.
150
- * @returns The streaming messages.
151
- */
152
- export function useStreamingThreadMessages<
153
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
154
- Query extends ThreadStreamQuery<any, any>,
155
- >(
156
- query: Query,
157
- args: (ThreadMessagesArgs<Query> & { startOrder?: number }) | "skip",
158
- ): Array<ThreadMessagesResult<Query>> | undefined {
159
- // Invariant: streamMessages[streamId] is comprised of all deltas up to the
160
- // cursor. There can be multiple messages in the same stream, e.g. for tool
161
- // calls.
162
- const [streams, setStreams] = useState<
163
- Array<{ streamId: string; cursor: number; messages: MessageDoc[] }>
164
- >([]);
165
- const startOrderRef = useRef<number>(0);
166
- const queryArgs = args === "skip" ? args : omit(args, ["startOrder"]);
167
- if (args !== "skip" && !startOrderRef.current && args.startOrder) {
168
- startOrderRef.current = args.startOrder;
169
- }
170
- // Get all the active streams
171
- const streamList = useQuery(
172
- query,
173
- queryArgs === "skip"
174
- ? queryArgs
175
- : ({
176
- ...queryArgs,
177
- paginationOpts: { cursor: null, numItems: 0 },
178
- streamArgs: {
179
- kind: "list",
180
- startOrder: startOrderRef.current,
181
- } as StreamArgs,
182
- } as FunctionArgs<Query>),
183
- ) as
184
- | { streams: Extract<SyncStreamsReturnValue, { kind: "list" }> }
185
- | undefined;
186
- // Get the cursors for all the active streams
187
- const cursors = useMemo(() => {
188
- if (!streamList?.streams) return [];
189
- if (streamList.streams.kind !== "list") {
190
- throw new Error("Expected list streams");
191
- }
192
- return streamList.streams.messages.map(({ streamId }) => {
193
- const stream = streams.find((s) => s.streamId === streamId);
194
- const cursor = stream?.cursor ?? 0;
195
- return { streamId, cursor };
196
- });
197
- }, [streamList, streams]);
198
- // Get the deltas for all the active streams, if any.
199
- const cursorQuery = useQuery(
200
- query,
201
- queryArgs === "skip" || !streamList
202
- ? ("skip" as const)
203
- : ({
204
- ...queryArgs,
205
- paginationOpts: { cursor: null, numItems: 0 },
206
- streamArgs: { kind: "deltas", cursors } as StreamArgs,
207
- } as FunctionArgs<Query>),
208
- ) as
209
- | { streams: Extract<SyncStreamsReturnValue, { kind: "deltas" }> }
210
- | undefined;
211
- // Merge any deltas into the streamChunks, keeping it unmodified if unchanged.
212
- const threadId = args === "skip" ? undefined : args.threadId;
213
- const [messages, newStreams, changed] = useMemo(() => {
214
- if (!threadId) return [undefined, [], false];
215
- if (!streamList) return [undefined, [], false];
216
- if (cursorQuery && cursorQuery.streams?.kind !== "deltas") {
217
- throw new Error("Expected deltas streams");
218
- }
219
- return mergeDeltas(
220
- threadId,
221
- streamList.streams.messages,
222
- streams,
223
- cursorQuery?.streams?.deltas ?? [],
224
- );
225
- }, [threadId, cursorQuery, streams, streamList]);
226
- // Now assemble the chunks into messages
227
- if (!threadId) {
228
- return undefined;
229
- }
230
- if (changed) {
231
- setStreams(newStreams);
232
- }
233
- return messages as ThreadMessagesResult<Query>[] | undefined;
234
- }
7
+ export { SmoothText } from "./SmoothText.js";
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";
235
15
 
236
16
  /**
237
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 },
@@ -12,33 +54,34 @@ export function optimisticallySendMessage(
12
54
  return (store, args) => {
13
55
  const queries = store.getAllQueries(query);
14
56
  let maxOrder = -1;
15
- let maxStepOrder = 0;
16
57
  for (const q of queries) {
17
58
  if (q.args?.threadId !== args.threadId) continue;
18
59
  if (q.args.streamArgs) continue;
19
60
  for (const m of q.value?.page ?? []) {
20
61
  maxOrder = Math.max(maxOrder, m.order);
21
- maxStepOrder = Math.max(maxStepOrder, m.stepOrder);
22
62
  }
23
63
  }
24
64
  const order = maxOrder + 1;
25
65
  const stepOrder = 0;
66
+ const id = randomUUID();
67
+ const { prompt, ...rest } = args;
26
68
  insertAtTop({
27
69
  paginatedQuery: query,
28
70
  argsToMatch: { threadId: args.threadId, streamArgs: undefined },
29
71
  item: {
72
+ ...rest,
30
73
  _creationTime: Date.now(),
31
- _id: randomUUID(),
74
+ _id: id,
75
+ id,
76
+ key: `${args.threadId}-${order}-${stepOrder}`,
32
77
  order,
33
78
  stepOrder,
34
79
  status: "pending",
35
- threadId: args.threadId,
36
80
  tool: false,
37
- message: {
38
- role: "user",
39
- content: args.prompt,
40
- },
41
- text: args.prompt,
81
+ message: { role: "user", content: prompt },
82
+ parts: [{ type: "text", text: prompt }],
83
+ role: "user",
84
+ text: prompt,
42
85
  },
43
86
  localQueryStore: store,
44
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 StreamQueryArgs<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;
@@ -0,0 +1,154 @@
1
+ "use client";
2
+
3
+ import type { StreamQuery, StreamQueryArgs } from "./types.js";
4
+ import type { SyncStreamsReturnValue } from "../client/types.js";
5
+ import type { FunctionArgs } from "convex/server";
6
+ import type { StreamArgs, StreamDelta, StreamMessage } from "../validators.js";
7
+ import { sorted } from "../shared.js";
8
+ import { useQuery } from "convex/react";
9
+ import { useState } from "react";
10
+ import { assert } from "convex-helpers";
11
+
12
+ export function useDeltaStreams<
13
+ Query extends StreamQuery<any> = StreamQuery<object>,
14
+ >(
15
+ query: Query,
16
+ args: StreamQueryArgs<Query> | "skip",
17
+ options?: {
18
+ startOrder?: number;
19
+ skipStreamIds?: string[];
20
+ },
21
+ ): { streamMessage: StreamMessage; deltas: StreamDelta[] }[] | undefined {
22
+ // We hold onto and modify state directly to avoid re-running unnecessarily.
23
+ const [state] = useState<{
24
+ startOrder: number;
25
+ threadId: string | undefined;
26
+ deltaStreams:
27
+ | Array<{
28
+ streamMessage: StreamMessage;
29
+ deltas: StreamDelta[];
30
+ }>
31
+ | undefined;
32
+ }>({
33
+ startOrder: options?.startOrder ?? 0,
34
+ deltaStreams: undefined,
35
+ threadId: args === "skip" ? undefined : args.threadId,
36
+ });
37
+ const [cursors, setCursors] = useState<Record<string, number>>({});
38
+ if (args !== "skip" && state.threadId !== args.threadId) {
39
+ state.threadId = args.threadId;
40
+ state.deltaStreams = undefined;
41
+ state.startOrder = options?.startOrder ?? 0;
42
+ setCursors({});
43
+ }
44
+ if (
45
+ state.deltaStreams?.length ||
46
+ (options?.startOrder && options.startOrder < state.startOrder)
47
+ ) {
48
+ const cacheFriendlyStartOrder = options?.startOrder
49
+ ? // round down to the nearest 10 for some cache benefits
50
+ options.startOrder - (options.startOrder % 10)
51
+ : 0;
52
+ if (cacheFriendlyStartOrder !== state.startOrder) {
53
+ state.startOrder = cacheFriendlyStartOrder;
54
+ }
55
+ }
56
+
57
+ // Get all the active streams
58
+ const streamList = useQuery(
59
+ query,
60
+ args === "skip"
61
+ ? args
62
+ : ({
63
+ ...args,
64
+ streamArgs: {
65
+ kind: "list",
66
+ startOrder: state.startOrder,
67
+ } as StreamArgs,
68
+ } as FunctionArgs<Query>),
69
+ ) as
70
+ | { streams: Extract<SyncStreamsReturnValue, { kind: "list" }> }
71
+ | undefined;
72
+
73
+ const streamMessages =
74
+ args === "skip"
75
+ ? undefined
76
+ : !streamList
77
+ ? state.deltaStreams?.map(({ streamMessage }) => streamMessage)
78
+ : sorted(
79
+ streamList.streams.messages.filter(
80
+ ({ streamId, order }) =>
81
+ !options?.skipStreamIds?.includes(streamId) &&
82
+ (!options?.startOrder || order >= options.startOrder),
83
+ ),
84
+ );
85
+
86
+ // Get the deltas for all the active streams, if any.
87
+ const cursorQuery = useQuery(
88
+ query,
89
+ args === "skip" || !streamMessages?.length
90
+ ? ("skip" as const)
91
+ : ({
92
+ ...args,
93
+ streamArgs: {
94
+ kind: "deltas",
95
+ cursors: streamMessages.map(({ streamId }) => ({
96
+ streamId,
97
+ cursor: cursors[streamId] ?? 0,
98
+ })),
99
+ } as StreamArgs,
100
+ } as FunctionArgs<Query>),
101
+ ) as
102
+ | { streams: Extract<SyncStreamsReturnValue, { kind: "deltas" }> }
103
+ | undefined;
104
+
105
+ const newDeltas = cursorQuery?.streams.deltas;
106
+ if (newDeltas?.length && streamMessages) {
107
+ const newDeltasByStreamId = new Map<string, StreamDelta[]>();
108
+ for (const delta of newDeltas) {
109
+ const oldCursor = cursors[delta.streamId];
110
+ if (oldCursor && delta.start < oldCursor) continue;
111
+ const existing = newDeltasByStreamId.get(delta.streamId);
112
+ if (existing) {
113
+ const previousEnd = existing.at(-1)!.end;
114
+ assert(
115
+ previousEnd === delta.start,
116
+ `Gap found in deltas for ${delta.streamId} jumping to ${delta.start} from ${previousEnd}`,
117
+ );
118
+ existing.push(delta);
119
+ } else {
120
+ assert(
121
+ !oldCursor || oldCursor === delta.start,
122
+ `Gap found - first delta after ${oldCursor} is ${delta.start} for stream ${delta.streamId}`,
123
+ );
124
+ newDeltasByStreamId.set(delta.streamId, [delta]);
125
+ }
126
+ }
127
+ const newCursors: Record<string, number> = {};
128
+ for (const { streamId } of streamMessages) {
129
+ const cursor =
130
+ newDeltasByStreamId.get(streamId)?.at(-1)?.end ?? cursors[streamId];
131
+ if (cursor !== undefined) {
132
+ newCursors[streamId] = cursor;
133
+ }
134
+ }
135
+ setCursors(newCursors);
136
+
137
+ // we defensively create a new object so object identity matches contents
138
+ state.deltaStreams = streamMessages.map((streamMessage) => {
139
+ const streamId = streamMessage.streamId;
140
+ const old = state.deltaStreams?.find(
141
+ (ds) => ds.streamMessage.streamId === streamId,
142
+ );
143
+ const newDeltas = newDeltasByStreamId.get(streamId);
144
+ if (!newDeltas && streamMessage === old?.streamMessage) {
145
+ return old;
146
+ }
147
+ return {
148
+ streamMessage,
149
+ deltas: [...(old?.deltas ?? []), ...(newDeltas ?? [])],
150
+ };
151
+ });
152
+ }
153
+ return state.deltaStreams;
154
+ }