Skip to main content

memorysync

Official Python client for the MemorySync API. Sync and async, no surprises.

pip install memorysync

Quick start

from memorysync import MemorySyncClient

ms = MemorySyncClient(
    api_key="...",
    base_url="https://api.memorysync.io",
    project_id="proj_xxxxxxxxxxxxxxxx",   # optional
    end_user_id="user_42",                 # optional
)

ms.add("User prefers dark mode.")

result = ms.query("ui preferences", k=5)
for m in result.memories:
    print(m.id, m.text)

Async usage

import asyncio
from memorysync import AsyncMemorySyncClient

async def main():
    async with AsyncMemorySyncClient(api_key="...", base_url="...") as ms:
        result = await ms.query("ui preferences", k=5)
        print(result.memories)

asyncio.run(main())

Use MemorySyncClient as a context manager when you want deterministic connection cleanup:

with MemorySyncClient(api_key="...", base_url="...") as ms:
    ms.add("...")

Configuration

Argument Required Description
api_key yes Sent as X-API-Key. Provision in your MemorySync dashboard.
base_url yes Deployment URL of your MemorySync instance.
project_id no Pin every request to a project (X-Project-ID). Format: proj_ + 16 hex chars.
end_user_id no Identify which of your users this client speaks for (X-End-User-ID).
timeout no Per-request timeout in seconds. Default 30.0.
transport no Inject a custom httpx transport (tests, retries, proxies).

end_user_id can also be passed per-call on add() to override the client default.

Methods

Every method maps 1:1 to a real HTTP route. The two clients share the same surface; only the call style differs (sync vs await).

Method Route
add(text, **opts) POST /memory/add
bulk_add(items) POST /memory/bulk-add
query(query, *, k=None, ...) POST /memory/query
get(memory_id) GET /memory/{id}
update(memory_id, **fields) PATCH /memory/{id}
forget(memory_ids, *, reason=None) DELETE /memory/forget
summarize(memory_ids, *, lossless=False) POST /memory/summarize
compose(prompt_template, *, recall_k=None) POST /memory/compose
export_all() GET /memory/export
create_relation(from_id, **opts) POST /memory/{id}/relations

add returns one of two shapes

add() runs through MemorySync's extraction pipeline, so input that carries no high-value content is intentionally skipped. Branch on the type of the result:

from memorysync import AddSkippedResponse, Memory

result = ms.add("User prefers dark mode.")
if isinstance(result, AddSkippedResponse):
    print("skipped:", result.reason)
else:
    assert isinstance(result, Memory)
    print(result.id, result.text)

Conversation turns

add_turn() sends one conversation turn to fact extraction. Only the durable facts in a user turn are stored, as memories with their own ids; the turn text itself is not stored, so memory_id in the response is always None. A user turn sent to extraction counts as one add. Assistant, system and tool turns store nothing and are not counted, so when you capture a conversation, sending only the user turns saves the round trips.

receipt = ms.add_turn(
    tenant_id="acme",
    user_id="user_42",
    text="I always fly with the window seat.",
    role="user",              # "user" | "assistant" | "system" | "tool"
    session_id="thread-42",   # copied onto the extracted facts
)
receipt["status"]             # "processing" when extraction started, else "ok"
receipt["processing_status"]  # "distilling", "skipped_non_user_turn", "skipped_low_value", ...
receipt["request_id"]         # correlates the extraction job

Sending the same speaker, occurred_at and text for the same user again within 30 days is recognised: the response has already_exists: True and nothing is extracted twice.

Conversation history

A transcript your agent reads back, such as chat history, has its own store. Turns are kept as given and are never memories: they do not show up in memory lists, search or recall. With extract=True (the default) the user turns are also sent to fact extraction.

ms.append_history(
    tenant_id="acme",
    user_id="user_42",
    session_id="thread-42",
    source="my-app",
    turns=[
        {"role": "user", "content": "I prefer window seats.", "seq": 0},
        {"role": "assistant", "content": "Noted.", "seq": 1},
    ],
)

history = ms.list_history(tenant_id="acme", user_id="user_42", session_id="thread-42")
for turn in history["items"]:  # oldest first
    print(turn["role"], turn["content"])

ms.delete_history(tenant_id="acme", user_id="user_42", session_id="thread-42")

A retried turn with the same seq, role and content, or the same idempotency_key, is not stored twice. delete_history takes exactly one of session_id or ids; delete_derived_facts=True also deletes the facts extracted from those turns.

Framework state

Namespaced key/value state a framework keeps between runs, such as a store item or a saved record. Values are never memories either.

ns = ["memories", "user_42"]
ms.put_state(tenant_id="acme", user_id="user_42", namespace=ns, key="diet",
             value={"diet": "vegan"}, source="my-app")
item = ms.get_state(tenant_id="acme", user_id="user_42", namespace=ns,
                    key="diet", source="my-app")["item"]
hits = ms.search_state(tenant_id="acme", user_id="user_42", query="what does the user eat",
                       namespace_prefix=["memories"], source="my-app")
ms.delete_state(tenant_id="acme", user_id="user_42", namespace=ns, keys=["diet"],
                source="my-app")

A put replaces the key's previous value, and putting the identical value again returns created: False. Omitting keys on delete_state deletes the whole namespace. Rows are scoped by source, and the state reads default to "api", so pass the same source on every call.

Method Route
add_turn(*, tenant_id, user_id, text, role=None, ...) POST /v1/memory/add_turn
append_history(*, tenant_id, user_id, session_id, turns, ...) POST /v1/history/append
list_history(*, tenant_id, user_id, session_id, ...) POST /v1/history/list
delete_history(*, tenant_id, user_id, session_id=None, ids=None, ...) POST /v1/history/delete
put_state(*, tenant_id, user_id, namespace, key, value, ...) POST /v1/state/put
get_state(*, tenant_id, user_id, namespace, key, ...) POST /v1/state/get
list_state(*, tenant_id, user_id, namespace_prefix=None, ...) POST /v1/state/list
search_state(*, tenant_id, user_id, query, ...) POST /v1/state/search
delete_state(*, tenant_id, user_id, namespace, keys=None, ...) POST /v1/state/delete
list_state_namespaces(*, tenant_id, user_id, ...) POST /v1/state/namespaces

These return the server's JSON as a dict. An append counts one add for each user turn it sends to fact extraction, or one add when it stores new turns and extracts none; a put counts one add when it stores a new value. A retry that stores nothing new is not counted, reads count one retrieval, and deletes are free. Over the plan limit they return normally and store nothing: writes answer status: "skipped" and reads come back empty.

Control-plane client

Dashboard and administrative routes use bearer authentication, not the memory client's API key. Tokens are never persisted or refreshed automatically. Responses are typed dictionaries with snake_case keys, including routes whose wire response uses camelCase.

import os
from memorysync import ControlPlaneClient

with ControlPlaneClient(
    "https://api.memorysync.io",
    access_token=os.environ["MEMORYSYNC_ACCESS_TOKEN"],
    project_id="project_abc123",  # optional X-Project-ID default
) as control:
    members = control.list_team_members()
    hooks = control.list_webhooks()

# Login is the only operation that does not require a configured token.
with ControlPlaneClient("https://api.memorysync.io") as control:
    login = control.login("developer@example.com", os.environ["MEMORYSYNC_PASSWORD"])

AsyncControlPlaneClient exposes the same methods with await and aclose(). Both clients accept base_url, optional access_token, optional project_id, timeout, and an injectable sync/async httpx transport. Project-scoped operations accept a per-call project_id override.

Method HTTP route
bulk_revoke_api_keys POST /org/api-keys/bulk-revoke
test_api_key POST /org/api-keys/{key_id}/test
login POST /auth/login
get_current_plan GET /org/billing/current-plan
list_team_members GET /admin/team/members
suspend_team_member PATCH /admin/team/members/{member_id}
remove_team_member DELETE /admin/team/members/{member_id}
list_sessions GET /auth/sessions
revoke_session POST /auth/sessions/{session_id}/revoke
list_audit_events GET /admin/audit-logs
list_integrations GET /api/v1/integrations/catalog
create_organization POST /organizations
list_organizations GET /organizations
list_organization_members delegates to list_team_members
get_organization_settings GET /admin/tenant-settings
list_projects GET /org/projects
create_webhook POST /org/webhooks
list_webhooks GET /org/webhooks
update_webhook PATCH /org/webhooks/{endpoint_id}
delete_webhook DELETE /org/webhooks/{endpoint_id}
test_webhook POST /org/webhooks/{endpoint_id}/test
replay_webhook_deliveries POST /org/webhooks/{endpoint_id}/replay
list_webhook_deliveries GET /org/webhooks/{endpoint_id}/deliveries

Errors

Every non-2xx response raises a typed subclass of MemorySyncError:

Class When
AuthError 401 / 403 — bad key, missing scope.
ValidationError 400 / 409 / 422.
NotFoundError 404 — record not visible to the caller.
RateLimitError 429 — read err.retry_after_seconds.
ServerError 5xx.
MemorySyncError Network errors, timeouts, anything else.

Every error carries status_code, response, and the server-issued request_id (when present) for support escalation.

import time
from memorysync import RateLimitError

try:
    ms.add("...")
except RateLimitError as e:
    time.sleep(e.retry_after_seconds)

License

MIT

Metadata

Release files for memorysync 1.10.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for memorysync 1.10.0
File Size Uploaded Provenance
memorysync-1.10.0.tar.gz 47.4 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for memorysync 1.10.0
File Interpreter ABI Platform
memorysync-1.10.0-py3-none-any.whl Python 3 none any Details

Total release size: 98.3 kB

Release files / memorysync-1.10.0.tar.gz

Download URL memorysync-1.10.0.tar.gz
Size 47.4 kB
Tags Source
SHA-256 checksum
How to use checksums
86149481727d91e93c741d7003a352b24265e4a5638409db12a5e9fe4b206960
BLAKE2b-256 checksum
How to use checksums
4e19d2e535b7ffd381efe13cadf85d5b03e8f74dc1a9118c363d8835286c6de4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.5

Release files / memorysync-1.10.0-py3-none-any.whl

Download URL memorysync-1.10.0-py3-none-any.whl
Size 50.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
79a304daac2e394024274c2464b175de5c0bec12250e8b51e49e98b1e3127e43
BLAKE2b-256 checksum
How to use checksums
15fc53bd3b728a165ff076373e8e46ef0e457c429c69465983a9223c17c7c6b4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.5

Security for memorysync 1.10.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

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