Metadata-Version: 2.5
Name: memorysync
Version: 1.10.1
Summary: Official Python client for the MemorySync API.
Project-URL: Homepage, https://memorysync.io
Project-URL: Documentation, https://docs.memorysync.io
Project-URL: API Reference, https://docs.memorysync.io/api/overview
Project-URL: Quickstart, https://docs.memorysync.io/sdks/python/overview
Project-URL: Changelog, https://docs.memorysync.io/release-notes
Project-URL: Support, https://docs.memorysync.io/debugging/support
Project-URL: Status, https://status.memorysync.io
Author: MemorySync
License: MIT
License-File: LICENSE
Keywords: agent-memory,ai,ai-agents,client,context,llm,long-term-memory,memory,memorysync,rag,retrieval,sdk
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: httpx<1.0,>=0.25
Requires-Dist: typing-extensions>=4.5; python_version < '3.11'
Description-Content-Type: text/markdown

# memorysync

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

```bash
pip install memorysync
```

## Quick start

```python
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

```python
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:

```python
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:

```python
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`.
Every call counts as one add, whatever it stores. Assistant, system and tool
turns store nothing but still count, so when you capture a conversation, send
only the user turns.

```python
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.

```python
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.

```python
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, however
many turns it carries, and a put counts one add; a retry of something already
stored is not counted again. Reads count one retrieval, whatever they return,
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.

```python
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.

```python
import time
from memorysync import RateLimitError

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

## License

MIT
