# How the Tool Registry System Works in Hermes Agent: A Complete Guide

> Discover how Hermes Agent's central tool registry system dynamically manages function schemas, Python handlers, and metadata for seamless tool discovery and dispatch. Learn to add new tools.

- Repository: [Nous Research/hermes-agent](https://github.com/NousResearch/hermes-agent)
- Tags: how-to-guide
- Published: 2026-03-09

---

**Hermes Agent uses a central singleton registry in [`tools/registry.py`](https://github.com/NousResearch/hermes-agent/blob/main/tools/registry.py) that collects OpenAI-style function schemas, Python handlers, and metadata for every tool, enabling dynamic discovery, filtering, and dispatch of function calls.**

The NousResearch/hermes-agent repository implements a sophisticated tool registry system that bridges large language models with executable Python functions. This architecture allows the agent to dynamically expose capabilities to the LLM while maintaining strict control over which tools are available based on environment configuration and runtime requirements.

## Architecture of the Tool Registry System

### The Central Singleton

The registry is implemented as a singleton in [`tools/registry.py`](https://github.com/NousResearch/hermes-agent/blob/main/tools/registry.py). It maintains an internal mapping of tool identifiers to `ToolEntry` objects, each containing the schema, handler function, requirement checks, and metadata necessary for execution.

### Tool Registration Pattern

Each tool module (such as [`tools/terminal_tool.py`](https://github.com/NousResearch/hermes-agent/blob/main/tools/terminal_tool.py)) imports the singleton registry and calls `registry.register()` during module initialization. This self-registration pattern ensures that importing a tool module automatically makes it available to the system.

```python

# Example from tools/terminal_tool.py

registry.register(
    name="terminal",
    toolset="terminal",
    schema=TERMINAL_SCHEMA,
    handler=_handle_terminal,
    check_fn=_check_terminal_requirements,
    requires_env=["TERMINAL_BACKEND"],
    is_async=False,
)

```

The registration parameters include:
- `name` – The identifier exposed to the LLM.
- `toolset` – The logical group used for enabling or disabling whole categories of tools.
- `schema` – The OpenAI function-definition JSON schema.
- `handler` – The Python callable that receives parsed arguments and returns a JSON string.
- `check_fn` – An optional callable returning **True** when external requirements (binaries, API keys) are satisfied.
- `requires_env` – A list of environment-variable names that trigger re-evaluation of the check function.
- `is_async` – Marks the handler as a coroutine; the registry bridges it with `_run_async`.

## How Tool Discovery Works

Tool discovery occurs in [`model_tools.py`](https://github.com/NousResearch/hermes-agent/blob/main/model_tools.py) through the `_discover_tools()` function. This function maintains a static list of module paths that are imported during initialization. When a module is imported, its top-level registration code executes, populating the central registry.

To add a new tool, you must append its module path to the `_modules` list in `_discover_tools()`.

## Schema Provision and Filtering

When the agent needs to inform the LLM about available capabilities, it calls `get_tool_definitions()` in [`model_tools.py`](https://github.com/NousResearch/hermes-agent/blob/main/model_tools.py). This function:

1. Resolves requested toolsets via `toolsets.resolve_toolset()`.
2. Queries `registry.get_definitions()` with the resolved tool names.
3. Filters out tools where `check_fn` returns **False**.

The `check_fn` parameter in `registry.register()` allows tools to verify runtime requirements before exposing their schema to the LLM. The `requires_env` list specifies environment variables that, when changed, trigger re-evaluation of the availability check.

## Function Dispatch and Execution

When the LLM invokes a function, `model_tools.handle_function_call()` routes the request to `registry.dispatch(name, args, …)`. The registry:

1. Looks up the `ToolEntry` by name.
2. Executes the handler function with provided arguments.
3. Automatically wraps async handlers (when `is_async=True`) using `_run_async`.
4. Catches exceptions and returns JSON error payloads.

This dispatch mechanism ensures consistent error handling and execution semantics across both synchronous and asynchronous tool implementations.

## Adding a New Tool to the Registry

Follow this complete example to add a weather lookup tool to the system.

**Step 1:** Create the module [`tools/weather_tool.py`](https://github.com/NousResearch/hermes-agent/blob/main/tools/weather_tool.py).

```python

# tools/weather_tool.py

import os
import json
from tools.registry import registry

# ------------------------------------------------------------

# Schema (OpenAI function definition)

# ------------------------------------------------------------

WEATHER_SCHEMA = {
    "name": "weather",
    "description": "Fetches the current weather for a city.",
    "parameters": {
        "type": "object",
        "properties": {
            "city": {"type": "string", "description": "Name of the city"},
            "units": {"type": "string", "enum": ["metric", "imperial"], "description": "Unit system"},
        },
        "required": ["city"],
    },
}

# ------------------------------------------------------------

# Optional requirement check (requires OPENWEATHER_API_KEY)

# ------------------------------------------------------------

def _weather_check() -> bool:
    return bool(os.getenv("OPENWEATHER_API_KEY"))

# ------------------------------------------------------------

# Handler – called by the registry

# ------------------------------------------------------------

def _handle_weather(args: dict, **kw) -> str:
    city = args.get("city")
    units = args.get("units", "metric")
    api_key = os.getenv("OPENWEATHER_API_KEY")
    # In a real implementation you would call the external API here.

    # For the knowledge‑base article we just stub a response.

    result = {
        "city": city,
        "temperature": 22,
        "units": units,
        "description": "Clear sky",
    }
    return json.dumps(result, ensure_ascii=False)

# ------------------------------------------------------------

# Register the tool with the central registry

# ------------------------------------------------------------

registry.register(
    name="weather",
    toolset="weather",
    schema=WEATHER_SCHEMA,
    handler=_handle_weather,
    check_fn=_weather_check,
    requires_env=["OPENWEATHER_API_KEY"],
    is_async=False,
    description="Get current weather data for a city",
)

```

**Step 2:** Add the module to discovery in [`model_tools.py`](https://github.com/NousResearch/hermes-agent/blob/main/model_tools.py).

```python

# In model_tools.py, inside _discover_tools()

_modules = [
    # … existing modules …

    "tools.weather_tool",   # ← new line

]

```

**Step 3:** Use the tool.

After restarting the agent, `get_tool_definitions()` will include the **weather** schema (provided the `OPENWEATHER_API_KEY` environment variable is set). The LLM can then invoke:

```json
{
  "name": "weather",
  "arguments": {"city": "Paris", "units": "metric"}
}

```

The registry will dispatch to `_handle_weather`, returning the JSON response to the agent.

## Summary

- The **tool registry system** in Hermes Agent uses a singleton pattern in [`tools/registry.py`](https://github.com/NousResearch/hermes-agent/blob/main/tools/registry.py) to manage tool metadata and execution.
- Tools **self-register** during module import by calling `registry.register()` with schemas, handlers, and requirement checks.
- **Discovery** occurs through a static module list in [`model_tools.py`](https://github.com/NousResearch/hermes-agent/blob/main/model_tools.py) that triggers registration on import.
- **Schema provision** filters unavailable tools using `check_fn` and `requires_env` parameters before exposing them to the LLM.
- The **dispatch system** handles both sync and async execution through `registry.dispatch()`, ensuring consistent error handling.

## Frequently Asked Questions

### How do I make a tool handler asynchronous?

Set `is_async=True` when calling `registry.register()`. The registry automatically wraps async handlers using `_run_async` during dispatch, allowing you to define the handler as `async def` and use `await` for I/O operations.

### Why is my new tool not appearing in the LLM's available functions?

Verify three things: First, ensure your module is imported in `_discover_tools()` in [`model_tools.py`](https://github.com/NousResearch/hermes-agent/blob/main/model_tools.py). Second, confirm that your `check_fn` returns **True** and any required environment variables listed in `requires_env` are set. Third, verify that the toolset containing your tool is enabled in the agent configuration.

### Can I disable specific toolsets at runtime?

Yes. The `get_tool_definitions()` function accepts a list of toolset names. By passing a filtered list to `toolsets.resolve_toolset()`, you can control which toolsets are exposed to the LLM without modifying the registry itself or restarting the agent.

### What happens if a tool handler raises an exception?

The `registry.dispatch()` method catches all exceptions and returns a JSON-formatted error string containing the exception message. This ensures the LLM receives structured feedback about tool execution failures rather than raw stack traces, allowing the conversation to continue gracefully.