新增 Tool
对应源码
| 扩展环节 | 源码 |
|---|---|
| Tool 基类契约 | nanobot/agent/tools/base.py::Tool |
| 发现、scope 和 entry point | nanobot/agent/tools/loader.py::ToolLoader.discover、load |
| 注册与 schema | nanobot/agent/tools/registry.py::ToolRegistry |
| 配置和上下文接入 | nanobot/config/schema.py、nanobot/agent/tools/context.py |
| Runner 执行边界 | nanobot/agent/runner.py::_run_tool、_execute_tools |
| 示例和测试 | example/nanobot/learning-CN/examples/text-stats-tool-plugin/、tests/agent/test_tool_loader_entrypoints.py、tests/tools/ |
学习目标
读完后,你应当能够:
- 实现一个最小 Tool 契约;
- 解释内置扫描与外部 entry point 两种发现方式;
- 决定 Tool 的 scope、配置和并发属性;
- 为参数校验、副作用和安全边界设计测试。
前置知识
- 理解 Tool 注册、校验与执行;
- 能编写一个 Python 类和异步函数;
- 若 Tool 访问文件或 URL,先完成 安全边界。
1. 最小 Tool 契约
nanobot/agent/tools/base.py::Tool 要求:
namedescriptionparametersasync execute(**kwargs)
一个只做纯文本统计的最小形态:
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()))这是说明契约的示例,不要求现在写入 nanobot 源码。
2. 为什么 description 也是运行逻辑
Provider 看到的是 Tool.to_schema() 生成的 function schema。模型主要依据:
- name 判断大类;
- description 判断何时使用;
- parameter description 决定每个字段的语义。
模糊的 schema 会让模型传入错误参数,即使 Python 实现完全正确。描述应说明用途和边界,不要把内部实现细节堆给模型。
3. 参数转换与校验顺序
ToolRegistry.execute() 会:
- 按名称找 Tool;
- 将可安全转换的字符串转成 integer/number/boolean;
- 调用
validate_params(); - 校验失败时返回明确错误;
- 校验成功才调用
execute()。
Tool 的 JSON Schema validator 支持常见类型、required、enum、min/max、字符串长度、数组和嵌套对象。业务约束仍可覆盖 validate_params()。
Schema 校验不等于业务授权
"path"是字符串,只能证明类型正确;是否允许访问该路径仍要调用 workspace policy。
4. 内置 Tool 如何被发现
ToolLoader.discover() 用 pkgutil.iter_modules() 扫描 nanobot.agent.tools:
- 跳过
base、schema、registry、loader、mcp等基础模块; - 导入其他公开模块;
- 查找具体、非抽象、非私有的
Tool子类; - 跳过
_plugin_discoverable=False; - 按类名排序。
因此,内置 Tool 通常只需放入一个可扫描模块,不需要在 AgentLoop 手工 import/register。需要 AgentLoop 本身引用的特殊 Tool 可以像 MyTool 一样关闭自动发现并手工注册,但这不是普通扩展的首选。
5. 外部 Tool plugin
不想改 nanobot 源码时,可在独立 Python 包中暴露 entry point:
[project.entry-points."nanobot.tools"]
word-count = "my_nanobot_tools:WordCountTool"ToolLoader._discover_plugins() 会加载 nanobot.tools 组中的 class。
名称冲突规则:
- 内置 Tool 先加载;
- plugin 若与内置 Tool 的
tool.name冲突,会被跳过; - 其他碰撞会记录警告,后注册项可能覆盖已有项。
不要依赖覆盖行为实现“猴子补丁”;应选择唯一、稳定的 Tool name。
6. ToolContext 与生命周期
Tool 可通过 classmethod 接入运行时:
@classmethod
def enabled(cls, ctx: ToolContext) -> bool:
...
@classmethod
def create(cls, ctx: ToolContext) -> Tool:
...ToolContext 可提供:
- tools config;
- workspace;
- MessageBus;
- subagent manager;
- Cron service;
- file state store;
- Provider snapshot loader;
- image generation provider config;
- timezone;
- RuntimeEventBus 等扩展上下文。
只读取实际需要的字段。一个纯函数 Tool 不必依赖整个 runtime。
7. scope
_scopes 决定 Tool 在哪些 registry 中出现:
coresubagentmemory
基类默认只有 {"core"}。例如,允许 main Agent 使用的工具不一定适合 Dream/Memory runner 或 subagent。
把 Tool 加入 subagent 前,检查:
- 是否会修改 main Session;
- 是否需要主 MessageBus;
- 是否依赖只有 main loop 才有的状态;
- workspace 权限是否仍正确。
对应实现与测试:
nanobot/agent/tools/loader.py::ToolLoader.loadtests/agent/test_tool_loader_scopes.py
8. 配置接入
内置 Tool 常用:
config_keyconfig_cls()enabled(ctx)create(ctx)
例如 ExecTool 用 config_key="exec",配置类型在 shell.py,ToolsConfig 通过延迟导入解析类型。
不要假设 entry point 自动完成所有配置接入
ToolLoader负责发现和实例化,不会替你设计 WebUI 表单、配置迁移或文档。外部 plugin 若需要自定义配置,应实际验证ToolsConfig的 extra 字段形态,并在enabled/create中显式解析;内置 Tool 则还要同步nanobot/config/schema.py中的ToolsConfig。
9. 并发属性
基类定义:
read_only=Falseexclusive=Falseconcurrency_safe = read_only and not exclusive
只有当并发执行不会竞争共享状态时,才设置 read_only=True 或覆盖并发属性。只读网络请求也可能受共享 rate limit、非线程安全 client 或全局 cursor 影响。
10. 安全与结果契约
新 Tool 至少考虑:
- 路径:复用
resolve_workspace_path(); - URL:复用
validate_url_target(); - secret:不把 key 写进 schema、结果或日志;
- timeout:外部 I/O 必须有界;
- cancellation:释放文件、client、process;
- output:限制体积,避免一次结果塞满上下文;
- error:给模型可行动的错误,但不要提示绕过策略;
- side effect:description 明确行为,必要时要求显式确认。
11. 最小测试集
- schema 正确;
- 合法参数返回预期;
- 缺失、错误类型和边界值;
enabled/create读取真实 config 对象;- ToolLoader 能发现且 scope 正确;
- 文件/网络安全边界;
- 取消、超时和资源清理;
- 结果过大或外部错误。
测试参考:
tests/tools/test_tool_loader.pytests/agent/test_tool_loader_entrypoints.pytests/agent/test_tool_loader_scopes.pytests/tools/test_tool_registry.py- 与同类 Tool 对应的
tests/tools/test_*.py
12. 最小修改范围
内置 Tool
nanobot/agent/tools/<name>.pynanobot/config/schema.py(仅当需要一等配置)tests/tools/test_<name>.py- 用户配置文档(仅当存在用户可配置项)
外部 plugin
- 独立 package;
- entry point;
- plugin 自己的 tests/readme;
- 不改
AgentLoop。
自检
async def是否意味着 Tool 可以并发?- 为什么参数 schema 不能代替路径授权?
- 内置 Tool 与 entry-point Tool 的注册路径有何不同?
- 一个 Skill 已能完成的流程,为什么不应立即新增 Tool?
下一篇:新增 Skill