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" },
  "kind": "default/v1",
  "created_at": "2026-01-01T00:00:00Z",
  "expires_at": "2026-01-02T00:00:00Z",
  "revision": 1
}

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. For optimistic concurrency, include expected_revision on PUT or PATCH; the request fails with 409 Conflict if the active memory revision no longer matches. Each successful write or archive update increments the revision.

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" },
  "kind": "default/v1",
  "created_at": "2026-01-01T00:00:00Z",
  "expires_at": "2026-01-02T00:00:00Z"
}

The value is decrypted on read. Returns 404 if no matching record exists for the requested archive mode, 403 if the caller lacks access.

Use the optional archived query parameter to control which version is readable:

ValueMeaning
excludeActive memories only (default)
includeActive and archived memories
onlyArchived memories only

Archiving a Memory

curl -X PATCH http://localhost:8080/v1/memories \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <token>" \
  -d '{
    "namespace": ["user", "alice", "notes"],
    "key": "python_tip",
    "archived": true
  }'

Returns 204 No Content. Archiving is recorded as a memory update, and semantic search respects the memory’s archive state via vector-store metadata plus datastore post-filtering.

Memory Kind Versions

Every memory row carries a canonical kind name — a string like default/v1 or customer-profile/v2 — that identifies which projection program was used to compute its plaintext attributes. Different memories can use different kinds in the same datastore, and migrations can move memories to a new kind online without stopping writes.

Kind Names

A kind name combines a family and a version, separated by exactly one /:

customer-profile/v2

Both segments are lowercase DNS-label-like values: 1–63 characters, starting with a letter, containing only letters, digits, and hyphens. The name is the resource’s unique identifier and is stored directly on the memory row.

The Built-In Default Kind

The built-in kind default/v1 is always registered from manifest and Rego source embedded in the Memory Service binary. Its immutable projection derives the compatibility namespace and sub attributes. Memories written without an explicit kind field always resolve to default/v1. The manifest and the built-in authz.rego, projection.rego, and filter.rego sources live together under internal/episodic/default-v1/; authz/filter remain global across kinds.

Its declared attributes are:

{
  "namespace": "string",
  "sub": "string"
}

Its attribute-projection program is:

package memories.attributes

default attributes = {}

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

This Rego belongs to the immutable default/v1 kind. It is separate from the global authz.rego and filter.rego programs described under Access Control.

Selecting a Kind at Write Time

curl -X PUT http://localhost:8080/v1/memories \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <token>" \
  -d '{
    "namespace": ["user", "alice", "events"],
    "key": "signup",
    "value": {"observedAt": "2026-01-01T00:00:00Z", "channel": "web"},
    "kind": "event-tracking/v2"
  }'

kind accepts:

ValueBehavior
"event-tracking/v2"Use that exact writable version
"event-tracking"Rejected; writes require an exact version
omittedUse the fixed built-in default/v1

The exact resolved name is returned in the write response as kind.

Searching by Kind

Attribute-only POST /v1/memories/search accepts an optional kind selector:

{
  "namespace_prefix": ["user", "alice"],
  "kind": "event-tracking/v2",
  "filter": { "channel": { "$eq": "web" } },
  "limit": 10
}
SelectorMatches
"event-tracking/v2"Only that exact version
"event-tracking"All versions in that family
omittedAll schemas

A field absent from a given schema version does not match a condition on that field, so the same filter can safely be used across a family search.

Attribute Sort

Attribute-only searches (no query or queries) support a single typed sort:

{
  "namespace_prefix": ["user", "alice", "events"],
  "kind": "event-tracking/v2",
  "sort": { "field": "observedAt", "direction": "desc" },
  "limit": 20
}

Rules:

  • field must be a top-level attribute name declared in the selected schema(s).
  • direction is "asc" or "desc".
  • Memories with the field absent sort last in both directions.
  • Ties break on created_at DESC, id DESC.
  • String sorting uses binary/locale-independent collation (COLLATE "C" on PostgreSQL, COLLATE BINARY on SQLite).
  • Sort is rejected when query or queries is present — semantic results are always ordered by similarity score.

Searching Memories

POST /v1/memories/search supports attribute-filter search, single-query semantic search, and multi-query semantic search. All search modes honor the same archived selector used by direct reads: exclude (default), include, or only.

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": {"sub": {"$eq": "alice"}},
    "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"
    }
  ]
}

For prompts with multiple retrieval intents, send queries instead of query. Each query item has a required text and an optional purpose; the response uses the purpose as query attribution and falls back to the text when purpose is omitted.

curl -X POST http://localhost:8080/v1/memories/search \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <token>" \
  -d '{
    "namespace_prefix": ["user", "alice", "cognition.v1"],
    "queries": [
      {"text": "release plan", "purpose": "release"},
      {"text": "Docker image build failure", "purpose": "docker"},
      {"text": "Python packages excluded from release", "purpose": "python-scope"}
    ],
    "per_query_limit": 5,
    "limit": 12
  }'

Multi-query search embeds all query texts in one batch, runs vector search independently for each query, deduplicates memory IDs, and merges rankings with Reciprocal Rank Fusion. Returned items include matchedQueries so callers can see which query purposes matched each memory:

{
  "items": [
    {
      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "namespace": ["user", "alice", "cognition.v1", "procedures"],
      "key": "procedure:deployment-debugging",
      "value": { "text": "Check CI image build logs before rerunning release." },
      "score": 0.0325,
      "matchedQueries": ["release", "docker"]
    }
  ]
}

query and queries are mutually exclusive. queries must be non-empty when present, and every queries[].text must be non-blank.

score is null for attribute-only results, a cosine similarity value (0–1) for single-query semantic results, and an RRF score for multi-query semantic results. Semantic search pre-filters by archive state in the vector store and then re-checks the hydrated memory rows before returning results.

Search Parameters

ParameterTypeRequiredDescription
namespace_prefixstring[]yesRestricts results to this namespace subtree
querystringnoSingle semantic search string. Mutually exclusive with queries
queriesobject[]noMulti-query semantic search strings with required text and optional purpose
per_query_limitintegernoPer-query vector search budget for multi-query search, default limit, max 100
filterobjectnoAttribute filter expressions (see below)
kindstringnoSchema selector: exact name, family name, or omit for all schemas
sortobjectnoOne-field typed sort: {"field":"<attr>","direction":"asc|desc"}. Rejected with semantic search
archivedstringnoexclude (default), include, or only
limitintegernoMax results, default 10, max 100

Attribute Filter Expressions

Filters are a flat JSON object where each key is an attribute field name. Search returns a bounded top-k result set; it is not pageable, and request fields such as offset, order, or after_cursor are rejected.

The filter language uses positive, pushdownable predicates only:

FormMeaningExample
Bare scalar or {"$eq": value}Equality{"topic": "python"}
Array or {"$in": [...]}Set membership{"lang": {"$in": ["python", "go"]}}
{"$gte"/"$lte": value}Numeric/timestamp range{"score": {"$gte": 0.5}}
{"$exists": true}Present non-null value{"sourceHash": {"$exists": true}}

All conditions in the object are ANDed. $ne, $nin, $exists: false, old unprefixed operators such as {"in": [...]}, and arbitrary datastore query operators are rejected.

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
archivedexclude (default), include, or only
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": "update",
      "occurred_at": "2026-01-03T00:00:00Z",
      "value": { "text": "Alice prefers list comprehensions over map/filter." },
      "attributes": { "namespace": "user", "sub": "alice" }
    }
  ],
  "after_cursor": "<opaque cursor>"
}
ParameterDescription
nsRepeated per segment; filters to a namespace prefix
kindsFilter by event kind: add, update, expired; default all
after / beforeISO 8601 timestamp bounds on occurred_at
after_cursorOpaque cursor for paginating through results
limitMax events per page; default 50, server-configurable maximum

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 expired events; archive operations appear as update 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
kindCanonical schema name used for this row (e.g. "default/v1")
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 definitions from the comma-separated --policy-import-path or MEMORY_SERVICE_POLICY_IMPORT_PATH setting.

A configured directory may contain this optional pair of global policy overrides:

  • authz.rego — read/write/delete authorization
  • filter.rego — search/list namespace and filter injection

The two global files are optional as a pair. When neither is present, the service uses its built-in authorization and scoping programs. Each configured directory contributes only root-level global files. You can also list either global file explicitly. The complete import path must resolve at most one file of each name and must provide both files or neither file. Other Rego files are available as assets for manifest-based policy types and are not loaded as global programs.

Each configured directory is searched recursively for .yaml and .yml files. Explicit file entries are examined regardless of their extension. Documents without kind: memory-kind are ignored. Matching documents are decoded strictly and may reference a projection file relative to their own directory. Imports insert only absent versions. Identical versions are logged, and same-name differences are logged without overwriting the database. The built-in default/v1 kind handles namespace/sub projection for memories written without an explicit kind.

If no path is set, the service uses its built-in authorization and scoping programs and imports no deployment-provided memory kinds. The distributed container image copies the policy tree to /etc/memory-service/policies/, including the cognition bundle under cognition/, and sets the parent directory as its default import path.

Policy import path configuration

The policy import path accepts a comma-separated list of files and directories. You can also repeat --policy-import-path; each occurrence appends to the list.

/etc/memory-service/policies/,/opt/acme/customer-profile.yaml

A directory can contain global Rego overrides and any number of manifest-based policy bundles:

<policy-directory>/
├── authz.rego                 # optional global override
├── filter.rego                # optional global override
└── cognition/
    ├── cognition.yaml         # discovered recursively
    └── projection.rego

The global overrides use filename-based discovery:

  • authz.rego and filter.rego are exact, case-sensitive filenames. They can be located directly in a configured directory or listed as explicit file entries.
  • The import path must provide both files or neither file. It cannot resolve more than one file of either name.
  • authz.rego must expose data.memories.authz.decision; filter.rego must expose data.memories.filter.
  • Global Rego discovery is not recursive. Other .rego files are not loaded as global programs.

Manifest-based policy documents use discriminator-based discovery. Every .yaml and .yml file below a configured directory is examined recursively. Files with other extensions in that directory are ignored. An explicit file entry is examined regardless of its extension. Documents without kind: memory-kind, including documents for future policy types, are ignored by the memory-kind importer.

A memory-kind manifest can embed its projection in projectionRego or reference a Rego file relative to the manifest with projectionRegoFile:

kind: memory-kind
name: customer-profile/v1
attributes:
  customerId: string
  updatedAt: timestamp
projectionRegoFile: projection.rego
writable: true

The referenced filename is arbitrary; projection.rego is the recommended convention. Relative references cannot escape the manifest’s directory. The referenced program must declare package memories.attributes and define an attributes rule, producing data.memories.attributes.attributes. A .rego file that is neither a root-level global override nor referenced by a manifest is ignored.

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
kindstringExact resolved kind for writes, or the selected row’s kind
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)

filter.rego (data.memories.filter)

input fieldTypeDescription
namespace_prefixstring[]Requested namespace prefix for search/list
filterobjectCaller-supplied attribute filter (may be empty)
kindstringCaller-supplied exact/family kind selector (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
  • kind (string) — optional exact/family selector that can only narrow the caller’s selector

The result must be an object and any returned fields must have these exact types. Malformed output fails the request closed.

Default Built-In Policy (Repo Default)

The default global authorization and search-scoping programs are shown below. They are independent of the default/v1 attribute-projection Rego above and apply across all memory kinds.

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.filter

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

user_prefix := ["user", input.context.user_id]

starts_with(ns, prefix) if {
  count(prefix) == 0
}
starts_with(ns, prefix) if {
  count(ns) >= count(prefix)
  not mismatch(ns, prefix)
}
mismatch(ns, prefix) if {
  some i
  i < count(prefix)
  ns[i] != prefix[i]
}

# Authorization scoping is entirely expressed by namespace_prefix. Custom kinds
# are not required to project security attributes.
attribute_filter := {}

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.
  • filter.rego: every public search/list call is constrained to the authenticated caller’s own ["user", <caller_user_id>] subtree, including calls made by principals that also have an admin role.

Administrative memory exploration uses the separate /admin/v1/... endpoints described below. Those endpoints bypass the user-facing filter.rego, except that admin search with as_user_id deliberately evaluates it as the target user with no administrative roles.

See the Admin APIs for schema version, migration, and index management endpoints.

Admin Memory Exploration

Admin and auditor users can inspect episodic memories through a dedicated admin surface. Admin users can also write or archive namespace-scoped memories through this surface. These endpoints are separate from /v1/memories; they do not rely on user-facing OPA policy injection unless an admin explicitly searches as a target user.

MethodEndpointRolePurpose
GET/admin/v1/memoriesadmin or auditorList latest memory rows across users with filters and cursor pagination
PUT/admin/v1/memoriesadminUpsert a memory in any namespace
PATCH/admin/v1/memoriesadminArchive an active memory by namespace and key
GET/admin/v1/memories/{id}admin or auditorRead a retained memory row by UUID without incrementing usage counters
POST/admin/v1/memories/searchadmin or auditorSearch across users by namespace prefix, safe attributes, and optional semantic query or query batch
GET/admin/v1/memory-namespacesadmin or auditorBrowse memory namespace trees across users

Admin list and namespace query parameters use camelCase query names, for example:

curl "http://localhost:8080/admin/v1/memories?namespacePrefix=user&namespacePrefix=alice&includeUsage=true" \
  -H "Authorization: Bearer <admin-or-auditor-token>" \
  -H "X-Justification: support investigation"

Admin search reuses the public memory search JSON shape, including single-query and multi-query semantic search, and adds as_user_id:

curl -X POST http://localhost:8080/admin/v1/memories/search \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <admin-or-auditor-token>" \
  -H "X-Justification: support investigation" \
  -d '{
    "namespace_prefix": ["user"],
    "as_user_id": "alice",
    "kind": "preference",
    "filter": {"category": {"$in": ["preference", "procedure"]}},
    "limit": 25
  }'

When as_user_id is omitted, admin search is admin-wide and caller filters narrow that result set. When as_user_id is set, the server applies the same memory search policy that the target user would receive from public POST /v1/memories/search.

Admin writes use the same memory body shape as public PUT /v1/memories and may include expected_revision for compare-and-swap updates. Admin archive uses repeated ns query parameters plus key, with {"archived": true} in the request body:

curl -X PUT "http://localhost:8080/admin/v1/memories?justification=cognition%20processor%20write" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <admin-token>" \
  -d '{
    "namespace": ["user", "alice", "cognition.v1", "facts"],
    "key": "preference-theme",
    "value": {"content": "Alice prefers light theme"},
    "index": {"content": "Alice prefers light theme"}
  }'
curl -X PATCH "http://localhost:8080/admin/v1/memories?ns=user&ns=alice&ns=cognition.v1&ns=facts&key=preference-theme&justification=cognition%20cleanup" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <admin-token>" \
  -d '{
    "archived": true,
    "expected_revision": 2
  }'

Admin memory writes are authorized by admin role, OIDC scope when configured, and optional justification enforcement. They intentionally bypass user-facing memory OPA authorization because they are administrative namespace operations. Attribute projection still uses the selected immutable MemoryKindVersion, with only persisted namespace, key, value, and index inputs, so agent and admin writes produce the same replayable metadata. Admin write requests do not carry on_behalf_of_user_id.

Operational memory admin routes (separate from the CRUD surface above):

MethodEndpointRole
GET/admin/v1/memory-index/statusadmin
POST/admin/v1/memory-index/triggeradmin
GET/admin/v1/memory-usageadmin
GET/admin/v1/memory-usage/topadmin
DELETE/admin/v1/memories/{id}admin

If admin justification enforcement is enabled, admin memory routes accept X-Justification or ?justification=...; gRPC admin memory requests carry a justification field.

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 attributes derived by the memory’s immutable kind projection.

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-langchain 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_langchain.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 and archive mode
PATCH/v1/memoriesArchive or unarchive a memory
POST/v1/memories/searchAttribute filter and/or semantic search
GET/v1/memories/namespacesList namespaces under a prefix and archive mode
GET/v1/memories/eventsPaginated event timeline (add, update, expired)

Next Steps