@convex-dev/agent 0.7.1 → 0.7.3
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/README.md +5 -8
- package/dist/client/messages.d.ts +10 -0
- package/dist/client/messages.d.ts.map +1 -1
- package/dist/client/messages.js +1 -0
- package/dist/client/messages.js.map +1 -1
- package/dist/component/_generated/component.d.ts +10 -8
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/messages.d.ts +340 -338
- package/dist/component/messages.d.ts.map +1 -1
- package/dist/component/messages.js +74 -14
- package/dist/component/messages.js.map +1 -1
- package/dist/component/schema.d.ts +726 -622
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/streams.d.ts +6 -1
- package/dist/component/streams.d.ts.map +1 -1
- package/dist/component/streams.js +14 -15
- package/dist/component/streams.js.map +1 -1
- package/dist/component/threads.d.ts.map +1 -1
- package/dist/component/threads.js +1 -0
- package/dist/component/threads.js.map +1 -1
- package/dist/component/vector/index.d.ts +9 -9
- package/dist/component/vector/index.d.ts.map +1 -1
- package/dist/component/vector/tables.d.ts +5 -5
- package/dist/component/vector/tables.d.ts.map +1 -1
- package/dist/component/vector/tables.js +1 -1
- package/dist/component/vector/tables.js.map +1 -1
- package/dist/errors.d.ts +2 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +106 -0
- package/dist/errors.js.map +1 -0
- package/dist/validators.d.ts +1952 -1952
- package/dist/vercel/client/definePlaygroundAPI.d.ts +132 -132
- package/dist/vercel/client/messages.d.ts +50 -32
- package/dist/vercel/client/messages.d.ts.map +1 -1
- package/dist/vercel/client/messages.js +2 -0
- package/dist/vercel/client/messages.js.map +1 -1
- package/dist/vercel/client/search.d.ts +475 -32
- package/dist/vercel/client/search.d.ts.map +1 -1
- package/dist/vercel/client/search.js +34 -9
- package/dist/vercel/client/search.js.map +1 -1
- package/dist/vercel/client/start.d.ts.map +1 -1
- package/dist/vercel/client/start.js +1 -0
- package/dist/vercel/client/start.js.map +1 -1
- package/dist/vercel/client/streamText.d.ts +1 -2
- package/dist/vercel/client/streamText.d.ts.map +1 -1
- package/dist/vercel/client/streamText.js +79 -22
- package/dist/vercel/client/streamText.js.map +1 -1
- package/dist/vercel/client/streaming.d.ts +326 -305
- package/dist/vercel/client/streaming.d.ts.map +1 -1
- package/dist/vercel/client/streaming.js +140 -25
- package/dist/vercel/client/streaming.js.map +1 -1
- package/dist/vercel/client/types.d.ts +2 -0
- package/dist/vercel/client/types.d.ts.map +1 -1
- package/dist/vercel/client/types.js.map +1 -1
- package/dist/vercel/client/utils.d.ts +9 -5
- package/dist/vercel/client/utils.d.ts.map +1 -1
- package/dist/vercel/client/utils.js +13 -13
- package/dist/vercel/client/utils.js.map +1 -1
- package/dist/vercel/index.d.ts +91 -90
- package/dist/vercel/index.d.ts.map +1 -1
- package/dist/vercel/index.js +2 -0
- package/dist/vercel/index.js.map +1 -1
- package/package.json +16 -9
- package/src/client/messages.ts +12 -0
- package/src/component/_generated/component.ts +10 -0
- package/src/component/messages.test.ts +404 -0
- package/src/component/messages.ts +94 -12
- package/src/component/streams.test.ts +54 -1
- package/src/component/streams.ts +20 -17
- package/src/component/threads.test.ts +39 -0
- package/src/component/threads.ts +1 -0
- package/src/component/vector/tables.ts +1 -1
- package/src/errors.test.ts +76 -0
- package/src/errors.ts +113 -0
- package/src/vercel/client/deltaFlush.test.ts +106 -0
- package/src/vercel/client/index.test.ts +47 -0
- package/src/vercel/client/messages.ts +20 -0
- package/src/vercel/client/search.test.ts +183 -8
- package/src/vercel/client/search.ts +46 -12
- package/src/vercel/client/start.ts +1 -0
- package/src/vercel/client/streamText.test.ts +577 -18
- package/src/vercel/client/streamText.ts +84 -32
- package/src/vercel/client/streaming.integration.test.ts +40 -3
- package/src/vercel/client/streaming.test.ts +101 -7
- package/src/vercel/client/streaming.throttle.test.ts +144 -0
- package/src/vercel/client/streaming.ts +161 -25
- package/src/vercel/client/types.ts +2 -0
- package/src/vercel/client/utils.test.ts +97 -0
- package/src/vercel/client/utils.ts +15 -15
- package/src/vercel/index.ts +2 -0
package/src/component/streams.ts
CHANGED
|
@@ -197,24 +197,29 @@ function publicStreamMessage(m: Doc<"streamingMessages">): StreamMessage {
|
|
|
197
197
|
};
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
+
export async function abortStreamsAtOrder(
|
|
201
|
+
ctx: MutationCtx,
|
|
202
|
+
args: { threadId: Id<"threads">; order: number; reason: string },
|
|
203
|
+
) {
|
|
204
|
+
const streams = await ctx.db
|
|
205
|
+
.query("streamingMessages")
|
|
206
|
+
.withIndex("threadId_state_order_stepOrder", (q) =>
|
|
207
|
+
q
|
|
208
|
+
.eq("threadId", args.threadId)
|
|
209
|
+
.eq("state.kind", "streaming")
|
|
210
|
+
.eq("order", args.order),
|
|
211
|
+
)
|
|
212
|
+
.take(100);
|
|
213
|
+
for (const stream of streams) {
|
|
214
|
+
await abortById(ctx, { streamId: stream._id, reason: args.reason });
|
|
215
|
+
}
|
|
216
|
+
return streams.length > 0;
|
|
217
|
+
}
|
|
218
|
+
|
|
200
219
|
export const abortByOrder = mutation({
|
|
201
220
|
args: { threadId: v.id("threads"), order: v.number(), reason: v.string() },
|
|
202
221
|
returns: v.boolean(),
|
|
203
|
-
handler:
|
|
204
|
-
const streams = await ctx.db
|
|
205
|
-
.query("streamingMessages")
|
|
206
|
-
.withIndex("threadId_state_order_stepOrder", (q) =>
|
|
207
|
-
q
|
|
208
|
-
.eq("threadId", args.threadId)
|
|
209
|
-
.eq("state.kind", "streaming")
|
|
210
|
-
.eq("order", args.order),
|
|
211
|
-
)
|
|
212
|
-
.take(100);
|
|
213
|
-
for (const stream of streams) {
|
|
214
|
-
await abortById(ctx, { streamId: stream._id, reason: args.reason });
|
|
215
|
-
}
|
|
216
|
-
return streams.length > 0;
|
|
217
|
-
},
|
|
222
|
+
handler: abortStreamsAtOrder,
|
|
218
223
|
});
|
|
219
224
|
|
|
220
225
|
export const abort = mutation({
|
|
@@ -465,8 +470,6 @@ export async function deleteStreamsPageForThreadId(
|
|
|
465
470
|
);
|
|
466
471
|
let deltaCursor = args.deltaCursor;
|
|
467
472
|
const streamMessage = await mergedStream(allStreamMessages, [
|
|
468
|
-
"threadId",
|
|
469
|
-
"state.kind",
|
|
470
473
|
"order",
|
|
471
474
|
"stepOrder",
|
|
472
475
|
]).first();
|
|
@@ -458,6 +458,45 @@ describe("threads", () => {
|
|
|
458
458
|
vi.useRealTimers();
|
|
459
459
|
});
|
|
460
460
|
|
|
461
|
+
test("deleteAllForThreadIdAsync preserves its limit across continuations", async () => {
|
|
462
|
+
vi.useFakeTimers();
|
|
463
|
+
|
|
464
|
+
const t = convexTest(schema, modules);
|
|
465
|
+
const thread = await t.mutation(api.threads.createThread, {
|
|
466
|
+
userId: "boundedDeleteUser",
|
|
467
|
+
title: "Bounded Delete Thread",
|
|
468
|
+
});
|
|
469
|
+
const threadId = thread._id as Id<"threads">;
|
|
470
|
+
await t.mutation(api.messages.addMessages, {
|
|
471
|
+
threadId,
|
|
472
|
+
messages: Array.from({ length: 4 }, (_, i) => ({
|
|
473
|
+
message: { role: "user" as const, content: `Message ${i}` },
|
|
474
|
+
})),
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
await t.mutation(api.threads.deleteAllForThreadIdAsync, {
|
|
478
|
+
threadId,
|
|
479
|
+
limit: 1,
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
vi.runOnlyPendingTimers();
|
|
483
|
+
await t.finishInProgressScheduledFunctions();
|
|
484
|
+
const afterOneContinuation = await t.query(
|
|
485
|
+
api.messages.listMessagesByThreadId,
|
|
486
|
+
{
|
|
487
|
+
threadId,
|
|
488
|
+
order: "asc",
|
|
489
|
+
paginationOpts: { cursor: null, numItems: 10 },
|
|
490
|
+
},
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
await t.finishAllScheduledFunctions(vi.runAllTimers);
|
|
494
|
+
vi.useRealTimers();
|
|
495
|
+
|
|
496
|
+
expect(afterOneContinuation.page).toHaveLength(2);
|
|
497
|
+
expect(await t.query(api.threads.getThread, { threadId })).toBeNull();
|
|
498
|
+
});
|
|
499
|
+
|
|
461
500
|
test("deleteAllForThreadIdSync handles thread with no messages", async () => {
|
|
462
501
|
const t = convexTest(schema, modules);
|
|
463
502
|
|
package/src/component/threads.ts
CHANGED
|
@@ -207,6 +207,7 @@ export const deleteAllForThreadIdAsync = mutation({
|
|
|
207
207
|
if (!isDone) {
|
|
208
208
|
await ctx.scheduler.runAfter(0, api.threads.deleteAllForThreadIdAsync, {
|
|
209
209
|
threadId: args.threadId,
|
|
210
|
+
limit: args.limit,
|
|
210
211
|
cursor: messagesResult.cursor,
|
|
211
212
|
messagesDone: messagesResult.isDone,
|
|
212
213
|
streamsDone: streamResult.isDone,
|
|
@@ -68,7 +68,7 @@ export type VectorSchema = SchemaDefinition<
|
|
|
68
68
|
>;
|
|
69
69
|
|
|
70
70
|
export const VectorDimensions = [
|
|
71
|
-
128, 256, 512, 768, 1024, 1408, 1536, 2048, 3072, 4096,
|
|
71
|
+
128, 256, 512, 768, 1024, 1408, 1536, 2048, 2560, 3072, 4096,
|
|
72
72
|
] as const;
|
|
73
73
|
export function validateVectorDimension(
|
|
74
74
|
dimension: number,
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { APICallError } from "@ai-sdk/provider";
|
|
2
|
+
import { describe, expect, test } from "vitest";
|
|
3
|
+
import { errorToString } from "./errors.js";
|
|
4
|
+
|
|
5
|
+
describe("errorToString", () => {
|
|
6
|
+
test("preserves provider error classifications", () => {
|
|
7
|
+
const details = {
|
|
8
|
+
error: {
|
|
9
|
+
code: "invalid_prompt",
|
|
10
|
+
message: "Invalid prompt: flagged by policy",
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
const apiError = new APICallError({
|
|
14
|
+
message: "Invalid prompt: flagged by policy",
|
|
15
|
+
url: "https://api.example.test",
|
|
16
|
+
requestBodyValues: {},
|
|
17
|
+
statusCode: 400,
|
|
18
|
+
data: details,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
expect(errorToString(details)).toBe(
|
|
22
|
+
"invalid_prompt: Invalid prompt: flagged by policy",
|
|
23
|
+
);
|
|
24
|
+
expect(errorToString(apiError)).toBe(
|
|
25
|
+
"invalid_prompt: Invalid prompt: flagged by policy",
|
|
26
|
+
);
|
|
27
|
+
expect(errorToString(new Error())).toBe("Error");
|
|
28
|
+
expect(errorToString(new TypeError())).toBe("TypeError");
|
|
29
|
+
const systemError = Object.assign(new Error("socket hang up"), {
|
|
30
|
+
code: "ECONNRESET",
|
|
31
|
+
});
|
|
32
|
+
expect(errorToString(systemError)).toBe("socket hang up");
|
|
33
|
+
const codeOnly = Object.assign(new Error("Request failed"), {
|
|
34
|
+
data: { code: "rate_limit" },
|
|
35
|
+
});
|
|
36
|
+
expect(errorToString(codeOnly)).toBe("rate_limit: Request failed");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("serializes objects without mistaking shared values for cycles", () => {
|
|
40
|
+
const shared = { detail: "provider disconnected" };
|
|
41
|
+
const circular: Record<string, unknown> = { shared };
|
|
42
|
+
circular.self = circular;
|
|
43
|
+
|
|
44
|
+
expect(errorToString({ x: shared, y: shared })).toBe(
|
|
45
|
+
'{"x":{"detail":"provider disconnected"},"y":{"detail":"provider disconnected"}}',
|
|
46
|
+
);
|
|
47
|
+
expect(errorToString(circular)).toBe(
|
|
48
|
+
'{"shared":{"detail":"provider disconnected"},"self":"[Circular]"}',
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("bounds stored error text without splitting surrogate pairs", () => {
|
|
53
|
+
const serialized = errorToString(`${"x".repeat(1022)}😀tail`);
|
|
54
|
+
|
|
55
|
+
expect(serialized.length).toBeLessThanOrEqual(1024);
|
|
56
|
+
expect(serialized.endsWith("x…")).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("does not throw when Error properties are hostile accessors", () => {
|
|
60
|
+
const error = new Error();
|
|
61
|
+
Object.defineProperties(error, {
|
|
62
|
+
message: {
|
|
63
|
+
get() {
|
|
64
|
+
throw new Error("message getter failed");
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
name: {
|
|
68
|
+
get() {
|
|
69
|
+
throw new Error("name getter failed");
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
expect(errorToString(error)).toBe("Unknown error");
|
|
75
|
+
});
|
|
76
|
+
});
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
const MAX_ERROR_LENGTH = 1024;
|
|
2
|
+
|
|
3
|
+
export function errorToString(error: unknown): string {
|
|
4
|
+
return truncateError(describeError(error));
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function describeError(error: unknown): string {
|
|
8
|
+
if (typeof error === "string") return error;
|
|
9
|
+
if (error instanceof Error) {
|
|
10
|
+
const message = property(error, "message");
|
|
11
|
+
if (typeof message !== "string" || message.length === 0) {
|
|
12
|
+
const name = property(error, "name");
|
|
13
|
+
return typeof name === "string" && name.length > 0
|
|
14
|
+
? name
|
|
15
|
+
: safeString(error);
|
|
16
|
+
}
|
|
17
|
+
const nested = errorDetails(
|
|
18
|
+
property(error, "error") ?? property(error, "data"),
|
|
19
|
+
);
|
|
20
|
+
return (
|
|
21
|
+
formatDetails({
|
|
22
|
+
message: nested.message ?? message,
|
|
23
|
+
code: nested.code,
|
|
24
|
+
}) ?? message
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const details = formatDetails(errorDetails(error));
|
|
29
|
+
if (details) return details;
|
|
30
|
+
|
|
31
|
+
if (error && typeof error === "object") {
|
|
32
|
+
try {
|
|
33
|
+
const ancestors: object[] = [];
|
|
34
|
+
const serialized = JSON.stringify(error, function (_key, value: unknown) {
|
|
35
|
+
if (typeof value === "bigint") return value.toString();
|
|
36
|
+
if (!value || typeof value !== "object") return value;
|
|
37
|
+
while (ancestors.length > 0 && ancestors.at(-1) !== this) {
|
|
38
|
+
ancestors.pop();
|
|
39
|
+
}
|
|
40
|
+
if (ancestors.includes(value)) return "[Circular]";
|
|
41
|
+
ancestors.push(value);
|
|
42
|
+
return value;
|
|
43
|
+
});
|
|
44
|
+
if (serialized) return serialized;
|
|
45
|
+
} catch {
|
|
46
|
+
return safeString(error);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return safeString(error);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function safeString(error: unknown): string {
|
|
54
|
+
try {
|
|
55
|
+
return String(error);
|
|
56
|
+
} catch {
|
|
57
|
+
return "Unknown error";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function errorDetails(error: unknown): { message?: string; code?: string } {
|
|
62
|
+
let current = error;
|
|
63
|
+
let message: string | undefined;
|
|
64
|
+
let code: string | undefined;
|
|
65
|
+
for (let depth = 0; depth < 3; depth++) {
|
|
66
|
+
if (typeof current === "string") {
|
|
67
|
+
message ??= current;
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
if (!current || typeof current !== "object") break;
|
|
71
|
+
|
|
72
|
+
const currentMessage = property(current, "message");
|
|
73
|
+
if (typeof currentMessage === "string" && currentMessage.length > 0) {
|
|
74
|
+
message ??= currentMessage;
|
|
75
|
+
}
|
|
76
|
+
const currentCode = property(current, "code");
|
|
77
|
+
if (typeof currentCode === "string" || typeof currentCode === "number") {
|
|
78
|
+
code ??= String(currentCode);
|
|
79
|
+
}
|
|
80
|
+
if (message && code) break;
|
|
81
|
+
current = property(current, "error") ?? property(current, "data");
|
|
82
|
+
}
|
|
83
|
+
return { message, code };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function property(value: object, key: string): unknown {
|
|
87
|
+
try {
|
|
88
|
+
return (value as Record<string, unknown>)[key];
|
|
89
|
+
} catch {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function formatDetails({
|
|
95
|
+
message,
|
|
96
|
+
code,
|
|
97
|
+
}: {
|
|
98
|
+
message?: string;
|
|
99
|
+
code?: string;
|
|
100
|
+
}): string | undefined {
|
|
101
|
+
if (message && code) {
|
|
102
|
+
return message.startsWith(`${code}:`) ? message : `${code}: ${message}`;
|
|
103
|
+
}
|
|
104
|
+
return message ?? code;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function truncateError(error: string): string {
|
|
108
|
+
if (error.length <= MAX_ERROR_LENGTH) return error;
|
|
109
|
+
let truncated = error.slice(0, MAX_ERROR_LENGTH - 1);
|
|
110
|
+
const last = truncated.charCodeAt(truncated.length - 1);
|
|
111
|
+
if (last >= 0xd800 && last <= 0xdbff) truncated = truncated.slice(0, -1);
|
|
112
|
+
return `${truncated}…`;
|
|
113
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { stepCountIs } from "ai";
|
|
4
|
+
import { Agent, createTool, createThread } from "../index.js";
|
|
5
|
+
import {
|
|
6
|
+
actionGeneric,
|
|
7
|
+
anyApi,
|
|
8
|
+
defineSchema,
|
|
9
|
+
type ActionBuilder,
|
|
10
|
+
type ApiFromModules,
|
|
11
|
+
type DataModelFromSchemaDefinition,
|
|
12
|
+
} from "convex/server";
|
|
13
|
+
import { v } from "convex/values";
|
|
14
|
+
import { components, initConvexTest } from "./setup.test.js";
|
|
15
|
+
import { mockModel } from "./mockModel.js";
|
|
16
|
+
|
|
17
|
+
const schema = defineSchema({});
|
|
18
|
+
type DataModel = DataModelFromSchemaDefinition<typeof schema>;
|
|
19
|
+
const action = actionGeneric as ActionBuilder<DataModel, "public">;
|
|
20
|
+
|
|
21
|
+
// The AI SDK emits tool-input-available before the tool runs. The whole point
|
|
22
|
+
// of issue #221 is that a client can see it while the tool is still working,
|
|
23
|
+
// so the tool itself is where the delta log has to be read.
|
|
24
|
+
const partsSeenDuringTool: string[] = [];
|
|
25
|
+
|
|
26
|
+
const sleepTool = createTool({
|
|
27
|
+
description: "sleep",
|
|
28
|
+
inputSchema: z.object({ seconds: z.number() }),
|
|
29
|
+
execute: async (ctx) => {
|
|
30
|
+
const readTypes = async () => {
|
|
31
|
+
const streams = await ctx.runQuery(components.agent.streams.list, {
|
|
32
|
+
threadId: ctx.threadId!,
|
|
33
|
+
statuses: ["streaming"],
|
|
34
|
+
});
|
|
35
|
+
const deltas = await ctx.runQuery(components.agent.streams.listDeltas, {
|
|
36
|
+
threadId: ctx.threadId!,
|
|
37
|
+
cursors: streams.map((s) => ({ streamId: s.streamId, cursor: 0 })),
|
|
38
|
+
});
|
|
39
|
+
return deltas.flatMap((d) => d.parts.map((p) => p.type));
|
|
40
|
+
};
|
|
41
|
+
// Wait on the condition rather than on a fixed sleep chosen to outrun the
|
|
42
|
+
// throttle: there is no margin to tune, so machine load cannot decide the
|
|
43
|
+
// outcome. Without a scheduled flush this simply never arrives.
|
|
44
|
+
const deadline = Date.now() + 2_000;
|
|
45
|
+
let types = await readTypes();
|
|
46
|
+
while (!types.includes("tool-input-available") && Date.now() < deadline) {
|
|
47
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
48
|
+
types = await readTypes();
|
|
49
|
+
}
|
|
50
|
+
partsSeenDuringTool.push(...types);
|
|
51
|
+
return { slept: 3 };
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const agent = new Agent(components.agent, {
|
|
56
|
+
name: "slow-tool",
|
|
57
|
+
languageModel: mockModel({
|
|
58
|
+
contentSteps: [
|
|
59
|
+
[
|
|
60
|
+
{
|
|
61
|
+
type: "tool-call",
|
|
62
|
+
toolCallId: "t1",
|
|
63
|
+
toolName: "sleepTool",
|
|
64
|
+
input: JSON.stringify({ seconds: 3 }),
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
[{ type: "text", text: "done" }],
|
|
68
|
+
],
|
|
69
|
+
}),
|
|
70
|
+
tools: { sleepTool },
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
export const run = action({
|
|
74
|
+
args: { threadId: v.string() },
|
|
75
|
+
handler: async (ctx, { threadId }) => {
|
|
76
|
+
const r = await agent.streamText(
|
|
77
|
+
ctx,
|
|
78
|
+
{ threadId },
|
|
79
|
+
{ prompt: "go", stopWhen: stepCountIs(3) },
|
|
80
|
+
{ saveStreamDeltas: { chunking: "word", throttleMs: 50 } },
|
|
81
|
+
);
|
|
82
|
+
await r.consumeStream();
|
|
83
|
+
return { ok: true };
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const testApi: ApiFromModules<{ fns: { run: typeof run } }>["fns"] =
|
|
88
|
+
anyApi["deltaFlush.test"] as unknown as ApiFromModules<{
|
|
89
|
+
fns: { run: typeof run };
|
|
90
|
+
}>["fns"];
|
|
91
|
+
|
|
92
|
+
describe("throttled deltas flush on time (issue #221)", () => {
|
|
93
|
+
test("the tool call is queryable while the tool is still running", async () => {
|
|
94
|
+
partsSeenDuringTool.length = 0;
|
|
95
|
+
const t = initConvexTest(schema);
|
|
96
|
+
const threadId = await t.run(async (ctx) =>
|
|
97
|
+
createThread(ctx, components.agent, { userId: "u" }),
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
await t.action(testApi.run, { threadId });
|
|
101
|
+
await t.finishAllScheduledFunctions(() => {});
|
|
102
|
+
|
|
103
|
+
expect(partsSeenDuringTool).toContain("tool-input-available");
|
|
104
|
+
expect(partsSeenDuringTool).not.toContain("tool-output-available");
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
createThread,
|
|
5
5
|
createTool,
|
|
6
6
|
filterOutOrphanedToolMessages,
|
|
7
|
+
saveMessage,
|
|
8
|
+
toUIMessages,
|
|
7
9
|
type MessageDoc,
|
|
8
10
|
} from "../index.js";
|
|
9
11
|
import type { DataModelFromSchemaDefinition } from "convex/server";
|
|
@@ -458,6 +460,51 @@ describe("Agent message operations", () => {
|
|
|
458
460
|
expect(messages.length).toBe(2);
|
|
459
461
|
expect(messages[1]._id).toBeDefined();
|
|
460
462
|
});
|
|
463
|
+
|
|
464
|
+
test("saveMessage can place a standalone assistant message on a new order", async () => {
|
|
465
|
+
const t = initConvexTest(schema);
|
|
466
|
+
const threadId = await t.run(async (ctx) =>
|
|
467
|
+
createThread(ctx, components.agent, { userId: "operator-test" }),
|
|
468
|
+
);
|
|
469
|
+
const { message: agentReply } = await t.run(async (ctx) =>
|
|
470
|
+
agent.saveMessage(ctx, {
|
|
471
|
+
threadId,
|
|
472
|
+
message: {
|
|
473
|
+
role: "assistant",
|
|
474
|
+
content: [
|
|
475
|
+
{
|
|
476
|
+
type: "tool-call",
|
|
477
|
+
toolCallId: "handoff-1",
|
|
478
|
+
toolName: "handoff",
|
|
479
|
+
input: {},
|
|
480
|
+
},
|
|
481
|
+
],
|
|
482
|
+
},
|
|
483
|
+
}),
|
|
484
|
+
);
|
|
485
|
+
const { message: operatorReply } = await t.run(async (ctx) =>
|
|
486
|
+
saveMessage(ctx, components.agent, {
|
|
487
|
+
threadId,
|
|
488
|
+
order: "next",
|
|
489
|
+
agentName: "human:Alex",
|
|
490
|
+
message: { role: "assistant", content: "Operator reply" },
|
|
491
|
+
}),
|
|
492
|
+
);
|
|
493
|
+
const uiMessages = toUIMessages([agentReply, operatorReply]);
|
|
494
|
+
|
|
495
|
+
expect(agentReply).toMatchObject({ order: 0, stepOrder: 0 });
|
|
496
|
+
expect(operatorReply).toMatchObject({
|
|
497
|
+
order: 1,
|
|
498
|
+
stepOrder: 0,
|
|
499
|
+
agentName: "human:Alex",
|
|
500
|
+
});
|
|
501
|
+
expect(uiMessages).toHaveLength(2);
|
|
502
|
+
expect(uiMessages[1]).toMatchObject({
|
|
503
|
+
order: 1,
|
|
504
|
+
agentName: "human:Alex",
|
|
505
|
+
text: "Operator reply",
|
|
506
|
+
});
|
|
507
|
+
});
|
|
461
508
|
});
|
|
462
509
|
|
|
463
510
|
describe("Agent text/object generation", () => {
|
|
@@ -11,6 +11,7 @@ import { toUIMessages, type UIMessage } from "../UIMessages.js";
|
|
|
11
11
|
import {
|
|
12
12
|
listMessages,
|
|
13
13
|
saveMessages as saveCanonicalMessages,
|
|
14
|
+
type MessageOrder,
|
|
14
15
|
} from "../../client/messages.js";
|
|
15
16
|
import type {
|
|
16
17
|
AgentComponent,
|
|
@@ -36,6 +37,15 @@ export async function listUIMessages(
|
|
|
36
37
|
export type SaveMessagesArgs = {
|
|
37
38
|
threadId: string;
|
|
38
39
|
userId?: string | null;
|
|
40
|
+
/**
|
|
41
|
+
* Save the first message at this order. Pass `"next"` to allocate a new
|
|
42
|
+
* order after the current latest message. If the numeric order already
|
|
43
|
+
* contains messages, the message is appended at the next stepOrder.
|
|
44
|
+
* Numeric orders must be non-negative safe integers less than
|
|
45
|
+
* Number.MAX_SAFE_INTEGER.
|
|
46
|
+
* Cannot be combined with promptMessageId or pendingMessageId.
|
|
47
|
+
*/
|
|
48
|
+
order?: MessageOrder;
|
|
39
49
|
/**
|
|
40
50
|
* The message that these messages are in response to. They will be
|
|
41
51
|
* the same "order" as this message, at increasing stepOrder(s).
|
|
@@ -86,6 +96,7 @@ export async function saveMessages(
|
|
|
86
96
|
userId: args.userId ?? undefined,
|
|
87
97
|
agentName: args.agentName,
|
|
88
98
|
promptMessageId: args.promptMessageId,
|
|
99
|
+
order: args.order,
|
|
89
100
|
pendingMessageId: args.pendingMessageId,
|
|
90
101
|
embeddings: args.embeddings,
|
|
91
102
|
messages: serialized.map(({ message }) => message),
|
|
@@ -105,6 +116,14 @@ export async function saveMessages(
|
|
|
105
116
|
export type SaveMessageArgs = {
|
|
106
117
|
threadId: string;
|
|
107
118
|
userId?: string | null;
|
|
119
|
+
/**
|
|
120
|
+
* Save the message at this order. Pass `"next"` to allocate a new order
|
|
121
|
+
* after the current latest message. If the numeric order already contains
|
|
122
|
+
* messages, the message is appended at the next stepOrder. Numeric orders
|
|
123
|
+
* must be non-negative safe integers less than Number.MAX_SAFE_INTEGER.
|
|
124
|
+
* Cannot be combined with promptMessageId or pendingMessageId.
|
|
125
|
+
*/
|
|
126
|
+
order?: MessageOrder;
|
|
108
127
|
/**
|
|
109
128
|
* The message that these messages are in response to. They will be
|
|
110
129
|
* the same "order" as this message, at increasing stepOrder(s).
|
|
@@ -169,6 +188,7 @@ export async function saveMessage(
|
|
|
169
188
|
userId: args.userId ?? undefined,
|
|
170
189
|
agentName: args.agentName,
|
|
171
190
|
promptMessageId: args.promptMessageId,
|
|
191
|
+
order: args.order,
|
|
172
192
|
pendingMessageId: args.pendingMessageId,
|
|
173
193
|
messages:
|
|
174
194
|
args.prompt !== undefined
|