Development - Retinue
Table of Contents
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:
- Detects the active Hermes home from the
HERMES_HOMEenvironment variable, or falls back to$HOME/.hermes. - Creates
$HERMES_HOME/plugins/retinue/and its.hf-cache/subdirectory if they do not exist. - Removes every previously deployed item in the plugin directory except the
PRESERVElist (.venv,.hf-cache), so stale code cannot linger. - Copies the whitelist
(plugin.yaml __init__.py src)from the workspace into the plugin directory withcp. - Resolves the Python interpreter that Hermes actually runs under (via the
PYTHONenvironment variable, common venv layouts, or thehermeslauncher shebang) and installs any missingsqlite-vec/model2vecdependencies into it withpip(falling back to--break-system-packagesinside that interpreter on PEP 668 systems). - Pre-downloads the embedding model into
.hf-cache/by callingretinue.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. - Sets
memory.provider: retinuewithhermes config setif the CLI is available. - 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.2. retinue_memory_search
Semantic search across all memories.
{
"name": "retinue_memory_search",
"description": "Search Retinue memories by semantic similarity.",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Natural-language search query"},
"limit": {"type": "integer", "default": 10}
},
"required": ["query"]
}
}
Read more: How memory retrieval 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
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
memorytool. Hermes then calls Retinue'son_memory_writehook, 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.- Hermes routes it to
handle_tool_call("retinue_memory_add", args). - Text is embedded with model2vec into a float32 vector packed as bytes.
On the first call in a process,
memory.pyresolves the model snapshot withsnapshot_download(MODEL, ignore_patterns["onnx/*"])= (skipping the repo's unused ~512 MB ONNX export) and loadsStaticModelfrom the returned local path — loading from a local path is what prevents re-fetching the skipped files on every load; Resulting vector with original text is written into database
INSERT INTO memories (embedding, content, created)
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:
- The LLM is notified the tool exists. At session start Hermes calls
get_tool_schemas()and places all fourretinue_memory_*schemas into the LLM's tool list next to the built-in tools. Theretinue_memory_searchschema declares two parameters:query(required string) andlimit(optional integer, default 50). Separately, the text ofsrc/retinue/system_prompt_block.txtis injected into the system prompt, instructing the LLM to runretinue_memory_searchbefore acting on a task. - The LLM invokes it with a natural-language query. Example call:
retinue_memory_search({"query": "coworker", "limit": 2}). - 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 asqlite-vecKNN query:WHERE embedding MATCH ? AND k = <limit>against thememoriesvirtual table. - Result count is limited by count only. The
kparameter is a hard top-N cap: sqlite-vec returns at mostlimitnearest rows, with no similarity threshold. Even a nonsense query ("zzzzqqq nonexistent") returns exactlylimitrows. Filtering weak matches is left to the LLM reading the scores. - 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). The tool returns a JSON string — the result of
json.dumps({"success": True, "result": [...]}). Each element has four fields:id(the rowid, forretinue_memory_delete),content(full stored text),created(ISO 8601 UTC timestamp), andsimilarity. Thesimilarityis computed inprovider.pyfrom the raw sqlite-vec distance as1 / (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.)
- 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
- Edit source files in the Retinue workspace.
Run the installer to copy the latest code into Hermes:
cd /path/to/retinue bash "Install Retinue"
Verify the plugin loads:
hermes memory status
Test through a Hermes session:
hermes chat -q "Remember that my default shell is fish." hermes chat -q "What shell do I use?"
- 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.