docs: Document RAG storage drivers, Qdrant attach, and sandbox wiring

Covers the pluggable RAG driver work: the yaml/duckdb/qdrant drivers and how
to choose between them, attaching a read-only remote Qdrant collection with
.rag attach, the driver field on graph rag nodes, and how an attached RAG's
host and API key reach a sandbox.

Also corrects two statements the driver work invalidated: embeddings are no
longer necessarily in-memory, and .rebuild rag now re-embeds the whole corpus
rather than just accommodating document changes.
2026-08-11 21:12:20 -06:00
parent 7314039883
commit 6ad2c80680
5 changed files with 125 additions and 2 deletions
+8
@@ -585,6 +585,14 @@ base is first built):
(default: `1`). `0` returns only documents directly linked to matched entities with no traversal.
Falls back to `rag_graph_hops`. Higher values surface more loosely related documents; keep at `1` for most corpora.
- **`batch_size`:** Embedding-request batch size.
- **`driver`:** [Storage driver](RAG#storage-drivers) for this node's knowledge base — `yaml`
(default) or `duckdb`. Use `duckdb` when the node's corpus is large enough that rebuilding
the in-memory index on every agent start is noticeable; it keeps vectors and text on disk in
a `<rag-node-id>.duckdb` file beside the generated knowledge base. An invalid driver is
rejected when the graph is validated, naming the offending node. Like every field in this
section it only applies when the knowledge base is *first* built — changing it later has no
effect until the knowledge base is deleted and rebuilt. The `qdrant` driver is not available
here: attaching a remote collection is an interactive flow (`.rag attach`).
Each falls back to the app-level `rag_*` config when omitted. **When
`embedding_model`, `chunk_size`, and `chunk_overlap` are all set, the
+96 -1
@@ -69,9 +69,15 @@ When you define RAG, Coyote will first "build" the RAG. This means that Coyote w
generate [embeddings](https://huggingface.co/spaces/hesamation/primer-llm-embedding) for that text. This essentially just means that Coyote translates the document into a language
the LLM can understand.
These embeddings are stored in an in-memory vector database. Coyote also indexes every document chunk in a full-text
These embeddings are stored in a vector database — in memory, on disk, or in a remote Qdrant collection, depending on
the RAG's [storage driver](#storage-drivers). Coyote also indexes every document chunk in a full-text
search index (BM25) for keyword-based retrieval.
Building costs real time and real money: every chunk is sent to an embedding model, and every chunk is sent again to a
chat model if graph RAG is enabled. Adding or editing documents with `.edit rag-docs` only processes what actually
changed, but `.rebuild rag` deliberately re-embeds the entire corpus from scratch — reach for it when a document's
contents changed underneath Coyote or the knowledge base looks wrong, not as routine maintenance.
If an [extractor model is configured](#graph-based-rag), Coyote additionally runs an LLM-based entity and relationship
extraction pass over each document chunk and builds a **knowledge graph** of named entities and their connections. This
graph is saved alongside the vector and BM25 indexes and enables a third, graph-based retrieval signal at query time.
@@ -114,6 +120,95 @@ below for more details on how to customize this.
Finally, the text snippets that were looked up in RAG are passed to the model as additional context to your prompt,
giving the model query-specific context to answer your question.
# Storage Drivers
Every RAG picks a **storage driver** when it's created. The driver decides where your vectors and document text
actually live: inside the RAG's own file, in a local database beside it, or in a Qdrant server you already run.
| Driver | Where the vectors live | Reach for it when |
|--------|------------------------|-------------------|
| `yaml` *(default)* | In the RAG's own YAML file, loaded into memory on use | You want the simplest thing that works, or several Coyote processes need the same RAG at once |
| `duckdb` | A `<name>.duckdb` file beside the RAG | The corpus is large enough that re-loading it into memory on every start is annoying |
| `qdrant` | A collection on a Qdrant server you already run | Something else already built and maintains the collection |
Coyote asks which one you want while creating a RAG with `.rag`:
```text
? RAG storage driver:
> yaml — portable, in-memory HNSW; usable from several Coyote processes at once (default)
duckdb — persistent on-disk store; vectors and content survive restarts; HNSW approximate search.
```
If you don't care, take the default. `yaml` is what Coyote has always done.
> **A RAG's driver is fixed when it's created.** There is no conversion step — switching means deleting the RAG and
> building it again, which re-embeds every document. Worth a moment's thought before you pick, but not worth agonizing
> over: for small corpora the difference is not something you'll notice.
## yaml
The default. Vectors and document text are stored in the RAG's YAML file and read into memory when the RAG is loaded,
where the HNSW index is built on the fly.
Its useful property is that nothing holds a lock: as many Coyote processes as you like can use the same RAG
simultaneously. Its cost is startup — a large corpus means a large file to parse every time the RAG is loaded.
## duckdb
Vectors, document text and the search indexes live in a `<name>.duckdb` file next to the RAG, and stay there between
runs. Nothing is rebuilt at startup, so loading a large RAG is quick regardless of size.
Two things are worth knowing before you choose it:
* **Concurrency is readers-or-writer.** Several Coyote processes can query the same duckdb RAG at once. While one
process is *ingesting* or *rebuilding* it, though, the others can't read it until that finishes. If you routinely
run several Coyotes against one RAG and rebuild it often, `yaml` will annoy you less.
* **First use needs network access.** DuckDB's vector (`vss`) and full-text (`fts`) extensions are downloaded the
first time a duckdb RAG is created, then cached and reused. On a machine that's offline from the start, create one
duckdb RAG while connected and every later one works offline. Pre-installing them from a DuckDB shell is fiddlier
than it sounds — the cache is keyed by DuckDB version, and Coyote bundles its own — so connecting once is much the
easier route.
Deleting the RAG removes the `.duckdb` file and its write-ahead log along with it.
## qdrant — attaching an existing collection
The `qdrant` driver connects a RAG to a collection on a Qdrant server you already run. It is **read-only**: Coyote
queries the collection and never writes to it, so whatever populated it stays the sole owner of its contents. This is
the driver to use when another system — a LangChain pipeline, an ingestion job, a colleague's tooling — already
maintains a collection you'd like to ask questions about.
Because the collection already exists, you don't create this kind of RAG with `.rag`; you *attach* it:
```shell
.rag attach my-docs
```
Coyote then walks you through it:
1. **Host** — for example `qdrant.company.com:6333`.
2. **API key** — Coyote asks whether the instance needs one, then which [Vault](Vault) secret holds it (defaulting to
`QDRANT_API_KEY`). If that secret doesn't exist yet you can create it right there, without leaving the wizard. The
key itself is never written into the RAG file; only the secret's name is.
3. **Collection** — Coyote connects, lists what's available, and lets you pick.
It then checks the collection is actually usable and tells you what it found — the vector dimension, and a warning if
the collection is empty (you can attach anyway, but every query will return nothing until something writes to it).
A collection has to meet two requirements:
* **A single unnamed vector.** Named or multi-vector collections are rejected, because Coyote queries with one unnamed
vector and Qdrant would reject every request.
* **Document text in a `page_content` payload field.** This is the field Coyote reads answers out of. Collections
built by LangChain use this name by default.
Point IDs may be integers or UUIDs; both work.
> **Use the same embedding model that built the collection.** Coyote can't detect a mismatch — the dimensions may line
> up perfectly while the vectors mean entirely different things. The symptom is retrieval that returns confident
> nonsense, so it's worth checking rather than discovering later.
### Attached RAGs inside a sandbox
If you run Coyote in a [sandbox](Sandboxes), an attached RAG's host is allow-listed and its API key is handed to the
sbx proxy automatically, so queries work inside the sandbox exactly as they do outside. See
[Sandboxes > Credentials & Secrets](Sandboxes#credentials--secrets).
# Supported Document Sources
Coyote supports a number of document sources that can be used for RAG:
+2 -1
@@ -148,8 +148,9 @@ complete tasks using the documents as additional context.
| Command | Description |
|------------------|------------------------------------------------------------------------------|
| `.rag` | Initialize or access a RAG |
| `.rag attach <name>` | Attach a RAG to an existing remote [Qdrant](RAG#qdrant--attaching-an-existing-collection) collection (read-only) |
| `.edit rag-docs` | Add or remove documents from the active RAG using your preferred text editor |
| `.rebuild rag` | Rebuild the active RAG to accommodate document changes |
| `.rebuild rag` | Rebuild the active RAG from scratch, re-embedding every document |
| `.sources rag` | Show a works-cited of the sources used in the last query |
| `.info rag` | Display information about the active RAG |
| `.exit rag` | Exit the active RAG |
+16
@@ -180,6 +180,22 @@ Secrets already registered with `sbx` are silently skipped to keep re-attaches f
If your LLM client uses **OAuth** instead of an API key (`auth: oauth` in your config), no secret is injected. The
sbx proxy handles OAuth natively without a stored key.
### Attached RAG collections
A RAG [attached to a remote Qdrant collection](RAG#qdrant--attaching-an-existing-collection) needs two things to work
inside a sandbox, and Coyote arranges both without being asked: the collection's host has to be reachable through the
network policy, and the API key has to reach the server without ever entering the VM.
When you attach a RAG, Coyote writes a small mixin next to it declaring the host as an allowed domain and the API key
as a proxy-managed credential. On launch that mixin is discovered alongside the generated `coyote-mcp` one, the key is
registered with the sbx secret store on your host, and the proxy writes it into the outgoing request header at the
network edge — the same treatment LLM provider keys get. Queries then behave identically inside and outside the
sandbox.
This is also why a RAG's `api_key` must be a `{{SECRET_NAME}}` placeholder rather than the key itself: Coyote reads
that placeholder back out to learn which vault secret to hand to sbx. A literal key can't be provisioned, so Coyote
refuses to load a RAG configured that way and tells you which secret to create.
### The generated `coyote-mcp` mixin
Alongside secret registration, every non-`--fresh` launch renders a mixin kit named `coyote-mcp` and
+3
@@ -57,6 +57,9 @@
- [File Discovery](Workspace-Instructions#file-discovery)
- [Configuration](Workspace-Instructions#configuration)
- [RAG](RAG)
- [Storage Drivers](RAG#storage-drivers)
- [Attaching a Qdrant Collection](RAG#qdrant--attaching-an-existing-collection)
- [Graph-Based RAG](RAG#graph-based-rag)
- [Macros](Macros)
- [Roles](Roles)
- [Skills](Skills)