Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .changeset/crazy-eagles-float.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
"@voltagent/core": patch
---

feat: enable `andAgent` tool usage by switching to `generateText` with `Output.object` while keeping structured output

Example:

```ts
import { Agent, createTool, createWorkflowChain } from "@voltagent/core";
import { z } from "zod";
import { openai } from "@ai-sdk/openai";

const getWeather = createTool({
name: "get_weather",
description: "Get weather for a city",
parameters: z.object({ city: z.string() }),
execute: async ({ city }) => ({ city, temp: 72, condition: "sunny" }),
});

const agent = new Agent({
name: "WeatherAgent",
model: openai("gpt-4o-mini"),
tools: [getWeather],
});

const workflow = createWorkflowChain({
id: "weather-flow",
input: z.object({ city: z.string() }),
}).andAgent(({ data }) => `What is the weather in ${data.city}?`, agent, {
schema: z.object({ temp: z.number(), condition: z.string() }),
});
```
26 changes: 15 additions & 11 deletions packages/core/src/workflow/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,8 @@ export class WorkflowChain<
* ```
*
* @param task - The task (prompt) to execute for the agent, can be a string or a function that returns a string
* @param agent - The agent to execute the task using `generateObject`
* @param config - The config for the agent (schema) `generateObject` call
* @param agent - The agent to execute the task using `generateText`
* @param config - The config for the agent (schema) `generateText` call
* @returns A workflow step that executes the agent with the task
*/
andAgent<SCHEMA extends z.ZodTypeAny>(
Expand Down Expand Up @@ -414,11 +414,11 @@ export class WorkflowChain<
* id: "process-pending",
* condition: async ({ data }) => data.status === "pending",
* execute: async ({ data }) => {
* const result = await agent.generateObject(
* const result = await agent.generateText(
* `Process pending request for ${data.userId}`,
* z.object({ processed: z.boolean() })
* { output: Output.object({ schema: z.object({ processed: z.boolean() }) }) }
* );
* return { ...data, ...result.object };
* return { ...data, ...result.output };
* }
Comment thread
omeraplak marked this conversation as resolved.
* });
* ```
Expand Down Expand Up @@ -588,11 +588,11 @@ export class WorkflowChain<
* {
* id: "generate-recommendations",
* execute: async ({ data }) => {
* const result = await agent.generateObject(
* const result = await agent.generateText(
* `Generate recommendations for user ${data.userId}`,
* z.object({ recommendations: z.array(z.string()) })
* { output: Output.object({ schema: z.object({ recommendations: z.array(z.string()) }) }) }
* );
* return result.object;
* return result.output;
* }
* }
* ]
Expand Down Expand Up @@ -662,11 +662,15 @@ export class WorkflowChain<
* {
* id: "ai-fallback",
* execute: async ({ data }) => {
* const result = await agent.generateObject(
* const result = await agent.generateText(
* `Generate fallback response for: ${data.query}`,
* z.object({ source: z.literal("ai"), result: z.string() })
* {
* output: Output.object({
* schema: z.object({ source: z.literal("ai"), result: z.string() }),
* }),
* }
* );
* return result.object;
* return result.output;
* }
* }
* ]
Expand Down
18 changes: 11 additions & 7 deletions packages/core/src/workflow/steps/and-agent.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ModelMessage } from "@ai-sdk/provider-utils";
import type { UIMessage } from "ai";
import { Output, type UIMessage } from "ai";
import type { z } from "zod";
import type { Agent, BaseGenerationOptions } from "../../agent/agent";
import { convertUsage } from "../../utils/usage-converter";
Expand Down Expand Up @@ -33,8 +33,8 @@ export type AgentConfig<SCHEMA extends z.ZodTypeAny, INPUT, DATA> = BaseGenerati
* ```
*
* @param task - The task (prompt) to execute for the agent, can be a string or a function that returns a string
* @param agent - The agent to execute the task using `generateObject`
* @param config - The config for the agent (schema) `generateObject` call
* @param agent - The agent to execute the task using `generateText`
* @param config - The config for the agent (schema) `generateText` call
* @returns A workflow step that executes the agent with the task
*/
export function andAgent<INPUT, DATA, SCHEMA extends z.ZodTypeAny>(
Expand All @@ -58,15 +58,18 @@ export function andAgent<INPUT, DATA, SCHEMA extends z.ZodTypeAny>(
const finalTask = typeof task === "function" ? await task(context) : task;
const finalSchema = typeof schema === "function" ? await schema(context) : schema;

const output = Output.object({ schema: finalSchema });

// Create step context and publish start event
if (!state.workflowContext) {
// No workflow context, execute without events
const result = await agent.generateObject(finalTask, finalSchema, {
const result = await agent.generateText(finalTask, {
...restConfig,
context: restConfig.context ?? state.context,
conversationId: restConfig.conversationId ?? state.conversationId,
userId: restConfig.userId ?? state.userId,
// No parentSpan when there's no workflow context
output,
});
// Accumulate usage if available (no workflow context)
if (result.usage && state.usage) {
Expand All @@ -81,19 +84,20 @@ export function andAgent<INPUT, DATA, SCHEMA extends z.ZodTypeAny>(
}
state.usage.totalTokens += convertedUsage?.totalTokens || 0;
}
return result.object;
return result.output as z.infer<SCHEMA>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add error handling for structured output generation failures and validate result.output.

The return statements cast result.output to z.infer<SCHEMA> without any validation. Consider:

  1. Add try-catch blocks around generateText calls to handle potential failures when generating structured output.
  2. Add null/undefined checks for result.output before using it, or document the assumptions about the ai library's guarantees.
  3. The type assertion (as z.infer<SCHEMA>) assumes the model returned valid structured output matching the schema—consider adding runtime validation if the schema is complex.

Also applies to: 119-119

🤖 Prompt for AI Agents
In @packages/core/src/workflow/steps/and-agent.ts at line 87, Wrap calls to
generateText in try-catch (in the function containing the current return of
result.output and the similar location near the other return) to surface and
log/throw generation errors; check result.output for null/undefined before
casting and handle the absence (throw a descriptive error or fallback); and
perform runtime validation with the Zod schema (use SCHEMA.parse or
SCHEMA.safeParse) to ensure result.output matches z.infer<SCHEMA> and throw a
clear error when validation fails instead of using a blind type assertion.

}

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

try {
const result = await agent.generateObject(finalTask, finalSchema, {
const result = await agent.generateText(finalTask, {
...restConfig,
context: restConfig.context ?? state.context,
conversationId: restConfig.conversationId ?? state.conversationId,
userId: restConfig.userId ?? state.userId,
// Pass the current step span as parent for proper span hierarchy
parentSpan: state.workflowContext?.currentStepSpan,
output,
});

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

return result.object;
return result.output as z.infer<SCHEMA>;
} catch (error) {
// Check if this is a suspension, not an error
if (
Expand Down
16 changes: 8 additions & 8 deletions website/docs/workflows/steps/and-agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,13 @@ const result = await workflow.run({ text: "I love this!" });
)
```

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

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

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

## Function Signature

Expand Down Expand Up @@ -177,9 +177,9 @@ createWorkflowChain({ id: "smart-email" })
});
```

## Using Tools or Streaming
## Streaming or Custom Tool Handling

If you need the agent to use tools or stream responses, use `andThen` instead:
`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:

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

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

## Next Steps

Expand Down
6 changes: 3 additions & 3 deletions website/docs/workflows/steps/and-then.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ createWorkflowChain({

### Agent with Tools

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

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

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

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

## Suspend & Resume Support

Expand Down