Sub-agents
Sub-agents are agents that work under a supervisor agent to handle specific tasks. This architecture allows you to create agent workflows where each sub-agent focuses on a specific domain, coordinated by a supervisor.
Why Use Sub-agents?
- Task delegation: Assign specific tasks to agents configured for particular domains (e.g., coding, translation, data analysis)
- Workflow orchestration: Build multi-step workflows by delegating tasks to appropriate agents
- Code organization: Break down complex problems into smaller components
- Modularity: Add or swap agents without disrupting the entire system
Creating and Using Sub-agents
Creating Individual Agents
Create the agents that will serve as sub-agents:
import { Agent } from "@voltagent/core";
import { openai } from "@ai-sdk/openai";
// Create an agent for content creation
const contentCreatorAgent = new Agent({
name: "ContentCreator",
instructions: "Creates short text content on requested topics",
model: openai("gpt-4o-mini"),
});
// Create an agent for formatting
const formatterAgent = new Agent({
name: "Formatter",
instructions: "Formats and styles text content",
model: openai("gpt-4o-mini"),
});
Creating a Supervisor Agent
Pass the agents in the subAgents
array during supervisor initialization:
import { Agent } from "@voltagent/core";
import { openai } from "@ai-sdk/openai";
const supervisorAgent = new Agent({
name: "Supervisor",
instructions: "Coordinates between content creation and formatting agents",
model: openai("gpt-4o-mini"),
subAgents: [contentCreatorAgent, formatterAgent],
});
By default, sub-agents use the streamText
method. You can specify different methods like generateText
, generateObject
, or streamObject
with custom schemas and options.
Customizing Supervisor Behavior
Supervisor agents use an automatically generated system message that includes guidelines for managing sub-agents. Customize this behavior using the supervisorConfig
option.
See the generateSupervisorSystemMessage implementation on GitHub.
The supervisorConfig
option is only available when subAgents
are provided. TypeScript will prevent you from using supervisorConfig
on agents without sub-agents.
Basic Supervisor Configuration
import { Agent } from "@voltagent/core";
import { openai } from "@ai-sdk/openai";
const supervisorAgent = new Agent({
name: "Content Supervisor",
instructions: "Coordinate content creation workflow",
model: openai("gpt-4o-mini"),
subAgents: [writerAgent, editorAgent],
supervisorConfig: {
// Add custom guidelines to the default ones
customGuidelines: [
"Always thank the user at the end",
"Keep responses concise and actionable",
"Prioritize user experience",
],
// Control whether to include previous agent interactions
includeAgentsMemory: true, // default: true
},
});
Stream Event Forwarding Configuration
Control which events from sub-agents are forwarded to the parent stream. By default, only tool-call
and tool-result
events are forwarded.
const supervisorAgent = new Agent({
name: "Content Supervisor",
instructions: "Coordinate content creation workflow",
model: openai("gpt-4o-mini"),
subAgents: [writerAgent, editorAgent],
supervisorConfig: {
// Configure which sub-agent events to forward
fullStreamEventForwarding: {
// Default: ['tool-call', 'tool-result']
types: ["tool-call", "tool-result", "text-delta", "reasoning", "source", "error", "finish"],
},
},
});
Common Configurations:
// Minimal - Only tool events (default)
fullStreamEventForwarding: {
types: ['tool-call', 'tool-result'],
}
// Text + Tools - Include text generation
fullStreamEventForwarding: {
types: ['tool-call', 'tool-result', 'text-delta'],
}
// Full visibility - All events including reasoning
fullStreamEventForwarding: {
types: ['tool-call', 'tool-result', 'text-delta', 'reasoning', 'source', 'error', 'finish'],
}
// Clean tool names - No agent prefix (add prefix manually when consuming events if desired)
fullStreamEventForwarding: {
types: ['tool-call', 'tool-result'],
}
This configuration balances stream performance and information detail for sub-agent interactions.
Error Handling Configuration
Control how the supervisor handles sub-agent failures.
const supervisorAgent = new Agent({
name: "Supervisor",
instructions: "Coordinate between agents",
model: openai("gpt-4o-mini"),
subAgents: [dataProcessor, analyzer],
supervisorConfig: {
// Control whether stream errors throw exceptions
throwOnStreamError: false, // default: false
// Control whether error messages appear in empty responses
includeErrorInEmptyResponse: true, // default: true
},
});
Configuration Options
throwOnStreamError
(boolean, default: false
)
- When
false
: Stream errors are caught and returned as error results withstatus: "error"
- When
true
: Stream errors throw exceptions that must be caught with try/catch - Set to
true
to handle errors at a higher level or trigger retry logic
includeErrorInEmptyResponse
(boolean, default: true
)
- When
true
: Error messages are included in the response when no content was generated - When
false
: Returns empty string in result, but still marks status as "error" - Set to
false
to handle error messaging yourself
Common Error Handling Patterns
Default - Graceful Error Handling:
// Errors are returned as results with helpful messages
supervisorConfig: {
throwOnStreamError: false,
includeErrorInEmptyResponse: true,
}
// Usage:
const result = await supervisor.streamText("Process data");
// If sub-agent fails:
// result contains error message like "Error in DataProcessor: Stream failed"
VoltAgent uses the AI SDK's native retry mechanism (default: 3 attempts). Setting throwOnStreamError: true
is useful for custom error handling or logging at a higher level, not for implementing retry logic.
Silent Errors - Custom Messaging:
// Errors don't include automatic messages
supervisorConfig: {
includeErrorInEmptyResponse: false,
}
// Usage with custom error handling:
const result = await supervisor.streamText("Process data");
for await (const event of result.fullStream) {
if (event.type === "error") {
// Provide custom user-friendly error message
console.log("We're having trouble processing your request. Please try again.");
}
}
Production Setup - Error Tracking:
supervisorConfig: {
throwOnStreamError: false, // Don't crash the app
includeErrorInEmptyResponse: true, // Help with debugging
// Capture error events for monitoring
fullStreamEventForwarding: {
types: ['tool-call', 'tool-result', 'error'],
},
}
Error Handling in Practice
The supervisor's behavior when a sub-agent encounters an error depends on your configuration:
// Example: Sub-agent fails during stream
const supervisor = new Agent({
name: "Supervisor",
subAgents: [unreliableAgent],
supervisorConfig: {
throwOnStreamError: false,
includeErrorInEmptyResponse: true,
},
});
// The supervisor handles the failure
const response = await supervisor.streamText("Do something risky");
// Check the response
if (response.status === "error") {
console.log("Sub-agent failed:", response.error);
// response.result contains: "Error in UnreliableAgent: [error details]"
} else {
// Process successful response
}
Using with fullStream
When using fullStream
, the configuration controls what you receive from sub-agents:
// Stream with full event details
const result = await supervisorAgent.streamText("Create and edit content", {
fullStream: true,
});
// Process different event types
for await (const event of result.fullStream) {
switch (event.type) {
case "tool-call":
console.log(
`${event.subAgentName ? `[${event.subAgentName}] ` : ""}Tool called: ${event.data.toolName}`
);
break;
case "tool-result":
console.log(
`${event.subAgentName ? `[${event.subAgentName}] ` : ""}Tool result: ${event.data.result}`
);
break;
case "text-delta":
// Only appears if included in types array
console.log(`Text: ${event.data}`);
break;
case "reasoning":
// Only appears if included in types array
console.log(`Reasoning: ${event.data}`);
break;
}
}
Filtering Sub-agent Events
Identify which events come from sub-agents by checking for subAgentId
and subAgentName
properties:
const result = await supervisorAgent.streamText("Create and edit content", {
fullStream: true,
});
for await (const event of result.fullStream) {
// Check if this event is from a sub-agent
if (event.subAgentId && event.subAgentName) {
console.log(`Event from sub-agent ${event.subAgentName}:`);
console.log(` Type: ${event.type}`);
console.log(` Data:`, event.data);
// Filter by specific sub-agent
if (event.subAgentName === "WriterAgent") {
// Handle writer agent events specifically
}
} else {
// This is from the supervisor agent itself
console.log(`Supervisor event: ${event.type}`);
}
}
This allows you to:
- Distinguish between supervisor and sub-agent events
- Filter events by specific sub-agent
- Apply different handling logic based on the event source
Complete System Message Override
Provide a custom systemMessage
to replace the default template:
const supervisorAgent = new Agent({
name: "Custom Supervisor",
instructions: "This will be ignored when systemMessage is provided",
model: openai("gpt-4o-mini"),
subAgents: [writerAgent, editorAgent],
supervisorConfig: {
systemMessage: `
You are a content manager named "ContentBot".
Your team:
- Writer: Creates original content
- Editor: Reviews and improves content
Your workflow:
1. Analyze user requests
2. Use delegate_task to assign work to appropriate specialists
3. Coordinate between specialists as needed
4. Provide final responses
5. Maintain a professional tone
Remember: Use the delegate_task tool to assign tasks to your specialists.
`.trim(),
// Control memory inclusion even with custom system message
includeAgentsMemory: true,
},
});
Quick Usage
Add custom rules:
supervisorConfig: {
customGuidelines: ["Verify sources", "Include confidence levels"];
}
Override entire system message:
supervisorConfig: {
systemMessage: "You are TaskBot. Use delegate_task(task, [agentNames]) to assign work.";
}
Control memory:
supervisorConfig: {
includeAgentsMemory: false; // Fresh context each interaction (default: true)
}
Configure event forwarding:
supervisorConfig: {
fullStreamEventForwarding: {
types: ['tool-call', 'tool-result', 'text-delta'], // Control which events to forward
}
}
Handle errors gracefully:
supervisorConfig: {
throwOnStreamError: false, // Return errors as results (default)
includeErrorInEmptyResponse: true // Include error details in response (default)
}
Throw exceptions for custom error handling:
supervisorConfig: {
throwOnStreamError: true; // Throw exceptions on sub-agent failures
}
How Sub-agents Work
The supervisor agent delegates tasks to its sub-agents using the automatically provided delegate_task
tool.
- A user sends a request to the supervisor agent.
- The supervisor's LLM analyzes the request and its system prompt (which lists available sub-agents).
- Based on the task, the supervisor decides which sub-agent(s) to use.
- The supervisor uses the
delegate_task
tool to hand off the task(s).
The delegate_task
Tool
This tool is automatically added to supervisor agents and handles delegation.
- Name:
delegate_task
- Description: "Delegate a task to one or more specialized agents"
- Parameters:
task
(string, required): The task description to be delegatedtargetAgents
(array of strings, required): Sub-agent names to delegate the task to. The supervisor can delegate to multiple agents simultaneouslycontext
(object, optional): Additional context needed by the sub-agent(s)
- Execution:
- Finds the sub-agent instances based on the provided names
- Calls the
handoffTask
(orhandoffToMultiple
) method internally - Passes the supervisor's agent ID (
parentAgentId
) and history entry ID (parentHistoryEntryId
) for observability
- Returns: An array of objects with results from each delegated agent:
[
{
agentName: string; // Name of the sub-agent that executed the task
response: string; // The text result returned by the sub-agent
usage?: any; // Token usage information
},
// ... more results if multiple agents were targeted
]
- Sub-agents process their delegated tasks independently. They can use their own tools or delegate further if they are also supervisors.
- Each sub-agent returns its result to the
delegate_task
tool execution context. - The supervisor receives the results from the
delegate_task
tool. - The supervisor synthesizes the final response based on its instructions and the received results.
Complete Example
import { Agent } from "@voltagent/core";
import { openai } from "@ai-sdk/openai";
// Create agents
const writer = new Agent({
name: "Writer",
instructions: "Write creative stories",
model: openai("gpt-4o-mini"),
});
const translator = new Agent({
name: "Translator",
instructions: "Translate text accurately",
model: openai("gpt-4o-mini"),
});
// Create supervisor
const supervisor = new Agent({
name: "Supervisor",
instructions: "Coordinate story writing and translation",
model: openai("gpt-4o-mini"),
subAgents: [writer, translator],
});
// Use it
const result = await supervisor.streamText("Write a story about AI and translate to Spanish");
for await (const chunk of result.textStream) {
process.stdout.write(chunk);
}
What happens:
- Supervisor analyzes request
- Calls
delegate_task
→ Writer creates story - Calls
delegate_task
→ Translator translates - Combines results and responds
Using Hooks
Monitor task delegation with the onHandoff
hook:
const supervisor = new Agent({
name: "Supervisor",
subAgents: [writer, translator],
hooks: {
onHandoff: ({ agent, sourceAgent }) => {
console.log(`${sourceAgent.name} → ${agent.name}`);
},
},
});
Context Sharing
Sub-agents automatically inherit the supervisor's context:
// Supervisor passes context
const response = await supervisor.streamText("Task", {
context: new Map([["projectId", "123"]]),
});
// Sub-agent receives it automatically
const subAgent = new Agent({
hooks: {
onStart: (context) => {
const projectId = context.context.get("projectId"); // "123"
},
},
});
Step Control
Control workflow steps with maxSteps
:
const supervisor = new Agent({
subAgents: [writer, editor],
maxSteps: 20, // Inherited by all sub-agents
});
// Override per request
const result = await supervisor.generateText("Task", { maxSteps: 10 });
Default: 10 × number_of_sub-agents
(prevents infinite loops)
Observability
Sub-agent operations are automatically linked to their supervisor for traceability in monitoring tools.
Advanced Configuration
Use different execution methods for sub-agents:
import { createSubagent } from "@voltagent/core";
import { z } from "zod";
const AnalysisSchema = z.object({
insights: z.array(z.string()),
confidence: z.number().min(0).max(1),
});
const supervisor = new Agent({
subAgents: [
writer, // Default streamText
createSubagent({
agent: analyzer,
method: "generateObject",
schema: AnalysisSchema,
options: { temperature: 0.1 },
}),
],
});
Available methods:
streamText
(default) - Real-time text streaminggenerateText
- Text generationgenerateObject
- Structured data with Zod schemastreamObject
- Streaming structured data
Dynamic Sub-agents
Add sub-agents after initialization:
supervisor.addSubAgent(newAgent);
Remove Sub-agents
supervisor.removeSubAgent(agentId);
Troubleshooting
Sub-agent not being called?
- Check agent names match exactly
- Make supervisor instructions explicit about when to delegate
- Use
onHandoff
hook to debug delegation flow