Skip to main content

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-agent requires 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 missing hermes binary. (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_weakness on a server named app-tools reaches the model as mcp__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, terminal and execute_code tools 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 cwd you 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; a PermissionDenied event is emitted so you can see it happened. Opt in with AllowTools([...]) (allow-list), CallbackPolicy(fn) (decide per request), or AllowAll() (unsafe — local experiments only).

  • Filesystem — off. FsPolicy() allows no reads or writes. Opt in with FsPolicy(root=Path("./workspace"), allow_read=True); paths are resolved against root and 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:

  1. new_session lies about the model. It reports a current_model_id, but inference still goes out with an empty model — so every prompt fails with HTTP 400: ... but you passed .. You must call set_session_model explicitly. session() always does this for you. This is the single biggest reason the package exists.
  2. Never pass --provider / -m to hermes. Those flags blank out the model. The provider is auto-detected from the environment (e.g. DEEPSEEK_API_KEY).
  3. Model ids are provider:model"deepseek:deepseek-v4-pro", not "deepseek-v4-pro".
  4. Hermes can't speak ACP without its extrapip install "hermes-agent[acp]", or the hermes acp subcommand does not exist.
  5. The ACP Client role 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

hermes_acp_sdk-0.1.0.tar.gz (38.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

hermes_acp_sdk-0.1.0-py3-none-any.whl (26.7 kB view details)

Uploaded Python 3

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

Hashes for hermes_acp_sdk-0.1.0.tar.gz
Algorithm Hash digest
SHA256 0b873557dc0d26f038a416551b74a2b60f06c78331c5f0af468b2022b31fc7e7
MD5 e73814447fe248447fe31fe3d6fcd99c
BLAKE2b-256 7a94ec0714e15c3975b495b80c436a227966463eb119fe839644bb05d7c9bf6d

See more details on using hashes here.

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

Hashes for hermes_acp_sdk-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 319407c1a7968cd059b31a0acf238a3420d7ea841147f8d4770353295962c95d
MD5 8f7f5c59eb3f0e36c0de213a0d802d72
BLAKE2b-256 dd2c5400e23f75b5de1d76cc0c39c61e91cdceced129058b1ac0c8fa71a19082

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page