hermes-agent/acp_adapter/server.py
teknium1 e80786cc94 feat: add ACP (Agent Client Protocol) server for editor integration
Adds full ACP support enabling hermes-agent to work as a coding agent
inside VS Code (via vscode-acp extension), Zed, JetBrains IDEs, and
any ACP-compatible editor.

## New module: acp_adapter/

- server.py: HermesACPAgent implementing all 15 Agent protocol methods
  (initialize, authenticate, new/load/list/fork/resume session, prompt,
  cancel, set mode/model/config, on_connect)
- session.py: Thread-safe SessionManager with per-session AIAgent lifecycle
- events.py: Callback factories translating hermes callbacks to ACP
  session_update notifications (tool_call, agent_thought, agent_message)
- tools.py: Tool kind mapping (20+ tools → read/edit/execute/search/fetch/think)
  and content builders (diffs for file edits, terminal output, text previews)
- permissions.py: Bridges hermes approval_callback to ACP requestPermission
  RPC for dangerous command approval dialogs in the editor
- auth.py: Provider credential verification
- entry.py: CLI entry point with .env loading and stderr logging

## Integration points

- run_agent.py: ACP tool bridge hook in _execute_tool_calls() for
  delegating file/terminal operations to the editor
- hermes_cli/main.py: 'hermes acp' subcommand
- pyproject.toml: [acp] optional dependency, hermes-acp entry point,
  included in [all] extras (auto-installed via install.sh)

## Supporting files

- acp_registry/agent.json: ACP Registry manifest
- acp_registry/icon.svg: Hermes caduceus icon
- docs/acp-setup.md: User-facing setup guide for VS Code, Zed, JetBrains

## Tests

- 41 new tests across 5 test files covering tools, sessions, permissions,
  server lifecycle, and auth
- Full test suite: 2901 passed, 0 failures

## User flow

1. hermes is already installed (install.sh)
2. Install 'ACP Client' extension in VS Code
3. Configure: command='hermes', args=['acp']
4. Chat with Hermes in the editor — diffs, terminals, approval
   dialogs, thinking blocks all rendered natively
2026-03-10 08:41:11 -07:00

135 lines
4 KiB
Python

"""ACP agent server — exposes hermes-agent via the Agent Communication Protocol."""
from __future__ import annotations
import asyncio
import logging
from typing import Any, Optional, Sequence
import acp
from acp.schema import (
AgentCapabilities,
AuthenticateResponse,
AuthMethod,
ClientCapabilities,
ForkSessionResponse,
Implementation,
InitializeResponse,
ListSessionsResponse,
NewSessionResponse,
PromptResponse,
SessionCapabilities,
SessionForkCapabilities,
SessionListCapabilities,
SessionInfo,
TextContentBlock,
ImageContentBlock,
AudioContentBlock,
ResourceContentBlock,
EmbeddedResourceContentBlock,
HttpMcpServer,
SseMcpServer,
McpServerStdio,
)
from acp_adapter.auth import detect_provider, has_provider
from acp_adapter.session import SessionManager
logger = logging.getLogger(__name__)
HERMES_VERSION = "0.1.0"
class HermesACPAgent(acp.Agent):
"""ACP Agent implementation wrapping hermes-agent."""
def __init__(self, session_manager: SessionManager | None = None):
super().__init__()
self.session_manager = session_manager or SessionManager()
# ---- ACP lifecycle ------------------------------------------------------
def initialize(
self,
protocol_version: int,
client_capabilities: ClientCapabilities | None = None,
client_info: Implementation | None = None,
**kwargs: Any,
) -> InitializeResponse:
provider = detect_provider()
auth_methods = []
if provider:
auth_methods.append(
AuthMethod(
id=provider,
name=f"{provider} API key",
description=f"Authenticate via {provider}",
)
)
return InitializeResponse(
protocol_version=acp.PROTOCOL_VERSION,
agent_info=Implementation(name="hermes-agent", version=HERMES_VERSION),
agent_capabilities=AgentCapabilities(
session_capabilities=SessionCapabilities(
fork=SessionForkCapabilities(),
list=SessionListCapabilities(),
),
),
auth_methods=auth_methods if auth_methods else None,
)
def authenticate(self, method_id: str, **kwargs: Any) -> AuthenticateResponse | None:
if has_provider():
return AuthenticateResponse()
return None
# ---- Session management -------------------------------------------------
def new_session(
self,
cwd: str,
mcp_servers: list | None = None,
**kwargs: Any,
) -> NewSessionResponse:
state = self.session_manager.create_session(cwd=cwd)
return NewSessionResponse(session_id=state.session_id)
def cancel(self, session_id: str, **kwargs: Any) -> None:
state = self.session_manager.get_session(session_id)
if state and state.cancel_event:
state.cancel_event.set()
def fork_session(
self,
cwd: str,
session_id: str,
mcp_servers: list | None = None,
**kwargs: Any,
) -> ForkSessionResponse:
state = self.session_manager.fork_session(session_id, cwd=cwd)
return ForkSessionResponse(session_id=state.session_id if state else "")
def list_sessions(
self,
cursor: str | None = None,
cwd: str | None = None,
**kwargs: Any,
) -> ListSessionsResponse:
infos = self.session_manager.list_sessions()
sessions = [
SessionInfo(session_id=s["session_id"], cwd=s["cwd"])
for s in infos
]
return ListSessionsResponse(sessions=sessions)
# ---- Prompt (placeholder) -----------------------------------------------
def prompt(
self,
prompt: list,
session_id: str,
**kwargs: Any,
) -> PromptResponse:
# Full implementation would run AIAgent here.
return PromptResponse(stop_reason="end_turn")