@copilotkitnext/agent 1.53.1-next.2 → 1.54.0-next.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/CHANGELOG.md +10 -0
- package/dist/index.cjs +23 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -6
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +11 -6
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +24 -5
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -4
- package/src/__tests__/standard-schema-tools.test.ts +313 -0
- package/src/__tests__/standard-schema-types.test.ts +158 -0
- package/src/__tests__/utils.test.ts +78 -0
- package/src/__tests__/zod-regression.test.ts +350 -0
- package/src/index.ts +41 -12
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { describe, it, expectTypeOf } from "vitest";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import * as v from "valibot";
|
|
4
|
+
import { type } from "arktype";
|
|
5
|
+
import type { StandardSchemaV1 } from "@copilotkitnext/shared";
|
|
6
|
+
import { defineTool, type ToolDefinition } from "../index";
|
|
7
|
+
|
|
8
|
+
describe("ToolDefinition type inference", () => {
|
|
9
|
+
describe("defineTool with Zod", () => {
|
|
10
|
+
it("infers execute args from Zod schema", () => {
|
|
11
|
+
const tool = defineTool({
|
|
12
|
+
name: "weather",
|
|
13
|
+
description: "Get weather",
|
|
14
|
+
parameters: z.object({
|
|
15
|
+
city: z.string(),
|
|
16
|
+
units: z.enum(["celsius", "fahrenheit"]),
|
|
17
|
+
}),
|
|
18
|
+
execute: async (args) => {
|
|
19
|
+
// args should be fully typed
|
|
20
|
+
expectTypeOf(args).toEqualTypeOf<{
|
|
21
|
+
city: string;
|
|
22
|
+
units: "celsius" | "fahrenheit";
|
|
23
|
+
}>();
|
|
24
|
+
return {};
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
expectTypeOf(tool.name).toBeString();
|
|
29
|
+
expectTypeOf(tool.description).toBeString();
|
|
30
|
+
expectTypeOf(tool.parameters).toMatchTypeOf<StandardSchemaV1>();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("infers execute args with optional Zod fields", () => {
|
|
34
|
+
defineTool({
|
|
35
|
+
name: "search",
|
|
36
|
+
description: "Search",
|
|
37
|
+
parameters: z.object({
|
|
38
|
+
query: z.string(),
|
|
39
|
+
limit: z.number().optional(),
|
|
40
|
+
}),
|
|
41
|
+
execute: async (args) => {
|
|
42
|
+
expectTypeOf(args).toEqualTypeOf<{
|
|
43
|
+
query: string;
|
|
44
|
+
limit?: number | undefined;
|
|
45
|
+
}>();
|
|
46
|
+
return {};
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("defineTool with Valibot", () => {
|
|
53
|
+
it("infers execute args from Valibot schema", () => {
|
|
54
|
+
defineTool({
|
|
55
|
+
name: "search",
|
|
56
|
+
description: "Search",
|
|
57
|
+
parameters: v.object({
|
|
58
|
+
query: v.string(),
|
|
59
|
+
limit: v.number(),
|
|
60
|
+
}),
|
|
61
|
+
execute: async (args) => {
|
|
62
|
+
expectTypeOf(args).toEqualTypeOf<{
|
|
63
|
+
query: string;
|
|
64
|
+
limit: number;
|
|
65
|
+
}>();
|
|
66
|
+
return {};
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("infers execute args with optional Valibot fields", () => {
|
|
72
|
+
defineTool({
|
|
73
|
+
name: "search",
|
|
74
|
+
description: "Search",
|
|
75
|
+
parameters: v.object({
|
|
76
|
+
query: v.string(),
|
|
77
|
+
limit: v.optional(v.number()),
|
|
78
|
+
}),
|
|
79
|
+
execute: async (args) => {
|
|
80
|
+
expectTypeOf(args).toEqualTypeOf<{
|
|
81
|
+
query: string;
|
|
82
|
+
limit?: number | undefined;
|
|
83
|
+
}>();
|
|
84
|
+
return {};
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("defineTool with ArkType", () => {
|
|
91
|
+
it("infers execute args from ArkType schema", () => {
|
|
92
|
+
defineTool({
|
|
93
|
+
name: "search",
|
|
94
|
+
description: "Search",
|
|
95
|
+
parameters: type({
|
|
96
|
+
query: "string",
|
|
97
|
+
limit: "number",
|
|
98
|
+
}),
|
|
99
|
+
execute: async (args) => {
|
|
100
|
+
expectTypeOf(args).toEqualTypeOf<{
|
|
101
|
+
query: string;
|
|
102
|
+
limit: number;
|
|
103
|
+
}>();
|
|
104
|
+
return {};
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("infers execute args with optional ArkType fields", () => {
|
|
110
|
+
defineTool({
|
|
111
|
+
name: "profile",
|
|
112
|
+
description: "Profile",
|
|
113
|
+
parameters: type({
|
|
114
|
+
name: "string",
|
|
115
|
+
"age?": "number",
|
|
116
|
+
}),
|
|
117
|
+
execute: async (args) => {
|
|
118
|
+
expectTypeOf(args).toEqualTypeOf<{
|
|
119
|
+
name: string;
|
|
120
|
+
age?: number;
|
|
121
|
+
}>();
|
|
122
|
+
return {};
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("ToolDefinition interface", () => {
|
|
129
|
+
it("accepts Zod schema as generic parameter", () => {
|
|
130
|
+
type ZodTool = ToolDefinition<z.ZodObject<{ city: z.ZodString }>>;
|
|
131
|
+
|
|
132
|
+
expectTypeOf<ZodTool["execute"]>().toBeFunction();
|
|
133
|
+
expectTypeOf<Parameters<ZodTool["execute"]>[0]>().toEqualTypeOf<{
|
|
134
|
+
city: string;
|
|
135
|
+
}>();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("default generic parameter is StandardSchemaV1", () => {
|
|
139
|
+
type DefaultTool = ToolDefinition;
|
|
140
|
+
|
|
141
|
+
expectTypeOf<
|
|
142
|
+
DefaultTool["parameters"]
|
|
143
|
+
>().toMatchTypeOf<StandardSchemaV1>();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("preserves schema type through defineTool return", () => {
|
|
147
|
+
const schema = z.object({ x: z.number() });
|
|
148
|
+
const tool = defineTool({
|
|
149
|
+
name: "t",
|
|
150
|
+
description: "d",
|
|
151
|
+
parameters: schema,
|
|
152
|
+
execute: async () => ({}),
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
expectTypeOf(tool.parameters).toEqualTypeOf<typeof schema>();
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
});
|
|
@@ -421,6 +421,84 @@ describe("convertToolDefinitionsToVercelAITools", () => {
|
|
|
421
421
|
});
|
|
422
422
|
});
|
|
423
423
|
|
|
424
|
+
describe("ensureObjectArgs via convertMessagesToVercelAISDKMessages", () => {
|
|
425
|
+
function makeAssistantWithToolArgs(argsJson: string): Message[] {
|
|
426
|
+
return [
|
|
427
|
+
{
|
|
428
|
+
id: "1",
|
|
429
|
+
role: "assistant",
|
|
430
|
+
content: "calling tool",
|
|
431
|
+
toolCalls: [
|
|
432
|
+
{
|
|
433
|
+
id: "call1",
|
|
434
|
+
type: "function",
|
|
435
|
+
function: {
|
|
436
|
+
name: "myTool",
|
|
437
|
+
arguments: argsJson,
|
|
438
|
+
},
|
|
439
|
+
},
|
|
440
|
+
],
|
|
441
|
+
},
|
|
442
|
+
];
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
it("should pass through valid object arguments", () => {
|
|
446
|
+
const result = convertMessagesToVercelAISDKMessages(
|
|
447
|
+
makeAssistantWithToolArgs('{"key":"value"}'),
|
|
448
|
+
);
|
|
449
|
+
const toolCall = (result[0] as any).content[1];
|
|
450
|
+
expect(toolCall.input).toEqual({ key: "value" });
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it("should replace a string argument with an empty object", () => {
|
|
454
|
+
const result = convertMessagesToVercelAISDKMessages(
|
|
455
|
+
makeAssistantWithToolArgs('""'),
|
|
456
|
+
);
|
|
457
|
+
const toolCall = (result[0] as any).content[1];
|
|
458
|
+
expect(toolCall.input).toEqual({});
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it("should replace an array argument with an empty object", () => {
|
|
462
|
+
const result = convertMessagesToVercelAISDKMessages(
|
|
463
|
+
makeAssistantWithToolArgs("[1,2,3]"),
|
|
464
|
+
);
|
|
465
|
+
const toolCall = (result[0] as any).content[1];
|
|
466
|
+
expect(toolCall.input).toEqual({});
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it("should replace a null argument with an empty object", () => {
|
|
470
|
+
const result = convertMessagesToVercelAISDKMessages(
|
|
471
|
+
makeAssistantWithToolArgs("null"),
|
|
472
|
+
);
|
|
473
|
+
const toolCall = (result[0] as any).content[1];
|
|
474
|
+
expect(toolCall.input).toEqual({});
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it("should replace a numeric argument with an empty object", () => {
|
|
478
|
+
const result = convertMessagesToVercelAISDKMessages(
|
|
479
|
+
makeAssistantWithToolArgs("42"),
|
|
480
|
+
);
|
|
481
|
+
const toolCall = (result[0] as any).content[1];
|
|
482
|
+
expect(toolCall.input).toEqual({});
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it("should replace a boolean argument with an empty object", () => {
|
|
486
|
+
const result = convertMessagesToVercelAISDKMessages(
|
|
487
|
+
makeAssistantWithToolArgs("true"),
|
|
488
|
+
);
|
|
489
|
+
const toolCall = (result[0] as any).content[1];
|
|
490
|
+
expect(toolCall.input).toEqual({});
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it("should replace unparseable JSON with an empty object", () => {
|
|
494
|
+
const result = convertMessagesToVercelAISDKMessages(
|
|
495
|
+
makeAssistantWithToolArgs("{broken"),
|
|
496
|
+
);
|
|
497
|
+
const toolCall = (result[0] as any).content[1];
|
|
498
|
+
expect(toolCall.input).toEqual({});
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
|
|
424
502
|
describe("defineTool", () => {
|
|
425
503
|
it("should create a ToolDefinition", () => {
|
|
426
504
|
const tool = defineTool({
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression tests proving that the agent package's defineTool and
|
|
3
|
+
* convertToolDefinitionsToVercelAITools still work identically with
|
|
4
|
+
* Zod schemas after the Standard Schema migration.
|
|
5
|
+
*
|
|
6
|
+
* Covers:
|
|
7
|
+
* 1. defineTool with complex Zod schemas
|
|
8
|
+
* 2. convertToolDefinitionsToVercelAITools passes Zod schemas directly
|
|
9
|
+
* (not through JSON Schema conversion — critical behavioral regression)
|
|
10
|
+
* 3. BuiltInAgent with Zod tools still works end-to-end
|
|
11
|
+
* 4. execute callback receives correctly shaped args
|
|
12
|
+
*/
|
|
13
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
import {
|
|
16
|
+
defineTool,
|
|
17
|
+
convertToolDefinitionsToVercelAITools,
|
|
18
|
+
BuiltInAgent,
|
|
19
|
+
} from "../index";
|
|
20
|
+
import type { RunAgentInput } from "@ag-ui/client";
|
|
21
|
+
import { streamText } from "ai";
|
|
22
|
+
import { mockStreamTextResponse, finish, collectEvents } from "./test-helpers";
|
|
23
|
+
|
|
24
|
+
vi.mock("ai", async () => {
|
|
25
|
+
const actual = await vi.importActual("ai");
|
|
26
|
+
return {
|
|
27
|
+
...actual,
|
|
28
|
+
streamText: vi.fn(),
|
|
29
|
+
tool: vi.fn((config) => config),
|
|
30
|
+
stepCountIs: vi.fn((count: number) => ({ type: "stepCount", count })),
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
vi.mock("@ai-sdk/openai", () => ({
|
|
35
|
+
createOpenAI: vi.fn(() => (modelId: string) => ({
|
|
36
|
+
modelId,
|
|
37
|
+
provider: "openai",
|
|
38
|
+
})),
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
describe("defineTool Zod regression", () => {
|
|
42
|
+
it("complex Zod schema with nested objects, arrays, unions", () => {
|
|
43
|
+
const tool = defineTool({
|
|
44
|
+
name: "complexTool",
|
|
45
|
+
description: "A complex tool",
|
|
46
|
+
parameters: z.object({
|
|
47
|
+
query: z.string().describe("Search query"),
|
|
48
|
+
filters: z
|
|
49
|
+
.object({
|
|
50
|
+
category: z.enum(["books", "movies", "music"]).optional(),
|
|
51
|
+
minRating: z.number().min(0).max(5).optional(),
|
|
52
|
+
tags: z.array(z.string()).optional(),
|
|
53
|
+
})
|
|
54
|
+
.optional(),
|
|
55
|
+
pagination: z
|
|
56
|
+
.object({
|
|
57
|
+
page: z.number().int().positive().default(1),
|
|
58
|
+
perPage: z.number().int().positive().default(20),
|
|
59
|
+
})
|
|
60
|
+
.optional(),
|
|
61
|
+
}),
|
|
62
|
+
execute: async (args) => {
|
|
63
|
+
return { query: args.query };
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(tool.name).toBe("complexTool");
|
|
68
|
+
expect(tool.parameters["~standard"].vendor).toBe("zod");
|
|
69
|
+
expect(tool.parameters["~standard"].version).toBe(1);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("Zod schema with discriminated union", () => {
|
|
73
|
+
const tool = defineTool({
|
|
74
|
+
name: "actionTool",
|
|
75
|
+
description: "An action tool",
|
|
76
|
+
parameters: z.object({
|
|
77
|
+
action: z.discriminatedUnion("type", [
|
|
78
|
+
z.object({ type: z.literal("search"), query: z.string() }),
|
|
79
|
+
z.object({ type: z.literal("navigate"), url: z.string().url() }),
|
|
80
|
+
z.object({
|
|
81
|
+
type: z.literal("execute"),
|
|
82
|
+
code: z.string(),
|
|
83
|
+
language: z.enum(["javascript", "python"]),
|
|
84
|
+
}),
|
|
85
|
+
]),
|
|
86
|
+
}),
|
|
87
|
+
execute: async (args) => {
|
|
88
|
+
return { action: args.action.type };
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(tool.name).toBe("actionTool");
|
|
93
|
+
expect(tool.parameters["~standard"].vendor).toBe("zod");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("Zod schema with nullable and record fields", () => {
|
|
97
|
+
const tool = defineTool({
|
|
98
|
+
name: "flexTool",
|
|
99
|
+
description: "Flexible tool",
|
|
100
|
+
parameters: z.object({
|
|
101
|
+
title: z.string(),
|
|
102
|
+
description: z.string().nullable(),
|
|
103
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
104
|
+
}),
|
|
105
|
+
execute: async (args) => {
|
|
106
|
+
return { title: args.title };
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
expect(tool.name).toBe("flexTool");
|
|
111
|
+
expect(tool.parameters["~standard"].vendor).toBe("zod");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("execute callback receives correctly typed args", async () => {
|
|
115
|
+
const receivedArgs: unknown[] = [];
|
|
116
|
+
|
|
117
|
+
const tool = defineTool({
|
|
118
|
+
name: "argCapture",
|
|
119
|
+
description: "Captures args",
|
|
120
|
+
parameters: z.object({
|
|
121
|
+
city: z.string(),
|
|
122
|
+
units: z.enum(["celsius", "fahrenheit"]),
|
|
123
|
+
}),
|
|
124
|
+
execute: async (args) => {
|
|
125
|
+
receivedArgs.push(args);
|
|
126
|
+
return { temp: 22 };
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
await tool.execute({ city: "Berlin", units: "celsius" });
|
|
131
|
+
|
|
132
|
+
expect(receivedArgs).toHaveLength(1);
|
|
133
|
+
expect(receivedArgs[0]).toEqual({ city: "Berlin", units: "celsius" });
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe("convertToolDefinitionsToVercelAITools Zod regression", () => {
|
|
138
|
+
it("Zod schemas are passed directly to AI SDK (not converted via JSON Schema)", () => {
|
|
139
|
+
const zodSchema = z.object({
|
|
140
|
+
city: z.string(),
|
|
141
|
+
units: z.enum(["celsius", "fahrenheit"]).optional(),
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const tools = [
|
|
145
|
+
defineTool({
|
|
146
|
+
name: "weather",
|
|
147
|
+
description: "Get weather",
|
|
148
|
+
parameters: zodSchema,
|
|
149
|
+
execute: async () => ({ temp: 22 }),
|
|
150
|
+
}),
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
const aiTools = convertToolDefinitionsToVercelAITools(tools);
|
|
154
|
+
|
|
155
|
+
expect(aiTools).toHaveProperty("weather");
|
|
156
|
+
// The inputSchema should be the original Zod schema (passed directly),
|
|
157
|
+
// NOT a jsonSchema() wrapper. We verify by checking the Zod-specific
|
|
158
|
+
// ~standard.vendor property is preserved on the schema.
|
|
159
|
+
const inputSchema = aiTools.weather.inputSchema;
|
|
160
|
+
expect(inputSchema).toBeDefined();
|
|
161
|
+
// For Zod schemas, the AI SDK receives the raw Zod object (which has ~standard.vendor === "zod")
|
|
162
|
+
// If it went through jsonSchema(), it would lose the ~standard property
|
|
163
|
+
expect(inputSchema["~standard"]?.vendor).toBe("zod");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("multiple Zod tools all get direct pass-through", () => {
|
|
167
|
+
const tools = [
|
|
168
|
+
defineTool({
|
|
169
|
+
name: "tool1",
|
|
170
|
+
description: "Tool 1",
|
|
171
|
+
parameters: z.object({ a: z.string() }),
|
|
172
|
+
execute: async () => ({}),
|
|
173
|
+
}),
|
|
174
|
+
defineTool({
|
|
175
|
+
name: "tool2",
|
|
176
|
+
description: "Tool 2",
|
|
177
|
+
parameters: z.object({ b: z.number(), c: z.boolean().optional() }),
|
|
178
|
+
execute: async () => ({}),
|
|
179
|
+
}),
|
|
180
|
+
defineTool({
|
|
181
|
+
name: "tool3",
|
|
182
|
+
description: "Tool 3",
|
|
183
|
+
parameters: z.object({
|
|
184
|
+
nested: z.object({ x: z.number(), y: z.number() }),
|
|
185
|
+
}),
|
|
186
|
+
execute: async () => ({}),
|
|
187
|
+
}),
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
const aiTools = convertToolDefinitionsToVercelAITools(tools);
|
|
191
|
+
|
|
192
|
+
for (const name of ["tool1", "tool2", "tool3"]) {
|
|
193
|
+
expect(aiTools).toHaveProperty(name);
|
|
194
|
+
expect(aiTools[name].inputSchema["~standard"]?.vendor).toBe("zod");
|
|
195
|
+
expect(typeof aiTools[name].execute).toBe("function");
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("preserves tool description and execute function", () => {
|
|
200
|
+
const executeFn = vi.fn().mockResolvedValue({ result: "ok" });
|
|
201
|
+
|
|
202
|
+
const tools = [
|
|
203
|
+
defineTool({
|
|
204
|
+
name: "myTool",
|
|
205
|
+
description: "My custom tool description",
|
|
206
|
+
parameters: z.object({ input: z.string() }),
|
|
207
|
+
execute: executeFn,
|
|
208
|
+
}),
|
|
209
|
+
];
|
|
210
|
+
|
|
211
|
+
const aiTools = convertToolDefinitionsToVercelAITools(tools);
|
|
212
|
+
|
|
213
|
+
expect(aiTools.myTool.description).toBe("My custom tool description");
|
|
214
|
+
expect(aiTools.myTool.execute).toBeDefined();
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe("BuiltInAgent Zod regression", () => {
|
|
219
|
+
const originalEnv = process.env;
|
|
220
|
+
|
|
221
|
+
beforeEach(() => {
|
|
222
|
+
vi.clearAllMocks();
|
|
223
|
+
process.env = { ...originalEnv };
|
|
224
|
+
process.env.OPENAI_API_KEY = "test-key";
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
afterEach(() => {
|
|
228
|
+
process.env = originalEnv;
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("Zod tools are included in streamText call", async () => {
|
|
232
|
+
const agent = new BuiltInAgent({
|
|
233
|
+
model: "openai/gpt-4o",
|
|
234
|
+
tools: [
|
|
235
|
+
defineTool({
|
|
236
|
+
name: "weather",
|
|
237
|
+
description: "Get weather",
|
|
238
|
+
parameters: z.object({
|
|
239
|
+
city: z.string(),
|
|
240
|
+
units: z.enum(["celsius", "fahrenheit"]).optional(),
|
|
241
|
+
}),
|
|
242
|
+
execute: async () => ({ temp: 22 }),
|
|
243
|
+
}),
|
|
244
|
+
],
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
vi.mocked(streamText).mockReturnValue(
|
|
248
|
+
mockStreamTextResponse([finish()]) as any,
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
const input: RunAgentInput = {
|
|
252
|
+
threadId: "thread1",
|
|
253
|
+
runId: "run1",
|
|
254
|
+
messages: [],
|
|
255
|
+
tools: [],
|
|
256
|
+
context: [],
|
|
257
|
+
state: {},
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
await collectEvents(agent["run"](input));
|
|
261
|
+
|
|
262
|
+
const callArgs = vi.mocked(streamText).mock.calls[0][0];
|
|
263
|
+
expect(callArgs.tools).toHaveProperty("weather");
|
|
264
|
+
expect(typeof callArgs.tools?.weather.execute).toBe("function");
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("multiple Zod tools coexist with built-in state tools", async () => {
|
|
268
|
+
const agent = new BuiltInAgent({
|
|
269
|
+
model: "openai/gpt-4o",
|
|
270
|
+
tools: [
|
|
271
|
+
defineTool({
|
|
272
|
+
name: "search",
|
|
273
|
+
description: "Search",
|
|
274
|
+
parameters: z.object({ query: z.string() }),
|
|
275
|
+
execute: async () => ({}),
|
|
276
|
+
}),
|
|
277
|
+
defineTool({
|
|
278
|
+
name: "calculate",
|
|
279
|
+
description: "Calculate",
|
|
280
|
+
parameters: z.object({
|
|
281
|
+
expression: z.string(),
|
|
282
|
+
precision: z.number().int().optional(),
|
|
283
|
+
}),
|
|
284
|
+
execute: async () => ({}),
|
|
285
|
+
}),
|
|
286
|
+
],
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
vi.mocked(streamText).mockReturnValue(
|
|
290
|
+
mockStreamTextResponse([finish()]) as any,
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
const input: RunAgentInput = {
|
|
294
|
+
threadId: "thread1",
|
|
295
|
+
runId: "run1",
|
|
296
|
+
messages: [],
|
|
297
|
+
tools: [],
|
|
298
|
+
context: [],
|
|
299
|
+
state: {},
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
await collectEvents(agent["run"](input));
|
|
303
|
+
|
|
304
|
+
const callArgs = vi.mocked(streamText).mock.calls[0][0];
|
|
305
|
+
// User-defined Zod tools
|
|
306
|
+
expect(callArgs.tools).toHaveProperty("search");
|
|
307
|
+
expect(callArgs.tools).toHaveProperty("calculate");
|
|
308
|
+
// Built-in state tools
|
|
309
|
+
expect(callArgs.tools).toHaveProperty("AGUISendStateSnapshot");
|
|
310
|
+
expect(callArgs.tools).toHaveProperty("AGUISendStateDelta");
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("Zod tool execute is invocable after conversion", async () => {
|
|
314
|
+
const executeFn = vi.fn().mockResolvedValue({ temp: 22 });
|
|
315
|
+
|
|
316
|
+
const agent = new BuiltInAgent({
|
|
317
|
+
model: "openai/gpt-4o",
|
|
318
|
+
tools: [
|
|
319
|
+
defineTool({
|
|
320
|
+
name: "weather",
|
|
321
|
+
description: "Get weather",
|
|
322
|
+
parameters: z.object({ city: z.string() }),
|
|
323
|
+
execute: executeFn,
|
|
324
|
+
}),
|
|
325
|
+
],
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
vi.mocked(streamText).mockReturnValue(
|
|
329
|
+
mockStreamTextResponse([finish()]) as any,
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
const input: RunAgentInput = {
|
|
333
|
+
threadId: "thread1",
|
|
334
|
+
runId: "run1",
|
|
335
|
+
messages: [],
|
|
336
|
+
tools: [],
|
|
337
|
+
context: [],
|
|
338
|
+
state: {},
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
await collectEvents(agent["run"](input));
|
|
342
|
+
|
|
343
|
+
const callArgs = vi.mocked(streamText).mock.calls[0][0];
|
|
344
|
+
const weatherTool = callArgs.tools?.weather;
|
|
345
|
+
|
|
346
|
+
// Invoke the execute function to verify it's wired correctly
|
|
347
|
+
await weatherTool.execute({ city: "Berlin" });
|
|
348
|
+
expect(executeFn).toHaveBeenCalledWith({ city: "Berlin" });
|
|
349
|
+
});
|
|
350
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -42,7 +42,14 @@ import { createOpenAI } from "@ai-sdk/openai";
|
|
|
42
42
|
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
43
43
|
import { createGoogleGenerativeAI } from "@ai-sdk/google";
|
|
44
44
|
import { randomUUID } from "crypto";
|
|
45
|
+
import { safeParseToolArgs } from "@copilotkitnext/shared";
|
|
45
46
|
import { z } from "zod";
|
|
47
|
+
import type {
|
|
48
|
+
StandardSchemaV1,
|
|
49
|
+
InferSchemaOutput,
|
|
50
|
+
} from "@copilotkitnext/shared";
|
|
51
|
+
import { schemaToJsonSchema } from "@copilotkitnext/shared";
|
|
52
|
+
import { jsonSchema as aiJsonSchema } from "ai";
|
|
46
53
|
import {
|
|
47
54
|
StreamableHTTPClientTransport,
|
|
48
55
|
StreamableHTTPClientTransportOptions,
|
|
@@ -224,27 +231,27 @@ export function resolveModel(
|
|
|
224
231
|
* Tool definition for BuiltInAgent
|
|
225
232
|
*/
|
|
226
233
|
export interface ToolDefinition<
|
|
227
|
-
TParameters extends
|
|
234
|
+
TParameters extends StandardSchemaV1 = StandardSchemaV1,
|
|
228
235
|
> {
|
|
229
236
|
name: string;
|
|
230
237
|
description: string;
|
|
231
238
|
parameters: TParameters;
|
|
232
|
-
execute: (args:
|
|
239
|
+
execute: (args: InferSchemaOutput<TParameters>) => Promise<unknown>;
|
|
233
240
|
}
|
|
234
241
|
|
|
235
242
|
/**
|
|
236
243
|
* Define a tool for use with BuiltInAgent
|
|
237
244
|
* @param name - The name of the tool
|
|
238
245
|
* @param description - Description of what the tool does
|
|
239
|
-
* @param parameters -
|
|
246
|
+
* @param parameters - Schema for the tool's input parameters (any Standard Schema V1 compatible library: Zod, Valibot, ArkType, etc.)
|
|
240
247
|
* @param execute - Function to execute the tool server-side
|
|
241
248
|
* @returns Tool definition
|
|
242
249
|
*/
|
|
243
|
-
export function defineTool<TParameters extends
|
|
250
|
+
export function defineTool<TParameters extends StandardSchemaV1>(config: {
|
|
244
251
|
name: string;
|
|
245
252
|
description: string;
|
|
246
253
|
parameters: TParameters;
|
|
247
|
-
execute: (args:
|
|
254
|
+
execute: (args: InferSchemaOutput<TParameters>) => Promise<unknown>;
|
|
248
255
|
}): ToolDefinition<TParameters> {
|
|
249
256
|
return {
|
|
250
257
|
name: config.name,
|
|
@@ -327,7 +334,7 @@ export function convertMessagesToVercelAISDKMessages(
|
|
|
327
334
|
type: "tool-call",
|
|
328
335
|
toolCallId: toolCall.id,
|
|
329
336
|
toolName: toolCall.function.name,
|
|
330
|
-
input:
|
|
337
|
+
input: safeParseToolArgs(toolCall.function.arguments),
|
|
331
338
|
};
|
|
332
339
|
parts.push(toolCallPart);
|
|
333
340
|
}
|
|
@@ -480,7 +487,18 @@ export function convertToolsToVercelAITools(
|
|
|
480
487
|
}
|
|
481
488
|
|
|
482
489
|
/**
|
|
483
|
-
*
|
|
490
|
+
* Check whether a schema is a Zod schema by inspecting its Standard Schema vendor.
|
|
491
|
+
*/
|
|
492
|
+
function isZodSchema(schema: StandardSchemaV1): boolean {
|
|
493
|
+
return schema["~standard"]?.vendor === "zod";
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Converts ToolDefinition array to Vercel AI SDK ToolSet.
|
|
498
|
+
*
|
|
499
|
+
* For Zod schemas, passes them directly to the AI SDK (Zod satisfies FlexibleSchema).
|
|
500
|
+
* For non-Zod schemas, converts to JSON Schema via schemaToJsonSchema() and wraps
|
|
501
|
+
* with the AI SDK's jsonSchema() helper.
|
|
484
502
|
*/
|
|
485
503
|
export function convertToolDefinitionsToVercelAITools(
|
|
486
504
|
tools: ToolDefinition[],
|
|
@@ -489,11 +507,22 @@ export function convertToolDefinitionsToVercelAITools(
|
|
|
489
507
|
const result: Record<string, any> = {};
|
|
490
508
|
|
|
491
509
|
for (const tool of tools) {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
510
|
+
if (isZodSchema(tool.parameters)) {
|
|
511
|
+
// Zod schemas can be passed directly to AI SDK (satisfies FlexibleSchema)
|
|
512
|
+
result[tool.name] = createVercelAISDKTool({
|
|
513
|
+
description: tool.description,
|
|
514
|
+
inputSchema: tool.parameters as any,
|
|
515
|
+
execute: tool.execute,
|
|
516
|
+
});
|
|
517
|
+
} else {
|
|
518
|
+
// Non-Zod: convert to JSON Schema and wrap with AI SDK's jsonSchema()
|
|
519
|
+
const jsonSchemaObj = schemaToJsonSchema(tool.parameters);
|
|
520
|
+
result[tool.name] = createVercelAISDKTool({
|
|
521
|
+
description: tool.description,
|
|
522
|
+
inputSchema: aiJsonSchema(jsonSchemaObj),
|
|
523
|
+
execute: tool.execute,
|
|
524
|
+
});
|
|
525
|
+
}
|
|
497
526
|
}
|
|
498
527
|
|
|
499
528
|
return result;
|