Drive the Hermes Agent from any Python app over the Agent Client Protocol (ACP) — streaming events, your own tools, safe by default. No Jupyter required.
Project description
hermes-acp-sdk
Drive the Hermes Agent from any Python app — a CLI, a backend service, a bot —
over the Agent Client Protocol (ACP). No Jupyter, no
notebook kernel, no editor. The SDK spawns hermes acp as a subprocess, speaks the wire
protocol for you, and hands back a clean async iterator of typed events.
You also get to hand the agent your own Python functions as tools — running inside your process, with full access to your application's state.
"Simplicity is prerequisite for reliability." — Edsger W. Dijkstra
ACP is a two-way protocol with a large callback surface and a couple of traps that are documented nowhere. This SDK absorbs all of it: ~10 lines instead of ~300.
Features
| Feature | What it does |
|---|---|
HermesClient() |
Spawns hermes acp, does the handshake, cleans the subprocess up |
session() |
Opens a session and selects a model — the trap everyone hits (see below) |
prompt() → async for |
Streaming becomes an ordinary loop over typed events |
| Agent thoughts | AgentThought — watch the model reason, not just answer |
| Tool calls & plans | ToolCall, PlanUpdated, Usage events, all typed |
tools=[your_function] |
Give the agent your Python functions via an in-process MCP server |
| Deny-by-default | Permissions refused, filesystem off, terminals off — until you opt in |
| No secret leakage | An agent-spawned command never inherits your env (DEEPSEEK_API_KEY, …) |
Finds hermes itself |
On PATH or next to the running interpreter (venv, not activated) |
| Fully typed | py.typed, frozen dataclasses, 64 tests + real-Hermes integration tests |
See it working
-
demo.ipynb— run against a real Hermes and committed with its output saved, so you can read the real streamed answers, the agent's thoughts, the security model and the MCP tools without running anything or spending a token. -
examples/chat.py— a streaming chat REPL in a plain terminal (~30 lines). This is the whole thesis: no Jupyter required.python examples/chat.py --thoughts
Install
pip install hermes-acp-sdk # the SDK
pip install "hermes-agent[acp]" # the agent it talks to (note the [acp] extra!)
export DEEPSEEK_API_KEY="sk-..." # or any provider Hermes supports
Optional, to expose your own functions as tools:
pip install "hermes-acp-sdk[tools]"
⚠️ Python version — read this first
hermes-agentrequires Python>=3.11,<3.14. On 3.14 it will not install at all, and the symptom is baffling:ModuleNotFoundError: No module named 'acp', or a missinghermesbinary. (The SDK itself runs on 3.10+, but it is useless without a Hermes to talk to.)python3.13 -m venv .venv ./.venv/bin/pip install hermes-acp-sdk "hermes-agent[acp]" ./.venv/bin/python your_app.py # works even without activating the venv
Quick start
import asyncio
from hermes_acp_sdk import HermesClient, AgentText, AgentThought, Finished
async def main() -> None:
async with HermesClient() as hermes: # spawns `hermes acp`
print(f"connected to: {hermes.agent_name} {hermes.agent_version}")
async with hermes.session() as s: # + selects a model for you
async for ev in s.prompt("In one sentence: what is a Python traceback?"):
if isinstance(ev, AgentText):
print(ev.text, end="", flush=True) # streams in, token by token
elif isinstance(ev, Finished):
print(f"\n[{ev.stop_reason}]")
asyncio.run(main())
Real output:
connected to: hermes-agent 0.18.2
A Python traceback is the error report that shows the chain of function calls leading up
to an exception, listing each file, line number, and code snippet in the call stack.
[end_turn]
Every event is typed, so you choose what to render — including the agent's reasoning:
events = [ev async for ev in s.prompt("Think it through: what is 12 * 12?")]
# {'AgentThought': 36, 'AgentText': 2, 'Usage': 2, 'Finished': 1}
How it works
┌──────────────────┐ ┌────────────────────┐ ┌───────────────────┐ ┌──────────────┐
│ HermesClient │ → │ ACP over stdio │ → │ hermes acp │ → │ your LLM │
│ spawn + handshake│ │ (JSON-RPC, TWO-WAY│ │ (subprocess) │ │ provider │
│ │ │ — the agent calls│ │ │ │ │
│ session() │ │ BACK into you) │ │ skills · memory │ │ DeepSeek, … │
│ + set_model ⚑ │ │ │ │ tools · delegation│ │ │
└──────────────────┘ └────────────────────┘ └───────────────────┘ └──────────────┘
│ ▲
│ prompt() │ the agent's callbacks (permissions, files, terminals)
▼ │ are answered by your policies — deny-by-default
async for ev in … ◀──────────┘
AgentText · AgentThought · ToolCall · PlanUpdated · Usage · Finished
Without the SDK you would: spawn the subprocess and wire its pipes; implement all 12 callbacks the agent invokes on you (miss one and it breaks); demultiplex a firehose of async notifications into something usable; and then discover — the hard way — the two traps in Why this exists.
Events
s.prompt(...) yields frozen dataclasses from hermes_acp_sdk.events:
| Event | Fields | Meaning |
|---|---|---|
AgentText |
text |
A chunk of the agent's visible reply |
AgentThought |
text |
A chunk of its internal reasoning |
ToolCall |
tool_call_id, title, status, kind |
A tool call started, or changed status |
PlanUpdated |
steps: list[PlanStep] |
The agent published or revised its plan |
Usage |
input_tokens, output_tokens |
Token accounting, when the agent reports it |
PermissionDenied |
tool_title |
Your policy refused a tool call |
Finished |
stop_reason |
Always last; the turn is over |
Unknown ACP update shapes are dropped rather than raised — a new agent version can't crash your app.
Give the agent your app's own tools
An agent only knows what its tools let it know — and it knows nothing about your
database, your users, your state. ToolServer fixes that: hand it plain Python functions
and it runs an MCP server inside your own process, so the tools are ordinary closures
with full access to your application.
def student_weakness(student_id: str) -> str:
"""Look up which Python error family a given student most often gets wrong."""
return db.worst_family(student_id) # ← your live application state
async with HermesClient() as hermes:
async with hermes.session(tools=[student_weakness]) as s:
async for ev in s.prompt("What should student bex practise?"):
...
The function's name, type hints and docstring become the tool's name, schema and description — the docstring is what the model reads when deciding to call it, so write it for the model.
In-process is the point. A separate MCP server process cannot see your state; this one is your process. That is what makes "the agent looks up this student's history" possible.
Security: the server binds 127.0.0.1 on an ephemeral port and requires a per-session
bearer token, so no other local process can invoke your functions. It lives exactly as long
as the session.
Naming: Hermes namespaces MCP tools —
student_weaknesson a server namedapp-toolsreaches the model asmcp__app_tools__student_weakness. Use that name if you refer to it explicitly in a prompt.
Safety
⚠️ What these policies cover — and what they do NOT
They govern what the agent asks you, the client, to do over ACP: permission requests,
read_text_file/write_text_file, terminals.They do not sandbox Hermes itself. Hermes ships its own internal
read_file,search_files,terminalandexecute_codetools that run inside its process and never pass through this SDK. While testing, we watched it answer a question by grepping the working directory instead of calling the tool it was handed.The real boundary is the
cwdyou give it. Pass a directory you are willing to expose — not your home, not a repo full of secrets.
What the SDK itself grants is deny-by-default:
-
Permissions — refused.
DenyAll()unless you say otherwise; aPermissionDeniedevent is emitted so you can see it happened. Opt in withAllowTools([...])(allow-list),CallbackPolicy(fn)(decide per request), orAllowAll()(unsafe — local experiments only). -
Filesystem — off.
FsPolicy()allows no reads or writes. Opt in withFsPolicy(root=Path("./workspace"), allow_read=True); paths are resolved againstrootand any escape (including via symlinks) is rejected. -
Terminals — off, and enabling is not a blank cheque.
TerminalPolicy(enabled=True)alone still runs nothing — you must name the commands:TerminalPolicy( enabled=True, allowed_commands=frozenset({"/bin/ls"}), # required — no allow-list, no execution cwd_root=Path("./workspace"), # confine where it may run inherit_env=False, # default: your API keys are NOT passed on )
from pathlib import Path
from hermes_acp_sdk import HermesClient, AllowTools, FsPolicy, TerminalPolicy
async with HermesClient(
policy=AllowTools(["Read File"]),
fs=FsPolicy(root=Path("./workspace"), allow_read=True),
terminal=TerminalPolicy(), # off
cwd="./workspace", # the real boundary
) as hermes:
...
Why this exists
Five traps, found by driving a real Hermes. The SDK absorbs every one:
new_sessionlies about the model. It reports acurrent_model_id, but inference still goes out with an empty model — so every prompt fails withHTTP 400: ... but you passed .. You must callset_session_modelexplicitly.session()always does this for you. This is the single biggest reason the package exists.- Never pass
--provider/-mtohermes. Those flags blank out the model. The provider is auto-detected from the environment (e.g.DEEPSEEK_API_KEY). - Model ids are
provider:model—"deepseek:deepseek-v4-pro", not"deepseek-v4-pro". - Hermes can't speak ACP without its extra —
pip install "hermes-agent[acp]", or thehermes acpsubcommand does not exist. - The ACP
Clientrole is a big callback surface — session updates, permissions, file I/O, terminals, extension methods — and the agent breaks if any of it is missing. The SDK implements all of it, routed through your policies.
Developer guide
hermes_acp_sdk/
├── client.py # HermesClient: spawn `hermes acp`, handshake, sessions [the entry point]
├── session.py # HermesSession: prompt() as an async generator of events
├── handler.py # the ACP `Client` role: agent callbacks → typed events + policy decisions
├── events.py # AgentText, AgentThought, ToolCall, PlanUpdated, Usage, Finished
├── policy.py # PermissionPolicy, FsPolicy, TerminalPolicy [the security boundary]
├── tools.py # ToolServer: your Python functions → in-process MCP server
└── errors.py
- Run tests:
pip install -e ".[dev]" && pytest -q— 64 tests, all against an in-process fake ACP agent, so they are fast, offline and cost nothing. - Integration tests (real Hermes, marked
integration):HERMES_BIN=… DEEPSEEK_API_KEY=… pytest -m integration.
License
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file hermes_acp_sdk-0.1.0.tar.gz.
File metadata
- Download URL: hermes_acp_sdk-0.1.0.tar.gz
- Upload date:
- Size: 38.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0b873557dc0d26f038a416551b74a2b60f06c78331c5f0af468b2022b31fc7e7
|
|
| MD5 |
e73814447fe248447fe31fe3d6fcd99c
|
|
| BLAKE2b-256 |
7a94ec0714e15c3975b495b80c436a227966463eb119fe839644bb05d7c9bf6d
|
File details
Details for the file hermes_acp_sdk-0.1.0-py3-none-any.whl.
File metadata
- Download URL: hermes_acp_sdk-0.1.0-py3-none-any.whl
- Upload date:
- Size: 26.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
319407c1a7968cd059b31a0acf238a3420d7ea841147f8d4770353295962c95d
|
|
| MD5 |
8f7f5c59eb3f0e36c0de213a0d802d72
|
|
| BLAKE2b-256 |
dd2c5400e23f75b5de1d76cc0c39c61e91cdceced129058b1ac0c8fa71a19082
|