Agent as Tool
Use agents as callable tools for hierarchical agent composition
Agent as Tool turns any agent into a callable tool so a parent agent can invoke specialists and keep control of the result.
from praisonaiagents import Agent
researcher = Agent(name="Researcher", instructions="Research topics thoroughly")
writer = Agent(
name="Writer",
instructions="Write polished copy using specialist tools",
tools=[researcher.as_tool()],
)
writer.start("Write a short post about quantum computing with cited facts")The user asks for a polished article; the writer agent invokes the researcher as a tool and composes the answer.
Quick Start
- Simple Usage
Basic Usage from praisonaiagents import Agent # Create specialist agents researcher = Agent( name="Researcher", instructions="Research topics thoroughly and return findings" ) coder = Agent( name="Coder", instructions="Write clean Python code" ) # Parent agent uses specialists as tools writer = Agent( name="Writer", instructions="Write technical articles using your tools", tools=[ researcher.as_tool("Research a topic and return findings"), coder.as_tool("Write Python code for a given task"), ] ) # Writer invokes researcher and coder as needed result = writer.start("Write an article about async Python patterns")Custom Tool Names from praisonaiagents import Agent analyst = Agent(name="DataAnalyst", instructions="Analyze data") # Custom tool name and description tool = analyst.as_tool( description="Analyze dataset and return insights", tool_name="analyze_data" ) coordinator = Agent( name="Coordinator", tools=[tool] ) - With Configuration
Pass custom tool_name and description to as_tool() when the default labels are not clear enough for the parent LLM.
- Direct Invocation
Call the generated tool yourself with
execute_tool— the tool exposes an explicittaskparameter:python from praisonaiagents import Agent coder = Agent(name="Coder", instructions="Write clean Python code") writer = Agent( name="Writer", instructions="Delegate coding to Coder", tools=[coder.as_tool(tool_name="invoke_coder")], ) # New in PR #4232: the tool exposes an explicit `task` parameter writer.execute_tool("invoke_coder", {"task": "write a haiku about async I/O"})The generated tool now has
(task: str)in its signature — the LLM sees it in the schema and fills it, and direct callers must pass it.
How It Works
| Step | What happens |
|---|---|
| 1. Request | User asks the parent agent for a result |
| 2. Invoke | Parent LLM decides to call the specialist as a tool |
| 3. Execute | Specialist agent runs with a clean context |
| 4. Return | Result is returned to the parent agent |
| 5. Respond | Parent composes the final response |
as_tool() vs Handoffs
| Feature | as_tool() | Handoffs |
|---|---|---|
| Control | Parent retains control | Control transfers to target |
| Context | No history passed (clean slate) | Context passed based on policy |
| Tool naming | invoke_{name} | transfer_to_{name} |
| Context policy | ContextPolicy.NONE (clean slate) | Default ContextPolicy.SUMMARY |
| Use Case | Hierarchical composition | Task delegation |
| Return | Result returned to parent | Target continues conversation |
If you previously “worked around” the broken as_tool() by switching to handoffs=, note the workaround wasn’t equivalent — invoke_{name} runs with ContextPolicy.NONE (a genuine clean slate, now that context policies take effect) while transfer_to_{name} shares context per its policy (SUMMARY by default). Now that as_tool() works in tools=[...], switch back if you want the clean-slate behaviour.
API Reference
Agent.as_tool()
def as_tool(
self,
description: Optional[str] = None,
tool_name: Optional[str] = None,
) -> HandoffstrTool description for the LLM. Describes what this agent does. Default: "Invoke {agent_name} to complete a subtask and return the result"
strCustom tool name. Default: invoke_{agent_name} (snake_case)
HandoffA Handoff configured with ContextPolicy.NONE (no history passed to child).
The return type is Handoff (not a callable) because to_tool_function() needs the parent agent that will own this tool — and at as_tool() call time, that parent doesn’t exist yet. Conversion happens automatically the moment you place the result inside a parent’s tools=[...].
Generated tool signature
Because as_tool() always pins ContextPolicy.NONE, the generated callable exposes a task: str keyword-only parameter. This is what the LLM tool schema advertises and what direct execute_tool calls must supply.
def invoke_<agent_name>(*, task: str) -> strstrThe prompt passed verbatim to the sub-agent. If task is not provided, prompt is accepted as an alias.
Examples
Research + Writing Pipeline
from praisonaiagents import Agent
# Specialist agents
researcher = Agent(
name="Researcher",
instructions="Search the web and compile research findings"
)
fact_checker = Agent(
name="FactChecker",
instructions="Verify facts and cite sources"
)
# Writer uses both as tools
writer = Agent(
name="Writer",
instructions="""Write articles using your tools:
- Use invoke_researcher for initial research
- Use invoke_factchecker to verify claims""",
tools=[
researcher.as_tool("Research a topic"),
fact_checker.as_tool("Verify facts and claims"),
]
)
result = writer.start("Write about quantum computing breakthroughs in 2024")Code Review Pipeline
from praisonaiagents import Agent
# Specialist agents
linter = Agent(
name="Linter",
instructions="Check code for style issues and bugs"
)
security_scanner = Agent(
name="SecurityScanner",
instructions="Scan code for security vulnerabilities"
)
# Reviewer orchestrates both
reviewer = Agent(
name="CodeReviewer",
instructions="Review code using your analysis tools",
tools=[
linter.as_tool("Lint code for issues"),
security_scanner.as_tool("Scan for security vulnerabilities"),
]
)
code = """
def login(username, password):
query = f"SELECT * FROM users WHERE name='{username}'"
return db.execute(query)
"""
result = reviewer.start(f"Review this code:\n{code}")Multi-Step Analysis
from praisonaiagents import Agent
# Create a chain of specialists
data_fetcher = Agent(name="DataFetcher", instructions="Fetch data from APIs")
analyzer = Agent(name="Analyzer", instructions="Analyze data patterns")
visualizer = Agent(name="Visualizer", instructions="Create visualizations")
# Orchestrator uses all three
orchestrator = Agent(
name="Orchestrator",
instructions="Coordinate data analysis workflow",
tools=[
data_fetcher.as_tool("Fetch data from a source"),
analyzer.as_tool("Analyze data and find patterns"),
visualizer.as_tool("Create charts and visualizations"),
]
)
result = orchestrator.start("Analyze sales trends for Q4 2024")Best Practices
Clear Descriptions
Provide clear tool descriptions so the LLM knows when to invoke each specialist.
Single Responsibility
Each specialist agent should have one clear purpose.
Avoid Deep Nesting
Keep hierarchies shallow (2-3 levels max) for clarity.
Test Individually
Test each specialist agent independently before composing.
Cloning for channels rebinds handoffs
Each channel’s clone rebinds the as_tool() handoff to itself, not the source agent. This means Discord and Telegram clones each get their own invoke_researcher bound to their own chat_history / tools / memory — no cross-channel leakage. You do not need to rebuild tools=[...] per channel; clone_for_channel() handles it.