How the Tool Registry System Works in Hermes Agent: A Complete Guide
Hermes Agent uses a central singleton registry in 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. 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) 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.
# 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 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. This function:
- Resolves requested toolsets via
toolsets.resolve_toolset(). - Queries
registry.get_definitions()with the resolved tool names. - Filters out tools where
check_fnreturns 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:
- Looks up the
ToolEntryby name. - Executes the handler function with provided arguments.
- Automatically wraps async handlers (when
is_async=True) using_run_async. - 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.
# 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.
# 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:
{
"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.pyto 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.pythat triggers registration on import. - Schema provision filters unavailable tools using
check_fnandrequires_envparameters 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. 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →