/ acp_adapter / entry.py
entry.py
1 """CLI entry point for the hermes-agent ACP adapter. 2 3 Loads environment variables from ``~/.hermes/.env``, configures logging 4 to write to stderr (so stdout is reserved for ACP JSON-RPC transport), 5 and starts the ACP agent server. 6 7 Usage:: 8 9 python -m acp_adapter.entry 10 # or 11 hermes acp 12 # or 13 hermes-acp 14 """ 15 16 import asyncio 17 import logging 18 import sys 19 from pathlib import Path 20 from hermes_constants import get_hermes_home 21 22 23 # Methods clients send as periodic liveness probes. They are not part of the 24 # ACP schema, so the acp router correctly returns JSON-RPC -32601 to the 25 # caller — but the supervisor task that dispatches the request then surfaces 26 # the raised RequestError via ``logging.exception("Background task failed")``, 27 # which dumps a traceback to stderr every probe interval. Clients like 28 # acp-bridge already treat the -32601 response as "agent alive", so the 29 # traceback is pure noise. We keep the protocol response intact and only 30 # silence the stderr noise for this specific benign case. 31 _BENIGN_PROBE_METHODS = frozenset({"ping", "health", "healthcheck"}) 32 33 34 class _BenignProbeMethodFilter(logging.Filter): 35 """Suppress acp 'Background task failed' tracebacks caused by unknown 36 liveness-probe methods (e.g. ``ping``) while leaving every other 37 background-task error — including method_not_found for any non-probe 38 method — visible in stderr. 39 """ 40 41 def filter(self, record: logging.LogRecord) -> bool: 42 if record.getMessage() != "Background task failed": 43 return True 44 exc_info = record.exc_info 45 if not exc_info: 46 return True 47 exc = exc_info[1] 48 # Imported lazily so this module stays importable when the optional 49 # ``agent-client-protocol`` dependency is not installed. 50 try: 51 from acp.exceptions import RequestError 52 except ImportError: 53 return True 54 if not isinstance(exc, RequestError): 55 return True 56 if getattr(exc, "code", None) != -32601: 57 return True 58 data = getattr(exc, "data", None) 59 method = data.get("method") if isinstance(data, dict) else None 60 return method not in _BENIGN_PROBE_METHODS 61 62 63 def _setup_logging() -> None: 64 """Route all logging to stderr so stdout stays clean for ACP stdio.""" 65 handler = logging.StreamHandler(sys.stderr) 66 handler.setFormatter( 67 logging.Formatter( 68 "%(asctime)s [%(levelname)s] %(name)s: %(message)s", 69 datefmt="%Y-%m-%d %H:%M:%S", 70 ) 71 ) 72 handler.addFilter(_BenignProbeMethodFilter()) 73 root = logging.getLogger() 74 root.handlers.clear() 75 root.addHandler(handler) 76 root.setLevel(logging.INFO) 77 78 # Quiet down noisy libraries 79 logging.getLogger("httpx").setLevel(logging.WARNING) 80 logging.getLogger("httpcore").setLevel(logging.WARNING) 81 logging.getLogger("openai").setLevel(logging.WARNING) 82 83 84 def _load_env() -> None: 85 """Load .env from HERMES_HOME (default ``~/.hermes``).""" 86 from hermes_cli.env_loader import load_hermes_dotenv 87 88 hermes_home = get_hermes_home() 89 loaded = load_hermes_dotenv(hermes_home=hermes_home) 90 if loaded: 91 for env_file in loaded: 92 logging.getLogger(__name__).info("Loaded env from %s", env_file) 93 else: 94 logging.getLogger(__name__).info( 95 "No .env found at %s, using system env", hermes_home / ".env" 96 ) 97 98 99 def main() -> None: 100 """Entry point: load env, configure logging, run the ACP agent.""" 101 _setup_logging() 102 _load_env() 103 104 logger = logging.getLogger(__name__) 105 logger.info("Starting hermes-agent ACP adapter") 106 107 # Ensure the project root is on sys.path so ``from run_agent import AIAgent`` works 108 project_root = str(Path(__file__).resolve().parent.parent) 109 if project_root not in sys.path: 110 sys.path.insert(0, project_root) 111 112 import acp 113 from .server import HermesACPAgent 114 115 # MCP tool discovery from config.yaml — run before asyncio.run() so 116 # it's safe to use blocking waits. (ACP also registers per-session 117 # MCP servers dynamically via asyncio.to_thread inside the event 118 # loop; that path is unaffected.) Moved from model_tools.py module 119 # scope to avoid freezing the gateway's loop on lazy import (#16856). 120 try: 121 from tools.mcp_tool import discover_mcp_tools 122 discover_mcp_tools() 123 except Exception: 124 logger.debug("MCP tool discovery failed at ACP startup", exc_info=True) 125 126 agent = HermesACPAgent() 127 try: 128 asyncio.run(acp.run_agent(agent, use_unstable_protocol=True)) 129 except KeyboardInterrupt: 130 logger.info("Shutting down (KeyboardInterrupt)") 131 except Exception: 132 logger.exception("ACP agent crashed") 133 sys.exit(1) 134 135 136 if __name__ == "__main__": 137 main()