@convex-dev/agent 0.2.8-alpha.5 → 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.
- package/dist/client/definePlaygroundAPI.d.ts +5 -5
- package/dist/client/index.d.ts +5 -5
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +2 -3
- package/dist/client/index.js.map +1 -1
- package/dist/client/messages.d.ts +1 -1
- package/dist/client/search.d.ts +1 -1
- package/dist/client/streaming.d.ts +17 -12
- package/dist/client/streaming.d.ts.map +1 -1
- package/dist/client/streaming.js +49 -2
- package/dist/client/streaming.js.map +1 -1
- package/dist/component/_generated/api.d.ts +8 -8
- package/dist/component/messages.d.ts +11 -11
- package/dist/component/schema.d.ts +20 -20
- package/dist/component/streams.d.ts.map +1 -1
- package/dist/component/streams.js +1 -0
- package/dist/component/streams.js.map +1 -1
- package/dist/deltas.d.ts +2 -6
- package/dist/deltas.d.ts.map +1 -1
- package/dist/deltas.js +52 -49
- package/dist/deltas.js.map +1 -1
- package/dist/react/useDeltaStreams.d.ts +10 -0
- package/dist/react/useDeltaStreams.d.ts.map +1 -0
- package/dist/react/useDeltaStreams.js +101 -0
- package/dist/react/useDeltaStreams.js.map +1 -0
- package/dist/react/useStreamingUIMessages.d.ts.map +1 -1
- package/dist/react/useStreamingUIMessages.js +66 -181
- package/dist/react/useStreamingUIMessages.js.map +1 -1
- package/dist/react/useUIMessages.d.ts.map +1 -1
- package/dist/react/useUIMessages.js +4 -1
- package/dist/react/useUIMessages.js.map +1 -1
- package/dist/validators.d.ts +78 -78
- package/dist/validators.js +1 -1
- package/dist/validators.js.map +1 -1
- package/package.json +7 -5
- package/src/client/index.ts +2 -1
- package/src/client/streaming.test.ts +2 -2
- package/src/client/streaming.ts +58 -5
- package/src/component/_generated/api.d.ts +8 -8
- package/src/component/streams.ts +1 -0
- package/src/deltas.ts +62 -54
- package/src/react/useDeltaStreams.ts +155 -0
- package/src/react/useStreamingUIMessages.ts +94 -251
- package/src/react/useUIMessages.ts +5 -2
- package/src/validators.ts +1 -1
- package/dist/package.json +0 -3
|
@@ -1,26 +1,24 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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 {
|
|
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 [
|
|
55
|
-
Record<
|
|
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
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
281
|
-
return
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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 {
|
|
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(
|
|
163
|
+
results: dedupeMessages(combined, streamMessages ?? []),
|
|
161
164
|
};
|
|
162
165
|
}, [paginated, streamMessages]);
|
|
163
166
|
|
package/src/validators.ts
CHANGED
package/dist/package.json
DELETED