Skip to main content
Tools are the actions your agent can take. At runtime, the Engine dispatches tool calls from a Decision to the ToolRegistry, which looks up and executes the matching callable.

The @tool decorator

Mark any callable as a QitOS tool with @tool. The decorator attaches metadata to the function without changing its behavior — you can still call the function normally in tests.
from qitos import tool
from qitos.core.tool import ToolPermission

@tool(
    name="read_file",
    description="Read the contents of a file at the given path.",
    timeout_s=10.0,
    permissions=ToolPermission(filesystem_read=True),
)
def read_file(path: str) -> str:
    with open(path, "r") as f:
        return f.read()

@tool parameters

ParameterTypeDescription
namestr | NoneTool name used in Decision.actions. Defaults to the function’s __name__.
descriptionstr | NoneDescription shown to the LLM. Falls back to the function’s docstring.
timeout_sfloat | NonePer-call timeout in seconds. None means no timeout.
max_retriesintHow many times to retry on failure. Defaults to 0.
permissionsToolPermission | NoneDeclares which system capabilities this tool requires.
required_opslist[str] | NoneLow-level operation identifiers required from the environment.

ToolPermission

ToolPermission declares what the tool is allowed to do. The Engine uses this information during preflight validation to check environment capabilities.
from qitos.core.tool import ToolPermission

# A tool that reads files and makes network requests
ToolPermission(
    filesystem_read=True,
    filesystem_write=False,
    network=True,
    command=False,
)
All four fields default to False.

ToolRegistry

ToolRegistry is the collection the Engine queries when dispatching actions. Pass it to your AgentModule via the constructor.
from qitos import ToolRegistry

registry = ToolRegistry()

Registering individual tools

Use registry.register() to add a single callable or BaseTool instance:
@tool(name="add")
def add(a: int, b: int) -> int:
    """Add two integers."""
    return a + b

registry.register(add)
You can also supply a custom name or override metadata at registration time:
from qitos.core.tool import ToolMeta

registry.register(add, name="math.add")
registry.register(some_func, meta=ToolMeta(description="Custom description"))
Tool names must be unique within a registry. Registering two tools with the same name raises a ValueError.

Scanning a module or object with include

registry.include(obj) scans all public, callable attributes of obj and registers any that have @tool metadata:
class MyTools:
    @tool(name="summarize")
    def summarize(self, text: str) -> str:
        ...

    @tool(name="translate")
    def translate(self, text: str, lang: str) -> str:
        ...

tools = MyTools()
registry.include(tools)
This is the preferred pattern when you organize related tools as methods on a class.

Registering toolsets

A toolset is any object that has a tools() method returning a list of callables or BaseTool instances. Register it with register_toolset():
from qitos.kit import CodingToolSet

registry.register_toolset(
    CodingToolSet(
        workspace_root="/tmp/work",
        include_notebook=False,
        enable_lsp=False,
        enable_tasks=False,
        enable_web=False,
        expose_modern_names=False,
    )
)
Tools from a toolset are automatically namespaced: toolset_name.tool_name. You can override the namespace:
registry.register_toolset(CodingToolSet(workspace_root="/tmp/work"), namespace="coding")
# registers as coding.view, coding.str_replace, coding.run_command, etc.

BaseTool and FunctionTool

For tools that need shared state or lifecycle management, subclass BaseTool directly:
from qitos.core.tool import BaseTool, ToolSpec, ToolPermission


class DatabaseTool(BaseTool):
    """Query a SQLite database."""

    def __init__(self, db_path: str):
        self.db_path = db_path
        super().__init__(
            ToolSpec(
                name="query_db",
                description="Run a SQL query and return rows as a list.",
                parameters={"sql": {"type": "string", "description": "SQL statement"}},
                required=["sql"],
                permissions=ToolPermission(filesystem_read=True),
            )
        )

    def run(self, sql: str) -> list:
        import sqlite3
        with sqlite3.connect(self.db_path) as conn:
            return conn.execute(sql).fetchall()


registry.register(DatabaseTool(db_path="data.db"))
FunctionTool is the wrapper that register() creates automatically when you pass a plain callable. You rarely need to instantiate it directly.
For most practical coding agents, prefer preset toolsets such as CodingToolSet or the registry builders in qitos.kit.toolset rather than hand-registering every file and shell tool yourself.

Passing the registry to AgentModule

Pass your populated ToolRegistry to AgentModule.__init__():
from qitos import AgentModule, ToolRegistry, tool


class SearchAgent(AgentModule[MyState, dict, Action]):
    def __init__(self):
        registry = ToolRegistry()
        registry.register(web_search)
        registry.register(read_file)
        super().__init__(tool_registry=registry)
The Engine reads agent.tool_registry and creates an ActionExecutor from it. You can also call registry.get_tool_descriptions() to get a formatted string of all registered tools for inclusion in your system prompt:
def build_system_prompt(self, state: MyState) -> str | None:
    tools_text = self.tool_registry.get_tool_descriptions()
    return f"You have access to these tools:\n\n{tools_text}"

Full example

from qitos import tool, ToolPermission

@tool(
    name="read_file",
    timeout_s=5.0,
    permissions=ToolPermission(filesystem_read=True),
)
def read_file(path: str) -> str:
    """Read the contents of a file."""
    with open(path) as f:
        return f.read()


@tool(
    name="write_file",
    timeout_s=5.0,
    permissions=ToolPermission(filesystem_write=True),
)
def write_file(path: str, content: str) -> str:
    """Write content to a file."""
    with open(path, "w") as f:
        f.write(content)
    return f"Written {len(content)} bytes to {path}"