Skip to content

Commit 4c07d30

Browse files
committed
chore: enhance VoltAgent 3.x with AI SDK Tool Integration
1 parent 0a0161a commit 4c07d30

11 files changed

Lines changed: 533 additions & 171 deletions

File tree

.changeset/voltagent-3-next.md

Lines changed: 69 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ const result = await agent.streamText({
151151

152152
VoltAgent composes the fields it must own for framework behavior: `model`, `prompt`/`messages`, `tools`, `abortSignal`, `maxRetries`, `onStepEnd`, `onEnd`/`onFinish`, and `onError`. User callbacks are still accepted at the top level and are invoked after VoltAgent memory, guardrails, hooks, tracing, and recovery handling.
153153

154-
Top-level AI SDK `runtimeContext`, `toolsContext`, `telemetry`, and `experimental_telemetry` are intentionally not exposed. Use `voltagent.context` for per-call application context, VoltAgent tool context/hooks for tool execution context, and VoltAgent observability/OpenTelemetry configuration for telemetry.
154+
Top-level AI SDK `toolsContext` is passed through for native AI SDK tools that declare `contextSchema`. Top-level AI SDK `runtimeContext`, `telemetry`, and `experimental_telemetry` are intentionally not exposed. Use `voltagent.context` for per-call application context and VoltAgent observability/OpenTelemetry configuration for telemetry.
155155

156156
The same shape works for streaming:
157157

@@ -178,10 +178,11 @@ for await (const part of result.stream) {
178178

179179
### Tool usage
180180

181-
VoltAgent now accepts AI SDK-style tool sets directly. The recommended custom tool API is `tool()` from `@voltagent/core`, extended with VoltAgent-specific metadata under the `voltagent` namespace.
181+
VoltAgent now accepts AI SDK-style tool sets directly. Raw AI SDK tools are first-class, so the recommended custom tool API is `tool()` from `ai`. Add VoltAgent-specific metadata with `withVoltAgentMetadata` when you need hooks, tags, API metadata, or display metadata.
182182

183183
```ts
184-
import { Agent, tool } from "@voltagent/core";
184+
import { tool } from "ai";
185+
import { Agent } from "@voltagent/core";
185186
import { openai } from "@ai-sdk/openai";
186187
import { z } from "zod";
187188

@@ -202,25 +203,77 @@ const agent = new Agent({
202203
});
203204
```
204205

205-
VoltAgent-specific tool metadata is available through the `voltagent` namespace, so existing framework features can be used with AI SDK-style tools:
206+
Native AI SDK tool options such as `contextSchema` work unchanged with call-level `toolsContext`:
206207

207208
```ts
208-
const refundCustomer = tool({
209-
description: "Refund a customer order",
210-
inputSchema: z.object({
211-
orderId: z.string(),
212-
reason: z.string(),
209+
const weather = tool({
210+
description: "Get weather for a city",
211+
inputSchema: z.object({ city: z.string() }),
212+
contextSchema: z.object({
213+
apiKey: z.string(),
214+
defaultUnit: z.enum(["celsius", "fahrenheit"]),
213215
}),
214-
execute: async ({ orderId, reason }) => {
215-
return issueRefund(orderId, reason);
216+
execute: async ({ city }, { context }) => {
217+
return fetchWeather(city, {
218+
apiKey: context.apiKey,
219+
unit: context.defaultUnit,
220+
});
216221
},
217-
voltagent: {
218-
name: "Refund Customer",
219-
purpose: "Issue customer refunds",
222+
});
223+
224+
await agent.generateText({
225+
prompt: "What is the weather in San Francisco?",
226+
tools: { weather },
227+
toolsContext: {
228+
weather: {
229+
apiKey: process.env.WEATHER_API_KEY!,
230+
defaultUnit: "fahrenheit",
231+
},
220232
},
221233
});
222234
```
223235

236+
VoltAgent-specific tool metadata is optional and stored out-of-band so it is not sent to the model provider:
237+
238+
```ts
239+
import { tool } from "ai";
240+
import { withVoltAgentMetadata } from "@voltagent/core";
241+
242+
const refundCustomer = withVoltAgentMetadata(
243+
tool({
244+
description: "Refund a customer order",
245+
inputSchema: z.object({
246+
orderId: z.string(),
247+
reason: z.string(),
248+
}),
249+
contextSchema: z.object({
250+
actorId: z.string(),
251+
permissions: z.array(z.string()),
252+
}),
253+
execute: async ({ orderId, reason }, { context }) => {
254+
if (!context.permissions.includes("refund:write")) {
255+
throw new Error("Not allowed to refund orders");
256+
}
257+
258+
return issueRefund({
259+
orderId,
260+
reason,
261+
actorId: context.actorId,
262+
});
263+
},
264+
}),
265+
{
266+
name: "Refund Customer",
267+
purpose: "Issue customer refunds",
268+
tags: ["billing", "dangerous", "customer-support"],
269+
metadata: {
270+
owner: "payments-team",
271+
riskLevel: "high",
272+
},
273+
}
274+
);
275+
```
276+
224277
Register the tool under the canonical name the model should call:
225278

226279
```ts
@@ -233,11 +286,9 @@ const agent = new Agent({
233286
});
234287
```
235288

236-
The ToolSet key remains the canonical `tool.name` used for tool calls, approval, routing, and telemetry correlation. `voltagent.name` is display metadata and is emitted as `tool.display_name` for Console and observability consumers.
237-
238-
VoltAgent `tool()` intentionally does not expose AI SDK `contextSchema`, `runtimeContext`, `toolsContext`, `telemetry`, or `experimental_telemetry`. Use `voltagent.context` on the agent call for per-request application context, `voltagent` tool hooks for VoltAgent lifecycle context, and VoltAgent observability/OpenTelemetry configuration for telemetry.
289+
The ToolSet key remains the canonical `tool.name` used for tool calls, approval, routing, and telemetry correlation. VoltAgent metadata `name` is display metadata and is emitted as `tool.display_name` for Console and observability consumers.
239290

240-
`createTool` is now a legacy compatibility helper for existing class-style tools. New code should use `tool()`.
291+
`tool()` from `@voltagent/core` remains available as a convenience wrapper when inline `voltagent` metadata is preferred. `createTool` is now a legacy compatibility helper for existing class-style tools.
241292

242293
### Tool approval
243294

packages/core/src/agent/agent.spec-d.ts

Lines changed: 71 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
import type { ModelMessage } from "@ai-sdk/provider-utils";
2-
import { type FinishReason, type LanguageModelUsage, Output, type UIMessage } from "ai";
2+
import {
3+
type FinishReason,
4+
type LanguageModelUsage,
5+
Output,
6+
type UIMessage,
7+
tool as rawAiTool,
8+
} from "ai";
39
import { MockLanguageModelV3 } from "ai/test";
410
import { describe, expectTypeOf, it } from "vitest";
511
import { z } from "zod";
612
import type { ModelRouterModelId } from "../registries/model-provider-types.generated";
713
import type { BaseRetriever } from "../retriever/retriever";
8-
import { Tool, type Toolkit, tool as aiSdkTool, createTool } from "../tool";
14+
import { Tool, type Toolkit, tool as aiSdkTool, createTool, withVoltAgentMetadata } from "../tool";
915
import type { StreamEventType } from "../utils/streams";
1016
import type { Voice } from "../voice";
1117
import type { VoltOpsClient } from "../voltops/client";
@@ -238,46 +244,69 @@ describe("Agent Type System", () => {
238244
expectTypeOf(agent).toMatchTypeOf<Agent>();
239245
});
240246

241-
it("should reject VoltAgent-owned context and telemetry fields on AI SDK-style tools", () => {
242-
aiSdkTool({
243-
description: "Test",
244-
inputSchema: z.object({ value: z.string() }),
245-
// @ts-expect-error - use `voltagent.context` and VoltAgent tool hooks instead
246-
contextSchema: z.object({ tenantId: z.string() }),
247-
execute: async ({ value }) => ({ result: value }),
247+
it("should accept AI SDK contextSchema on AI SDK-style tools", () => {
248+
const weatherTool = aiSdkTool({
249+
description: "Get weather",
250+
inputSchema: z.object({ city: z.string() }),
251+
contextSchema: z.object({
252+
apiKey: z.string(),
253+
defaultUnit: z.enum(["celsius", "fahrenheit"]),
254+
}),
255+
execute: async ({ city }, { context }) => {
256+
expectTypeOf(context.apiKey).toEqualTypeOf<string>();
257+
expectTypeOf(context.defaultUnit).toEqualTypeOf<"celsius" | "fahrenheit">();
258+
return { city, unit: context.defaultUnit };
259+
},
248260
});
249261

250-
aiSdkTool({
251-
description: "Test",
252-
inputSchema: z.object({ value: z.string() }),
253-
// @ts-expect-error - runtime context is owned by VoltAgent agent calls
254-
runtimeContext: { tenantId: "tenant-1" },
255-
execute: async ({ value }) => ({ result: value }),
262+
const agent = new Agent({
263+
name: "Weather",
264+
instructions: "Test",
265+
model: mockModel,
266+
tools: {
267+
weather: weatherTool,
268+
},
256269
});
257270

258-
aiSdkTool({
259-
description: "Test",
260-
inputSchema: z.object({ value: z.string() }),
261-
// @ts-expect-error - tools context is owned by VoltAgent
262-
toolsContext: {},
263-
execute: async ({ value }) => ({ result: value }),
264-
});
271+
expectTypeOf(agent).toMatchTypeOf<Agent>();
272+
});
265273

266-
aiSdkTool({
267-
description: "Test",
268-
inputSchema: z.object({ value: z.string() }),
269-
// @ts-expect-error - use VoltAgent observability/OpenTelemetry instead
270-
telemetry: { isEnabled: false },
271-
execute: async ({ value }) => ({ result: value }),
272-
});
274+
it("should attach VoltAgent metadata to raw AI SDK tools", () => {
275+
const refundCustomer = withVoltAgentMetadata(
276+
rawAiTool({
277+
description: "Refund a customer order",
278+
inputSchema: z.object({
279+
orderId: z.string(),
280+
reason: z.string(),
281+
}),
282+
contextSchema: z.object({
283+
actorId: z.string(),
284+
permissions: z.array(z.string()),
285+
}),
286+
execute: async ({ orderId, reason }, { context }) => {
287+
expectTypeOf(context.actorId).toEqualTypeOf<string>();
288+
expectTypeOf(context.permissions).toEqualTypeOf<string[]>();
289+
return { orderId, reason, actorId: context.actorId };
290+
},
291+
}),
292+
{
293+
tags: ["billing", "dangerous"],
294+
metadata: {
295+
owner: "payments-team",
296+
},
297+
},
298+
);
273299

274-
aiSdkTool({
275-
description: "Test",
276-
inputSchema: z.object({ value: z.string() }),
277-
// @ts-expect-error - use VoltAgent observability/OpenTelemetry instead
278-
experimental_telemetry: { isEnabled: false },
279-
execute: async ({ value }) => ({ result: value }),
300+
const agent = new Agent({
301+
name: "Support",
302+
instructions: "Test",
303+
model: mockModel,
304+
tools: {
305+
refundCustomer,
306+
},
280307
});
308+
309+
expectTypeOf(agent).toMatchTypeOf<Agent>();
281310
});
282311
});
283312

@@ -616,6 +645,9 @@ describe("Agent Type System", () => {
616645
responseBody: true,
617646
},
618647
experimental_download: async (downloads) => downloads.map(() => null),
648+
toolsContext: {
649+
lookup_order: {},
650+
},
619651
onStart: async () => {},
620652
onStepStart: async () => {},
621653
onLanguageModelCallStart: async () => {},
@@ -633,7 +665,7 @@ describe("Agent Type System", () => {
633665
expectTypeOf(result).toMatchTypeOf<GenerateTextResultWithContext>();
634666
});
635667

636-
it("should reject VoltAgent-owned AI SDK context and telemetry fields", () => {
668+
it("should accept toolsContext and reject VoltAgent-owned runtime/telemetry fields", () => {
637669
const runtimeContextOptions: GenerateTextOptions = {
638670
// @ts-expect-error - use `voltagent.context` instead
639671
runtimeContext: {
@@ -642,8 +674,9 @@ describe("Agent Type System", () => {
642674
};
643675

644676
const toolsContextOptions: GenerateTextOptions = {
645-
// @ts-expect-error - use VoltAgent tool context APIs instead
646-
toolsContext: {},
677+
toolsContext: {
678+
lookup_order: {},
679+
},
647680
};
648681

649682
const telemetryOptions: GenerateTextOptions = {

packages/core/src/agent/agent.spec.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1608,7 +1608,7 @@ Use pandas and summarize findings.`.split("\n"),
16081608
expect(startContext.conversationId).toBe("runtime-conv");
16091609
});
16101610

1611-
it("should pass AI SDK v7 generateText options, strip VoltAgent-owned fields, and compose managed callbacks", async () => {
1611+
it("should pass AI SDK v7 generateText options, strip VoltAgent-owned telemetry/runtime fields, and compose managed callbacks", async () => {
16121612
const hookOnStepEnd = vi.fn();
16131613
const userOnStepEnd = vi.fn();
16141614
const userOnEnd = vi.fn();
@@ -1691,7 +1691,11 @@ Use pandas and summarize findings.`.split("\n"),
16911691
expect(callArgs.telemetry).toBeUndefined();
16921692
expect(callArgs.experimental_telemetry).toBeUndefined();
16931693
expect(callArgs.runtimeContext).toBeUndefined();
1694-
expect(callArgs.toolsContext).toBeUndefined();
1694+
expect(callArgs.toolsContext).toEqual({
1695+
lookup_order: {
1696+
requestId: "tool-request-1",
1697+
},
1698+
});
16951699
expect(callArgs.include).toEqual({
16961700
requestBody: true,
16971701
requestMessages: true,

0 commit comments

Comments
 (0)