Development - Retinue

Table of Contents

Back to main documentation

1. Architecture

Retinue uses:

model2vec
Static embedding model (minishlab/potion-multilingual-128M, ~540 MB download on first use, then offline; the repo's unused ~512 MB ONNX export is skipped). Understands meaning across languages.
sqlite-vec
SQLite extension for vector similarity search. No server, no daemon, no port.
Unified store
All memories share one table. Each memory has a content string, a timestamp, and an auto-generated embedding.

1.1. Database layout

Retinue stores all data in a single SQLite virtual table under the active Hermes home:

$HERMES_HOME/retinue_memory.db

"memories" virtual table:

Column Type Meaning
rowid INTEGER Auto-increment primary key. Stable ID used for deletion.
embedding float[MODEL_DIM] The model2vec embedding vector for content.
content TEXT The full text of the memory/fact.
created TEXT ISO 8601 UTC timestamp when the row was inserted.

MODEL_DIM depends on the model (hardcoded to minishlab/potion-multilingual-128M). The table is created with sqlite-vec and supports cosine-similarity search.

Why a virtual table? In SQLite, a virtual table is created with CREATE VIRTUAL TABLE ... USING vec0(...). The vec0 module from sqlite-vec provides the storage and indexing logic for the embedding column, which enables the embedding MATCH ? vector similarity query. The table is still persisted in the same SQLite .db file; “virtual” only means SQLite delegates the table engine to the sqlite-vec extension.

2. Project structure

Retinue/
├── Documentation/
│   └── index.org             # This file
├── Install Retinue           # Deploy into the active Hermes plugin directory
├── plugin.yaml               # Hermes plugin metadata
├── __init__.py               # Plugin entry point: register(ctx)
├── src/                      # Plugin source package
│   └── retinue/
│       ├── __init__.py       # Package init
│       ├── memory.py        # Memory engine: embeddings + sqlite-vec
│       ├── provider.py      # MemoryProvider subclass
│       └── cli.py           # Debug CLI behind "Show Retinue memory contents"
├── Tools/                    # Development helpers (desktop scripts)
│   ├── Run tests            # Standalone smoke test runner
│   ├── Show Retinue memory contents  # Debug CLI: dump/search all memories
│   ├── Import Holographic memories   # Migrate facts from Holographic memory_store.db
│   └── Update web site      # Export Documentation/*.org to HTML + publish
└── $HERMES_HOME/retinue_memory.db    # Created at runtime: SQLite + sqlite-vec

3. What the installer does

Install Retinue is the deployment bridge between the Retinue workspace and the active Hermes home. It is intended for iterative development: edit the source tree, run the script, and test inside Hermes immediately.

The script is idempotent, so you can run it after every code change. It preserves runtime artifacts (the database, virtual environments, and caches) while replacing the plugin source code.

What it does, in order:

  1. Detects the active Hermes home from the HERMES_HOME environment variable, or falls back to $HOME/.hermes.
  2. Creates $HERMES_HOME/plugins/retinue/ and its .hf-cache/ subdirectory if they do not exist.
  3. Removes every previously deployed item in the plugin directory except the PRESERVE list (.venv, .hf-cache), so stale code cannot linger.
  4. Copies the whitelist (plugin.yaml __init__.py src) from the workspace into the plugin directory with cp.
  5. Resolves the Python interpreter that Hermes actually runs under (via the PYTHON environment variable, common venv layouts, or the hermes launcher shebang) and installs any missing sqlite-vec / model2vec dependencies into it with pip (falling back to --break-system-packages inside that interpreter on PEP 668 systems).
  6. Pre-downloads the embedding model into .hf-cache/ by calling retinue.memory.get_model() with the Hermes Python — the same code path the plugin uses at runtime. The snapshot download skips the repo's unused ~512 MB ONNX export (ignore_patterns["onnx/*"]=) and loads from the returned local path, so the cache is ~540 MB and the first Hermes session does not stall. Failure here is non-fatal: a warning is printed and the model simply downloads on first use instead.
  7. Sets memory.provider: retinue with hermes config set if the CLI is available.
  8. Runs a lightweight import check to confirm the plugin loads.

To target a specific Hermes profile or home directory:

HERMES_HOME="$HOME/.hermes/profiles/coder" bash "Install Retinue"

4. Tools exposed to Hermes

When Retinue is the active memory provider, Hermes can call these tools natively. The exact names and schemas are defined in get_tool_schemas() and dispatched through handle_tool_call().

4.1. retinue_memory_add

Store a memory with content.

{
  "name": "retinue_memory_add",
  "description": "Store a semantic memory in Retinue.",
  "parameters": {
    "type": "object",
    "properties": {
      "content": {"type": "string", "description": "The memory content"}
    },
    "required": ["content"]
  }
}

Read more: How memory save works.

4.3. retinue_memory_delete

Delete a memory by ID.

4.4. retinue_memory_stats

Returns facts about the store itself — not the memories. Takes no parameters; the LLM calls it with an empty object:

retinue_memory_stats({})

and gets back a single object: total (number of stored memories), latest_id (the rowid assigned to the most recent memory — the next memory gets latest_id + 1, and ids up to it are valid delete candidates even after deletions), db_path (which database file is in use), model (embedding model name), and dim (embedding vector dimension):

{"success": true, "result": {"total": 2418, "latest_id": 2675,
  "db_path": "/home/john/.hermes/retinue_memory.db",
  "model": "minishlab/potion-multilingual-128M", "dim": 256}}

5. How memory save works

  1. LLM decides something is worth remembering it issues the call:

    retinue_memory_add({"content": "ProjectX is hosted on our primary server"})
    

    Alternative trigger: LLM instead calls Hermes' built-in memory tool. Hermes then calls Retinue's on_memory_write hook, which feeds the same text into the same chain at step 2. Both triggers produce one identical memory in the same single store — one memory system, two ways to trigger a save.

  2. Hermes routes it to handle_tool_call("retinue_memory_add", args).
  3. Text is embedded with model2vec into a float32 vector packed as bytes. On the first call in a process, memory.py resolves the model snapshot with snapshot_download(MODEL, ignore_patterns["onnx/*"])= (skipping the repo's unused ~512 MB ONNX export) and loads StaticModel from the returned local path — loading from a local path is what prevents re-fetching the skipped files on every load;
  4. Resulting vector with original text is written into database

    INSERT INTO memories (embedding, content, created)
    
  5. JSON string is then returned back to the LLM:

    {"success": true, "result": {"id": 1, "content": "ProjectX is hosted on our primary server"}}
    

6. How memory retrieval works

Retrieval happens through exactly one concrete path: the retinue_memory_search tool. The prefetch hook is a no-op, so nothing is injected automatically — every retrieval is an explicit tool call visible in the conversation transcript.

The step-by-step mechanics:

  1. The LLM is notified the tool exists. At session start Hermes calls get_tool_schemas() and places all four retinue_memory_* schemas into the LLM's tool list next to the built-in tools. The retinue_memory_search schema declares two parameters: query (required string) and limit (optional integer, default 50). Separately, the text of src/retinue/system_prompt_block.txt is injected into the system prompt, instructing the LLM to run retinue_memory_search before acting on a task.
  2. The LLM invokes it with a natural-language query. Example call: retinue_memory_search({"query": "coworker", "limit": 2}).
  3. Retinue embeds the query and runs a vector query. The query text is embedded with the same model2vec model used at write time, then MemoryStore.search() executes a sqlite-vec KNN query: WHERE embedding MATCH ? AND k = <limit> against the memories virtual table.
  4. Result count is limited by count only. The k parameter is a hard top-N cap: sqlite-vec returns at most limit nearest rows, with no similarity threshold. Even a nonsense query ("zzzzqqq nonexistent") returns exactly limit rows. Filtering weak matches is left to the LLM reading the scores.
  5. Results are sorted best-first. sqlite-vec orders KNN results by ascending distance, so the most similar memory is always results[0]. Ordering is deterministic; ties can occur (two different memories scored 0.458 on the same query).
  6. The tool returns a JSON string — the result of json.dumps({"success": True, "result": [...]}). Each element has four fields: id (the rowid, for retinue_memory_delete), content (full stored text), created (ISO 8601 UTC timestamp), and similarity. The similarity is computed in provider.py from the raw sqlite-vec distance as 1 / (1 + distance) — higher is better, 1.0 means identical vectors. (Raw distance in the database is unbounded L2 on the model's non-normalized embeddings, so typical real-world matches land in the 0.4–0.6 range; treat the score as a relative ranking signal, not an absolute confidence.)

    {"success": true, "result": [
      {"id": 2, "content": "Coworker John reads Spanish",
       "created": "2026-07-17T20:33:43.081952+00:00", "similarity": 0.4626},
      {"id": 1, "content": "ProjectX is hosted on our serverY",
       "created": "2026-07-17T20:33:40.987271+00:00", "similarity": 0.4357}
    ]}
    

    (Captured verbatim from handle_tool_call("retinue_memory_search", {"query": "coworker", "limit": 2}) against a store holding exactly those two memories; similarity values shortened for print.)

  1. The LLM reads the JSON and decides. Weak-looking results (all scores near the noise floor) mean the store has nothing relevant; the LLM proceeds without memory context.

7. Development and testing workflow

Retinue is developed in its own source tree and deployed into the active Hermes home with Install Retinue. This makes iteration fast and safe: the runtime data/ directory lives in Hermes, while the source code lives in the workspace.

7.1. Iterative development loop

  1. Edit source files in the Retinue workspace.
  2. Run the installer to copy the latest code into Hermes:

    cd /path/to/retinue
    bash "Install Retinue"
    
  3. Verify the plugin loads:

    hermes memory status
    
  4. Test through a Hermes session:

    hermes chat -q "Remember that my default shell is fish."
    hermes chat -q "What shell do I use?"
    
  5. Repeat from step 1.

7.2. Standalone testing

You can also test Retinue without installing it into Hermes. The project includes a standalone test runner:

cd /path/to/retinue
"Tools/Run tests"

This uses a project-local .venv and caches downloaded models in .hf-cache, both of which are excluded from deployment by the installer.

7.3. Inspecting the memory store

For debugging, the Show Retinue memory contents script dumps every stored memory to the terminal without going through Hermes:

cd /path/to/retinue
"Tools/Show Retinue memory contents"                                  # dump all memories
"Tools/Show Retinue memory contents" --search "deployment"            # semantic search
"Tools/Show Retinue memory contents" --home ~/.hermes/profiles/coder  # other Hermes home
"Tools/Show Retinue memory contents" --json                           # machine-readable output

It resolves the database from --db, --home, $HERMES_HOME, or by auto-detecting a single profile database under ~/.hermes/profiles/. Plain listing never loads the embedding model, so it works offline and starts instantly; only --search loads it (~540 MB into .hf-cache on first use). The implementation lives in src/retinue/cli.py and is not deployed by the installer — it is a development-only tool.

Created: 2026-07-18 la 15:26

Validate