Skip to content

Tools

This guide shows how to define a Bub tool — a Python function the model can call during a turn — and how to make sure your plugin actually registers it.

  • A plugin package wired to the bub entry-point group (see Plugins).
  • The marker from bub import tool available in your module.

Decorate any function with @tool. The decorator builds a Tool object, wraps it with timing logs, and inserts it into the central REGISTRY:

from bub import tool


@tool
def add(a: int, b: int) -> int:
    """Add two integers and return the sum."""
    return a + b

The tool’s name defaults to the function name. Override it with @tool(name="math.add") or pass description= to control how it appears in the model prompt. Dotted registry names are preserved for runtime lookup and comma commands, but model-facing tool names replace dots with underscores (math.add becomes math_add). Async functions are supported — the wrapper awaits them.

The REGISTRY lives in bub.tools:

# from src/bub/tools.py
REGISTRY: dict[str, Tool] = {}

Every @tool call mutates this dict at import time. Bub’s builtin agent reads from REGISTRY when assembling the tool list for the model. There is no separate registration step.

Because registration is an import-time side effect, the module that defines @tool functions must actually be imported before Bub asks for tools. Inside your plugin’s entry-point module, add:

# bub_myplugin/plugin.py
from bub import hookimpl

from . import tools  # noqa: F401  — registers @tool decorators

If that import is missing, the tool module never runs, nothing lands in REGISTRY, and the model never sees the tool.

The builtin runtime follows the same pattern — see BuiltinImpl.__init__, which imports bub.builtin.tools for the same reason.

Both are callable units inside Bub, but they are operated by different actors:

SurfaceCallerTrigger
ToolThe modelTool-call message during a turn
Comma commandThe humanInbound text starting with a comma

A line like ,skill name=hello is a comma command — the operator typed it, and Bub’s builtin build_prompt marks the message as kind="command" so it bypasses the model. Tools, by contrast, are invoked by the model itself when it produces a tool-call event.

See Surfaces for the full split between operator and model surfaces.

Tools do not appear in bub hooks, but you can confirm registration with a Python one-liner:

uv run python -c "import bub_myplugin.plugin; from bub.tools import REGISTRY; print(sorted(REGISTRY))"

The output should include your tool’s name. From there, run a turn that asks the model to call it.

  • Hooks — combine tools with hooks to build a complete plugin
  • Skills — package model-facing instructions alongside tools
  • Surfaces — operator vs model surfaces