Agent Surface
Multi agent

Supervisor Pattern

Implementing orchestrator-worker agent architectures across Mastra, Vercel AI SDK, LangGraph, CrewAI, and OpenAI

Summary

A supervisor decomposes tasks, delegates to specialists via agents-as-tools or handoffs, collects results, and synthesizes output. Agents-as-tools (tool calls returning results) are standard for quick delegations; handoffs (transfer control, sub-agent talks to user) are for extended specialist sessions. Sub-agents are isolated from each other and receive only what the supervisor passes.

Supervisor (receives goal)
  ├─ Tool call: Research Agent → result 1
  ├─ Tool call: Analyst Agent → result 2
  └─ Tool call: Writer Agent → final output
  • Agents-as-tools: supervisor receives output, can aggregate and reason
  • Handoffs: supervisor transfers control, specialist owns conversation
  • Memory isolation: sub-agents don't share state, pass data explicitly
  • Over-delegation: avoid delegating every decision to sub-agents
  • Validation: supervisor checks results meet requirements before proceeding

The supervisor pattern puts a single orchestrating agent in charge of decomposing a goal, delegating work to specialist sub-agents, collecting results, and synthesizing a final output. The supervisor is the only agent that communicates directly with the user or the calling system; workers are implementation details.

This is the most widely implemented multi-agent pattern across frameworks because it maps cleanly onto how teams work: a manager who understands the overall goal and specialists who execute specific tasks without needing to understand the full picture.

The Core Distinction: Handoffs vs. Agents-as-Tools

Before implementation details, the most important conceptual distinction in supervisor-pattern work:

Handoffs (transfer control): The supervisor transfers execution to a sub-agent and stops. The sub-agent communicates directly with the user until it transfers control back or concludes. The supervisor does not receive the sub-agent's output programmatically — it re-enters when the sub-agent initiates a return handoff.

Agents-as-tools (return control): The supervisor invokes a sub-agent as a tool call. The sub-agent runs to completion and returns a result. The supervisor receives that result, adds it to its context, and continues reasoning. The sub-agent never communicates directly with the user.

HandoffAgent-as-Tool
Who talks to userSub-agent after transferSupervisor only
Result flowBack via return handoffReturned as tool output
Context continuitySub-agent has full conversation historySub-agent has only what supervisor passes
Use caseDeep specialist sessionsQuick delegated lookups

Most production supervisor implementations use agents-as-tools for the orchestration layer, reserving handoffs for cases where the specialist needs extended back-and-forth with the user.

Mastra

Mastra's supervisor pattern uses the agents property to declare which sub-agents a supervisor can delegate to. Sub-agents are invoked through tool calls generated by the supervisor's language model.

import { Agent } from "@mastra/core/agent"
import { openai } from "@ai-sdk/openai"

// --- Sub-agents ---

const researchAgent = new Agent({
  name: "Researcher",
  instructions: `You are a research specialist. Given a topic or question,
    search for relevant information and return a structured summary with:
    - key facts
    - relevant sources
    - confidence level (high/medium/low)
    Return only factual content; do not interpret or recommend.`,
  model: openai("gpt-4o-mini"),
  tools: { webSearch, fetchUrl, extractText }
})

const analystAgent = new Agent({
  name: "Analyst",
  instructions: `You are a data analysis specialist. Given research summaries
    and data, identify patterns, draw conclusions, and flag uncertainties.
    Return structured analysis with explicit confidence bounds.`,
  model: openai("gpt-4o"),
  tools: { runQuery, generateChart, calculateStats }
})

const writerAgent = new Agent({
  name: "Writer",
  instructions: `You are a technical writer. Given research and analysis,
    produce clear, structured reports for a technical audience.
    Follow house style: active voice, concrete examples, no jargon.`,
  model: openai("gpt-4o"),
  tools: { formatMarkdown, checkGrammar }
})

// --- Supervisor ---

const supervisorAgent = new Agent({
  name: "Supervisor",
  instructions: `You coordinate research, analysis, and writing tasks.
    
    For any user request:
    1. Determine which specialists are needed
    2. Delegate to Researcher first to gather facts
    3. Delegate to Analyst with the research results
    4. Delegate to Writer with the analysis to produce the final output
    5. Review the output and return it to the user
    
    Always delegate — do not attempt research, analysis, or writing yourself.
    If a specialist returns an error, report it clearly and suggest a retry or alternative.`,
  model: openai("gpt-4o"),
  agents: [researchAgent, analystAgent, writerAgent]
})

// Usage
const response = await supervisorAgent.generate("Analyze the competitive landscape for MCP servers")

Delegation Hooks

Mastra agents support onGenerateText hooks for logging, tracing, or intercepting delegations:

const supervisorAgent = new Agent({
  name: "Supervisor",
  instructions: "...",
  model: openai("gpt-4o"),
  agents: [researchAgent, analystAgent, writerAgent],
  onGenerateText: async ({ messages, tools }) => {
    // Log which agent is being invoked for observability
    const delegationCalls = tools?.filter(t => t.type === "agent")
    if (delegationCalls?.length) {
      console.log(`Supervisor delegating to: ${delegationCalls.map(t => t.name).join(", ")}`)
    }
  }
})

Memory Isolation

Sub-agents in Mastra do not share memory with the supervisor by default. Each agent maintains its own message history, working memory, and semantic recall. This is correct behavior for most use cases — you do not want the analyst's memory of past analyses bleeding into the researcher's behavior.

To share context between agents, pass information explicitly through the task delegation parameters rather than relying on shared memory state.

Vercel AI SDK

The AI SDK implements supervisor patterns through generateText with sub-agents exposed as tools. The supervisor's generate loop calls the sub-agent tools, receives their results as tool outputs, and continues reasoning.

import { generateText, tool } from "ai"
import { openai } from "@ai-sdk/openai"
import { z } from "zod"

// --- Sub-agent tools ---

const researchTool = tool({
  description: `Research a topic and return structured findings.
    Use when you need factual information about a specific subject.
    Returns sources, key facts, and confidence level.`,
  parameters: z.object({
    topic: z.string().describe("The topic or question to research"),
    depth: z.enum(["quick", "thorough"]).describe("How deep to search")
  }),
  execute: async ({ topic, depth }) => {
    // The sub-agent is invoked here as a regular function call
    const { text } = await generateText({
      model: openai("gpt-4o-mini"),
      system: `You are a research specialist. Search thoroughly and return structured JSON:
        { "facts": [], "sources": [], "confidence": "high|medium|low" }`,
      prompt: `Research: ${topic} (depth: ${depth})`
    })
    return text
  }
})

const analysisTool = tool({
  description: `Analyze data or research findings and return structured conclusions.
    Use after research is complete to interpret findings and identify patterns.
    Returns conclusions with explicit confidence bounds.`,
  parameters: z.object({
    data: z.string().describe("The research findings or data to analyze"),
    question: z.string().describe("The specific analytical question to answer")
  }),
  execute: async ({ data, question }) => {
    const { text } = await generateText({
      model: openai("gpt-4o"),
      system: "You are a data analysis specialist. Return structured JSON analysis.",
      prompt: `Analyze this data:\n${data}\n\nQuestion: ${question}`
    })
    return text
  }
})

// --- Supervisor ---

const { text } = await generateText({
  model: openai("gpt-4o"),
  system: `You coordinate research and analysis tasks.
    Always use the research tool before the analysis tool.
    Synthesize the final result into a clear, actionable summary.`,
  prompt: userRequest,
  tools: { research: researchTool, analyze: analysisTool },
  maxSteps: 10 // prevent runaway loops
})

toModelOutput for Interface Separation

The toModelOutput property transforms tool output before it re-enters the supervisor's context window. This is critical for large sub-agent responses — return only what the supervisor needs to continue reasoning, not the full raw output:

const researchTool = tool({
  description: "Research a topic...",
  parameters: z.object({ topic: z.string() }),
  execute: async ({ topic }) => {
    // Returns a large object with full research data
    return await runResearchAgent(topic)
  },
  // Only pass a compact summary to the supervisor's context
  toModelOutput: (result) => ({
    type: "text",
    text: `Research complete. Key facts: ${result.facts.slice(0, 3).join("; ")}. Confidence: ${result.confidence}.`
  })
})

Without toModelOutput, a sub-agent that returns 5000 tokens of research will consume 5000 tokens of the supervisor's context window on every subsequent reasoning step. Use toModelOutput to extract the decision-relevant signal.

LangGraph

LangGraph's supervisor pattern uses a state graph where the supervisor node controls routing. Sub-agents are wrapped as tools or invoked as nodes, and the supervisor decides which node to execute next.

from langgraph.graph import StateGraph, END
from langgraph.prebuilt import create_supervisor
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage

# --- Sub-agents as LangGraph nodes ---

def research_node(state: dict) -> dict:
    """Research sub-agent node."""
    model = ChatOpenAI(model="gpt-4o-mini")
    result = model.invoke([
        HumanMessage(content=f"Research: {state['task']}")
    ])
    return {"research": result.content, "messages": [result]}

def analysis_node(state: dict) -> dict:
    """Analysis sub-agent node."""
    model = ChatOpenAI(model="gpt-4o")
    result = model.invoke([
        HumanMessage(content=f"Analyze this research:\n{state['research']}")
    ])
    return {"analysis": result.content, "messages": [result]}

# --- Using create_supervisor prebuilt ---

from langgraph.prebuilt import create_supervisor

supervisor_graph = create_supervisor(
    agents=[research_agent, analysis_agent, writer_agent],
    model=ChatOpenAI(model="gpt-4o"),
    prompt="""You coordinate research, analysis, and writing.
    
    Route to 'research' for fact-gathering tasks.
    Route to 'analysis' after research is complete and needs interpretation.
    Route to 'writer' to produce the final document.
    Route to FINISH when the output is complete and reviewed."""
)

# --- Manual supervisor implementation for full control ---

def supervisor_node(state: dict) -> dict:
    """Supervisor determines the next step."""
    next_step = supervisor_model.invoke(state["messages"])
    return {"next": next_step.content}

def route(state: dict) -> str:
    if state["next"] == "FINISH":
        return END
    return state["next"]

builder = StateGraph(dict)
builder.add_node("supervisor", supervisor_node)
builder.add_node("research", research_node)
builder.add_node("analysis", analysis_node)
builder.add_edge("__start__", "supervisor")
builder.add_conditional_edges("supervisor", route)
builder.add_edge("research", "supervisor")
builder.add_edge("analysis", "supervisor")

graph = builder.compile()

Workers Wrapped as Tools

For tighter coupling, LangGraph supports wrapping entire sub-graphs as tools that the supervisor invokes through its language model's native tool use:

from langgraph.prebuilt import create_react_agent

research_agent = create_react_agent(
    model=ChatOpenAI(model="gpt-4o-mini"),
    tools=[web_search, fetch_url],
    name="researcher",
    prompt="You are a research specialist..."
)

# Expose the agent as a tool for the supervisor
research_tool = research_agent.as_tool(
    name="research",
    description="Research a topic and return structured findings. "
                "Use before analysis tasks."
)

supervisor = create_react_agent(
    model=ChatOpenAI(model="gpt-4o"),
    tools=[research_tool, analysis_tool, write_tool],
    prompt="You coordinate research, analysis, and writing..."
)

CrewAI

CrewAI's hierarchical process creates a manager agent automatically. Set process=Process.hierarchical and the framework generates the manager LLM, injects worker descriptions, and handles task delegation.

from crewai import Agent, Task, Crew, Process
from langchain_openai import ChatOpenAI

# --- Workers ---

researcher = Agent(
    role="Research Specialist",
    goal="Find accurate, relevant information for any given topic",
    backstory="""You have deep expertise in research methodology and source evaluation.
    You are rigorous, skeptical, and precise in your findings.""",
    tools=[web_search, document_reader],
    llm=ChatOpenAI(model="gpt-4o-mini"),
    verbose=True
)

analyst = Agent(
    role="Data Analyst",
    goal="Derive meaningful insights from research and data",
    backstory="""You specialize in pattern recognition, statistical reasoning,
    and translating complex data into clear conclusions.""",
    tools=[run_calculation, generate_chart],
    llm=ChatOpenAI(model="gpt-4o"),
    verbose=True
)

writer = Agent(
    role="Technical Writer",
    goal="Produce clear, accurate reports from research and analysis",
    backstory="""You write for technical audiences. Your reports are precise,
    structured, and free of jargon.""",
    tools=[format_document],
    llm=ChatOpenAI(model="gpt-4o"),
    verbose=True
)

# --- Tasks ---

research_task = Task(
    description="Research the following topic thoroughly: {topic}",
    expected_output="Structured research summary with sources and confidence levels",
    agent=researcher
)

analysis_task = Task(
    description="Analyze the research findings and identify key insights",
    expected_output="Analysis with conclusions and evidence",
    agent=analyst,
    context=[research_task]  # This task depends on research_task
)

report_task = Task(
    description="Write a comprehensive report based on the research and analysis",
    expected_output="Complete technical report in Markdown",
    agent=writer,
    context=[research_task, analysis_task]
)

# --- Crew with hierarchical process ---

crew = Crew(
    agents=[researcher, analyst, writer],
    tasks=[research_task, analysis_task, report_task],
    process=Process.hierarchical,
    manager_llm=ChatOpenAI(model="gpt-4o"),  # auto-created manager
    verbose=True
)

result = crew.kickoff(inputs={"topic": "MCP server adoption patterns"})

The auto-created manager agent receives descriptions of all workers and tasks, and decides the execution order, delegation, and when the crew is done. For most use cases, this is sufficient. For complex routing logic, provide an explicit manager_agent instead of manager_llm.

OpenAI Agents SDK

The OpenAI Agents SDK's supervisor pattern uses the handoffs array for transfer-of-control and .as_tool() for agents-as-tools. These are distinct APIs with different semantics.

Handoffs Array

from openai.agents import Agent, Runner

research_agent = Agent(
    name="Researcher",
    instructions="""Research topics and return structured findings.
    When done, hand off back to the supervisor.""",
    tools=[web_search, fetch_url]
)

analysis_agent = Agent(
    name="Analyst",
    instructions="""Analyze research findings and return structured analysis.
    When done, hand off back to the supervisor.""",
    tools=[run_calculation]
)

supervisor = Agent(
    name="Supervisor",
    instructions="""Coordinate research and analysis to fulfill user requests.
    
    1. Hand off to Researcher to gather facts
    2. Hand off to Analyst to interpret findings
    3. Synthesize the final response
    
    Note: When you hand off, execution transfers to that agent.
    They will hand back to you when done.""",
    handoffs=[research_agent, analysis_agent]
)

result = await Runner.run(supervisor, "Analyze the MCP ecosystem")
print(result.final_output)

.as_tool() Pattern

When you need the sub-agent's output returned to the supervisor as a tool result rather than transferring control:

from openai.agents import Agent, Runner

research_agent = Agent(
    name="Researcher",
    instructions="Research topics thoroughly and return structured findings.",
    tools=[web_search, fetch_url]
)

# Expose as a tool — supervisor receives the output as a tool result
research_tool = research_agent.as_tool(
    tool_name="research",
    tool_description="Research a topic and return structured findings. "
                     "Use before analysis tasks. Returns facts, sources, confidence."
)

supervisor = Agent(
    name="Supervisor",
    instructions="""Use your tools to complete research and analysis tasks.
    Always research before analyzing. Synthesize the final response yourself.""",
    tools=[research_tool, analysis_tool]  # sub-agents appear as regular tools
)

result = await Runner.run(supervisor, "Analyze the MCP ecosystem")

The .as_tool() pattern is preferable when:

  • The supervisor needs to use the sub-agent's output to make further decisions
  • The sub-agent task is quick and the supervisor should maintain context
  • You want the sub-agent's output to be type-validated before the supervisor sees it

The handoffs pattern is preferable when:

  • The sub-agent needs extended back-and-forth with external systems
  • The sub-agent's work is long enough that it should own the conversation thread
  • You want clear separation: the supervisor decides, the specialist executes

Failure Modes Common to All Implementations

Over-delegation: The supervisor delegates every decision to sub-agents, including decisions it could make directly from available context. Each delegation adds latency, cost, and an opportunity for failure. Supervisors should delegate tasks, not questions.

Under-specified sub-agents: A sub-agent whose instructions do not clearly scope its role will attempt to answer questions outside its domain, producing unreliable results. Every sub-agent's instructions should define both what it does and what it does not do.

Context leakage across delegations: When a supervisor accumulates tool results from multiple sub-agents in its context, earlier results affect later delegations through the supervisor's reasoning. If the research agent returns uncertain findings, this uncertainty can incorrectly bias the analyst's instructions. Structure delegation prompts to isolate each sub-agent's inputs.

No result validation: The supervisor receives a sub-agent's output and passes it directly to the next stage without checking whether it met the task requirements. Add explicit validation at the supervisor level: "Did the researcher return the confidence level I asked for? If not, retry with a clarified request."

On this page