Multi-Agent Orchestration

View as MarkdownOpen in Claude

A single agent can only do so much. For complex use cases, connect multiple specialized agents. Each agent focuses on what it does best, and orchestration coordinates them.

Why Multiple Agents?

ApproachProsCons
Single agentSimple, less latencyPrompt becomes too complex
Multiple agentsFocused prompts, easier to maintainMore coordination needed

Use multiple agents when:

  • Different tasks require different prompts or tools
  • You need to route users to specialists
  • Workflows have distinct phases

The Router Pattern

A lightweight router node analyzes intent and directs traffic:

Implementation

1from smallestai.atoms.agent.nodes import Node, OutputAgentNode
2
3
4class RouterNode(Node):
5 """Routes conversations to the appropriate specialist."""
6
7 def __init__(self, sales_agent, support_agent, billing_agent):
8 super().__init__(name="router")
9 self.sales = sales_agent
10 self.support = support_agent
11 self.billing = billing_agent
12
13 async def process_event(self, event):
14 # Simple keyword-based routing
15 if hasattr(event, "content"):
16 content = event.content.lower()
17
18 if any(word in content for word in ["buy", "price", "demo"]):
19 event.metadata["route"] = "sales"
20 elif any(word in content for word in ["broken", "help", "issue"]):
21 event.metadata["route"] = "support"
22 elif any(word in content for word in ["invoice", "payment", "bill"]):
23 event.metadata["route"] = "billing"
24 else:
25 event.metadata["route"] = "support" # Default
26
27 await self.send_event(event)
28
29
30class SalesAgent(OutputAgentNode):
31 """Handles sales inquiries."""
32
33 def __init__(self):
34 super().__init__(name="sales-agent")
35
36 async def process_event(self, event):
37 if event.metadata.get("route") == "sales":
38 await super().process_event(event)
39 else:
40 await self.send_event(event)
41
42
43class SupportAgent(OutputAgentNode):
44 """Handles support requests."""
45
46 def __init__(self):
47 super().__init__(name="support-agent")
48
49 async def process_event(self, event):
50 if event.metadata.get("route") == "support":
51 await super().process_event(event)
52 else:
53 await self.send_event(event)

Setting Up the Graph

1async def setup(session: AgentSession):
2 # Create agents
3 sales = SalesAgent()
4 support = SupportAgent()
5 billing = BillingAgent()
6 router = RouterNode(sales, support, billing)
7
8 # Add to session
9 session.add_node(router)
10 session.add_node(sales)
11 session.add_node(support)
12 session.add_node(billing)
13
14 # Connect the graph
15 session.add_edge(router, sales)
16 session.add_edge(router, support)
17 session.add_edge(router, billing)
18
19 await session.start()
20 await session.wait_until_complete()

The Handoff Pattern

An agent explicitly transfers control when it reaches its limits:

Implementation

1class GreeterAgent(OutputAgentNode):
2 """Greets users and hands off to scheduler."""
3
4 async def generate_response(self):
5 # Get last user message
6 user_messages = [m for m in self.context.messages if m["role"] == "user"]
7 user_message = user_messages[-1]["content"] if user_messages else ""
8
9 if "appointment" in user_message.lower():
10 # Trigger handoff
11 await self.send_event(HandoffEvent(
12 target="scheduler",
13 reason="User wants to schedule",
14 context={"user_name": self.user_name}
15 ))
16 yield "Let me transfer you to our scheduling assistant."
17 return
18
19 # Normal greeting
20 yield "Hello! How can I help you today?"
21
22
23class SchedulerAgent(OutputAgentNode):
24 """Handles appointment scheduling."""
25
26 async def process_event(self, event):
27 if isinstance(event, HandoffEvent) and event.target == "scheduler":
28 # Received handoff, take over
29 self.handoff_context = event.context
30
31 await super().process_event(event)

Shared Context

When handing off, pass relevant context:

1class HandoffEvent(SDKEvent):
2 """Custom event for agent handoffs."""
3 type: str = "internal.handoff"
4 target: str # Target agent name
5 reason: str # Why the handoff is happening
6 context: dict = {} # Shared data
7
8
9class PrequalAgent(OutputAgentNode):
10 def __init__(self):
11 super().__init__(name="prequal-agent")
12 self.collected_data = {}
13
14 async def generate_response(self):
15 # Check if we have all needed info
16 if self._qualification_complete():
17 await self.send_event(HandoffEvent(
18 target="closer",
19 reason="Qualification complete",
20 context={
21 "lead_name": self.collected_data["name"],
22 "company": self.collected_data["company"],
23 "budget": self.collected_data["budget"],
24 "timeline": self.collected_data["timeline"]
25 }
26 ))
27 yield "Great! Let me connect you with our sales team."
28 return
29
30 # Continue qualification...

LLM-Based Routing

For complex routing, use an LLM:

1from smallestai.atoms.agent.nodes import Node
2from smallestai.atoms.agent.clients.openai import OpenAIClient
3
4class SmartRouterNode(Node):
5 def __init__(self):
6 super().__init__(name="smart-router")
7 self.llm = OpenAIClient(model="gpt-4o-mini")
8
9 async def process_event(self, event):
10 if not hasattr(event, "content"):
11 await self.send_event(event)
12 return
13
14 # Ask LLM to classify
15 response = await self.llm.chat(
16 messages=[
17 {
18 "role": "system",
19 "content": """Classify the user intent. Respond with one word:
20- SALES: pricing, demos, purchasing
21- SUPPORT: issues, bugs, help
22- BILLING: invoices, payments, refunds
23- GENERAL: everything else"""
24 },
25 {"role": "user", "content": event.content}
26 ]
27 )
28
29 intent = response.content.strip().upper()
30 event.metadata["route"] = intent.lower()
31
32 await self.send_event(event)

Fallback Agents

Add a fallback for unhandled cases:

1class FallbackAgent(OutputAgentNode):
2 """Handles requests that don't match any specialist."""
3
4 async def process_event(self, event):
5 route = event.metadata.get("route")
6
7 # Only handle if no other agent claimed it
8 if route == "general" or route is None:
9 await super().process_event(event)
10 else:
11 await self.send_event(event)

Monitoring Handoffs

Log handoffs for debugging:

1class LoggingRouterNode(Node):
2 async def process_event(self, event):
3 route = self._determine_route(event)
4 event.metadata["route"] = route
5
6 logger.info(
7 f"Routing to {route}",
8 extra={
9 "event_type": event.type,
10 "route": route,
11 "session_id": self.session_id
12 }
13 )
14
15 await self.send_event(event)

Tips

Simple keyword matching covers most cases. Add LLM routing only if needed.

Include what the previous agent learned so users don’t repeat themselves.

Route to a general agent if no specialist matches. Never leave users hanging.