Aphrodite 💋 Hermes Plugin
[!NOTE]
CCR compression plugin for Hermes Agent - thin Python loader + Rust dylib. Sub-ms tool output compression, 30-type classifier, 13 tools, 6 hooks, context engine. Skills ship dev-side, not with the plugin.
Aphrodite intercepts tool output before it reaches the LLM and replaces it with compact, structured previews. The agent sees 15 tokens of metadata instead of 500 tokens of raw text - and retrieves the full content only when it actually needs it. All compression logic runs in the Rust dylib.
Install ⚡
Hermes plugin + explicit setup
Terminal
git clone https://github.com/PlayForm/Aphrodite-Hermes.git
cd Aphrodite-Hermes
ln -s "$(pwd)" ~/.hermes/plugins/aphrodite
bash download.sh # explicit setup step: binary + dylib from GitHub Releases
cd ..
hermes plugins enable aphrodite
hermes
The ln -s line links the plugin into ~/.hermes/plugins/ so Hermes can
discover it. Create it at install time - the plugin never creates, converts,
or recreates this link: ~/.hermes/plugins/aphrodite is Hermes-owned
(layout_check.py is report-only; the install path may be a symlink or a
real directory).
Binaries are never shipped inside the repository. The plugin downloads
them from the GitHub release (tag pinned in BINARY_VERSION, checksums
generated by Build.yml into the in-tree SHA256SUMS.txt) with an
explicit setup step - bash ./download.sh (or pwsh ./download.ps1 on
Windows) - which verifies SHA-256 against the in-tree checksum list and
refuses on mismatch. register() never downloads and never writes into
~/.hermes/plugins/aphrodite/: if the binaries are missing it logs the
setup command and the plugin stays disabled until they are present. No Rust
toolchain required.
[!IMPORTANT]
Native Windows: run
pwsh ./download.ps1instead ofdownload.sh- no Git Bash/WSL needed. See Windows install for the full walkthrough, and Troubleshooting if the proxy doesn't come up after enabling the plugin.
LLM provider configuration (required)
The aphrodite proxy is an OpenAI-compatible LLM API proxy - it forwards requests upstream - so it needs its own provider credentials even though Hermes already has a provider configured. The plugin cannot read Hermes' provider config, and there is no keyless / compression-only mode: without a key the proxy refuses to start and the plugin is unusable.
Terminal
export APHRODITE_API_KEY="sk-..." # REQUIRED
export APHRODITE_API_URL="https://api.openai.com" # optional
export APHRODITE_MODEL="default-model" # optional
Alternatives: run aphrodite setup, or add api_key / api_url / model to
~/.hermes/aphrodite/aphrodite.toml. If the proxy fails to start,
~/.hermes/aphrodite/proxy-stderr.log shows the reason - no API key configured means the key is missing.
What changes after install
After installing and launching Hermes once:
Filesystem
~/.hermes/
├── plugins/
│ └── aphrodite → /path/to/Aphrodite-Hermes ← manual symlink to this repo
├── aphrodite/
│ ├── binaries/
│ │ ├── aphrodite ← proxy binary (fetched by `download.sh` ~35 MB)
│ │ └── libaphrodite_hermes.dylib ← plugin dylib (fetched by `download.sh`)
│ ├── ccr.db ← SQLite CCR store (on first run)
│ └── proxy-stderr.log ← proxy logs (on failure)
The plugin also adds to your Hermes config:
config.yaml
# Added automatically on enable
plugins:
enabled:
- aphrodite
# Recommended additions (manual)
context:
engine: aphrodite
engine_threshold_pct: 100 # 100 = engine effectively off; lower = compress sooner
model:
context_length: 1000000
Two proxy processes launch on :9797 (cache) and :9798 (token).
On registration the plugin probes both health endpoints and reuses an
already-running proxy pair instead of launching a second instance.
Verify it's working
Terminal
# In a Hermes session:
aphrodite_stats
# Or via CLI:
curl http://127.0.0.1:9798/health
# → {"status":"healthy","version":"<installed binary version - see BINARY_VERSION>"}
Clean uninstall
Terminal
hermes plugins disable aphrodite
rm ~/.hermes/plugins/aphrodite # remove the manual symlink you created
pkill -f "$HOME/.hermes/aphrodite/binaries/aphrodite"
Architecture 🏗️
The plugin is a thin Python registration shim over a Rust dylib - every hook, tool, and byte of compression logic lives in Rust. Python exists only to load the dylib via ctypes and register its surface with Hermes.
Layers
Call chain
Hermes Agent (hooks + tool dispatch)
│
▼
plugins/aphrodite/__init__.py ← ~1,150-line Python loader
│ ctypes FFI, registers hooks/tools/engine - no logic
▼
libaphrodite_hermes.dylib ← Hermes bridge (JSON contract)
│ 6 hooks · 13 tools · schemas
▼
libaphrodite (core engine) ← ALL compression logic
│ hooks · resolve · retrieve · marker · preview
│ stage2 · struct_extract · state · session
│ catalog · prefetch · poll_worker · directives
│ config_loader · builtin_directives
▼
CCR store (SQLite :9798 / in-memory :9797 / inline)
Data flow
Plugin mode
Tool executes → output intercepted by hook
↓
classify → preview → store (SQLite / in-memory / inline)
↓
Agent ← [type:enriched preview] (not raw output)
↓
aphrodite_retrieve(hash) → full content (only when needed)
Dual listeners
The plugin auto-launches two proxy processes (and reuses an already-running pair instead of starting a second instance):
| Listener | Port | CCR backend | Threshold | Best for |
|---|---|---|---|---|
| Cache | :9797 | In-memory (DashMap, 10K entries) | >8 KB | Speed, transient sessions |
| Token | :9798 | SQLite (persistent) | >1 KB | Durability, tool relay |
Hooks
Six Hermes hooks drive the plugin (provides_hooks in plugin.yaml), all
dispatched to the Rust dylib:
| Hook | Role |
|---|---|
on_session_start |
Engine bootstrap, directive seeding, proxy health check |
transform_tool_result |
Compress every tool result before it reaches the LLM |
transform_terminal_output |
Compress terminal output with exit-code context |
pre_llm_call |
Inject directives, compress overflowing middle turns |
post_llm_call |
Capture savings, update adaptive thresholds |
pre_tool_call |
Prefetch-aware dispatch, auto-background slow calls |
[!NOTE]
No hot-reload: the dylib is resolved once per process from the installed binary set and loaded directly (
ctypes.CDLLon the canonical path). The load is deterministic from the pinned tree - it never races a rebuild. To pick up a new build, restart the Hermes session.
Tools 🛠️
| Tool | Description |
|---|---|
aphrodite_retrieve |
Resolve <<<CCR:hash|type|size>>> markers |
aphrodite_compress |
Compress content via CCR with type hint |
aphrodite_stats |
Proxy health, engine status, inline store size |
aphrodite_rebuild |
Report binary/proxy version + a rebuild hint (does not rebuild or restart itself) |
aphrodite_files |
Tracked file references grouped by tool |
aphrodite_diff |
Conversation turn history with summaries |
aphrodite_search |
Search CCR store by keyword or type |
aphrodite_directive |
List/swap/add/remove/reset active behavioral directives |
aphrodite_test |
Smoke test suite: quick (1 sample) or full (3 samples) |
aphrodite_catalog |
Full CCR catalog with hashes, types, sizes, previews |
aphrodite_reclassify |
Retroactive metadata enrichment |
aphrodite_prefetch |
Read + compress files on demand; markers returned inline |
aphrodite_prefetch_status |
Live prefetch schedule: loading, ready, errors |
Configuration ⚙️
All tuning in aphrodite.toml - searched in ./aphrodite.toml, then
~/.hermes/aphrodite/aphrodite.toml (when APHRODITE_CONFIG_PATH is unset):
aphrodite.toml
[compression]
engine_threshold_pct = 45 # shipped default (dylib status flag); 100+ disables engine compression
engine_protect_first = 2 # messages to keep at start
engine_protect_last = 5 # messages to keep at end
engine_min_msgs = 8 # minimum before activating
tool_threshold_token = 512 # token proxy threshold (bytes)
tool_threshold_cache = 4096 # cache proxy threshold (bytes)
terminal_threshold = 1024 # terminal output threshold (bytes)
inline_threshold = 2048 # inline-vs-durable CCR storage cutoff (bytes)
code_multiplier = 3.0 # keep code in context longer
context_engine = true # dylib status flag; default-on, no env var needed
[previews]
model_family = "code_first" # compact | code_first | balance
code_structure_map = true # show fn/struct/class sigs
[prompts]
retrieve_guidance = "minimal"
ccr_marker_hint = false
Env var overrides: APHRODITE_ENGINE_THRESHOLD_PCT, APHRODITE_CONTEXT_ENGINE, etc.
See docs/config/env-vars.md.
Runtime home (one decision, shared by both halves)
The runtime home - where aphrodite.toml, binaries/, directives/,
ccr.db, and proxy-stderr.log live - is resolved by one shared
decision, made identically by the plugin shim and the Rust binary:
APHRODITE_HOME- explicit override; never second-guessed.<hermes-home>/aphrodite-$HERMES_HOMEwhen set (Docker image, profile gateways), else~/.hermes/aphrodite.
At startup the shim exports its decision into APHRODITE_HOME (and
APHRODITE_DIRECTIVES_DIR), so the dylib and the spawned proxy binary
resolve the same directory by construction - the two halves cannot diverge.
The startup log names the result: runtime home: <path> (decided by APHRODITE_HOME override|HERMES_HOME|default).
Upgrading from ≤ 2.1.5: when the Hermes-home-derived runtime home does
not hold an install but the pre-2.2 ~/.hermes/aphrodite does, the old home
is adopted (a one-line warning in the log) - nothing is migrated and
binaries/ / aphrodite.toml keep resolving. Adoption never fires for a
throwaway/scratch HERMES_HOME (e.g. the hermes plugins validate probe,
which must stay isolated from the real install). Set APHRODITE_HOME to pin
the location explicitly. The Rust binary follows the same resolution, so
standalone runs (aphrodite setup, proxy launches) agree even without the
shim.
Set APHRODITE_NO_AUTO_LAUNCH=1 to skip the proxy auto-launch entirely, e.g.
when a cargo watch dev loop runs the proxy itself.
Directives
Custom behavioral directives are name.md files in
~/.hermes/aphrodite/directives/ - an empty file means an intentionally
empty directive. The plugin does NOT ship a directives/ set: the binary
provides them (embedded builtins) and materializes them into the user-data
home at startup/setup, so the plugin dir stays a pure loader. The dylib
reads them from APHRODITE_DIRECTIVES_DIR (defaults to
~/.hermes/aphrodite/directives, user override wins). If no directive
directory is found, the compiled built-in set loads as a fallback - its
activation is logged. Manage them at runtime with aphrodite_directive
(list/swap/add/load/remove/reset).
Dev Install (Rust source)
Terminal
git clone https://github.com/PlayForm/Aphrodite.git
cd Aphrodite
cargo build -p aphrodite-hermes
# Dylib: target/debug/libaphrodite_hermes.dylib (crate aphrodite-hermes;
# `-p aphrodite` alone builds only the proxy binary). The loader resolves the
# dylib from the canonical runtime home first (env override
# APHRODITE_HERMES_DYLIB_PATH wins when set), so a dev loop copies it there:
mkdir -p ~/.hermes/aphrodite/binaries
cp target/debug/libaphrodite_hermes.dylib ~/.hermes/aphrodite/binaries/
An already-running dev proxy (cargo run -p aphrodite) that answers the
health endpoints is reused instead of relaunched - the plugin probes both
ports before launching (see _start_proxy).
Files
Layout
Aphrodite-Hermes/
├── __init__.py ← ~1,150-line Python loader (ctypes FFI)
├── plugin.yaml ← 13 tools, 6 hooks, context engine
├── download.sh ← Explicit binary fetch (macOS/Linux/Git Bash/WSL; optional - validates in-tree checksums)
├── download.ps1 ← Explicit binary fetch (native Windows PowerShell; optional)
├── BINARY_VERSION ← Pinned binary release tag
├── SHA256SUMS.txt ← In-tree checksums (GENERATED by Build.yml; download.sh verifies against this)
├── tests/ ← Plugin test suite
├── README.md ← This file
└── .gitignore
Binaries are never stored in this directory and register() never
downloads: the plugin downloads from the GitHub release only via the
explicit download.sh / download.ps1 setup step into
~/.hermes/aphrodite/binaries/ (the canonical runtime home), validated
against the in-tree SHA256SUMS.txt (refuse on mismatch). The plugin never
modifies ~/.hermes/plugins/aphrodite/.