Add a Tool

Corresponding source

Extension stepSource
Tool base contractnanobot/agent/tools/base.py::Tool
Discovery, scopes, and entry pointsnanobot/agent/tools/loader.py::ToolLoader.discover, load
Registration and schemananobot/agent/tools/registry.py::ToolRegistry
Configuration and context integrationnanobot/config/schema.py, nanobot/agent/tools/context.py
Runner execution boundarynanobot/agent/runner.py::_run_tool, _execute_tools
Example and testsexample/nanobot/learning-EN/examples/text-stats-tool-plugin/, tests/agent/test_tool_loader_entrypoints.py, tests/tools/

Learning objectives

After this lesson, you should be able to:

  • implement the minimum Tool contract;
  • explain built-in scanning and external entry-point discovery;
  • choose Tool scope, configuration, and concurrency properties;
  • design tests for argument validation, side effects, and security boundaries.

Prerequisites

1. Minimum Tool contract

nanobot/agent/tools/base.py::Tool requires:

  • name;
  • description;
  • parameters;
  • async execute(**kwargs).

A minimal pure-text word counter looks like:

from typing import Any
 
from nanobot.agent.tools.base import Tool, tool_parameters
from nanobot.agent.tools.schema import StringSchema, tool_parameters_schema
 
 
@tool_parameters(
    tool_parameters_schema(
        required=["text"],
        text=StringSchema("Text whose words should be counted"),
    )
)
class WordCountTool(Tool):
    @property
    def name(self) -> str:
        return "word_count"
 
    @property
    def description(self) -> str:
        return "Count whitespace-separated words in text."
 
    @property
    def read_only(self) -> bool:
        return True
 
    async def execute(self, text: str, **_: Any) -> str:
        return str(len(text.split()))

This demonstrates the contract. It does not need to be added to nanobot source.

2. Why description is runtime logic

The Provider sees the function schema generated by Tool.to_schema(). The model mainly uses:

  • name to identify the broad capability;
  • description to decide when to use it;
  • parameter descriptions to understand each field.

An ambiguous schema produces bad arguments even when the Python implementation is perfect. Describe purpose and boundaries rather than internal implementation.

3. Argument conversion and validation order

ToolRegistry.execute():

  1. locates the Tool by name;
  2. converts safely coercible strings to integer, number, or boolean;
  3. calls validate_params();
  4. returns an explicit error after validation failure;
  5. calls execute() only after validation succeeds.

Tool’s JSON Schema validator supports common types, required fields, enums, min/max, string lengths, arrays, and nested objects. A Tool can still override validate_params() for business rules.

Schema validation is not business authorization

Proving that "path" is a string says nothing about whether the caller may access that path. Filesystem access still requires workspace policy.

4. Built-in Tool discovery

ToolLoader.discover() scans nanobot.agent.tools with pkgutil.iter_modules():

  • skips infrastructure modules such as base, schema, registry, loader, and mcp;
  • imports other public modules;
  • finds concrete, nonabstract, nonprivate Tool subclasses;
  • skips _plugin_discoverable=False;
  • sorts by class name.

A normal built-in Tool can therefore live in a scanned module without a manual import/register call in AgentLoop. A special Tool referenced directly by AgentLoop can disable discovery like MyTool and register manually, but that is not the default extension path.

5. External Tool plugin

An independent Python package can expose an entry point without modifying nanobot:

[project.entry-points."nanobot.tools"]
word-count = "my_nanobot_tools:WordCountTool"

ToolLoader._discover_plugins() loads classes from the nanobot.tools group.

Name-conflict rules:

  • built-in Tools load first;
  • a plugin whose tool.name conflicts with a built-in Tool is skipped;
  • other collisions log a warning, and a later registration may replace an earlier one.

Do not use replacement behavior as monkey-patching. Choose a unique, stable Tool name.

6. ToolContext and lifecycle

A Tool can integrate through class methods:

@classmethod
def enabled(cls, ctx: ToolContext) -> bool:
    ...
 
@classmethod
def create(cls, ctx: ToolContext) -> Tool:
    ...

ToolContext can provide:

  • Tools configuration;
  • workspace;
  • MessageBus;
  • Subagent manager;
  • Cron service;
  • file-state store;
  • Provider snapshot loader;
  • image-generation Provider configuration;
  • timezone;
  • RuntimeEventBus and other extension context.

Read only the fields the Tool needs. A pure-function Tool should not depend on the entire runtime.

7. Scope

_scopes determines which registries contain the Tool:

  • core
  • subagent
  • memory

The base default is only {"core"}. A Tool available to the parent Agent may be inappropriate for a Dream/Memory runner or Subagent.

Before adding Subagent scope, check:

  • whether it changes the parent Session;
  • whether it needs the parent MessageBus;
  • whether it depends on state available only in the main loop;
  • whether workspace permissions remain correct.

Implementation and test:

  • nanobot/agent/tools/loader.py::ToolLoader.load
  • tests/agent/test_tool_loader_scopes.py

8. Configuration integration

Built-in Tools commonly use:

  • config_key;
  • config_cls();
  • enabled(ctx);
  • create(ctx).

For example, ExecTool uses config_key="exec", defines its configuration type in shell.py, and lets ToolsConfig resolve the type through a delayed import.

Entry-point discovery does not complete configuration integration

ToolLoader discovers and instantiates; it does not design a WebUI form, configuration migration, or documentation. An external plugin with custom configuration must verify the actual shape of ToolsConfig extra fields and parse them explicitly in enabled/create. A built-in Tool may also need a matching field under ToolsConfig in nanobot/config/schema.py.

9. Concurrency properties

Base values:

  • read_only=False
  • exclusive=False
  • concurrency_safe = read_only and not exclusive

Set read_only=True or override concurrency only when concurrent execution cannot compete for shared state. A read-only network request may still share a rate limit, non-thread-safe client, or global cursor.

10. Security and result contract

At minimum, consider:

  • paths: reuse resolve_workspace_path();
  • URLs: reuse validate_url_target();
  • secrets: never put keys in schemas, results, or logs;
  • timeout: bound external I/O;
  • cancellation: release files, clients, and processes;
  • output: bound size so one result cannot fill context;
  • errors: give the model actionable information without suggesting policy bypass;
  • side effects: describe behavior clearly and require explicit confirmation when appropriate.

11. Minimum test set

  1. Correct schema.
  2. Expected result for valid arguments.
  3. Missing, wrong-type, and boundary values.
  4. enabled/create with a real configuration object.
  5. ToolLoader discovery and correct scope.
  6. Filesystem/network security boundaries.
  7. Cancellation, timeout, and resource cleanup.
  8. Oversized result or external failure.

Test references:

  • tests/tools/test_tool_loader.py
  • tests/agent/test_tool_loader_entrypoints.py
  • tests/agent/test_tool_loader_scopes.py
  • tests/tools/test_tool_registry.py
  • matching tests/tools/test_*.py for a similar Tool

12. Minimum modification scope

Built-in Tool

  • nanobot/agent/tools/<name>.py
  • nanobot/config/schema.py only for first-class configuration
  • tests/tools/test_<name>.py
  • user configuration documentation only when user-configurable

External plugin

  • independent package;
  • entry point;
  • plugin-owned tests/readme;
  • no AgentLoop change.

Self-check

  1. Does async def mean a Tool can run concurrently?
  2. Why can a parameter schema not replace path authorization?
  3. How do built-in and entry-point registration paths differ?
  4. If a Skill already solves the workflow, why should you not add a Tool immediately?

Next: Add a Skill