Skip to main content
Agent composition allows you to use agents as tools within other agents. This enables building complex multi-agent systems where specialized agents handle specific tasks.

Why Compose Agents?

  • Separation of concerns: Each agent focuses on one domain
  • Reusability: The same agent can be used in multiple contexts
  • Hierarchical reasoning: Coordinator agents can delegate to specialists
  • Scalability: Complex tasks are broken into manageable pieces

Basic Composition

Use the as_tool() method to convert an agent into a tool:
from opper_agents import Agent, tool

# Specialist agent for math
@tool
def calculate(expression: str) -> float:
    """Evaluate a math expression."""
    return eval(expression)

math_agent = Agent(
    name="MathAgent",
    description="Solves mathematical problems",
    tools=[calculate]
)

# Specialist agent for research
@tool
def search(query: str) -> str:
    """Search for information."""
    return f"Results for: {query}"

research_agent = Agent(
    name="ResearchAgent",
    description="Researches topics and provides information",
    tools=[search]
)

# Coordinator that uses both specialists
coordinator = Agent(
    name="Coordinator",
    description="Coordinates specialists to solve complex problems",
    tools=[
        math_agent.as_tool(),
        research_agent.as_tool()
    ]
)

# The coordinator can now delegate to specialists
result = await coordinator.process(
    "What is the population of France, and what is 15% of that number?"
)

How It Works

When an agent is used as a tool:
1

Parent calls child agent

The coordinator receives a task like “population of France, calculate 15% of it” and decides to delegate to research_agent.
2

Child agent runs its loop

The research agent executes its own think-act loop, searches for information, and returns “67 million”.
3

Results flow back

The coordinator receives the result and decides to call math_agent with “calculate 15% of 67M”.
4

Parent continues

After receiving “10.05 million” from the math agent, the coordinator synthesizes the final answer.

Custom Tool Names

Customize how the agent appears as a tool:
# Default: uses agent name and description
math_tool = math_agent.as_tool()

# Custom name and description
math_tool = math_agent.as_tool(
    name="calculator",
    description="Use this for any mathematical calculations"
)

Tracing Nested Agents

Parent spans automatically connect to child spans:
Coordinator Execution
├── Think: "I need to research first"
├── Tool: research_agent
│   └── ResearchAgent Execution
│       ├── Think: "I'll search for this"
│       ├── Tool: search
│       └── Return result
├── Think: "Now I need to calculate"
├── Tool: math_agent
│   └── MathAgent Execution
│       ├── Think: "I'll calculate this"
│       ├── Tool: calculate
│       └── Return result
└── Return final answer

Multi-Level Hierarchies

Agents can be nested multiple levels deep:
# Level 1: Specialist tools
@tool
def web_search(query: str) -> str:
    return f"Web results for: {query}"

@tool
def database_query(sql: str) -> str:
    return f"DB results for: {sql}"

# Level 2: Domain agents
web_researcher = Agent(
    name="WebResearcher",
    description="Searches the web",
    tools=[web_search]
)

db_analyst = Agent(
    name="DBAnalyst",
    description="Queries databases",
    tools=[database_query]
)

# Level 3: Research coordinator
research_lead = Agent(
    name="ResearchLead",
    description="Coordinates research across sources",
    tools=[
        web_researcher.as_tool(),
        db_analyst.as_tool()
    ]
)

# Level 4: Executive assistant
executive_assistant = Agent(
    name="ExecutiveAssistant",
    description="Handles complex executive requests",
    tools=[
        research_lead.as_tool(),
        # ... other high-level tools
    ]
)

Parallel Agent Execution

When a coordinator calls multiple agents, they can execute in parallel:
# If the coordinator decides to call both agents at once,
# they run concurrently
coordinator = Agent(
    name="Coordinator",
    tools=[
        agent_a.as_tool(),  # Can run in parallel
        agent_b.as_tool(),  # with this
        agent_c.as_tool()   # and this
    ]
)

Best Practices

  1. Clear descriptions: Help the parent agent know when to use each child
  2. Single responsibility: Each agent should do one thing well
  3. Appropriate depth: Too many levels adds latency and token cost
  4. Shared context: Pass relevant context through input, not globals
  5. Error handling: Child agent errors bubble up; handle gracefully