@convex-dev/agent 0.5.0-alpha.1 → 0.6.0-alpha.1

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 (235) hide show
  1. package/MIGRATION.md +153 -0
  2. package/README.md +32 -27
  3. package/dist/UIMessages.d.ts +46 -0
  4. package/dist/UIMessages.d.ts.map +1 -0
  5. package/dist/UIMessages.js +546 -0
  6. package/dist/UIMessages.js.map +1 -0
  7. package/dist/client/createTool.d.ts +126 -27
  8. package/dist/client/createTool.d.ts.map +1 -1
  9. package/dist/client/createTool.js +67 -12
  10. package/dist/client/createTool.js.map +1 -1
  11. package/dist/client/defaultComponent.d.ts +11 -0
  12. package/dist/client/defaultComponent.d.ts.map +1 -0
  13. package/dist/client/defaultComponent.js +7 -0
  14. package/dist/client/defaultComponent.js.map +1 -0
  15. package/dist/client/definePlaygroundAPI.d.ts +1335 -204
  16. package/dist/client/definePlaygroundAPI.d.ts.map +1 -1
  17. package/dist/client/definePlaygroundAPI.js +52 -28
  18. package/dist/client/definePlaygroundAPI.js.map +1 -1
  19. package/dist/client/files.d.ts +20 -7
  20. package/dist/client/files.d.ts.map +1 -1
  21. package/dist/client/files.js +68 -11
  22. package/dist/client/files.js.map +1 -1
  23. package/dist/client/index.d.ts +1116 -978
  24. package/dist/client/index.d.ts.map +1 -1
  25. package/dist/client/index.js +332 -747
  26. package/dist/client/index.js.map +1 -1
  27. package/dist/client/messages.d.ts +461 -0
  28. package/dist/client/messages.d.ts.map +1 -0
  29. package/dist/client/messages.js +106 -0
  30. package/dist/client/messages.js.map +1 -0
  31. package/dist/client/mockModel.d.ts +42 -0
  32. package/dist/client/mockModel.d.ts.map +1 -0
  33. package/dist/client/mockModel.js +182 -0
  34. package/dist/client/mockModel.js.map +1 -0
  35. package/dist/client/saveInputMessages.d.ts +20 -0
  36. package/dist/client/saveInputMessages.d.ts.map +1 -0
  37. package/dist/client/saveInputMessages.js +58 -0
  38. package/dist/client/saveInputMessages.js.map +1 -0
  39. package/dist/client/search.d.ts +350 -39
  40. package/dist/client/search.d.ts.map +1 -1
  41. package/dist/client/search.js +350 -39
  42. package/dist/client/search.js.map +1 -1
  43. package/dist/client/start.d.ts +84 -0
  44. package/dist/client/start.d.ts.map +1 -0
  45. package/dist/client/start.js +185 -0
  46. package/dist/client/start.js.map +1 -0
  47. package/dist/client/streamText.d.ts +46 -0
  48. package/dist/client/streamText.d.ts.map +1 -0
  49. package/dist/client/streamText.js +117 -0
  50. package/dist/client/streamText.js.map +1 -0
  51. package/dist/client/streaming.d.ts +3716 -32
  52. package/dist/client/streaming.d.ts.map +1 -1
  53. package/dist/client/streaming.js +161 -59
  54. package/dist/client/streaming.js.map +1 -1
  55. package/dist/client/threads.d.ts +46 -0
  56. package/dist/client/threads.d.ts.map +1 -0
  57. package/dist/client/threads.js +49 -0
  58. package/dist/client/threads.js.map +1 -0
  59. package/dist/client/types.d.ts +266 -128
  60. package/dist/client/types.d.ts.map +1 -1
  61. package/dist/client/utils.d.ts +4 -0
  62. package/dist/client/utils.d.ts.map +1 -0
  63. package/dist/client/utils.js +21 -0
  64. package/dist/client/utils.js.map +1 -0
  65. package/dist/component/_generated/api.d.ts +24 -2178
  66. package/dist/component/_generated/api.d.ts.map +1 -1
  67. package/dist/component/_generated/api.js +10 -1
  68. package/dist/component/_generated/api.js.map +1 -1
  69. package/dist/component/_generated/component.d.ts +3120 -0
  70. package/dist/component/_generated/component.d.ts.map +1 -0
  71. package/dist/component/_generated/component.js +11 -0
  72. package/dist/component/_generated/component.js.map +1 -0
  73. package/dist/component/_generated/dataModel.d.ts +4 -18
  74. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  75. package/dist/component/_generated/dataModel.js +11 -0
  76. package/dist/component/_generated/dataModel.js.map +1 -0
  77. package/dist/component/_generated/server.d.ts +10 -38
  78. package/dist/component/_generated/server.d.ts.map +1 -1
  79. package/dist/component/_generated/server.js +9 -5
  80. package/dist/component/_generated/server.js.map +1 -1
  81. package/dist/component/files.d.ts +16 -10
  82. package/dist/component/files.d.ts.map +1 -1
  83. package/dist/component/files.js +10 -2
  84. package/dist/component/files.js.map +1 -1
  85. package/dist/component/messages.d.ts +2578 -366
  86. package/dist/component/messages.d.ts.map +1 -1
  87. package/dist/component/messages.js +397 -154
  88. package/dist/component/messages.js.map +1 -1
  89. package/dist/component/schema.d.ts +5697 -3584
  90. package/dist/component/schema.d.ts.map +1 -1
  91. package/dist/component/schema.js +18 -41
  92. package/dist/component/schema.js.map +1 -1
  93. package/dist/component/streams.d.ts +39 -339
  94. package/dist/component/streams.d.ts.map +1 -1
  95. package/dist/component/streams.js +114 -73
  96. package/dist/component/streams.js.map +1 -1
  97. package/dist/component/threads.d.ts +13 -13
  98. package/dist/component/users.d.ts +7 -7
  99. package/dist/component/vector/index.d.ts +1 -1
  100. package/dist/component/vector/index.d.ts.map +1 -1
  101. package/dist/component/vector/index.js +1 -3
  102. package/dist/component/vector/index.js.map +1 -1
  103. package/dist/deltas.d.ts +43 -0
  104. package/dist/deltas.d.ts.map +1 -0
  105. package/dist/deltas.js +446 -0
  106. package/dist/deltas.js.map +1 -0
  107. package/dist/mapping.d.ts +38 -20
  108. package/dist/mapping.d.ts.map +1 -1
  109. package/dist/mapping.js +365 -97
  110. package/dist/mapping.js.map +1 -1
  111. package/dist/react/SmoothText.d.ts +5 -0
  112. package/dist/react/SmoothText.d.ts.map +1 -0
  113. package/dist/react/SmoothText.js +6 -0
  114. package/dist/react/SmoothText.js.map +1 -0
  115. package/dist/react/index.d.ts +5 -77
  116. package/dist/react/index.d.ts.map +1 -1
  117. package/dist/react/index.js +6 -160
  118. package/dist/react/index.js.map +1 -1
  119. package/dist/react/optimisticallySendMessage.d.ts +36 -3
  120. package/dist/react/optimisticallySendMessage.d.ts.map +1 -1
  121. package/dist/react/optimisticallySendMessage.js +35 -9
  122. package/dist/react/optimisticallySendMessage.js.map +1 -1
  123. package/dist/react/types.d.ts +4 -18
  124. package/dist/react/types.d.ts.map +1 -1
  125. package/dist/react/useDeltaStreams.d.ts +10 -0
  126. package/dist/react/useDeltaStreams.d.ts.map +1 -0
  127. package/dist/react/useDeltaStreams.js +106 -0
  128. package/dist/react/useDeltaStreams.js.map +1 -0
  129. package/dist/react/useSmoothText.d.ts +13 -12
  130. package/dist/react/useSmoothText.d.ts.map +1 -1
  131. package/dist/react/useSmoothText.js +32 -15
  132. package/dist/react/useSmoothText.js.map +1 -1
  133. package/dist/react/useStreamingUIMessages.d.ts +22 -0
  134. package/dist/react/useStreamingUIMessages.d.ts.map +1 -0
  135. package/dist/react/useStreamingUIMessages.js +92 -0
  136. package/dist/react/useStreamingUIMessages.js.map +1 -0
  137. package/dist/react/useThreadMessages.d.ts +104 -0
  138. package/dist/react/useThreadMessages.d.ts.map +1 -0
  139. package/dist/react/useThreadMessages.js +148 -0
  140. package/dist/react/useThreadMessages.js.map +1 -0
  141. package/dist/react/useUIMessages.d.ts +96 -0
  142. package/dist/react/useUIMessages.d.ts.map +1 -0
  143. package/dist/react/useUIMessages.js +108 -0
  144. package/dist/react/useUIMessages.js.map +1 -0
  145. package/dist/shared.d.ts +20 -4
  146. package/dist/shared.d.ts.map +1 -1
  147. package/dist/shared.js +45 -8
  148. package/dist/shared.js.map +1 -1
  149. package/dist/validators.d.ts +22981 -5666
  150. package/dist/validators.d.ts.map +1 -1
  151. package/dist/validators.js +245 -137
  152. package/dist/validators.js.map +1 -1
  153. package/package.json +101 -51
  154. package/src/UIMessages.combineUIMessages.test.ts +239 -0
  155. package/src/UIMessages.test.ts +273 -0
  156. package/src/UIMessages.ts +739 -0
  157. package/src/client/approval.test.ts +350 -0
  158. package/src/client/createTool.ts +291 -76
  159. package/src/client/defaultComponent.ts +17 -0
  160. package/src/client/definePlaygroundAPI.ts +67 -31
  161. package/src/client/files.ts +100 -20
  162. package/src/client/index.test.ts +40 -85
  163. package/src/client/index.ts +638 -1289
  164. package/src/client/messages.ts +237 -0
  165. package/src/client/mockModel.ts +252 -0
  166. package/src/client/saveInputMessages.test.ts +583 -0
  167. package/src/client/saveInputMessages.ts +101 -0
  168. package/src/client/search.test.ts +1207 -0
  169. package/src/client/search.ts +581 -70
  170. package/src/client/start.ts +327 -0
  171. package/src/client/streamText.ts +187 -0
  172. package/src/client/streaming.test.ts +186 -0
  173. package/src/client/streaming.ts +241 -97
  174. package/src/client/threads.ts +83 -0
  175. package/src/client/types.ts +370 -219
  176. package/src/client/utils.ts +27 -0
  177. package/src/component/_generated/api.ts +64 -0
  178. package/src/component/_generated/component.ts +4902 -0
  179. package/src/component/_generated/{server.d.ts → server.ts} +33 -21
  180. package/src/component/files.ts +11 -2
  181. package/src/component/messages.test.ts +195 -51
  182. package/src/component/messages.ts +500 -201
  183. package/src/component/schema.ts +20 -46
  184. package/src/component/setup.test.ts +7 -0
  185. package/src/component/streams.ts +184 -83
  186. package/src/component/users.test.ts +0 -1
  187. package/src/component/vector/index.ts +1 -3
  188. package/src/deltas.test.ts +626 -0
  189. package/src/deltas.ts +569 -0
  190. package/src/fromUIMessages.test.ts +497 -0
  191. package/src/mapping.test.ts +180 -6
  192. package/src/mapping.ts +479 -162
  193. package/src/react/SmoothText.tsx +9 -0
  194. package/src/react/index.ts +10 -230
  195. package/src/react/optimisticallySendMessage.ts +55 -12
  196. package/src/react/types.ts +6 -39
  197. package/src/react/useDeltaStreams.ts +160 -0
  198. package/src/react/useSmoothText.ts +56 -36
  199. package/src/react/useStreamingUIMessages.ts +143 -0
  200. package/src/react/useThreadMessages.ts +262 -0
  201. package/src/react/useUIMessages.test.ts +255 -0
  202. package/src/react/useUIMessages.ts +195 -0
  203. package/src/shared.ts +88 -12
  204. package/src/test.ts +18 -0
  205. package/src/toUIMessages.test.ts +1269 -0
  206. package/src/validators.test.ts +18 -19
  207. package/src/validators.ts +325 -185
  208. package/dist/client/_generated/_ignore.d.ts +0 -1
  209. package/dist/client/_generated/_ignore.d.ts.map +0 -1
  210. package/dist/client/_generated/_ignore.js +0 -3
  211. package/dist/client/_generated/_ignore.js.map +0 -1
  212. package/dist/client/listMessages.d.ts +0 -22
  213. package/dist/client/listMessages.d.ts.map +0 -1
  214. package/dist/client/listMessages.js +0 -25
  215. package/dist/client/listMessages.js.map +0 -1
  216. package/dist/package.json +0 -3
  217. package/dist/react/deltas.d.ts +0 -26
  218. package/dist/react/deltas.d.ts.map +0 -1
  219. package/dist/react/deltas.js +0 -384
  220. package/dist/react/deltas.js.map +0 -1
  221. package/dist/react/toUIMessages.d.ts +0 -15
  222. package/dist/react/toUIMessages.d.ts.map +0 -1
  223. package/dist/react/toUIMessages.js +0 -211
  224. package/dist/react/toUIMessages.js.map +0 -1
  225. package/src/client/listMessages.ts +0 -38
  226. package/src/component/_generated/api.d.ts +0 -2202
  227. package/src/component/_generated/api.js +0 -23
  228. package/src/component/_generated/server.js +0 -90
  229. package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
  230. package/src/react/deltas.test.ts +0 -315
  231. package/src/react/deltas.ts +0 -478
  232. package/src/react/toUIMessages.test.ts +0 -420
  233. package/src/react/toUIMessages.ts +0 -253
  234. package/src/vitest.config.ts +0 -7
  235. /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,160 @@
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
+ // When no active streams remain, clear the stale state so we stop
87
+ // returning old streaming UIMessages.
88
+ if (streamMessages !== undefined && streamMessages.length === 0) {
89
+ state.deltaStreams = undefined;
90
+ }
91
+
92
+ // Get the deltas for all the active streams, if any.
93
+ const cursorQuery = useQuery(
94
+ query,
95
+ args === "skip" || !streamMessages?.length
96
+ ? ("skip" as const)
97
+ : ({
98
+ ...args,
99
+ streamArgs: {
100
+ kind: "deltas",
101
+ cursors: streamMessages.map(({ streamId }) => ({
102
+ streamId,
103
+ cursor: cursors[streamId] ?? 0,
104
+ })),
105
+ } as StreamArgs,
106
+ } as FunctionArgs<Query>),
107
+ ) as
108
+ | { streams: Extract<SyncStreamsReturnValue, { kind: "deltas" }> }
109
+ | undefined;
110
+
111
+ const newDeltas = cursorQuery?.streams.deltas;
112
+ if (newDeltas?.length && streamMessages) {
113
+ const newDeltasByStreamId = new Map<string, StreamDelta[]>();
114
+ for (const delta of newDeltas) {
115
+ const oldCursor = cursors[delta.streamId];
116
+ if (oldCursor && delta.start < oldCursor) continue;
117
+ const existing = newDeltasByStreamId.get(delta.streamId);
118
+ if (existing) {
119
+ const previousEnd = existing.at(-1)!.end;
120
+ assert(
121
+ previousEnd === delta.start,
122
+ `Gap found in deltas for ${delta.streamId} jumping to ${delta.start} from ${previousEnd}`,
123
+ );
124
+ existing.push(delta);
125
+ } else {
126
+ assert(
127
+ !oldCursor || oldCursor === delta.start,
128
+ `Gap found - first delta after ${oldCursor} is ${delta.start} for stream ${delta.streamId}`,
129
+ );
130
+ newDeltasByStreamId.set(delta.streamId, [delta]);
131
+ }
132
+ }
133
+ const newCursors: Record<string, number> = {};
134
+ for (const { streamId } of streamMessages) {
135
+ const cursor =
136
+ newDeltasByStreamId.get(streamId)?.at(-1)?.end ?? cursors[streamId];
137
+ if (cursor !== undefined) {
138
+ newCursors[streamId] = cursor;
139
+ }
140
+ }
141
+ setCursors(newCursors);
142
+
143
+ // we defensively create a new object so object identity matches contents
144
+ state.deltaStreams = streamMessages.map((streamMessage) => {
145
+ const streamId = streamMessage.streamId;
146
+ const old = state.deltaStreams?.find(
147
+ (ds) => ds.streamMessage.streamId === streamId,
148
+ );
149
+ const newDeltas = newDeltasByStreamId.get(streamId);
150
+ if (!newDeltas && streamMessage === old?.streamMessage) {
151
+ return old;
152
+ }
153
+ return {
154
+ streamMessage,
155
+ deltas: [...(old?.deltas ?? []), ...(newDeltas ?? [])],
156
+ };
157
+ });
158
+ }
159
+ return state.deltaStreams;
160
+ }