Files
hermes-agent/website/docs/developer-guide/acp-internals.md
teknium1 94c0e49307 docs: scope the ACP toolset parity wording
ACP resolves toolsets like the gateway for the same platform config,
which includes plugin toolsets. An empty platform_toolsets.acp list still
adds enabled plugin toolsets, so the docs point at agent.disabled_toolsets
for removing them.
2026-09-23 16:27:05 -07:00

198 lines
6.1 KiB
Markdown

---
sidebar_position: 2
title: "ACP Internals"
description: "How the ACP adapter works: lifecycle, sessions, event bridge, approvals, and tool rendering"
---
# ACP Internals
The ACP adapter wraps Hermes' synchronous `AIAgent` in an async JSON-RPC stdio server.
Key implementation files:
- `acp_adapter/entry.py`
- `acp_adapter/server.py`
- `acp_adapter/session.py`
- `acp_adapter/events.py`
- `acp_adapter/permissions.py`
- `acp_adapter/tools.py`
- `acp_adapter/auth.py`
## Boot flow
```text
hermes 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)
```
Stdout is reserved for ACP JSON-RPC transport. Human-readable logs go to stderr.
## Major components
### `HermesACPAgent`
`acp_adapter/server.py` implements the ACP agent protocol.
Responsibilities:
- initialize / authenticate
- new/load/resume/fork/list/cancel session methods
- prompt execution
- session model switching
- wiring sync AIAgent callbacks into ACP async notifications
### `SessionManager`
`acp_adapter/session.py` tracks live ACP sessions.
Each session stores:
- `session_id`
- `agent`
- `cwd`
- `model`
- `history`
- `cancel_event`
The manager is thread-safe and supports:
- create
- get
- remove
- fork
- list
- cleanup
- cwd updates
### Event bridge
`acp_adapter/events.py` converts AIAgent callbacks into ACP `session_update` events.
Bridged callbacks:
- `tool_progress_callback`
- `thinking_callback` (currently set to `None` in the ACP bridge — reasoning is forwarded through `step_callback` instead)
- `step_callback`
Every ACP tool call reaches a terminal status: `tool.completed` closes the call with its own
result (`completed` / `failed`), the `step_callback` `prev_tools` pass is only a fallback for
runtimes that never project a completion, and anything still open when the turn ends —
a denied or blocked call, an interrupted one — is marked `failed` before the response is
returned.
Because `AIAgent` runs in a worker thread while ACP I/O lives on the main event loop, the bridge uses:
```python
asyncio.run_coroutine_threadsafe(...)
```
### Permission bridge
`acp_adapter/permissions.py` adapts dangerous terminal approval prompts into ACP permission requests.
Mapping:
- `allow_once` -> Hermes `once`
- `allow_always` -> Hermes `always`
- reject options -> Hermes `deny`
Timeouts and bridge failures deny by default.
### Tool rendering helpers
`acp_adapter/tools.py` maps Hermes tools to ACP tool kinds and builds editor-facing content.
Examples:
- `patch` / `write_file` -> file diffs
- `terminal` -> shell command text
- `read_file` / `search_files` -> text previews
- large results -> truncated text blocks for UI safety
## Session lifecycle
```text
new_session(cwd)
-> create SessionState
-> create AIAgent(platform="acp", enabled_toolsets=<_get_platform_tools(config, "acp"), as on the
gateway: platform_toolsets.acp (default hermes-acp) plus the resolver's extras
such as plugin toolsets, with its admitted MCP servers keyed mcp-<server>>,
disabled_toolsets=<agent.disabled_toolsets>)
-> 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
```
A turn that ends in a terminal failure (provider refusal, non-retryable error, exhausted
retries, interrupt before any reply) is closed by the core loop with a Hermes-authored
assistant row ("Your request was not processed…" / "This turn did not complete…") so the
durable transcript never ends on an open `user` row. Without it the next prompt would be
merged into the failed request and replayed. Context-overflow failures are exempt: their
repair is session rotation, not another row.
### Cancelation
`cancel(session_id)`:
- sets the session cancel event
- calls `agent.interrupt()` when available
- causes the prompt response to return `stop_reason="cancelled"`
### Forking
`fork_session()` deep-copies message history into a new live session, preserving conversation state while giving the fork its own session ID and cwd.
## Provider/auth behavior
ACP does not implement its own auth store.
Instead it reuses Hermes' runtime resolver:
- `acp_adapter/auth.py`
- `hermes_cli/runtime_provider.py`
So 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 ACP clients can open Hermes' interactive model/provider configuration before starting a normal ACP session.
## Working directory binding
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.
## Duplicate same-name tool calls
The event bridge tracks tool IDs FIFO per tool name, not just one ID per name. This is important for:
- parallel same-name calls
- repeated same-name calls in one step
Without FIFO queues, completion events would attach to the wrong tool invocation.
## Approval callback restoration
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.
## Current limitations
- ACP sessions are persisted to the shared `~/.hermes/state.db` (SessionDB) and transparently restored across process restarts; they appear in `session_search`
- non-text prompt blocks are currently ignored for request text extraction
- editor-specific UX varies by ACP client implementation
## Related files
- `tests/acp_adapter/` — ACP test suite
- `toolsets.py` — `hermes-acp` toolset definition
- `hermes_cli/main.py` — `hermes acp` CLI subcommand
- `pyproject.toml` — `[acp]` optional dependency + `hermes-acp` script