mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-03 02:11:48 +00:00
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
135 lines
4 KiB
Python
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")
|