"""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 from hermes_constants import get_hermes_home # 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 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", ) ) handler.addFilter(_BenignProbeMethodFilter()) 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``).""" from hermes_cli.env_loader import load_hermes_dotenv hermes_home = get_hermes_home() 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) else: logging.getLogger(__name__).info( "No .env found at %s, using system env", hermes_home / ".env" ) 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: asyncio.run(acp.run_agent(agent, use_unstable_protocol=True)) except KeyboardInterrupt: logger.info("Shutting down (KeyboardInterrupt)") except Exception: logger.exception("ACP agent crashed") sys.exit(1) if __name__ == "__main__": main()