/ 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()