twin memory

Python SDK

twinmemory for Python - sync and async clients, typed results, retries, and a client that fails open when memory is down.

twinmemory is the Python client. It is on PyPI Soon: this page describes version 0.2.0, and until it is published, plain HTTP does the same job.

pip install twinmemory

Python 3.10 or newer. One dependency: httpx.

Quickstart

from twinmemory import Memory

memory = Memory()  # reads TWIN_MEMORY_API_KEY
dana = memory.user("dana")  # your own id for one of your end users

dana.add("I moved to Lisbon for the job at Acme.", role="user", session="onboarding")
dana.add({"id": "diet", "kind": "note", "text": "Vegetarian, allergic to peanuts."}, wait=True)

result = dana.search("Where does Dana live?")
print(result.context())  # ready to paste into a prompt

Make a key in the console, under API keys. A key belongs to one project; each end user you name with memory.user(...) has a memory of their own inside it. See End users.

SettingArgumentEnvironment variableDefault
API keyapi_keyTWIN_MEMORY_API_KEYrequired
Base URLbase_urlTWIN_MEMORY_BASE_URLhttps://api.twinmemory.dev
Timeouttimeout60 seconds
Retriesmax_retries2
Fail openfail_openFalse

For a stack on your machine, set TWIN_MEMORY_BASE_URL=http://localhost:8210.

Adding

add takes a string, an item dict, or a list of either. Keyword fields apply to every item that does not set its own.

dana.add(
    [
        {"text": "Can you book the same hotel as last time?", "role": "user"},
        {"text": "Booked the Memmo Alfama again for 3-5 May.", "role": "assistant"},
    ],
    session="trip-may",
)
dana.add("Moving house on 1 December.", kind="note", expires="2026-12-31")

The fields are those of Add: text (required), kind, id, session, role, author, title, about, at, expires and metadata. An add is accepted at once and searchable a moment later; wait=True returns once it is searchable. Lists longer than 500 items go in batches, and every add carries an Idempotency-Key, so a retried request never saves an item twice.

Searching

result = dana.search("What should I avoid cooking for Dana?")
result.known  # what is true now, as lines
result.facts  # dated facts, each with the passage it came from
result.hits  # whole passages, with their addresses
result.held  # False when memory says it does not hold the answer
result.context(max_chars=4000)

fast=True skips the model filter: free and quicker, a little less precise. before and after (ISO dates) and scope (a path prefix) narrow a search. See Search.

Everything else

MethodRoute
memory.me()Me
user.history(subject, attribute=None)History
user.latest(k=10, place="", after=, before=)Latest
user.grep(pattern, scope=None, limit=50)Grep
user.read(path) / user.ls(path)Read / Ls
user.around(passage_id, width=2)Around
user.overview()Overview
user.forget(items=, sessions=, threads=, events=)Forget
user.correct(record_id, replacement=None)Correct
user.versions(item_id)Versions
user.status(ids)Status
user.lineage(id)Lineage

Every result is a typed object; .raw holds the JSON as the API sent it.

Async

from twinmemory import AsyncMemory

async with AsyncMemory() as memory:
    dana = memory.user("dana")
    await dana.add("Prefers green tea.")
    result = await dana.search("What does Dana drink?", fast=True)

Errors

Every error is a TwinMemoryError with status, the API's detail, request_id, and outage: whether it means memory is unavailable rather than that the request or the key is wrong. See Errors.

ErrorWhen
AuthenticationError401: no key, or one the service did not issue
PermissionDeniedError403: the key reads, and the call writes
NotFoundError404: nothing was ever added for this end user
InvalidRequestError400, 413, 422: the request itself is wrong
RateLimitError429, after retries
ServiceUnavailableError503, after retries
APIConnectionErrorthe service could not be reached, after retries
APITimeoutErrorthe service did not answer in time, after retries

429 and 503 are retried twice, waiting as the Retry-After header says; connection errors the same way.

Failing open

Memory being down should cost a reply its memory, not the reply. With Memory(fail_open=True), an outage - memory unreachable, timing out, or answering 429 or 5xx after its retries - does not raise:

  • search answers an empty result with failed=True;
  • add saves nothing and answers failed=True;
  • each logs a warning on the twinmemory logger.

A wrong key or a wrong request (401, 403, 400) still raises: that is a mistake to fix, not an outage to ride out. Pair it with a short timeout where a reply cannot wait.

Memory(integration="langgraph") adds a word to the User-Agent, so the console's request logs show where requests came from.

LangGraph

uvx twinmemory-cli init langgraph writes memory into a LangGraph project as code you own, using this client. See LangGraph.

Until it is on PyPI

The API is plain HTTP and JSON, so a few lines over requests do the same job now:

twin.py
import os
import requests

BASE = "https://api.twinmemory.dev/v1/memory"


def twin(user: str, route: str, body: dict) -> dict | None:
    r = requests.post(
        f"{BASE}/{route}",
        json=body,
        timeout=60,
        headers={
            "Authorization": f"Bearer {os.environ['TWIN_MEMORY_API_KEY']}",
            "X-Twin-Account": user,
        },
    )
    if r.status_code == 404 and route != "correct":
        return None  # nothing was ever written for this end user
    r.raise_for_status()
    return r.json()


twin("dana", "add", {"items": [{"text": "I moved to Lisbon.", "role": "user"}]})
brief = twin("dana", "search", {"query": "Where does Dana live?"})

Every route has the same shape: a POST to /v1/memory/<name> with a JSON body. See the API reference.

On this page