hermes-agent/acp_adapter/entry.py
teknium1 cc61f54cd3 feat: add ACP (Agent Client Protocol) server for editor integration
Complete ACP implementation enabling hermes-agent to work as a coding
agent inside VS Code, Zed, JetBrains IDEs, and any ACP-compatible editor.

Based on PR #837 by teknium1, with full implementation of the prompt flow
and fixes for broken event bridging.

## ACP Adapter (acp_adapter/, ~1200 lines)

server.py — HermesACPAgent with all 15 Agent protocol methods:
  - Full session lifecycle (new, load, resume, list, fork, cancel)
  - prompt() runs AIAgent in thread executor, streams tool events,
    thinking, and agent messages back to the editor in real-time
  - Permission bridging for dangerous command approval dialogs
  - Model switching support

session.py — Thread-safe SessionManager with per-session AIAgent,
  conversation history, and model tracking

events.py — Callback factories bridging AIAgent's sync callbacks to
  ACP's async notifications via run_coroutine_threadsafe()

tools.py — Tool kind mapping (25+ tools) with human-readable titles,
  diff content for file edits, and result truncation

permissions.py — Maps ACP permission dialogs to hermes approval flow
auth.py — Provider credential detection
entry.py — CLI entry point with stderr logging

## Key Design Decisions

- No modifications to run_agent.py — ACP works entirely through
  AIAgent's existing callback system (tool_progress_callback,
  thinking_callback, step_callback)
- File edits shown as diffs in the editor (FileEditToolCallContent)
- Terminal commands shown with $ prefix
- Large tool outputs truncated for the UI (5000 char limit)
- Approval for dangerous commands routed to editor permission dialog

## Also includes
- jupyter-live-kernel skill for data science workflows
- acp_registry/ with agent.json for editor auto-discovery
- docs/acp-setup.md with VS Code, Zed, JetBrains setup guides
- hermes acp CLI subcommand

## Tests
- 81 new ACP tests covering server, session, events, tools, auth,
  permissions
- Full suite: 3330 passed, 16 skipped

Closes #837
2026-03-11 09:07:19 -07:00

86 lines
2.5 KiB
Python

"""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-agent --acp (once wired into the CLI)
"""
import asyncio
import logging
import os
import sys
from pathlib import Path
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",
)
)
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 dotenv import load_dotenv
hermes_home = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes"))
env_file = hermes_home / ".env"
if env_file.exists():
try:
load_dotenv(dotenv_path=env_file, encoding="utf-8")
except UnicodeDecodeError:
load_dotenv(dotenv_path=env_file, encoding="latin-1")
logging.getLogger(__name__).info("Loaded env from %s", env_file)
else:
logging.getLogger(__name__).info(
"No .env found at %s, using system env", env_file
)
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))
except KeyboardInterrupt:
logger.info("Shutting down (KeyboardInterrupt)")
except Exception:
logger.exception("ACP agent crashed")
sys.exit(1)
if __name__ == "__main__":
main()