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)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| memorysync-1.10.0.tar.gz | 47.4 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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.