Tool Orchestration¶
Overview¶
Tool orchestration—how agents discover, invoke, and coordinate multiple tools—is a critical architectural decision that fundamentally differs between deepagents and Claude SDK.
Tool Orchestration Philosophy¶
Deepagents: Custom Orchestration¶
Philosophy: "Build exactly the orchestration logic your domain needs"
Key Principle: Maximum flexibility to implement domain-specific coordination patterns
Claude SDK: Protocol-Based Orchestration¶
Philosophy: "Standardized tool protocol enables managed orchestration"
Key Principle: Consistent, safe tool invocation through Model Context Protocol (MCP)
Tool Definition & Discovery¶
Deepagents Approach¶
Dynamic Tool Creation¶
# Define tools as Python functions
@tool
def get_site_devices(site_name: str) -> List[dict]:
"""Get all devices at a specific site."""
return netbox_client.dcim.devices.filter(site=site_name)
@tool
def analyze_vlan_usage(site_name: str) -> dict:
"""Analyze VLAN utilization across site."""
devices = get_site_devices(site_name)
vlans = [extract_vlans(d) for d in devices]
return calculate_utilization(vlans)
# Tool composition
@tool
def site_health_check(site_name: str) -> dict:
"""Complete site health analysis."""
devices = get_site_devices(site_name)
vlans = analyze_vlan_usage(site_name)
# Custom orchestration logic
return {
"device_count": len(devices),
"vlan_utilization": vlans,
"health_score": calculate_health(devices, vlans)
}
Characteristics: - Unlimited tool complexity - Direct Python function calls - Custom composition patterns - No protocol constraints
Tool Registration¶
# Register tools with agent
agent = await async_create_deep_agent(
name="NetBox Analyst",
tools=[
get_site_devices,
analyze_vlan_usage,
site_health_check,
# Can add 100+ custom tools
]
)
# Or dynamic discovery
tools = discover_domain_tools(module="netbox_tools")
agent = await async_create_deep_agent(name="NetBox Analyst", tools=tools)
Benefits: - Add tools without external processes - Tool composition at Python level - Instant tool availability - Full control over tool lifecycle
Claude SDK Approach¶
Model Context Protocol (MCP)¶
# Tools defined in separate MCP server process
# mcp_netbox/server.py
@mcp.tool()
async def netbox_get_objects(
object_type: str,
filters: Optional[dict] = None,
fields: Optional[List[str]] = None,
limit: int = 50
) -> dict:
"""Generic tool for querying NetBox objects."""
# Tool schema automatically generated
# Input validation handled by MCP
# Output serialization managed by protocol
return await netbox_client.get_objects(
object_type=object_type,
filters=filters or {},
fields=fields,
limit=limit
)
Tool Discovery via MCP¶
# SDK automatically discovers MCP tools
mcp_servers = {
"netbox": {
"command": "python",
"args": ["-m", "mcp_netbox"],
"env": {"NETBOX_URL": url, "NETBOX_TOKEN": token}
}
}
options = ClaudeAgentOptions(
mcp_servers=mcp_servers,
allowed_tools=[
"netbox_get_objects",
"netbox_get_object_by_id",
"netbox_search_objects"
]
)
# Tools discovered at runtime via MCP protocol
agent = ChatAgent(options)
await agent.start_session() # Tool discovery happens here
Characteristics: - Standardized tool interface - Separate process for tools - Schema-driven validation - Managed lifecycle
Tool Coordination Patterns¶
Deepagents: Explicit Coordination¶
Sequential Coordination¶
# Explicit multi-step orchestration
class NetBoxWorkflow:
async def analyze_infrastructure(self, site: str):
# Step 1: Get foundational data
devices = await self.get_site_devices(site)
# Step 2: Analyze based on step 1 results
vlans = await self.analyze_vlans(devices)
# Step 3: Cross-reference data
circuits = await self.get_site_circuits(site)
# Step 4: Correlation logic
return self.correlate_infrastructure(devices, vlans, circuits)
Parallel Coordination¶
# Custom parallel execution
class ParallelNetBoxAgent:
async def multi_site_analysis(self, sites: List[str]):
# Launch parallel tool calls
tasks = [
self.analyze_infrastructure(site)
for site in sites
]
# Gather results
results = await asyncio.gather(*tasks)
# Custom aggregation
return self.aggregate_site_analyses(results)
Conditional Coordination¶
# Dynamic tool selection based on conditions
class AdaptiveNetBoxAgent:
async def smart_query(self, query: str):
# Analyze query intent
intent = await self.classify_intent(query)
if intent == "single_object":
return await self.get_object_by_id(extract_id(query))
elif intent == "filtered_list":
filters = await self.extract_filters(query)
return await self.get_objects_filtered(filters)
elif intent == "complex_analysis":
# Multi-step workflow
return await self.complex_analysis_workflow(query)
Benefits: - Complete control over execution order - Custom error handling per step - Domain-specific optimization - Transparent orchestration logic
Claude SDK: LLM-Driven Coordination¶
Autonomous Tool Selection¶
# LLM decides which tools to call and when
async def query(self, message: str) -> AsyncIterator[StreamChunk]:
"""
SDK + LLM handle orchestration automatically.
User: "Show all devices at site DM-Scranton with their IP addresses"
LLM reasoning (internal):
1. Need to query devices with site filter
2. Tool: netbox_get_objects(object_type="devices", filters={"site": "DM-Scranton"})
3. Results include device IDs
4. Need IP addresses? Already in results
5. Format and return
"""
await self.client.query(message)
async for response in self.client.receive_response():
# SDK streams tool usage and results
yield response
Characteristics: - LLM chooses tools dynamically - Automatic multi-step planning - Error recovery by LLM - No explicit orchestration code
Hook-Based Observation¶
# Observe orchestration via hooks
options = ClaudeAgentOptions(
hooks={
"pre_tool_use": lambda tool, args: log_tool_call(tool, args),
"post_tool_use": lambda tool, result: validate_result(tool, result)
}
)
# Orchestration visible through hooks, but not controllable
Benefits: - No orchestration code to maintain - LLM adapts to novel queries - Handles unexpected scenarios - Simpler codebase
Trade-offs: - Less control over execution order - Can't optimize specific patterns - LLM may make suboptimal choices - Harder to guarantee behavior
Tool Performance Optimization¶
Deepagents: Custom Optimization¶
Batching Pattern¶
# Custom batching for efficiency
class BatchedNetBoxAgent:
async def get_multiple_objects(self, object_ids: List[int]):
# Custom batching logic
batch_size = 50
batches = [
object_ids[i:i + batch_size]
for i in range(0, len(object_ids), batch_size)
]
# Parallel batch requests
results = []
for batch in batches:
batch_results = await self.netbox_batch_get(batch)
results.extend(batch_results)
return results
Caching Pattern¶
# Custom caching strategy
class CachedNetBoxAgent:
def __init__(self):
self.cache = TTLCache(maxsize=1000, ttl=300) # 5-minute TTL
async def get_site_devices(self, site: str):
cache_key = f"devices:{site}"
if cache_key in self.cache:
logger.info(f"Cache hit: {cache_key}")
return self.cache[cache_key]
# Cache miss
devices = await self.netbox_client.get_devices(site=site)
self.cache[cache_key] = devices
return devices
Benefits: - Optimize for specific access patterns - Fine-grained cache control - Custom invalidation strategies - Domain-specific batching
Claude SDK: MCP-Level Optimization¶
Server-Side Caching¶
# Caching in MCP server (transparent to agent)
class NetBoxMCPServer:
def __init__(self):
self.cache = TTLCache(maxsize=1000, ttl=60)
@mcp.tool()
async def netbox_get_objects(self, object_type: str, filters: dict):
cache_key = self.make_cache_key(object_type, filters)
if cache_key in self.cache:
return self.cache[cache_key]
result = await self.fetch_from_netbox(object_type, filters)
self.cache[cache_key] = result
return result
Characteristics: - Caching transparent to agent - Server manages cache lifecycle - Agent code remains simple - Consistent across all agents using server
Prompt Caching¶
# Anthropic's prompt caching (automatic)
options = ClaudeAgentOptions(
system_prompt={...}, # Automatically cached
mcp_servers={...} # Tool schemas cached
)
# 70% cost reduction on repeated system prompts
# No code changes required
Benefits: - Automatic optimization - No agent-level complexity - Provider-managed efficiency - Cost reduction without code changes
Tool Error Handling¶
Deepagents: Custom Error Recovery¶
# Complete control over error handling
class RobustNetBoxAgent:
async def resilient_query(self, object_type: str, filters: dict):
max_retries = 3
for attempt in range(max_retries):
try:
return await self.netbox_get_objects(object_type, filters)
except RateLimitError:
# Custom backoff strategy
wait_time = 2 ** attempt
await asyncio.sleep(wait_time)
except NetworkError as e:
if attempt == max_retries - 1:
# Final attempt: try fallback
return await self.fallback_query(object_type, filters)
else:
logger.warning(f"Network error, retrying: {e}")
except ValidationError:
# Fix validation issues programmatically
filters = self.sanitize_filters(filters)
return await self.netbox_get_objects(object_type, filters)
raise MaxRetriesExceeded()
Claude SDK: Hook-Based Error Handling¶
# Error handling via hooks
def handle_tool_error(tool_name: str, error: Exception) -> dict:
"""Custom error handling logic."""
if isinstance(error, RateLimitError):
# Return error context to LLM
return {
"error": "rate_limit",
"message": "NetBox API rate limit exceeded",
"suggestion": "Reduce query scope or wait 60 seconds"
}
elif isinstance(error, NotFoundError):
return {
"error": "not_found",
"message": f"Object not found in NetBox",
"suggestion": "Check object type and ID"
}
# Default handling
raise error
options = ClaudeAgentOptions(
hooks={"error": handle_tool_error}
)
Characteristics: - LLM receives error context - LLM decides recovery strategy - Less deterministic recovery - Simpler error handling code
Comparison Matrix¶
| Aspect | Deepagents | Claude SDK |
|---|---|---|
| Tool Definition | Python functions | MCP protocol |
| Tool Discovery | Python imports | Runtime MCP discovery |
| Orchestration Control | Explicit code | LLM-driven |
| Execution Order | Deterministic | LLM-determined |
| Parallel Execution | Custom async code | LLM may parallelize |
| Caching | Custom implementation | MCP server + prompt cache |
| Error Handling | Custom retry logic | Hook-based + LLM recovery |
| Performance Tuning | Fine-grained control | Server-level optimization |
| Debugging | Standard Python debugging | Hook-based observation |
Recommendations¶
Choose Deepagents orchestration when: - Need deterministic tool execution order - Require custom coordination patterns - Performance optimization critical - Domain-specific batching needed
Choose Claude SDK orchestration when: - Standard tool coordination sufficient - Prefer LLM adaptability - Want minimal orchestration code - Value managed optimization
Conclusion¶
Tool orchestration philosophies differ fundamentally:
- Deepagents: Explicit, controlled, optimized
- Claude SDK: Implicit, adaptive, managed
Both approaches successfully handle complex multi-tool workflows—the choice depends on your need for control vs convenience.
See Selection Guide for choosing the right approach for your use case.