2026-03-14 00:09:05 -07:00
|
|
|
"""CLI entry point for the hermes-agent ACP adapter.
|
|
|
|
|
|
|
|
|
|
Loads environment variables from ``~/.hermes/.env``, configures logging
|
|
|
|
|
to write to stderr (so stdout is reserved for ACP JSON-RPC transport),
|
|
|
|
|
and starts the ACP agent server.
|
|
|
|
|
|
|
|
|
|
Usage::
|
|
|
|
|
|
|
|
|
|
python -m acp_adapter.entry
|
|
|
|
|
# or
|
|
|
|
|
hermes acp
|
|
|
|
|
# or
|
|
|
|
|
hermes-acp
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import asyncio
|
|
|
|
|
import logging
|
|
|
|
|
import sys
|
|
|
|
|
from pathlib import Path
|
refactor: consolidate get_hermes_home() and parse_reasoning_effort() (#3062)
Centralizes two widely-duplicated patterns into hermes_constants.py:
1. get_hermes_home() — Path resolution for ~/.hermes (HERMES_HOME env var)
- Was copy-pasted inline across 30+ files as:
Path(os.getenv("HERMES_HOME", Path.home() / ".hermes"))
- Now defined once in hermes_constants.py (zero-dependency module)
- hermes_cli/config.py re-exports it for backward compatibility
- Removed local wrapper functions in honcho_integration/client.py,
tools/website_policy.py, tools/tirith_security.py, hermes_cli/uninstall.py
2. parse_reasoning_effort() — Reasoning effort string validation
- Was copy-pasted in cli.py, gateway/run.py, cron/scheduler.py
- Same validation logic: check against (xhigh, high, medium, low, minimal, none)
- Now defined once in hermes_constants.py, called from all 3 locations
- Warning log for unknown values kept at call sites (context-specific)
31 files changed, net +31 lines (125 insertions, 94 deletions)
Full test suite: 6179 passed, 0 failed
2026-03-25 15:54:28 -07:00
|
|
|
from hermes_constants import get_hermes_home
|
2026-03-14 00:09:05 -07:00
|
|
|
|
|
|
|
|
|
2026-04-20 00:10:27 -07:00
|
|
|
# Methods clients send as periodic liveness probes. They are not part of the
|
|
|
|
|
# ACP schema, so the acp router correctly returns JSON-RPC -32601 to the
|
|
|
|
|
# caller — but the supervisor task that dispatches the request then surfaces
|
|
|
|
|
# the raised RequestError via ``logging.exception("Background task failed")``,
|
|
|
|
|
# which dumps a traceback to stderr every probe interval. Clients like
|
|
|
|
|
# acp-bridge already treat the -32601 response as "agent alive", so the
|
|
|
|
|
# traceback is pure noise. We keep the protocol response intact and only
|
|
|
|
|
# silence the stderr noise for this specific benign case.
|
|
|
|
|
_BENIGN_PROBE_METHODS = frozenset({"ping", "health", "healthcheck"})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _BenignProbeMethodFilter(logging.Filter):
|
|
|
|
|
"""Suppress acp 'Background task failed' tracebacks caused by unknown
|
|
|
|
|
liveness-probe methods (e.g. ``ping``) while leaving every other
|
|
|
|
|
background-task error — including method_not_found for any non-probe
|
|
|
|
|
method — visible in stderr.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def filter(self, record: logging.LogRecord) -> bool:
|
|
|
|
|
if record.getMessage() != "Background task failed":
|
|
|
|
|
return True
|
|
|
|
|
exc_info = record.exc_info
|
|
|
|
|
if not exc_info:
|
|
|
|
|
return True
|
|
|
|
|
exc = exc_info[1]
|
|
|
|
|
# Imported lazily so this module stays importable when the optional
|
|
|
|
|
# ``agent-client-protocol`` dependency is not installed.
|
|
|
|
|
try:
|
|
|
|
|
from acp.exceptions import RequestError
|
|
|
|
|
except ImportError:
|
|
|
|
|
return True
|
|
|
|
|
if not isinstance(exc, RequestError):
|
|
|
|
|
return True
|
|
|
|
|
if getattr(exc, "code", None) != -32601:
|
|
|
|
|
return True
|
|
|
|
|
data = getattr(exc, "data", None)
|
|
|
|
|
method = data.get("method") if isinstance(data, dict) else None
|
|
|
|
|
return method not in _BENIGN_PROBE_METHODS
|
|
|
|
|
|
|
|
|
|
|
2026-03-14 00:09:05 -07:00
|
|
|
def _setup_logging() -> None:
|
|
|
|
|
"""Route all logging to stderr so stdout stays clean for ACP stdio."""
|
|
|
|
|
handler = logging.StreamHandler(sys.stderr)
|
|
|
|
|
handler.setFormatter(
|
|
|
|
|
logging.Formatter(
|
|
|
|
|
"%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
|
|
|
datefmt="%Y-%m-%d %H:%M:%S",
|
|
|
|
|
)
|
|
|
|
|
)
|
2026-04-20 00:10:27 -07:00
|
|
|
handler.addFilter(_BenignProbeMethodFilter())
|
2026-03-14 00:09:05 -07:00
|
|
|
root = logging.getLogger()
|
|
|
|
|
root.handlers.clear()
|
|
|
|
|
root.addHandler(handler)
|
|
|
|
|
root.setLevel(logging.INFO)
|
|
|
|
|
|
|
|
|
|
# Quiet down noisy libraries
|
|
|
|
|
logging.getLogger("httpx").setLevel(logging.WARNING)
|
|
|
|
|
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
|
|
|
|
logging.getLogger("openai").setLevel(logging.WARNING)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _load_env() -> None:
|
|
|
|
|
"""Load .env from HERMES_HOME (default ``~/.hermes``)."""
|
2026-03-15 06:46:28 -07:00
|
|
|
from hermes_cli.env_loader import load_hermes_dotenv
|
2026-03-14 00:09:05 -07:00
|
|
|
|
refactor: consolidate get_hermes_home() and parse_reasoning_effort() (#3062)
Centralizes two widely-duplicated patterns into hermes_constants.py:
1. get_hermes_home() — Path resolution for ~/.hermes (HERMES_HOME env var)
- Was copy-pasted inline across 30+ files as:
Path(os.getenv("HERMES_HOME", Path.home() / ".hermes"))
- Now defined once in hermes_constants.py (zero-dependency module)
- hermes_cli/config.py re-exports it for backward compatibility
- Removed local wrapper functions in honcho_integration/client.py,
tools/website_policy.py, tools/tirith_security.py, hermes_cli/uninstall.py
2. parse_reasoning_effort() — Reasoning effort string validation
- Was copy-pasted in cli.py, gateway/run.py, cron/scheduler.py
- Same validation logic: check against (xhigh, high, medium, low, minimal, none)
- Now defined once in hermes_constants.py, called from all 3 locations
- Warning log for unknown values kept at call sites (context-specific)
31 files changed, net +31 lines (125 insertions, 94 deletions)
Full test suite: 6179 passed, 0 failed
2026-03-25 15:54:28 -07:00
|
|
|
hermes_home = get_hermes_home()
|
2026-03-15 06:46:28 -07:00
|
|
|
loaded = load_hermes_dotenv(hermes_home=hermes_home)
|
|
|
|
|
if loaded:
|
|
|
|
|
for env_file in loaded:
|
|
|
|
|
logging.getLogger(__name__).info("Loaded env from %s", env_file)
|
2026-03-14 00:09:05 -07:00
|
|
|
else:
|
|
|
|
|
logging.getLogger(__name__).info(
|
2026-03-15 06:46:28 -07:00
|
|
|
"No .env found at %s, using system env", hermes_home / ".env"
|
2026-03-14 00:09:05 -07:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main() -> None:
|
|
|
|
|
"""Entry point: load env, configure logging, run the ACP agent."""
|
|
|
|
|
_setup_logging()
|
|
|
|
|
_load_env()
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
logger.info("Starting hermes-agent ACP adapter")
|
|
|
|
|
|
|
|
|
|
# Ensure the project root is on sys.path so ``from run_agent import AIAgent`` works
|
|
|
|
|
project_root = str(Path(__file__).resolve().parent.parent)
|
|
|
|
|
if project_root not in sys.path:
|
|
|
|
|
sys.path.insert(0, project_root)
|
|
|
|
|
|
|
|
|
|
import acp
|
|
|
|
|
from .server import HermesACPAgent
|
|
|
|
|
|
|
|
|
|
agent = HermesACPAgent()
|
|
|
|
|
try:
|
2026-03-28 23:45:53 -07:00
|
|
|
asyncio.run(acp.run_agent(agent, use_unstable_protocol=True))
|
2026-03-14 00:09:05 -07:00
|
|
|
except KeyboardInterrupt:
|
|
|
|
|
logger.info("Shutting down (KeyboardInterrupt)")
|
|
|
|
|
except Exception:
|
|
|
|
|
logger.exception("ACP agent crashed")
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
main()
|