Agent-as-Tool Pattern Guide
The Agent-as-Tool pattern enables multi-agent collaboration where specialized agents work behind the scenes to help each other, without the user knowing multiple agents are involved.
When to Use This Pattern
Use Agent-as-Tool when you need:
- Specialized Processing: Different agents excel at different tasks
- Behind-the-Scenes Coordination: Agents collaborate invisibly to the user
- Multi-step Workflows: Complex processes requiring different expertise
- Modular Architecture: Clean separation of concerns between agents
Core Concept
Handoffs vs Agent-as-Tool
Handoffs: “Let me transfer you to billing”
- User-visible conversation transfer
- Full context shared
- Agent takes over conversation
Agent-as-Tool: “Let me check that for you” (uses billing agent internally)
- Invisible to user
- Limited context (state only)
- Returns control to caller
Basic Implementation
Step 1: Create Specialized Agents
# Research agent - finds customer information
research_agent = Agents::Agent.new(
name: "ResearchAgent",
instructions: <<~PROMPT
You research customer information and history.
Return contact details including email addresses.
PROMPT,
tools: [customer_lookup_tool, conversation_search_tool]
)
# Billing agent - handles payment operations
billing_agent = Agents::Agent.new(
name: "BillingAgent",
instructions: <<~PROMPT
You handle billing operations using Stripe.
CRITICAL: You need customer email addresses for billing lookups.
Contact IDs will NOT work.
PROMPT,
tools: [stripe_billing_tool]
)
Step 2: Create Orchestrator with Agent Tools
# Main agent coordinates specialists
orchestrator = Agents::Agent.new(
name: "SupportCopilot",
instructions: <<~PROMPT
You help support agents by coordinating specialist agents.
**CRITICAL: Multi-Step Workflow Approach**
For complex queries, break them into steps and use tools sequentially:
1. Plan your approach: What information do you need?
2. Execute sequentially: Use EXACT results from previous tools
3. Build context progressively: Each tool builds on previous findings
**Tool Requirements:**
- research_customer: Returns contact details including emails
- check_billing: Requires customer email (not contact ID)
Always think: "What did I learn and how do I use it next?"
PROMPT,
tools: [
research_agent.as_tool(
name: "research_customer",
description: "Research customer details. Returns contact info including email."
),
billing_agent.as_tool(
name: "check_billing",
description: "Check billing status. Requires customer email address."
)
]
)
Step 3: Use with Context Persistence
# Set up runner with context persistence
runner = Agents::Runner.with_agents(orchestrator)
context = {}
# Interactive loop maintains context
loop do
user_input = gets.chomp
break if user_input == "exit"
# Pass and update context each turn
result = runner.run(user_input, context: context)
context = result.context if result.context
puts result.output
end
Advanced Features
Custom Output Extraction
Extract specific information for other tools:
research_agent.as_tool(
name: "get_customer_email",
description: "Get customer email address",
output_extractor: ->(result) {
# Extract just the email instead of full response
email_match = result.output.match(/Email:\s*([^\s]+)/i)
email_match&.captures&.first || "Email not found"
}
)
Best Practices
1. Clear Tool Descriptions with Requirements
Specify what each tool needs and provides:
# Good: Clear requirements
billing_agent.as_tool(
name: "check_stripe_billing",
description: "Check Stripe billing info. Requires customer email (not contact ID)."
)
research_agent.as_tool(
name: "research_customer",
description: "Research customer details. Returns email address and contact info."
)
# Avoid: Vague descriptions
agent.as_tool(name: "process", description: "Do stuff")
2. Multi-Step Workflow Instructions
Guide orchestrators to chain tool calls properly:
orchestrator = Agent.new(
instructions: <<~PROMPT
**For complex queries requiring multiple pieces of information:**
1. Plan what information you need to gather
2. Use tools sequentially, building on previous results
3. Extract specific values from tool outputs for subsequent calls
4. Don't pass original parameters - use discovered values
**Example:** To check billing for CONTACT-123:
Step 1: research_customer("Get details for CONTACT-123") → finds email
Step 2: check_billing("Check billing for [discovered email]") → not original ID
PROMPT
)
3. Explicit Parameter Requirements in Agent Instructions
Make tool parameter needs crystal clear:
billing_agent = Agent.new(
instructions: <<~PROMPT
**CRITICAL: Billing Requirements**
- Stripe billing lookups REQUIRE customer email addresses
- Contact IDs, names, phone numbers will NOT work
- If you don't have email, clearly state you need it
PROMPT
)
4. Handle Errors with Guidance
Provide helpful error messages that guide next steps:
# In orchestrator instructions
instructions = <<~PROMPT
**Error Handling:**
- If billing fails due to missing email: Use research_customer first
- If contact not found: Ask for more identifying information
- Always provide helpful responses even if tools fail
PROMPT
5. Context Persistence for Multi-Turn Conversations
Maintain state across conversation turns:
# Maintain context between interactions
runner = Agents::Runner.with_agents(orchestrator)
context = {}
# Each turn builds on previous context
result = runner.run(user_input, context: context)
context = result.context if result.context
6. Design Focused Agents
Keep agent responsibilities clear and narrow:
# Good: Focused responsibility
customer_agent = Agent.new(
name: "CustomerAgent",
instructions: "Handle customer data lookups and history research"
)
# Avoid: Too broad
everything_agent = Agent.new(
name: "EverythingAgent",
instructions: "Handle all customer operations, billing, support, and analysis"
)