2025-08-09 09:52:25 -07:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""
|
|
|
|
|
Model Tools Module
|
|
|
|
|
|
2026-02-21 20:22:33 -08:00
|
|
|
Thin orchestration layer over the tool registry. Each tool file in tools/
|
|
|
|
|
self-registers its schema, handler, and metadata via tools.registry.register().
|
|
|
|
|
This module triggers discovery (by importing all tool modules), then provides
|
|
|
|
|
the public API that run_agent.py, cli.py, batch_runner.py, and the RL
|
|
|
|
|
environments consume.
|
|
|
|
|
|
|
|
|
|
Public API (signatures preserved from the original 2,400-line version):
|
|
|
|
|
get_tool_definitions(enabled_toolsets, disabled_toolsets, quiet_mode) -> list
|
|
|
|
|
handle_function_call(function_name, function_args, task_id, user_task) -> str
|
|
|
|
|
TOOL_TO_TOOLSET_MAP: dict (for batch_runner.py)
|
|
|
|
|
TOOLSET_REQUIREMENTS: dict (for cli.py, doctor.py)
|
|
|
|
|
get_all_tool_names() -> list
|
|
|
|
|
get_toolset_for_tool(name) -> str
|
|
|
|
|
get_available_toolsets() -> dict
|
|
|
|
|
check_toolset_requirements() -> dict
|
|
|
|
|
check_tool_availability(quiet) -> tuple
|
2025-08-09 09:52:25 -07:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
import asyncio
|
2026-02-02 19:28:27 -08:00
|
|
|
import os
|
2026-02-21 20:22:33 -08:00
|
|
|
import logging
|
2026-03-20 09:44:50 -07:00
|
|
|
import threading
|
2026-02-02 19:28:27 -08:00
|
|
|
from typing import Dict, Any, List, Optional, Tuple
|
2025-08-09 09:52:25 -07:00
|
|
|
|
2026-02-21 20:22:33 -08:00
|
|
|
from tools.registry import registry
|
2026-02-20 23:23:32 -08:00
|
|
|
from toolsets import resolve_toolset, validate_toolset
|
2025-08-09 09:52:25 -07:00
|
|
|
|
2026-02-21 20:22:33 -08:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
2026-02-02 19:28:27 -08:00
|
|
|
|
refactor: deduplicate toolsets, unify async bridging, fix approval race condition, harden security
- Replace 4 copy-pasted messaging platform toolsets with shared _HERMES_CORE_TOOLS list
- Consolidate 5 ad-hoc async-bridging patterns into single _run_async() in model_tools.py
- Removes deprecated get_event_loop()/set_event_loop() calls
- Makes all tool handlers self-protecting regardless of caller's event loop state
- RL handler refactored from if/elif chain to dispatch dict
- Fix exec approval race condition: replace module-level globals with thread-safe
per-session tools/approval.py (submit_pending, pop_pending, approve_session, is_approved)
- Session A approving "rm" no longer approves it for all other sessions
- Fix config deep merge: user overriding tts.elevenlabs.voice_id no longer clobbers
tts.elevenlabs.model_id; migration detection now recurses to arbitrary depth
- Gateway default-deny: unauthenticated users denied unless GATEWAY_ALLOW_ALL_USERS=true
- Add 10 dangerous command patterns: rm --recursive, bash -c, python -e, curl|bash,
xargs rm, find -delete
- Sanitize gateway error messages: users see generic message, full traceback goes to logs
2026-02-21 18:28:49 -08:00
|
|
|
# =============================================================================
|
2026-02-21 20:22:33 -08:00
|
|
|
# Async Bridging (single source of truth -- used by registry.dispatch too)
|
refactor: deduplicate toolsets, unify async bridging, fix approval race condition, harden security
- Replace 4 copy-pasted messaging platform toolsets with shared _HERMES_CORE_TOOLS list
- Consolidate 5 ad-hoc async-bridging patterns into single _run_async() in model_tools.py
- Removes deprecated get_event_loop()/set_event_loop() calls
- Makes all tool handlers self-protecting regardless of caller's event loop state
- RL handler refactored from if/elif chain to dispatch dict
- Fix exec approval race condition: replace module-level globals with thread-safe
per-session tools/approval.py (submit_pending, pop_pending, approve_session, is_approved)
- Session A approving "rm" no longer approves it for all other sessions
- Fix config deep merge: user overriding tts.elevenlabs.voice_id no longer clobbers
tts.elevenlabs.model_id; migration detection now recurses to arbitrary depth
- Gateway default-deny: unauthenticated users denied unless GATEWAY_ALLOW_ALL_USERS=true
- Add 10 dangerous command patterns: rm --recursive, bash -c, python -e, curl|bash,
xargs rm, find -delete
- Sanitize gateway error messages: users see generic message, full traceback goes to logs
2026-02-21 18:28:49 -08:00
|
|
|
# =============================================================================
|
|
|
|
|
|
2026-03-20 09:44:50 -07:00
|
|
|
_tool_loop = None # persistent loop for the main (CLI) thread
|
|
|
|
|
_tool_loop_lock = threading.Lock()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _get_tool_loop():
|
|
|
|
|
"""Return a long-lived event loop for running async tool handlers.
|
|
|
|
|
|
|
|
|
|
Using a persistent loop (instead of asyncio.run() which creates and
|
|
|
|
|
*closes* a fresh loop every time) prevents "Event loop is closed"
|
|
|
|
|
errors that occur when cached httpx/AsyncOpenAI clients attempt to
|
|
|
|
|
close their transport on a dead loop during garbage collection.
|
|
|
|
|
"""
|
|
|
|
|
global _tool_loop
|
|
|
|
|
with _tool_loop_lock:
|
|
|
|
|
if _tool_loop is None or _tool_loop.is_closed():
|
|
|
|
|
_tool_loop = asyncio.new_event_loop()
|
|
|
|
|
return _tool_loop
|
|
|
|
|
|
|
|
|
|
|
refactor: deduplicate toolsets, unify async bridging, fix approval race condition, harden security
- Replace 4 copy-pasted messaging platform toolsets with shared _HERMES_CORE_TOOLS list
- Consolidate 5 ad-hoc async-bridging patterns into single _run_async() in model_tools.py
- Removes deprecated get_event_loop()/set_event_loop() calls
- Makes all tool handlers self-protecting regardless of caller's event loop state
- RL handler refactored from if/elif chain to dispatch dict
- Fix exec approval race condition: replace module-level globals with thread-safe
per-session tools/approval.py (submit_pending, pop_pending, approve_session, is_approved)
- Session A approving "rm" no longer approves it for all other sessions
- Fix config deep merge: user overriding tts.elevenlabs.voice_id no longer clobbers
tts.elevenlabs.model_id; migration detection now recurses to arbitrary depth
- Gateway default-deny: unauthenticated users denied unless GATEWAY_ALLOW_ALL_USERS=true
- Add 10 dangerous command patterns: rm --recursive, bash -c, python -e, curl|bash,
xargs rm, find -delete
- Sanitize gateway error messages: users see generic message, full traceback goes to logs
2026-02-21 18:28:49 -08:00
|
|
|
def _run_async(coro):
|
|
|
|
|
"""Run an async coroutine from a sync context.
|
|
|
|
|
|
|
|
|
|
If the current thread already has a running event loop (e.g., inside
|
|
|
|
|
the gateway's async stack or Atropos's event loop), we spin up a
|
|
|
|
|
disposable thread so asyncio.run() can create its own loop without
|
|
|
|
|
conflicting.
|
|
|
|
|
|
2026-03-20 09:44:50 -07:00
|
|
|
For the common CLI path (no running loop), we use a persistent event
|
|
|
|
|
loop so that cached async clients (httpx / AsyncOpenAI) remain bound
|
|
|
|
|
to a live loop and don't trigger "Event loop is closed" on GC.
|
|
|
|
|
|
refactor: deduplicate toolsets, unify async bridging, fix approval race condition, harden security
- Replace 4 copy-pasted messaging platform toolsets with shared _HERMES_CORE_TOOLS list
- Consolidate 5 ad-hoc async-bridging patterns into single _run_async() in model_tools.py
- Removes deprecated get_event_loop()/set_event_loop() calls
- Makes all tool handlers self-protecting regardless of caller's event loop state
- RL handler refactored from if/elif chain to dispatch dict
- Fix exec approval race condition: replace module-level globals with thread-safe
per-session tools/approval.py (submit_pending, pop_pending, approve_session, is_approved)
- Session A approving "rm" no longer approves it for all other sessions
- Fix config deep merge: user overriding tts.elevenlabs.voice_id no longer clobbers
tts.elevenlabs.model_id; migration detection now recurses to arbitrary depth
- Gateway default-deny: unauthenticated users denied unless GATEWAY_ALLOW_ALL_USERS=true
- Add 10 dangerous command patterns: rm --recursive, bash -c, python -e, curl|bash,
xargs rm, find -delete
- Sanitize gateway error messages: users see generic message, full traceback goes to logs
2026-02-21 18:28:49 -08:00
|
|
|
This is the single source of truth for sync->async bridging in tool
|
|
|
|
|
handlers. The RL paths (agent_loop.py, tool_context.py) also provide
|
|
|
|
|
outer thread-pool wrapping as defense-in-depth, but each handler is
|
|
|
|
|
self-protecting via this function.
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
loop = asyncio.get_running_loop()
|
|
|
|
|
except RuntimeError:
|
|
|
|
|
loop = None
|
|
|
|
|
|
|
|
|
|
if loop and loop.is_running():
|
|
|
|
|
import concurrent.futures
|
|
|
|
|
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
|
|
|
|
|
future = pool.submit(asyncio.run, coro)
|
|
|
|
|
return future.result(timeout=300)
|
2026-03-20 09:44:50 -07:00
|
|
|
|
|
|
|
|
tool_loop = _get_tool_loop()
|
|
|
|
|
return tool_loop.run_until_complete(coro)
|
refactor: deduplicate toolsets, unify async bridging, fix approval race condition, harden security
- Replace 4 copy-pasted messaging platform toolsets with shared _HERMES_CORE_TOOLS list
- Consolidate 5 ad-hoc async-bridging patterns into single _run_async() in model_tools.py
- Removes deprecated get_event_loop()/set_event_loop() calls
- Makes all tool handlers self-protecting regardless of caller's event loop state
- RL handler refactored from if/elif chain to dispatch dict
- Fix exec approval race condition: replace module-level globals with thread-safe
per-session tools/approval.py (submit_pending, pop_pending, approve_session, is_approved)
- Session A approving "rm" no longer approves it for all other sessions
- Fix config deep merge: user overriding tts.elevenlabs.voice_id no longer clobbers
tts.elevenlabs.model_id; migration detection now recurses to arbitrary depth
- Gateway default-deny: unauthenticated users denied unless GATEWAY_ALLOW_ALL_USERS=true
- Add 10 dangerous command patterns: rm --recursive, bash -c, python -e, curl|bash,
xargs rm, find -delete
- Sanitize gateway error messages: users see generic message, full traceback goes to logs
2026-02-21 18:28:49 -08:00
|
|
|
|
|
|
|
|
|
2026-02-02 19:28:27 -08:00
|
|
|
# =============================================================================
|
2026-02-21 20:22:33 -08:00
|
|
|
# Tool Discovery (importing each module triggers its registry.register calls)
|
2026-02-02 19:28:27 -08:00
|
|
|
# =============================================================================
|
|
|
|
|
|
2026-02-21 20:22:33 -08:00
|
|
|
def _discover_tools():
|
|
|
|
|
"""Import all tool modules to trigger their registry.register() calls.
|
|
|
|
|
|
|
|
|
|
Wrapped in a function so import errors in optional tools (e.g., fal_client
|
|
|
|
|
not installed) don't prevent the rest from loading.
|
|
|
|
|
"""
|
|
|
|
|
_modules = [
|
|
|
|
|
"tools.web_tools",
|
|
|
|
|
"tools.terminal_tool",
|
|
|
|
|
"tools.file_tools",
|
|
|
|
|
"tools.vision_tools",
|
|
|
|
|
"tools.mixture_of_agents_tool",
|
|
|
|
|
"tools.image_generation_tool",
|
|
|
|
|
"tools.skills_tool",
|
|
|
|
|
"tools.skill_manager_tool",
|
|
|
|
|
"tools.browser_tool",
|
|
|
|
|
"tools.cronjob_tools",
|
|
|
|
|
"tools.rl_training_tool",
|
|
|
|
|
"tools.tts_tool",
|
|
|
|
|
"tools.todo_tool",
|
|
|
|
|
"tools.memory_tool",
|
|
|
|
|
"tools.session_search_tool",
|
|
|
|
|
"tools.clarify_tool",
|
|
|
|
|
"tools.code_execution_tool",
|
|
|
|
|
"tools.delegate_tool",
|
|
|
|
|
"tools.process_registry",
|
|
|
|
|
"tools.send_message_tool",
|
2026-02-25 19:34:25 -05:00
|
|
|
"tools.honcho_tools",
|
2026-02-28 13:32:48 +03:00
|
|
|
"tools.homeassistant_tool",
|
2026-02-12 10:05:08 -08:00
|
|
|
]
|
2026-02-21 20:22:33 -08:00
|
|
|
import importlib
|
|
|
|
|
for mod_name in _modules:
|
|
|
|
|
try:
|
|
|
|
|
importlib.import_module(mod_name)
|
|
|
|
|
except Exception as e:
|
2026-03-17 04:31:26 -07:00
|
|
|
logger.warning("Could not import tool module %s: %s", mod_name, e)
|
2026-02-12 10:05:08 -08:00
|
|
|
|
|
|
|
|
|
2026-02-21 20:22:33 -08:00
|
|
|
_discover_tools()
|
Add messaging platform enhancements: STT, stickers, Discord UX, Slack, pairing, hooks
Major feature additions inspired by OpenClaw/ClawdBot integration analysis:
Voice Message Transcription (STT):
- Auto-transcribe voice/audio messages via OpenAI Whisper API
- Download voice to ~/.hermes/audio_cache/ on Telegram/Discord/WhatsApp
- Inject transcript as text so all models can understand voice input
- Configurable model (whisper-1, gpt-4o-mini-transcribe, gpt-4o-transcribe)
Telegram Sticker Understanding:
- Describe static stickers via vision tool with JSON-backed cache
- Cache keyed by file_unique_id avoids redundant API calls
- Animated/video stickers get emoji-based fallback description
Discord Rich UX:
- Native slash commands (/ask, /reset, /status, /stop) via app_commands
- Button-based exec approvals (Allow Once / Always Allow / Deny)
- ExecApprovalView with user authorization and timeout handling
Slack Integration:
- Full SlackAdapter using slack-bolt with Socket Mode
- DMs, channel messages (mention-gated), /hermes slash command
- File attachment handling with bot-token-authenticated downloads
DM Pairing System:
- Code-based user authorization as alternative to static allowlists
- 8-char codes from unambiguous alphabet, 1-hour expiry
- Rate limiting, lockout after failed attempts, chmod 0600 on data
- CLI: hermes pairing list/approve/revoke/clear-pending
Event Hook System:
- File-based hook discovery from ~/.hermes/hooks/
- HOOK.yaml + handler.py per hook, sync/async handler support
- Events: gateway:startup, session:start/reset, agent:start/step/end
- Wildcard matching (command:* catches all command events)
Cross-Channel Messaging:
- send_message agent tool for delivering to any connected platform
- Enables cron job delivery and cross-platform notifications
Human-Like Response Pacing:
- Configurable delays between message chunks (off/natural/custom)
- HERMES_HUMAN_DELAY_MODE env var with min/max ms settings
Warm Injection Message Style:
- Retrofitted image vision messages with friendly kawaii-consistent tone
- All new injection messages (STT, stickers, errors) use warm style
Also: updated config migration to prompt for optional keys interactively,
bumped config version, updated README, AGENTS.md, .env.example,
cli-config.yaml.example, install scripts, pyproject.toml, and toolsets.
2026-02-15 21:38:59 -08:00
|
|
|
|
feat: add MCP (Model Context Protocol) client support
Connect to external MCP servers via stdio transport, discover their tools
at startup, and register them into the hermes-agent tool registry.
- New tools/mcp_tool.py: config loading, server connection via background
event loop, tool handler factories, discovery, and graceful shutdown
- model_tools.py: trigger MCP discovery after built-in tool imports
- cli.py: call shutdown_mcp_servers in _run_cleanup
- pyproject.toml: add mcp>=1.2.0 as optional dependency
- 27 unit tests covering config, schema conversion, handlers, registration,
SDK interaction, toolset injection, graceful fallback, and shutdown
Config format (in ~/.hermes/config.yaml):
mcp_servers:
filesystem:
command: "npx"
args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
2026-03-02 21:03:14 +03:00
|
|
|
# MCP tool discovery (external MCP servers from config)
|
|
|
|
|
try:
|
|
|
|
|
from tools.mcp_tool import discover_mcp_tools
|
|
|
|
|
discover_mcp_tools()
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.debug("MCP tool discovery failed: %s", e)
|
|
|
|
|
|
feat: first-class plugin architecture (#1555)
Plugin system for extending Hermes with custom tools, hooks, and
integrations — no source code changes required.
Core system (hermes_cli/plugins.py):
- Plugin discovery from ~/.hermes/plugins/, .hermes/plugins/, and
pip entry_points (hermes_agent.plugins group)
- PluginContext with register_tool() and register_hook()
- 6 lifecycle hooks: pre/post tool_call, pre/post llm_call,
on_session_start/end
- Namespace package handling for relative imports in plugins
- Graceful error isolation — broken plugins never crash the agent
Integration (model_tools.py):
- Plugin discovery runs after built-in + MCP tools
- Plugin tools bypass toolset filter via get_plugin_tool_names()
- Pre/post tool call hooks fire in handle_function_call()
CLI:
- /plugins command shows loaded plugins, tool counts, status
- Added to COMMANDS dict for autocomplete
Docs:
- Getting started guide (build-a-hermes-plugin.md) — full tutorial
building a calculator plugin step by step
- Reference page (features/plugins.md) — quick overview + tables
- Covers: file structure, schemas, handlers, hooks, data files,
bundled skills, env var gating, pip distribution, common mistakes
Tests: 16 tests covering discovery, loading, hooks, tool visibility.
2026-03-16 07:17:36 -07:00
|
|
|
# Plugin tool discovery (user/project/pip plugins)
|
|
|
|
|
try:
|
|
|
|
|
from hermes_cli.plugins import discover_plugins
|
|
|
|
|
discover_plugins()
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.debug("Plugin discovery failed: %s", e)
|
|
|
|
|
|
Add messaging platform enhancements: STT, stickers, Discord UX, Slack, pairing, hooks
Major feature additions inspired by OpenClaw/ClawdBot integration analysis:
Voice Message Transcription (STT):
- Auto-transcribe voice/audio messages via OpenAI Whisper API
- Download voice to ~/.hermes/audio_cache/ on Telegram/Discord/WhatsApp
- Inject transcript as text so all models can understand voice input
- Configurable model (whisper-1, gpt-4o-mini-transcribe, gpt-4o-transcribe)
Telegram Sticker Understanding:
- Describe static stickers via vision tool with JSON-backed cache
- Cache keyed by file_unique_id avoids redundant API calls
- Animated/video stickers get emoji-based fallback description
Discord Rich UX:
- Native slash commands (/ask, /reset, /status, /stop) via app_commands
- Button-based exec approvals (Allow Once / Always Allow / Deny)
- ExecApprovalView with user authorization and timeout handling
Slack Integration:
- Full SlackAdapter using slack-bolt with Socket Mode
- DMs, channel messages (mention-gated), /hermes slash command
- File attachment handling with bot-token-authenticated downloads
DM Pairing System:
- Code-based user authorization as alternative to static allowlists
- 8-char codes from unambiguous alphabet, 1-hour expiry
- Rate limiting, lockout after failed attempts, chmod 0600 on data
- CLI: hermes pairing list/approve/revoke/clear-pending
Event Hook System:
- File-based hook discovery from ~/.hermes/hooks/
- HOOK.yaml + handler.py per hook, sync/async handler support
- Events: gateway:startup, session:start/reset, agent:start/step/end
- Wildcard matching (command:* catches all command events)
Cross-Channel Messaging:
- send_message agent tool for delivering to any connected platform
- Enables cron job delivery and cross-platform notifications
Human-Like Response Pacing:
- Configurable delays between message chunks (off/natural/custom)
- HERMES_HUMAN_DELAY_MODE env var with min/max ms settings
Warm Injection Message Style:
- Retrofitted image vision messages with friendly kawaii-consistent tone
- All new injection messages (STT, stickers, errors) use warm style
Also: updated config migration to prompt for optional keys interactively,
bumped config version, updated README, AGENTS.md, .env.example,
cli-config.yaml.example, install scripts, pyproject.toml, and toolsets.
2026-02-15 21:38:59 -08:00
|
|
|
|
2026-02-21 20:22:33 -08:00
|
|
|
# =============================================================================
|
|
|
|
|
# Backward-compat constants (built once after discovery)
|
|
|
|
|
# =============================================================================
|
Add background process management with process tool, wait, PTY, and stdin support
New process registry and tool for managing long-running background processes
across all terminal backends (local, Docker, Singularity, Modal, SSH).
Process Registry (tools/process_registry.py):
- ProcessSession tracking with rolling 200KB output buffer
- spawn_local() with optional PTY via ptyprocess for interactive CLIs
- spawn_via_env() for non-local backends (runs inside sandbox, never on host)
- Background reader threads per process (Popen stdout or PTY)
- wait() with timeout clamping, interrupt support, and transparent limit reporting
- JSON checkpoint to ~/.hermes/processes.json for gateway crash recovery
- Module-level singleton shared across agent loop, gateway, and RL
Process Tool (model_tools.py):
- 7 actions: list, poll, log, wait, kill, write, submit
- Paired with terminal in all toolsets (CLI, messaging, RL)
- Timeout clamping with transparent notes in response
Terminal Tool Updates (tools/terminal_tool.py):
- Replaced nohup background mode with registry spawn (returns session_id)
- Added workdir parameter for per-command working directory
- Added check_interval parameter for gateway auto-check watchers
- Added pty parameter for interactive CLI tools (Codex, Claude Code)
- Updated TERMINAL_TOOL_DESCRIPTION with full background workflow docs
- Cleanup thread now respects active background processes (won't reap sandbox)
Gateway Integration (gateway/run.py, session.py, config.py):
- Session reset protection: sessions with active processes exempt from reset
- Default idle timeout increased from 2 hours to 24 hours
- from_dict fallback aligned to match (was 120, now 1440)
- session_key env var propagated to process registry for session mapping
- Crash recovery on gateway startup via checkpoint probe
- check_interval watcher: asyncio task polls process, delivers updates to platform
RL Safety (environments/):
- tool_context.py cleanup() kills background processes on episode end
- hermes_base_env.py warns when enabled_toolsets is None (loads all tools)
- Process tool safe in RL via wait() blocking the agent loop
Also:
- Added ptyprocess as optional dependency (in pyproject.toml [pty] extra + [all])
- Fixed pre-existing bug: rl_test_inference missing from TOOL_TO_TOOLSET_MAP
- Updated AGENTS.md with process management docs and project structure
- Updated README.md terminal section with process management overview
2026-02-17 02:51:31 -08:00
|
|
|
|
2026-02-21 20:22:33 -08:00
|
|
|
TOOL_TO_TOOLSET_MAP: Dict[str, str] = registry.get_tool_to_toolset_map()
|
Add background process management with process tool, wait, PTY, and stdin support
New process registry and tool for managing long-running background processes
across all terminal backends (local, Docker, Singularity, Modal, SSH).
Process Registry (tools/process_registry.py):
- ProcessSession tracking with rolling 200KB output buffer
- spawn_local() with optional PTY via ptyprocess for interactive CLIs
- spawn_via_env() for non-local backends (runs inside sandbox, never on host)
- Background reader threads per process (Popen stdout or PTY)
- wait() with timeout clamping, interrupt support, and transparent limit reporting
- JSON checkpoint to ~/.hermes/processes.json for gateway crash recovery
- Module-level singleton shared across agent loop, gateway, and RL
Process Tool (model_tools.py):
- 7 actions: list, poll, log, wait, kill, write, submit
- Paired with terminal in all toolsets (CLI, messaging, RL)
- Timeout clamping with transparent notes in response
Terminal Tool Updates (tools/terminal_tool.py):
- Replaced nohup background mode with registry spawn (returns session_id)
- Added workdir parameter for per-command working directory
- Added check_interval parameter for gateway auto-check watchers
- Added pty parameter for interactive CLI tools (Codex, Claude Code)
- Updated TERMINAL_TOOL_DESCRIPTION with full background workflow docs
- Cleanup thread now respects active background processes (won't reap sandbox)
Gateway Integration (gateway/run.py, session.py, config.py):
- Session reset protection: sessions with active processes exempt from reset
- Default idle timeout increased from 2 hours to 24 hours
- from_dict fallback aligned to match (was 120, now 1440)
- session_key env var propagated to process registry for session mapping
- Crash recovery on gateway startup via checkpoint probe
- check_interval watcher: asyncio task polls process, delivers updates to platform
RL Safety (environments/):
- tool_context.py cleanup() kills background processes on episode end
- hermes_base_env.py warns when enabled_toolsets is None (loads all tools)
- Process tool safe in RL via wait() blocking the agent loop
Also:
- Added ptyprocess as optional dependency (in pyproject.toml [pty] extra + [all])
- Fixed pre-existing bug: rl_test_inference missing from TOOL_TO_TOOLSET_MAP
- Updated AGENTS.md with process management docs and project structure
- Updated README.md terminal section with process management overview
2026-02-17 02:51:31 -08:00
|
|
|
|
2026-02-21 20:22:33 -08:00
|
|
|
TOOLSET_REQUIREMENTS: Dict[str, dict] = registry.get_toolset_requirements()
|
2025-11-17 01:14:31 -05:00
|
|
|
|
2026-02-21 20:22:33 -08:00
|
|
|
# Resolved tool names from the last get_tool_definitions() call.
|
|
|
|
|
# Used by code_execution_tool to know which tools are available in this session.
|
|
|
|
|
_last_resolved_tool_names: List[str] = []
|
2025-11-17 01:14:31 -05:00
|
|
|
|
2025-08-09 09:52:25 -07:00
|
|
|
|
2026-02-21 20:22:33 -08:00
|
|
|
# =============================================================================
|
|
|
|
|
# Legacy toolset name mapping (old _tools-suffixed names -> tool name lists)
|
|
|
|
|
# =============================================================================
|
2025-08-09 09:52:25 -07:00
|
|
|
|
2026-02-21 20:22:33 -08:00
|
|
|
_LEGACY_TOOLSET_MAP = {
|
|
|
|
|
"web_tools": ["web_search", "web_extract"],
|
|
|
|
|
"terminal_tools": ["terminal"],
|
|
|
|
|
"vision_tools": ["vision_analyze"],
|
|
|
|
|
"moa_tools": ["mixture_of_agents"],
|
|
|
|
|
"image_tools": ["image_generate"],
|
|
|
|
|
"skills_tools": ["skills_list", "skill_view", "skill_manage"],
|
|
|
|
|
"browser_tools": [
|
|
|
|
|
"browser_navigate", "browser_snapshot", "browser_click",
|
|
|
|
|
"browser_type", "browser_scroll", "browser_back",
|
|
|
|
|
"browser_press", "browser_close", "browser_get_images",
|
2026-03-17 02:02:49 -07:00
|
|
|
"browser_vision", "browser_console"
|
2026-02-21 20:22:33 -08:00
|
|
|
],
|
2026-03-14 12:21:50 -07:00
|
|
|
"cronjob_tools": ["cronjob"],
|
2026-02-21 20:22:33 -08:00
|
|
|
"rl_tools": [
|
|
|
|
|
"rl_list_environments", "rl_select_environment",
|
|
|
|
|
"rl_get_current_config", "rl_edit_config",
|
|
|
|
|
"rl_start_training", "rl_check_status",
|
|
|
|
|
"rl_stop_training", "rl_get_results",
|
|
|
|
|
"rl_list_runs", "rl_test_inference"
|
|
|
|
|
],
|
|
|
|
|
"file_tools": ["read_file", "write_file", "patch", "search_files"],
|
|
|
|
|
"tts_tools": ["text_to_speech"],
|
2026-02-08 20:19:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-02-21 20:22:33 -08:00
|
|
|
# =============================================================================
|
|
|
|
|
# get_tool_definitions (the main schema provider)
|
|
|
|
|
# =============================================================================
|
2026-02-19 23:23:43 -08:00
|
|
|
|
2025-08-09 09:52:25 -07:00
|
|
|
def get_tool_definitions(
|
|
|
|
|
enabled_toolsets: List[str] = None,
|
2026-01-31 06:30:48 +00:00
|
|
|
disabled_toolsets: List[str] = None,
|
|
|
|
|
quiet_mode: bool = False,
|
2025-08-09 09:52:25 -07:00
|
|
|
) -> List[Dict[str, Any]]:
|
|
|
|
|
"""
|
2025-09-10 00:43:55 -07:00
|
|
|
Get tool definitions for model API calls with toolset-based filtering.
|
2026-02-21 20:22:33 -08:00
|
|
|
|
|
|
|
|
All tools must be part of a toolset to be accessible.
|
|
|
|
|
|
2025-08-09 09:52:25 -07:00
|
|
|
Args:
|
2026-02-21 20:22:33 -08:00
|
|
|
enabled_toolsets: Only include tools from these toolsets.
|
|
|
|
|
disabled_toolsets: Exclude tools from these toolsets (if enabled_toolsets is None).
|
|
|
|
|
quiet_mode: Suppress status prints.
|
|
|
|
|
|
2025-08-09 09:52:25 -07:00
|
|
|
Returns:
|
2026-02-21 20:22:33 -08:00
|
|
|
Filtered list of OpenAI-format tool definitions.
|
2025-08-09 09:52:25 -07:00
|
|
|
"""
|
2026-02-21 20:22:33 -08:00
|
|
|
# Determine which tool names the caller wants
|
|
|
|
|
tools_to_include: set = set()
|
2025-11-17 01:14:31 -05:00
|
|
|
|
2025-08-09 09:52:25 -07:00
|
|
|
if enabled_toolsets:
|
|
|
|
|
for toolset_name in enabled_toolsets:
|
2025-09-10 00:43:55 -07:00
|
|
|
if validate_toolset(toolset_name):
|
2026-02-21 20:22:33 -08:00
|
|
|
resolved = resolve_toolset(toolset_name)
|
|
|
|
|
tools_to_include.update(resolved)
|
|
|
|
|
if not quiet_mode:
|
|
|
|
|
print(f"✅ Enabled toolset '{toolset_name}': {', '.join(resolved) if resolved else 'no tools'}")
|
|
|
|
|
elif toolset_name in _LEGACY_TOOLSET_MAP:
|
|
|
|
|
legacy_tools = _LEGACY_TOOLSET_MAP[toolset_name]
|
|
|
|
|
tools_to_include.update(legacy_tools)
|
2026-02-02 23:46:41 -08:00
|
|
|
if not quiet_mode:
|
2026-02-21 20:22:33 -08:00
|
|
|
print(f"✅ Enabled legacy toolset '{toolset_name}': {', '.join(legacy_tools)}")
|
2025-08-09 09:52:25 -07:00
|
|
|
else:
|
2026-02-21 20:22:33 -08:00
|
|
|
if not quiet_mode:
|
|
|
|
|
print(f"⚠️ Unknown toolset: {toolset_name}")
|
|
|
|
|
|
2025-08-09 09:52:25 -07:00
|
|
|
elif disabled_toolsets:
|
2025-09-10 00:43:55 -07:00
|
|
|
from toolsets import get_all_toolsets
|
2026-02-21 20:22:33 -08:00
|
|
|
for ts_name in get_all_toolsets():
|
|
|
|
|
tools_to_include.update(resolve_toolset(ts_name))
|
|
|
|
|
|
2025-09-10 00:43:55 -07:00
|
|
|
for toolset_name in disabled_toolsets:
|
|
|
|
|
if validate_toolset(toolset_name):
|
2026-02-21 20:22:33 -08:00
|
|
|
resolved = resolve_toolset(toolset_name)
|
|
|
|
|
tools_to_include.difference_update(resolved)
|
|
|
|
|
if not quiet_mode:
|
|
|
|
|
print(f"🚫 Disabled toolset '{toolset_name}': {', '.join(resolved) if resolved else 'no tools'}")
|
|
|
|
|
elif toolset_name in _LEGACY_TOOLSET_MAP:
|
|
|
|
|
legacy_tools = _LEGACY_TOOLSET_MAP[toolset_name]
|
|
|
|
|
tools_to_include.difference_update(legacy_tools)
|
2026-02-02 23:46:41 -08:00
|
|
|
if not quiet_mode:
|
2026-02-21 20:22:33 -08:00
|
|
|
print(f"🚫 Disabled legacy toolset '{toolset_name}': {', '.join(legacy_tools)}")
|
2025-09-10 00:43:55 -07:00
|
|
|
else:
|
2026-02-21 20:22:33 -08:00
|
|
|
if not quiet_mode:
|
|
|
|
|
print(f"⚠️ Unknown toolset: {toolset_name}")
|
2025-09-10 00:43:55 -07:00
|
|
|
else:
|
|
|
|
|
from toolsets import get_all_toolsets
|
2026-02-21 20:22:33 -08:00
|
|
|
for ts_name in get_all_toolsets():
|
|
|
|
|
tools_to_include.update(resolve_toolset(ts_name))
|
|
|
|
|
|
feat: first-class plugin architecture (#1555)
Plugin system for extending Hermes with custom tools, hooks, and
integrations — no source code changes required.
Core system (hermes_cli/plugins.py):
- Plugin discovery from ~/.hermes/plugins/, .hermes/plugins/, and
pip entry_points (hermes_agent.plugins group)
- PluginContext with register_tool() and register_hook()
- 6 lifecycle hooks: pre/post tool_call, pre/post llm_call,
on_session_start/end
- Namespace package handling for relative imports in plugins
- Graceful error isolation — broken plugins never crash the agent
Integration (model_tools.py):
- Plugin discovery runs after built-in + MCP tools
- Plugin tools bypass toolset filter via get_plugin_tool_names()
- Pre/post tool call hooks fire in handle_function_call()
CLI:
- /plugins command shows loaded plugins, tool counts, status
- Added to COMMANDS dict for autocomplete
Docs:
- Getting started guide (build-a-hermes-plugin.md) — full tutorial
building a calculator plugin step by step
- Reference page (features/plugins.md) — quick overview + tables
- Covers: file structure, schemas, handlers, hooks, data files,
bundled skills, env var gating, pip distribution, common mistakes
Tests: 16 tests covering discovery, loading, hooks, tool visibility.
2026-03-16 07:17:36 -07:00
|
|
|
# Always include plugin-registered tools — they bypass the toolset filter
|
|
|
|
|
# because their toolsets are dynamic (created at plugin load time).
|
|
|
|
|
try:
|
|
|
|
|
from hermes_cli.plugins import get_plugin_tool_names
|
|
|
|
|
plugin_tools = get_plugin_tool_names()
|
|
|
|
|
if plugin_tools:
|
|
|
|
|
tools_to_include.update(plugin_tools)
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
|
2026-02-21 20:22:33 -08:00
|
|
|
# Ask the registry for schemas (only returns tools whose check_fn passes)
|
|
|
|
|
filtered_tools = registry.get_definitions(tools_to_include, quiet=quiet_mode)
|
|
|
|
|
|
2026-03-19 10:08:14 -07:00
|
|
|
# The set of tool names that actually passed check_fn filtering.
|
|
|
|
|
# Use this (not tools_to_include) for any downstream schema that references
|
|
|
|
|
# other tools by name — otherwise the model sees tools mentioned in
|
|
|
|
|
# descriptions that don't actually exist, and hallucinates calls to them.
|
|
|
|
|
available_tool_names = {t["function"]["name"] for t in filtered_tools}
|
|
|
|
|
|
2026-03-06 17:36:06 -08:00
|
|
|
# Rebuild execute_code schema to only list sandbox tools that are actually
|
2026-03-19 10:08:14 -07:00
|
|
|
# available. Without this, the model sees "web_search is available in
|
|
|
|
|
# execute_code" even when the API key isn't configured or the toolset is
|
|
|
|
|
# disabled (#560-discord).
|
|
|
|
|
if "execute_code" in available_tool_names:
|
2026-03-06 17:36:06 -08:00
|
|
|
from tools.code_execution_tool import SANDBOX_ALLOWED_TOOLS, build_execute_code_schema
|
2026-03-19 10:08:14 -07:00
|
|
|
sandbox_enabled = SANDBOX_ALLOWED_TOOLS & available_tool_names
|
2026-03-06 17:36:06 -08:00
|
|
|
dynamic_schema = build_execute_code_schema(sandbox_enabled)
|
|
|
|
|
for i, td in enumerate(filtered_tools):
|
|
|
|
|
if td.get("function", {}).get("name") == "execute_code":
|
|
|
|
|
filtered_tools[i] = {"type": "function", "function": dynamic_schema}
|
|
|
|
|
break
|
|
|
|
|
|
2026-03-19 10:08:14 -07:00
|
|
|
# Strip web tool cross-references from browser_navigate description when
|
|
|
|
|
# web_search / web_extract are not available. The static schema says
|
|
|
|
|
# "prefer web_search or web_extract" which causes the model to hallucinate
|
|
|
|
|
# those tools when they're missing.
|
|
|
|
|
if "browser_navigate" in available_tool_names:
|
|
|
|
|
web_tools_available = {"web_search", "web_extract"} & available_tool_names
|
|
|
|
|
if not web_tools_available:
|
|
|
|
|
for i, td in enumerate(filtered_tools):
|
|
|
|
|
if td.get("function", {}).get("name") == "browser_navigate":
|
|
|
|
|
desc = td["function"].get("description", "")
|
|
|
|
|
desc = desc.replace(
|
|
|
|
|
" For simple information retrieval, prefer web_search or web_extract (faster, cheaper).",
|
|
|
|
|
"",
|
|
|
|
|
)
|
|
|
|
|
filtered_tools[i] = {
|
|
|
|
|
"type": "function",
|
|
|
|
|
"function": {**td["function"], "description": desc},
|
|
|
|
|
}
|
|
|
|
|
break
|
|
|
|
|
|
2026-01-31 06:30:48 +00:00
|
|
|
if not quiet_mode:
|
|
|
|
|
if filtered_tools:
|
|
|
|
|
tool_names = [t["function"]["name"] for t in filtered_tools]
|
|
|
|
|
print(f"🛠️ Final tool selection ({len(filtered_tools)} tools): {', '.join(tool_names)}")
|
|
|
|
|
else:
|
|
|
|
|
print("🛠️ No tools selected (all filtered out or unavailable)")
|
2026-02-21 20:22:33 -08:00
|
|
|
|
2026-02-19 23:23:43 -08:00
|
|
|
global _last_resolved_tool_names
|
|
|
|
|
_last_resolved_tool_names = [t["function"]["name"] for t in filtered_tools]
|
2026-01-29 06:10:24 +00:00
|
|
|
|
2026-02-21 20:22:33 -08:00
|
|
|
return filtered_tools
|
2026-01-29 06:10:24 +00:00
|
|
|
|
2026-02-02 08:26:42 -08:00
|
|
|
|
2026-02-21 20:22:33 -08:00
|
|
|
# =============================================================================
|
|
|
|
|
# handle_function_call (the main dispatcher)
|
|
|
|
|
# =============================================================================
|
2026-02-02 08:26:42 -08:00
|
|
|
|
2026-02-21 20:22:33 -08:00
|
|
|
# Tools whose execution is intercepted by the agent loop (run_agent.py)
|
|
|
|
|
# because they need agent-level state (TodoStore, MemoryStore, etc.).
|
|
|
|
|
# The registry still holds their schemas; dispatch just returns a stub error
|
|
|
|
|
# so if something slips through, the LLM sees a sensible message.
|
|
|
|
|
_AGENT_LOOP_TOOLS = {"todo", "memory", "session_search", "delegate_task"}
|
2026-03-18 03:04:07 -07:00
|
|
|
_READ_SEARCH_TOOLS = {"read_file", "search_files"}
|
2026-02-03 23:41:26 -08:00
|
|
|
|
|
|
|
|
|
2026-02-21 20:22:33 -08:00
|
|
|
def handle_function_call(
|
2026-02-05 03:49:46 -08:00
|
|
|
function_name: str,
|
|
|
|
|
function_args: Dict[str, Any],
|
2026-01-29 06:10:24 +00:00
|
|
|
task_id: Optional[str] = None,
|
2026-02-21 20:22:33 -08:00
|
|
|
user_task: Optional[str] = None,
|
2026-03-10 06:32:08 -07:00
|
|
|
enabled_tools: Optional[List[str]] = None,
|
2026-03-16 00:23:47 -07:00
|
|
|
honcho_manager: Optional[Any] = None,
|
|
|
|
|
honcho_session_key: Optional[str] = None,
|
2026-01-29 06:10:24 +00:00
|
|
|
) -> str:
|
2025-08-09 09:52:25 -07:00
|
|
|
"""
|
2026-02-21 20:22:33 -08:00
|
|
|
Main function call dispatcher that routes calls to the tool registry.
|
2025-11-03 17:42:23 -05:00
|
|
|
|
2025-08-09 09:52:25 -07:00
|
|
|
Args:
|
2026-02-21 20:22:33 -08:00
|
|
|
function_name: Name of the function to call.
|
|
|
|
|
function_args: Arguments for the function.
|
|
|
|
|
task_id: Unique identifier for terminal/browser session isolation.
|
|
|
|
|
user_task: The user's original task (for browser_snapshot context).
|
2026-03-10 06:32:08 -07:00
|
|
|
enabled_tools: Tool names enabled for this session. When provided,
|
|
|
|
|
execute_code uses this list to determine which sandbox
|
|
|
|
|
tools to generate. Falls back to the process-global
|
|
|
|
|
``_last_resolved_tool_names`` for backward compat.
|
2025-11-03 17:42:23 -05:00
|
|
|
|
2025-08-09 09:52:25 -07:00
|
|
|
Returns:
|
2026-02-21 20:22:33 -08:00
|
|
|
Function result as a JSON string.
|
2025-08-09 09:52:25 -07:00
|
|
|
"""
|
fix: improve read-loop detection — consecutive-only, correct thresholds, fix bugs
Follow-up to PR #705 (merged from 0xbyt4). Addresses several issues:
1. CONSECUTIVE-ONLY TRACKING: Redesigned the read/search tracker to only
warn/block on truly consecutive identical calls. Any other tool call
in between (write, patch, terminal, etc.) resets the counter via
notify_other_tool_call(), called from handle_function_call() in
model_tools.py. This prevents false blocks in read→edit→verify flows.
2. THRESHOLD ADJUSTMENT: Warn on 3rd consecutive (was 2nd), block on
4th+ consecutive (was 3rd+). Gives the model more room before
intervening.
3. TUPLE UNPACKING BUG: Fixed get_read_files_summary() which crashed on
search keys (5-tuple) when trying to unpack as 3-tuple. Now uses a
separate read_history set that only tracks file reads.
4. WEB_EXTRACT DOCSTRING: Reverted incorrect removal of 'title' from
web_extract return docs in code_execution_tool.py — the field IS
returned by web_tools.py.
5. TESTS: Rewrote test_read_loop_detection.py (35 tests) to cover
consecutive-only behavior, notify_other_tool_call, interleaved
read/search, and summary-unaffected-by-searches.
2026-03-10 16:25:41 -07:00
|
|
|
# Notify the read-loop tracker when a non-read/search tool runs,
|
|
|
|
|
# so the *consecutive* counter resets (reads after other work are fine).
|
|
|
|
|
if function_name not in _READ_SEARCH_TOOLS:
|
|
|
|
|
try:
|
|
|
|
|
from tools.file_tools import notify_other_tool_call
|
|
|
|
|
notify_other_tool_call(task_id or "default")
|
|
|
|
|
except Exception:
|
|
|
|
|
pass # file_tools may not be loaded yet
|
|
|
|
|
|
2025-08-09 09:52:25 -07:00
|
|
|
try:
|
2026-02-21 20:22:33 -08:00
|
|
|
if function_name in _AGENT_LOOP_TOOLS:
|
|
|
|
|
return json.dumps({"error": f"{function_name} must be handled by the agent loop"})
|
Add background process management with process tool, wait, PTY, and stdin support
New process registry and tool for managing long-running background processes
across all terminal backends (local, Docker, Singularity, Modal, SSH).
Process Registry (tools/process_registry.py):
- ProcessSession tracking with rolling 200KB output buffer
- spawn_local() with optional PTY via ptyprocess for interactive CLIs
- spawn_via_env() for non-local backends (runs inside sandbox, never on host)
- Background reader threads per process (Popen stdout or PTY)
- wait() with timeout clamping, interrupt support, and transparent limit reporting
- JSON checkpoint to ~/.hermes/processes.json for gateway crash recovery
- Module-level singleton shared across agent loop, gateway, and RL
Process Tool (model_tools.py):
- 7 actions: list, poll, log, wait, kill, write, submit
- Paired with terminal in all toolsets (CLI, messaging, RL)
- Timeout clamping with transparent notes in response
Terminal Tool Updates (tools/terminal_tool.py):
- Replaced nohup background mode with registry spawn (returns session_id)
- Added workdir parameter for per-command working directory
- Added check_interval parameter for gateway auto-check watchers
- Added pty parameter for interactive CLI tools (Codex, Claude Code)
- Updated TERMINAL_TOOL_DESCRIPTION with full background workflow docs
- Cleanup thread now respects active background processes (won't reap sandbox)
Gateway Integration (gateway/run.py, session.py, config.py):
- Session reset protection: sessions with active processes exempt from reset
- Default idle timeout increased from 2 hours to 24 hours
- from_dict fallback aligned to match (was 120, now 1440)
- session_key env var propagated to process registry for session mapping
- Crash recovery on gateway startup via checkpoint probe
- check_interval watcher: asyncio task polls process, delivers updates to platform
RL Safety (environments/):
- tool_context.py cleanup() kills background processes on episode end
- hermes_base_env.py warns when enabled_toolsets is None (loads all tools)
- Process tool safe in RL via wait() blocking the agent loop
Also:
- Added ptyprocess as optional dependency (in pyproject.toml [pty] extra + [all])
- Fixed pre-existing bug: rl_test_inference missing from TOOL_TO_TOOLSET_MAP
- Updated AGENTS.md with process management docs and project structure
- Updated README.md terminal section with process management overview
2026-02-17 02:51:31 -08:00
|
|
|
|
feat: first-class plugin architecture (#1555)
Plugin system for extending Hermes with custom tools, hooks, and
integrations — no source code changes required.
Core system (hermes_cli/plugins.py):
- Plugin discovery from ~/.hermes/plugins/, .hermes/plugins/, and
pip entry_points (hermes_agent.plugins group)
- PluginContext with register_tool() and register_hook()
- 6 lifecycle hooks: pre/post tool_call, pre/post llm_call,
on_session_start/end
- Namespace package handling for relative imports in plugins
- Graceful error isolation — broken plugins never crash the agent
Integration (model_tools.py):
- Plugin discovery runs after built-in + MCP tools
- Plugin tools bypass toolset filter via get_plugin_tool_names()
- Pre/post tool call hooks fire in handle_function_call()
CLI:
- /plugins command shows loaded plugins, tool counts, status
- Added to COMMANDS dict for autocomplete
Docs:
- Getting started guide (build-a-hermes-plugin.md) — full tutorial
building a calculator plugin step by step
- Reference page (features/plugins.md) — quick overview + tables
- Covers: file structure, schemas, handlers, hooks, data files,
bundled skills, env var gating, pip distribution, common mistakes
Tests: 16 tests covering discovery, loading, hooks, tool visibility.
2026-03-16 07:17:36 -07:00
|
|
|
try:
|
|
|
|
|
from hermes_cli.plugins import invoke_hook
|
|
|
|
|
invoke_hook("pre_tool_call", tool_name=function_name, args=function_args, task_id=task_id or "")
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
|
2026-02-21 20:22:33 -08:00
|
|
|
if function_name == "execute_code":
|
2026-03-10 06:32:08 -07:00
|
|
|
# Prefer the caller-provided list so subagents can't overwrite
|
|
|
|
|
# the parent's tool set via the process-global.
|
|
|
|
|
sandbox_enabled = enabled_tools if enabled_tools is not None else _last_resolved_tool_names
|
feat: first-class plugin architecture (#1555)
Plugin system for extending Hermes with custom tools, hooks, and
integrations — no source code changes required.
Core system (hermes_cli/plugins.py):
- Plugin discovery from ~/.hermes/plugins/, .hermes/plugins/, and
pip entry_points (hermes_agent.plugins group)
- PluginContext with register_tool() and register_hook()
- 6 lifecycle hooks: pre/post tool_call, pre/post llm_call,
on_session_start/end
- Namespace package handling for relative imports in plugins
- Graceful error isolation — broken plugins never crash the agent
Integration (model_tools.py):
- Plugin discovery runs after built-in + MCP tools
- Plugin tools bypass toolset filter via get_plugin_tool_names()
- Pre/post tool call hooks fire in handle_function_call()
CLI:
- /plugins command shows loaded plugins, tool counts, status
- Added to COMMANDS dict for autocomplete
Docs:
- Getting started guide (build-a-hermes-plugin.md) — full tutorial
building a calculator plugin step by step
- Reference page (features/plugins.md) — quick overview + tables
- Covers: file structure, schemas, handlers, hooks, data files,
bundled skills, env var gating, pip distribution, common mistakes
Tests: 16 tests covering discovery, loading, hooks, tool visibility.
2026-03-16 07:17:36 -07:00
|
|
|
result = registry.dispatch(
|
2026-02-21 20:22:33 -08:00
|
|
|
function_name, function_args,
|
2026-02-19 23:23:43 -08:00
|
|
|
task_id=task_id,
|
2026-03-10 06:32:08 -07:00
|
|
|
enabled_tools=sandbox_enabled,
|
2026-03-16 00:23:47 -07:00
|
|
|
honcho_manager=honcho_manager,
|
|
|
|
|
honcho_session_key=honcho_session_key,
|
2026-02-19 23:23:43 -08:00
|
|
|
)
|
feat: first-class plugin architecture (#1555)
Plugin system for extending Hermes with custom tools, hooks, and
integrations — no source code changes required.
Core system (hermes_cli/plugins.py):
- Plugin discovery from ~/.hermes/plugins/, .hermes/plugins/, and
pip entry_points (hermes_agent.plugins group)
- PluginContext with register_tool() and register_hook()
- 6 lifecycle hooks: pre/post tool_call, pre/post llm_call,
on_session_start/end
- Namespace package handling for relative imports in plugins
- Graceful error isolation — broken plugins never crash the agent
Integration (model_tools.py):
- Plugin discovery runs after built-in + MCP tools
- Plugin tools bypass toolset filter via get_plugin_tool_names()
- Pre/post tool call hooks fire in handle_function_call()
CLI:
- /plugins command shows loaded plugins, tool counts, status
- Added to COMMANDS dict for autocomplete
Docs:
- Getting started guide (build-a-hermes-plugin.md) — full tutorial
building a calculator plugin step by step
- Reference page (features/plugins.md) — quick overview + tables
- Covers: file structure, schemas, handlers, hooks, data files,
bundled skills, env var gating, pip distribution, common mistakes
Tests: 16 tests covering discovery, loading, hooks, tool visibility.
2026-03-16 07:17:36 -07:00
|
|
|
else:
|
|
|
|
|
result = registry.dispatch(
|
|
|
|
|
function_name, function_args,
|
|
|
|
|
task_id=task_id,
|
|
|
|
|
user_task=user_task,
|
|
|
|
|
honcho_manager=honcho_manager,
|
|
|
|
|
honcho_session_key=honcho_session_key,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
from hermes_cli.plugins import invoke_hook
|
|
|
|
|
invoke_hook("post_tool_call", tool_name=function_name, args=function_args, result=result, task_id=task_id or "")
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
2026-02-19 23:23:43 -08:00
|
|
|
|
feat: first-class plugin architecture (#1555)
Plugin system for extending Hermes with custom tools, hooks, and
integrations — no source code changes required.
Core system (hermes_cli/plugins.py):
- Plugin discovery from ~/.hermes/plugins/, .hermes/plugins/, and
pip entry_points (hermes_agent.plugins group)
- PluginContext with register_tool() and register_hook()
- 6 lifecycle hooks: pre/post tool_call, pre/post llm_call,
on_session_start/end
- Namespace package handling for relative imports in plugins
- Graceful error isolation — broken plugins never crash the agent
Integration (model_tools.py):
- Plugin discovery runs after built-in + MCP tools
- Plugin tools bypass toolset filter via get_plugin_tool_names()
- Pre/post tool call hooks fire in handle_function_call()
CLI:
- /plugins command shows loaded plugins, tool counts, status
- Added to COMMANDS dict for autocomplete
Docs:
- Getting started guide (build-a-hermes-plugin.md) — full tutorial
building a calculator plugin step by step
- Reference page (features/plugins.md) — quick overview + tables
- Covers: file structure, schemas, handlers, hooks, data files,
bundled skills, env var gating, pip distribution, common mistakes
Tests: 16 tests covering discovery, loading, hooks, tool visibility.
2026-03-16 07:17:36 -07:00
|
|
|
return result
|
2026-02-12 10:05:08 -08:00
|
|
|
|
2026-02-21 20:22:33 -08:00
|
|
|
except Exception as e:
|
|
|
|
|
error_msg = f"Error executing {function_name}: {str(e)}"
|
|
|
|
|
logger.error(error_msg)
|
|
|
|
|
return json.dumps({"error": error_msg}, ensure_ascii=False)
|
Add messaging platform enhancements: STT, stickers, Discord UX, Slack, pairing, hooks
Major feature additions inspired by OpenClaw/ClawdBot integration analysis:
Voice Message Transcription (STT):
- Auto-transcribe voice/audio messages via OpenAI Whisper API
- Download voice to ~/.hermes/audio_cache/ on Telegram/Discord/WhatsApp
- Inject transcript as text so all models can understand voice input
- Configurable model (whisper-1, gpt-4o-mini-transcribe, gpt-4o-transcribe)
Telegram Sticker Understanding:
- Describe static stickers via vision tool with JSON-backed cache
- Cache keyed by file_unique_id avoids redundant API calls
- Animated/video stickers get emoji-based fallback description
Discord Rich UX:
- Native slash commands (/ask, /reset, /status, /stop) via app_commands
- Button-based exec approvals (Allow Once / Always Allow / Deny)
- ExecApprovalView with user authorization and timeout handling
Slack Integration:
- Full SlackAdapter using slack-bolt with Socket Mode
- DMs, channel messages (mention-gated), /hermes slash command
- File attachment handling with bot-token-authenticated downloads
DM Pairing System:
- Code-based user authorization as alternative to static allowlists
- 8-char codes from unambiguous alphabet, 1-hour expiry
- Rate limiting, lockout after failed attempts, chmod 0600 on data
- CLI: hermes pairing list/approve/revoke/clear-pending
Event Hook System:
- File-based hook discovery from ~/.hermes/hooks/
- HOOK.yaml + handler.py per hook, sync/async handler support
- Events: gateway:startup, session:start/reset, agent:start/step/end
- Wildcard matching (command:* catches all command events)
Cross-Channel Messaging:
- send_message agent tool for delivering to any connected platform
- Enables cron job delivery and cross-platform notifications
Human-Like Response Pacing:
- Configurable delays between message chunks (off/natural/custom)
- HERMES_HUMAN_DELAY_MODE env var with min/max ms settings
Warm Injection Message Style:
- Retrofitted image vision messages with friendly kawaii-consistent tone
- All new injection messages (STT, stickers, errors) use warm style
Also: updated config migration to prompt for optional keys interactively,
bumped config version, updated README, AGENTS.md, .env.example,
cli-config.yaml.example, install scripts, pyproject.toml, and toolsets.
2026-02-15 21:38:59 -08:00
|
|
|
|
2026-02-17 17:02:33 -08:00
|
|
|
|
2026-02-21 20:22:33 -08:00
|
|
|
# =============================================================================
|
|
|
|
|
# Backward-compat wrapper functions
|
|
|
|
|
# =============================================================================
|
2026-02-19 00:57:31 -08:00
|
|
|
|
2026-02-21 20:22:33 -08:00
|
|
|
def get_all_tool_names() -> List[str]:
|
|
|
|
|
"""Return all registered tool names."""
|
|
|
|
|
return registry.get_all_tool_names()
|
2026-02-19 00:57:31 -08:00
|
|
|
|
2026-02-20 03:15:53 -08:00
|
|
|
|
2026-02-21 20:22:33 -08:00
|
|
|
def get_toolset_for_tool(tool_name: str) -> Optional[str]:
|
|
|
|
|
"""Return the toolset a tool belongs to."""
|
|
|
|
|
return registry.get_toolset_for_tool(tool_name)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_available_toolsets() -> Dict[str, dict]:
|
|
|
|
|
"""Return toolset availability info for UI display."""
|
|
|
|
|
return registry.get_available_toolsets()
|
2025-08-09 09:52:25 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def check_toolset_requirements() -> Dict[str, bool]:
|
2026-02-21 20:22:33 -08:00
|
|
|
"""Return {toolset: available_bool} for every registered toolset."""
|
|
|
|
|
return registry.check_toolset_requirements()
|
2025-11-17 01:14:31 -05:00
|
|
|
|
2025-08-09 09:52:25 -07:00
|
|
|
|
2026-02-21 20:22:33 -08:00
|
|
|
def check_tool_availability(quiet: bool = False) -> Tuple[List[str], List[dict]]:
|
|
|
|
|
"""Return (available_toolsets, unavailable_info)."""
|
|
|
|
|
return registry.check_tool_availability(quiet=quiet)
|