Files
2026-07-10 15:02:09 +01:00

210 lines
9.2 KiB
Markdown

# Honcho Memory Plugin
Plugin: `crates/plugin-honcho/src/lib.rs`
HTTP client: `crates/honcho-client/` (separate workspace crate)
---
## Purpose
Streams completed chat turns to a [Honcho](https://honcho.dev) server so that it can extract long-term conclusions about the user (write path), and reads that context back into every LLM turn via the [`Memory`](memory.md) trait (read path).
---
## Self-hosted Docker package
A ready-to-run Docker Compose setup is in the [`honcho/`](../honcho/) folder at the project root.
It starts four services: the Honcho API, the deriver background worker, PostgreSQL + pgvector, and Redis.
**Quick start:**
```sh
cd honcho
cp .env.example .env
# Edit .env — set at least LLM_OPENAI_API_KEY=sk-...
docker compose up -d
# API available at http://localhost:8000
```
Full instructions, LLM provider options (OpenAI, OpenRouter, Ollama), and troubleshooting are in [`honcho/README.md`](../honcho/README.md).
---
## Setup
1. Start the Honcho server (see above).
2. Enable the plugin via the agent or REST API:
```json
PUT /api/plugins/honcho
{
"enabled": true,
"config": {
"base_url": "http://localhost:8000",
"api_key": "",
"workspace_id": "personal-agent"
}
}
```
Or ask the main agent: _"enable the honcho plugin"_.
---
## Configuration
Stored in the `plugins` SQLite table (`config` JSON blob). Managed at runtime — no entry in `config.yml`.
| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `base_url` | string | `http://localhost:8000` | Honcho server URL |
| `api_key` | string | _(empty)_ | API key; leave empty for local/unauthenticated instances |
| `workspace_id` | string | `personal-agent` | Honcho workspace identifier for this agent instance |
---
## Honcho Object Model
```
workspace (workspace_id from config — one per agent instance)
├── peer "user" observe_others=true
├── peer "assistant" observe_me=true
└── session one per local chat_sessions.id
├── message peer_id="user"
├── message peer_id="assistant"
└── …
```
**Workspace and peers** are created (idempotently) each time the plugin starts. If they already exist, the API returns an error which is logged at `WARN`/`DEBUG` and ignored.
**Sessions** are created lazily on the first event for a new `chat_sessions.id`, then cached in memory for the life of the listener task. The Honcho session UUID is stored in the session cache but not persisted to SQLite — restarting the plugin creates new Honcho sessions for subsequent events.
---
## Event Filtering
An event is forwarded only when **all** of the following conditions hold:
| Condition | Reason |
| --- | --- |
| `is_interactive = true` | A real user is in the conversation |
| `is_ephemeral = false` | Not a short-lived automated session (cron, tic) |
| `is_synthetic = false` | Message content was typed by the user, not injected by the system |
| `role` is `User` or `Assistant` | Sub-agent messages (`Agent` role) are skipped |
| `content` is non-empty | Guard against empty strings |
---
## Lifecycle
1. **`start()`** — subscribes to `skald.event_bus`, calls `ensure_workspace_ready` (best-effort), then spawns the listener task.
2. **Listener task** — `tokio::select!` loop on the bus receiver and a `CancellationToken`. On `RecvError::Lagged`, logs a warning and continues (some turns are missed but the task stays alive).
3. **`stop()`** — cancels the token and awaits the task.
4. **`reload()`** — follows the standard plugin pattern: start/stop/restart-on-change.
---
## Error Handling
All Honcho API errors are **fire-and-forget**: logged as `warn!` and never propagated to the session handler or the user. A Honcho outage has zero impact on chat functionality.
`HonchoError::Request`'s `Display` walks the full `source()` chain, so transport
failures surface the real cause in logs (e.g. `Request failed: error sending
request for url (...): Connection reset by peer`) instead of just reqwest's
opaque top-line. This makes host↔container issues (e.g. a stale Docker Desktop
port-forward after a container recreation) diagnosable from the `warn!` alone.
---
## Read Path
`HonchoMemory` implements the [`Memory`](../memory.md) trait. Before each LLM turn,
`query_context` is called automatically by `ChatSessionHandler::handle_message` — for
**all** session types: interactive, cron, and tic.
### Flow
1. Checks `is_available()` — returns `None` immediately if the plugin is stopped.
2. Looks up the Honcho session UUID for the local `session_id` in the shared `session_map`.
3. **If a mapping exists** (interactive session with at least one turn written):
- Calls `client.session_context(workspace_id, honcho_session_id, tokens=2000, search_query=user_msg)`.
- Returns the formatted result on success.
- On error: logs `warn!` and falls through to the peer-context fallback **without** `search_query`
(avoids a second embedding of the same user message — `session_context` already embedded it before failing).
4. **Fallback — `peer_context("user")`** (no mapping, or session_context error):
- Cold start / cron / tic (no `session_map` entry): calls with `search_query=user_msg` for relevance.
- After a `session_context` failure: calls **without** `search_query` to avoid double-embedding.
- Returns global user knowledge derived from all sessions Honcho has observed.
- On error: logs `warn!` and returns `None`.
The formatted context is prepended to `extra_system_context` and injected into the system prompt. Errors are never propagated — they degrade gracefully to `None`.
### Context format
`format_context()` extracts, in priority order:
1. `conclusions[].content` → "Known facts about the user: …"
2. `summary` → "Conversation summary: …"
3. Fallback: pretty-printed raw JSON
The result is wrapped in `--- Honcho memory context --- / --- end ---` markers.
---
## LLM-callable Tools
`HonchoMemory::tools()` returns **five** tools whenever the plugin is active
(`is_available()` true). They give the LLM direct, on-demand access to every
layer of Honcho's API, complementing the automatic pre-turn `query_context`
injection. All operate on the `user` peer and are inherited by sub-agents via
`AgentRunConfig::memory_tools`.
The [official Honcho documentation](https://honcho.dev/docs/v3/documentation/features/chat) recommends exposing these as tools so the agent decides on its own when to read or write memory, rather than only relying on automatic injection.
| Tool | Endpoint | Cost | What it does |
| --- | --- | --- | --- |
| `memory_query` | `POST .../peers/user/chat` | High (LLM synthesis) | Natural-language question → synthesized answer (dialectic reasoning, `reasoning_level=low`) |
| `honcho_search` | `GET .../peers/user/context?search_query=…` | Low | Semantic search over derived facts; returns raw ranked excerpts (with ids when present) |
| `honcho_context` | `GET .../peers/user/context` | Low | Full context snapshot (conclusions + summary), no synthesis; optional focus `query` |
| `honcho_profile` | `GET`/`PUT .../peers/user/card` | Low | Read the peer card, or overwrite it with a list of fact strings (`card`) |
| `honcho_conclude` | `POST .../conclusions` / `DELETE .../conclusions/{id}` | Low | Write a new fact (`conclusion`) or delete one by id (`delete_id`); exactly one required |
**Peer model — all tools operate on the `user` peer as both observer and observed.**
This plugin configures the `user` peer with `observe_me = true`, so the user's
self-knowledge lives in the `observer = user / observed = user` slot. Therefore
`honcho_conclude` writes with `observer_id = observed_id = user`, and `honcho_search`
uses `peer_context` (not the `conclusions/query` endpoint, which requires explicit
observer/observed filters) — the same proven path as the automatic read-path
injection. This differs from setups where the assistant observes the user
(`observer = assistant`); keeping observer = user is what lets the read-path see
facts written by `honcho_conclude`.
**When to use vs. the automatic injection:**
| Mechanism | When it fires | Best for |
| --- | --- | --- |
| `query_context` (auto) | Before every LLM turn | Background context, cold-start facts |
| `memory_query` (tool) | LLM calls it explicitly | On-demand deep reasoning mid-conversation |
| `honcho_search` / `honcho_context` (tools) | LLM calls them explicitly | Cheap raw recall without LLM synthesis |
| `honcho_profile` / `honcho_conclude` (tools) | LLM calls them explicitly | Actively curating long-term memory |
**Implementation note:** `Tool::execute` is synchronous but the Honcho calls are
async. All five tools share the `run_blocking` helper, which uses
`tokio::task::block_in_place` + `Handle::current().block_on(...)` to drive the
future from within the Tokio multi-thread scheduler without spawning a new thread.
---
## Future Work
- **Session persistence** — store the Honcho session UUID in a new `chat_sessions.honcho_session_id` column so the mapping survives a plugin restart.
---
## When to Update This File
- Config fields change
- Honcho object model or peer setup changes
- Filtering rules change
- `query_context` flow changes (session vs peer fallback logic)
- Docker Compose setup in `honcho/` changes significantly
- Public API of `crates/honcho-client/` changes