mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-17 04:31:55 +00:00
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
This commit is contained in:
parent
cfc3ccb212
commit
e80786cc94
21 changed files with 1485 additions and 2 deletions
103
tests/acp/test_server.py
Normal file
103
tests/acp/test_server.py
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
"""Tests for acp_adapter.server — HermesACPAgent ACP server."""
|
||||
|
||||
import os
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
import acp
|
||||
from acp.schema import (
|
||||
AgentCapabilities,
|
||||
AuthenticateResponse,
|
||||
Implementation,
|
||||
InitializeResponse,
|
||||
NewSessionResponse,
|
||||
SessionInfo,
|
||||
)
|
||||
from acp_adapter.server import HermesACPAgent, HERMES_VERSION
|
||||
from acp_adapter.session import SessionManager
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def mock_manager():
|
||||
"""SessionManager with a mock agent factory."""
|
||||
return SessionManager(agent_factory=lambda: MagicMock(name="MockAIAgent"))
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def agent(mock_manager):
|
||||
"""HermesACPAgent backed by a mock session manager."""
|
||||
return HermesACPAgent(session_manager=mock_manager)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# initialize
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestInitialize:
|
||||
def test_initialize_returns_correct_protocol_version(self, agent):
|
||||
resp = agent.initialize(protocol_version=1)
|
||||
assert isinstance(resp, InitializeResponse)
|
||||
assert resp.protocol_version == acp.PROTOCOL_VERSION
|
||||
|
||||
def test_initialize_returns_agent_info(self, agent):
|
||||
resp = agent.initialize(protocol_version=1)
|
||||
assert resp.agent_info is not None
|
||||
assert isinstance(resp.agent_info, Implementation)
|
||||
assert resp.agent_info.name == "hermes-agent"
|
||||
assert resp.agent_info.version == HERMES_VERSION
|
||||
|
||||
def test_initialize_returns_capabilities(self, agent):
|
||||
resp = agent.initialize(protocol_version=1)
|
||||
caps = resp.agent_capabilities
|
||||
assert isinstance(caps, AgentCapabilities)
|
||||
assert caps.session_capabilities is not None
|
||||
assert caps.session_capabilities.fork is not None
|
||||
assert caps.session_capabilities.list is not None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# authenticate
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestAuthenticate:
|
||||
def test_authenticate_with_provider_configured(self, agent, monkeypatch):
|
||||
monkeypatch.setenv("OPENROUTER_API_KEY", "sk-test-123")
|
||||
resp = agent.authenticate(method_id="openrouter")
|
||||
assert isinstance(resp, AuthenticateResponse)
|
||||
|
||||
def test_authenticate_without_provider(self, agent, monkeypatch):
|
||||
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
|
||||
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
|
||||
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
|
||||
resp = agent.authenticate(method_id="openrouter")
|
||||
assert resp is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# new_session / cancel
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestSessionOps:
|
||||
def test_new_session_creates_session(self, agent):
|
||||
resp = agent.new_session(cwd="/home/user/project")
|
||||
assert isinstance(resp, NewSessionResponse)
|
||||
assert resp.session_id
|
||||
# Session should be retrievable from the manager
|
||||
state = agent.session_manager.get_session(resp.session_id)
|
||||
assert state is not None
|
||||
assert state.cwd == "/home/user/project"
|
||||
|
||||
def test_cancel_sets_event(self, agent):
|
||||
resp = agent.new_session(cwd=".")
|
||||
state = agent.session_manager.get_session(resp.session_id)
|
||||
assert not state.cancel_event.is_set()
|
||||
agent.cancel(session_id=resp.session_id)
|
||||
assert state.cancel_event.is_set()
|
||||
|
||||
def test_cancel_nonexistent_session_is_noop(self, agent):
|
||||
# Should not raise
|
||||
agent.cancel(session_id="does-not-exist")
|
||||
Loading…
Add table
Add a link
Reference in a new issue