Skip to content

Système d'outils ToolRegistry

源码版本v2026.7.20

Responsabilité

Un Tool est une fonction atomique que l'agent peut appeler (terminal, navigateur, fichier, MCP, appel de compétence…). À l'import du module, ToolRegistry découvre automatiquement, via un scan AST, les outils (tools) déclarés dans tools/*.py et les enregistre ; il expose une table de correspondance name → ToolEntry que la boucle principale utilise pour distribuer les appels d'outils. Les outils sont statiques — par opposition aux compétences qui sont « évolutives ».

Mot de conception

Pourquoi un registre (registry) plutôt que des branches codées en dur ? hermes-agent a environ 80 fichiers d'outils ; si chaque nouvel outil devait être enregistré dans une chaîne if name == "xxx", ça pourrirait vite. Le scan AST fait que « déposer dans tools/ un fichier qui appelle registry.register » est l'action d'intégration. registry est un singleton global ; les outils dynamiques MCP, les plugins (plugins) et les outils intégrés partagent une seule table.

Fichiers clés

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

On saute __init__.py / registry.py / mcp_tool.py ; les autres .py sont importés dès que l'AST confirme qu'ils ont appelé registry.register, et l'import lui-même déclenche le register(...) au niveau module. L'échec d'import d'un module ne produit qu'un 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 est un callable zéro-arg, invoqué à chaque get_definitions(), pour fusionner dans le schema les champs dépendant de la config runtime (par ex. max_concurrent_children de delegate_task) — afin que le modèle ne voie pas des limites erronées.

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 refresh écrase MCP
            elif override:
                # plugin opt-in explicite pour écraser un builtin, nécessite operator allow_tool_override
                ...
            else:
                logger.error("Tool registration REJECTED: '%s' ...", name, toolset, existing.toolset)
                return
        self._tools[name] = ToolEntry(...)
        self._generation += 1

Le remplacement d'un outil homonyme est refusé par défaut. MCP contre MCP est autorisé (scénario de refresh notifications/tools/list_changed) ; un plugin qui écrase un builtin nécessite override=True + operator allow_tool_override: true, sinon PermissionError. L'incrément de _generation permet au cache externe de détecter un miss.

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)

La boucle principale, après avoir reçu tool_call.name, fait un seul get_entry pour récupérer le handler. register_toolset_alias permet d'écrire plus court, dans la config, les toolsets dynamiques comme tools="mcp-foo".

Flux de données

  1. Au démarrage, init_agent:276 déclenche la configuration des toolsets.
  2. discover_builtin_tools:67 parcourt le répertoire tools/ et utilise un scan AST pour déterminer si chaque module appelle registry.register ; les modules retenus sont importés, et leurs appels registry.register(...) au niveau module s'exécutent et écrivent un ToolEntry dans singleton registry:765.
  3. À chaque tour, la boucle principale collecte les ToolEntry.schema des toolsets activés dans le champ tools envoyé au provider.
  4. Le provider renvoie un tool_call → la boucle principale utilise get_entry(name) (tools/registry.py:274) pour trouver le handler, l'exécute → le résultat est enveloppé via tool_result:798 et réinjecté.

Limites et échecs

  • Conflit de noms d'outils : un outil intégré homonyme refuse l'enregistrement ; un plugin qui écrase un builtin nécessite override=True + operator allow_tool_override: true, sinon PermissionError. Exception : MCP contre MCP (préfixe mcp-), où le nuke-and-repave du server au refresh est légitime.
  • Bruit check_fn : les sondes Docker / playwright peuvent retourner False ponctuellement. _check_fn_cached a un TTL 30 s + fenêtre de grâce 60 s : dans la fenêtre de grâce, même si la nouvelle sonde échoue, on renvoie le dernier True connu — pour éviter qu'un sous-agent delegate_task se plaigne soudain « Tool read_file does not exist ».
  • Concurrence MCP : quand le server MCP pousse tools/list_changed, des threads peuvent être en train de lire. _lock est un RLock, les mutations sont sérialisées, les lecteurs passent par un snapshot. _generation permet au cache externe de détecter un miss.
  • Échec d'import de module : un fichier d'outil en erreur d'import ne fait pas tomber le registre, seul un warning est émis et l'outil est sauté — mais la boucle principale ne pourra pas l'appeler ; chercher « Could not import tool module » dans agent.log.

Résumé

La subtilité du système d'outils tient à « importer c'est s'enregistrer » — grâce au scan AST, on évite de maintenir une liste à la main ; ajouter un outil consiste simplement à déposer dans tools/ un fichier qui appelle register. Le singleton registry est la seule source de vérité globale, et la boucle principale se contente de le lire. Différence avec les compétences : les outils sont des fonctions figées, les compétences sont des flux que la boucle d'apprentissage peut réécrire.

Site d'apprentissage communautaire non officiel. Basé sur le code source de NousResearch/hermes-agent (licence MIT).