26382a7ac6
CI / Clippy (push) Failing after 15m13s
CI / Test (ubuntu-latest) (push) Failing after 16m1s
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Build (no embeddings / no ORT) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Cookbook (Node) (push) Has been cancelled
CI / Pi Extension (Node) (push) Has been cancelled
CI / Rust SDK (lean-ctx-client) (push) Has been cancelled
CI / Embed SDK (lean-ctx-sdk) (push) Has been cancelled
CI / Python SDK (leanctx) (push) Has been cancelled
CI / Hermes Plugin (Python) (push) Has been cancelled
CI / SDK Conformance Matrix (push) Has been cancelled
CI / Coverage (push) Has been cancelled
CI / cargo-deny (push) Has been cancelled
CI / Adversarial Safety (push) Has been cancelled
CI / Benchmarks (push) Has been cancelled
CI / Output-Quality Gate (eval A/B) (push) Has been cancelled
CI / Documentation (push) Has been cancelled
CI / CI Green (push) Has been cancelled
JetBrains Plugin / Actionlint (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (rust) (push) Has been cancelled
JetBrains Plugin / Validation (push) Has been cancelled
JetBrains Plugin / Build (push) Has been cancelled
JetBrains Plugin / Test (push) Has been cancelled
Security Check / Security Scan (push) Has been cancelled
204 lines
9.3 KiB
Markdown
204 lines
9.3 KiB
Markdown
# Journey 20 — Hermes Context Engine
|
||
|
||
> You're embedding lean-ctx *inside* an agent framework rather than calling it as
|
||
> an MCP server. This journey covers making lean-ctx the **active context engine**
|
||
> for [Hermes Agent](https://github.com/hermes-agent): it replaces Hermes' built-in
|
||
> `ContextCompressor`, owns the context window, and gives the agent first-class
|
||
> recall tools to page durable memory back in losslessly.
|
||
|
||
Source files referenced here:
|
||
- `rust/src/tools/ctx_transcript_compact.rs` — `compact_messages`, `render_result`,
|
||
`serialize_transcript` (the deterministic compaction core)
|
||
- `rust/src/tools/registered/ctx_transcript_compact.rs` — the MCP/`/v1` tool wrapper
|
||
- `integrations/hermes-lean-ctx/engine.py` — `LeanCtxEngine` (the `ContextEngine` adapter)
|
||
- `integrations/hermes-lean-ctx/compaction.py` — the local Python fallback
|
||
- `integrations/hermes-lean-ctx/tools.py`, `schemas.py` — native recall tools
|
||
- `integrations/hermes-lean-ctx/transport.py`, `config.py`, `presets.py` — `/v1`
|
||
client, `LEANCTX_*` config, model-window presets
|
||
- `integrations/hermes-lean-ctx/plugin.yaml` — the Hermes plugin manifest
|
||
|
||
---
|
||
|
||
## 0. The mental model — engine vs. MCP server
|
||
|
||
Every other journey treats lean-ctx as a tool an agent *calls*. Here it is the
|
||
other way around: lean-ctx becomes the component the agent loop *delegates its
|
||
context window to*.
|
||
|
||
- As an **MCP server**, lean-ctx answers `ctx_*` tool calls the model decides to
|
||
make. It never sees the full conversation.
|
||
- As a **context engine**, the host (Hermes) hands lean-ctx the entire message
|
||
array on every turn and asks it to compact it. lean-ctx decides what stays
|
||
verbatim, what becomes a recoverable summary, and what gets offloaded into
|
||
durable session memory.
|
||
|
||
Only one context engine can be active at a time, so lean-ctx and Hermes'
|
||
built-in `ContextCompressor` (or `hermes-lcm`) are mutually exclusive.
|
||
|
||
---
|
||
|
||
## 1. The compaction core — `ctx_transcript_compact`
|
||
|
||
**What it does:** Compacts an OpenAI-format message array deterministically. It
|
||
keeps the system preamble and a *fresh tail* verbatim, replaces older turns with
|
||
a recoverable summary, and offloads the raw turns into lean-ctx session memory so
|
||
the recall tools (and the autonomy consolidation pipeline) can page them back in.
|
||
|
||
It is the 77th MCP tool and is exposed on both the MCP surface and the HTTP `/v1`
|
||
tools API, so every client — this plugin, the CLI, other editors — gets the same
|
||
tested behaviour.
|
||
|
||
```text
|
||
ctx_transcript_compact messages=<OpenAI message array>
|
||
fresh_tail_tokens=4000 # recent tokens kept verbatim
|
||
protect_min_messages=6 # min recent messages kept verbatim
|
||
focus_topic="auth refactor" # optional: bias the summary
|
||
```
|
||
|
||
| Parameter | Default | Meaning |
|
||
|---|---|---|
|
||
| `messages` (required) | – | OpenAI-format message array to compact |
|
||
| `fresh_tail_tokens` | `4000` | Recent tokens kept verbatim (the fresh tail) |
|
||
| `protect_min_messages` | `6` | Minimum recent messages kept verbatim |
|
||
| `focus_topic` | – | Optional topic to prioritise in the summary |
|
||
|
||
**Returns** JSON `{messages, stats}`: the compacted array plus deterministic
|
||
stats (`original_tokens`, `compacted_tokens`, `did_compact`, …).
|
||
|
||
**Two invariants, enforced and tested:**
|
||
1. **A `tool_call` and its `tool_result` are never split** across the compaction
|
||
boundary. Truncating between them would leave the model with a dangling call.
|
||
2. **Output is byte-stable** for the same input — no timestamps, counters or
|
||
randomness — so it preserves the provider's prompt-cache prefix (#498).
|
||
|
||
**Under the hood:** `compact_messages()` (`rust/src/tools/ctx_transcript_compact.rs`)
|
||
splits the array into the protected head + tail and the summarizable middle,
|
||
renders the summary, and returns a `CompactResult`. The registered wrapper
|
||
(`registered/ctx_transcript_compact.rs`) then best-effort offloads the summarized
|
||
turns into the bound session via `ctx_session` (as a `finding`), capped at
|
||
`OFFLOAD_MAX_CHARS` (8 000). Offload is skipped when no session is bound (e.g. a
|
||
one-shot CLI call), so the tool is safe to call anywhere.
|
||
|
||
---
|
||
|
||
## 2. The plugin — `integrations/hermes-lean-ctx`
|
||
|
||
**What it does:** A thin Python `ContextEngine` (`LeanCtxEngine`, `engine.py`) that
|
||
Hermes loads via `register_context_engine`. It is an adapter, not a
|
||
re-implementation — the heavy lifting stays in the daemon.
|
||
|
||
```
|
||
Hermes agent loop
|
||
└─ ContextEngine ABC ── LeanCtxEngine (this plugin, thin adapter)
|
||
└─ leanctx SDK ── HTTP /v1 ── lean-ctx daemon
|
||
└─ ctx_transcript_compact,
|
||
ctx_search, ctx_knowledge, …
|
||
```
|
||
|
||
- **`compress(messages)`** keeps the system preamble + fresh tail verbatim and
|
||
replaces older turns with a recoverable summary. It calls the daemon's
|
||
`ctx_transcript_compact`; if the daemon is unreachable it falls back to a pure
|
||
Python compaction (`compaction.py`) so the agent loop never breaks.
|
||
- **Native recall tools** (`tools.py` / `schemas.py`) inject `ctx_search`,
|
||
`ctx_semantic_search`, `ctx_read`, `ctx_expand`, `ctx_knowledge` and
|
||
`ctx_summary` into the agent's tool list, so the model can page detail back in
|
||
on demand after a compaction.
|
||
- **Cross-session persistence** via session lifecycle hooks: `resume` on start,
|
||
`ctx_summary` + a deterministic `ctx_handoff` ledger on end.
|
||
- **Model-window presets** (`presets.py`) infer the context window from the model
|
||
name until the host calls `update_model(context_length=…)`, which always wins.
|
||
|
||
---
|
||
|
||
## 3. Setup
|
||
|
||
```bash
|
||
# 1. Install the plugin (symlinks this checkout into ~/.hermes/plugins).
|
||
cd integrations/hermes-lean-ctx && ./scripts/install.sh
|
||
|
||
# 2. Start the lean-ctx HTTP tools API (serves /v1; default port 8080).
|
||
# NOTE: the always-on proxy (4444+) does NOT serve /v1/tools — use `serve`.
|
||
lean-ctx serve --host 127.0.0.1 --port 8080
|
||
|
||
# 3. Install the SDK in Hermes' Python.
|
||
pip install lean-ctx-client
|
||
|
||
# 4. Activate the engine in ~/.hermes/config.yaml:
|
||
# context:
|
||
# engine: "lean-ctx"
|
||
```
|
||
|
||
`lean-ctx init --agent hermes` prints this same engine-plugin hint, so the
|
||
onboarding path points here automatically.
|
||
|
||
If the server is not on the default, point the plugin at it:
|
||
|
||
```bash
|
||
export LEANCTX_BASE_URL=http://127.0.0.1:8080
|
||
export LEANCTX_TOKEN=<token> # only if you ran serve with --auth-token
|
||
```
|
||
|
||
---
|
||
|
||
## 4. Configuration — `LEANCTX_*` env vars
|
||
|
||
Read by `config.py`; Hermes' explicit `update_model(context_length=…)` always
|
||
overrides the inferred window.
|
||
|
||
| Variable | Default | Meaning |
|
||
|---|---|---|
|
||
| `LEANCTX_BASE_URL` | `http://127.0.0.1:8080` | lean-ctx `/v1` base URL |
|
||
| `LEANCTX_HTTP_PORT` | `8080` | Port used when `LEANCTX_BASE_URL` is unset |
|
||
| `LEANCTX_TOKEN` | – | Bearer token (if `serve --auth-token`) |
|
||
| `LEANCTX_TIMEOUT` | `30.0` | HTTP timeout (seconds) |
|
||
| `LEANCTX_CONTEXT_LENGTH` | `200000` | Window used until the host calls `update_model` |
|
||
| `LEANCTX_THRESHOLD_FRACTION` | `0.75` | Fraction of the window at which compaction fires |
|
||
| `LEANCTX_PROTECT_FRACTION` | `0.25` | Recent fraction kept verbatim (the fresh tail) |
|
||
| `LEANCTX_PROTECT_MIN_MESSAGES` | `6` | Minimum recent messages kept verbatim |
|
||
| `LEANCTX_PROTECT_MIN_TOKENS` | `2000` | Minimum tail token budget |
|
||
| `LEANCTX_ENABLE_TOOLS` | `1` | Inject native recall tools into the agent |
|
||
| `LEANCTX_CORE_COMPACTION` | `1` | Prefer the daemon tool (fallback: local Python) |
|
||
| `LEANCTX_WORKSPACE_ID` / `LEANCTX_CHANNEL_ID` | – | Optional routing for multi-workspace daemons |
|
||
|
||
---
|
||
|
||
## 5. Why lean-ctx over the alternatives
|
||
|
||
| | built-in `ContextCompressor` | `hermes-lcm` | **hermes-lean-ctx** |
|
||
|---|---|---|---|
|
||
| Strategy | summarize + drop | DAG + SQLite + FTS | BM25 + graph + knowledge + semantic + LITM placement |
|
||
| Recall after compaction | lossy | lossless (grep/expand) | lossless (`ctx_search`/`ctx_semantic_search`/`ctx_expand`/`ctx_read`/`ctx_knowledge`) |
|
||
| Cross-session memory | no | per-project | yes (sessions, knowledge, handoff ledgers) |
|
||
| Determinism / prompt-cache | n/a | partial | deterministic, byte-stable output |
|
||
| Engine location | in-agent | in-plugin | in the lean-ctx daemon (single source of truth) |
|
||
|
||
---
|
||
|
||
## 6. Testing & benchmarks
|
||
|
||
```bash
|
||
# Hermetic unit tests (no daemon required):
|
||
cd integrations/hermes-lean-ctx && python -m pytest
|
||
|
||
# Live integration against a real daemon:
|
||
lean-ctx serve --host 127.0.0.1 --port 8080 --auth-token test-token &
|
||
LEANCTX_LIVE_URL=http://127.0.0.1:8080 LEANCTX_LIVE_TOKEN=test-token \
|
||
python -m pytest tests/test_live_daemon.py -v
|
||
```
|
||
|
||
`benchmarks/` (`run.py`) is a real, runnable head-to-head harness — token
|
||
savings, `compress()` latency and recoverable-recall against the import-guarded
|
||
`ContextCompressor` / `hermes-lcm`. No mock data: unit tests exercise the real
|
||
compaction logic and a recording gateway; live tests hit a real daemon.
|
||
|
||
---
|
||
|
||
## Where the neighbouring topics live
|
||
|
||
| Topic | Reference |
|
||
|---|---|
|
||
| The `/v1` HTTP+SSE contract and SDKs | [Team, Cloud & CI](09-team-cloud-ci.md) |
|
||
| `lean-ctx serve` over HTTP, multi-repo | [Advanced & Integrations](05-advanced.md) |
|
||
| Sessions, knowledge, handoff ledgers | [Memory & Knowledge](03-memory-and-knowledge.md) |
|
||
| Writing your own plugins / WASM | [Advanced & Integrations](05-advanced.md) |
|