Skip to content

ツールシステム ToolRegistry

源码版本v2026.7.20

職務

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)、組み込みツールが一枚のテーブルを共用する。

主要ファイル

python
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 にとどめる。

python
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_taskmax_concurrent_children など)を schema にマージし、モデルに誤った上限を見せないようにする。

python
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 できる。

python
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 を設定に短く書ける。

データフロー

  1. init_agent:276 が toolsets 設定をトリガーする。
  2. discover_builtin_tools:67tools/ を AST 走査し、該当モジュールを import すると、モジュールレベルの registry.register(...)ToolEntryregistry シングルトン:765 に書き込む。
  3. 主ループは毎ラウンド、有効な toolsets に対応する ToolEntry.schema を provider へ送る tools フィールドに集める。
  4. 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 している最中に別スレッドが読むことがある。_lockRLock で、mutate は直列化され、reader は snapshot を取る。_generation で外側のキャッシュヒットを検知する。
  • モジュール import 失敗:ツールファイルの import error はレジストリを道連れにせず、warning でスキップされる。ただし該当ツールは主ループから呼べない——agent.log の "Could not import tool module" を確認すること。

まとめ

ツールシステムの核は「インポート即登録」——AST 走査で手動のリスト管理を回避し、新規ツールは tools/register を呼ぶファイルを置くだけ。registry シングルトンが唯一の真実の情報源で、主ループは読むだけ。スキルとの違い:ツールは固定関数、スキルは学習ループで書き換え可能なフロー。

非公式コミュニティ学習サイト。MIT ライセンスの NousResearch/hermes-agent ソースに基づく。