| --- |
| sidebar_position: 2 |
| title: "Adding Tools" |
| description: "How to add a new tool to Hermes Agent — schemas, handlers, registration, and toolsets" |
| --- |
| |
| # Adding Tools |
|
|
| Before writing a tool, ask yourself: **should this be a [skill](creating-skills.md) instead?** |
|
|
| Make it a **Skill** when the capability can be expressed as instructions + shell commands + existing tools (arXiv search, git workflows, Docker management, PDF processing). |
|
|
| Make it a **Tool** when it requires end-to-end integration with API keys, custom processing logic, binary data handling, or streaming (browser automation, TTS, vision analysis). |
|
|
| ## Overview |
|
|
| Adding a tool touches **3 files**: |
|
|
| 1. **`tools/your_tool.py`** — handler, schema, check function, `registry.register()` call |
| 2. **`toolsets.py`** — add tool name to `_HERMES_CORE_TOOLS` (or a specific toolset) |
| 3. **`model_tools.py`** — add `"tools.your_tool"` to the `_discover_tools()` list |
|
|
| ## Step 1: Create the Tool File |
|
|
| Every tool file follows the same structure: |
|
|
| ```python |
| # tools/weather_tool.py |
| """Weather Tool -- look up current weather for a location.""" |
| |
| import json |
| import os |
| import logging |
| |
| logger = logging.getLogger(__name__) |
| |
| |
| # --- Availability check --- |
| |
| def check_weather_requirements() -> bool: |
| """Return True if the tool's dependencies are available.""" |
| return bool(os.getenv("WEATHER_API_KEY")) |
| |
| |
| # --- Handler --- |
| |
| def weather_tool(location: str, units: str = "metric") -> str: |
| """Fetch weather for a location. Returns JSON string.""" |
| api_key = os.getenv("WEATHER_API_KEY") |
| if not api_key: |
| return json.dumps({"error": "WEATHER_API_KEY not configured"}) |
| try: |
| # ... call weather API ... |
| return json.dumps({"location": location, "temp": 22, "units": units}) |
| except Exception as e: |
| return json.dumps({"error": str(e)}) |
| |
| |
| # --- Schema --- |
| |
| WEATHER_SCHEMA = { |
| "name": "weather", |
| "description": "Get current weather for a location.", |
| "parameters": { |
| "type": "object", |
| "properties": { |
| "location": { |
| "type": "string", |
| "description": "City name or coordinates (e.g. 'London' or '51.5,-0.1')" |
| }, |
| "units": { |
| "type": "string", |
| "enum": ["metric", "imperial"], |
| "description": "Temperature units (default: metric)", |
| "default": "metric" |
| } |
| }, |
| "required": ["location"] |
| } |
| } |
| |
| |
| # --- Registration --- |
| |
| from tools.registry import registry |
| |
| registry.register( |
| name="weather", |
| toolset="weather", |
| schema=WEATHER_SCHEMA, |
| handler=lambda args, **kw: weather_tool( |
| location=args.get("location", ""), |
| units=args.get("units", "metric")), |
| check_fn=check_weather_requirements, |
| requires_env=["WEATHER_API_KEY"], |
| ) |
| ``` |
|
|
| ### Key Rules |
|
|
| :::danger Important |
| - Handlers **MUST** return a JSON string (via `json.dumps()`), never raw dicts |
| - Errors **MUST** be returned as `{"error": "message"}`, never raised as exceptions |
| - The `check_fn` is called when building tool definitions — if it returns `False`, the tool is silently excluded |
| - The `handler` receives `(args: dict, **kwargs)` where `args` is the LLM's tool call arguments |
| ::: |
|
|
| ## Step 2: Add to a Toolset |
|
|
| In `toolsets.py`, add the tool name: |
|
|
| ```python |
| # If it should be available on all platforms (CLI + messaging): |
| _HERMES_CORE_TOOLS = [ |
| ... |
| "weather", # <-- add here |
| ] |
| |
| # Or create a new standalone toolset: |
| "weather": { |
| "description": "Weather lookup tools", |
| "tools": ["weather"], |
| "includes": [] |
| }, |
| ``` |
|
|
| ## Step 3: Add Discovery Import |
|
|
| In `model_tools.py`, add the module to the `_discover_tools()` list: |
|
|
| ```python |
| def _discover_tools(): |
| _modules = [ |
| ... |
| "tools.weather_tool", # <-- add here |
| ] |
| ``` |
|
|
| This import triggers the `registry.register()` call at the bottom of your tool file. |
|
|
| ## Async Handlers |
|
|
| If your handler needs async code, mark it with `is_async=True`: |
|
|
| ```python |
| async def weather_tool_async(location: str) -> str: |
| async with aiohttp.ClientSession() as session: |
| ... |
| return json.dumps(result) |
| |
| registry.register( |
| name="weather", |
| toolset="weather", |
| schema=WEATHER_SCHEMA, |
| handler=lambda args, **kw: weather_tool_async(args.get("location", "")), |
| check_fn=check_weather_requirements, |
| is_async=True, # registry calls _run_async() automatically |
| ) |
| ``` |
|
|
| The registry handles async bridging transparently — you never call `asyncio.run()` yourself. |
|
|
| ## Handlers That Need task_id |
| |
| Tools that manage per-session state receive `task_id` via `**kwargs`: |
| |
| ```python |
| def _handle_weather(args, **kw): |
| task_id = kw.get("task_id") |
| return weather_tool(args.get("location", ""), task_id=task_id) |
| |
| registry.register( |
| name="weather", |
| ... |
| handler=_handle_weather, |
| ) |
| ``` |
| |
| ## Agent-Loop Intercepted Tools |
|
|
| Some tools (`todo`, `memory`, `session_search`, `delegate_task`) need access to per-session agent state. These are intercepted by `run_agent.py` before reaching the registry. The registry still holds their schemas, but `dispatch()` returns a fallback error if the intercept is bypassed. |
|
|
| ## Optional: Setup Wizard Integration |
|
|
| If your tool requires an API key, add it to `hermes_cli/config.py`: |
|
|
| ```python |
| OPTIONAL_ENV_VARS = { |
| ... |
| "WEATHER_API_KEY": { |
| "description": "Weather API key for weather lookup", |
| "prompt": "Weather API key", |
| "url": "https://weatherapi.com/", |
| "tools": ["weather"], |
| "password": True, |
| }, |
| } |
| ``` |
|
|
| ## Checklist |
|
|
| - [ ] Tool file created with handler, schema, check function, and registration |
| - [ ] Added to appropriate toolset in `toolsets.py` |
| - [ ] Discovery import added to `model_tools.py` |
| - [ ] Handler returns JSON strings, errors returned as `{"error": "..."}` |
| - [ ] Optional: API key added to `OPTIONAL_ENV_VARS` in `hermes_cli/config.py` |
| - [ ] Optional: Added to `toolset_distributions.py` for batch processing |
| - [ ] Tested with `hermes chat -q "Use the weather tool for London"` |
|
|