Agent Delegation with Atlas
As AI features grow in complexity, you often need more than one agent. A support agent for customer issues, a billing agent for account questions, maybe an agent who handles specialized tool calls. The challenge isn't building individual agents; it's getting them to work together.
I've seen a few common approaches to this. The first is hardcoding which agent handles what, usually with a bunch of if-else logic that becomes a maintenance nightmare. The second is building a custom routing layer that tries to parse user intent and dispatch accordingly.
Atlas takes a different approach: let the AI agent figure out which other agent to use. The delegation happens through tools, which means the orchestrator agent can reason about its options the same way it reasons about any other capability.
The Delegation Pattern
The idea is simple. You create an orchestrator agent that has access to two tools: one that lists available agents, and another that delegates tasks to them. The AI sees what agents exist, reads their descriptions, and decides where to route the request.
Here's what that looks like in practice.
ListAgentsTool exposes your registered agents to the AI:
use Atlasphp\Atlas\Tools\ToolDefinition;
use Atlasphp\Atlas\Tools\Support\ToolContext;
use Atlasphp\Atlas\Tools\Support\ToolResult;
use Atlasphp\Atlas\Atlas;
class ListAgentsTool extends ToolDefinition
{
public function name(): string
{
return 'list_agents';
}
public function description(): string
{
return 'Lists all available agents that can handle specialized tasks. Call this to see what agents are available before delegating.';
}
public function parameters(): array
{
return [];
}
public function handle(array $params, ToolContext $context): ToolResult
{
$currentAgent = $context->getMeta('current_agent');
$agents = [];
foreach (Atlas::getAgentRegistry()->all() as $key => $definition) {
// Don't include yourself in the list
if ($key === $currentAgent) {
continue;
}
$agents[] = [
'key' => $key,
'name' => $definition->name(),
'description' => $definition->description(),
];
}
return ToolResult::json($agents);
}
}DelegateToAgentTool handles the actual handoff:
use Atlasphp\Atlas\Tools\ToolDefinition;
use Atlasphp\Atlas\Tools\Support\ToolContext;
use Atlasphp\Atlas\Tools\Support\ToolParameter;
use Atlasphp\Atlas\Tools\Support\ToolResult;
use Atlasphp\Atlas\Atlas;
class DelegateToAgentTool extends ToolDefinition
{
public function name(): string
{
return 'delegate_to_agent';
}
public function description(): string
{
return 'Delegate a task to a specialized agent. Use list_agents first to see available options.';
}
public function parameters(): array
{
return [
ToolParameter::string('agent_key', 'The key of the agent to delegate to', required: true),
ToolParameter::string('task', 'The task or question to send to the agent', required: true),
];
}
public function handle(array $params, ToolContext $context): ToolResult
{
$agentKey = $params['agent_key'];
$task = $params['task'];
// Verify the agent exists
if (! Atlas::getAgentRegistry()->has($agentKey)) {
return ToolResult::error("Agent '{$agentKey}' not found");
}
// Prevent self-delegation loops
if ($agentKey === $context->getMeta('current_agent')) {
return ToolResult::error('Cannot delegate to yourself');
}
// Pass along relevant context
$response = Atlas::agent($agentKey)
->withMetadata([
'user_id' => $context->getMeta('user_id'),
'session_id' => $context->getMeta('session_id'),
])
->chat($task);
return ToolResult::text($response->text());
}
}Building the Orchestrator
Now you need an agent that uses these tools. The orchestrator doesn't handle tasks directly; it figures out where they should go.
use Atlasphp\Atlas\Agents\AgentDefinition;
class OrchestratorAgent extends AgentDefinition
{
public function name(): string
{
return 'Orchestrator';
}
public function description(): string
{
return 'Routes requests to the appropriate specialized agent';
}
public function systemPrompt(): ?string
{
return <<<PROMPT
You are a friendly support assistant for {company_name}. Your job is to
understand what the user needs and delegate to the right specialist.
Always call list_agents first to see what's available, then use
delegate_to_agent to hand off the task.
When you receive a response from a specialist:
- Present it naturally in your own voice, don't just echo their reply
- Keep the tone helpful and conversational
- If the response needs clarification, ask a follow-up question
- Never mention that you delegated or that other agents exist
The user should feel like they're talking to one assistant, not a system.
PROMPT;
}
public function tools(): array
{
return [
ListAgentsTool::class,
DelegateToAgentTool::class,
];
}
}A Real-World Example
Let's say you have a customer-facing support system with three specialized agents: one for order questions, one for billing, and one for product information.
// Register your specialized agents
Atlas::registerAgent('orders', OrderAgent::class);
Atlas::registerAgent('billing', BillingAgent::class);
Atlas::registerAgent('products', ProductAgent::class);
Atlas::registerAgent('orchestrator', OrchestratorAgent::class);When a user asks a question, you route it through the orchestrator:
$response = Atlas::agent('orchestrator')
->withMessages($pastMessages)
->withMetadata([
'user_id' => auth()->id(),
'session_id' => session()->id(),
'current_agent' => 'orchestrator',
])
->chat('I need to return the shoes I ordered last week');
echo $response->text();
// The orchestrator calls list_agents, sees the options, and delegates
// to the orders agent which handles the return request.The flow looks like this:
- User message hits the orchestrator
- Orchestrator calls
list_agentsand sees: orders, billing, products - AI decides this is an order-related question
- Orchestrator calls
delegate_to_agentwithagent_key: orders - OrderAgent processes the request with its own tools (maybe a
lookup_ordertool, aninitiate_returntool) - Response bubbles back through the orchestrator to the user
Why Tools Instead of Hardcoded Routing
The key insight is that the AI is already good at understanding intent. Instead of writing rules to classify requests, you let the model read agent descriptions and make the call.
This has a few benefits. Adding a new agent is just registering it. No routing logic to update. The orchestrator automatically learns about it through list_agents. You can also add agents dynamically based on context. Maybe premium users get access to a priority support agent that others don't see.
It also handles edge cases naturally. If a user asks something that spans multiple domains ("I want to return my order and update my payment method"), the orchestrator can make multiple delegation calls in sequence.
A Few Things to Watch
The orchestrator adds a hop. Every request goes through it first, which means extra latency and token usage. For high-volume applications, you might want the orchestrator to use a faster model (like gpt-4o-mini) since it's just making routing decisions.
Debugging gets easier if you log the delegation chain. When something goes wrong, you want to know which agent handled it and what task was passed. The metadata context is a good place to track this - add a delegated_from key so each agent knows who called it.
Atlas is open source and available on GitHub. Full documentation at atlasphp.org.
