The first memory project kept the information needed for the next model call. That memory was temporary and constrained by tokens.

But some facts should survive the conversation entirely.

If a project uses Python 3.12, a user prefers a dark theme, or a deployment belongs in ap-south-1, the agent should not rediscover that value through chat history or semantic search. The application already knows the address of the fact.

That is the job of key-value memory.

First: What Is Key-Value Memory?

Key-value memory answers a precise question with a precise address:

namespace + entity + key
            ↓
       exact lookup
            ↓
          value

For example:

(project, memory-series, python_version)
                    ↓
                  "3.12"

There is no ranking step. The result is either the stored value or no value.

Known address  → KV memory
Known meaning  → vector memory
Current context → working memory

Why a Database Is Better Than More Prompt

A durable preference does not need to occupy every turn of a conversation.

Chat transcript grows
        ↓
old preference may be compacted or omitted

SQLite row persists
        ↓
application retrieves it only when needed

The project separates conversation from durable state. /set, /get, /list, /delete, /clear, /cleanup, and /stats operate locally without calling a model.

That boundary matters: storage continues to work when the API is unavailable, and the model is not required for deterministic CRUD.

The Address Is the Contract

Every value is identified by three fields:

@dataclass(frozen=True, slots=True)
class MemoryKey:
    namespace: str
    entity_id: str
    key: str

The same key name can safely exist in different scopes:

(user,    123, language) → "Java"
(project, 123, language) → "Python"
(user,    456, language) → "Go"

SQLite enforces uniqueness across the full logical key:

UNIQUE(namespace, entity_id, memory_key)

The implementation passes each component as a SQL parameter. It does not build a compound SQL expression from user strings.

Values Stay Typed

The value is canonical JSON plus an explicit type:

class ValueType(str, Enum):
    STRING = "string"
    NUMBER = "number"
    BOOLEAN = "boolean"
    LIST = "list"
    OBJECT = "object"
    NULL = "null"

This lets the same store preserve strings, numbers, booleans, lists, objects, and null.

"3.12"                    → string
3.12                      → number
true                      → boolean
["pytest", "ruff"]       → list
{"theme":"dark"}         → object
null                      → null

The distinction is easy to miss in a CLI. 3.12 is valid JSON and becomes a number; "3.12" remains a version string.

Set Means Create or Replace

set() behaves as an upsert.

INSERT INTO kv_memories (...)
VALUES (?, ?, ?, ...)
ON CONFLICT(namespace, entity_id, memory_key) DO UPDATE SET
    value_json = excluded.value_json,
    value_type = excluded.value_type,
    updated_at = excluded.updated_at,
    expires_at = excluded.expires_at,
    metadata_json = excluded.metadata_json

An update preserves the row ID and created_at, while changing updated_at and the stored value.

first set
id=A  created=09:00  updated=09:00  value=3.11

second set
id=A  created=09:00  updated=09:00:05  value=3.12

This makes one logical fact evolve without creating duplicate rows.

TTL Makes Temporary Facts Disappear

Some exact facts are durable only for a limited time. set() accepts a positive TTL:

memory.set(
    "deployment",
    "release-42",
    "region",
    "ap-south-1",
    ttl_seconds=60,
)

Once expires_at is reached, the entry behaves as missing immediately.

before expiry → get() returns the value
after expiry  → get() returns None
cleanup later → expired row is physically deleted

Logical expiration and physical cleanup are separate. Reads stay correct even if the maintenance job has not run yet.

What Actually Happened?

I ran the real KVMemoryManager against a temporary SQLite file with a deterministic clock.

The trace created two project facts, updated one of them, expired a deployment value, cleaned it up, and reopened the database:

created: 3.11 string
updated: 3.12 same_id= True created_at_preserved= True
project keys: [('python_version', '3.12'), ('test_framework', 'pytest')]
expired lookup: None
cleanup removed: 1
stats: MemoryStats(active_entries=2, expired_entries=0,
                   namespaces=1, entities=1)
after restart: 3.12

The last line is the important one. The process ended, a new manager opened the same file, and the exact value was still available.

No transcript and no model call were required.

Two Ways the Agent Can Use It

The project demonstrates application-managed and tool-managed access.

Application-Managed Retrieval

The caller already knows which keys are relevant:

question
   ↓
application selects exact keys
   ↓
KV lookup
   ↓
only found values enter model input

If the application asks for python_version and unknown, the model receives only the existing python_version. Unrequested test_framework data stays out of the prompt.

This is the cleanest path when a product screen or workflow already knows its required configuration fields.

Tool-Managed Retrieval

The alternate loop exposes four validated tools:

get_memory
set_memory
delete_memory
list_memories

The first model turn must call a KV tool. Later turns return to automatic tool selection so the model can answer after observing the result.

The model never receives raw SQL. Every request still passes through identifier, JSON, TTL, and scope validation.

The Write Policy Is Deliberately Narrow

The tool instructions allow explicit durable facts such as preferences and configuration.

They reject:

  • greetings
  • raw logs
  • temporary conversational details
  • arbitrary chat history
  • hidden model reasoning

Persistence is a product decision. Automatically storing every user message would turn a deterministic fact store into an unbounded transcript with unclear consent and retention rules.

Validation Is Part of Memory Design

The manager rejects empty or oversized identifiers, control characters, invalid TTLs, unsupported values, non-finite numbers, and non-JSON metadata.

model or user input
        ↓
normalization + validation
        ↓
parameterized storage boundary
        ↓
SQLite

These checks are not decoration around memory. They define what the application is willing to remember.

What the Tests Prove

The project has 27 offline tests. I ran the full suite:

27 passed in 0.90s

They cover persistence, replacement semantics, timestamps, deletion, exact lookup, namespace isolation, TTL behavior, cleanup, JSON round trips, duplicate logical keys, SQL-injection-like strings as data, scoped clearing, application-managed injection, and tool-managed writes.

They also verify that a tool-managed agent cannot answer before performing its required lookup.

Where KV Memory Stops

KV memory works only when the caller knows the address.

"What Python version belongs to memory-series?"
→ project / memory-series / python_version

"Have we seen a similar checkout failure?"
→ no exact key is obvious

The second question needs semantic retrieval. Adding embeddings to the KV path would make a deterministic lookup slower and less exact. That belongs in the next memory type.

What I Took Away

Key-value memory is the simplest durable memory because its uncertainty lives outside retrieval.

The application chooses the address.
The database returns the exact value.

Its engineering questions are still substantial: ownership, scope, type, overwrite semantics, TTL, authorization, deletion, and audit.

The useful mental model is not “the agent remembers.” It is “the application maintains a typed fact at a stable address.”

Source Code

The complete project includes SQLite persistence, typed JSON values, TTL cleanup, validated Responses API tools, application-managed context injection, a Rich terminal UI, and offline tests.

GITHUB / SOURCE CODEExplore the key-value memory implementationsriraman-10 / applied-ai-concepts / memory / 02-kv-memory

References

SQLITE / UPSERTSQLite UPSERT documentationConflict handling used to preserve one row per logical memory key