Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

surrealdb-memory

Agent memory client for the surrealdb Python SDK — a client for Spectron, a memory service.

Shipped as its own distribution so its version moves independently of the SDK's, and imported through the SDK as surrealdb.memory.

from surrealdb.memory import Memory

with Memory(
    context="acme-prod",
    endpoint="https://api.spectron.example",
    api_key="sk-spec-...",
) as memory:
    memory.remember("I work at Acme as CTO")
    hits = memory.recall("what do I do at Acme")
    print(hits.hits)

Install

pip install 'surrealdb[memory]'

# Using uv
uv add 'surrealdb[memory]'

The extra depends on this package with >=, so you can hold surrealdb at 3.x and move surrealdb-memory across its own major versions.

Clients

from surrealdb.memory import Memory, AsyncMemory

with Memory(context="acme-prod", endpoint="https://api.spectron.example", api_key="sk-...") as memory:
    memory.remember("I work at Acme as CTO")

async with AsyncMemory(context="acme-prod", endpoint="https://api.spectron.example", api_key="sk-...") as memory:
    await memory.remember("I work at Acme as CTO")

Memory uses requests. AsyncMemory uses aiohttp. Same method names on both; add await for the async one.

Both clients are pinned to one context and hit /api/v1/{context}/....

Constructor

Arg Default
context required Context id, e.g. "acme-prod".
endpoint required Full URL of the memory service host, e.g. "https://api.spectron.example". No default.
api_key required Bearer token, as a string. Sent as Authorization: Bearer <key>.
timeout 30.0 Seconds per request.
max_retries 3 Retries for GETs and idempotent writes.
transport None Inject your own for testing.

Pass api_key as a string from wherever you keep secrets. The SDK never reads environment variables.

Surface

The client covers the full memory service end-user API. Headline verbs are methods on the client; grouped resources live under namespaces (memory.documents.…, memory.sessions.…, etc.). Both Memory and AsyncMemory expose the same names — await the async one.

Memory verbs

Method Endpoint
remember(text, ...) POST /api/v1/{context}/facts
remember_many(items, ...) POST /api/v1/{context}/facts/batch
recall(query, ...) POST /api/v1/{context}/query
forget(query, ...) POST /api/v1/{context}/forget
chat(message, stream=…) POST /api/v1/{context}/chat (SSE if stream=True)
consolidate(...) POST /api/v1/{context}/consolidate
reflect(query, ...) POST /api/v1/{context}/reflect
elaborate(...) POST /api/v1/{context}/elaborate
query_context(query, ...) POST /api/v1/{context}/context
inspect(ref, ...) GET /api/v1/{context}/inspect
state() GET /api/v1/{context}/state
audit(...) GET /api/v1/{context}/audit
whoami() GET /api/v1/{context}/me
profile() GET /api/v1/{context}/profile
health() GET /api/v1/health (not context-scoped)

Namespaces

Namespace Methods
documents upload, get, delete, list, query, fetch_raw, reprocess, recompute_links, chunks
documents.keywords list, get, search, for_document
sessions create, delete, context, turns
entities list, get, delete, history
scopes register, list, delete, forget
principals list, get, grant, revoke, effective
keys create, list, delete, rotate
traces list, get, stats
lifecycle decay, expire, fsck

Remember

memory.remember("I work at Acme as CTO")
memory.remember("Acme acquired Beta", session_id="sess:abc", scopes="org/acme")
memory.remember("Q3 board notes", labels=["topic=board"], memory_category="context")

memory.remember_many(
    [
        {"role": "user", "content": "I just got promoted to CTO"},
        {"role": "assistant", "content": "Congratulations!"},
    ],
    extract="whole_conversation",
)

infer selects the extraction path (full, triples, preview, none). remember_many also takes extract (per_message or whole_conversation). labels are key=value strings recorded on the rows.

remember and remember_many set an Idempotency-Key header derived from sha256(method | path | body | 30s-bucket), so a retry inside the bucket collapses onto the previous attempt server-side.

Recall

result = memory.recall("what role do I have at Acme", k=10, mode="hybrid")
for hit in result.hits:
    print(hit.score, hit.source, hit.text)

Optional filters: labels (a key=value path list), lens (a read scope selector taking the same DNF shape as scopes, see Scope), scope_view (strict | merged | crossTeam), include (facts, passages), the temporal bounds as_of / at_instant / valid_from / valid_until, and a location geo filter ({"near": {"lat": ..., "lng": ..., "radiusKm": ...}} or {"within": "<WKT>"}).

Forget

memory.forget("anything about my old job")
memory.forget("draft notes", purge=True)

Chat

reply = memory.chat("what's my role?")
print(reply.reply)

# Streaming (Server-Sent Events). Yields ChatChunk objects.
for chunk in memory.chat("what's my role?", stream=True):
    if chunk.delta:
        print(chunk.delta, end="", flush=True)
    if chunk.done:
        print("\n[trace]", chunk.trace_id)

Async streaming:

stream = await memory.chat("what's my role?", stream=True)
async for chunk in stream:
    ...

Documents

result = memory.documents.upload(
    "returns.pdf",
    content_type="application/pdf",
    title="Returns policy",
    source="kb",
    scopes="org/acme",
)
print(result.id, result.status)

path accepts a filesystem path, bytes, or a file-like object. title, source, and scopes are optional and ride along as the document's metadata. scopes takes the same DNF shape as the write verbs (see Scope); each path must lie within the key's write region, and omitting it falls back to the whole write region.

The rest of the document surface manages the corpus:

page = memory.documents.list(status="Ready", page=1, page_size=20)
doc = memory.documents.get("document:abc")
hits = memory.documents.query("refund window", k=5, mode="hybrid")
raw = memory.documents.fetch_raw("document:abc")          # -> bytes
memory.documents.reprocess("document:abc")
memory.documents.recompute_links()
chunks = memory.documents.chunks("document:abc")

# Keyword index (corpus-level)
memory.documents.keywords.list(q="refund")
memory.documents.keywords.get("refund")
memory.documents.keywords.search("returns policy", k=10)
memory.documents.keywords.for_document("document:abc")

Consolidate, reflect, elaborate

# Pool recent facts into durable observations (preview with dry_run=True).
outcome = memory.consolidate(dry_run=True)
print(outcome.created, outcome.superseded, outcome.outcomes)

# Summarise what the store knows, optionally persisting the reflection.
memory.reflect("what are my preferences", persist=True)

# Expand an entity's relations from existing memory.
memory.elaborate(entity_ref="entity:person/stu")

Introspection

memory.state()                      # working-memory snapshot
memory.whoami()                     # caller identity + effective grants
memory.profile()                    # static/dynamic profile entries
memory.inspect("entity:person/stu") # raw, kind-discriminated payload
memory.query_context("who is stu")  # composed context string

Sessions, entities, scopes

session = memory.sessions.create(scopes="org/acme")
memory.sessions.context(session.id, "recap our last call")
memory.sessions.turns(session.id, limit=20)

memory.entities.list(entity_type="person")
memory.entities.get("person", "Stu")
memory.entities.history("person", "Stu", "role")

memory.scopes.register("org/acme/", display_name="Acme")
memory.scopes.list()
memory.scopes.forget("user/stu/")

Access control: principals & self-service keys

memory.principals.list()
memory.principals.grant("agent-7", "org/acme/*", ["memory:read", "memory:write"])
memory.principals.effective("agent-7", "org/acme/")

minted = memory.keys.create(name="ci", ttl_seconds=3600)
print(minted.key)                   # full bearer key, shown once
memory.keys.rotate("ci")
memory.keys.list()

Observability & maintenance

memory.traces.list(limit=50)
memory.traces.get("trace:xyz")
memory.traces.stats()
memory.audit(kind="decision", limit=100)   # access/cost log

memory.lifecycle.decay()                    # age out importance
memory.lifecycle.expire()                   # drop expired rows
memory.lifecycle.fsck(check="duplicates")   # integrity report

Errors

from surrealdb.memory import MemoryAPIError, MemoryNotFoundError

try:
    memory.recall("...")
except MemoryNotFoundError as exc:
    print(exc.status_code, exc.message)
except MemoryAPIError as exc:
    print(exc.status_code, exc.message, exc.trace_id)
Exception HTTP
MemoryServiceError base
MemoryAPIError any non-2xx without a more specific class
MemoryAuthError 401
MemoryScopeError 403
MemoryNotFoundError 404

Every MemoryAPIError carries status_code, message, trace_id, and the decoded body.

Retries and timeouts

  • GET and idempotent writes (remember, remember_many) retry on connection errors and 5xx: 250ms, 500ms, 1s. Up to max_retries (default 3).
  • Non-idempotent writes never retry. You handle it.
  • Default timeout is 30s. Override with timeout= on the constructor.

Scope

scopes is a DNF (disjunctive-normal-form) selector: an OR of conjunctive clauses, serialised on the wire as array<array<string>>. The outer list ORs across clauses; each inner list ANDs the scope paths within a clause. The same shape is used by the write verbs (scopes) and the read lens (lens). Omit it to use the key's default write region.

The facade accepts these input forms:

  • A single path string is one singleton clause: "team/eng" -> [["team/eng"]].
  • A flat list of paths is an OR of singletons: ["team/eng", "org/acme"] -> [["team/eng"], ["org/acme"]] (eng OR acme).
  • A nested list is an AND clause: [["team/eng", "org/acme"]] -> [["team/eng", "org/acme"]] (eng AND acme).
  • The string and nested forms mix freely: ["team/eng", ["org/acme", "region/eu"]] -> [["team/eng"], ["org/acme", "region/eu"]].
memory.remember("...", scopes="team/eng")
memory.remember("...", scopes=["team/eng", "org/acme"])     # eng OR acme
memory.remember("...", scopes=[["team/eng", "org/acme"]])   # eng AND acme

Authentication

Every request carries Authorization: Bearer <api_key>.

Delegation (on-behalf-of)

Every verb takes an optional on_behalf_of argument. When set, the request carries X-Memory-On-Behalf-Of: <principal> and the server runs it as that principal, with effective grants narrowed to caller ∩ target (the caller can only delegate to a principal it is allowed to act for). The value may be a bare principal id or the principal:<id> form; it is sent verbatim.

memory.recall("open tickets", on_behalf_of="principal:agent-7")
memory.remember("ticket triaged", on_behalf_of="principal:agent-7")

Delegation is per call, so a single client can act for different principals on different requests.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

surrealdb_memory-1.0.0b1.tar.gz (37.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

surrealdb_memory-1.0.0b1-py3-none-any.whl (35.5 kB view details)

Uploaded Python 3

File details

Details for the file surrealdb_memory-1.0.0b1.tar.gz.

File metadata

  • Download URL: surrealdb_memory-1.0.0b1.tar.gz
  • Upload date:
  • Size: 37.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for surrealdb_memory-1.0.0b1.tar.gz
Algorithm Hash digest
SHA256 e1dac1525e2c1da110fad683c1090b5d04a226a522361be982d243a360637748
MD5 f6b0e185cd8ce0b457cd12e554e87b05
BLAKE2b-256 9be6cfefa0a7ca4b50abb3c5f820feb9d370d6c5db4747df609bd8285ffc31c6

See more details on using hashes here.

Provenance

The following attestation bundles were made for surrealdb_memory-1.0.0b1.tar.gz:

Publisher: build.yml on surrealdb/surrealdb.py

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file surrealdb_memory-1.0.0b1-py3-none-any.whl.

File metadata

File hashes

Hashes for surrealdb_memory-1.0.0b1-py3-none-any.whl
Algorithm Hash digest
SHA256 dc2357e1442d5868c30b3347485b39c4444d4780bde67526827119790836b482
MD5 4196d43cdef766d1263bc6aee951111d
BLAKE2b-256 0c84eb67f4f915badb7b7d95d32fb0b7e8440abe991b2d520d1cf46d6674e62b

See more details on using hashes here.

Provenance

The following attestation bundles were made for surrealdb_memory-1.0.0b1-py3-none-any.whl:

Publisher: build.yml on surrealdb/surrealdb.py

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

1.0.0b1 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page