How to Create Custom Tools¶
Tools extend agent capabilities by giving them access to external services, databases, APIs, or any custom logic. This guide covers all the ways to create and register tools in pico-agent.
Basics¶
A pico-agent tool is a class decorated with @tool(name, description) and @component. The LLM sees the name and description when deciding which tool to invoke.
from pico_ioc import component
from pico_agent import tool
@tool(name="calculator", description="Evaluate a mathematical expression")
@component
class CalculatorTool:
def run(self, expression: str) -> str:
return str(eval(expression))
Required attributes¶
| Attribute | Purpose |
|---|---|
name | Unique identifier -- must match the name used in @agent(tools=[...]) |
description | Shown to the LLM so it knows when to call the tool |
Callable resolution¶
ToolWrapper looks for an entry point on your class in this order:
__call__runexecuteinvoke
If none of these exist, a ValueError is raised:
Tool \<name> must implement __call__, run, execute, or invoke.
Tools with Dependencies¶
Tools are pico-ioc components, so they support full constructor injection:
@tool(name="db_query", description="Run a SQL query against the database")
@component
class DatabaseQueryTool:
def __init__(self, db_service: DatabaseService):
self.db = db_service
def run(self, query: str) -> str:
rows = self.db.execute(query)
return str(rows)
Attaching Tools to Agents¶
Reference tools by name in the @agent decorator:
from pico_agent import agent, AgentType
@agent(
name="analyst",
system_prompt="You are a data analyst.",
agent_type=AgentType.REACT,
tools=["calculator", "db_query"],
)
class AnalystAgent(Protocol):
def analyze(self, question: str) -> str: ...
Tip: Use
AgentType.REACTfor agents that need to iterate with tools.ONE_SHOTagents receive tool definitions but cannot loop.
Tag-Based Dynamic Tools¶
Tools can be registered with tags so they are automatically attached to any agent sharing those tags:
An agent with tags=["observability"] will automatically receive logger_tool in addition to its explicitly listed tools.
Tools tagged with "global" are attached to every agent.
Dynamic Tools at Runtime¶
Use VirtualToolManager to create tools from plain functions at runtime:
from pico_agent import VirtualToolManager
manager = container.get(VirtualToolManager)
tool = manager.create_tool(
name="greeting",
description="Generate a personalised greeting",
func=lambda name: f"Hello, {name}!",
)
Custom argument schema¶
Pass a Pydantic model to control the tool's argument schema:
from pydantic import BaseModel, Field
class GreetingInput(BaseModel):
name: str = Field(description="The person's name")
formal: bool = Field(default=False, description="Use formal tone")
tool = manager.create_tool(
name="greeting",
description="Generate a greeting",
func=my_greeting_func,
schema=GreetingInput,
)
Agents as Tools¶
Child agents can be used as tools by a parent agent. List them in the agents parameter:
@agent(
name="orchestrator",
agent_type=AgentType.REACT,
agents=["research_agent", "writer_agent"],
)
class Orchestrator(Protocol):
def run(self, task: str) -> str: ...
Each child agent is wrapped as an AgentAsTool, with its args_schema derived from the child's Protocol method signature. The LLM sees the child agent's description and can invoke it like any other tool.
Tool Registration Flow¶
flowchart TD
A["@tool decorator"] --> B["ToolScanner.auto_scan()"]
B --> C["ToolRegistry.register()"]
D["VirtualToolManager.create_tool()"] --> C
C --> E["DynamicAgentProxy._resolve_dependencies()"]
E --> F["ToolWrapper wraps instance"]
F --> G["Bound to LLM via bind_tools()"]