Skip to content

Sistema de herramientas (tool) ToolRegistry

源码版本v2026.7.20

Responsabilidad

Las Tools son funciones atómicas que el agent puede invocar (terminal, navegador, archivos, MCP, llamadas a skills…). ToolRegistry descubre automáticamente al importar el módulo, vía escaneo AST, las herramientas en tools/*.py y las registra, ofreciendo al bucle principal una tabla name → ToolEntry para despachar las llamadas. Las herramientas son estáticas, en contraste con la «evolutividad» de las habilidades.

Motivo de diseño

¿Por qué un registro (registry) en lugar de ramas hardcodeadas? hermes-agent tiene unos 80 archivos de herramienta; añadir una nueva exigiendo meterla en una cadena if name == "xxx" se pudre enseguida. El escaneo AST convierte «soltar en tools/ un archivo que llama a registry.register» en la acción de integración. registry es un singleton global; herramientas dinámicas de MCP, plugins y herramientas internas comparten la misma tabla.

Archivos clave

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

Salta __init__.py / registry.py / mcp_tool.py; el resto de .py se importa si el AST detecta que llama a registry.register, y la propia importación dispara el register(...) a nivel de módulo. Un módulo que falle al importar solo queda como 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 es un callable sin argumentos que se invoca cada vez que get_definitions() reúne los schemas, para mezclar campos que dependen de la config en runtime (como max_concurrent_children en delegate_task) y evitar que el modelo vea límites incorrectos.

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 refresca y pisa a MCP
            elif override:
                # un plugin pisa explícitamente uno interno; pide allow_tool_override del operator
                ...
            else:
                logger.error("Tool registration REJECTED: '%s' ...", name, toolset, existing.toolset)
                return
        self._tools[name] = ToolEntry(...)
        self._generation += 1

Mismo nombre de herramienta: por defecto se rechaza el override. MCP sobre MCP está permitido (escenario de refresco con notifications/tools/list_changed); un plugin pisando uno interno necesita override=True + allow_tool_override: true del operator, si no PermissionError. El _generation que se incrementa permite a las cachés externas detectar invalidación.

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)

El bucle principal, tras recibir tool_call.name, hace un único get_entry y obtiene el handler. register_toolset_alias permite escribir de forma más corta toolsets dinámicos como tools="mcp-foo" en la config.

Flujo de datos

  1. init_agent:276 dispara la configuración de toolsets.
  2. discover_builtin_tools:67 escanea tools/ con AST; los módulos que pasan el filtro se importan y su registry.register(...) a nivel de módulo escribe el ToolEntry en el singleton registry:765.
  3. El bucle principal, cada turno (turn), recolecta los ToolEntry.schema de los toolsets activos en el campo tools enviado al provider.
  4. El provider devuelve un tool_call → el bucle principal hace get_entry(name) (tools/registry.py:274) para encontrar el handler y ejecutarlo → el resultado se envuelve con tool_result:798 y se devuelve al bucle principal.

Límites y fallos

  • Colisión de nombres: una herramienta interna con el mismo nombre se rechaza al registrar; un plugin que la pise requiere override=True + allow_tool_override: true del operator, si no PermissionError. La excepción es MCP contra MCP (prefijo mcp-): el refresco nuke-and-repave del server es legítimo.
  • Temblores del check_fn: las sondas de Docker / playwright pueden devolver False transitorio. _check_fn_cached tiene TTL de 30s + grace window de 60s: dentro de la grace, aunque la sonda nueva falle, se devuelve el último True, para que un subagent de delegate_task no suene de repente con «Tool read_file does not exist».
  • Concurrencia MCP: un server MCP puede emitir tools/list_changed mientras otro hilo lo lee. El _lock es un RLock: mutaciones serializadas, lectores por snapshot. El _generation permite a las cachés externas detectar hits.
  • Import de módulo fallido: un archivo de tool con import error no mata el registro, solo suelta un warning y se salta; pero esa herramienta no se podrá invocar desde el bucle principal — buscar en agent.log "Could not import tool module".

Resumen

La sutileza del sistema de herramientas está en «importar = registrar»: el escaneo AST evita mantener una lista a mano; añadir una herramienta es tan simple como soltar en tools/ un archivo que llame a register. El singleton registry es la única fuente de verdad; el bucle principal solo lo lee. La diferencia con las habilidades: las herramientas son funciones estáticas; las habilidades, flujos que el bucle de aprendizaje puede reescribir.

Sitio de aprendizaje comunitario no oficial. Basado en el código fuente de NousResearch/hermes-agent (licencia MIT).