Skip to content

Custom Tools

When the built-in toolkits don't cover your use case, you can expose your own code as tools in three escalating levels of control: a plain function, the @tool decorator, or a Toolkit subclass.

Plain functions

The simplest tool is a normal Python function. Pass it directly to the agent; its type hints and docstring are turned into a JSON schema by Function.from_callable.

from buddy.agent import Agent

def convert_currency(amount: float, rate: float) -> str:
    """Convert an amount using an exchange rate.

    Args:
        amount (float): The source amount.
        rate (float): The exchange rate to apply.
    """
    return f"{amount * rate:.2f}"

agent = Agent(tools=[convert_currency])

Docstrings are the contract

The Args: section is parsed (via docstring_parser) into parameter descriptions the model sees. Parameters without a default value become required in the schema.

The @tool decorator

Use @tool when you need more than a schema — caching, result display, user confirmation, or hooks. It converts the function into a Function instance.

from buddy.tools import tool

@tool(
    name="search_docs",
    description="Search internal documentation.",
    show_result=True,
    cache_results=True,
    cache_ttl=600,
)
def search_docs(query: str) -> str:
    """Search the docs index for a query."""
    ...
    return "results"

Decorator options

@tool accepts only the following keyword arguments (passing any other raises a ValueError):

Argument Type Effect
name str Override the tool name.
description str Override the description (defaults to the docstring).
strict bool Enforce strict JSON-schema parameter checking.
instructions str Usage instructions for the tool.
add_instructions bool Add instructions to the system message (default True).
show_result bool Show the result in the response after the call.
stop_after_tool_call bool Stop the agent run after this tool runs.
requires_confirmation bool Require user confirmation before executing.
requires_user_input bool Require user-provided input before executing.
user_input_fields List[str] Fields supplied by the user rather than the model.
external_execution bool Execute outside the agent loop.
pre_hook Callable Runs before the function.
post_hook Callable Runs after the function.
tool_hooks List[Callable] Middleware wrapped around the call.
cache_results bool Cache results to disk.
cache_dir str Cache directory (defaults to the system temp dir).
cache_ttl int Cache lifetime in seconds (default 3600).

Mutually exclusive flags

Only one of requires_user_input, requires_confirmation, or external_execution may be True at a time — setting more than one raises a ValueError.

@tool works with and without parentheses, and supports sync, async, and async generator functions:

@tool
def simple(): ...

@tool(cache_results=True)
def configured(): ...

@tool
async def async_tool(): ...

Subclassing Toolkit

Group related operations into a Toolkit. Register the methods you want to expose by passing them to super().__init__(..., tools=[...]); with auto_register=True (the default) they are wrapped as Function instances.

from typing import List, Callable
from buddy.tools import Toolkit

class InventoryTools(Toolkit):
    def __init__(self, **kwargs):
        tools: List[Callable] = [self.check_stock, self.reserve_item]
        super().__init__(name="inventory", tools=tools, **kwargs)

    def check_stock(self, sku: str) -> str:
        """Return the available quantity for a SKU.

        Args:
            sku (str): The product SKU.
        """
        return "42 in stock"

    def reserve_item(self, sku: str, quantity: int) -> str:
        """Reserve a quantity of a SKU.

        Args:
            sku (str): The product SKU.
            quantity (int): Units to reserve.
        """
        return f"Reserved {quantity} of {sku}"

agent_tools = InventoryTools()

Toolkit constructor options

The Toolkit base class exposes shared controls for every toolkit:

Argument Purpose
name A descriptive name for the toolkit.
tools The list of callables to register.
instructions / add_instructions Toolkit-level instructions and whether to inject them.
include_tools / exclude_tools Allow/deny lists of method names to register.
requires_confirmation_tools Method names that require user confirmation.
external_execution_required_tools Method names executed outside the agent loop.
stop_after_tool_call_tools Method names that stop the run after executing.
show_result_tools Method names whose results are shown.
cache_results / cache_ttl / cache_dir Caching configuration for all methods.
auto_register Automatically register the methods in tools (default True).

Validated names

include_tools and exclude_tools are validated against the registered method names — referencing an unknown method raises a ValueError.

You can also register a function after construction:

tk = InventoryTools()
tk.register(some_callable, name="optional_alias")

Accessing the agent inside a tool

If a tool's signature includes an agent (or team) parameter, the agent injects itself at call time and removes it from the model-facing schema:

@tool
def remember(fact: str, agent) -> str:
    """Store a fact about the user."""
    agent.session_state.setdefault("facts", []).append(fact)
    return "stored"

See Function Calling for how schemas are generated and Tool Execution for the hook and execution flow.