Skip to content

Agent as Tool

The agent-as-tool pattern lets an orchestrating agent call specialized agents on demand. Each tool invocation is stateless — a fresh state is created, the query is passed in, and the structured output is returned.

The Pattern

Instead of giving every agent access to the full conversation, you create focused agents that do one thing well and wrap them as tools for an orchestrator:

User → Orchestrator Agent
            ├── summarizer tool (wraps SummaryAgent)
            ├── translator tool (wraps TranslationAgent)
            └── analyst tool (wraps AnalystAgent)

The orchestrator's LLM decides which tool to call based on the user's request.

Using as_tool()

from orqest.agents import as_tool

# Create specialized agents
summarizer = SummaryAgent(model="openai:gpt-4.1", api_key="sk-...")
translator = TranslationAgent(model="openai:gpt-4.1", api_key="sk-...")

# Wrap as tools — one line each
summary_tool = as_tool(
    summarizer,
    description="Summarize a piece of text. Returns a concise summary and key points.",
)

translation_tool = as_tool(
    translator,
    description="Translate text to a target language.",
)

Parameters

Parameter Type Default Description
agent BaseAgent required The agent to wrap
name str \| None None Tool name visible to the LLM (defaults to agent.agent_name)
description str required Tool description — tells the LLM when to use this tool

Under the Hood

Each time the orchestrator's LLM calls the tool, as_tool() does this:

state = GlobalState()           # fresh state — no history
state.add_message("user", query)
output = await agent.run(state) # runs _run_implementation → call_model
return output.model_dump_json() # structured output as JSON string

The orchestrator receives the JSON string and can use the structured data in its response.

Full Example

from pydantic_ai import Agent
from orqest import load_config
from orqest.agents import as_tool
from orqest.utils.llm_model import resolve_model

config = load_config()

# Specialized agents (defined elsewhere)
summarizer = SummaryAgent(model=config.llm_model, api_key=config.llm_api_key)
translator = TranslationAgent(model=config.llm_model, api_key=config.llm_api_key)

# Wrap as tools
summary_tool = as_tool(summarizer, description="Summarize text.")
translation_tool = as_tool(translator, description="Translate text.")

# Create orchestrator with the tools
orchestrator = Agent(
    model=resolve_model(config.llm_model, api_key=config.llm_api_key),
    system_prompt="Use the available tools to help the user.",
    tools=[summary_tool, translation_tool],
)

# Run — the orchestrator decides which tool to call
result = await orchestrator.run("Summarize this article about quantum computing...")
print(result.output)

Stateful vs. Stateless

The same agent class works in both patterns — the invocation context determines behavior:

Multi-turn (direct) Agent as Tool
State Shared GlobalState across turns Fresh GlobalState per call
History Accumulates via call_model() Always empty
Use case Conversational agent Focused, one-shot specialist
Invocation await agent.run(state) Called by orchestrator via tool

An agent using self.call_model() in its _run_implementation() works in both contexts:

  • When called directly with a persistent state, history accumulates across turns
  • When called via as_tool(), each invocation gets a fresh state so history is always empty