How to use semantic recall & memory¶
Search a scope semantically and give an agent durable memory — offline, no
server. This is the task recipe; the model behind it is in
Search & memory, and every flag is in the
dna recall /
dna search /
dna memory reference.
All outputs below are real runs against
examples/hello-genome
and this repo's own scope.
1. Install the extra¶
Semantic search is an opt-in extra — the core SDK never drags vector or ML dependencies:
pip install "dna-sdk[search-sqlite]" # sqlite-vec + FTS5 + RRF (the embeddable default)
pip install "dna-sdk[embed-onnx]" # optional: real ONNX embeddings (all-MiniLM-L6-v2)
pip install "dna-sdk[search-pgvector]" # optional: Postgres + pgvector, for scale
Working from the repo (pre-1.0)
The packages are not on PyPI yet. From a clone, the dev extra already
includes search-sqlite:
cd packages/sdk-py && uv venv && uv pip install -e ".[dev]" -e ../cli.
2. Search a scope: dna recall¶
dna recall registers the sqlite-vec provider, indexes the scope's records
on demand (idempotent — re-runs skip unchanged docs by text hash), and runs a
hybrid dense + lexical + RRF search:
$ dna recall "friendly assistant" --scope hello-genome --kind Agent -k 3
🔎 hybrid (dense+lexical+RRF) · scope=hello-genome · 'friendly assistant'
1. Agent/greeter (0.0328)
greeter You are Helio, a friendly assistant. Greet people warmly and answer…
dna search is the same command under a neutral name. Useful knobs:
--kind (repeatable) restricts kinds, --tenant searches base ∪ overlay
(overlay shadows base), --json emits machine-readable hits. The index
lives in a .dna-search/ directory beside your .dna/ (override with
DNA_SEARCH_DIR).
What one search does end to end:
sequenceDiagram
participant CLI as dna recall
participant K as kernel.search
participant P as sqlite-vec provider
CLI->>P: index scope records (hash-skip)
CLI->>K: search(scope, query)
K->>P: dense KNN + FTS5 BM25
P->>P: RRF fusion
P-->>K: ranked hits
K-->>CLI: hits · degraded: false
Without the extra: honest degradation¶
Search is a read — it never raises on a missing dependency. Without
search-sqlite you get the kernel's lexical token scan, clearly labeled:
$ dna recall "friendly assistant" --scope hello-genome --kind Agent -k 3
⚠ search-sqlite extra not installed — degrading to lexical scan (pip install 'dna-sdk[search-sqlite]' for semantic recall)
🔎 lexical (degraded) · scope=hello-genome · 'friendly assistant'
1. Agent/greeter (0.5000)
Same query, same top hit here — but the lexical scan only matches tokens, so paraphrased queries ("how do I welcome users") will miss what hybrid search finds.
3. Give an agent memory: dna memory¶
Memory is the record Kinds you already have (Engram, Research,
Evidence) plus four verbs. Write one:
$ dna memory remember "Always deep-copy a doc's spec before mutating — the cache hands back a shared reference" \
--scope hello-genome --area Feature/kernel --affect regret \
--reason "Mutating the cached dict in place corrupted every later read in the same process" \
--tag cache
🧠 remembered Engram/rem-5d60593f38
Always deep-copy a doc's spec before mutating — the cache hands back a shared reference
That wrote a plain Engram YAML into the scope (deterministically
enriched: encoding context, memory type, valid_from) and indexed it.
Recall it — hits are re-ranked by search score × Ebbinghaus retention ×
affect, and each surfaced memory gets its cue recorded:
$ dna memory recall "cache mutation" --scope hello-genome
🧠 recall · hybrid (dense+lexical+RRF) · scope=hello-genome · 'cache mutation'
1. Engram/rem-5d60593f38 (0.0426) [retention 1.00]
rem-5d60593f38 Always deep-copy a doc's spec before mutating — the cache hands back…
Recall by paraphrase: the semantic blend¶
Recall has a second, additive ranking plane. When the provider is available,
dna memory recall also embeds the cue and each candidate's semantic
payload (area/title/summary/body — the same fields the ecphory
scoring reads; names, dates and affect labels never dilute the similarity),
feeds the cosine into the deterministic ecphory ranking, and fuses the two
rankings with the same RRF the search plane uses. A memory phrased
differently from your cue — no shared phrase, no token-subset match — can
still surface, and the hits say why:
$ dna memory recall "mutating instances safely" --scope hello-genome -k 2
🧠 recall · hybrid (dense+lexical+RRF) + semantic (ecphory×cosine) · scope=hello-genome · 'mutating instances safely'
1. Engram/rem-5d60593f38 (0.0328) [retention 1.00] [cos 0.44]
rem-5d60593f38 Always deep-copy a doc's spec before mutating — the cache hands back…
--semantic/--no-semantic controls the blend; the default is auto — on
exactly when the provider is available, so without the extra the behavior
(and every score) is unchanged. In JSON output, fused hits carry
rank_recall / rank_ecphory / score_recall / semantic (the cosine) so
you can see both rankings.
Memories written before the provider existed (or on a machine without the
local .dna-search/ store) are not a migration problem: every recall
lazy-backfills the index first (dna.memory.backfill_index — idempotent by
text hash, unchanged docs are never re-embedded).
Forgetting is bi-temporal demotion — the instance stays, auditable, but stops surfacing:
$ dna memory forget rem-5d60593f38 --scope hello-genome
🕯 forgotten: Engram/rem-5d60593f38 (valid_to=2026-07-09T20:59:11+00:00)
(retained + auditable — bi-temporal invalidation, not deleted)
$ dna memory recall "cache mutation" --scope hello-genome
🧠 recall · hybrid (dense+lexical+RRF) · scope=hello-genome · 'cache mutation'
(no memories)
$ dna memory list --scope hello-genome --all
name state affect area summary
-------------- --------- ------ -------------- ------------------------------------------------------------
rem-5d60593f38 forgotten regret Feature/kernel Always deep-copy a doc's spec before mutating — the cache ha
And consolidate is the deterministic maintenance pass — recompute
retention, report stale memories (soft-forget them with --apply):
4. Register providers programmatically¶
The CLI (and the MCP server — see §5) wire the sqlite-vec provider and,
when the embed-onnx extra is installed, the local ONNX embedder for you — so
dna recall is genuinely semantic offline the moment both extras are present.
In your own code you register them on the kernel once, at boot. This script is
runnable as-is next to a .dna/ directory:
import asyncio
from dna import Kernel
from dna.adapters.filesystem.writable import FilesystemWritableSource
from dna.adapters.search.sqlite_vec import (
SqliteVecRecordSearchProvider,
document_text,
)
SCOPE = "hello-genome"
async def main() -> None:
kernel = Kernel.auto(source=FilesystemWritableSource(".dna"))
# 1. Register the provider (one per kernel; boot-time wiring).
provider = SqliteVecRecordSearchProvider(kernel, db_dir=".dna-search")
kernel.record_search_provider(provider)
# 2. Index the records you want searchable (idempotent by text hash).
records = []
async for raw in kernel.query(SCOPE, "Agent"):
name = raw["metadata"]["name"]
records.append({
"scope": SCOPE, "kind": "Agent", "name": name,
"tenant": "", "text": document_text(raw), "title": name,
})
await provider.index(records)
# 3. Search — hybrid now, honest lexical fallback if the provider errors.
res = await kernel.search(SCOPE, "friendly assistant", k=3)
print("degraded:", res["degraded"])
for hit in res["hits"]:
print(f" {hit['kind']}/{hit['name']} {hit['score']:.4f}")
asyncio.run(main())
The memory verbs are the same surface one level up —
from dna.memory import remember, recall, forget, consolidate — each an
async function taking (kernel, scope, ...).
Embeddings: the floor and the real thing¶
With no embedding provider registered, kernel.embed() uses the
deterministic hash-based fake — zero dependencies, bit-identical between
Python and TypeScript, honest about not being semantic:
kernel = Kernel.auto()
print("model:", kernel.embedding_model_id, "| dims:", kernel.embedding_dims)
[vec] = await kernel.embed(["reciprocal rank fusion"])
print("non-zero dims:", sum(1 for v in vec if v))
For real semantic similarity, install the embed-onnx extra and register the
ONNX provider — same 384 dims, so the swap changes nothing downstream. The
model artifact is lazy-downloaded and cached on the first embed() call
(never at install or import time):
from dna.adapters.embedding.onnx import OnnxEmbeddingProvider
kernel.embedding_provider(OnnxEmbeddingProvider()) # all-MiniLM-L6-v2
Rebuild the index after swapping providers: vectors from different
model_ids are not comparable, so the store pins its embedding space and
refuses to open under a different (model_id, dims) rather than mix
them silently. Delete the .dna-search/ store and re-index.
Scaling up: pgvector¶
When one file per scope stops being enough, the search-pgvector extra
provides PgVecRecordSearchProvider — same port, same RRF, same conformance
suite, backed by the Postgres you already run for the source plane:
from dna.adapters.search.pgvector import PgVecRecordSearchProvider
provider = PgVecRecordSearchProvider(kernel, dsn="postgresql://dna@localhost/dna")
kernel.record_search_provider(provider)
Nothing else in your code changes — that is the point of the port.
Prove your stack: the memory conformance kit¶
Whatever combination you assemble — custom source, custom provider, custom
embedder — the SDK ships a public battery that certifies the memory verbs
over it: memory_conformance_suite (the verb lifecycle, capability-aware)
and memory_scoring_conformance_suite / memoryScoringConformanceSuite
(the pure scoring core, Py↔TS twinned). See Running the conformance
kit.
5. Enable local semantic recall in the MCP server¶
The MCP server exposes memory as the remember / recall tools (and reads
through the same kernel the dna CLI does). Its boot path registers the search
provider and the embedder from the same choke point as the CLI, so enabling
offline semantic recall for it is purely a matter of installing the two local
extras into the environment the server runs from — no code change, no external
API, no network at query time.
# into the venv the `dna` binary resolves from (`which dna`):
pip install "dna-sdk[search-sqlite]" # sqlite-vec: the vector + FTS5 + RRF search plane
pip install "dna-sdk[embed-onnx]" # fastembed/onnxruntime: real local embeddings
# from a clone: uv pip install 'sqlite-vec>=0.1.6' 'fastembed>=0.3'
That is the whole enable step. With both extras present the server, on boot,
registers SqliteVecRecordSearchProvider (so recall is provider-backed, not
the lexical fallback) and OnnxEmbeddingProvider (all-MiniLM-L6-v2, so the
dense plane is real paraphrase similarity instead of the deterministic
fake-hash floor). The ONNX model artifact is fetched and cached on the first
embed (the Chroma pattern) — never an external API on the query path.
Verify it end to end¶
Boot a server against a scope and drive it with any MCP client:
Before (neither extra installed — the honest degraded floor):
remember → { "kind": "Engram", "name": "rem-…", "indexed": false }
recall → { "semantic": false, "degraded": true, "hits": [ … ] }
indexed:false because no search provider is registered; recall degrades to
the kernel's lexical token scan (degraded:true), and the semantic plane is
off (semantic:false).
After (both extras installed):
remember → { "kind": "Engram", "name": "rem-…", "indexed": true }
recall → { "semantic": true, "degraded": false, "hits": [ … ] }
A paraphrased cue that shares no tokens with the stored memory now surfaces it
as the top hit — e.g. recalling "how does foliage make food from solar
radiation" returns a memory whose summary is "Plants convert sunlight into
chemical energy stored in glucose" (semantic:true, degraded:false), which
the fake-hash floor cannot do (its cosine for that pair is ~0).
Why both extras
search-sqlite alone already flips indexed/semantic to true — but the
dense plane then runs on the fake-hash floor, which is orthogonal for a
paraphrase (cosine ≈ 0), so semantic:true would be lexical-in-disguise.
Adding embed-onnx makes the dense plane genuinely semantic. The server
registers the ONNX embedder automatically when the extra is present; it
never clobbers an embedder you wired explicitly (via config or code).
From another language¶
Recall is reachable over the REST and MCP
faces — the recall
MCP tool and the /v1/* read routes serve the same fused results this guide
describes, with no second implementation to keep in step. See the
Python API reference for the in-process
surface.