Build a read-only Tool plugin
Exercise objective
Understand and run an independent text_stats Tool plugin. Test its Tool contract first, verify entry-point discovery second, and finish with one real DeepSeek Tool Call. Do not modify nanobot core at any point.
Prerequisites
- Complete the Workspace Skill exercise.
- Be able to create and use an isolated Python virtual environment.
- Read ToolLoader and ToolRegistry.
- Use the lab workspace and configuration.
Example location
This learning library includes a runnable example:
example/nanobot/learning-EN/examples/text-stats-tool-plugin/
├── pyproject.toml
├── nanobot_text_stats/
│ ├── __init__.py
│ └── tool.py
└── tests/
└── test_tool.pySource to read
Read the example first, then compare it with:
nanobot/agent/tools/base.py::Toolnanobot/agent/tools/schema.pynanobot/agent/tools/loader.py::ToolLoadernanobot/agent/tools/registry.py::ToolRegistrytests/agent/test_tool_loader_entrypoints.py
Predict before running
- Must the entry-point key equal
Tool.name? - What behavior does
read_only=Trueaffect? - If
textis missing, doesexecute()return the error, or does Registry? - Which implementation wins if a plugin and a built-in Tool share a name?
Procedure
1. Read the Tool
TextStatsTool:
- is named
text_stats; - requires a
textargument of 1–20000 characters; - returns a JSON string;
- reads neither files nor the network;
- sets
read_only=True, so it may run concurrently in non-exclusive mode.
Explain each design choice in your own words before proceeding.
2. Install the source checkout
In a dedicated virtual environment:
Set-Location "example\nanobot\code-EN"
python -m pip install -e .3. Install the plugin
python -m pip install -e "..\learning-EN\examples\text-stats-tool-plugin" --no-depsThe checkout from the previous step already provides nanobot. --no-deps prevents pip from installing a different nanobot release from PyPI.
4. Run the Tool unit tests
From example/nanobot/learning-EN/examples/text-stats-tool-plugin:
python -m pytest tests -q5. Verify the entry point
python -c "from nanobot.agent.tools.loader import ToolLoader; print(sorted(ToolLoader()._discover_plugins()))"The result should include:
text-statsThis is the entry-point key. The function name exposed to the model is text_stats.
6. Verify Registry behavior
In the debugger or a short-lived Python REPL:
- create a
ToolContext; - create a
ToolRegistry; - call
ToolLoader().load(ctx, registry); - check
registry.has("text_stats"); - execute both valid and invalid arguments.
7. Real DeepSeek smoke test
Restart the nanobot process so that it discovers the entry point again. Send:
You must use the text_stats tool to analyze the following text. Tell me its characters, non_whitespace_characters, and lines:
first line
second lineConfirm the Tool Call name and arguments at a Runner breakpoint. Do not infer that the Tool ran merely from the final natural-language response.
Expected observations
- ToolLoader discovers the independent package.
- The Provider Tool schema includes
text_stats. - Registry rejects missing or oversized arguments.
- A valid result first returns to the model as a tool message.
- The plugin requires no AgentLoop modification.
read_onlymakes the base class’sconcurrency_safeproperty true.
Completion criteria
- The example tests pass.
- The entry point is discoverable.
- Registry can execute the Tool.
- A real model invokes the Tool at least once.
- Explain the distinction among the entry-point key, class name, and Tool name.
example/nanobot/code-ENhas no source diff.
Verification
Add one more test that checks one of these cases:
- an empty string fails;
- 20001 characters fail;
- newline counting is correct.
The example area is explicitly writable for this exercise. Rerun the plugin tests and confirm that the diff is confined to the example directory.
Review questions
- Why does the example use a pure, read-only Tool?
- Does the framework prove that
read_only=Trueis truthful? - Why test through Registry before making a real request?
- If a future version adds a URL argument, which security controls become necessary?