Skip to content

Commit 2712078

Browse files
authored
feat: enable andAgent tool usage by switching to generateText with Output.object (#927)
* feat: enable `andAgent` tool usage by switching to `generateText` with `Output.object` * fix: code reviews
1 parent 2e0c3b7 commit 2712078

File tree

5 files changed

+70
-29
lines changed

5 files changed

+70
-29
lines changed

.changeset/crazy-eagles-float.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
"@voltagent/core": patch
3+
---
4+
5+
feat: enable `andAgent` tool usage by switching to `generateText` with `Output.object` while keeping structured output
6+
7+
Example:
8+
9+
```ts
10+
import { Agent, createTool, createWorkflowChain } from "@voltagent/core";
11+
import { z } from "zod";
12+
import { openai } from "@ai-sdk/openai";
13+
14+
const getWeather = createTool({
15+
name: "get_weather",
16+
description: "Get weather for a city",
17+
parameters: z.object({ city: z.string() }),
18+
execute: async ({ city }) => ({ city, temp: 72, condition: "sunny" }),
19+
});
20+
21+
const agent = new Agent({
22+
name: "WeatherAgent",
23+
model: openai("gpt-4o-mini"),
24+
tools: [getWeather],
25+
});
26+
27+
const workflow = createWorkflowChain({
28+
id: "weather-flow",
29+
input: z.object({ city: z.string() }),
30+
}).andAgent(({ data }) => `What is the weather in ${data.city}?`, agent, {
31+
schema: z.object({ temp: z.number(), condition: z.string() }),
32+
});
33+
```

packages/core/src/workflow/chain.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,8 @@ export class WorkflowChain<
141141
* ```
142142
*
143143
* @param task - The task (prompt) to execute for the agent, can be a string or a function that returns a string
144-
* @param agent - The agent to execute the task using `generateObject`
145-
* @param config - The config for the agent (schema) `generateObject` call
144+
* @param agent - The agent to execute the task using `generateText`
145+
* @param config - The config for the agent (schema) `generateText` call
146146
* @returns A workflow step that executes the agent with the task
147147
*/
148148
andAgent<SCHEMA extends z.ZodTypeAny>(
@@ -414,11 +414,11 @@ export class WorkflowChain<
414414
* id: "process-pending",
415415
* condition: async ({ data }) => data.status === "pending",
416416
* execute: async ({ data }) => {
417-
* const result = await agent.generateObject(
417+
* const result = await agent.generateText(
418418
* `Process pending request for ${data.userId}`,
419-
* z.object({ processed: z.boolean() })
419+
* { output: Output.object({ schema: z.object({ processed: z.boolean() }) }) }
420420
* );
421-
* return { ...data, ...result.object };
421+
* return { ...data, ...result.output };
422422
* }
423423
* });
424424
* ```
@@ -588,11 +588,11 @@ export class WorkflowChain<
588588
* {
589589
* id: "generate-recommendations",
590590
* execute: async ({ data }) => {
591-
* const result = await agent.generateObject(
591+
* const result = await agent.generateText(
592592
* `Generate recommendations for user ${data.userId}`,
593-
* z.object({ recommendations: z.array(z.string()) })
593+
* { output: Output.object({ schema: z.object({ recommendations: z.array(z.string()) }) }) }
594594
* );
595-
* return result.object;
595+
* return result.output;
596596
* }
597597
* }
598598
* ]
@@ -662,11 +662,15 @@ export class WorkflowChain<
662662
* {
663663
* id: "ai-fallback",
664664
* execute: async ({ data }) => {
665-
* const result = await agent.generateObject(
665+
* const result = await agent.generateText(
666666
* `Generate fallback response for: ${data.query}`,
667-
* z.object({ source: z.literal("ai"), result: z.string() })
667+
* {
668+
* output: Output.object({
669+
* schema: z.object({ source: z.literal("ai"), result: z.string() }),
670+
* }),
671+
* }
668672
* );
669-
* return result.object;
673+
* return result.output;
670674
* }
671675
* }
672676
* ]

packages/core/src/workflow/steps/and-agent.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { ModelMessage } from "@ai-sdk/provider-utils";
2-
import type { UIMessage } from "ai";
2+
import { Output, type UIMessage } from "ai";
33
import type { z } from "zod";
44
import type { Agent, BaseGenerationOptions } from "../../agent/agent";
55
import { convertUsage } from "../../utils/usage-converter";
@@ -33,8 +33,8 @@ export type AgentConfig<SCHEMA extends z.ZodTypeAny, INPUT, DATA> = BaseGenerati
3333
* ```
3434
*
3535
* @param task - The task (prompt) to execute for the agent, can be a string or a function that returns a string
36-
* @param agent - The agent to execute the task using `generateObject`
37-
* @param config - The config for the agent (schema) `generateObject` call
36+
* @param agent - The agent to execute the task using `generateText`
37+
* @param config - The config for the agent (schema) `generateText` call
3838
* @returns A workflow step that executes the agent with the task
3939
*/
4040
export function andAgent<INPUT, DATA, SCHEMA extends z.ZodTypeAny>(
@@ -58,15 +58,18 @@ export function andAgent<INPUT, DATA, SCHEMA extends z.ZodTypeAny>(
5858
const finalTask = typeof task === "function" ? await task(context) : task;
5959
const finalSchema = typeof schema === "function" ? await schema(context) : schema;
6060

61+
const output = Output.object({ schema: finalSchema });
62+
6163
// Create step context and publish start event
6264
if (!state.workflowContext) {
6365
// No workflow context, execute without events
64-
const result = await agent.generateObject(finalTask, finalSchema, {
66+
const result = await agent.generateText(finalTask, {
6567
...restConfig,
6668
context: restConfig.context ?? state.context,
6769
conversationId: restConfig.conversationId ?? state.conversationId,
6870
userId: restConfig.userId ?? state.userId,
6971
// No parentSpan when there's no workflow context
72+
output,
7073
});
7174
// Accumulate usage if available (no workflow context)
7275
if (result.usage && state.usage) {
@@ -81,19 +84,20 @@ export function andAgent<INPUT, DATA, SCHEMA extends z.ZodTypeAny>(
8184
}
8285
state.usage.totalTokens += convertedUsage?.totalTokens || 0;
8386
}
84-
return result.object;
87+
return result.output as z.infer<SCHEMA>;
8588
}
8689

8790
// Step start event removed - now handled by OpenTelemetry spans
8891

8992
try {
90-
const result = await agent.generateObject(finalTask, finalSchema, {
93+
const result = await agent.generateText(finalTask, {
9194
...restConfig,
9295
context: restConfig.context ?? state.context,
9396
conversationId: restConfig.conversationId ?? state.conversationId,
9497
userId: restConfig.userId ?? state.userId,
9598
// Pass the current step span as parent for proper span hierarchy
9699
parentSpan: state.workflowContext?.currentStepSpan,
100+
output,
97101
});
98102

99103
// Step success event removed - now handled by OpenTelemetry spans
@@ -112,7 +116,7 @@ export function andAgent<INPUT, DATA, SCHEMA extends z.ZodTypeAny>(
112116
state.usage.totalTokens += convertedUsage?.totalTokens || 0;
113117
}
114118

115-
return result.object;
119+
return result.output as z.infer<SCHEMA>;
116120
} catch (error) {
117121
// Check if this is a suspension, not an error
118122
if (

website/docs/workflows/steps/and-agent.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,13 @@ const result = await workflow.run({ text: "I love this!" });
4444
)
4545
```
4646

47-
**Important:** `andAgent` uses `generateObject` under the hood, which means:
47+
**Important:** `andAgent` uses `generateText` with `Output.object` under the hood, which means:
4848

4949
- ✅ You get **structured, typed responses** based on your schema
50-
- The agent **cannot use tools** during this step
50+
- The agent **can use tools** during this step
5151
-**Streaming is not supported** (response returns when complete)
5252

53-
**Need tools or streaming?** Use [andThen](./and-then.md) to call the agent directly with `streamText` or `generateText`.
53+
**Need streaming or custom tool handling?** Use [andThen](./and-then.md) to call the agent directly with `streamText` or `generateText`.
5454

5555
## Function Signature
5656

@@ -177,9 +177,9 @@ createWorkflowChain({ id: "smart-email" })
177177
});
178178
```
179179

180-
## Using Tools or Streaming
180+
## Streaming or Custom Tool Handling
181181

182-
If you need the agent to use tools or stream responses, use `andThen` instead:
182+
`andAgent` supports tools, but it only returns the structured output when the step completes. Use `andThen` when you need streaming tokens or to inspect tool calls/results directly:
183183

184184
```typescript
185185
import { Agent, createTool } from "@voltagent/core";
@@ -201,11 +201,11 @@ const agent = new Agent({
201201
tools: [getWeatherTool],
202202
});
203203

204-
// Use andThen to call agent directly with tools
204+
// Use andThen to call the agent directly when you need streaming or tool call inspection
205205
createWorkflowChain({ id: "weather-flow" }).andThen({
206206
id: "get-weather",
207207
execute: async ({ data }) => {
208-
// Call streamText/generateText directly for tool support
208+
// Call streamText/generateText directly for streaming or tool call handling
209209
const result = await agent.generateText(`What's the weather in ${data.city}?`);
210210
return { response: result.text };
211211
},
@@ -218,7 +218,7 @@ createWorkflowChain({ id: "weather-flow" }).andThen({
218218
2. **Use enums for categories** - `z.enum()` ensures valid options
219219
3. **Add descriptions to schema fields** - Helps AI understand what you want
220220
4. **Handle edge cases** - Check for missing or low-confidence results
221-
5. **Need tools?** - Use `andThen` with direct agent calls instead of `andAgent`
221+
5. **Need streaming or tool inspection?** - Use `andThen` with direct agent calls instead of `andAgent`
222222

223223
## Next Steps
224224

website/docs/workflows/steps/and-then.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ createWorkflowChain({
131131

132132
### Agent with Tools
133133

134-
When you need tool support or streaming (not available in `andAgent`), call the agent directly:
134+
When you need streaming or custom tool handling, call the agent directly:
135135

136136
```typescript
137137
import { Agent, createTool } from "@voltagent/core";
@@ -172,8 +172,8 @@ const agent = new Agent({
172172

173173
**Why use `andThen` instead of `andAgent`?**
174174

175-
- `andAgent` uses `generateObject` (structured output only, no tools)
176-
- `andThen` with direct agent calls supports `streamText`/`generateText` (tools + streaming)
175+
- `andAgent` uses `generateText` with `Output.object` (structured output, no streaming)
176+
- `andThen` with direct agent calls supports `streamText`/`generateText` (streaming + direct tool access)
177177

178178
## Suspend & Resume Support
179179

0 commit comments

Comments
 (0)