Chapter 8 · Custom Tools and Toolkit Development
Learning objectives: Master three ways to write custom tools — plain functions,
@tooldecorator, and Toolkit subclasses; learn to control tool behavior with decorator parameters, and build a correct sense of tool design granularity.
8.1 Plain Functions: The Lightest Custom Tool
Chapter 7 already showed plain functions as tools; here we fill in engineering details. Function tools support both synchronous and asynchronous definitions:
# custom_func.py —— Both sync and async functions work as tools
import asyncio
import os
import sqlite3
from agno.agent import Agent
from agno.models.openai import OpenAIChat
def get_user(user_id: int) -> dict:
"""Get a user's name and email by user id.
Args:
user_id: The numeric id of the user.
"""
# In a real project this would typically query a database or call an internal API
return {"id": user_id, "name": "Xiaogao", "email": "gao@example.com"}
async def query_report(report_date: str) -> str:
"""Fetch the daily report content for a given date.
Args:
report_date: Date string in YYYY-MM-DD format.
"""
await asyncio.sleep(0.1) # Simulate IO
return f"Daily report for {report_date}: 1024 new users today."
agent = Agent(
model=OpenAIChat(
id="deepseek-chat",
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com/v1",
),
tools=[get_user, query_report], # Sync and async can be mixed
)
agent.print_response("Look up user 42's email, then give me the要点 from the 2026-01-10 daily report")Return value convention
Tools can return strings, numbers, dicts, lists, and other serializable values. Returning structured data (dict/list) is usually better than concatenating long text — the model extracts needed fields more accurately. For media files alongside text, use ToolResult.
8.2 @tool Decorator: Adding Execution Control to Functions
The agno.tools.tool decorator (new in v2, carried in 3.x) attaches execution policies to a tool without changing the function body. All parameters below have been verified against agno 3.x source:
| Parameter | Default | Description |
|---|---|---|
name / description | None | Override the model-visible tool name/description |
show_result | False | Show the result to the user immediately after execution |
stop_after_tool_call | False | Terminate the current run after this tool executes |
cache_results / cache_dir / cache_ttl | False | Result caching with directory and TTL (seconds) |
requires_confirmation | False | Requires human confirmation before execution (covered in Chapter 21) |
pre_hook / post_hook / tool_hooks | None | Hooks before/after tool execution |
# tool_decorator.py —— Control tool behavior with @tool
import os
import time
from agno.agent import Agent
from agno.models.openai import OpenAIChat
from agno.tools import tool
@tool(cache_results=True, cache_ttl=300) # Same args hit cache within 5 minutes
def slow_exchange_rate(base: str, quote: str) -> str:
"""Get the current exchange rate from base currency to quote currency.
Args:
base: The base currency code, e.g. USD.
quote: The quote currency code, e.g. CNY.
"""
time.sleep(2) # Simulate an expensive third-party API call
return f"1 {base} = 7.23 {quote}"
@tool(stop_after_tool_call=True, show_result=True)
def submit_ticket(title: str) -> str:
"""Create a support ticket and finish the conversation.
Args:
title: Short summary of the issue.
"""
return f"Ticket #{8848} created: {title}"
agent = Agent(
model=OpenAIChat(
id="deepseek-chat",
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com/v1",
),
tools=[slow_exchange_rate, submit_ticket],
)
agent.print_response("What is the current USD to CNY exchange rate?")
agent.print_response("Submit a ticket: laptop cannot connect to WiFi")Two parameters deserve special attention:
stop_after_tool_call: suited for "terminal actions." After submitting a ticket there's no need for the model to summarize again; end the run directly and save one model call;show_result: suited for deterministic outputs where "the result IS the answer" (calculators, lookups); skips the model's re-statement step.
8.3 Toolkit Subclass: Batch-Register a Group of Related Tools
When a business domain has multiple operations that need shared state (e.g., a unified client, auth config), write a Toolkit. The pattern is to inherit agno.tools.Toolkit and implement __init__:
# todo_toolkit.py —— Custom todo-list Toolkit
import os
from agno.agent import Agent
from agno.models.openai import OpenAIChat
from agno.tools import Toolkit
class TodoTools(Toolkit):
def __init__(self, **kwargs):
super().__init__(name="todo_tools", **kwargs)
self._todos: list[str] = [] # Shared state within the toolkit
self.register(self.add_todo)
self.register(self.list_todos)
self.register(self.remove_todo)
def add_todo(self, task: str) -> str:
"""Add a task to the todo list.
Args:
task: The task description to add.
"""
self._todos.append(task)
return f"Added: {task}"
def list_todos(self) -> str:
"""List all tasks in the todo list."""
if not self._todos:
return "List is empty"
return "\n".join(f"{i + 1}. {t}" for i, t in enumerate(self._todos))
def remove_todo(self, index: int) -> str:
"""Remove a task by its 1-based index.
Args:
index: The 1-based position of the task to remove.
"""
removed = self._todos.pop(index - 1)
return f"Removed: {removed}"
agent = Agent(
model=OpenAIChat(
id="deepseek-chat",
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com/v1",
),
tools=[TodoTools()],
instructions="Manage the user's todo list; show the full list after each add/remove/query.",
)
agent.print_response("Add three things: buy milk, write weekly report, run for half an hour")
agent.print_response("Delete item 2")Toolkit constructors also accept batch control-strategy parameters (from Toolkit.__init__, verified in agno 3.x source): stop_after_tool_call_tools=["submit_order"], show_result_tools=[...], requires_confirmation_tools=[...], and package-level cache_results=True. These are equivalent to the decorator effects from section 8.2, but applied at the Toolkit level.
8.4 Exception Handling in Tools
Tools reach into the outside world; failures are the norm. Agno catches tool exceptions and feeds the error back to the model, letting the model decide whether to retry or pivot. You can also use tool_hooks for unified fallback:
# safe_tools.py —— Exception fallback example
import os
from agno.agent import Agent
from agno.models.openai import OpenAIChat
from agno.tools import tool
def retry_on_error(func_name: str, func, *args, **kwargs):
"""post_hook-style fallback: log and transform the exception."""
try:
return func(*args, **kwargs)
except Exception as e:
# Error messages returned to the model should be "actionable": tell it what to do next
return f"Call failed ({type(e).__name__}): {e}. Please check the arguments and retry once."
@tool(post_hook=retry_on_error)
def divide(a: float, b: float) -> float:
"""Divide a by b.
Args:
a: Numerator.
b: Denominator, must not be zero.
"""
return a / b # ZeroDivisionError is caught by the hook
agent = Agent(
model=OpenAIChat(
id="deepseek-chat",
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com/v1",
),
tools=[divide],
)
agent.print_response("Calculate 10 divided by 0")Rule of thumb: design error messages differently for "humans" vs. "models." For the model, spell out what to do next ("please check the arguments", "no data for that city, try another"), not a stack trace dump.
8.5 Tool Design Granularity Principles
The two most common beginner mistakes are opposite extremes:
- ❌ Too fine-grained:
get_id,get_name,get_emailas three tools — the model needs multiple rounds to assemble the answer; - ❌ Too coarse-grained: one
manage_company(query: str)catch-all interface — vague schema leads to the model passing random parameters.
The recommended approach is task-oriented modeling: one tool completes one complete thing from the model's perspective, with as few and as clear parameters as possible. Compare:
# granularity.py —— Task-oriented vs. database-oriented
def get_weather_bad(city: str, field: str) -> str:
"""Get one field of weather. BAD: model has to guess the field value."""
def get_weather_good(city: str) -> dict:
"""Get current weather summary (temperature, condition, humidity) for a city.
Args:
city: City name in Chinese or English.
"""
# Return all structured fields at once; the model picks what it needs
return {"city": city, "temperature": 26, "condition": "cloudy", "humidity": 62}Two more production守则:
- Separate idempotent reads from side effects: read-only tools are free to call; side-effect tools (placing orders, sending emails) must pair with
requires_confirmation=Trueor rate limiting; - Handle timeouts and retries inside the tool: don't expect the model to handle network jitter itself.
Chapter Summary
- Three customization styles: plain functions (lightest),
@tooldecorator (attach execution policies),Toolkitsubclass (shared state + batch registration); - Key
@toolparams:cache_resultsfor caching,stop_after_tool_callto end the run,show_resultto output directly,requires_confirmationfor human approval; - Toolkits support batch policy declaration via
*_toolslist params; - Tool exceptions are caught by Agno and fed back to the model; error text should be "actionable" guidance;
- Design tools by task granularity: one call solves one complete problem, return structured data.
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. A tool is used for "submitting an order" and you want the run to end immediately after it completes. Which parameter?
2. Regarding self.register(self.add_todo) in a Toolkit subclass, what is correct?
3. When a tool throws an exception, what is Agno's default behavior?
4. Which tool design best follows the "task-oriented granularity" principle?
🛠️ Hands-On Exercises
- Add a
clear_finished(done_index: int)method to theTodoToolsfrom section 8.3 and register it; verify the model can correctly pass "which item number" in natural language. - For a simulated slow quote tool (3-second delay), toggle
cache_resultson/off; ask the same question twice and compare response differences in logs. - Design a "file statistics" toolkit: compare Option A (two fine-grained tools:
count_lines(path)andsearch_in_file(path, keyword)) vs. Option B (oneanalyze_filecomposite tool); run 5 question sets each and compare average call rounds.
After completing the hands-on exercises, proceed to Chapter 9: Agent Team Basics — the multi-agent collaboration world beyond a single Agent.