mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-29 01:31:41 +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
116 lines
3.6 KiB
Python
116 lines
3.6 KiB
Python
"""ACP tool-call helpers for mapping hermes tools to ACP ToolKind and building content."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, Dict, List, Optional, Sequence
|
|
|
|
import acp
|
|
from acp.schema import (
|
|
ToolCallLocation,
|
|
ToolCallStart,
|
|
ToolCallProgress,
|
|
ToolKind,
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Map hermes tool names -> ACP ToolKind
|
|
# ---------------------------------------------------------------------------
|
|
|
|
TOOL_KIND_MAP: Dict[str, ToolKind] = {
|
|
"read_file": "read",
|
|
"search_files": "search",
|
|
"terminal": "execute",
|
|
"patch": "edit",
|
|
"write_file": "edit",
|
|
"process": "execute",
|
|
"vision_analyze": "read",
|
|
}
|
|
|
|
|
|
def get_tool_kind(tool_name: str) -> ToolKind:
|
|
"""Return the ACP ToolKind for a hermes tool, defaulting to 'other'."""
|
|
return TOOL_KIND_MAP.get(tool_name, "other")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Build ACP content objects for tool-call events
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def build_tool_start(
|
|
tool_call_id: str,
|
|
tool_name: str,
|
|
arguments: Dict[str, Any],
|
|
) -> ToolCallStart:
|
|
"""Create a ToolCallStart event for the given hermes tool invocation."""
|
|
kind = get_tool_kind(tool_name)
|
|
title = tool_name
|
|
locations = extract_locations(arguments)
|
|
|
|
if tool_name == "patch":
|
|
# Produce a diff content block
|
|
path = arguments.get("path", "")
|
|
old = arguments.get("old_string", "")
|
|
new = arguments.get("new_string", "")
|
|
content = [acp.tool_diff_content(path=path, new_text=new, old_text=old)]
|
|
return acp.start_tool_call(
|
|
tool_call_id, title, kind=kind, content=content, locations=locations,
|
|
raw_input=arguments,
|
|
)
|
|
|
|
if tool_name == "terminal":
|
|
command = arguments.get("command", "")
|
|
content = [acp.tool_content(acp.text_block(f"$ {command}"))]
|
|
return acp.start_tool_call(
|
|
tool_call_id, title, kind=kind, content=content, locations=locations,
|
|
raw_input=arguments,
|
|
)
|
|
|
|
if tool_name == "read_file":
|
|
path = arguments.get("path", "")
|
|
content = [acp.tool_content(acp.text_block(f"Reading {path}"))]
|
|
return acp.start_tool_call(
|
|
tool_call_id, title, kind=kind, content=content, locations=locations,
|
|
raw_input=arguments,
|
|
)
|
|
|
|
# Generic fallback
|
|
content = [acp.tool_content(acp.text_block(str(arguments)))]
|
|
return acp.start_tool_call(
|
|
tool_call_id, title, kind=kind, content=content, locations=locations,
|
|
raw_input=arguments,
|
|
)
|
|
|
|
|
|
def build_tool_complete(
|
|
tool_call_id: str,
|
|
tool_name: str,
|
|
result: str,
|
|
) -> ToolCallProgress:
|
|
"""Create a ToolCallUpdate (progress) event for a completed tool call."""
|
|
kind = get_tool_kind(tool_name)
|
|
content = [acp.tool_content(acp.text_block(result))]
|
|
return acp.update_tool_call(
|
|
tool_call_id,
|
|
kind=kind,
|
|
status="completed",
|
|
content=content,
|
|
raw_output=result,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Location extraction
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def extract_locations(
|
|
arguments: Dict[str, Any],
|
|
) -> List[ToolCallLocation]:
|
|
"""Extract file-system locations from tool arguments."""
|
|
locations: List[ToolCallLocation] = []
|
|
path = arguments.get("path")
|
|
if path:
|
|
line = arguments.get("offset") or arguments.get("line")
|
|
locations.append(ToolCallLocation(path=path, line=line))
|
|
return locations
|