@convex-dev/agent 0.2.0-alpha.2 → 0.2.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.
- package/dist/client/createTool.d.ts +3 -5
- package/dist/client/createTool.d.ts.map +1 -1
- package/dist/client/createTool.js +0 -1
- package/dist/client/createTool.js.map +1 -1
- package/dist/client/definePlaygroundAPI.d.ts +28 -28
- package/dist/client/files.d.ts +8 -1
- package/dist/client/files.d.ts.map +1 -1
- package/dist/client/files.js +60 -5
- package/dist/client/files.js.map +1 -1
- package/dist/client/index.d.ts +223 -239
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +368 -382
- package/dist/client/index.js.map +1 -1
- package/dist/client/messages.d.ts +91 -373
- package/dist/client/messages.d.ts.map +1 -1
- package/dist/client/messages.js +7 -1
- package/dist/client/messages.js.map +1 -1
- package/dist/client/search.d.ts +15 -15
- package/dist/client/search.d.ts.map +1 -1
- package/dist/client/search.js +3 -3
- package/dist/client/search.js.map +1 -1
- package/dist/client/streaming.d.ts +21 -22
- package/dist/client/streaming.d.ts.map +1 -1
- package/dist/client/streaming.js +35 -40
- package/dist/client/streaming.js.map +1 -1
- package/dist/client/types.d.ts +148 -61
- package/dist/client/types.d.ts.map +1 -1
- package/dist/component/_generated/api.d.ts +47 -357
- package/dist/component/messages.d.ts +157 -150
- package/dist/component/messages.d.ts.map +1 -1
- package/dist/component/messages.js +124 -70
- package/dist/component/messages.js.map +1 -1
- package/dist/component/schema.d.ts +1147 -2687
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +5 -4
- package/dist/component/schema.js.map +1 -1
- package/dist/component/streams.d.ts +29 -323
- package/dist/component/streams.d.ts.map +1 -1
- package/dist/component/streams.js +104 -60
- package/dist/component/streams.js.map +1 -1
- package/dist/component/threads.d.ts +4 -4
- package/dist/component/users.d.ts +3 -3
- package/dist/mapping.d.ts +4 -1
- package/dist/mapping.d.ts.map +1 -1
- package/dist/mapping.js +17 -1
- package/dist/mapping.js.map +1 -1
- package/dist/parts.d.ts +3 -0
- package/dist/parts.d.ts.map +1 -0
- package/dist/parts.js +34 -0
- package/dist/parts.js.map +1 -0
- package/dist/react/SmoothText.d.ts +5 -0
- package/dist/react/SmoothText.d.ts.map +1 -0
- package/dist/react/SmoothText.js +6 -0
- package/dist/react/SmoothText.js.map +1 -0
- package/dist/react/deltas.d.ts +3 -2
- package/dist/react/deltas.d.ts.map +1 -1
- package/dist/react/deltas.js +19 -79
- package/dist/react/deltas.js.map +1 -1
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +31 -11
- package/dist/react/index.js.map +1 -1
- package/dist/react/optimisticallySendMessage.d.ts.map +1 -1
- package/dist/react/optimisticallySendMessage.js +2 -8
- package/dist/react/optimisticallySendMessage.js.map +1 -1
- package/dist/react/useSmoothText.d.ts +13 -12
- package/dist/react/useSmoothText.d.ts.map +1 -1
- package/dist/react/useSmoothText.js +1 -1
- package/dist/react/useSmoothText.js.map +1 -1
- package/dist/validators.d.ts +109 -1590
- package/dist/validators.d.ts.map +1 -1
- package/dist/validators.js +4 -81
- package/dist/validators.js.map +1 -1
- package/package.json +3 -3
- package/src/client/createTool.ts +4 -9
- package/src/client/files.ts +88 -10
- package/src/client/index.test.ts +9 -21
- package/src/client/index.ts +664 -650
- package/src/client/messages.ts +16 -2
- package/src/client/search.ts +10 -7
- package/src/client/streaming.ts +53 -57
- package/src/client/types.ts +227 -97
- package/src/component/_generated/api.d.ts +47 -357
- package/src/component/messages.ts +157 -96
- package/src/component/schema.ts +4 -4
- package/src/component/streams.ts +180 -69
- package/src/mapping.ts +23 -2
- package/src/parts.ts +39 -0
- package/src/react/SmoothText.tsx +9 -0
- package/src/react/deltas.test.ts +38 -42
- package/src/react/deltas.ts +24 -86
- package/src/react/index.ts +35 -13
- package/src/react/optimisticallySendMessage.ts +2 -8
- package/src/react/useSmoothText.ts +14 -15
- package/src/validators.test.ts +0 -17
- package/src/validators.ts +8 -101
package/src/component/streams.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { omit, pick } from "convex-helpers";
|
|
2
2
|
import { v } from "convex/values";
|
|
3
3
|
import {
|
|
4
|
+
type MessageWithMetadataInternal,
|
|
4
5
|
type StreamDelta,
|
|
5
6
|
vStreamDelta,
|
|
6
7
|
vStreamMessage,
|
|
@@ -18,6 +19,9 @@ import schema from "./schema.js";
|
|
|
18
19
|
import { stream } from "convex-helpers/server/stream";
|
|
19
20
|
import { mergedStream } from "convex-helpers/server/stream";
|
|
20
21
|
import { paginator } from "convex-helpers/server/pagination";
|
|
22
|
+
import type { WithoutSystemFields } from "convex/server";
|
|
23
|
+
import { mergeDeltas } from "../react/deltas.js";
|
|
24
|
+
import { serializeOrThrow } from "../mapping.js";
|
|
21
25
|
|
|
22
26
|
const SECOND = 1000;
|
|
23
27
|
const MINUTE = 60 * SECOND;
|
|
@@ -33,13 +37,16 @@ export const addDelta = mutation({
|
|
|
33
37
|
args: deltaValidator,
|
|
34
38
|
returns: v.boolean(),
|
|
35
39
|
handler: async (ctx, args) => {
|
|
36
|
-
await ctx.db.insert("streamDeltas", args);
|
|
37
|
-
await heartbeatStream(ctx, { streamId: args.streamId });
|
|
38
40
|
const stream = await ctx.db.get(args.streamId);
|
|
39
|
-
if (stream
|
|
40
|
-
console.warn(
|
|
41
|
+
if (!stream) {
|
|
42
|
+
console.warn("Stream not found", args.streamId);
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
if (stream.state.kind !== "streaming") {
|
|
41
46
|
return false;
|
|
42
47
|
}
|
|
48
|
+
await ctx.db.insert("streamDeltas", args);
|
|
49
|
+
await heartbeatStream(ctx, { streamId: args.streamId });
|
|
43
50
|
return true;
|
|
44
51
|
},
|
|
45
52
|
});
|
|
@@ -48,10 +55,7 @@ export const listDeltas = query({
|
|
|
48
55
|
args: {
|
|
49
56
|
threadId: v.id("threads"),
|
|
50
57
|
cursors: v.array(
|
|
51
|
-
v.object({
|
|
52
|
-
streamId: v.id("streamingMessages"),
|
|
53
|
-
cursor: v.number(),
|
|
54
|
-
}),
|
|
58
|
+
v.object({ streamId: v.id("streamingMessages"), cursor: v.number() }),
|
|
55
59
|
),
|
|
56
60
|
},
|
|
57
61
|
returns: v.array(vStreamDelta),
|
|
@@ -85,10 +89,7 @@ export const create = mutation({
|
|
|
85
89
|
args: omit(schema.tables.streamingMessages.validator.fields, ["state"]),
|
|
86
90
|
returns: v.id("streamingMessages"),
|
|
87
91
|
handler: async (ctx, args) => {
|
|
88
|
-
const state = {
|
|
89
|
-
kind: "streaming" as const,
|
|
90
|
-
lastHeartbeat: Date.now(),
|
|
91
|
-
};
|
|
92
|
+
const state = { kind: "streaming" as const, lastHeartbeat: Date.now() };
|
|
92
93
|
const streamId = await ctx.db.insert("streamingMessages", {
|
|
93
94
|
...args,
|
|
94
95
|
state,
|
|
@@ -152,11 +153,7 @@ export const list = query({
|
|
|
152
153
|
});
|
|
153
154
|
|
|
154
155
|
export const abortByOrder = mutation({
|
|
155
|
-
args: {
|
|
156
|
-
threadId: v.id("threads"),
|
|
157
|
-
order: v.number(),
|
|
158
|
-
reason: v.string(),
|
|
159
|
-
},
|
|
156
|
+
args: { threadId: v.id("threads"), order: v.number(), reason: v.string() },
|
|
160
157
|
returns: v.boolean(),
|
|
161
158
|
handler: async (ctx, args) => {
|
|
162
159
|
const streams = await ctx.db
|
|
@@ -169,10 +166,7 @@ export const abortByOrder = mutation({
|
|
|
169
166
|
)
|
|
170
167
|
.take(100);
|
|
171
168
|
for (const stream of streams) {
|
|
172
|
-
await abortById(ctx, {
|
|
173
|
-
streamId: stream._id,
|
|
174
|
-
reason: args.reason,
|
|
175
|
-
});
|
|
169
|
+
await abortById(ctx, { streamId: stream._id, reason: args.reason });
|
|
176
170
|
}
|
|
177
171
|
return streams.length > 0;
|
|
178
172
|
},
|
|
@@ -182,6 +176,7 @@ export const abort = mutation({
|
|
|
182
176
|
args: {
|
|
183
177
|
streamId: v.id("streamingMessages"),
|
|
184
178
|
reason: v.string(),
|
|
179
|
+
finalDelta: v.optional(deltaValidator),
|
|
185
180
|
},
|
|
186
181
|
returns: v.boolean(),
|
|
187
182
|
handler: abortById,
|
|
@@ -189,16 +184,20 @@ export const abort = mutation({
|
|
|
189
184
|
|
|
190
185
|
async function abortById(
|
|
191
186
|
ctx: MutationCtx,
|
|
192
|
-
args: {
|
|
187
|
+
args: {
|
|
188
|
+
streamId: Id<"streamingMessages">;
|
|
189
|
+
reason: string;
|
|
190
|
+
finalDelta?: WithoutSystemFields<Doc<"streamDeltas">>;
|
|
191
|
+
},
|
|
193
192
|
) {
|
|
194
193
|
const stream = await ctx.db.get(args.streamId);
|
|
195
194
|
if (!stream) {
|
|
196
195
|
throw new Error(`Stream not found: ${args.streamId}`);
|
|
197
196
|
}
|
|
197
|
+
if (args.finalDelta) {
|
|
198
|
+
await ctx.db.insert("streamDeltas", args.finalDelta);
|
|
199
|
+
}
|
|
198
200
|
if (stream.state.kind !== "streaming") {
|
|
199
|
-
console.warn(
|
|
200
|
-
`Stream trying to abort but not currently streaming (${stream.state.kind}): ${args.streamId}`,
|
|
201
|
-
);
|
|
202
201
|
return false;
|
|
203
202
|
}
|
|
204
203
|
await cleanupTimeoutFn(ctx, stream);
|
|
@@ -220,49 +219,65 @@ async function cleanupTimeoutFn(
|
|
|
220
219
|
}
|
|
221
220
|
}
|
|
222
221
|
|
|
222
|
+
// No longer used from the DeltaStreamer
|
|
223
223
|
export const finish = mutation({
|
|
224
224
|
args: {
|
|
225
225
|
streamId: v.id("streamingMessages"),
|
|
226
226
|
finalDelta: v.optional(deltaValidator),
|
|
227
227
|
},
|
|
228
228
|
returns: v.null(),
|
|
229
|
-
handler:
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
if (stream.state.kind !== "streaming") {
|
|
238
|
-
console.warn(
|
|
239
|
-
`Stream trying to finish but not currently streaming: ${args.streamId}`,
|
|
240
|
-
);
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
await cleanupTimeoutFn(ctx, stream);
|
|
244
|
-
const cleanupFnId = await ctx.scheduler.runAfter(
|
|
245
|
-
DELETE_STREAM_DELAY,
|
|
246
|
-
api.streams.deleteStreamAsync,
|
|
247
|
-
{ streamId: args.streamId },
|
|
248
|
-
);
|
|
249
|
-
await ctx.db.patch(args.streamId, {
|
|
250
|
-
state: { kind: "finished", endedAt: Date.now(), cleanupFnId },
|
|
251
|
-
});
|
|
229
|
+
handler: finishHandler,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
export async function finishHandler(
|
|
233
|
+
ctx: MutationCtx,
|
|
234
|
+
args: {
|
|
235
|
+
streamId: Id<"streamingMessages">;
|
|
236
|
+
finalDelta?: WithoutSystemFields<Doc<"streamDeltas">>;
|
|
252
237
|
},
|
|
238
|
+
) {
|
|
239
|
+
if (args.finalDelta) {
|
|
240
|
+
await ctx.db.insert("streamDeltas", args.finalDelta);
|
|
241
|
+
}
|
|
242
|
+
const stream = await ctx.db.get(args.streamId);
|
|
243
|
+
if (!stream) {
|
|
244
|
+
throw new Error(`Stream not found: ${args.streamId}`);
|
|
245
|
+
}
|
|
246
|
+
if (stream.state.kind !== "streaming") {
|
|
247
|
+
console.warn(
|
|
248
|
+
`Stream trying to finish ${args.streamId} but is ${stream.state.kind}`,
|
|
249
|
+
);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
await cleanupTimeoutFn(ctx, stream);
|
|
253
|
+
const cleanupFnId = await ctx.scheduler.runAfter(
|
|
254
|
+
DELETE_STREAM_DELAY,
|
|
255
|
+
api.streams.deleteStreamAsync,
|
|
256
|
+
{ streamId: args.streamId },
|
|
257
|
+
);
|
|
258
|
+
await ctx.db.patch(args.streamId, {
|
|
259
|
+
state: { kind: "finished", endedAt: Date.now(), cleanupFnId },
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// TODO: use this heartbeat while streaming, every 30 seconds or so,
|
|
264
|
+
// then reduce the timeout to 60 seconds.
|
|
265
|
+
export const heartbeat = mutation({
|
|
266
|
+
args: { streamId: v.id("streamingMessages") },
|
|
267
|
+
returns: v.null(),
|
|
268
|
+
handler: heartbeatStream,
|
|
253
269
|
});
|
|
254
270
|
|
|
255
271
|
async function heartbeatStream(
|
|
256
272
|
ctx: MutationCtx,
|
|
257
273
|
args: { streamId: Id<"streamingMessages"> },
|
|
258
|
-
) {
|
|
274
|
+
): Promise<void> {
|
|
259
275
|
const stream = await ctx.db.get(args.streamId);
|
|
260
276
|
if (!stream) {
|
|
261
277
|
console.warn("Stream not found", args.streamId);
|
|
262
278
|
return;
|
|
263
279
|
}
|
|
264
280
|
if (stream.state.kind !== "streaming") {
|
|
265
|
-
console.warn("Stream is not streaming", args.streamId);
|
|
266
281
|
return;
|
|
267
282
|
}
|
|
268
283
|
if (Date.now() - stream.state.lastHeartbeat < TIMEOUT_INTERVAL / 4) {
|
|
@@ -286,11 +301,7 @@ async function heartbeatStream(
|
|
|
286
301
|
{ streamId: args.streamId },
|
|
287
302
|
);
|
|
288
303
|
await ctx.db.patch(args.streamId, {
|
|
289
|
-
state: {
|
|
290
|
-
kind: "streaming",
|
|
291
|
-
lastHeartbeat: Date.now(),
|
|
292
|
-
timeoutFnId,
|
|
293
|
-
},
|
|
304
|
+
state: { kind: "streaming", lastHeartbeat: Date.now(), timeoutFnId },
|
|
294
305
|
});
|
|
295
306
|
}
|
|
296
307
|
|
|
@@ -304,10 +315,7 @@ export const timeoutStream = internalMutation({
|
|
|
304
315
|
return;
|
|
305
316
|
}
|
|
306
317
|
await ctx.db.patch(args.streamId, {
|
|
307
|
-
state: {
|
|
308
|
-
kind: "aborted",
|
|
309
|
-
reason: "timeout",
|
|
310
|
-
},
|
|
318
|
+
state: { kind: "aborted", reason: "timeout" },
|
|
311
319
|
});
|
|
312
320
|
},
|
|
313
321
|
});
|
|
@@ -362,11 +370,7 @@ export async function deleteStreamsPageForThreadId(
|
|
|
362
370
|
"stepOrder",
|
|
363
371
|
]).first();
|
|
364
372
|
if (!streamMessage) {
|
|
365
|
-
return {
|
|
366
|
-
isDone: true,
|
|
367
|
-
streamOrder: undefined,
|
|
368
|
-
deltaCursor: undefined,
|
|
369
|
-
};
|
|
373
|
+
return { isDone: true, streamOrder: undefined, deltaCursor: undefined };
|
|
370
374
|
}
|
|
371
375
|
const result = await deletePageForStreamId(ctx, {
|
|
372
376
|
streamId: streamMessage._id,
|
|
@@ -375,11 +379,7 @@ export async function deleteStreamsPageForThreadId(
|
|
|
375
379
|
if (result.isDone) {
|
|
376
380
|
deltaCursor = undefined;
|
|
377
381
|
}
|
|
378
|
-
return {
|
|
379
|
-
isDone: false,
|
|
380
|
-
streamOrder: streamMessage.order,
|
|
381
|
-
deltaCursor,
|
|
382
|
-
};
|
|
382
|
+
return { isDone: false, streamOrder: streamMessage.order, deltaCursor };
|
|
383
383
|
}
|
|
384
384
|
|
|
385
385
|
export const deleteStreamsPageForThreadIdMutation = internalMutation({
|
|
@@ -474,3 +474,114 @@ export const deleteAllStreamsForThreadIdSync = action({
|
|
|
474
474
|
}
|
|
475
475
|
},
|
|
476
476
|
});
|
|
477
|
+
|
|
478
|
+
export async function getStreamingMessages(
|
|
479
|
+
ctx: MutationCtx,
|
|
480
|
+
threadId: Id<"threads">,
|
|
481
|
+
order: number,
|
|
482
|
+
stepOrder: number,
|
|
483
|
+
): Promise<Doc<"streamingMessages">[]> {
|
|
484
|
+
return mergedStream(
|
|
485
|
+
(["aborted", "streaming", "finished"] as const).map((state) =>
|
|
486
|
+
stream(ctx.db, schema)
|
|
487
|
+
.query("streamingMessages")
|
|
488
|
+
.withIndex("threadId_state_order_stepOrder", (q) =>
|
|
489
|
+
q
|
|
490
|
+
.eq("threadId", threadId)
|
|
491
|
+
.eq("state.kind", state)
|
|
492
|
+
.eq("order", order)
|
|
493
|
+
.lte("stepOrder", stepOrder),
|
|
494
|
+
)
|
|
495
|
+
.order("desc"),
|
|
496
|
+
),
|
|
497
|
+
["stepOrder"],
|
|
498
|
+
).take(10);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
export async function getStreamingMessagesWithMetadata(
|
|
502
|
+
ctx: MutationCtx,
|
|
503
|
+
{
|
|
504
|
+
threadId,
|
|
505
|
+
order,
|
|
506
|
+
stepOrder,
|
|
507
|
+
}: { threadId: Id<"threads">; order: number; stepOrder: number },
|
|
508
|
+
metadata: { status: "success" | "failed"; error?: string },
|
|
509
|
+
): Promise<MessageWithMetadataInternal[]> {
|
|
510
|
+
// See if there are any streaming messages for this order
|
|
511
|
+
const streamingMessages = await getStreamingMessages(
|
|
512
|
+
ctx,
|
|
513
|
+
threadId,
|
|
514
|
+
order,
|
|
515
|
+
stepOrder,
|
|
516
|
+
);
|
|
517
|
+
const messages = (
|
|
518
|
+
await Promise.all(
|
|
519
|
+
streamingMessages.map((m) =>
|
|
520
|
+
getMessagesWithMetadataForStreamingMessage(
|
|
521
|
+
ctx,
|
|
522
|
+
threadId,
|
|
523
|
+
stepOrder,
|
|
524
|
+
m,
|
|
525
|
+
metadata,
|
|
526
|
+
),
|
|
527
|
+
),
|
|
528
|
+
)
|
|
529
|
+
).flat();
|
|
530
|
+
return messages;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
export async function getMessagesWithMetadataForStreamingMessage(
|
|
534
|
+
ctx: MutationCtx,
|
|
535
|
+
threadId: Id<"threads">,
|
|
536
|
+
stepOrder: number,
|
|
537
|
+
streamingMessage: Doc<"streamingMessages">,
|
|
538
|
+
metadata: { status: "success" | "failed"; error?: string },
|
|
539
|
+
): Promise<MessageWithMetadataInternal[]> {
|
|
540
|
+
const deltas = await ctx.db
|
|
541
|
+
.query("streamDeltas")
|
|
542
|
+
.withIndex("streamId_start_end", (q) =>
|
|
543
|
+
q.eq("streamId", streamingMessage._id),
|
|
544
|
+
)
|
|
545
|
+
.take(1000);
|
|
546
|
+
const [messageDocs] = mergeDeltas(
|
|
547
|
+
threadId,
|
|
548
|
+
[
|
|
549
|
+
{
|
|
550
|
+
...streamingMessage,
|
|
551
|
+
status: "streaming",
|
|
552
|
+
streamId: streamingMessage._id,
|
|
553
|
+
},
|
|
554
|
+
],
|
|
555
|
+
[],
|
|
556
|
+
deltas,
|
|
557
|
+
);
|
|
558
|
+
// We don't save messages that have already been saved
|
|
559
|
+
const numToSkip = stepOrder - streamingMessage.stepOrder;
|
|
560
|
+
const messages = await Promise.all(
|
|
561
|
+
messageDocs
|
|
562
|
+
.slice(numToSkip)
|
|
563
|
+
.filter((m) => m.message !== undefined)
|
|
564
|
+
.map(async (msg) => {
|
|
565
|
+
const message = await serializeOrThrow(msg.message!);
|
|
566
|
+
return {
|
|
567
|
+
message,
|
|
568
|
+
...pick(msg, [
|
|
569
|
+
"fileIds",
|
|
570
|
+
"status",
|
|
571
|
+
"finishReason",
|
|
572
|
+
"model",
|
|
573
|
+
"provider",
|
|
574
|
+
"providerMetadata",
|
|
575
|
+
"sources",
|
|
576
|
+
"reasoning",
|
|
577
|
+
"reasoningDetails",
|
|
578
|
+
"usage",
|
|
579
|
+
"warnings",
|
|
580
|
+
"error",
|
|
581
|
+
]),
|
|
582
|
+
...metadata,
|
|
583
|
+
} as MessageWithMetadataInternal;
|
|
584
|
+
}),
|
|
585
|
+
);
|
|
586
|
+
return messages;
|
|
587
|
+
}
|
package/src/mapping.ts
CHANGED
|
@@ -76,6 +76,25 @@ export async function serializeMessage(
|
|
|
76
76
|
};
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
export async function serializeOrThrow(
|
|
80
|
+
message: ModelMessage | Message,
|
|
81
|
+
): Promise<SerializedMessage> {
|
|
82
|
+
const { content } = await serializeContent(
|
|
83
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
84
|
+
{} as any,
|
|
85
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
86
|
+
{} as any,
|
|
87
|
+
message.content,
|
|
88
|
+
);
|
|
89
|
+
return {
|
|
90
|
+
role: message.role,
|
|
91
|
+
content,
|
|
92
|
+
...(message.providerOptions
|
|
93
|
+
? { providerOptions: message.providerOptions }
|
|
94
|
+
: {}),
|
|
95
|
+
} as SerializedMessage;
|
|
96
|
+
}
|
|
97
|
+
|
|
79
98
|
export function deserializeMessage(message: SerializedMessage): ModelMessage {
|
|
80
99
|
return {
|
|
81
100
|
...message,
|
|
@@ -128,7 +147,7 @@ export async function serializeNewMessagesInStep<TOOLS extends ToolSet>(
|
|
|
128
147
|
component: AgentComponent,
|
|
129
148
|
step: StepResult<TOOLS>,
|
|
130
149
|
metadata: { model: string; provider: string },
|
|
131
|
-
): Promise<MessageWithMetadata[]> {
|
|
150
|
+
): Promise<{ messages: MessageWithMetadata[] }> {
|
|
132
151
|
// If there are tool results, there's another message with the tool results
|
|
133
152
|
// ref: https://github.com/vercel/ai/blob/main/packages/ai/core/generate-text/to-response-messages.ts
|
|
134
153
|
const assistantFields = {
|
|
@@ -158,7 +177,8 @@ export async function serializeNewMessagesInStep<TOOLS extends ToolSet>(
|
|
|
158
177
|
};
|
|
159
178
|
}),
|
|
160
179
|
);
|
|
161
|
-
|
|
180
|
+
// TODO: capture step.files separately?
|
|
181
|
+
return { messages };
|
|
162
182
|
}
|
|
163
183
|
|
|
164
184
|
export async function serializeObjectResult(
|
|
@@ -288,6 +308,7 @@ export async function serializeContent(
|
|
|
288
308
|
providerOptions: part.providerOptions,
|
|
289
309
|
} satisfies Infer<typeof vReasoningPart>;
|
|
290
310
|
}
|
|
311
|
+
// Not in current generation output, but could be in historical messages
|
|
291
312
|
case "redacted-reasoning": {
|
|
292
313
|
return {
|
|
293
314
|
type: part.type,
|
package/src/parts.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { TextStreamPart, ToolSet } from "ai";
|
|
2
|
+
|
|
3
|
+
export function serializeTextStreamingPartsV5(
|
|
4
|
+
parts: TextStreamPart<ToolSet>[],
|
|
5
|
+
): TextStreamPart<ToolSet>[] {
|
|
6
|
+
const compressed: TextStreamPart<ToolSet>[] = [];
|
|
7
|
+
for (const part of parts) {
|
|
8
|
+
const last = compressed.at(-1);
|
|
9
|
+
if (part.type === "text-delta" && last?.type === "text-delta") {
|
|
10
|
+
last.text += part.text;
|
|
11
|
+
} else if (
|
|
12
|
+
part.type === "reasoning-delta" &&
|
|
13
|
+
last?.type === "reasoning-delta"
|
|
14
|
+
) {
|
|
15
|
+
last.text += part.text;
|
|
16
|
+
} else {
|
|
17
|
+
if (
|
|
18
|
+
part.type === "start-step" ||
|
|
19
|
+
part.type === "finish-step" ||
|
|
20
|
+
part.type === "start" ||
|
|
21
|
+
part.type === "finish"
|
|
22
|
+
) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (part.type === "file") {
|
|
26
|
+
compressed.push({
|
|
27
|
+
type: "file",
|
|
28
|
+
file: {
|
|
29
|
+
mediaType: part.file.mediaType,
|
|
30
|
+
base64: part.file.base64,
|
|
31
|
+
uint8Array: new Uint8Array([]),
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
compressed.push(part);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return compressed;
|
|
39
|
+
}
|
package/src/react/deltas.test.ts
CHANGED
|
@@ -1,36 +1,24 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
import { mergeDeltas, applyDeltasToStreamMessage } from "./deltas.js";
|
|
3
|
-
import type {
|
|
4
|
-
StreamMessage,
|
|
5
|
-
StreamDelta,
|
|
6
|
-
TextStreamPart,
|
|
7
|
-
} from "../validators.js";
|
|
3
|
+
import type { StreamMessage, StreamDelta } from "../validators.js";
|
|
8
4
|
import { omit } from "convex-helpers";
|
|
5
|
+
import type { TextStreamPart, ToolSet } from "ai";
|
|
9
6
|
|
|
10
7
|
function makeStreamMessage(
|
|
11
8
|
streamId: string,
|
|
12
9
|
order: number,
|
|
13
10
|
stepOrder: number,
|
|
14
11
|
): StreamMessage {
|
|
15
|
-
return {
|
|
16
|
-
streamId,
|
|
17
|
-
order,
|
|
18
|
-
stepOrder,
|
|
19
|
-
} as StreamMessage;
|
|
12
|
+
return { streamId, order, stepOrder } as StreamMessage;
|
|
20
13
|
}
|
|
21
14
|
|
|
22
15
|
function makeDelta(
|
|
23
16
|
streamId: string,
|
|
24
17
|
start: number,
|
|
25
18
|
end: number,
|
|
26
|
-
parts: TextStreamPart[],
|
|
19
|
+
parts: TextStreamPart<ToolSet>[],
|
|
27
20
|
): StreamDelta {
|
|
28
|
-
return {
|
|
29
|
-
streamId,
|
|
30
|
-
start,
|
|
31
|
-
end,
|
|
32
|
-
parts,
|
|
33
|
-
};
|
|
21
|
+
return { streamId, start, end, parts };
|
|
34
22
|
}
|
|
35
23
|
|
|
36
24
|
describe("mergeDeltas", () => {
|
|
@@ -38,7 +26,9 @@ describe("mergeDeltas", () => {
|
|
|
38
26
|
const streamId = "s1";
|
|
39
27
|
const streamMessages = [makeStreamMessage(streamId, 1, 0)];
|
|
40
28
|
const deltas = [
|
|
41
|
-
makeDelta(streamId, 0, 5, [
|
|
29
|
+
makeDelta(streamId, 0, 5, [
|
|
30
|
+
{ type: "text-delta", id: "1", text: "Hello" },
|
|
31
|
+
]),
|
|
42
32
|
];
|
|
43
33
|
const [messages, newStreams, changed] = mergeDeltas(
|
|
44
34
|
"thread1",
|
|
@@ -57,9 +47,11 @@ describe("mergeDeltas", () => {
|
|
|
57
47
|
const streamId = "s1";
|
|
58
48
|
const streamMessages = [makeStreamMessage(streamId, 1, 0)];
|
|
59
49
|
const deltas = [
|
|
60
|
-
makeDelta(streamId, 0, 5, [
|
|
50
|
+
makeDelta(streamId, 0, 5, [
|
|
51
|
+
{ type: "text-delta", id: "1", text: "Hello" },
|
|
52
|
+
]),
|
|
61
53
|
makeDelta(streamId, 5, 11, [
|
|
62
|
-
{ type: "text-delta",
|
|
54
|
+
{ type: "text-delta", id: "2", text: " World!" },
|
|
63
55
|
]),
|
|
64
56
|
];
|
|
65
57
|
const [messages, newStreams, changed] = mergeDeltas(
|
|
@@ -83,7 +75,7 @@ describe("mergeDeltas", () => {
|
|
|
83
75
|
type: "tool-call",
|
|
84
76
|
toolCallId: "call1",
|
|
85
77
|
toolName: "myTool",
|
|
86
|
-
|
|
78
|
+
input: "",
|
|
87
79
|
},
|
|
88
80
|
]),
|
|
89
81
|
makeDelta(streamId, 1, 2, [
|
|
@@ -91,7 +83,8 @@ describe("mergeDeltas", () => {
|
|
|
91
83
|
type: "tool-result",
|
|
92
84
|
toolCallId: "call1",
|
|
93
85
|
toolName: "myTool",
|
|
94
|
-
|
|
86
|
+
input: undefined,
|
|
87
|
+
output: "42",
|
|
95
88
|
},
|
|
96
89
|
]),
|
|
97
90
|
];
|
|
@@ -106,12 +99,7 @@ describe("mergeDeltas", () => {
|
|
|
106
99
|
expect(messages[0].tool).toBe(true);
|
|
107
100
|
const content = messages[0].message?.content;
|
|
108
101
|
expect(content).toEqual([
|
|
109
|
-
{
|
|
110
|
-
type: "tool-call",
|
|
111
|
-
toolCallId: "call1",
|
|
112
|
-
toolName: "myTool",
|
|
113
|
-
args: "",
|
|
114
|
-
},
|
|
102
|
+
{ type: "tool-call", toolCallId: "call1", toolName: "myTool", args: "" },
|
|
115
103
|
]);
|
|
116
104
|
expect(messages[1].message?.role).toBe("tool");
|
|
117
105
|
expect(messages[1].tool).toBe(true);
|
|
@@ -145,8 +133,8 @@ describe("mergeDeltas", () => {
|
|
|
145
133
|
const s1 = makeStreamMessage("s1", 1, 0);
|
|
146
134
|
const s2 = makeStreamMessage("s2", 2, 0);
|
|
147
135
|
const deltas = [
|
|
148
|
-
makeDelta("s2", 0, 3, [{ type: "text-delta",
|
|
149
|
-
makeDelta("s1", 0, 3, [{ type: "text-delta",
|
|
136
|
+
makeDelta("s2", 0, 3, [{ type: "text-delta", id: "1", text: "B" }]),
|
|
137
|
+
makeDelta("s1", 0, 3, [{ type: "text-delta", id: "2", text: "A" }]),
|
|
150
138
|
];
|
|
151
139
|
const [messages, _, changed] = mergeDeltas("thread1", [s2, s1], [], deltas);
|
|
152
140
|
expect(messages).toHaveLength(2);
|
|
@@ -162,11 +150,13 @@ describe("mergeDeltas", () => {
|
|
|
162
150
|
const streamId = "s4";
|
|
163
151
|
const streamMessages = [makeStreamMessage(streamId, 4, 0)];
|
|
164
152
|
const deltas = [
|
|
165
|
-
makeDelta(streamId, 0, 5, [
|
|
153
|
+
makeDelta(streamId, 0, 5, [
|
|
154
|
+
{ type: "text-delta", id: "1", text: "Hello" },
|
|
155
|
+
]),
|
|
166
156
|
makeDelta(streamId, 5, 11, [
|
|
167
|
-
{ type: "text-delta",
|
|
157
|
+
{ type: "text-delta", id: "2", text: " World!" },
|
|
168
158
|
]),
|
|
169
|
-
makeDelta(streamId, 11, 12, [{ type: "text-delta",
|
|
159
|
+
makeDelta(streamId, 11, 12, [{ type: "text-delta", id: "3", text: "!" }]),
|
|
170
160
|
];
|
|
171
161
|
const [messages] = mergeDeltas("thread1", streamMessages, [], deltas);
|
|
172
162
|
expect(messages).toHaveLength(1);
|
|
@@ -185,10 +175,10 @@ describe("mergeDeltas", () => {
|
|
|
185
175
|
const streamMessages = [makeStreamMessage(streamId, 6, 0)];
|
|
186
176
|
const deltas = [
|
|
187
177
|
makeDelta(streamId, 0, 1, [
|
|
188
|
-
{ type: "reasoning",
|
|
178
|
+
{ type: "reasoning-delta", id: "1", text: "I'm thinking..." },
|
|
189
179
|
]),
|
|
190
180
|
makeDelta(streamId, 1, 2, [
|
|
191
|
-
{ type: "reasoning",
|
|
181
|
+
{ type: "reasoning-delta", id: "2", text: " Still thinking..." },
|
|
192
182
|
]),
|
|
193
183
|
];
|
|
194
184
|
const [messages] = mergeDeltas("thread1", streamMessages, [], deltas);
|
|
@@ -206,9 +196,11 @@ describe("mergeDeltas", () => {
|
|
|
206
196
|
const streamId = "s7";
|
|
207
197
|
const streamMessage = makeStreamMessage(streamId, 7, 0);
|
|
208
198
|
const deltas = [
|
|
209
|
-
makeDelta(streamId, 0, 5, [
|
|
199
|
+
makeDelta(streamId, 0, 5, [
|
|
200
|
+
{ type: "text-delta", id: "1", text: "Hello" },
|
|
201
|
+
]),
|
|
210
202
|
makeDelta(streamId, 5, 11, [
|
|
211
|
-
{ type: "text-delta",
|
|
203
|
+
{ type: "text-delta", id: "2", text: " World!" },
|
|
212
204
|
]),
|
|
213
205
|
];
|
|
214
206
|
// First call: apply both deltas
|
|
@@ -232,7 +224,7 @@ describe("mergeDeltas", () => {
|
|
|
232
224
|
// Third call: add a new delta
|
|
233
225
|
const moreDeltas = [
|
|
234
226
|
...deltas,
|
|
235
|
-
makeDelta(streamId, 11, 12, [{ type: "text-delta",
|
|
227
|
+
makeDelta(streamId, 11, 12, [{ type: "text-delta", id: "3", text: "!" }]),
|
|
236
228
|
];
|
|
237
229
|
[result, changed] = applyDeltasToStreamMessage(
|
|
238
230
|
"thread1",
|
|
@@ -259,9 +251,11 @@ describe("mergeDeltas", () => {
|
|
|
259
251
|
const streamId = "s8";
|
|
260
252
|
const streamMessages = [makeStreamMessage(streamId, 8, 0)];
|
|
261
253
|
const deltas = [
|
|
262
|
-
makeDelta(streamId, 0, 5, [
|
|
254
|
+
makeDelta(streamId, 0, 5, [
|
|
255
|
+
{ type: "text-delta", id: "1", text: "Hello" },
|
|
256
|
+
]),
|
|
263
257
|
makeDelta(streamId, 5, 11, [
|
|
264
|
-
{ type: "text-delta",
|
|
258
|
+
{ type: "text-delta", id: "2", text: " World!" },
|
|
265
259
|
]),
|
|
266
260
|
];
|
|
267
261
|
// Deep freeze inputs to catch mutation
|
|
@@ -306,9 +300,11 @@ describe("mergeDeltas", () => {
|
|
|
306
300
|
// Inputs should remain unchanged
|
|
307
301
|
expect(streamMessages).toEqual([makeStreamMessage(streamId, 8, 0)]);
|
|
308
302
|
expect(deltas).toEqual([
|
|
309
|
-
makeDelta(streamId, 0, 5, [
|
|
303
|
+
makeDelta(streamId, 0, 5, [
|
|
304
|
+
{ type: "text-delta", id: "1", text: "Hello" },
|
|
305
|
+
]),
|
|
310
306
|
makeDelta(streamId, 5, 11, [
|
|
311
|
-
{ type: "text-delta",
|
|
307
|
+
{ type: "text-delta", id: "2", text: " World!" },
|
|
312
308
|
]),
|
|
313
309
|
]);
|
|
314
310
|
});
|