Memories

Memories are a persistent, namespaced key-value store that lets LLM agents save and recall information across conversations. Unlike conversation entries — which record the chronological exchange between users and models — memories hold arbitrary facts, preferences, and context that agents want to retain long-term.

What is a Memory?

A memory in Memory Service is:

  • A key-value item identified by a namespace tuple and a string key
  • Stored in a hierarchical namespace that organizes memories by user, agent, or session
  • Encrypted at rest — values are AES-256-GCM encrypted; metadata, derived attributes, and caller-provided index text are stored in plaintext
  • Optionally indexed for semantic search via vector embeddings
  • Subject to OPA/Rego access control enforced at the service level
  • Compatible with the LangGraph BaseStore interface via a Python client library

Namespace Model

A namespace is an ordered list of non-empty string segments that forms a path-like address. Namespaces let you organize memories into hierarchies — per-user, per-agent, per-session, or shared.

namespace: ["user", "alice", "notes"]
key:       "python_tip"

Common patterns:

PatternExample namespaceUse case
Per-user["user", "alice"]Personal preferences and facts
Per-user + category["user", "alice", "notes"]Categorized user memories
Per-agent["agent", "support-bot"]Agent-global knowledge
Session-scoped["session", "<session-id>"]Short-lived context
Shared["shared", "product-faqs"]Knowledge shared across agents

The maximum namespace depth is admin-configurable (default: 5 segments).

Memory Lifecycle

Writing a Memory

curl -X PUT http://localhost:8080/v1/memories \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <token>" \
  -d '{
    "namespace": ["user", "alice", "notes"],
    "key": "python_tip",
    "value": {
      "text": "Alice prefers list comprehensions over map/filter."
    },
    "index": {"text": "Alice prefers list comprehensions over map/filter."},
    "ttl_seconds": 86400
  }'

Response:

{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "namespace": ["user", "alice", "notes"],
  "key": "python_tip",
  "attributes": { "namespace": "user", "sub": "alice" },
  "created_at": "2026-01-01T00:00:00Z",
  "expires_at": "2026-01-02T00:00:00Z"
}

The value is not echoed back in the response — only the write confirmation is returned.

Calling PUT with an existing (namespace, key) pair upserts the memory, replacing the previous value.

Reading a Memory

Use repeated ns query parameters — one per namespace segment:

curl "http://localhost:8080/v1/memories?ns=user&ns=alice&ns=notes&key=python_tip" \
  -H "Authorization: Bearer <token>"

Response:

{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "namespace": ["user", "alice", "notes"],
  "key": "python_tip",
  "value": {
    "text": "Alice prefers list comprehensions over map/filter."
  },
  "attributes": { "namespace": "user", "sub": "alice" },
  "created_at": "2026-01-01T00:00:00Z",
  "expires_at": "2026-01-02T00:00:00Z"
}

The value is decrypted on read. Returns 404 if no active record exists for the key, 403 if the caller lacks access.

Deleting a Memory

curl -X DELETE "http://localhost:8080/v1/memories?ns=user&ns=alice&ns=notes&key=python_tip" \
  -H "Authorization: Bearer <token>"

Returns 204 No Content. The background indexer removes the corresponding vector entry on its next cycle.

Searching Memories

POST /v1/memories/search supports two modes depending on whether a query string is provided.

Without a query, the service applies an attribute filter against the primary store:

curl -X POST http://localhost:8080/v1/memories/search \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <token>" \
  -d '{
    "namespace_prefix": ["user", "alice"],
    "filter": {"topic": "python"},
    "limit": 10
  }'

With a query, the service embeds the query text and performs an approximate nearest-neighbor search in the vector store, then fetches and decrypts the matching memories from the primary store:

curl -X POST http://localhost:8080/v1/memories/search \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <token>" \
  -d '{
    "namespace_prefix": ["user", "alice"],
    "query": "whitespace-sensitive syntax",
    "limit": 5
  }'

Response:

{
  "items": [
    {
      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "namespace": ["user", "alice", "notes"],
      "key": "python_tip",
      "value": { "text": "Alice prefers list comprehensions over map/filter." },
      "attributes": { "namespace": "user", "sub": "alice" },
      "score": 0.92,
      "created_at": "2026-01-01T00:00:00Z"
    }
  ]
}

score is null for attribute-only results and a cosine similarity value (0–1) for semantic results.

Search Parameters

ParameterTypeRequiredDescription
namespace_prefixstring[]yesRestricts results to this namespace subtree
querystringnoIf set, enables vector similarity search
filterobjectnoAttribute filter expressions (see below)
limitintegernoMax results, default 10, max 100
offsetintegernoPagination offset (attribute-only mode)

Attribute Filter Expressions

Filters are a flat JSON object where each key is an attribute field name. Three expression forms are supported:

FormMeaningExample
Bare scalarEquality{"topic": "python"}
{"in": [...]}Set membership{"lang": {"in": ["python", "go"]}}
{"gt"/"gte"/"lt"/"lte": value}Numeric/timestamp range{"score": {"gte": 0.5}}

All conditions in the object are ANDed.

Listing Namespaces

Navigate the namespace hierarchy to discover what subtrees exist:

curl "http://localhost:8080/v1/memories/namespaces?prefix=user&prefix=alice&max_depth=3" \
  -H "Authorization: Bearer <token>"

Response:

{
  "namespaces": [
    ["user", "alice", "notes"],
    ["user", "alice", "tasks"]
  ]
}
ParameterDescription
prefixRepeated per segment; only namespaces under this prefix are returned
suffixOnly return namespaces ending with this suffix
max_depthTruncate returned namespaces to this depth

Memory Event Timeline

GET /v1/memories/events returns a paginated, time-ordered stream of memory lifecycle events — useful for syncing external systems, auditing changes, or replaying history.

curl "http://localhost:8080/v1/memories/events?ns=user&ns=alice&limit=50" \
  -H "Authorization: Bearer <token>"
{
  "events": [
    {
      "id": "a1b2c3d4-...",
      "namespace": ["user", "alice", "notes"],
      "key": "python_tip",
      "kind": "add",
      "occurred_at": "2026-01-01T00:00:00Z",
      "value": { "text": "Alice prefers list comprehensions." },
      "attributes": { "namespace": "user", "sub": "alice" }
    },
    {
      "id": "b2c3d4e5-...",
      "namespace": ["user", "alice", "notes"],
      "key": "python_tip",
      "kind": "update",
      "occurred_at": "2026-01-02T00:00:00Z",
      "value": { "text": "Alice prefers list comprehensions over map/filter." },
      "attributes": { "namespace": "user", "sub": "alice" }
    },
    {
      "id": "c3d4e5f6-...",
      "namespace": ["user", "alice", "notes"],
      "key": "python_tip",
      "kind": "delete",
      "occurred_at": "2026-01-03T00:00:00Z",
      "value": null,
      "attributes": null
    }
  ],
  "after_cursor": "<opaque cursor>"
}
ParameterDescription
nsRepeated per segment; filters to a namespace prefix
kindsFilter by event kind: add, update, delete, expired; default all
after / beforeISO 8601 timestamp bounds on occurred_at
after_cursorOpaque cursor for paginating through results
limitMax events per page; default 50, max 200

The same OPA access control that governs memory reads applies here — callers only see events for namespaces they can access. value and attributes are null for delete and expired events.

Memory Properties

PropertyDescription
idUnique UUID assigned on each write
namespaceOrdered list of string segments forming the address
keyUnique key within the namespace
valueArbitrary JSON object; encrypted at rest
attributesPolicy-derived plaintext attributes used for filtering/search scoping
created_atTimestamp of this version
expires_atTTL expiry timestamp, or null for no expiry
scoreCosine similarity score (search results only; null for attribute-only)

TTL and Expiry

Set ttl_seconds on a PUT request to make a memory expire automatically:

{
  "namespace": ["session", "abc123"],
  "key": "context",
  "value": { "summary": "User asked about billing." },
  "ttl_seconds": 3600
}

A background goroutine expires memories on a configurable interval (default: 60 s). The vector indexer removes the corresponding vector entries on its next cycle.

Access Control

Memory access is enforced by embedded OPA/Rego policies evaluated on every memory API call. The service loads policy files from:

  • --policy-dir
  • MEMORY_SERVICE_POLICY_DIR

Expected files in that directory:

  • authz.rego - read/write/delete authorization
  • attributes.rego - plaintext policy attributes extraction
  • filter.rego - search/list namespace+filter injection

If no directory is set, or if a file is missing, the service falls back to the built-in default for that file.

Rego Policy Input Variables

Each policy is evaluated with an input object. Available fields differ by policy type.

authz.rego (data.memories.authz.decision)

input fieldTypeDescription
operationstringOperation being authorized: write, read, or delete
namespacestring[]Full namespace segments from the request
keystringMemory key from the request
valueobjectPresent for write; full memory value payload
indexobject<string,string>Present for write; caller-provided redacted index payload
context.user_idstringAuthenticated subject/user ID
context.client_idstringAuthenticated client ID (API key/OIDC client), when present
context.jwt_claimsobjectRaw JWT claims map (for example roles)

attributes.rego (data.memories.attributes.attributes)

input fieldTypeDescription
namespacestring[]Full namespace segments from the write request
keystringMemory key from the write request
valueobjectMemory value JSON body
indexobject<string,string>Caller-provided redacted index payload
context.user_idstringAuthenticated subject/user ID
context.client_idstringAuthenticated client ID (API key/OIDC client), when present
context.jwt_claimsobjectRaw JWT claims map (for example roles)

Typical value and index payloads passed to attributes.rego:

{
  "value": {
    "text": "Alice prefers list comprehensions over map/filter.",
    "topic": "python",
    "confidence": 0.92,
    "source": {
      "type": "chat",
      "conversationId": "8fa3deec-4a45-42a5-a36d-6076b20a2c8d"
    },
    "tags": ["style", "python", "preferences"]
  },
  "index": {
    "text": "Alice prefers list comprehensions over map/filter.",
    "topic": "python"
  }
}

filter.rego (data.memories.filter)

input fieldTypeDescription
namespace_prefixstring[]Requested namespace prefix for search/list
filterobjectCaller-supplied attribute filter (may be empty)
context.user_idstringAuthenticated subject/user ID
context.client_idstringAuthenticated client ID (API key/OIDC client), when present
context.jwt_claimsobjectRaw JWT claims map (for example roles)

The filter.rego result may return:

  • namespace_prefix (string[]) - effective prefix to enforce
  • attribute_filter (object) - merged into the caller filter before datastore query

Default Built-In Policy (Repo Default)

The default policy bundle shipped in this repository is:

package memories.authz

default decision = {"allow": false, "reason": "access denied"}

decision = {"allow": true} if {
  input.namespace[0] == "user"
  input.namespace[1] == input.context.user_id
}
package memories.attributes

default attributes = {}

attributes = {"namespace": input.namespace[0], "sub": input.namespace[1]} if {
  count(input.namespace) >= 2
}
package memories.filter

is_admin if {
  "admin" in input.context.jwt_claims.roles
}

namespace_prefix := input.namespace_prefix if { is_admin }
namespace_prefix := user_prefix if {
  not is_admin
  not starts_with(input.namespace_prefix, user_prefix)
}

attribute_filter := {} if { is_admin }
attribute_filter := {"namespace": "user", "sub": input.context.user_id} if { not is_admin }

What this means in practice:

  • authz.rego: direct PUT/GET/DELETE is allowed only under ["user", <caller_user_id>, ...]; deny responses can carry a reason.
  • attributes.rego: each memory gets plaintext policy attributes namespace and sub for policy-aware filtering.
  • filter.rego: non-admin search/list calls are constrained to the caller’s own ["user", <caller_user_id>] subtree; admin callers keep the requested prefix and no forced attribute filter.

Important: with the default bundle, admin role affects search/list filtering, but does not bypass authz.rego for direct read/write/delete. If you want admin bypass there, add it in authz.rego.

See the Admin APIs for policy management endpoints.

Encryption

Memory values are encrypted at rest using AES-256-GCM via the service’s existing key-management infrastructure. The namespace, key, policy-derived attributes, caller-provided index payload (stored as indexed_content), and expiry timestamp are stored in plaintext for filtering and indexing.

Vector stores never receive encrypted data. They hold only embeddings and plaintext policy attributes derived by the OPA attribute-extraction policy.

Vector Indexing

When a memory is written with an index payload, the background indexer embeds those field values and upserts them to the configured vector store (PGVector or Qdrant). Indexing is decoupled from the write path: writes return immediately, and the indexer catches up asynchronously.

Control which fields are embedded by sending a redacted index map on PUT:

{
  "namespace": ["user", "alice", "notes"],
  "key": "tip",
  "value": { "text": "...", "tags": ["python"] },
  "index": { "text": "..." }
}

Set "index": {} (or omit index) to disable vector indexing for that memory version.

Admin-configurable indexing settings:

SettingDefaultDescription
memory.episodic.indexing.batch_size100Items processed per indexer cycle
memory.episodic.indexing.interval30 sPolling interval
memory.episodic.namespace.max_depth5Maximum namespace depth

LangGraph Compatibility

The memory-service-langgraph Python package implements LangGraph’s BaseStore interface by calling the Memory Service REST API. This lets any LangGraph agent use the Memory Service as a drop-in persistent store without changing agent code.

from memory_service_langgraph import MemoryServiceStore

store = MemoryServiceStore(
    url="http://localhost:8080",
    token="<your-token>"
)

# Standard LangGraph BaseStore interface
store.put(("user", "alice", "notes"), "python_tip", {"text": "Use list comprehensions."})
item = store.get(("user", "alice", "notes"), "python_tip")
results = store.search(("user", "alice"), query="python syntax", limit=5)

An async variant (AsyncMemoryServiceStore) is also available for use in async LangGraph workflows.

API Operations

MethodPathPurpose
PUT/v1/memoriesUpsert a memory
GET/v1/memoriesGet a single memory by namespace + key
DELETE/v1/memoriesDelete a memory by namespace + key
POST/v1/memories/searchAttribute filter and/or semantic search
GET/v1/memories/namespacesList namespaces under a prefix
GET/v1/memories/eventsPaginated event timeline (add, update, delete, expired)

Next Steps