Skip to main content

Overview

Agents as Tools exposes child agents as callable tools to a parent agent, enabling the parent’s LLM to naturally route, parallelize, and orchestrate work across specialized agents through function calling. This pattern combines the simplicity of tool calling with the power of multi-agent coordination.
Inspired by OpenAI’s Agents SDK Agents as Tools feature, with enhancements for parallel execution and detached instances.

Key Features

  • Natural Composition: Child agents exposed as tools the LLM can call
  • Parallel Execution: Multiple child agents run concurrently automatically
  • Detached Instances: Each tool call spawns an independent clone
  • Hybrid Capability: Parent agent can use both MCP tools and agent-tools
  • Three Patterns in One: Routing, parallelization, and orchestration through instruction

Basic Usage

import asyncio
from fast_agent import FastAgent

fast = FastAgent("Agents-as-Tools simple demo")

@fast.agent(
    name="NY-Project-Manager",
    instruction="Return NY time + timezone, plus a one-line project status.",
    servers=["time"],
)
@fast.agent(
    name="London-Project-Manager",
    instruction="Return London time + timezone, plus a one-line news update.",
    servers=["time"],
)
@fast.agent(
    name="PMO-orchestrator",
    instruction=(
        "Get reports. Always use one tool call per project/news. "  # parallelization
        "Responsibilities: NY projects: [OpenAI, Fast-Agent, Anthropic]. "
        "London news: [Economics, Art, Culture]. "  # routing
        "Aggregate results and add a one-line PMO summary."
    ),
    default=True,
    agents=["NY-Project-Manager", "London-Project-Manager"],  # orchestrator-workers
)
async def main() -> None:
    async with fast.run() as agent:
        await agent("Get PMO report. Projects: all. News: Art, Culture")

if __name__ == "__main__":
    asyncio.run(main())

Configuration Parameters

name
string
required
Name of the orchestrator agent
instruction
string
required
Orchestrator instruction describing routing/decomposition/aggregation logic
agents
list[str]
required
List of child agent names exposed as tools: agent__agent1, agent__agent2, etc.
max_parallel
int
default:"128"
Cap on parallel child tool calls (OpenAI API limit is 128)
child_timeout_sec
float
Per-child timeout in seconds (no timeout if not set)
max_display_instances
int
default:"20"
Collapse progress display after showing top-N instances
history_source
HistorySource
default:"none"
Where child instances load history from:
  • none: Empty history (default)
  • child: From template child agent
  • orchestrator: From parent agent
  • messages: From configured message files
history_merge_target
HistoryMergeTarget
default:"none"
Where child instance history merges back to:
  • none: No merge-back (default)
  • child: Merge into template child
  • orchestrator: Merge into parent

Three Patterns in One

Agents as Tools enables three composition patterns through instruction alone:

1. Routing

Choose the right specialist based on request:
@fast.agent(
    name="billing_specialist",
    instruction="Handle billing, payments, and invoice questions",
)
@fast.agent(
    name="technical_support",
    instruction="Resolve technical issues and product questions",
)
@fast.agent(
    name="account_manager",
    instruction="Handle account changes and general inquiries",
)
@fast.agent(
    name="customer_service",
    instruction="Route customer inquiries to the appropriate specialist based on the topic.",
    agents=["billing_specialist", "technical_support", "account_manager"],
)

2. Parallelization

Fan out over independent items:
@fast.agent(
    name="translator",
    instruction="Translate text to the specified language",
)
@fast.agent(
    name="translation_coordinator",
    instruction=(
        "Translate the given text to French, German, and Spanish. "
        "Use one tool call per language. Return all translations."
    ),
    agents=["translator"],
)

3. Orchestrator-Workers

Break task into scoped subtasks:
@fast.agent(
    name="researcher",
    instruction="Research topics using web search",
    servers=["fetch"],
)
@fast.agent(
    name="writer",
    instruction="Write comprehensive articles",
)
@fast.agent(
    name="editor",
    instruction="Edit and improve content",
)
@fast.agent(
    name="content_pipeline",
    instruction=(
        "Create a blog post: "
        "1. Call researcher to gather information "
        "2. Call writer with research to draft article "
        "3. Call editor with draft to polish final version"
    ),
    agents=["researcher", "writer", "editor"],
)

Tool Naming Convention

Child agents are exposed with the prefix agent__:
Child agent name: "NY-Project-Manager"
Tool name: "agent__NY-Project-Manager"
The LLM sees both as tools:
{
  "tools": [
    {"name": "get_current_time", "type": "mcp_tool"},
    {"name": "agent__NY-Project-Manager", "type": "agent_tool"},
    {"name": "agent__London-Project-Manager", "type": "agent_tool"}
  ]
}

Detached Instances

Each tool call spawns an independent clone with its own:
  • LLM connection
  • MCP aggregator and servers
  • Message history
  • Usage tracking
# Parent makes 3 parallel calls to "reporter" agent
Tool call 1 → reporter[1]  # Independent clone
Tool call 2 → reporter[2]  # Independent clone  
Tool call 3 → reporter[3]  # Independent clone

# After completion, usage merges back to template "reporter" agent

Progress Display

Parallel instances appear in the progress panel with suffixed names:
Before parallel execution:
▎▶ Chatting      ▎ PM-orchestrator              sonnet turn 1

During parallel execution:
▎▶ Chatting      ▎ PM-orchestrator              sonnet turn 1
▎▶ Chatting      ▎ NY-Project-Manager[1]        haiku turn 1
▎▶ Calling tool  ▎ NY-Project-Manager[2]        haiku (get_time)
▎▶ Chatting      ▎ London-Project-Manager[1]    haiku turn 1

After completion:
▎▶ Chatting      ▎ PM-orchestrator              sonnet turn 2
▎✓ Ready         ▎ NY-Project-Manager[1]        haiku
▎✓ Ready         ▎ NY-Project-Manager[2]        haiku
▎✓ Ready         ▎ London-Project-Manager[1]    haiku

Advanced Examples

Multi-Project Status Dashboard

@fast.agent(
    name="project_status_agent",
    instruction="Get project status including health, blockers, and next steps",
    servers=["jira", "github"],
)
@fast.agent(
    name="dashboard_coordinator",
    instruction=(
        "Generate a status dashboard for all active projects. "
        "Call project_status_agent once per project in parallel. "
        "Projects: [ProjectA, ProjectB, ProjectC, ProjectD]. "
        "Aggregate into a summary dashboard with highlights and concerns."
    ),
    agents=["project_status_agent"],
    max_parallel=10,
)
async def main() -> None:
    async with fast.run() as agent:
        await agent.dashboard_coordinator.send("Generate current project dashboard")

Intelligent Content Processing

@fast.agent(
    name="summarizer",
    instruction="Create concise summaries of long documents",
)
@fast.agent(
    name="sentiment_analyzer",
    instruction="Analyze sentiment and emotional tone",
)
@fast.agent(
    name="entity_extractor",
    instruction="Extract key entities (people, places, organizations)",
)
@fast.agent(
    name="content_processor",
    instruction=(
        "Process documents by: "
        "1. Determining which analysis types are relevant "
        "2. Calling appropriate agents in parallel "
        "3. Synthesizing a comprehensive report. "
        "Available: summarizer, sentiment_analyzer, entity_extractor"
    ),
    agents=["summarizer", "sentiment_analyzer", "entity_extractor"],
)
async def main() -> None:
    async with fast.run() as agent:
        await agent.content_processor.send("Process customer_feedback.txt")

Adaptive Research Assistant

@fast.agent(
    name="academic_researcher",
    instruction="Search academic papers and journals",
    servers=["fetch"],
)
@fast.agent(
    name="news_researcher",
    instruction="Find recent news and current events",
    servers=["fetch"],
)
@fast.agent(
    name="code_researcher",
    instruction="Search code repositories and documentation",
    servers=["github", "fetch"],
)
@fast.agent(
    name="research_coordinator",
    instruction=(
        "Conduct comprehensive research on the given topic. "
        "Determine which research types are relevant, call those agents, "
        "and synthesize findings into a coherent report. "
        "Available: academic_researcher, news_researcher, code_researcher"
    ),
    agents=["academic_researcher", "news_researcher", "code_researcher"],
    servers=["filesystem"],  # Coordinator can also use tools directly
)
async def main() -> None:
    async with fast.run() as agent:
        await agent.research_coordinator.send(
            "Research the latest developments in transformer architectures"
        )

Multi-Language Documentation Generator

@fast.agent(
    name="doc_writer",
    instruction="Write technical documentation in the specified language",
)
@fast.agent(
    name="doc_coordinator",
    instruction=(
        "Generate documentation in English, Spanish, French, German, and Japanese. "
        "Call doc_writer once per language with the language specified in the message. "
        "Aggregate all versions and create an index page."
    ),
    agents=["doc_writer"],
    servers=["filesystem"],
    max_parallel=5,
)
async def main() -> None:
    async with fast.run() as agent:
        await agent.doc_coordinator.send(
            "Generate API documentation for the authentication module"
        )

Tool Input Schemas

Customize how the LLM calls child agents:

Default Schema (Message String)

# Automatic default schema:
{
  "type": "object",
  "properties": {
    "message": {
      "type": "string",
      "description": "Message to send to the agent"
    }
  },
  "required": ["message"]
}

Custom Schema

from fast_agent.agents.agent_types import AgentConfig

@fast.agent(
    name="data_processor",
    config=AgentConfig(
        instruction="Process data with specified parameters",
        tool_input_schema={
            "type": "object",
            "properties": {
                "data_source": {"type": "string"},
                "operation": {"type": "string", "enum": ["filter", "transform", "aggregate"]},
                "parameters": {"type": "object"}
            },
            "required": ["data_source", "operation"]
        }
    ),
)

History Management

Control how message history flows between parent and child agents:
@fast.agent(
    name="orchestrator",
    instruction="Coordinate tasks across child agents",
    agents=["worker1", "worker2"],
    history_source="orchestrator",      # Children see parent's history
    history_merge_target="orchestrator", # Children's history merges back to parent
)
History Source Options:
  • none: Children start with empty history (default)
  • child: Children start with template child’s history
  • orchestrator: Children start with parent’s history
  • messages: Load from configured message files
Merge Target Options:
  • none: No merge-back (default)
  • child: Merge into template child agent
  • orchestrator: Merge into parent agent

Best Practices

Clear Instructions

Write explicit orchestrator instructions describing routing logic, parallelization strategy, and aggregation approach

Distinct Child Roles

Give child agents focused, non-overlapping responsibilities to reduce LLM confusion

Parallel Limits

Set appropriate max_parallel based on API limits and cost constraints

Monitor Instances

Use max_display_instances to prevent UI clutter with many parallel calls

Performance Considerations

Cost and Latency:
  • Each tool call spawns a new agent instance with initialization overhead
  • Parallel calls reduce latency but increase concurrent API usage
  • Monitor max_parallel to balance speed and API rate limits
  • Template agent tracks consolidated usage across all instances

Comparison with Other Patterns

FeatureAgents as ToolsRouterOrchestratorParallel
LLM Controls Selection✅ Yes✅ Yes✅ Yes❌ No
Parallel Execution✅ Automatic❌ Single❌ Sequential✅ All
Dynamic Composition✅ Full❌ Single choice✅ Task decomp❌ Fixed
Routing LogicLLM instructionLLM + structuredLLM + planningN/A
ImplementationTool callingStructured outputPlan generationasyncio.gather
ComplexityMediumLowHighLow
Best ForDynamic workflowsSimple delegationComplex tasksKnown fan-out

Use Cases

  • Multi-Domain Systems: Single entry point with LLM routing to specialists
  • Batch Processing: Parallel processing with dynamic workload distribution
  • Adaptive Pipelines: LLM determines which steps are needed
  • Project Management: Coordinate multiple specialized agents for complex projects
  • Content Generation: Orchestrate research, writing, editing, translation
  • Data Processing: Route data through appropriate processing agents

Debugging

Enable detailed logging to see tool calls and instance lifecycle:
import logging
logging.basicConfig(level=logging.DEBUG)

# You'll see:
# DEBUG: Spawning detached instance: NY-Project-Manager[1]
# DEBUG: Child tool call: agent__NY-Project-Manager[1] with args {"message": "..."}
# DEBUG: Instance NY-Project-Manager[1] completed, merging usage
# DEBUG: Shutting down detached instance: NY-Project-Manager[1]
  • Router - Simpler single-agent delegation
  • Orchestrator - Explicit planning without tool calling
  • Parallel - Fixed fan-out/fan-in without LLM control
  • Chain - Sequential execution without dynamic routing

Build docs developers (and LLMs) love