@convex-dev/agent 0.2.8-alpha.4 → 0.2.8-alpha.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/client/definePlaygroundAPI.d.ts +5 -5
  2. package/dist/client/index.d.ts +5 -5
  3. package/dist/client/index.d.ts.map +1 -1
  4. package/dist/client/index.js +2 -3
  5. package/dist/client/index.js.map +1 -1
  6. package/dist/client/messages.d.ts +1 -1
  7. package/dist/client/search.d.ts +1 -1
  8. package/dist/client/streaming.d.ts +17 -12
  9. package/dist/client/streaming.d.ts.map +1 -1
  10. package/dist/client/streaming.js +49 -2
  11. package/dist/client/streaming.js.map +1 -1
  12. package/dist/client/types.d.ts +7 -1
  13. package/dist/client/types.d.ts.map +1 -1
  14. package/dist/component/_generated/api.d.ts +8 -8
  15. package/dist/component/messages.d.ts +11 -11
  16. package/dist/component/schema.d.ts +20 -20
  17. package/dist/component/streams.d.ts.map +1 -1
  18. package/dist/component/streams.js +1 -0
  19. package/dist/component/streams.js.map +1 -1
  20. package/dist/deltas.d.ts +2 -6
  21. package/dist/deltas.d.ts.map +1 -1
  22. package/dist/deltas.js +52 -49
  23. package/dist/deltas.js.map +1 -1
  24. package/dist/react/useDeltaStreams.d.ts +10 -0
  25. package/dist/react/useDeltaStreams.d.ts.map +1 -0
  26. package/dist/react/useDeltaStreams.js +101 -0
  27. package/dist/react/useDeltaStreams.js.map +1 -0
  28. package/dist/react/useStreamingUIMessages.d.ts.map +1 -1
  29. package/dist/react/useStreamingUIMessages.js +66 -181
  30. package/dist/react/useStreamingUIMessages.js.map +1 -1
  31. package/dist/react/useUIMessages.d.ts.map +1 -1
  32. package/dist/react/useUIMessages.js +4 -1
  33. package/dist/react/useUIMessages.js.map +1 -1
  34. package/dist/validators.d.ts +78 -78
  35. package/dist/validators.js +1 -1
  36. package/dist/validators.js.map +1 -1
  37. package/package.json +7 -5
  38. package/src/client/index.ts +2 -1
  39. package/src/client/streaming.test.ts +2 -2
  40. package/src/client/streaming.ts +58 -5
  41. package/src/client/types.ts +7 -0
  42. package/src/component/_generated/api.d.ts +8 -8
  43. package/src/component/streams.ts +1 -0
  44. package/src/deltas.ts +62 -54
  45. package/src/react/useDeltaStreams.ts +155 -0
  46. package/src/react/useStreamingUIMessages.ts +94 -251
  47. package/src/react/useUIMessages.ts +5 -2
  48. package/src/validators.ts +1 -1
  49. package/dist/package.json +0 -3
@@ -1,26 +1,24 @@
1
1
  "use client";
2
- import { omit } from "convex-helpers";
3
- import { useQuery } from "convex/react";
4
- import type { FunctionArgs } from "convex/server";
5
- import { useMemo, useState, useEffect, useRef } from "react";
6
- import {
7
- readUIMessageStream,
8
- type TextUIPart,
9
- type UIDataTypes,
10
- type UIMessageChunk,
11
- type UITools,
12
- } from "ai";
13
- import type { SyncStreamsReturnValue } from "../client/types.js";
14
- import type { StreamArgs } from "../validators.js";
2
+ import { useMemo, useState, useEffect } from "react";
3
+ import { type UIDataTypes, type UIMessageChunk, type UITools } from "ai";
15
4
  import type { StreamQuery, StreamQueryArgs } from "./types.js";
16
5
  import { type UIMessage } from "../UIMessages.js";
17
6
  import {
18
7
  blankUIMessage,
19
8
  getParts,
9
+ updateFromUIMessageChunks,
20
10
  deriveUIMessagesFromTextStreamParts,
21
- statusFromStreamStatus,
22
11
  } from "../deltas.js";
23
- import { sorted } from "../shared.js";
12
+ import { useDeltaStreams } from "./useDeltaStreams.js";
13
+
14
+ // Polyfill structuredClone to support readUIMessageStream on ReactNative
15
+ if (!("structuredClone" in globalThis)) {
16
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
+ void import("@ungap/structured-clone" as any).then(
18
+ ({ default: structuredClone }) =>
19
+ (globalThis.structuredClone = structuredClone),
20
+ );
21
+ }
24
22
 
25
23
  /**
26
24
  * A hook that fetches streaming messages from a thread and converts them to UIMessages
@@ -51,252 +49,97 @@ export function useStreamingUIMessages<
51
49
  },
52
50
  // TODO: make generic on metadata, etc.
53
51
  ): UIMessage<METADATA, DATA_PARTS, TOOLS>[] | undefined {
54
- const [uiMessages, setUIMessages] = useState<
55
- Record<string, UIMessage<METADATA, DATA_PARTS, TOOLS>>
52
+ const [messageState, setMessageState] = useState<
53
+ Record<
54
+ string,
55
+ {
56
+ uiMessage: UIMessage<METADATA, DATA_PARTS, TOOLS>;
57
+ cursor: number;
58
+ }
59
+ >
56
60
  >({});
57
- const [streamCursors, setStreamCursors] = useState<Record<string, number>>(
58
- {},
59
- );
60
- const uiMessageStreamControllers = useRef<
61
- Map<string, ReadableStreamDefaultController<UIMessageChunk>>
62
- >(new Map());
63
-
64
- const startOrder = options?.startOrder
65
- ? // round down to the nearest 10 for some cache benefits
66
- options.startOrder - (options.startOrder % 10)
67
- : 0;
68
61
 
69
- // Get all the active streams
70
- const streamList = useQuery(
71
- query,
72
- args === "skip"
73
- ? args
74
- : ({
75
- ...args,
76
- streamArgs: {
77
- kind: "list",
78
- startOrder,
79
- } as StreamArgs,
80
- } as FunctionArgs<Query>),
81
- ) as
82
- | { streams: Extract<SyncStreamsReturnValue, { kind: "list" }> }
83
- | undefined;
84
-
85
- const minOrder = Math.max(
86
- options?.startOrder ?? 0,
87
- streamList?.streams.messages.length
88
- ? Math.min(...streamList.streams.messages.map(({ order }) => order))
89
- : 0,
90
- );
91
-
92
- // Get the cursors for all the active streams
93
- const cursors = useMemo(() => {
94
- if (!streamList?.streams) return [];
95
- if (streamList.streams.kind !== "list") {
96
- throw new Error("Expected list streams");
97
- }
98
- return streamList.streams.messages
99
- .filter(({ streamId }) => !options?.skipStreamIds?.includes(streamId))
100
- .filter(({ order }) => order >= minOrder)
101
- .map(({ streamId }) => {
102
- const cursor = streamCursors[streamId] ?? 0;
103
- return { streamId, cursor };
104
- });
105
- }, [streamList, streamCursors, options?.skipStreamIds, minOrder]);
106
-
107
- // Get the deltas for all the active streams, if any.
108
- const cursorQuery = useQuery(
109
- query,
110
- args === "skip" || !streamList
111
- ? ("skip" as const)
112
- : ({
113
- ...args,
114
- streamArgs: { kind: "deltas", cursors } as StreamArgs,
115
- } as FunctionArgs<Query>),
116
- ) as
117
- | { streams: Extract<SyncStreamsReturnValue, { kind: "deltas" }> }
118
- | undefined;
62
+ const streams = useDeltaStreams(query, args, options);
119
63
 
120
64
  const threadId = args === "skip" ? undefined : args.threadId;
121
65
 
122
66
  useEffect(() => {
123
- const activeStreamIds = new Set(
124
- streamList?.streams.messages.map((m) => m.streamId) ?? [],
125
- );
126
- // Clean up finished streams
127
- setUIMessages((prev) => {
128
- const toRemove = Object.keys(prev).filter(
129
- (streamId) => !activeStreamIds.has(streamId),
130
- );
131
- return toRemove.length ? omit(prev, toRemove) : prev;
132
- });
133
- for (const [
134
- streamId,
135
- controller,
136
- ] of uiMessageStreamControllers.current.entries()) {
137
- if (!activeStreamIds.has(streamId)) {
138
- uiMessageStreamControllers.current.delete(streamId);
139
- controller.close();
67
+ if (!streams) return;
68
+ // return if there are no new deltas beyond the cursors
69
+ let noNewDeltas = true;
70
+ for (const stream of streams) {
71
+ const lastDelta = stream.deltas.at(-1);
72
+ const cursor = messageState[stream.streamMessage.streamId]?.cursor;
73
+ if (!cursor) {
74
+ noNewDeltas = false;
75
+ break;
140
76
  }
141
- }
142
- if (!cursorQuery?.streams?.deltas || !threadId) return;
143
-
144
- const deltasByStream = new Map<string, typeof cursorQuery.streams.deltas>();
145
-
146
- // Group deltas by streamId
147
- for (const delta of cursorQuery.streams.deltas) {
148
- if (!deltasByStream.has(delta.streamId)) {
149
- deltasByStream.set(delta.streamId, []);
77
+ if (lastDelta && lastDelta.start >= cursor) {
78
+ noNewDeltas = false;
79
+ break;
150
80
  }
151
- deltasByStream.get(delta.streamId)!.push(delta);
152
81
  }
153
-
154
- // Process each stream's deltas
155
- for (const [streamId, deltas] of deltasByStream.entries()) {
156
- const streamMessage = streamList?.streams?.messages.find(
157
- (m) => m.streamId === streamId,
158
- );
159
- if (!streamMessage) continue;
160
- if (streamMessage.format === "UIMessageChunk") {
161
- setStreamCursors((streamCursors) => {
162
- const currentCursor = streamCursors[streamId] ?? 0;
163
-
164
- const { parts, cursor } = getParts<UIMessageChunk>(
165
- deltas,
166
- currentCursor,
167
- );
168
-
169
- const newStreamCursors =
170
- cursor === currentCursor
171
- ? streamCursors
172
- : { ...streamCursors, [streamId]: cursor };
173
-
174
- if (parts.length === 0) return newStreamCursors;
175
-
176
- // Get existing message for this stream as starting point
177
- const existingStream =
178
- uiMessageStreamControllers.current.get(streamId);
179
- if (existingStream) {
180
- for (const part of parts) {
181
- existingStream.enqueue(part);
182
- }
183
- } else {
184
- const stream = new ReadableStream<UIMessageChunk>({
185
- start(controller) {
186
- uiMessageStreamControllers.current.set(streamId, controller);
187
- for (const part of parts) {
188
- controller.enqueue(part);
189
- }
190
- },
191
- });
192
- const initialMessage = blankUIMessage(streamMessage, threadId);
193
- setUIMessages((prev) => ({
194
- ...prev,
195
- [streamId]: initialMessage as UIMessage<
196
- METADATA,
197
- DATA_PARTS,
198
- TOOLS
199
- >,
200
- }));
201
-
202
- const messageStream = readUIMessageStream({
203
- message: initialMessage,
204
- stream,
205
- onError: (error) => {
206
- setUIMessages((prev) => ({
207
- ...prev,
208
- [streamId]: {
209
- ...prev[streamId],
210
- status: "failed",
211
- },
212
- }));
213
- console.error(`Error in stream ${streamId}:`, error);
214
- },
215
- terminateOnError: false,
216
- });
217
-
218
- // Process the async iterator
219
- void (async () => {
220
- for await (const message of messageStream) {
221
- message.text = message.parts
222
- .filter((p) => p.type === "text")
223
- .map((p) => p.text)
224
- .join("");
225
- setUIMessages((prev) =>
226
- // If we don't have a ui message assume we've aborted.
227
- prev[streamId]
228
- ? {
229
- ...prev,
230
- [streamId]: message as UIMessage<
231
- METADATA,
232
- DATA_PARTS,
233
- TOOLS
234
- >,
235
- }
236
- : prev,
237
- );
238
- }
239
- })().catch((error) => {
240
- console.error(`Error processing stream ${streamId}:`, error);
241
- });
242
- }
243
- return newStreamCursors;
244
- });
245
- } else {
246
- setUIMessages((uiMessages) => {
247
- const existingUIMessage = uiMessages[streamId];
248
- const [[uiMessage], [streamMetadata], changed] =
249
- deriveUIMessagesFromTextStreamParts(
250
- threadId,
251
- [streamMessage],
252
- existingUIMessage
253
- ? [
254
- {
255
- streamId,
256
- cursor: streamCursors[streamId] ?? 0,
257
- message: existingUIMessage,
258
- },
259
- ]
260
- : [],
261
- deltas,
262
- );
263
- if (changed) {
264
- setStreamCursors((prev) => ({
265
- ...prev,
266
- [streamId]: streamMetadata.cursor,
267
- }));
268
- return {
269
- ...uiMessages,
270
- [streamId]: uiMessage as UIMessage<METADATA, DATA_PARTS, TOOLS>,
271
- };
272
- }
273
- return uiMessages;
274
- });
275
- }
82
+ if (noNewDeltas) {
83
+ return;
276
84
  }
277
- }, [cursorQuery, streamCursors, threadId, streamList?.streams?.messages]);
85
+ const abortController = new AbortController();
86
+ void (async () => {
87
+ const newMessageState: Record<
88
+ string,
89
+ {
90
+ uiMessage: UIMessage<METADATA, DATA_PARTS, TOOLS>;
91
+ cursor: number;
92
+ }
93
+ > = Object.fromEntries(
94
+ await Promise.all(
95
+ streams.map(async ({ deltas, streamMessage }) => {
96
+ const { parts, cursor } = getParts<UIMessageChunk>(deltas, 0);
97
+ if (streamMessage.format === "UIMessageChunk") {
98
+ // Unfortunately this can't handle resuming from a UIMessage and
99
+ // adding more chunks, so we re-create it from scratch each time.
100
+ const uiMessage = await updateFromUIMessageChunks(
101
+ blankUIMessage(streamMessage, threadId),
102
+ parts,
103
+ );
104
+ return [
105
+ streamMessage.streamId,
106
+ {
107
+ uiMessage,
108
+ cursor,
109
+ },
110
+ ];
111
+ } else {
112
+ const [uiMessages] = deriveUIMessagesFromTextStreamParts(
113
+ threadId,
114
+ [streamMessage],
115
+ [],
116
+ deltas,
117
+ );
118
+ return [
119
+ streamMessage.streamId,
120
+ {
121
+ uiMessage: uiMessages[0],
122
+ cursor,
123
+ },
124
+ ];
125
+ }
126
+ }),
127
+ ),
128
+ );
129
+ if (abortController.signal.aborted) return;
130
+ setMessageState(newMessageState);
131
+ })();
132
+ return () => {
133
+ abortController.abort();
134
+ };
135
+ }, [messageState, streams, threadId]);
278
136
 
279
137
  return useMemo(() => {
280
- if (!streamList) return undefined;
281
- return sorted(
282
- Array.from(
283
- Object.entries(uiMessages)
284
- .map(([streamId, uiMessage]) => {
285
- const streamMessage = streamList?.streams.messages.find(
286
- (m) => m.streamId === streamId,
287
- );
288
- if (!streamMessage) return undefined;
289
- uiMessage.status = statusFromStreamStatus(
290
- streamMessage?.status ?? "finished",
291
- );
292
- uiMessage.text = uiMessage.parts
293
- .filter((p): p is TextUIPart => p.type === "text")
294
- .map((p) => p.text)
295
- .join("");
296
- return uiMessage;
297
- })
298
- .filter((uiMessage) => uiMessage !== undefined),
299
- ),
300
- );
301
- }, [uiMessages, streamList]);
138
+ if (!streams) return undefined;
139
+ return streams
140
+ .map(
141
+ ({ streamMessage }) => messageState[streamMessage.streamId]?.uiMessage,
142
+ )
143
+ .filter((uiMessage) => uiMessage !== undefined);
144
+ }, [messageState, streams]);
302
145
  }
@@ -17,11 +17,12 @@ import type {
17
17
  } from "convex/server";
18
18
  import { useMemo } from "react";
19
19
  import type { SyncStreamsReturnValue } from "../client/types.js";
20
- import type { MessageStatus, StreamArgs } from "../validators.js";
20
+ import type { StreamArgs } from "../validators.js";
21
21
  import type { StreamQuery } from "./types.js";
22
22
  import { type UIMessage, type UIStatus } from "../UIMessages.js";
23
23
  import { sorted } from "../shared.js";
24
24
  import { useStreamingUIMessages } from "./useStreamingUIMessages.js";
25
+ import { combineUIMessages } from "../deltas.js";
25
26
 
26
27
  export type UIMessageLike = {
27
28
  order: number;
@@ -155,9 +156,11 @@ export function useUIMessages<
155
156
  );
156
157
 
157
158
  const merged = useMemo(() => {
159
+ // Messages may have been split by pagination. Re-combine them here.
160
+ const combined = combineUIMessages(sorted(paginated.results));
158
161
  return {
159
162
  ...paginated,
160
- results: dedupeMessages(paginated.results, streamMessages ?? []),
163
+ results: dedupeMessages(combined, streamMessages ?? []),
161
164
  };
162
165
  }, [paginated, streamMessages]);
163
166
 
package/src/validators.ts CHANGED
@@ -93,7 +93,7 @@ export const vSourcePart = v.union(
93
93
  sourceType: v.literal("url"),
94
94
  id: v.string(),
95
95
  url: v.string(),
96
- title: v.string(),
96
+ title: v.optional(v.string()),
97
97
  providerOptions,
98
98
  providerMetadata,
99
99
  }),
package/dist/package.json DELETED
@@ -1,3 +0,0 @@
1
- {
2
- "type": "module"
3
- }