mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-10-20 05:15:31 +00:00
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
121 lines
3.9 KiB
Python
121 lines
3.9 KiB
Python
"""ACP session manager — maps ACP sessions to hermes AIAgent instances."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import copy
|
|
import logging
|
|
import uuid
|
|
from dataclasses import dataclass, field
|
|
from threading import Lock
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclass
|
|
class SessionState:
|
|
"""Tracks per-session state for an ACP-managed hermes agent."""
|
|
|
|
session_id: str
|
|
agent: Any # AIAgent instance
|
|
cwd: str = "."
|
|
model: str = ""
|
|
history: List[Dict[str, Any]] = field(default_factory=list)
|
|
cancel_event: Any = None # threading.Event
|
|
|
|
|
|
class SessionManager:
|
|
"""Thread-safe manager for ACP sessions backed by hermes AIAgent instances."""
|
|
|
|
def __init__(self, agent_factory=None):
|
|
"""
|
|
Args:
|
|
agent_factory: Callable that creates an AIAgent.
|
|
Defaults to ``AIAgent(platform="acp")``.
|
|
Accepts optional ``model`` kwarg.
|
|
"""
|
|
self._sessions: Dict[str, SessionState] = {}
|
|
self._lock = Lock()
|
|
self._agent_factory = agent_factory
|
|
|
|
# ---- public API ---------------------------------------------------------
|
|
|
|
def create_session(self, cwd: str = ".") -> SessionState:
|
|
"""Create a new session with a unique ID and a fresh AIAgent."""
|
|
import threading
|
|
|
|
session_id = str(uuid.uuid4())
|
|
agent = self._make_agent()
|
|
state = SessionState(
|
|
session_id=session_id,
|
|
agent=agent,
|
|
cwd=cwd,
|
|
cancel_event=threading.Event(),
|
|
)
|
|
with self._lock:
|
|
self._sessions[session_id] = state
|
|
logger.info("Created ACP session %s (cwd=%s)", session_id, cwd)
|
|
return state
|
|
|
|
def get_session(self, session_id: str) -> Optional[SessionState]:
|
|
"""Return the session for *session_id*, or ``None``."""
|
|
with self._lock:
|
|
return self._sessions.get(session_id)
|
|
|
|
def remove_session(self, session_id: str) -> bool:
|
|
"""Remove a session. Returns True if it existed."""
|
|
with self._lock:
|
|
return self._sessions.pop(session_id, None) is not None
|
|
|
|
def fork_session(self, session_id: str, cwd: str = ".") -> Optional[SessionState]:
|
|
"""Deep-copy a session's history into a new session."""
|
|
import threading
|
|
|
|
with self._lock:
|
|
original = self._sessions.get(session_id)
|
|
if original is None:
|
|
return None
|
|
|
|
new_id = str(uuid.uuid4())
|
|
agent = self._make_agent(model=original.model or None)
|
|
state = SessionState(
|
|
session_id=new_id,
|
|
agent=agent,
|
|
cwd=cwd,
|
|
model=original.model,
|
|
history=copy.deepcopy(original.history),
|
|
cancel_event=threading.Event(),
|
|
)
|
|
self._sessions[new_id] = state
|
|
logger.info("Forked ACP session %s -> %s", session_id, new_id)
|
|
return state
|
|
|
|
def list_sessions(self) -> List[Dict[str, Any]]:
|
|
"""Return lightweight info dicts for all sessions."""
|
|
with self._lock:
|
|
return [
|
|
{
|
|
"session_id": s.session_id,
|
|
"cwd": s.cwd,
|
|
"model": s.model,
|
|
"history_len": len(s.history),
|
|
}
|
|
for s in self._sessions.values()
|
|
]
|
|
|
|
def cleanup(self) -> None:
|
|
"""Remove all sessions."""
|
|
with self._lock:
|
|
self._sessions.clear()
|
|
|
|
# ---- internal -----------------------------------------------------------
|
|
|
|
def _make_agent(self, model: str | None = None):
|
|
if self._agent_factory is not None:
|
|
return self._agent_factory()
|
|
# Default: import and construct AIAgent with ACP platform
|
|
from run_agent import AIAgent
|
|
kwargs = {"platform": "acp"}
|
|
if model:
|
|
kwargs["model"] = model
|
|
return AIAgent(**kwargs)
|