Memory

std/memory provides durable observations that can be recalled across later runs without treating transcript history as long-term knowledge.

import "std/memory"

memory_store("workspace/acme", "alice-profile", {
  text: "Alice prefers Rust examples and concise plans",
}, ["profile", "preference"])

let related = memory_recall("workspace/acme", "rust preference", 3)
let summary = memory_summarize("workspace/acme", {limit: 10})

API

FunctionReturnsDescription
memory_open(namespace, options?)memory_openSelect the recall backend (bm25, vector, or hybrid) for this namespace
memory_store(namespace, key, value, tags?, options?)memory_recordAppend an observation
memory_recall(namespace, query, k?, options?)list<memory_record>Recall active records ranked by the namespace backend (override per-call with options.mode)
memory_summarize(namespace, window?, options?)memory_summaryBuild an extractive summary over recent or query-filtered records
memory_forget(namespace, predicate, options?)dictAppend a tombstone for matching records

Typed fact helpers live in std/agent/fact and store harn.fact.v1 envelopes on top of this same memory log.

Storage

The VM-native backend stores append-only JSONL events at .harn/memory/<namespace>/events.jsonl by default. Pass {root: "path"} in the options argument to use a different root. Namespaces are relative path segments; absolute paths and .. escapes are rejected.

Records contain:

{
  "_type": "memory_record",
  "id": "uuid-v7",
  "namespace": "workspace/acme",
  "key": "alice-profile",
  "value": {"text": "Alice prefers Rust examples"},
  "text": "{\"text\":\"Alice prefers Rust examples\"}",
  "tags": ["profile"],
  "stored_at": "2026-04-29T00:00:00Z",
  "provenance": null
}

memory_store accepts options.id, options.now, and options.provenance. These are useful for tests, imports, and replay fixtures.

Recall and summary

memory_recall defaults to deterministic, local BM25. It tokenizes the record key, tags, text, and JSON value, then ranks active records with BM25 plus small exact key/tag boosts.

Vector and hybrid recall are available via memory_open. When the active backend uses embeddings, recall calls the host’s memory.embed capability (see Host boundary) and caches the result on disk so subsequent recalls on the same (namespace, query, mode, model_hint, top_k) are deterministic.

memory_summarize returns {_type, namespace, count, text, records}. window may be nil, an integer limit, or a dict with limit, query, and tag or tags. The summary text is an extractive bullet list capped to a bounded size. Callers that need model-written prose can pass summary.records to llm_call.

Forgetting

memory_forget is soft-delete. It appends a tombstone event and leaves prior observations in the log for auditability.

Predicates may be a string substring match, or a dict with any combination of id, key, tag / tags, and query. Dict predicates are conjunctive: all provided fields must match.

Typed facts

std/agent/fact provides typed assertions over std/memory for agents that need durable, queryable claims rather than freeform observations:

import { recall_facts, store_fact } from "std/agent/fact"

store_fact({
  kind: "claim",
  claim: "Alice prefers Rust examples",
  confidence: 0.82,
  evidence: [{kind: "file_range", ref: "README.md:1-3"}],
  provenance: {agent: "codex", run_id: "run-1"},
})

let facts = recall_facts("Rust examples", "claim", 0.8)

Facts normalize to harn.fact.v1 with kind, claim, evidence, confidence, provenance, optional valid_until, and asserted_at. store_fact writes the fact as MemoryRecord.value, sets the memory record id to the fact id, and uses the reserved key shape fact:<kind>:<id> with fact, fact:<kind>, schema:harn.fact.v1, and evidence tags. The default namespace is project/facts; pass options.namespace, options.scope, or normal memory options such as root to control placement.

recall_facts(query, kind?, min_confidence?, scope?) returns normalized facts augmented with score, memory_record_id, memory_key, memory_namespace, and stored_at. invalidate_facts(predicate, scope?) appends memory tombstones; predicates accept an exact fact_... id string or a dict with id, key, kind, claim, query, tag, tags, evidence_ref, or evidence. Evidence predicates match canonical evidence tags such as fact:evidence:file_range:README.md:1-3; when kind is supplied, they match kind-scoped tags such as fact:claim:evidence:file_range:README.md:1-3. Validation failures include HARN-FACT-NNN codes.

Probe-first verification

std/agent/probe layers a probe primitive on top of std/agent/fact: run a small snippet, capture the outcome deterministically, and auto-record it as an Observation so future sessions recall the verified answer instead of re-guessing.

import { probe_eval, probe_typecheck } from "std/agent/probe"

let helper = probe_eval("git diff --quiet HEAD -- crates/harn-stdlib", {expected: 0})
let tc = probe_typecheck(
  "pipeline summary() { let x: int = len([1, 2, 3]) __io_println(x) }\n",
  {expected: 0},
)

Every probe returns a harn.probe.v1 envelope (kind, outcome, observed, evidence, fact_id) and, unless options.store_fact = false, writes a harn.fact.v1 Observation with provenance.source = "probe" and provenance.probe_kind = "<kind>". Recall those observations with recall_facts(query, "Observation", 0.0, scope) to surface prior probe outcomes before re-running. See std/agent/probe in the language spec for the full surface area and HARN-PROBE-NNN diagnostics.

Vector and hybrid backends

memory_open(namespace, options) writes an append-only configuration event that selects the recall backend:

import "std/memory"

memory_open("workspace/acme", {
  backend: "hybrid",          // "bm25" (default), "vector", or "hybrid"
  embed_model_hint: "voyage-2",
  embed_dim: 1024,
  bm25_weight: 0.4,           // hybrid only
  cosine_weight: 0.6,         // hybrid only
})

The latest open event wins, so re-opening a namespace re-keys recall without rewriting prior records. memory_recall accepts a per-call options.mode (lexical | semantic | hybrid) that overrides the namespace default for that query only.

When a namespace uses vector or hybrid, memory_store eagerly embeds the record’s searchable text so subsequent semantic recall hits the cache. Callers can also pass options.embed: true on memory_store to embed against an otherwise lexical namespace, or options.skip_embed: true to suppress eager embedding for one call.

Embeddings come from the host via the typed memory.embed capability:

RequestResponse
{text: string, model_hint: string}{vector: list<float>, model?: string, dim?: int}

Harn never bundles an embedding model. Hosts choose the model, handle rate limiting, and decide cost accounting. For tests, register the capability via host_mock("memory", "embed", {result: {vector: [...], dim: N, model: "..."}}).

Embeddings are cached on disk at .harn/memory/<namespace>/vectors/<sanitized_model_hint>/<sha256(text)>.json. The cache key is (model_hint, content_hash), so swapping models invalidates the cache without rewriting any records, and identical inputs always reuse the same bytes.

Replay

Memory is separate from transcript history. Runs that recall memory should persist the recalled records in their run record before deterministic replay.

For vector and hybrid backends, the event log and the on-disk embedding cache are the run record from memory’s perspective: as long as both survive into the replay environment, recall returns the same ordered hits without re-invoking the host. Embedding host calls are also recorded into the host-call mock log so test fixtures can audit which texts were embedded under which model hint.