Recommended resources
The freely available resources this lesson will direct you to.
Welcome back to the course. In our last lesson, we built a crucial piece of our multi-tenant system: a TenantService that resolves a complete operational context and a tenantAwareAgent that dynamically configures itself using the Vercel AI SDK's prepareCall function. This gave us a single, flexible agent that adapts its tools and instructions on a per-request basis.
Today, we will build upon that foundation to implement the core of our hierarchical design. We are moving from a single agent that does tasks to a top-level Orchestrator agent that delegates tasks. Your goal for this lesson is to initialize this orchestrator with a set of tenant-specific sub-agents, effectively transforming them into tools the orchestrator can wield. This will set the stage for the full end-to-end agentic workflow.
The Orchestrator-Worker Pattern
In a hierarchical agentic system, the top-level agent acts as a router or a project manager. It analyzes an incoming request, breaks it down, and delegates the constituent parts to specialized "worker" agents. It does not perform the detailed work itself. Its primary "tools" are the other agents in the system.

This pattern is powerful for a few reasons:
- Specialization: Each worker agent can be fine-tuned with specific prompts, tools, and even models for a narrow domain (e.g., a
researchAgent, acodeGenerationAgent). - Modularity: You can add, remove, or update worker agents without altering the orchestrator's core logic.
- Scalability: Complex problems can be broken down and potentially parallelized across multiple agents.
This concept of using agents as tools is a key feature we can implement with the Vercel AI SDK.
The video "Build an Agent with Vercel" directly addresses this architecture. The speaker explains how the SDK's agent abstraction was designed to support this pattern.
Please watch the section from orchestrator pattern. The speaker describes a top-level agent whose tools are themselves sub-agents. This is precisely the architecture we are implementing.
Implementing an "Agent as a Tool"
So, how do we practically make one agent a "tool" for another? The tool function from the Vercel AI SDK requires an execute function that performs an action. This action can be anything—an API call, a database query, or, in our case, an invocation of another agent.
Let's imagine we have a researchAgent defined elsewhere. We can wrap it in a tool like this:
import { tool } from 'ai';
import { z } from 'zod';
import { researchAgent } from './sub-agents/research'; // A predefined ToolLoopAgent
const researchTool = tool({
description: 'Use this tool for in-depth research on a topic. Provide a clear and specific research query.',
parameters: z.object({
query: z.string().describe('The detailed research question to investigate.'),
}),
execute: async ({ query }) => {
// Invoke the sub-agent with the user's query
const { finalResponse } = await researchAgent.run({
input: query,
});
// Return the sub-agent's final output as the tool's result
return finalResponse;
},
});
A crucial point here is that the sub-agent (researchAgent) is not a simple, single-shot function. It's a ToolLoopAgent itself, capable of running its own internal loop of reasoning and tool use. The orchestrator doesn't need to know about the sub-agent's internal complexity; it just invokes the tool and gets a result.
A Complete Guide To Vercel’s AI SDK // The ESSENTIAL Tool For Shipping AI Apps
In the video "A Complete Guide To Vercel’s AI SDK," Matt Pocock explains how maxSteps enables agentic loops.
Watch the segment from agentic loops to understand how a tool call can trigger a multi-step process. This is what happens inside our researchTool's execute function: we trigger a whole new agentic loop.
Dynamically Initializing the Orchestrator
Just as our single agent was tenant-aware, our orchestrator must be as well. The set of sub-agents an orchestrator can delegate to should be determined by the resolved tenantContext. A "free" tier tenant might only have access to a basicQA_Agent, while an "enterprise" tenant might have a full suite of powerful, specialized agents.
This requires two main steps:
- Dynamically composing the orchestrator's system prompt: The prompt must inform the orchestrator about which sub-agent "tools" are at its disposal for the current request.
- Dynamically creating the tool wrappers: We need to generate the
toolobjects for each available sub-agent at runtime.
For the first step, we can use the dynamic prompt engine pattern you've seen before.
Building a Dynamic Prompt Engine for an AI SaaS (Next.js + Vercel ...
This article provides an excellent, practical guide for building a dynamic prompt generation system, which is exactly what we need for our orchestrator.
Please review the article, focusing on these sections: The Solution: Understand the core idea of generating prompts based on structured parameters like role and intent. The Architecture: See how a prompt engine fits into a Next.js application. The Implementation: Study the code for defining parameters, generating a prompt string, and injecting it into an AI SDK call.
Now, let's put it all together. We will refactor the tenantAwareAgent from the previous lesson into a createTenantOrchestrator function.
Step 1: Extend the Tenant Context and Define Sub-Agents
First, our TenantContext needs to know about sub-agents. We'll also define a placeholder for our sub-agents. In a full application, these definitions would also be stored in and resolved from your database.
// src/services/TenantService.ts (Updated Interface)
export interface TenantContext {
// ... existing properties
subAgents: { name: string; description: string }[];
}
// src/agents/sub-agents.ts (New file)
import { ToolLoopAgent } from 'ai';
import { openai } from '@ai-sdk/openai';
// A placeholder for a specialized research agent.
// In a real app, it would have its own tools (e.g., web search).
export const researchAgent = new ToolLoopAgent({
model: openai('gpt-4-turbo'),
instructions: 'You are a world-class research analyst. Find and synthesize information to answer the user query.',
tools: { /* Web search tools, etc. */ }
});
// A placeholder for a data analysis agent.
export const dataAnalysisAgent = new ToolLoopAgent({
model: openai('gpt-4-turbo'),
instructions: 'You are a data analyst. Analyze data and provide insights.',
tools: { /* CSV parsers, charting tools, etc. */ }
});
// A registry to easily look up agent definitions
export const agentRegistry = {
researchAgent,
dataAnalysisAgent,
};
Step 2: Create the Orchestrator Definition
Now for the main event. We'll create our orchestrator. The logic inside prepareCall is where we dynamically build its capabilities based on the tenant context.
// src/agents/orchestrator.ts (Refactored from previous lesson's agent.ts)
import { ToolLoopAgent, tool, z } from 'ai';
import { openai } from '@ai-sdk/openai';
import { TenantContext } from '@/services/TenantService';
import { agentRegistry } from './sub-agents';
// The schema for our call options remains the same
const tenantContextSchema = z.custom<TenantContext>();
// A function to dynamically generate the system prompt for the orchestrator
function generateOrchestratorPrompt(tenantContext: TenantContext): string {
const basePrompt = `You are a master orchestrator agent. Your role is to analyze the user's request and delegate it to the most appropriate specialist agent. Do not answer the user directly. You must use one of your available tools.`;
const availableToolsDescription = tenantContext.subAgents
.map(agent => `- ${agent.name}: ${agent.description}`)
.join('\n');
return `${basePrompt}\n\nAvailable specialist agents (tools):\n${availableToolsDescription}`;
}
// Define the orchestrator
export const orchestrator = new ToolLoopAgent({
model: openai('gpt-4o'), // Use a powerful model for orchestration
callOptionsSchema: tenantContextSchema,
// The magic happens here!
prepareCall: async ({ options: tenantContext, ...settings }) => {
// 1. Generate the dynamic system prompt
const finalInstructions = generateOrchestratorPrompt(tenantContext);
// 2. Dynamically build the tools object from the tenant's sub-agents
const availableTools = {};
for (const agentInfo of tenantContext.subAgents) {
const agentDefinition = agentRegistry[agentInfo.name];
if (agentDefinition) {
availableTools[agentInfo.name] = tool({
description: agentInfo.description,
parameters: z.object({
input: z.string().describe("The detailed task or query to pass to the specialist agent."),
}),
execute: async ({ input }) => {
console.log(`Orchestrator delegating to ${agentInfo.name} with input: "${input}"`);
// --- CRUCIAL ---
// Here, you would pass the tenantContext down to the sub-agent's `run` call
// if the sub-agent itself needs to be tenant-aware (e.g., to use tenant-specific tools).
// For now, we'll keep it simple.
const { finalResponse } = await agentDefinition.run({ input });
return finalResponse;
},
});
}
}
return {
...settings,
instructions: finalInstructions,
tools: availableTools,
// We can also override the model based on tenant plan, etc.
model: openai(tenantContext.agentConfig.model),
};
},
});
Step 3: Update the API Route
Finally, your API route simply needs to import and use the new orchestrator instead of the old tenantAwareAgent. The rest of the code, which you've already written, remains the same.
// app/api/chat/route.ts (Minor update)
// ... imports
import { orchestrator } from '@/agents/orchestrator'; // Use the new orchestrator
export async function POST(request: Request) {
// ... (code for getting messages, tenantId, and resolving tenantContext is unchanged)
if (!tenantContext) { /* ... error handling ... */ }
// The only change is here:
return createAgentUIStreamResponse({
agent: orchestrator, // Use the orchestrator
messages,
options: tenantContext,
});
}
With this structure, when a request for Tenant A comes in, the prepareCall function might arm the orchestrator with researchAgent and dataAnalysisAgent. For a request from Tenant B, it might only provide a basicQA_Agent. The orchestrator's behavior is now fully dictated by the resolved tenant context, right down to the specialized agents it's allowed to command.
Conclusion
In this lesson, you've taken a significant step toward realizing our full agentic hierarchy. You've transformed the concept of a "tool" from a simple function into a powerful, self-contained sub-agent. By dynamically initializing an orchestrator with a tenant-specific set of these agent-tools, you have laid the groundwork for complex, multi-agent workflows.
Key Takeaways:
- The Orchestrator Pattern: The primary agent in a hierarchy should delegate tasks to specialized worker agents rather than executing them directly.
- Agent as a Tool: Any agent can be wrapped in the Vercel AI SDK's
toolabstraction, allowing it to be called by another agent. - Dynamic Hierarchy Initialization: The
prepareCallfunction is the ideal place to construct the agent hierarchy at runtime, using a resolvedtenantContextto determine which sub-agents are available and how the orchestrator should be instructed to use them.
You have successfully initialized the orchestrator. In our next lesson, we will focus on the next logical step: wiring the orchestrator's task routing to properly invoke these sub-agents and handle the results, completing the end-to-end delegation loop.