Introduction & Context

Welcome back! In the previous lesson, you mastered building agentic pipelines in which specialized agents work together in a fixed sequence. Today, we're taking a significant architectural leap forward by learning how to build agent orchestration systems, where a central planner agent can dynamically decide which specialist agents to call based on the specific task at hand.

The key insight is that complex agentic systems can be wrapped as simple tools for other agents. This creates a powerful hierarchy in which agents can leverage the full capabilities of other agents as easily as they use basic functions, enabling much more flexible and intelligent problem-solving than fixed pipeline sequences.

The Agent-as-Tool Concept

The fundamental difference between pipelines and orchestration lies in decision-making authority. In a pipeline, you as the developer decide the sequence: every problem goes through analysis, then calculation, then presentation. In orchestration, the central agent makes these decisions dynamically based on the nature of each specific request. The key insight is that we can encapsulate an agent call as a tool, allowing an orchestrator agent with access to that tool to seamlessly call another specialist agent.

Here's how the agent orchestration flow works:

  1. User submits a request to the orchestrator agent.
  2. Orchestrator analyzes the request and decides whether to handle it directly or delegate to a specialist.
  3. If delegation is needed, the orchestrator calls the specialist agent as a tool.
  4. Specialist processes the request and returns results to the orchestrator.
  5. Orchestrator provides the final response to the user.

Consider how this changes the system's behavior. If someone asks, "What is the capital of France?", a pipeline system would still run the question through all three agents unnecessarily. An orchestrated system, however, allows the central agent to recognize that this is a straightforward knowledge question requiring no mathematical tools and respond directly. But when someone asks about solving equations, the orchestrator can intelligently delegate this to a calculator specialist. This dynamic delegation creates systems that are both more efficient and more capable.

Building Specialist Agents

Before we can orchestrate agents, we need to create the specialist agents that will serve as tools. Let's build a calculator assistant that specializes in mathematical problem-solving, designed to work as a standalone tool that can handle mathematical questions from start to finish.

import json
from agent import Agent
from functions import sum_numbers, multiply_numbers, subtract_numbers, divide_numbers, power, square_root

# Load tool schemas
with open('schemas.json', 'r') as f:
    tool_schemas = json.load(f)

# Math tools
math_tools = {
    "sum_numbers": sum_numbers,
    "multiply_numbers": multiply_numbers,
    "subtract_numbers": subtract_numbers,
    "divide_numbers": divide_numbers,
    "power": power,
    "square_root": square_root
}

# Create a calculator assistant
calculator_assistant = Agent(
    name="calculator_assistant",
    system_prompt="You are a calculator assistant. Always use the available tools to perform calculations accurately.",
    tools=math_tools,
    tool_schemas=tool_schemas
)

This calculator assistant is designed with a tool-first approach. The system prompt explicitly instructs the agent to always use the available mathematical tools rather than attempting calculations independently. This is crucial for specialist agents that will be called by orchestrators — we want them to leverage their tools consistently and reliably, ensuring accurate results every time. By enforcing this tool-based approach, we create a specialist that's both predictable and trustworthy when delegated tasks from the orchestrator.

Creating Agent Tool Wrappers

To enable agents to use other agents as tools, we need to wrap any agent into a callable function that follows the same interface as our other tools. This helper function will streamline our workflow: we'll call it with our specialist agent to get back both a callable function and its schema, which we can then pass to our orchestrator agent, enabling it to call the specialist agent just like any other tool.

def create_agent_tool(agent, description):
    """
    Create a tool function and schema for using an agent as a tool.
    
    Args:
        agent: The agent to wrap as a tool
        description: Description of what this agent tool does
    
    Returns:
        tuple: (tool_function, tool_schema)
    """
    def agent_tool_function(message):
        print(f"🦾 Agent tool called ({agent.name}): {message}")
        
        # Call the agent
        _, response = agent.run([{"role": "user", "content": message}])
        
        print(f"📊 Agent response ({agent.name}): {response}")
        
        return response
    
    # Create schema for this agent tool
    tool_schema = {
        "type": "function",
        "name": f"{agent.name}_agent",
        "description": description,
        "parameters": {
            "type": "object",
            "properties": {
                "message": {
                    "type": "string",
                    "description": "The message to send to the agent"
                }
            },
            "required": ["message"],
            "additionalProperties": False
        }
    }
    
    return agent_tool_function, tool_schema

The key part of this function is the inner agent_tool_function. When we call agent.run(), it returns two things: the complete message history and the final text response. We use _ to ignore the message history because when one agent calls another as a tool, it only needs the final answer — not the entire conversation history with all the intermediate function calls and reasoning steps.

For example, if our orchestrator asks the calculator agent to solve an equation, it just wants the final answer like "x = 3 and x = 2," not the detailed trace of every mathematical function that was called along the way. This keeps the tool interface clean and focused on results.

The tool schema follows OpenAI's function calling structure with "type": "function" at the top level and "parameters" containing the object schema. The "additionalProperties": False constraint ensures that only the defined message parameter can be passed to the tool, preventing unexpected inputs.

Implementing the Orchestrator Agent

With our specialist agent ready and our wrapper function defined, we can now create the agent tool wrapper and build our orchestrator agent. The orchestrator will be a central planner that prioritizes delegation to specialist agents whenever possible.

# Create agent tool for calculator
calculator_tool_function, calculator_tool_schema = create_agent_tool(
    calculator_assistant,
    "Call the calculator assistant to solve mathematical problems and equations."
)

# Create a general assistant
helpful_assistant = Agent(
    name="helpful_assistant",
    system_prompt=(
        "You are a central planner who calls specialist agents. "
        "If a relevant tool exists, delegate and use its result; do not answer yourself. "
        "Only answer directly when no relevant tool exists."
    ),
    tools={calculator_tool_schema["name"]: calculator_tool_function},
    tool_schemas=[calculator_tool_schema]
)

The orchestrator agent (helpful_assistant) uses a strict delegation-first system prompt that fundamentally changes how it approaches tasks. Rather than trying to handle requests directly and only delegating when necessary, this orchestrator is instructed to always prefer delegation when a relevant tool exists. This architectural choice has important implications: it ensures that specialist expertise is consistently utilized, reduces the risk of the orchestrator providing suboptimal answers when better tools are available, and creates a clear separation of concerns where the orchestrator focuses on routing rather than execution.

The prompt explicitly states "do not answer yourself" when tools are available, establishing a strong bias toward delegation. This is particularly valuable in production systems where you want to ensure that domain-specific tasks are always handled by the appropriate specialists. The "when unsure, prefer delegation" guidance handles edge cases where the orchestrator might be uncertain whether a task requires specialist help — in such cases, it's safer to delegate and let the specialist determine if it can help rather than risk providing an inadequate direct response.

Notice how we pass the calculator tool to the orchestrator just like any other tool — from the orchestrator's perspective, calling another agent is no different from calling a mathematical function.

Testing General Knowledge Questions

Let's test our orchestrated system with a general knowledge question to see how the orchestrator handles tasks that don't require specialist agents.

# Create a message list with a general knowledge question
messages = [
    {
        'role': 'user', 
        'content': 'What is the capital of France?'
    }
]

# Run the orchestrator agent
result_messages, response = helpful_assistant.run(messages)

# Display the orchestrator's response to the user
print(response)

When we run this code, the orchestrator agent will receive the question and analyze whether it needs specialist assistance. With our strict delegation-first prompt, the agent has a lower threshold for attempting delegation compared to a more permissive system prompt. However, for obvious general knowledge questions like this one, where no relevant specialist tool exists, the orchestrator should still recognize that it can provide the answer directly.

Here's what happens when we execute this request:

Paris.

The orchestrator correctly identified this as a question it could handle directly since no relevant specialist tool exists for general geography questions. While the delegation-first prompt creates a bias toward using tools, the agent still recognizes when direct responses are appropriate. The response was immediate and concise, demonstrating that the system remains efficient for simple tasks that genuinely don't require specialist assistance.

It's worth noting that with a stricter delegation prompt, you may occasionally see the orchestrator attempt to delegate questions that seem obviously outside the specialist's domain. This is a trade-off: the system prioritizes ensuring specialist expertise is used when available, even if it means occasionally making unnecessary delegation attempts. In production systems, this bias toward delegation is often preferable to the risk of providing suboptimal direct answers when better tools exist.

Testing Mathematical Problem Delegation

Now let's test the system with a mathematical question that requires the specialist agent's expertise.

# Create a message list with a math problem
messages = [
    {
        'role': 'user', 
        'content': 'What is the solution to the equation x² - 5x + 6 = 0?'
    }
]

# Run the orchestrator agent
result_messages, response = helpful_assistant.run(messages)

# Display the orchestrator's response to the user
print('\n=== Final Response ===\n')
print(response)

In this example, the orchestrator will recognize that solving a quadratic equation requires mathematical expertise. With our delegation-first prompt, the agent should immediately decide to delegate this task to the calculator assistant tool, which will then use its mathematical functions to solve the equation step by step.

Here's the complete execution trace showing the delegation and calculation process:

🔧 Tool called: calculator_assistant_agent({'message': 'Solve the quadratic equation: x^2 - 5x + 6 = 0'})
🦾 Agent tool called (calculator_assistant): Solve the quadratic equation: x^2 - 5x + 6 = 0
🔧 Tool called: power({'base': -5, 'exponent': 2})
🔧 Tool called: multiply_numbers({'a': 1, 'b': 6})
🔧 Tool called: multiply_numbers({'a': 4, 'b': 1})
🔧 Tool called: multiply_numbers({'a': -1, 'b': -5})
🔧 Tool called: power({'base': -5, 'exponent': 2})
🔧 Tool called: power({'base': -5, 'exponent': 2})
🔧 Tool called: multiply_numbers({'a': 4, 'b': 6})
🔧 Tool called: power({'base': -5, 'exponent': 2})
🔧 Tool called: multiply_numbers({'a': 4, 'b': 6})
🔧 Tool called: power({'base': -5, 'exponent': 2})
🔧 Tool called: multiply_numbers({'a': 4, 'b': 6})
🔧 Tool called: power({'base': -5, 'exponent': 2})
🔧 Tool called: multiply_numbers({'a': 4, 'b': 6})
📊 Agent response (calculator_assistant): x = 2 or x = 3.

=== Final Response ===

The solutions are x = 2 and x = 3.

For the mathematical equation, the orchestrator immediately recognized that this required mathematical expertise and delegated the task to the calculator assistant. The debug output shows the agent tool being called with the original question, followed by the calculator assistant processing the request using multiple mathematical tools to solve the quadratic equation. The calculator assistant provided the solution "x = 2 or x = 3," which the orchestrator then received and presented to the user as the final answer.

This demonstrates the full delegation and response cycle: the orchestrator identified the need for specialist help, called the appropriate agent tool, and delivered the specialist's result to the user. The delegation-first approach ensured that the mathematical problem was handled by the agent with the appropriate tools and expertise, rather than the orchestrator attempting to solve it directly.

Summary & Exercises

You've now learned how to build agent orchestration systems in which a central planner can dynamically delegate tasks to specialist agents. This architectural pattern offers significant advantages over fixed pipelines: it's more efficient for simple tasks, more flexible for complex problems, and allows you to build increasingly sophisticated specialist agents without complicating the overall system design.

The delegation-first approach you've implemented creates a clear separation of concerns: the orchestrator focuses on routing decisions while specialists focus on execution within their domains. This pattern scales well as you add more specialists — the orchestrator simply gains access to more tools without needing to understand the internal workings of each specialist.

In your upcoming practice exercises, you'll build your own orchestrated agent systems with multiple specialists for different domains. This foundation will enable you to create much more sophisticated and flexible agentic systems that can adapt their approach based on the specific requirements of each task.

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal