Recommended resources
The freely available resources this lesson will direct you to.
Welcome to the final lesson of our "End-to-End System Integration" module. In our last session, we successfully connected the orchestrator to its sub-agents, enabling it to delegate tasks and pass down the crucial tenantContext. This established a fully context-aware agent hierarchy. However, the journey of a request isn't over yet. The sub-agent completes its task and sends a result back, but what happens then?
Today, we will complete the entire request lifecycle. We'll focus on the orchestrator's final and most important role: acting as an aggregator. You will learn how the orchestrator synthesizes the results from its specialist agents into a coherent, final answer and how to stream that answer back to the user through your API. By the end of this lesson, you will have a fully functional, end-to-end multi-agent system that can receive a request, orchestrate a complex workflow, and deliver a final response.
From Delegation to Synthesis
In our previous lesson, the orchestrator's tool definition for a sub-agent looked like this:
// from the previous lesson...
execute: async ({ input }) => {
// ...
const { finalResponse } = await agentDefinition.run({
input,
options: tenantContext,
});
return finalResponse; // The sub-agent's result is returned
},
When this execute function completes, the finalResponse from the sub-agent is returned as a tool-result. The Vercel AI SDK automatically takes this result and feeds it back into the orchestrator's underlying Large Language Model (LLM) for another processing step. This is the critical moment where the orchestrator transitions from a delegator to a synthesizer.
This multi-step process, where the model first calls a tool and then processes its result, is a core feature of agentic workflows.
Multi-Step (Agentic) Generation with the AI SDK
The Vercel AI SDK team explains this exact mechanism in their "Multi-Step Generation" video.
Watch the segment from this new feature. The key insight here is that when a tool is called, the AI SDK "will pipe the results back to the llm alongside the initial prompt for another generation." This automatic continuation is what allows our orchestrator to synthesize the final answer.
Our job is to ensure the orchestrator is properly instructed on what to do in this second step. It shouldn't just echo the sub-agent's output; it must craft a final, user-facing response. We can achieve this by refining its system prompt.
Let's update the orchestrator's prepareCall function to include this crucial instruction for synthesis.
// src/agents/orchestrator.ts (inside prepareCall)
// ... existing prompt logic ...
const toolDescriptions = Object.values(agentRegistry)
.map(agent => `- ${agent.name}: ${agent.description}`)
.join('\n');
const systemPrompt = `You are a master orchestrator agent for a multi-tenant SaaS application.
You are responsible for analyzing user requests and delegating them to the appropriate specialist agent.
You have access to the following specialist agents:
${toolDescriptions}
First, analyze the user's request to determine which specialist is best suited for the task.
Then, call the corresponding tool to delegate the work.
CRITICAL: After the specialist agent returns a result, your final job is to synthesize that information into a clear and concise answer for the end-user. Do not simply repeat the raw output from the specialist. Format it as a helpful, final response.`;
// ... return the system prompt in the settings
With this updated prompt, the orchestrator now has a clear, two-part mission: first delegate, then synthesize.
Delivering the Final Response
Now that the orchestrator is producing a final, synthesized text response, we need to capture it and send it back through our API endpoint. The orchestrator.run() method returns a result object that contains everything we need.
The structure of this result object, as defined by the underlying streamText function, gives us access to streams, promises for final content, tool calls, and more. Most importantly, it includes methods to easily convert this complex result into a standard HTTP streaming response.
This documentation page for streamText details the object returned by our agent's run method and how to handle it.
First, review the Returns section to understand the various properties available on the result object, such as text, toolCalls, and steps. Then, focus on the final part of the page describing the response helper methods, particularly the toUIMessageStreamResponse function. This is the function we'll use to create our API response.
The toUIMessageStreamResponse() function is designed to work seamlessly with the useChat hook from the Vercel AI SDK's React package. It formats the output into a specific streaming protocol that the frontend can easily consume.
Let's modify our API route to use this function and complete the end-to-end flow.
// src/app/api/chat/route.ts
import { orchestrator } from '@/agents/orchestrator';
import { TenantService } from '@/services/TenantService';
import { UIMessage } from 'ai';
import { NextRequest, NextResponse } from 'next/server';
export const maxDuration = 60; // Allow longer execution for complex agent tasks
export async function POST(req: NextRequest) {
try {
const { messages, tenantId }: { messages: UIMessage[]; tenantId: string } = await req.json();
if (!tenantId) {
return NextResponse.json({ error: 'tenantId is required' }, { status: 400 });
}
const tenantContext = await TenantService.resolveContext(tenantId);
if (!tenantContext) {
return NextResponse.json({ error: `Tenant not found: ${tenantId}` }, { status: 404 });
}
// The run method of the agent returns a result object.
const result = await orchestrator.run({
messages, // Pass the full conversation history from the client
options: tenantContext,
});
// Convert the agent's result into a streaming response for the client.
return result.toUIMessageStreamResponse();
} catch (error) {
console.error('Error in chat API:', error);
const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred';
return NextResponse.json({ error: errorMessage }, { status: 500 });
}
}
With this change, our API is now fully capable of streaming the orchestrator's final, synthesized response back to the client.
The Full Picture: Orchestration and Aggregation
Let's visualize the complete flow you've now built.

In our implementation, the Orchestrator and Aggregator roles are both fulfilled by our single orchestrator agent, powered by the ToolLoopAgent.
- IN: The POST request hits
/api/chat. - ORCHESTRATOR (Delegation): The agent's first LLM call analyzes the prompt and decides to call a sub-agent tool (e.g.,
researchAgent). - AGENT 1/2/3: The sub-agent's
runmethod is executed, performing the specialized task. - AGGREGATOR (Synthesis): The sub-agent's result is piped back to the orchestrator agent for a second LLM call. Guided by its system prompt, it synthesizes the final response.
- OUT: The
toUIMessageStreamResponse()function streams this final synthesized response back to the client.
The Vercel AI SDK handles the complexities of this multi-step process, allowing you to define the high-level logic in the agent's instructions.
Understanding the Streaming Protocol
When you call toUIMessageStreamResponse(), you are not just sending back plain text. You are using the AI SDK's Data Stream Protocol.
This documentation explains the format of the data stream.
Read the section on the Data Stream Protocol. Note that it uses Server-Sent Events (SSE) and can carry different types of information, including text, tool calls, and custom data. This rich protocol enables advanced UIs that can show the agent's "thought process."
While our current implementation primarily streams the final text, this underlying protocol is powerful. It allows the frontend to receive real-time updates about the agent's state, such as when it starts calling a tool and when it receives a result. This is invaluable for building transparent and engaging user experiences.
Conclusion
Congratulations! You have successfully completed the final link in the chain, building a fully operational, end-to-end agentic workflow. You have empowered your orchestrator to not only delegate tasks but also to synthesize the results into a final, user-facing answer, and you have implemented the mechanism to stream this response back to the client.
Key Takeaways:
- Agent Synthesis: The orchestrator's job doesn't end at delegation. Its second, equally important role is to act as an aggregator, synthesizing tool results into a final response.
- System Prompt for Synthesis: You can guide the agent's synthesis step by adding explicit instructions to its system prompt.
- Streaming API Responses: The
runmethod of an agent returns a result object with helper functions liketoUIMessageStreamResponse()to easily create streaming API responses compatible with frontend hooks. - Data Stream Protocol: The AI SDK uses a rich SSE-based protocol to stream not just text but also structured data about the agent's execution, enabling advanced UIs.
In this module, you have laid the entire foundation for your multi-tenant agentic infrastructure. You've created the API endpoint, implemented tenant resolution, initialized the orchestrator, wired it to sub-agents, and now, returned the final result.
Our next and final lesson in this course will be a capstone: writing an integration test that validates this entire multi-agent, multi-tenant workflow from start to finish, ensuring the robustness of the system you've built.