website/docs/developer-guide/acp-internals.md
The ACP adapter wraps Hermes' synchronous AIAgent in an async JSON-RPC stdio server.
Key implementation files:
acp_adapter/entry.pyacp_adapter/server.pyacp_adapter/session.pyacp_adapter/events.pyacp_adapter/permissions.pyacp_adapter/tools.pyacp_adapter/auth.pyacp_registry/agent.jsonhermes acp / hermes-acp / python -m acp_adapter
-> acp_adapter.entry.main()
-> parse --version / --check / --setup before server startup
-> load ~/.hermes/.env
-> configure stderr logging
-> construct HermesACPAgent
-> acp.run_agent(agent, use_unstable_protocol=True)
The Zed ACP Registry path launches the same adapter through uvx --from 'hermes-agent[acp]==<version>' hermes-acp, pointed at the hermes-agent PyPI release.
Stdout is reserved for ACP JSON-RPC transport. Human-readable logs go to stderr.
HermesACPAgentacp_adapter/server.py implements the ACP agent protocol.
Responsibilities:
SessionManageracp_adapter/session.py tracks live ACP sessions.
Each session stores:
session_idagentcwdmodelhistorycancel_eventThe manager is thread-safe and supports:
acp_adapter/events.py converts AIAgent callbacks into ACP session_update events.
Bridged callbacks:
tool_progress_callbackthinking_callback (currently set to None in the ACP bridge — reasoning is forwarded through step_callback instead)step_callbackBecause AIAgent runs in a worker thread while ACP I/O lives on the main event loop, the bridge uses:
asyncio.run_coroutine_threadsafe(...)
acp_adapter/permissions.py adapts dangerous terminal approval prompts into ACP permission requests.
Mapping:
allow_once -> Hermes onceallow_always -> Hermes alwaysdenyTimeouts and bridge failures deny by default.
acp_adapter/tools.py maps Hermes tools to ACP tool kinds and builds editor-facing content.
Examples:
patch / write_file -> file diffsterminal -> shell command textread_file / search_files -> text previewsnew_session(cwd)
-> create SessionState
-> create AIAgent(platform="acp", enabled_toolsets=["hermes-acp"])
-> bind task_id/session_id to cwd override
prompt(..., session_id)
-> extract text from ACP content blocks
-> reset cancel event
-> install callbacks + approval bridge
-> run AIAgent in ThreadPoolExecutor
-> update session history
-> emit final agent message chunk
cancel(session_id):
agent.interrupt() when availablestop_reason="cancelled"fork_session() deep-copies message history into a new live session, preserving conversation state while giving the fork its own session ID and cwd.
ACP does not implement its own auth store.
Instead it reuses Hermes' runtime resolver:
acp_adapter/auth.pyhermes_cli/runtime_provider.pySo ACP advertises and uses the currently configured Hermes provider/credentials. It also always advertises a terminal setup auth method (hermes-setup, args --setup) so first-run registry clients can open Hermes' interactive model/provider configuration before starting a normal ACP session.
ACP sessions carry an editor cwd.
The session manager binds that cwd to the ACP session ID via task-scoped terminal/file overrides, so file and terminal tools operate relative to the editor workspace.
The event bridge tracks tool IDs FIFO per tool name, not just one ID per name. This is important for:
Without FIFO queues, completion events would attach to the wrong tool invocation.
ACP temporarily installs an approval callback on the terminal tool during prompt execution, then restores the previous callback afterward. This avoids leaving ACP session-specific approval handlers installed globally forever.
~/.hermes/state.db (SessionDB) and transparently restored across process restarts; they appear in session_searchtests/acp/ — ACP test suitetoolsets.py — hermes-acp toolset definitionhermes_cli/main.py — hermes acp CLI subcommandpyproject.toml — [acp] optional dependency + hermes-acp script