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). · Privacidad · Términos · Acerca de