ツールシステム ToolRegistry
職務
Tool は agent が呼べる原子関数(ターミナル、ブラウザ、ファイル、MCP、スキル呼び出し……)。ToolRegistry はモジュールインポート時に AST 走査で tools/*.py のツール (tool) を自動発見して登録し、name → ToolEntry のルックアップテーブルを主ループのツールディスパッチ (tool dispatch) に供給する。ツールは静的——スキルの「進化可能」と対照的。
設計動機
なぜハードコード分岐ではなくレジストリ (registry) にするのか? hermes-agent には約 80 のツールファイルがある。新ツールを追加するたびに if name == "xxx" チェーンに登録していたら、すぐ腐る。AST 走査により、「tools/ に registry.register を呼ぶファイルを置く」こと自体が接入アクションになる。registry はグローバルシングルトンで、MCP 動的ツール、プラグイン (plugin)、組み込みツールが一枚のテーブルを共用する。
主要ファイル
discover_builtin_tools:67-86— AST 走査でtools/*.pyからツール登録モジュールを見つける。
def discover_builtin_tools(tools_dir: Optional[Path] = None) -> List[str]:
tools_path = Path(tools_dir) if tools_dir is not None else Path(__file__).resolve().parent
module_names = [
f"tools.{path.stem}"
for path in sorted(tools_path.glob("*.py"))
if path.name not in {"__init__.py", "registry.py", "mcp_tool.py"}
and _module_registers_tools(path)
]
imported: List[str] = []
for mod_name in module_names:
try:
importlib.import_module(mod_name)
imported.append(mod_name)
except Exception as e:
logger.warning("Could not import tool module %s: %s", mod_name, e)
return imported__init__.py / registry.py / mcp_tool.py をスキップし、残りの .py は AST で registry.register を呼んでいれば import する。import 動作自体がモジュールレベルの register(...) を引き起こす。単一モジュールの import 失敗は warning にとどめる。
_is_registry_register_call / _module_registers_tools:30-43— モジュールがregistry.registerを呼ぶか判定class ToolEntry:87-153— 単一ツールのメタデータ。__slots__で 80+ entry の占有を最小化する。
class ToolEntry:
__slots__ = (
"name", "toolset", "schema", "handler", "check_fn",
"requires_env", "is_async", "description", "emoji",
"max_result_size_chars", "dynamic_schema_overrides",
)dynamic_schema_overrides は引数なし callable で、get_definitions() のたびに呼ばれ、実行時設定に依存するフィールド(delegate_task の max_concurrent_children など)を schema にマージし、モデルに誤った上限を見せないようにする。
class ToolRegistry:217-365— レジストリ本体def register:365-436— ツール一つを登録。核心は上書きと衝突判定だ。
def register(self, name, toolset, schema, handler, ..., override: bool = False):
with self._lock:
existing = self._tools.get(name)
if existing and existing.toolset != toolset:
both_mcp = (existing.toolset.startswith("mcp-")
and toolset.startswith("mcp-"))
if both_mcp:
... # MCP 刷新覆盖 MCP
elif override:
# プラグインが明示 opt-in で組み込みを上書き、operator の allow_tool_override が必要
...
else:
logger.error("Tool registration REJECTED: '%s' ...", name, toolset, existing.toolset)
return
self._tools[name] = ToolEntry(...)
self._generation += 1同名ツールの上書きはデフォルトで拒否される。MCP 対 MCP は許可(notifications/tools/list_changed のリフレッシュシナリオ)、プラグインが組み込みを上書きするには override=True + operator の allow_tool_override: true が要り、さもないと PermissionError。_generation のインクリメントで外側のキャッシュが memoize できる。
def get_entry(self, name: str) -> Optional[ToolEntry]:
"""Return a registered tool entry by name, or None."""
with self._lock:
return self._tools.get(name)主ループは tool_call.name を受け取ったら get_entry を一回呼ぶだけで handler を取得する。register_toolset_alias により tools="mcp-foo" のような動的 toolset を設定に短く書ける。
register_plugin_override_policy:316-365— プラグインツールの名前空間准入ポリシーregistry シングルトン:765—registry = ToolRegistry()(モジュールレベルシングルトン)tool_error / tool_result:784-810— ツール戻り値の標準ラップtools/ ディレクトリ— 約 80 のツールファイル(terminal/browser/web/file/mcp/skills/delegate…)
データフロー
init_agent:276が toolsets 設定をトリガーする。discover_builtin_tools:67がtools/を AST 走査し、該当モジュールを import すると、モジュールレベルのregistry.register(...)がToolEntryをregistry シングルトン:765に書き込む。- 主ループは毎ラウンド、有効な toolsets に対応する
ToolEntry.schemaを provider へ送るtoolsフィールドに集める。 - provider が tool_call を返す →
get_entry(name)(tools/registry.py:274) で handler を探して実行 → 結果をtool_result:798でラップして戻す。
境界と失敗
- ツール名衝突:同名の組み込みツールの登録は拒否される。プラグインが組み込みを上書きするには
override=True+ operator のallow_tool_override: trueが必要で、さもないとPermissionError。MCP 対 MCP(mcp-プレフィックス)だけは例外で、server のリフレッシュ時の nuke-and-repave は合法。 - check_fn の揺らぎ:Docker / playwright のプローブが瞬時に False を返すことがある。
_check_fn_cachedは 30s TTL + 60s grace window を持ち、grace window 内では新しいプローブが失敗しても前回成功した True を返す。これにより delegate_task の子 agent が突然「Tool read_file does not exist」と報告するのを防ぐ。 - MCP の並行:MCP server が
tools/list_changedを push している最中に別スレッドが読むことがある。_lockはRLockで、mutate は直列化され、reader は snapshot を取る。_generationで外側のキャッシュヒットを検知する。 - モジュール import 失敗:ツールファイルの import error はレジストリを道連れにせず、warning でスキップされる。ただし該当ツールは主ループから呼べない——agent.log の "Could not import tool module" を確認すること。
まとめ
ツールシステムの核は「インポート即登録」——AST 走査で手動のリスト管理を回避し、新規ツールは tools/ に register を呼ぶファイルを置くだけ。registry シングルトンが唯一の真実の情報源で、主ループは読むだけ。スキルとの違い:ツールは固定関数、スキルは学習ループで書き換え可能なフロー。