Why Hermes Agent Manages 40+ Built‑in Tools Without a Config File
The article dissects Hermes Agent’s tool system, explaining its four‑layer architecture, singleton registry, import‑based auto‑registration, AST discovery, argument coercion, async bridging, error handling, schema design, toolset composition, and best‑practice recommendations, all backed by concrete code examples and design rationale.
Hermes Agent is an open‑source self‑evolving AI agent framework. Its Tools & Toolsets subsystem manages more than 40 built‑in tools without any configuration file.
1. Overall Architecture – Four Layers
The tool system is organized into four independent layers:
tools/*.py ← individual tool modules (self‑register on import)
↓ register()
tools/registry.py ← singleton registry (ToolRegistry)
↓ get_definitions() / dispatch()
model_tools.py ← orchestration layer (discover + provide schema + schedule)
↓ resolve_toolset()
toolsets.py ← toolset definitions (grouping & composition)
↓
run_agent.py/cli.py ← entry point (consumes tool definitions, drives Agent Loop)2. ToolRegistry – The Singleton Hub
2.1 Core Data Structure
tools/registry.pydefines a module‑level singleton: registry = ToolRegistry() Each registered tool is stored as a ToolEntry instance with the following slots: name – globally unique tool identifier toolset – belonging toolset (e.g., file, web) schema – OpenAI Function Calling JSON schema handler – synchronous or asynchronous processing function check_fn – availability check with 30 s TTL cache requires_env – required environment variables is_async – whether the handler is async description – human‑readable description emoji – icon shown in UI max_result_size_chars – result size limit to avoid context overflow dynamic_schema_overrides – callback for runtime schema adjustments
2.2 Registration Safety
The register() method rejects duplicate tool names across different toolsets unless the registration originates from an MCP refresh or the caller explicitly passes override=True. This prevents accidental overwriting of built‑in tools.
2.3 Helper Functions
Two utility functions simplify handler implementations:
def tool_error(message, **extra) -> str:
result = {"error": str(message)}
if extra:
result.update(extra)
return json.dumps(result, ensure_ascii=False)
def tool_result(data=None, **kwargs) -> str:
if data is not None:
return json.dumps(data, ensure_ascii=False)
return json.dumps(kwargs, ensure_ascii=False)Handlers can return tool_error(...) or tool_result(...) directly.
3. Auto‑Registration – Import Triggers Registration
Each tool module ends with a call to registry.register(...). When the module is imported, Python executes the top‑level code, automatically registering the tool. Example from tools/file_tools.py:
registry.register(
name="read_file",
toolset="file",
schema=READ_FILE_SCHEMA,
handler=_handle_read_file,
check_fn=_check_file_reqs,
emoji="📖",
max_result_size_chars=100_000,
)3.2 AST‑Based Discovery
The function discover_builtin_tools() walks the tools directory, parses each .py file with AST, and imports only those that contain a top‑level registry.register(...) call. This avoids importing helper modules that do not define tools.
def discover_builtin_tools(tools_dir=None):
tools_path = Path(tools_dir) or 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)
]
for mod_name in module_names:
importlib.import_module(mod_name) # triggers self‑registration _module_registers_tools()parses the AST and checks for a top‑level registry.register(...) call, ignoring function bodies.
4. Full Tool Dispatch Flow
From a model’s function call to the final JSON result, the steps are:
Agent Loop → handle_function_call()
↓ coerce_tool_args() # type coercion
↓ pre_tool_call hook # plugin interception
↓ registry.dispatch() # route to handler
↓ async bridge (_run_async) # run async if needed
↓ post_tool_call hook # post‑processing
↓ transform_tool_result # result conversion
↓ return JSON string4.1 Argument Coercion
LLM outputs often mismatch the declared schema. coerce_tool_args() normalises common cases:
"42" → 42 (string → integer)
"true" → true (string → boolean)
"https://a.com" → ["https://a.com"] (bare value → array when schema expects array)
"['a','b']" → ['a', 'b'] (JSON string → list)
The function also supports union types and nullable fields, handling quirks from Open‑weight models such as DeepSeek, Qwen, and GLM.
4.2 Async Bridge
_run_async()enables async tools to run inside a synchronous context. Three execution contexts are distinguished:
CLI main thread – uses a persistent event loop to avoid “Event loop is closed” errors.
Worker thread (e.g., delegate_task thread pool) – each thread owns its own loop.
Existing async context (e.g., gateway) – spawns a dedicated thread with a 300 s timeout.
This design prevents client objects (e.g., httpx.AsyncClient) from being tied to a loop that later closes.
4.3 Structured Error Handling
Tool execution errors are caught, sanitized, and returned as JSON:
try:
if entry.is_async:
return _run_async(entry.handler(args, **kwargs))
return entry.handler(args, **kwargs)
except Exception as e:
raw = f"Tool execution failed: {type(e).__name__}: {e}"
sanitized = _sanitize_tool_error(raw)
return json.dumps({"error": sanitized}) _sanitize_tool_error()strips framing tokens (XML tags, CDATA, code fences) to keep the error safe for LLM consumption.
5. Availability Checks with 30‑Second TTL
Tools that depend on external state provide a check_fn returning a boolean. The result is cached for 30 seconds to avoid repeated costly checks:
_CHECK_FN_TTL_SECONDS = 30.0
_check_fn_cache = {}
def _check_fn_cached(fn):
now = time.monotonic()
cached = _check_fn_cache.get(fn)
if cached and (now - cached[0]) < _CHECK_FN_TTL_SECONDS:
return cached[1]
try:
value = bool(fn())
except Exception:
value = False
_check_fn_cache[fn] = (now, value)
return valueWhen get_definitions() builds the tool list, any tool whose check_fn returns False is filtered out, preventing futile calls.
6. Toolsets – Grouping & Composition
toolsets.pydefines a TOOLSETS dictionary. Each entry lists direct tools and an includes list for nested toolsets, supporting recursive resolution and diamond‑dependency de‑duplication.
TOOLSETS = {
"web": {
"description": "Web research and content extraction tools",
"tools": ["web_search", "web_extract"],
"includes": []
},
"debugging": {
"description": "Debugging and troubleshooting toolkit",
"tools": ["terminal", "process"],
"includes": ["web", "file"]
},
"safe": {
"description": "Safe toolkit without terminal access",
"tools": [],
"includes": ["web", "vision", "image_gen"]
},
}Platform presets (e.g., hermes-cli, hermes-telegram, hermes-gateway) share a core list _HERMES_CORE_TOOLS of 70+ tools, while allowing platform‑specific extensions.
6.3 Runtime Custom Toolsets
Developers can create ad‑hoc toolsets via create_custom_toolset():
create_custom_toolset(
name="my_workflow",
description="Custom workflow toolset",
tools=["my_tool", "web_search"],
includes=["file"]
)The resulting custom toolset behaves identically to static definitions.
7. Three Registration Modes
7.1 Synchronous Tool (simplest)
Example: read_file in tools/file_tools.py registers a synchronous handler that returns JSON via tool_result().
7.2 Asynchronous Tool
Example: web_extract sets is_async=True. The async bridge handles execution without the handler needing explicit loop management.
7.3 Dynamic Schema Overrides
Tools like delegate_task provide a dynamic_schema_overrides callback that rebuilds parts of the schema based on current configuration, ensuring the model always sees up‑to‑date parameter descriptions.
8. Schema Design Guidelines
Follow OpenAI Function Calling JSON format.
Write clear multi‑line descriptions to guide the model.
Use enum for constrained values (e.g., target = ["content", "files"]).
Provide sensible defaults (e.g., limit = 50, offset = 0).
Limit array lengths with maxItems (e.g., urls max 5).
9. Best Practices
9.1 Tool Boundary Design
Each tool should do one thing (e.g., separate read_file, write_file, patch, search_files) instead of a generic file_operation with an action parameter.
9.2 Result Size Control
Set max_result_size_chars (e.g., 100 000) to prevent a single tool from blowing up the LLM context window.
9.3 Structured Error Returns
Always return errors via tool_error() rather than raising exceptions, allowing the model to handle failures gracefully.
9.4 Mandatory check_fn
Tools that rely on external services must provide a check_fn so unavailable tools are hidden from the model, reducing token waste.
9.5 Toolset Composition Strategy
Group tools by scenario and reuse via includes. For example, the debugging toolset includes both web and file tools because debugging often requires searching the web and reading files.
9.6 MCP Integration
Hermes supports the MCP protocol for external tools. MCP tools are registered under a toolset named mcp-<server_name> and participate in the same registry flow (including check_fn, dispatch(), and get_definitions()).
Conclusion
The Hermes Agent tool system succeeds by keeping registration simple (import‑time self‑registration), centralising metadata in a singleton registry, layering responsibilities, and embedding defensive mechanisms such as registration isolation, error sanitisation, argument coercion, and result size limits. These design choices make the framework highly extensible, easy to debug, and performant.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Shuge Unlimited
Formerly "Ops with Skill", now officially upgraded. Fully dedicated to AI, we share both the why (fundamental insights) and the how (practical implementation). From technical operations to breakthrough thinking, we help you understand AI's transformation and master the core abilities needed to shape the future. ShugeX: boundless exploration, skillful execution.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
