# CLI command reference Source: https://docs.maximem.ai/cli/commands Every maximem-synap command and flag — auth, instances, api-keys, client, requests, memories, and config. All commands operate on the **client your session is bound to** — you don't pass a client id. Run any command with `--help` for its full options. ## Global flags These work on every command: | Flag | Description | | ----------------------------------- | ----------------------------------------------------------------- | | `--output`, `-o` `table\|json\|csv` | Output format. `table` on a terminal, `json`/`csv` for scripting. | | `--profile`, `-p` `` | Use a specific account/profile. | | `--env` `prod\|staging` | Target environment (default: prod). | | `--api-base` `` | Override the server URL (for local development). | | `--quiet` | Suppress success/info chatter (errors and data still print). | | `--verbose`, `-v` | Echo HTTP requests to stderr. | | `--version` | Print the CLI version. | Global flags may be placed before or after the command. Destructive commands accept `--yes` / `-y` to skip the confirmation prompt. ### Environment variables | Variable | Effect | | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- | | `SYNAP_API_KEY` | Authenticate with this key directly — no login, no config file. The zero-config path for CI, servers, and containers. Overrides any stored profile. | | `SYNAP_PROFILE` | Default profile to use (same as `--profile`). | | `SYNAP_API_BASE` / `SYNAP_ENV` | Default server / environment. | ## auth Log in, log out, and switch accounts. | Command | Description | | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `auth login [--profile ] [--no-browser]` | Browser device-pairing login. Creates or refreshes the profile's session. | | `auth activate --key ` | Log in with an existing API key — no browser (CI/servers). For ephemeral CI, prefer the `SYNAP_API_KEY` env var (no file written). | | `auth refresh` | Renew the active key before it expires — mints a fresh key (same scopes) and revokes the old one. Must run while the key is still valid. | | `auth logout` | Revoke the active key server-side and remove it locally. | | `auth whoami` *(alias `maximem-synap whoami`)* | Show client, scopes, key expiry, and profile. | | `auth list` | List all logged-in accounts (active one starred). | | `auth use ` | Set the active account. | | `auth print-access-token` | Print the active key (for scripts/CI). | ```bash theme={null} maximem-synap auth login maximem-synap whoami maximem-synap auth use acme ``` ## instances Provision and manage memory instances. | Command | Description | | -------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `instances list [--status ] [--page ] [--page-size ]` | List instances with status, config state, and memory counts. | | `instances get ` | Show one instance in detail. | | `instances create --name --relationship [--agent-type ] [--description ] [--metadata ] [--from-file ]` | Create an instance. `--relationship` is `b2c \| b2b \| internal \| agent_to_agent`. `--metadata` takes a JSON object; `--from-file` seeds the use-case from a markdown file. | | `instances update [--name ] [--description ] [--metadata ]` | Rename or edit an instance. `--metadata` replaces metadata with a JSON object. | | `instances promote --target-name [--copy-mode ]` | Create a production sibling. `--copy-mode` is `config_only \| config_and_memories`. | | `instances open [] [--no-browser]` | Open the instance (or the instances list) in the dashboard. | | `instances delete [--yes]` | Delete an instance (typed-name confirmation unless `--yes`). | ```bash theme={null} maximem-synap instances list maximem-synap instances create --name "Support Agent" --relationship b2c maximem-synap instances get inst_a1b2c3d4 ``` A freshly created instance generates its memory architecture (MACA) in the background and stays **pending** until approved in the [Dashboard](/dashboard/overview). `instances promote` requires an approved config. ## api-keys Manage the runtime API keys your agents use with the SDK. | Command | Description | | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | `api-keys list [--instance ]` | List keys (metadata only — never the secret). Omit `--instance` to list every key in your client. | | `api-keys create --instance [--label ]` | Mint a key. The secret is shown **once**. | | `api-keys get ` | Show metadata for one key. | | `api-keys rotate [--label ] [--yes]` | Mint a replacement key for the same instance, then revoke the old one. | | `api-keys revoke [--yes]` | Revoke a key immediately. | ```bash theme={null} maximem-synap api-keys create --instance inst_a1b2c3d4 --label prod maximem-synap api-keys list --instance inst_a1b2c3d4 maximem-synap api-keys revoke ``` The plaintext key is returned only at creation. Copy it immediately and store it as `SYNAP_API_KEY` in your agent's environment. ## client Inspect your client and manage its team. | Command | Description | | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- | | `client get` | Show the client your session is bound to. | | `client members list` | List members and their roles. | | `client members invite --email [--role ]` | Invite a member by email. `--role` is `owner \| admin \| member`. The invitee does not need a Synap account yet. | | `client members update --role ` | Change a member's role. | | `client members remove [--yes]` | Remove a member. | ```bash theme={null} maximem-synap client get maximem-synap client members list maximem-synap client members invite --email dev@acme.com --role member ``` An invite is **pending** until the invitee accepts it. They receive an email; if they don't have a Synap account yet, the invite is applied when they sign up with that address. Pending invites show in both `client members list` and the Dashboard's team list with state `pending`. An email can belong to only one client organization. ## requests Inspect request-level SDK activity — the same data as the Dashboard's Requests view. | Command | Description | | ---------------------------------- | ---------------------------------------------------------------------- | | `requests list [filters]` | List SDK requests (add / search / get\_all / delete) in a time window. | | `requests events ` | Show the memory actions a single request produced. | Filters for `requests list`: | Flag | Description | | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | `--since`, `-s` `` | `1h \| 6h \| 12h \| 1d \| 7d \| 14d \| 30d \| 90d \| all` (default `1d`). Or use `--start`/`--end` for a custom range. | | `--instance`, `-i` `` | Filter to one instance. | | `--type` `add\|search\|get_all\|delete` | Operation type. | | `--status` `succeeded\|failed\|processing` | Outcome. | | `--user`, `--agent`, `--app`, `--run`, `--request-id` | Filter by the corresponding identifier. | | `--has-results` | Only requests that returned results. | | `--sort` `time\|latency:asc\|desc` | Sort order (e.g. `latency:desc`). | | `--watch`, `-w` | Stream new requests as they arrive (Ctrl-C to stop). `--interval ` sets the poll cadence. | | `--page` / `--limit` | Pagination. | ```bash theme={null} maximem-synap requests list --since 1h maximem-synap requests list --since 7d --type search --status failed --sort latency:desc maximem-synap requests list --watch # live tail while you debug an integration ``` ## memories Inspect stored memories (read-only — writes happen through the SDK at runtime). | Command | Description | | ------------------------------ | ------------------------------------------------------------------- | | `memories list [filters]` | List memories with the Dashboard's filter set. | | `memories get ` | Show one memory in detail. | | `memories history ` | Show a memory's action changelog. | | `memories lineage ` | Show a memory's immediate lineage (enriched/superseded neighbours). | Filters for `memories list`: | Flag | Description | | -------------------------- | ------------------------------------------------------------------------------------------------ | | `--instance`, `-i` `` | Filter to one instance. | | `--search`, `-q` `` | Full-text search on content. | | `--type` `` | `facts \| preferences \| episodes \| emotions \| temporal_events`. Plural — `facts`, not `fact`. | | `--scope` `` | `client \| customer \| user`. | | `--entity` `` | Filter by entity. | | `--page` / `--limit` | Pagination. | ```bash theme={null} maximem-synap memories list --instance inst_a1b2c3d4 maximem-synap memories list --type preferences --search "window seat" ``` ## config Local CLI settings for the active profile. | Command | Description | | -------------------------- | ----------------------------------- | | `config get` | Show the active profile's settings. | | `config set ` | Set a setting (e.g. `api_base`). | | `config path` | Print the path of the config file. | ```bash theme={null} maximem-synap config get maximem-synap config path ``` ## usage Aggregate usage for your client — API calls, LLM tokens, memories stored. | Command | Description | | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------- | | `usage [--period

]` | Usage summary. `--period` is `all \| 1d \| 7d \| 30d \| 90d \| custom` (default `30d`); use `--start`/`--end` for a custom range. | ```bash theme={null} maximem-synap usage --period 7d maximem-synap usage --start 2026-01-01 --end 2026-01-31 ``` ## doctor Diagnose your setup — CLI version, Python, config, auth, server connectivity, and key expiry. ```bash theme={null} maximem-synap doctor ``` ## Use in CI No browser is available in CI, so authenticate with a key instead of `auth login`: ```bash theme={null} export SYNAP_API_KEY=synap_... # from `api-keys create` maximem-synap instances list --output json ``` `SYNAP_API_KEY` overrides any stored profile and writes nothing to disk, so it's safe for ephemeral runners. # Synap CLI Source: https://docs.maximem.ai/cli/overview Provision and operate Synap from your terminal with maximem-synap, the command-line counterpart to the Dashboard. The **`maximem-synap` CLI** lets you provision and operate Synap from your terminal: log in, create and manage instances, mint API keys, manage your team, and inspect requests and memories: the same operations as the [Dashboard](/dashboard/overview), scriptable. **CLI vs SDK.** The [SDK](/setup/installation) is the *runtime* surface: your agents import it to read and write memory (`recall`, `save`, `context`). The CLI is the *control plane*: you use it to set Synap up and operate it. They are separate tools for separate jobs. ## Install and log in The CLI ships on npm as a small bootstrapper that installs the tool onto your `PATH`. The fastest start is a single command: ```bash theme={null} npx @maximem/synap-cli@latest auth login ``` This installs the CLI (if it isn't already) and starts the browser login. After the first run, the `maximem-synap` command is on your `PATH`: ```bash theme={null} maximem-synap auth login maximem-synap whoami ``` Requires **Node.js 18+** and **Python 3.10+** on the host. The CLI is dependency-light and stores its config under `~/.config/synap/config.json` (mode `0600`). ## Logging in Authentication is a browser device-pairing flow, like `gcloud auth login`. You never paste a key; your identity stays in the browser. ```bash theme={null} maximem-synap auth login ``` The CLI prints a short pairing code and opens your browser: ``` Your pairing code is: XYZA-BCDE Approve this login at: https://synap.maximem.ai/cli/pair?code=XYZA-BCDE Waiting for approval… ``` On the [Synap dashboard](https://synap.maximem.ai) pair page, confirm the code matches your terminal, pick the client to connect to, and click **Approve**. If you aren't signed in, you'll be prompted first. The terminal confirms the login and stores the session under your active profile: ``` Logged in to client "Acme" · profile "default" ``` The minted key is bound to one client and expires after 90 days. Re-run `maximem-synap auth login` to refresh it. ### Which account am I? ```bash theme={null} maximem-synap whoami # client, scopes, key expiry, profile maximem-synap auth list # all logged-in accounts (active one starred) ``` ### Multiple accounts Each profile holds one session bound to one client. Log into different clients under different profiles and switch between them: ```bash theme={null} maximem-synap auth login --profile acme # log into Acme maximem-synap auth login --profile internal # log into another client maximem-synap auth use acme # switch the active profile ``` ## A quick tour ```bash theme={null} maximem-synap instances list # your memory instances maximem-synap instances create --name "Support" --relationship b2c maximem-synap api-keys create --instance --label prod # an SDK key, shown once maximem-synap requests list --since 1h # what happened in the last hour maximem-synap memories list --instance # inspect stored memories maximem-synap client members list # your team ``` Add `--output json` to any command for scripting, and `--help` to any command for its options. Global flags (`--output`, `--profile`, `--env`, `--api-base`, `--verbose`) work before or after the command. An API key minted with `api-keys create` is shown **once** and never again. Copy it immediately. This is the key your agents put in the SDK (`SYNAP_API_KEY`). ## Logging out ```bash theme={null} maximem-synap auth logout # revokes the key server-side and removes it locally ``` ## Next steps Every command and flag: auth, instances, api-keys, client, requests, memories, config. The same operations in the web UI. # Agent Topologies Source: https://docs.maximem.ai/concepts/agent-topologies How memory works across agent topologies: the runtime loop an agent runs on Synap, a single agent with its own MACA, and multiple agents that share memory across scopes. Every memory-enabled agent follows the same runtime loop: it reports what happened, retrieves relevant context, and generates a response. What changes between deployments is the *topology*: whether one agent serves your users, or several specialized agents share what they learn about the same people. This page covers all three together. Start with the runtime loop (it is identical regardless of topology), then read the single-agent and multi-agent sections to decide how many [Instances](/concepts/memory-scopes#clients-and-instances), [scopes](/concepts/memory-scopes), and [Memory Architecture Configurations (MACAs)](/concepts/memory-architecture) your deployment needs. ## Agent interactions The core pattern for a live agent is a four-phase cycle: **Report, Retrieve, Generate, Report**. The agent tells Synap what just happened on a long-lived stream, and Synap handles both sides of memory: pushing anticipated context down before the agent asks, and forming long-term memory from the reported turns when the conversation compacts. This loop is the same whether you run one agent or many. Only the scope IDs you address change. Agent runtime loop: sdk.initialize() initializes the SDK, then sdk.instance.listen() starts listening once at startup, then a repeating per-turn block of send_message(user_message), fetch() served from cache, your LLM, and send_message(assistant_message), and finally sdk.instance.stop_listening() on shutdown There is no ingestion call in this loop. Reported turns become long-term memory when the conversation compacts, at 3,000 tokens, 10 messages, or 5 minutes of inactivity. See [Agent Integration](/setup/agent-integration) for the full walkthrough, and [Retrieve, generate, ingest](#retrieve-generate-ingest) below for the request-response alternative. Your application receives a message from the user through whatever channel you support: a chat widget, API endpoint, mobile app, voice interface, or other integration. Before calling the LLM, the agent queries Synap for memories relevant to the current message. This retrieval considers the user's history, their organization's shared knowledge, and any client-scoped information. `conversation_id` must be a valid UUID. Generate one with `str(uuid.uuid4())` and reuse the same value for every turn in the conversation. ```python Python theme={null} import uuid conversation_id = str(uuid.uuid4()) # one UUID per conversation, reused across turns # Report the turn first: it registers the conversation, and tells Synap # what the agent is doing so it can anticipate the next retrieval. await sdk.instance.send_message( content=user_message, role="user", event_type="user_message", conversation_id=conversation_id, user_id="user_123", customer_id="acme_corp", ) context = await sdk.conversation.context.fetch( conversation_id=conversation_id, user_id="user_123", customer_id="acme_corp", search_query=[user_message], mode="fast" ) ``` ```javascript JavaScript theme={null} import { randomUUID } from 'node:crypto'; let conversation_id = randomUUID(); // one UUID per conversation, reused across turns // Report the turn first: it registers the conversation, and tells Synap // what the agent is doing so it can anticipate the next retrieval. await sdk.instance.send_message({ content: user_message, role: 'user', event_type: 'user_message', conversation_id: conversation_id, user_id: 'user_123', customer_id: 'acme_corp', }); const context = await sdk.conversation.context.fetch({ conversation_id: conversation_id, user_id: 'user_123', customer_id: 'acme_corp', search_query: [user_message], mode: 'fast', }); ``` ```typescript TypeScript theme={null} import { randomUUID } from 'node:crypto'; let conversation_id = randomUUID(); // one UUID per conversation, reused across turns // Report the turn first: it registers the conversation, and tells Synap // what the agent is doing so it can anticipate the next retrieval. await sdk.instance.send_message({ content: user_message, role: 'user', event_type: 'user_message', conversation_id: conversation_id, user_id: 'user_123', customer_id: 'acme_corp', }); const context = await sdk.conversation.context.fetch({ conversation_id: conversation_id, user_id: 'user_123', customer_id: 'acme_corp', search_query: [user_message], mode: 'fast', }); ``` The agent assembles the full prompt for the LLM: a system prompt, the retrieved memories as context, the recent conversation history, and the current user message. The retrieved context bridges the gap between what the LLM knows (nothing about this user) and what it needs to know. The agent calls the LLM (Anthropic, OpenAI, or any provider) with the assembled prompt. The LLM generates a response that is informed by the user's history and organizational context. The generated response is sent back to the user through your application's interface. After the response is delivered, the agent reports it on the stream. This completes the turn in conversation history and is what pre-warms anticipation for the next turn, so it belongs after the LLM call rather than before. ```python Python theme={null} await sdk.instance.send_message( content=assistant_response, role="assistant", event_type="assistant_message", conversation_id=conversation_id, user_id="user_123", customer_id="acme_corp", ) ``` ```typescript TypeScript theme={null} await sdk.instance.send_message(options: SendMessageOptions) ``` Nothing else is required. Both reported turns enter the long-term ingestion pipeline when this conversation compacts. When the user sends the next message, the cycle begins again. This time the retrieval step often resolves from the anticipation cache in about a millisecond, because Synap pushed a bundle down the stream between turns. Once the conversation compacts, earlier turns are also available as long-term memories, creating a continuously improving feedback loop. ### Injecting retrieved context into the prompt The most critical part of the integration is how you structure the retrieved context within your LLM prompt. The retrieved memories need to be clearly separated from the system instructions and the conversation history so the LLM can use them effectively. ``` [System instructions] - Your agent's persona, capabilities, and behavioral guidelines [Retrieved context from Synap] - Relevant facts, preferences, and historical context - Clearly labeled as "context from memory" [Conversation history] - Recent messages in the current session [Current user message] - The message being responded to ``` ```python theme={null} def build_prompt(system_instructions: str, context, conversation_history: list, user_message: str): """Build the full prompt with retrieved memories injected.""" messages = [ { "role": "system", "content": ( f"{system_instructions}\n\n" "## Context from memory\n" "The following information has been retrieved from previous conversations " "and documents. Use it to personalize your response and maintain continuity " "across interactions. If the context is not relevant to the current question, " "do not force it into your response.\n\n" f"{context.formatted_context}" ) } ] for msg in conversation_history: messages.append({"role": msg["role"], "content": msg["content"]}) messages.append({"role": "user", "content": user_message}) return messages ``` Include a brief instruction telling the LLM how to use the retrieved context. Phrases like "Use this to personalize your response" and "If the context is not relevant, do not force it" help the LLM apply memories appropriately without hallucinating connections. ### Choosing a retrieval mode Retrieval is on the critical path of your agent's response time: the user is waiting while your agent fetches context. The mode you pick trades latency for retrieval depth. | Mode | Search method | Best for | | ---------- | ------------------------------------------------------- | --------------------------------------------- | | `fast` | Vector + graph, no LLM query decomposition | Real-time conversations, single-topic queries | | `accurate` | Vector + graph + LLM subquery decomposition + reranking | Complex queries, relationship-aware context | For most real-time conversational agents, use `fast` mode: it returns quickly and adds minimal overhead. Reserve `accurate` mode for cases where retrieval quality matters more than speed, such as end-of-day summaries or complex analytical questions. See [Retrieval Modes](/concepts/retrieval-modes) for the full comparison. ```python Python theme={null} # Fast retrieval for real-time chat (recommended default) context = await sdk.conversation.context.fetch( conversation_id=conversation_id, user_id=user_id, customer_id=customer_id, search_query=[user_message], mode="fast" ) # Accurate retrieval for complex queries context = await sdk.conversation.context.fetch( conversation_id=conversation_id, user_id=user_id, customer_id=customer_id, search_query=["Summarize everything we know about Project Atlas, including all team members and key decisions"], mode="accurate" ) ``` ```javascript JavaScript theme={null} // Fast retrieval for real-time chat (recommended default) let context = await sdk.conversation.context.fetch({ conversation_id: conversation_id, user_id: user_id, customer_id: customer_id, search_query: [user_message], mode: 'fast', }); // Accurate retrieval for complex queries context = await sdk.conversation.context.fetch({ conversation_id: conversation_id, user_id: user_id, customer_id: customer_id, search_query: ['Summarize everything we know about Project Atlas, including all team members and key decisions'], mode: 'accurate', }); ``` ```typescript TypeScript theme={null} // Fast retrieval for real-time chat (recommended default) let context = await sdk.conversation.context.fetch({ conversation_id: conversation_id, user_id: user_id, customer_id: customer_id, search_query: [user_message], mode: 'fast', }); // Accurate retrieval for complex queries context = await sdk.conversation.context.fetch({ conversation_id: conversation_id, user_id: user_id, customer_id: customer_id, search_query: ['Summarize everything we know about Project Atlas, including all team members and key decisions'], mode: 'accurate', }); ``` ### Retrieve, generate, ingest Some agents cannot hold a stream open. A per-request serverless handler is the common case: the process ends with the response, so there is nothing to keep a connection alive. Those agents run the older three-phase cycle instead, replacing both `send_message` calls with an explicit ingestion at the end of the turn. Request-response agent loop: user sends a message, the agent retrieves context from Synap, generates a response with an LLM, sends the response to the user, ingests the conversation turn into Synap, and repeats Everything else on this page still applies. Scopes, MACAs, and the single- and multi-agent topologies are independent of which loop you run. Run one or the other for a given turn, not both. If you report a turn on the stream **and** ingest that same text, it is extracted twice: once by your call and once at compaction. See [What the stream does to memory](/concepts/real-time-anticipation#what-the-stream-does-to-memory). ### When to ingest explicitly If you are on the request-response loop above, or you are ingesting content that never travels the stream, these are the three common strategies and their tradeoffs: Ingest each conversation turn (user message + agent response) immediately after the response is delivered. This is the standard choice on the request-response loop. **Pros:** * Memories are available for retrieval within the same conversation session * No risk of data loss if the session ends unexpectedly * Fine-grained temporal resolution **Cons:** * Higher API call volume * Each turn is ingested independently, without full conversation context ```python Python theme={null} await sdk.memories.create( document=f"User: {user_message}\nAssistant: {response}", document_type="ai-chat-conversation", user_id=user_id, customer_id=customer_id, mode="fast" ) ``` ```typescript TypeScript theme={null} await sdk.memories.create(options: CreateMemoryOptions) ``` Accumulate the full conversation and ingest it as a single document when the session ends. **Pros:** * Fewer API calls * Full conversation context available for extraction: better entity resolution and relationship mapping * More efficient for long-range mode processing **Cons:** * Memories from this session are not available during the session itself * Risk of data loss if the session terminates unexpectedly (crash, timeout) * Requires session lifecycle management ```python theme={null} full_transcript = "\n".join( f"{'User' if msg['role'] == 'user' else 'Assistant'}: {msg['content']}" for msg in conversation_history ) await sdk.memories.create( document=full_transcript, document_type="ai-chat-conversation", user_id=user_id, customer_id=customer_id, mode="long-range" ) ``` Ingest each turn in fast mode for immediate availability, then ingest the full conversation in long-range mode at session end for deeper extraction. **Pros:** * Immediate availability of basic memories * Deep extraction from the full conversation context * Resilient to unexpected session termination **Cons:** * Higher API call volume and processing cost * Requires deduplication logic (use `document_id` to handle overlapping content) ```python Python theme={null} # During conversation: fast mode per turn await sdk.memories.create( document=f"User: {user_message}\nAssistant: {response}", document_type="ai-chat-conversation", document_id=f"turn_{session_id}_{turn_number}", user_id=user_id, customer_id=customer_id, mode="fast" ) # At conversation end: long-range mode for the full transcript await sdk.memories.create( document=full_transcript, document_type="ai-chat-conversation", document_id=f"session_{session_id}_full", user_id=user_id, customer_id=customer_id, mode="long-range" ) ``` ```typescript TypeScript theme={null} await sdk.memories.create(options: CreateMemoryOptions) ``` If your agent streams responses, ingest the conversation turn only after the full response has been assembled. Never ingest a partial or streaming response: the ingestion pipeline expects complete content to perform accurate entity extraction and relationship mapping, and partial content produces fragmented, low-quality memories. ### Best practices Even if you think the current query does not need historical context, always call retrieval. Synap's ranking ensures that irrelevant context is not returned, so the overhead is minimal. Skipping retrieval means your agent cannot benefit from accumulated memory. The LLM does not need to know how Synap works internally. Simply tell it to use the retrieved context naturally and to not fabricate memories. Over-engineering the memory instructions can cause the LLM to behave unnaturally. Log retrieval and generation latencies independently. If your agent feels slow, retrieval in fast mode is rarely the bottleneck. LLM generation is usually the dominant factor. Measuring both independently helps you optimize the right layer. When your application supports multi-turn conversations, pass a consistent `conversation_id` (a valid UUID) alongside the `user_id`. This helps Synap group related turns for better contextual understanding during retrieval. Your agent should still function if Synap is temporarily unreachable. Skip the retrieval step and generate a response without memory context. The user experience degrades (no personalization) but does not break entirely. ```python theme={null} try: context = await sdk.conversation.context.fetch(...) except Exception: context = None # Proceed without memory context ``` ## Single-agent memory Most Synap deployments are **single-agent**: one AI agent, backed by one [Instance](/concepts/memory-scopes#clients-and-instances) running one [MACA](/concepts/memory-architecture). All memory flows through that single Instance, and the people your agent serves are kept separate by [scopes](/concepts/memory-scopes), not by running additional Instances. If you have one agent with a single, coherent purpose, you want a single-agent architecture. You do not create a new Instance per user or per customer: scopes handle that isolation for you. A single-agent architecture has three fixed pieces: * **One Instance**: the deployed unit that owns the memory store and credentials. * **One MACA**: generated automatically from the [Use-Case Markdown](/concepts/memory-architecture#the-use-case-file) file you upload at creation. It governs what gets extracted, how it is stored, and how retrieval ranks results for *this* agent. * **Scopes for isolation**: every memory is written and read at a scope in the chain **User → Customer → Client**. Two different end-users on the same Instance never see each other's memories, because their memories live at different `user_id` scopes. The Instance is infrastructure; the scope chain is what isolates memory. You scale the number of people your agent serves by passing more `user_id` values (and `customer_id` values in B2B deployments), never by provisioning more Instances. ```python Python theme={null} from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK(api_key="synap_api_key") # Same Instance, different people: isolated by scope, not by Instance await sdk.memories.create( document="User: I prefer concise answers and dark mode.", document_type="ai-chat-conversation", user_id="user_alice", customer_id="acme_corp", ) await sdk.memories.create( document="User: Always include code examples in responses.", document_type="ai-chat-conversation", user_id="user_bob", customer_id="acme_corp", ) # Alice's retrieval never surfaces Bob's preferences context = await sdk.user.context.fetch( user_id="user_alice", customer_id="acme_corp", ) ``` ```javascript JavaScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_api_key' }); // Same Instance, different people: isolated by scope, not by Instance await sdk.memories.create({ document: 'User: I prefer concise answers and dark mode.', document_type: 'ai-chat-conversation', user_id: 'user_alice', customer_id: 'acme_corp', }); await sdk.memories.create({ document: 'User: Always include code examples in responses.', document_type: 'ai-chat-conversation', user_id: 'user_bob', customer_id: 'acme_corp', }); // Alice's retrieval never surfaces Bob's preferences const context = await sdk.user.context.fetch({ user_id: 'user_alice', customer_id: 'acme_corp', }); ``` ```typescript TypeScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_api_key' }); // Same Instance, different people: isolated by scope, not by Instance await sdk.memories.create({ document: 'User: I prefer concise answers and dark mode.', document_type: 'ai-chat-conversation', user_id: 'user_alice', customer_id: 'acme_corp', }); await sdk.memories.create({ document: 'User: Always include code examples in responses.', document_type: 'ai-chat-conversation', user_id: 'user_bob', customer_id: 'acme_corp', }); // Alice's retrieval never surfaces Bob's preferences const context = await sdk.user.context.fetch({ user_id: 'user_alice', customer_id: 'acme_corp', }); ``` ### How you map your world onto scopes The right way to map *your* world onto Synap's scopes depends on who your agent serves. Describe these roles in the [Role Descriptions](/concepts/memory-architecture#the-use-case-file) section of your use-case file, and Synap tunes the MACA's primary scope accordingly. One person is the Client, Customer, and User at once. The scope hierarchy collapses: effectively everything is User-scoped. Best for consumer (B2C) apps where each account is a single individual. One organization (Customer) with many individual Users. Shared organizational knowledge lives at Customer scope; personal context lives at User scope. Many Customers, each with many Users, all served by the same agent on one Instance. The most common SaaS shape. Customer scope isolates tenants; User scope isolates individuals within a tenant. All three are **single-agent**: they differ only in how you populate the scope chain, not in how many Instances or MACAs you run. ### Scaling a single agent A single-agent architecture scales to many users and customers without any change to its topology: * **More users or customers** never require more Instances. Pass new scope IDs and the chain isolates them automatically. * **Staging vs. production** is a separate concern. Use a distinct Instance per environment so test data never mixes with production memory, each is still a single-agent deployment. * **Behavior changes** (new task categories, new compliance rules) are made by re-uploading the use-case file, which regenerates the MACA. The topology stays the same. Separate Instances for staging and production is an *environment* split, not a multi-agent architecture. You still have one agent per Instance, each with its own MACA. Stay single-agent as long as your deployment is one agent with one purpose. Graduate to multi-agent when you run **specialized agents** that should share what they learn about the same users, when different agents need **materially different extraction behavior**, or when you are modeling a **team of agents** under one organizational umbrella. ## Multi-agent memory A **multi-agent** architecture runs more than one agent against Synap: for example a sales agent, a support agent, and an onboarding agent that together serve the same users. The central question is always the same: **which agents should share what they remember, and which should stay isolated?** ### The key rule: sharing is by scope, not by Instance An [Instance](/concepts/memory-scopes#clients-and-instances) is infrastructure: it is **not** a memory scope. What two agents share is decided entirely by the scope IDs they address: if they ingest and retrieve with the same `user_id` (and `customer_id` in B2B), they share that user's and customer's memories. The [scope chain](/concepts/memory-scopes) (**User → Customer → Client**) is the sharing boundary; the Instance boundary is the isolation boundary. Every pattern below is just a different combination of *which Instance* agents run on and *which scopes* they address. ### Three architecture patterns | Pattern | MACA | Memory sharing | Use when | | ---------------------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- | | **Shared Instance + shared scopes** | One MACA for all agents | Full, automatic sharing via the same scope IDs | Agents serve the same users and should each benefit from what the others learn | | **Separate Instance per agent** | A tailored MACA per agent role | Isolated by Instance; overlap only at Client scope if you write there | Agents have very different extraction needs, or must not see each other's memory | | **Hierarchical Instances (agent teams)** | Per-Instance MACA, organized under a parent | Organizational grouping via `parent_instance_id`; isolation still per-Instance | You are modeling a team of agents under one umbrella | #### Shared Instance + shared scopes All agents run on one Instance and address the same scopes. Because memory is keyed by scope, every agent automatically sees what the others have stored for that user or customer. This is how handoffs work: the receiving agent inherits the context the previous one built. This is the simplest and most common multi-agent setup. ```python Python theme={null} # Sales agent stores context for a user await sdk.memories.create( document="User: We're evaluating the enterprise plan for 500 seats.", document_type="ai-chat-conversation", user_id="user_123", customer_id="acme_corp", ) # Support agent (same Instance, same scopes) sees it automatically context = await sdk.user.context.fetch( user_id="user_123", customer_id="acme_corp", search_query=["enterprise plan and SSO"], ) # context surfaces the sales conversation without the user repeating it ``` ```typescript TypeScript theme={null} await sdk.memories.create(options: CreateMemoryOptions) ``` One MACA governs all the agents here, so they share both memory **and** extraction behavior. #### Separate Instance per agent Give each agent its own Instance, and therefore its own MACA generated from its own [Use-Case Markdown](/concepts/memory-architecture#the-use-case-file). A support agent can prioritize extracting error details and resolutions, while a sales agent prioritizes buying signals and account context. Memory is isolated per Instance: the agents do **not** automatically see each other's memories. Use this when agents have genuinely different jobs, or when policy requires their memories to stay separate. If two such agents still need a shared baseline (e.g. product knowledge), write that knowledge at **Client scope** in each Instance, or keep a dedicated knowledge Instance, but per-user context will not cross the Instance boundary. #### Hierarchical Instances (agent teams) When you have a team of specialized agents that belong together, model them as [hierarchical Instances](/concepts/memory-scopes#clients-and-instances): create a parent Instance and point each child's `parent_instance_id` at it. This is an **organizational** tool for grouping related deployments and sharing configuration patterns. Memory isolation is still enforced per-Instance. Hierarchy organizes your agents, it does not merge their memory. ### Per-agent vs. shared knowledge Once you have picked a pattern, decide deliberately what to share: * **To share across agents**, have them address the same scope IDs. User- and Customer-scoped memories then flow between them automatically. * **To isolate agents**, give them separate Instances: the Instance boundary prevents per-user memory from leaking between agents even if they happen to use the same IDs. * **For knowledge every agent should see**, write it at **Client scope** (no `user_id` / `customer_id`); it is visible to all retrievals within an Instance. There is exactly **one MACA per Instance**, and that single fact drives the build-vs-split decision: agents that should share memory **and** behave the same way belong on one Instance (one MACA); agents that need different extraction priorities, retention, or compliance handling need separate Instances (separate MACAs), and you accept that per-user memory will not automatically cross between them. ## Next steps Follow a single turn through retrieval, generation, and ingestion across the full system. Fast vs. accurate retrieval and when to use each. The Client, Customer, and User scopes that isolate and share memory. The MACA that Synap generates for each Instance. # Aliases Source: https://docs.maximem.ai/concepts/aliases When you protect a field type, Synap stores a stable placeholder called an alias instead of the value, and hands the real value back to your application on the way out. This page covers what an alias looks like, what your app actually receives, and why search still finds the memory. **What changes for your application: nothing, by default.** Your own API keys receive real values, so the text you read back is identical to what you read before you protected anything. Aliases are what sits on our side. You only see one if you deliberately choose a setting that keeps the value from your app too, or issue a key that is restricted on purpose. ## What an alias is An alias is a stable placeholder that stands in for one sensitive value. It looks like this: ```text theme={null} [[PERSON_AADHAAR_h2n7v5cx8m0d]] ``` Three parts: who it belongs to, what kind of value it is, and twelve characters that make it unique. A support engineer reading `PERSON_AADHAAR` knows whose Aadhaar number is missing without looking anything up, which is the point of putting the readable part first. The twelve characters leave out the ones people confuse when reading a string aloud or copying it from a screenshot, so there is no `0` or `O`, no `1` or `l` or `I`. The leading word groups the alias by what it is about: `PERSON`, `PAYMENT`, `HEALTH`, `PLACE`, `DEVICE`, `SECRET`, or `CUSTOM` for a field type you defined yourself. When a memory carries an alias, the sentence around it still reads: ```text theme={null} Confirmed the customer's Aadhaar [[PERSON_AADHAAR_h2n7v5cx8m0d]] before processing the refund. ``` The event survives. Only the value is gone. *** ## The same value always gets the same alias This is the property everything else depends on. Two documents mentioning the same phone number produce the same alias, so anything that compares memories still works: deduplication still merges duplicates, a correction still supersedes the thing it corrects, and the entity graph still sees one identity rather than two. Before matching, Synap puts the value into a canonical form, so different spellings of the same thing collapse to one alias: | Written as | Also written as | Same alias | | ----------------- | --------------- | ---------- | | `+91 98765 43210` | `9876543210` | Yes | | `Sarah` | `sarah` | Yes | An alias is shared at customer level, within one instance. Two users of the same customer who give the same phone number get the same alias, so a question that spans both their memories can still be answered. The same number under a different customer, or a different instance, gets a different alias, so nothing about one tenant's data can be inferred from another's. *** ## Why search still works This is the part that would break silently if it were not handled, so it is worth understanding. Your memory store holds `[[PERSON_PHONE_h2n7v5cx8m0d]]`, not `9876543210`. Someone searches for `9876543210`. Every layer of retrieval is now looking for characters that are not there, and finds nothing. The product looks like it forgot, and the reason has nothing to do with memory quality. So your query goes through the same detection step your content did. Anything in it that your policy stores as an alias is swapped for that same alias before retrieval runs, and the search then looks for exactly what was written down. This is a hard requirement in the design, not an optimisation. Three rules keep that safe: * **A query never creates an alias.** It only looks one up. Searching for a value Synap has never seen leaves the query alone, which is the right answer: no memory holds that value either. * **A query is rewritten only for field types you actually store as aliases.** If your policy keeps real values for a field type, the store holds the real value and rewriting would break the search it is meant to fix. * **A failed lookup never blocks a search.** The query goes through untouched, which is the same answer as "we have never seen this value". *** ## What your application receives On the way out, Synap puts real values back for exactly the field types the caller is entitled to, and leaves everything else as an alias. Entitlement is decided by two things, in this order: 1. **Your policy** sets the ceiling. It is the "Your app gets" column of the [settings table](/guides/pii-protection#the-six-settings). Choose **Protect at rest** and your app gets real values. Choose **Protect from everyone** and it gets aliases. 2. **The grant on the API key that made the call** can narrow that, and can never widen it. A key marked `masked` receives placeholders even for field types your policy would hand over. An alias your caller is not entitled to is left exactly as it is. That is the correct answer rather than an error: the text is readable, the memory still says what happened, and your application was told which fields it would get back. If you want to handle aliases explicitly, match on the shape: two square brackets, an uppercase label, an underscore, twelve lowercase alphanumeric characters, two closing brackets. Treat an alias as opaque, and never parse meaning out of the twelve characters. ### Restricted keys and caching If you use two keys with different grants against the same instance, the entitlement is part of what identifies a cached read, so a privileged fetch can never answer a restricted key's request. Values are put back after the cache is read, never before, so nothing cached holds a real value. *** ## Aliases and "do not store it" A field type set to **Do not store it** still gets an alias, so the same value can be recognised when it appears again and occurrences can be counted. What it does not get is anything the value can be recovered from. A reveal on it returns nothing, for anyone, and the memory keeps a short description of what was taken instead of the value. Field types on [the floor](/guides/pii-protection#the-floor) go further and get no alias at all, so there is nothing associated with them anywhere. *** ## What this costs you Reads pick up a small amount of extra work to look up and put values back, against a fetch that already takes far longer than that step. Ingestion picks up the detection pass, and ingestion is asynchronous, so you do not feel it in your request path. Every change to this feature is gated on the same benchmark suite Synap runs for retrieval quality, with a hard limit on how far any benchmark may move. ## Next steps The categories, the six settings, and the floor. Set a policy, and test it before approving. What deleting someone makes unreadable, and what survives. Will this break my app, and what happens to memories I already have. # Context, End to End Source: https://docs.maximem.ai/concepts/context-end-to-end What happens to a message after you send it, and what context you can read back. This page follows one message through ingestion, extraction, storage, and retrieval, then walks the four context layers (short-term, long-term, customer, and organizational) each with its own lifecycle, plus how long conversations are compacted. Every message your agent handles flows through the same arc: it is **ingested**, its meaning is **extracted** into structured memories, those memories are **stored** in the vector and graph engines, and they are **retrieved** to enrich the next turn. Around that arc sit four context layers, each with a different scope and lifespan, from the working memory of a single conversation to knowledge shared across your entire application. This page answers two questions in one place: *what happens to a message after I send it?* and *what context can I read back?* You should not need three tabs open to follow a conversation from the first user turn to the durable knowledge it leaves behind. Think of short-term context as working memory during a meeting (everything said so far) and long-term context as the takeaways that persist after the meeting ends. Customer and organizational context are the shared wikis and product brain that everyone in the room already knows. ## The lifecycle of a message A single turn moves through four stages. The first three (ingest, extract, store) run asynchronously after you record content; the fourth (retrieve) happens at the start of each turn to build the context your agent reasons over. You record the turn with `sdk.conversation.record_message()` and/or submit content for durable memory with `sdk.memories.create()` (or `sdk.memories.batch_create()` for bulk loads). The scope identifiers you pass (`user_id`, optional `customer_id`) determine where the resulting memories live. The content runs through a multi-stage pipeline: categorization, memory extraction (facts, preferences, episodes, emotions, temporal events), chunking, entity resolution, and organization. Each stage enriches the memory with metadata that improves retrieval later. For the full pipeline (stages, ingestion modes, document types, and the runtime vs bootstrap paths) see [How Ingestion Works](/concepts/how-ingestion-works). Processed memories are persisted in two complementary engines (a **vector store** for semantic similarity and a **graph store** for entity relationships) scoped to the right level (user, customer, client, or world). On the next turn, `context.fetch()` searches the applicable scopes, ranks the results, and returns the most relevant memories within your token budget. This retrieved context, plus the conversation's short-term history, is what your agent reasons over. ``` record_message / memories.create │ ingest ▼ multi-stage pipeline ──► vector store + graph store │ extract │ store ▼ ▼ structured memories context.fetch() ──► ranked context for the next turn │ retrieve ▼ your agent's prompt ``` `conversation_id` must be a valid UUID. Generate one with `str(uuid.uuid4())` and reuse the same value for every turn (and every compaction call) in the same conversation. ### Pre-warming the retrieve step The arc above is complete on its own. Optionally, you can make the **retrieve** stage resolve locally instead of over the network. [`instance.listen()`](/sdk-reference/instance/listen) opens a long-lived gRPC stream. Your app reports agent activity with `send_message()`, and Synap pushes anticipated context bundles down the stream between turns. The next `context.fetch()` then reads from an in-process cache, roughly a millisecond, falling through to the normal network path on a miss. ``` listen() stream │ anticipated bundles ▼ structured memories context.fetch() ──► ranked context for the next turn (cache hit ≈1ms, else network) ``` This changes *how fast* the retrieve stage returns, never *what* it returns. The stream does still feed the arc above, just on a delay: conversation turns sent over it are persisted, and when the conversation compacts, those raw turns re-enter the pipeline at **ingest** and become long-term memories. See [Real-Time Anticipation](/concepts/real-time-anticipation). ## The four context layers Context in Synap is organized into four layers. They are not separate systems: they are the same memories stored at different scopes, with different lifespans and read paths. | Layer | Scope | Identifier(s) | What it holds | How you read it | | ------------------------------- | ------------------- | ---------------------------------- | ----------------------------------------------------------------------- | ---------------------------------- | | **Short-term / conversational** | Single conversation | `conversation_id` | The running transcript of this session: turns, decisions, current state | `sdk.conversation.context.fetch()` | | **Long-term (user)** | One end user | `user_id` (+ `customer_id` on B2B) | Durable facts, preferences, episodes about a person | `sdk.user.context.fetch()` | | **Customer** | One tenant | `customer_id` | Policies, team structure, shared projects for a B2B organization | `sdk.customer.context.fetch()` | | **Organizational** | Your whole app | *(none)* | Product docs, announcements, domain knowledge for every user | `sdk.client.context.fetch()` | `customer_id` is **required on B2B (multi-tenant) instances** and **not accepted on B2C**, where a call carrying one is rejected with HTTP 400. The examples below use `user_id` and note where `customer_id` applies. Narrower scopes win. When the same fact exists at multiple levels, the user-scoped version takes priority over customer, which takes priority over client. See [Memory Scopes](/concepts/memory-scopes) for the full priority resolution rules. ## Short-term context Short-term context is the accumulated history of a single conversation: the questions asked, answers given, and decisions made so far. It is what lets your agent say "as I mentioned earlier..." without losing track of the thread. It lives only for the duration of the session. ### Registering the conversation Short-term context does not appear by magic. Each turn must be **registered** with `sdk.conversation.record_message()` (both the `user` and `assistant` roles) so Synap can build conversation-scoped context and feed compaction. ```python Python theme={null} import uuid from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK(api_key="synap_your_key_here") await sdk.initialize() # One UUID per conversation, reused across every turn conversation_id = str(uuid.uuid4()) await sdk.conversation.record_message( conversation_id=conversation_id, role="user", content="I prefer dark mode and concise answers.", user_id="user_alice", # customer_id="customer_acme", # B2B only; not accepted on B2C ) await sdk.conversation.record_message( conversation_id=conversation_id, role="assistant", content="Got it. I'll keep answers short and assume dark mode.", user_id="user_alice", ) ``` ```javascript JavaScript theme={null} import { randomUUID } from 'node:crypto'; import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); await sdk.initialize(); // One UUID per conversation, reused across every turn let conversation_id = randomUUID(); await sdk.conversation.record_message({ conversation_id: conversation_id, role: 'user', content: 'I prefer dark mode and concise answers.', user_id: 'user_alice', // customer_id="customer_acme", # B2B only; not accepted on B2C }); await sdk.conversation.record_message({ conversation_id: conversation_id, role: 'assistant', content: "Got it. I'll keep answers short and assume dark mode.", user_id: 'user_alice', }); ``` ```typescript TypeScript theme={null} import { randomUUID } from 'node:crypto'; import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); await sdk.initialize(); // One UUID per conversation, reused across every turn let conversation_id = randomUUID(); await sdk.conversation.record_message({ conversation_id: conversation_id, role: 'user', content: 'I prefer dark mode and concise answers.', user_id: 'user_alice', // customer_id="customer_acme", # B2B only; not accepted on B2C }); await sdk.conversation.record_message({ conversation_id: conversation_id, role: 'assistant', content: "Got it. I'll keep answers short and assume dark mode.", user_id: 'user_alice', }); ``` If a conversation is never registered with `record_message`, a later `conversation.context.fetch()` for that `conversation_id` returns **empty**: there is no transcript to draw on, and `memories_used` stays `0`. Registering each turn is what makes the conversation coherent on the next fetch. ### How the context grows A "turn" is a user message plus its assistant response. Each turn is appended to the running history, and your agent sees the full history on every subsequent turn: ``` Turn 1: User: "What's our current API rate limit?" Assistant: "Your current rate limit is 1,000 requests per minute." Turn 2: User: "Can we increase that for our enterprise plan?" Assistant: "Yes, enterprise plans support up to 10,000 req/min..." Turn 3: User: "What about burst handling?" Assistant: "Burst allowances provide a 2x multiplier..." ... ``` ### Why it can't grow forever Short-term context is bounded by three practical constraints, which is why compaction exists. Every LLM has a maximum context window. Filling it with raw conversation history leaves little room for retrieved long-term memories and system instructions. LLM cost scales with input tokens. Unbounded history makes every turn progressively more expensive. LLMs pay less attention to the middle of long contexts (the "lost in the middle" effect), so very long histories can actually degrade answer quality. At the start of each turn, long-term memories are retrieved to provide background, while the short-term transcript provides immediate continuity. The two paths converge in your prompt: ```python Python theme={null} context = await sdk.conversation.context.fetch( conversation_id=conversation_id, search_query=["migration timeline"], ) # context.facts / context.preferences / context.episodes hold the # long-term memories retrieved for this turn; the short-term transcript # supplies the in-session continuity. ``` ```javascript JavaScript theme={null} const context = await sdk.conversation.context.fetch({ conversation_id: conversation_id, search_query: ['migration timeline'], }); // context.facts / context.preferences / context.episodes hold the // long-term memories retrieved for this turn; the short-term transcript // supplies the in-session continuity. ``` ```typescript TypeScript theme={null} const context = await sdk.conversation.context.fetch({ conversation_id: conversation_id, search_query: ['migration timeline'], }); // context.facts / context.preferences / context.episodes hold the // long-term memories retrieved for this turn; the short-term transcript // supplies the in-session continuity. ``` When the transcript content has lasting value, you persist it to long-term memory with `sdk.memories.create()`: there is no explicit "end" call; you ingest what you want to remember whenever it is ready. That hands off to the long-term layer below. ## Long-term context Long-term context is the persistent knowledge layer: durable facts, preferences, and events that survive across sessions for days, weeks, or years. It is what gives your agent a memory that lasts: it knows Alice prefers concise summaries even if that was learned months ago. ### Lifecycle: from raw content to durable memory Content arrives via `sdk.memories.create()` (runtime, as conversations happen) or `sdk.memories.batch_create()` (bulk imports and backfills, see [Bootstrap Ingestion](/concepts/how-ingestion-works#bootstrap-ingestion)). At this point it is raw text with scope identifiers and an optional `document_id`. ```python Python theme={null} await sdk.memories.create( document="The customer prefers email communication over phone calls.", document_type="ai-chat-conversation", user_id="user_alice", # customer_id="customer_acme", # B2B only metadata={"source": "support_conversation"}, ) ``` ```typescript TypeScript theme={null} await sdk.memories.create(options: CreateMemoryOptions) ``` Raw text becomes structured, queryable memory through the same **categorization → extraction → chunking → entity resolution → organization** pipeline described in [How Ingestion Works](/concepts/how-ingestion-works). Extraction sorts content into the five [memory types](/concepts/memories-and-context#memory-types): facts, preferences, episodes, emotions, and temporal events. Entity resolution links mentions ("Alice," "Alice Chen," "A. Chen") to a single canonical entity in the [entity registry](/concepts/entity-resolution), creating the graph edges that power relationship queries. Memories land in both engines, each scoped immutably by the identifiers present at ingestion. Memory chunks are embedded for semantic similarity search: finding relevant memories even without shared keywords. Entity relationships are stored for traversal: "what do we know about this customer's team?" follows graph edges to connected memories. Scope is set by which identity fields are present, and cannot change after storage: | Identifiers at ingestion | Resulting scope | | ---------------------------------- | --------------- | | `user_id` (+ `customer_id` on B2B) | **USER** | | `customer_id` only | **CUSTOMER** | | neither | **CLIENT** | When the agent needs context, the retrieval engine embeds the query, searches the vector store, traverses the graph, merges results across all applicable scopes (USER + CUSTOMER + CLIENT + WORLD), ranks them, and returns the top results within the token budget. Frequently surfaced memories stay prominent; rarely surfaced ones gradually deprioritize. Ranking weighs **relevance, recency, and confidence**, so older, less-relevant memories naturally give way to current information. No manual cleanup required. How aggressively memories age, and how long they are retained, is governed by your [Memory Architecture Configuration](/concepts/memory-architecture); Synap derives sensible defaults from your [use-case file](/concepts/memory-architecture#the-use-case-file). When a memory is no longer retained it is **archived** (moved to cold storage, reachable only by explicit archive queries, good for compliance) or **deleted** (permanently removed from both stores, with entity connections cleaned up), depending on your configuration. ### Retrieval, scope, and ranking The retrieval engine searches the full scope chain and prefers the narrowest applicable scope: ``` USER scope → Alice's personal memories (highest priority) CUSTOMER scope → Acme Corp's shared knowledge CLIENT scope → your application's product knowledge WORLD scope → global domain knowledge (lowest priority) ``` ```python Python theme={null} context = await sdk.user.context.fetch( user_id="user_alice", # customer_id="customer_acme", # B2B only search_query=["project timeline", "Q2 deliverables"], types=["facts", "temporal_events"], # restrict to specific memory types ) # context.facts → "Q2 roadmap includes API v3 launch and dashboard redesign" # context.temporal_events → "API v3 launch deadline: June 15" ``` ```javascript JavaScript theme={null} const context = await sdk.user.context.fetch({ user_id: 'user_alice', // customer_id="customer_acme", # B2B only search_query: ['project timeline', 'Q2 deliverables'], types: ['facts', 'temporal_events'], // restrict to specific memory types }); // context.facts → "Q2 roadmap includes API v3 launch and dashboard redesign" // context.temporal_events → "API v3 launch deadline: June 15" ``` ```typescript TypeScript theme={null} const context = await sdk.user.context.fetch({ user_id: 'user_alice', // customer_id="customer_acme", # B2B only search_query: ['project timeline', 'Q2 deliverables'], types: ['facts', 'temporal_events'], // restrict to specific memory types }); // context.facts → "Q2 roadmap includes API v3 launch and dashboard redesign" // context.temporal_events → "API v3 launch deadline: June 15" ``` Two **retrieval modes** trade speed for depth: * **`fast`**: vector + graph search, tuned for low-latency interactive turns. * **`accurate`**: vector + graph plus LLM subquery decomposition and reranking, for deeper, higher-recall processing. See [Retrieval Modes](/concepts/retrieval-modes) for how to choose. Long-term memory is cumulative and self-managing: as it grows, entity resolution sharpens, retrieval gets richer, and ranking ensures only the most relevant memories surface regardless of total volume. Long-term context has two shared sub-layers based on scope (**customer** and **organizational**) covered next. ## Customer context Customer context is knowledge stored at the **CUSTOMER scope**: shared across all users within one B2B tenant, but invisible to other tenants. It is each customer's internal wiki: policies, team structure, shared projects, and domain terminology. ### Lifecycle: ingest with `customer_id`, no `user_id` You create customer context by ingesting with a `customer_id` but **no** `user_id`. That single distinction is what places the memory at the customer scope. ```python Python theme={null} await sdk.memories.create( document=""" Acme Corp Engineering Handbook - All services must use Python 3.11 or later - Production deployments: Tuesdays and Thursdays, 10am-2pm PT - Hotfix deployments require VP Engineering approval """, document_type="document", customer_id="customer_acme", # No user_id, shared across all users at this customer ) ``` ```typescript TypeScript theme={null} await sdk.memories.create(options: CreateMemoryOptions) ``` Do not accidentally include a `user_id` when ingesting customer-wide documents. With a `user_id`, the memory drops to the User scope and becomes visible to only that one person, defeating the purpose of shared tenant knowledge. It then flows through the same processing and storage pipeline as any long-term memory, scoped to the customer. Retention and aging follow your configuration. ### How you read it back Retrieve customer context directly, or let it surface automatically inside user conversations: ```python Python theme={null} context = await sdk.customer.context.fetch( customer_id="customer_acme", search_query=["deployment process", "production releases"], ) # Returns CUSTOMER + CLIENT + WORLD scopes (no USER scope): # - "Production deployments: Tues/Thurs 10am-2pm PT" (CUSTOMER) # - "Platform supports blue-green deployment strategy" (CLIENT) ``` ```javascript JavaScript theme={null} const context = await sdk.customer.context.fetch({ customer_id: 'customer_acme', search_query: ['deployment process', 'production releases'], }); // Returns CUSTOMER + CLIENT + WORLD scopes (no USER scope): // - "Production deployments: Tues/Thurs 10am-2pm PT" (CUSTOMER) // - "Platform supports blue-green deployment strategy" (CLIENT) ``` ```typescript TypeScript theme={null} const context = await sdk.customer.context.fetch({ customer_id: 'customer_acme', search_query: ['deployment process', 'production releases'], }); // Returns CUSTOMER + CLIENT + WORLD scopes (no USER scope): // - "Production deployments: Tues/Thurs 10am-2pm PT" (CUSTOMER) // - "Platform supports blue-green deployment strategy" (CLIENT) ``` ```python Python theme={null} context = await sdk.user.context.fetch( user_id="user_alice", customer_id="customer_acme", search_query=["when can I deploy to production"], ) # Scope chain results, narrowest first: # USER: "Alice deployed the billing service last Tuesday" # CUSTOMER: "Production deployments: Tues/Thurs 10am-2pm PT" # CLIENT: "Blue-green deployment support available" ``` ```javascript JavaScript theme={null} const context = await sdk.user.context.fetch({ user_id: 'user_alice', customer_id: 'customer_acme', search_query: ['when can I deploy to production'], }); // Scope chain results, narrowest first: // USER: "Alice deployed the billing service last Tuesday" // CUSTOMER: "Production deployments: Tues/Thurs 10am-2pm PT" // CLIENT: "Blue-green deployment support available" ``` ```typescript TypeScript theme={null} const context = await sdk.user.context.fetch({ user_id: 'user_alice', customer_id: 'customer_acme', search_query: ['when can I deploy to production'], }); // Scope chain results, narrowest first: // USER: "Alice deployed the billing service last Tuesday" // CUSTOMER: "Production deployments: Tues/Thurs 10am-2pm PT" // CLIENT: "Blue-green deployment support available" ``` The payoff is shared knowledge for every user in the tenant. If Alice ingests sprint planning notes at customer scope, Bob and Carol both see them on their next fetch, while each still has their own user-scoped memories. | Memory | Source scope | Alice | Bob | Carol | | --------------------------------------- | ------------ | ----- | --- | ----- | | "Auth migration to OAuth 2.1 by Q2" | CUSTOMER | Yes | Yes | Yes | | "Alice prefers Slack for notifications" | USER (Alice) | Yes | No | No | | "Bob is on the Platform team" | USER (Bob) | No | Yes | No | | "Product supports OAuth 2.0 and 2.1" | CLIENT | Yes | Yes | Yes | ## Organizational context Organizational context is knowledge at the **CLIENT scope**: the broadest application-level scope. It is your product's documentation, changelog, global policies, and domain knowledge, available to *every* user across *every* customer. Think of it as the product brain beneath all customer- and user-specific memories. ### Lifecycle: ingest with no scope identifiers Org context enters when you ingest **without** `user_id` or `customer_id`. For initial product-knowledge loads, `batch_create` is the recommended path: higher throughput, and processing a documentation set together improves cross-document entity resolution. ```python theme={null} from maximem_synap import CreateMemoryRequest # Single doc, no user_id or customer_id = CLIENT scope await sdk.memories.create( document="Our standard SLA guarantees 99.9% uptime...", document_type="document", document_id="doc_sla_v2", ) # Bulk load product documentation documents = [ CreateMemoryRequest(document=open("docs/api-reference.md").read(), document_type="document"), CreateMemoryRequest(document=open("docs/changelog-v3.md").read(), document_type="document"), ] await sdk.memories.batch_create(documents=documents) ``` It goes through the same pipeline as user memories, but all entity resolution and storage happen at CLIENT scope, so when a user later mentions "Product X," the system resolves it against the entity registered from your docs, connecting their question to the right documentation. CLIENT-scope memories are accessible to **all** users of your application. Do not store sensitive internal documents (HR, financial, executive communications) as org context unless your app is for internal use only. ### Updates, caching, and idempotency Re-ingest a changed document with the **same `document_id`** to update it idempotently: the old version is replaced, reprocessed, and entity connections are refreshed. Use a stable naming convention like `doc___v`, and for frequently changing sources (pricing, feature lists) schedule periodic re-ingestion from your source of truth. Because org knowledge is read-heavy and write-infrequent, client-scope retrieval results are cached with a **30-minute TTL**. This lowers latency and reduces load on the stores; the trade-off is that after an update, changes may take up to the TTL window to propagate everywhere. | Aspect | Detail | | ------------ | --------------------------------------------------------------------------------------- | | Cache TTL | 30 minutes | | Why cache | Org context is read-heavy, write-infrequent | | Invalidation | Re-ingesting (same `document_id`) refreshes affected entries within the next TTL window | ### How it surfaces in retrieval Org context is merged into every user query at the **lowest** priority, beneath user and customer memories: ``` User Query: "What is the refund policy?" │ ▼ 1. USER scope → "Alice has a VIP 60-day return window" ← highest priority 2. CUSTOMER scope → "Acme Corp negotiated 45-day returns" ← medium priority 3. CLIENT scope → "Standard refund policy: 30 days" ← lowest priority │ ▼ Agent receives all three, ranked. A well-designed prompt prefers the most specific (user) answer over the general (org) baseline. ``` Org context is the knowledge baseline that narrower scopes can override. When budget is tight, narrower-scope memories are preserved first and org context is trimmed if necessary. ## Context compaction Compaction solves the short-term growth problem from the other side: when a conversation gets long, sending the full transcript to your LLM becomes expensive and eventually hits the context window. Compaction intelligently compresses the history (preserving key facts, decisions, preferences, and current state) instead of blindly truncating it. The engine reads the full transcript and identifies facts, decisions, preferences, emotional shifts, and where the discussion currently stands. It pulls out five categories of essential information: **facts**, **decisions**, **preferences**, a **summary narrative** of the conversation arc, and the **current state** (active topic and open questions). The extracted information is compressed into your target token budget. Recent turns are preserved verbatim for conversational flow; older, resolved turns become summaries. A `validation_score` is computed so you can confirm critical information survived. Information with durable value is also routed through the ingestion pipeline into long-term memory, so knowledge from the conversation is not lost when the short-term context is compressed. Compaction is lossy by design. For conversations where every nuance matters (legal, medical, financial), keep the full history and use compaction only for supplementary context. ### Strategies | Strategy | Output size | Best for | | -------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------- | | `conservative` | Largest | Short conversations needing high detail; minimal information loss. | | `balanced` | Medium | General-purpose; good compression-vs-detail balance. | | `aggressive` | Smallest | Long or cost-sensitive conversations; keeps only the most critical facts. | | `adaptive` | Varies | Synap analyzes the conversation (length, density, repetition, recency, budget) and picks the strategy. **Recommended default.** | ### The SDK surface Compaction is asynchronous: [`compact`](/sdk-reference/conversation-context/compact) kicks off a job and returns a handle, [`get_compaction_status`](/sdk-reference/conversation-context/get-compaction-status) polls for completion, and [`get_compacted`](/sdk-reference/conversation-context/get-compacted) returns the result. ```python theme={null} import asyncio import uuid conversation_id = str(uuid.uuid4()) # reuse this conversation's UUID # Kick off compaction (fire-and-forget, returns a trigger handle) trigger = await sdk.conversation.context.compact( conversation_id=conversation_id, strategy="adaptive", target_tokens=2000, ) print(f"Compaction {trigger.compaction_id} status: {trigger.status}") # Poll until the run completes while True: status = await sdk.conversation.context.get_compaction_status( conversation_id=conversation_id, ) if status.status in ("completed", "failed"): break await asyncio.sleep(2) # Read the compacted result result = await sdk.conversation.context.get_compacted(conversation_id=conversation_id) print(f"Compressed {result.original_token_count} -> {result.compacted_token_count} tokens") print(f"Quality: {result.validation_score:.2f}, passed: {result.validation_passed}") print(f"Facts: {result.facts}") print(f"Current state: {result.current_state}") ``` Inspect `validation_score` (0.0-1.0) and `validation_passed` to confirm quality. If scores fall consistently low, switch to a less aggressive strategy or raise the token budget. ### Compaction vs. retrieval The two are complementary, not interchangeable: | Aspect | Compaction | Retrieval | | ----------- | ------------------------------------ | ------------------------------------------- | | **Input** | Current conversation history | Query against stored memories | | **Scope** | One conversation | All memories across all conversations | | **Purpose** | Reduce tokens for the current turn | Bring relevant past knowledge into the turn | | **Output** | Compressed view of this conversation | Ranked memories from vector + graph stores | A typical production turn does both: **retrieve** relevant long-term memories, **compact** the current conversation if it is long, then **combine** retrieved memories + compacted summary + recent verbatim turns into the prompt. ## Next steps The overview of how memories and context fit together in Synap. Choosing between `fast` (vector + graph) and `accurate` (vector + graph + LLM decomposition + reranking). How mentions are resolved to canonical entities and linked in the graph. The full scope chain and priority resolution rules. # Entity Resolution & Master Data Management Source: https://docs.maximem.ai/concepts/entity-resolution Entity Resolution (ER) is Synap's ability to identify and link mentions of the same real-world entity across different conversations and documents. When a user says "John", "Mr. Smith", "my manager", and "the person I met at the conference", Synap determines whether these all refer to the same individual and links them to a single canonical entity. This builds a coherent knowledge graph over time, even as the ways people refer to entities vary naturally. Beyond simple deduplication, entity resolution serves as a **master data management** layer for your AI agents. The entity registry acts as a master data store: a single source of truth for the people, organizations, products, and concepts your application encounters. As conversations accumulate, this registry grows into a rich organizational knowledge base that improves retrieval accuracy, enables entity-centric queries, and provides a foundation for building structured knowledge on top of unstructured conversations. Entity resolution runs automatically during ingestion. No additional SDK calls are needed. Every document that passes through the ingestion pipeline has its entities extracted and resolved before storage. ## Why master data management matters Traditional AI applications treat each conversation as isolated text. Over hundreds or thousands of interactions, the same entities appear under different names, in different contexts, and from different users. Without entity resolution, your agent has no way to connect "the CEO" mentioned in one conversation with "Maria Garcia" mentioned in another. The entity registry solves this by: * **Consolidating identity**: All references to the same real-world entity converge on a single canonical record, regardless of how they were originally mentioned * **Building organizational knowledge over time**: Each conversation enriches the registry with new aliases, context, and relationships, making future resolution more accurate * **Enabling entity-centric retrieval**: Instead of searching by keywords, you can retrieve all memories associated with a specific entity across all conversations and users * **Providing auditability**: The registry tracks when each entity was first seen, last referenced, and how it has been resolved, giving you a clear provenance trail ## How it works Entity resolution is a multi-step process that runs as part of the ingestion pipeline: Entity resolution flow: Extract entities from text, match against registry, use canonical name if matched, auto-register if unmatched The ingestion pipeline identifies entity mentions in the incoming content. Entities include people, organizations, products, locations, and other named references. Each entity mention is extracted with its surrounding context. Each extracted entity is matched against the entity registry. The search follows the scope chain (USER, CUSTOMER, CLIENT, WORLD), checking narrowest scopes first. Matching uses both exact text comparison and semantic similarity via vector embeddings. If a match is found, the entity mention is linked to the existing canonical entity. If no match is found, the entity is auto-registered at CUSTOMER scope for future lookups. Ambiguous matches (multiple possible candidates) can be queued for human review. Resolved entities receive a `canonical_name` that is consistent across all references. This canonical name is stored alongside the extracted memory, enabling precise retrieval by entity. *** ## The entity registry The entity registry is a database of known entities, organized by scope (User → Customer → Client → World). It functions as the master data store for all entities your application encounters. Each registry entry contains: | Field | Description | | ---------------- | ------------------------------------------------------------------------------- | | `canonical_name` | The authoritative name for this entity (e.g., "John Smith") | | `aliases` | Known alternative names and references (e.g., "John", "Mr. Smith", "JS") | | `entity_type` | Category: `person`, `organization`, `product`, `location`, `concept`, etc. | | `scope` | The scope level where this entity is registered (user, customer, client, world) | | `embedding` | A vector embedding for semantic matching | | `metadata` | Arbitrary metadata (role, department, relationship to user, etc.) | | `created_at` | When this entity was first registered | | `last_seen` | When this entity was last referenced in an ingestion | ### Scope-aware lookups The registry is searched following the scope chain, narrowest first: Scope-aware lookups: entity lookups search along the scope chain in order (User, then Customer, then Client, then World) narrowest to broadest, so lookups respect scope boundaries ``` 1. USER scope: Entities specific to this user 2. CUSTOMER scope: Entities shared within the organization 3. CLIENT scope: Entities shared across your application 4. WORLD scope: Global entities ``` This ordering means that if a user has a personal contact named "Alex" and the company also has an employee named "Alex", the user-scoped entity takes priority in that user's context. The customer-scoped entity remains available for other users in the same organization. *** ## Matching strategies Synap uses multiple matching strategies to resolve entities, applied in order of confidence: The extracted entity name exactly matches a canonical name or alias in the registry. ``` Input: "John Smith" Registry: canonical_name="John Smith" Result: Exact match (confidence: 1.0) ``` The extracted entity matches a known alias of a registered entity. ``` Input: "Mr. Smith" Registry: canonical_name="John Smith", aliases=["Mr. Smith", "JS"] Result: Alias match (confidence: 0.95) ``` The entity's vector embedding is compared against registry embeddings using cosine similarity. This catches cases where the surface form is different but the meaning is the same. ``` Input: "my team lead from engineering" Registry: canonical_name="John Smith", metadata={"role": "Engineering Team Lead"} Result: Semantic match (confidence: 0.82) ``` The surrounding context of the entity mention is used to disambiguate. If multiple registry entries match by name, the context helps pick the right one. ``` Input: "Alex from the billing department called" Registry: - canonical_name="Alex Chen", metadata={"department": "Engineering"} - canonical_name="Alex Rivera", metadata={"department": "Billing"} Result: Contextual match → Alex Rivera (confidence: 0.88) ``` *** ## Auto-registration When the resolution pipeline encounters an entity that does not match any existing registry entry, it automatically registers the entity at **CUSTOMER scope**. This means: Auto-registration: an unmatched extracted entity is automatically registered as a new canonical entity and added to the registry for future matches * The system learns new entities organically as conversations happen * Future mentions of the same entity will resolve to the auto-registered entry * No manual entity management is required for common use cases * Auto-registered entities can be promoted, edited, or merged through the review queue ``` Conversation 1: "I had a call with Sarah from the partner team." → No match found → Auto-registers "Sarah" at CUSTOMER scope canonical_name: "Sarah" entity_type: "person" metadata: {"context": "partner team"} Conversation 2: "Sarah mentioned the Q3 timeline is shifting." → Matches auto-registered "Sarah" → Links to same canonical entity Conversation 3: "Sarah Chen confirmed the new deadline." → Matches "Sarah" → Updates canonical_name to "Sarah Chen", adds alias "Sarah" ``` Auto-registration happens at CUSTOMER scope by default because it provides the right balance: entities are shared within an organization (so all users in that org benefit) but isolated from other organizations (preventing cross-tenant entity leakage). *** ## The review queue When the resolution pipeline encounters an ambiguous match (where multiple registry entries are plausible candidates) the entity is placed in a **review queue** for human review rather than making an incorrect automatic resolution. ### What triggers a review queue entry * Multiple registry entries match with similar confidence scores * A semantic match falls in the ambiguity zone (confidence between 0.5 and 0.8) * An auto-registered entity closely resembles an existing entry (possible duplicate) ### Managing the review queue Review queue items appear in the **Dashboard → Entities → Review queue** view. From there you can merge ambiguous mentions into an existing canonical entity, create a new entity, or dismiss the match. SDK-level access to the review queue is on the roadmap. For now, resolution is dashboard-only. Contact **[support@maximem.ai](mailto:support@maximem.ai)** if you need programmatic access for a specific workflow. *** ## Code examples ### Automatic resolution during ingestion Entity resolution happens transparently during ingestion. You do not need to make any special calls: ```python Python theme={null} from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK(api_key="your_api_key") # ER happens automatically during ingestion await sdk.memories.create( document="John Smith from Acme Corp called about the Q4 report.", document_type="ai-chat-conversation", user_id="user_123", customer_id="acme_corp" ) # Future mentions of "John", "Mr. Smith", "JS" will resolve to the same entity await sdk.memories.create( document="Mr. Smith followed up on the Q4 numbers. He wants the final version by Friday.", document_type="ai-chat-conversation", user_id="user_123", customer_id="acme_corp" ) # When retrieving, entities are already resolved context = await sdk.user.context.fetch( user_id="user_123", customer_id="acme_corp" ) # Memories about "John Smith" and "Mr. Smith" are linked to the same canonical entity, # this surfaces in retrieval as a single coherent set of facts about that person, even when # different mentions appear across different ingested documents. for fact in context.facts: print(fact.content) ``` ```javascript JavaScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'your_api_key' }); // ER happens automatically during ingestion await sdk.memories.create({ document: 'John Smith from Acme Corp called about the Q4 report.', document_type: 'ai-chat-conversation', user_id: 'user_123', customer_id: 'acme_corp', }); // Future mentions of "John", "Mr. Smith", "JS" will resolve to the same entity await sdk.memories.create({ document: 'Mr. Smith followed up on the Q4 numbers. He wants the final version by Friday.', document_type: 'ai-chat-conversation', user_id: 'user_123', customer_id: 'acme_corp', }); // When retrieving, entities are already resolved const context = await sdk.user.context.fetch({ user_id: 'user_123', customer_id: 'acme_corp', }); // Memories about "John Smith" and "Mr. Smith" are linked to the same canonical entity, // this surfaces in retrieval as a single coherent set of facts about that person, even when // different mentions appear across different ingested documents. for (const fact of context.facts ?? []) { console.log(fact.content); } ``` ```typescript TypeScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'your_api_key' }); // ER happens automatically during ingestion await sdk.memories.create({ document: 'John Smith from Acme Corp called about the Q4 report.', document_type: 'ai-chat-conversation', user_id: 'user_123', customer_id: 'acme_corp', }); // Future mentions of "John", "Mr. Smith", "JS" will resolve to the same entity await sdk.memories.create({ document: 'Mr. Smith followed up on the Q4 numbers. He wants the final version by Friday.', document_type: 'ai-chat-conversation', user_id: 'user_123', customer_id: 'acme_corp', }); // When retrieving, entities are already resolved const context = await sdk.user.context.fetch({ user_id: 'user_123', customer_id: 'acme_corp', }); // Memories about "John Smith" and "Mr. Smith" are linked to the same canonical entity, // this surfaces in retrieval as a single coherent set of facts about that person, even when // different mentions appear across different ingested documents. for (const fact of context.facts ?? []) { console.log(fact.content); } ``` ### Querying for entity-related context To retrieve memories about a specific entity, pass the entity name (or canonical form) as a search query. Synap's `accurate` retrieval mode does the heavy lifting: it traverses the entity graph to find memories linked to that entity, even when the entity isn't named verbatim in the source text. ```python Python theme={null} context = await sdk.user.context.fetch( user_id="user_123", customer_id="acme_corp", search_query=["John Smith"], max_results=20, mode="accurate", ) for fact in context.facts: print(f"[{fact.confidence:.0%}] {fact.content}") ``` ```javascript JavaScript theme={null} const context = await sdk.user.context.fetch({ user_id: 'user_123', customer_id: 'acme_corp', search_query: ['John Smith'], max_results: 20, mode: 'accurate', }); for (const fact of context.facts ?? []) { console.log(`[${((fact.confidence ?? 0) * 100).toFixed(0) + "%"}] ${fact.content}`); } ``` ```typescript TypeScript theme={null} const context = await sdk.user.context.fetch({ user_id: 'user_123', customer_id: 'acme_corp', search_query: ['John Smith'], max_results: 20, mode: 'accurate', }); for (const fact of context.facts ?? []) { console.log(`[${((fact.confidence ?? 0) * 100).toFixed(0) + "%"}] ${fact.content}`); } ``` *** ## Entity types Synap recognizes and categorizes entities into standard types: | Entity Type | Description | Examples | | -------------- | ----------------------------- | ---------------------------------------------------------- | | `person` | Individual people | "John Smith", "the CEO", "my manager" | | `organization` | Companies, teams, groups | "Acme Corp", "the engineering team", "Google" | | `product` | Products, services, tools | "Jira", "the new dashboard", "iPhone 15" | | `location` | Physical or virtual locations | "Portland", "the NYC office", "Slack channel #general" | | `concept` | Abstract concepts, topics | "microservices migration", "Q4 budget", "annual review" | | `event` | Named events or occurrences | "the Q3 launch", "last week's outage", "the board meeting" | *** ## Best practices The more context you include in ingested documents, the better entity resolution works. Full names, roles, and departments help distinguish between entities with similar names. Instead of: "Alex said the deadline is Friday." Prefer: "Alex Rivera from the billing team said the deadline is Friday." Entity resolution relies on scope boundaries. Ensure you use consistent `customer_id` values across all ingestion calls for the same organization. Inconsistent IDs will fragment the entity registry. The review queue catches edge cases that automatic resolution cannot handle. Review these regularly to maintain entity registry quality. Unresolved queue items do not block ingestion: they use the best available match and flag it for review. Auto-registration is designed to build the entity registry organically. Avoid manually populating the registry for every possible entity. Instead, let natural conversations populate it and use the review queue to catch errors. The entity registry is not just a deduplication tool: it is your application's master data store for entities. Invest in keeping it clean: merge duplicates, correct canonical names, and enrich metadata. The quality of entity resolution improves directly with registry quality. ## Working with entity resolution in the SDK Entity resolution is fully automatic. There are no explicit SDK calls to trigger or configure it. As you ingest more data through `sdk.memories.create()`, Synap continuously builds and refines its entity registry, improving resolution accuracy over time. The sections below show how resolution surfaces in practice and how it interacts with retrieval. ### Resolution across conversations The following example demonstrates how entity resolution enriches retrieval results across multiple conversations and users. The same person is mentioned as "Sarah Chen", "S. Chen", and "Sarah" across three separate ingestions, and resolution links them all to one canonical entity. ```python Python theme={null} import uuid # Conversation 1: User mentions "Sarah Chen" explicitly await sdk.memories.create( document="""User: I had a great meeting with Sarah Chen about the Q3 roadmap. Assistant: That sounds productive! What were the key takeaways? User: She wants to prioritize the API redesign and defer the dashboard rewrite.""", document_type="ai-chat-conversation", user_id="user_alice", customer_id="cust_acme_corp", mode="long-range" ) # Conversation 2: Same user mentions "S. Chen" in a different context await sdk.memories.create( document="""User: Can you remind me what S. Chen said about the timeline? Assistant: I'll look into that for you. User: Also, she mentioned something about needing two more engineers.""", document_type="ai-chat-conversation", user_id="user_alice", customer_id="cust_acme_corp", mode="long-range" ) # Conversation 3: Different user at same customer mentions "Sarah" await sdk.memories.create( document="""User: Sarah approved the new budget for infrastructure. Assistant: Great news! What's the approved amount? User: $150k for Q3, up from $120k last quarter.""", document_type="ai-chat-conversation", user_id="user_bob", customer_id="cust_acme_corp", mode="long-range" ) # Later retrieval: Querying about "Sarah Chen" returns facts from ALL three # conversations because ER resolved "S. Chen" and "Sarah" to the same entity. # conversation_id must be a valid UUID string; generate one with # str(uuid.uuid4()) or reuse a UUID you already manage per conversation. context = await sdk.conversation.context.fetch( conversation_id=str(uuid.uuid4()), search_query=["What do we know about Sarah Chen?"], mode="accurate" ) for fact in context.facts: print(f"- {fact.content}") # Example output: # - Sarah Chen wants to prioritize the API redesign for Q3 # - Sarah Chen wants to defer the dashboard rewrite # - Sarah Chen needs two more engineers # - Sarah Chen approved $150k infrastructure budget for Q3 ``` ```javascript JavaScript theme={null} import { randomUUID } from 'node:crypto'; // Conversation 1: User mentions "Sarah Chen" explicitly await sdk.memories.create({ document: `User: I had a great meeting with Sarah Chen about the Q3 roadmap. Assistant: That sounds productive! What were the key takeaways? User: She wants to prioritize the API redesign and defer the dashboard rewrite.`, document_type: 'ai-chat-conversation', user_id: 'user_alice', customer_id: 'cust_acme_corp', mode: 'long-range', }); // Conversation 2: Same user mentions "S. Chen" in a different context await sdk.memories.create({ document: `User: Can you remind me what S. Chen said about the timeline? Assistant: I'll look into that for you. User: Also, she mentioned something about needing two more engineers.`, document_type: 'ai-chat-conversation', user_id: 'user_alice', customer_id: 'cust_acme_corp', mode: 'long-range', }); // Conversation 3: Different user at same customer mentions "Sarah" await sdk.memories.create({ document: `User: Sarah approved the new budget for infrastructure. Assistant: Great news! What's the approved amount? User: $150k for Q3, up from $120k last quarter.`, document_type: 'ai-chat-conversation', user_id: 'user_bob', customer_id: 'cust_acme_corp', mode: 'long-range', }); // Later retrieval: Querying about "Sarah Chen" returns facts from ALL three // conversations because ER resolved "S. Chen" and "Sarah" to the same entity. // conversation_id must be a valid UUID string; generate one with // str(uuid.uuid4()) or reuse a UUID you already manage per conversation. const context = await sdk.conversation.context.fetch({ conversation_id: randomUUID(), search_query: ['What do we know about Sarah Chen?'], mode: 'accurate', }); for (const fact of context.facts ?? []) { console.log(`- ${fact.content}`); } // Example output: // - Sarah Chen wants to prioritize the API redesign for Q3 // - Sarah Chen wants to defer the dashboard rewrite // - Sarah Chen needs two more engineers // - Sarah Chen approved $150k infrastructure budget for Q3 ``` ```typescript TypeScript theme={null} import { randomUUID } from 'node:crypto'; // Conversation 1: User mentions "Sarah Chen" explicitly await sdk.memories.create({ document: `User: I had a great meeting with Sarah Chen about the Q3 roadmap. Assistant: That sounds productive! What were the key takeaways? User: She wants to prioritize the API redesign and defer the dashboard rewrite.`, document_type: 'ai-chat-conversation', user_id: 'user_alice', customer_id: 'cust_acme_corp', mode: 'long-range', }); // Conversation 2: Same user mentions "S. Chen" in a different context await sdk.memories.create({ document: `User: Can you remind me what S. Chen said about the timeline? Assistant: I'll look into that for you. User: Also, she mentioned something about needing two more engineers.`, document_type: 'ai-chat-conversation', user_id: 'user_alice', customer_id: 'cust_acme_corp', mode: 'long-range', }); // Conversation 3: Different user at same customer mentions "Sarah" await sdk.memories.create({ document: `User: Sarah approved the new budget for infrastructure. Assistant: Great news! What's the approved amount? User: $150k for Q3, up from $120k last quarter.`, document_type: 'ai-chat-conversation', user_id: 'user_bob', customer_id: 'cust_acme_corp', mode: 'long-range', }); // Later retrieval: Querying about "Sarah Chen" returns facts from ALL three // conversations because ER resolved "S. Chen" and "Sarah" to the same entity. // conversation_id must be a valid UUID string; generate one with // str(uuid.uuid4()) or reuse a UUID you already manage per conversation. const context = await sdk.conversation.context.fetch({ conversation_id: randomUUID(), search_query: ['What do we know about Sarah Chen?'], mode: 'accurate', }); for (const fact of context.facts ?? []) { console.log(`- ${fact.content}`); } // Example output: // - Sarah Chen wants to prioritize the API redesign for Q3 // - Sarah Chen wants to defer the dashboard rewrite // - Sarah Chen needs two more engineers // - Sarah Chen approved $150k infrastructure budget for Q3 ``` In this example, the ER system: 1. Registered "Sarah Chen" during the first ingestion 2. Resolved "S. Chen" to "Sarah Chen" during the second ingestion 3. Resolved "Sarah" to "Sarah Chen" during the third ingestion (CUSTOMER scope match) 4. Linked all extracted facts to the same canonical entity, enabling comprehensive retrieval ### Impact on retrieval modes Both retrieval modes benefit from entity resolution directly: resolved canonical entities give them the same graph linkage across conversations. `accurate` mode additionally adds LLM subquery decomposition and reranking on top. | Retrieval Mode | ER Benefit | | -------------- | ---------------------------------------------------------------------------------- | | `fast` | Direct: vector + graph traversal follows entity relationships across conversations | | `accurate` | Direct: vector + graph traversal plus LLM subquery decomposition and reranking | For queries that span multiple conversations or involve entity relationships, both modes benefit from a well-populated entity registry; `accurate` additionally trades extra latency for LLM-driven query decomposition and result reranking. ## Next steps See how entity resolution fits into the full ingestion pipeline. Understand the scope chain that entity lookups follow. How resolved entities are stored in the vector and graph engines during ingestion. # How Ingestion Works Source: https://docs.maximem.ai/concepts/how-ingestion-works Ingestion is how raw content becomes structured memory in Synap. Whether you feed in conversation turns as they happen or load historical data in bulk, every document passes through the same pipeline: categorized, extracted, chunked, resolved against existing entities, and stored. This page explains that shared pipeline once, then covers the two ingestion paths: runtime (live, per-turn) and bootstrap (bulk, backfill). Your application produces content all the time: live conversations, support tickets, product docs, CRM records. Ingestion is the process that turns that raw content into structured, retrievable memory. There are two paths into Synap, and they share the same underlying pipeline: * **Runtime ingestion** feeds content in as it is generated during live agent interactions, one turn at a time. * **Bootstrap ingestion** loads pre-existing data in bulk: historical conversations, documentation, migrations from another system. Both paths converge on the same processing pipeline. The difference is *how* you call them and *what defaults* make sense for each. **There is a third way content reaches this pipeline, and you do not call it directly.** Conversation turns sent over the real-time [Listen stream](/concepts/real-time-anticipation) with [`instance.send_message()`](/sdk-reference/instance/send-message) are persisted to conversation history, and when the conversation compacts, those raw turns are promoted into the same pipeline described below: *compaction promotion*. It is deferred but not conditional: compaction fires at 3,000 tokens, 10 messages, or 5 minutes of inactivity, so every conversation reaches this pipeline. That is why the [Agent Integration](/setup/agent-integration) needs no ingestion call: agents report turns and Synap forms the memory. Use the two paths below for content that is not a conversation turn, and never for text you already streamed, which would extract it twice. ## The ingestion pipeline Every document you send, via either path, flows through the same stages before it becomes a memory you can retrieve: The pipeline reads the document's `document_type` and selects the appropriate extraction logic. A chat conversation, an email, and a PDF are each processed differently. Synap analyzes the content to pull out the things worth remembering: facts, decisions, preferences, action items, and the entities involved. The depth of this step depends on the ingestion mode (see below). Larger documents are segmented into coherent units so that retrieval can return precise, relevant passages rather than whole documents. Extracted entities (people, projects, organizations) are matched against entities already in the store, so references to "Sarah" or "Project Atlas" link to the same entity across many documents. See [Entity Resolution](/concepts/entity-resolution). The resulting structured memories are written to the correct scope, indexed for both vector and graph retrieval, and made available to future queries. Ingestion is asynchronous on both paths. The SDK call returns quickly with an identifier, and processing continues in the background. Memories become available for retrieval once the pipeline finishes. Fast mode completes sooner than long-range mode. ### Ingestion modes A single parameter, `mode`, controls how deeply the extraction stage analyzes each document. The same two modes are available on both ingestion paths. Performs lightweight chunking, entity extraction, and vector embedding. Best when speed matters more than extraction depth: routine conversational turns, high-throughput logging, non-critical context. Runs the full extraction pipeline: deep entity resolution, relationship mapping, preference detection, and advanced categorization. Best for high-value content that deserves thorough analysis. This is the default. The ingestion mode you pick here is distinct from the retrieval mode you pick when querying. For how depth maps to query behavior (fast = vector + graph; accurate = vector + graph + LLM subquery decomposition + reranking), see [Retrieval Modes](/concepts/retrieval-modes). ### Document types `document_type` tells the pipeline what kind of content it is looking at and which extraction logic to apply. Both paths accept the same set: | Document Type | Description | Extraction Focus | | ---------------------- | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | | `ai-chat-conversation` | Chat between a user and an AI agent. The most common type for runtime ingestion. | Speaker identification, preference detection, decision tracking, entity extraction | | `document` | General text documents, articles, or notes. | Topic extraction, entity extraction, key facts | | `email` | Email content including headers and body. | Sender/recipient extraction, action items, references | | `pdf` | PDF document content (text extracted). | Structured content extraction, section awareness | | `image` | Image descriptions or OCR-extracted text. | Visual entity extraction, scene understanding | | `audio` | Transcribed audio content. | Speaker diarization, topic segmentation | | `meeting-transcript` | Meeting transcriptions with multiple speakers. | Action items, decisions, attendee tracking, topic flow | During runtime you will use `ai-chat-conversation` almost exclusively. The other types show up most often in bootstrap loads and specialized pipelines. ### Scoping every document `user_id` and `customer_id` determine where a memory is stored in the scope hierarchy, which in turn controls who can retrieve it later. On B2C agents, `customer_id` is resolved automatically and you only pass `user_id`; on B2B agents you pass both. The same scoping rules apply on both ingestion paths. See [Memory Scopes](/concepts/memory-scopes) for the full hierarchy. ## Runtime ingestion Runtime ingestion feeds content into Synap as it is generated during live agent interactions. This is the primary ingestion path for most applications. After each conversation turn (or at the end of a conversation) your application calls `sdk.memories.create()` to send the turn through the pipeline. The call returns immediately, so your agent never waits for ingestion before responding to the user; `fast` is the natural default here. Through a chat widget, API, mobile app, or other channel. Before responding, the agent fetches relevant memories. See [Context End to End](/concepts/context-end-to-end). It calls an LLM with the retrieved context and conversation history, then replies to the user. After the response is delivered, the agent calls `sdk.memories.create()`. This returns immediately and does not block the user experience. The pipeline extracts, resolves, and stores structured memories, which become available for future retrieval. ### The SDK call ```python Python theme={null} import uuid from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK(api_key="your_api_key") result = await sdk.memories.create( document="User: Can you remind me what we decided about the migration timeline?\n" "Assistant: In our last conversation, you and the team agreed to begin the " "database migration on March 15th, with a two-week buffer for testing.", document_type="ai-chat-conversation", user_id="user_123", customer_id="acme_corp", # B2B only; not accepted on B2C mode="fast", ) print(f"Ingestion ID: {result.ingestion_id}") # Returns immediately, processing happens in the background ``` ```javascript JavaScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'your_api_key' }); const result = await sdk.memories.create({ document: "User: Can you remind me what we decided about the migration timeline?\nAssistant: In our last conversation, you and the team agreed to begin the database migration on March 15th, with a two-week buffer for testing.", document_type: 'ai-chat-conversation', user_id: 'user_123', customer_id: 'acme_corp', // B2B only; not accepted on B2C mode: 'fast', }); console.log(`Ingestion ID: ${result.ingestion_id}`); // Returns immediately, processing happens in the background ``` ```typescript TypeScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'your_api_key' }); const result = await sdk.memories.create({ document: "User: Can you remind me what we decided about the migration timeline?\nAssistant: In our last conversation, you and the team agreed to begin the database migration on March 15th, with a two-week buffer for testing.", document_type: 'ai-chat-conversation', user_id: 'user_123', customer_id: 'acme_corp', // B2B only; not accepted on B2C mode: 'fast', }); console.log(`Ingestion ID: ${result.ingestion_id}`); // Returns immediately, processing happens in the background ``` | Parameter | Required | Description | | --------------------- | -------- | ---------------------------------------------------------------------------------------------------- | | `document` | Yes | The text to ingest. For conversations, include both user and assistant messages with speaker labels. | | `document_type` | Yes | The type of content. Determines how the pipeline processes the document. | | `user_id` | No | The user this content belongs to. Determines user-scope storage. | | `customer_id` | No | The customer organization (B2B only; not accepted on B2C). Determines customer-scope storage. | | `mode` | No | `fast` or `long-range`. Defaults to `long-range`; runtime callers typically pass `fast`. | | `document_id` | No | Unique identifier for idempotency. Prevents duplicate ingestion on retry. | | `document_created_at` | No | Timestamp override. Defaults to the current time for runtime ingestion (usually correct). | ### Fitting it into the agent loop ```python theme={null} import uuid from maximem_synap import MaximemSynapSDK from openai import AsyncOpenAI sdk = MaximemSynapSDK(api_key="synap_api_key") openai_client = AsyncOpenAI(api_key="openai_api_key") # conversation_id must be a valid UUID conversation_id = str(uuid.uuid4()) async def handle_message(user_message: str, user_id: str, customer_id: str, conversation_id: str): """Handle a single user message with memory-enabled context.""" # 1. Retrieve relevant context from Synap context = await sdk.conversation.context.fetch( conversation_id=conversation_id, user_id=user_id, customer_id=customer_id, # B2B only; not accepted on B2C search_query=[user_message], mode="fast", ) # 2. Build the prompt with retrieved memories system_prompt = ( "You are a helpful assistant. Use the following context from previous " "conversations to inform your response:\n\n" f"{context.formatted_context}\n\n" "If the context is not relevant, respond based on your general knowledge." ) # 3. Generate the response response = await openai_client.chat.completions.create( model="gpt-4o", messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_message}, ], ) assistant_message = response.choices[0].message.content # 4. Ingest the conversation turn (non-blocking) await sdk.memories.create( document=f"User: {user_message}\nAssistant: {assistant_message}", document_type="ai-chat-conversation", user_id=user_id, customer_id=customer_id, mode="fast", ) return assistant_message ``` For the full retrieve-generate-ingest pattern, see [Context End to End](/concepts/context-end-to-end). Always format conversations with clear `User:` and `Assistant:` labels. The pipeline uses them to identify who said what, which is critical for accurate preference detection and entity attribution. ```python Python theme={null} # Good: clear speaker labels document = "User: I need the report by Friday.\nAssistant: I'll have it ready by Thursday evening." # Bad: no speaker context document = "I need the report by Friday. I'll have it ready by Thursday evening." ``` ```javascript JavaScript theme={null} // Good: clear speaker labels let document = "User: I need the report by Friday.\nAssistant: I'll have it ready by Thursday evening."; // Bad: no speaker context document = "I need the report by Friday. I'll have it ready by Thursday evening."; ``` ```typescript TypeScript theme={null} // Good: clear speaker labels let document = "User: I need the report by Friday.\nAssistant: I'll have it ready by Thursday evening."; // Bad: no speaker context document = "I need the report by Friday. I'll have it ready by Thursday evening."; ``` Keep `user_id` (and `customer_id` on B2B) consistent across all calls for the same user and organization. Inconsistent IDs fragment the store into isolated pockets of context that cannot be retrieved together. Derive them from your auth system. Ingest the turn after your agent has responded, not before, so the ingested content includes both the user message and the agent's reply: complete context for future retrieval. Runtime ingestion should never block or crash your agent. Wrap calls in error handling and log failures. A missed ingestion is recoverable; a crashed agent is not. ```python Python theme={null} try: await sdk.memories.create( document=conversation_turn, document_type="ai-chat-conversation", user_id=user_id, customer_id=customer_id, mode="fast", ) except Exception as e: logger.warning(f"Ingestion failed, will retry: {e}") # Queue for retry or log for manual follow-up ``` ```javascript JavaScript theme={null} try { await sdk.memories.create({ document: conversation_turn, document_type: 'ai-chat-conversation', user_id: user_id, customer_id: customer_id, mode: 'fast', }); } catch (e) { console.warn(`Ingestion failed, will retry: ${e}`); // Queue for retry or log for manual follow-up } ``` ```typescript TypeScript theme={null} try { await sdk.memories.create({ document: conversation_turn, document_type: 'ai-chat-conversation', user_id: user_id, customer_id: customer_id, mode: 'fast', }); } catch (e) { console.warn(`Ingestion failed, will retry: ${e}`); // Queue for retry or log for manual follow-up } ``` If you retry failed ingestion calls, set a `document_id` to prevent duplicate memories. Derive it from your conversation or message identifier. ## Bootstrap ingestion Bootstrap ingestion loads pre-existing data into Synap in bulk. Before your agent goes live (or alongside live operation) you often need to seed it with historical context: past conversations, product documentation, knowledge base articles, customer records. You call `sdk.memories.batch_create()` with many documents at once. Because this data is historical and processed in the background, `long-range` is the natural default. Use bootstrap ingestion whenever you need to load a significant volume of existing data: * **Migrating from another system**: a custom memory solution, a competing product, or an in-house knowledge base. * **Loading historical conversations**: past chat logs, support tickets, or email threads. * **Seeding product documentation**: docs, FAQs, help center articles, internal wikis. * **Backfilling customer data**: CRM records, customer profiles, organizational context. * **Populating shared knowledge**: company policies, SOPs, reference material at customer or client scope. Bootstrap loads run at `BOOTSTRAP` priority in the ingestion queue, which processes below real-time but above maintenance tasks. Your live agent keeps operating normally while historical data is processed. You do not need to finish bootstrapping before going live. The two paths can run simultaneously without competing for resources. ### The batch ingestion method `sdk.memories.batch_create()` accepts multiple documents in a single call, reducing per-call overhead and enabling server-side throughput optimizations. Each document is a `CreateMemoryRequest` supporting the same fields as `sdk.memories.create()`. ```python theme={null} from maximem_synap import MaximemSynapSDK, CreateMemoryRequest sdk = MaximemSynapSDK(api_key="your_api_key") result = await sdk.memories.batch_create( documents=[ CreateMemoryRequest( document="User: How do I reset my password?\nAssistant: Go to Settings > Security > Reset Password.", document_type="ai-chat-conversation", document_id="migration_001", document_created_at="2024-03-15T10:30:00Z", user_id="user_456", customer_id="acme_corp", # B2B only; not accepted on B2C mode="long-range", ), CreateMemoryRequest( document="User: What integrations do you support?\nAssistant: We support Slack, Jira, and GitHub.", document_type="ai-chat-conversation", document_id="migration_002", document_created_at="2024-03-16T14:20:00Z", user_id="user_789", customer_id="acme_corp", mode="long-range", ), ], fail_fast=False, ) print(f"Succeeded: {result.succeeded}") print(f"Failed: {result.failed}") ``` | Parameter | Required | Description | | ----------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `documents` | Yes | List of `CreateMemoryRequest` objects to ingest (max 100 per call). | | `fail_fast` | No | If `True`, the whole batch fails on the first error. If `False` (default), errors are collected and returned alongside successful results. | Each `CreateMemoryRequest` supports the same fields as a single `create` call, including `document_id` for idempotency and `document_created_at` for temporal accuracy. ### Key considerations Always set `document_created_at` to the document's original creation time. Without it, Synap defaults to the ingestion time, which distorts temporal ordering. If a user later asks "What did we discuss last March?", accurate timestamps are essential for correct retrieval. Assign a unique `document_id` to every document, ideally derived from your source system's primary key (e.g. `migration_{source_id}`). If a batch is interrupted, you can safely retry it: already-ingested documents are skipped, preventing duplicates and making it easy to trace memories back to their source. Bootstrap data benefits from thorough extraction. `long-range` (the default for batch) performs deep entity resolution, relationship mapping, and preference detection. The extra processing time is fine for background loads. Clean your historical data first: drop empty conversations, strip PII you should not store, ensure timestamps are ISO 8601, and confirm each document carries the correct scope. Incorrect scoping is difficult to fix later: you would have to re-ingest. Start with a small test batch and verify scoping, timestamps, and entity resolution before the full load. For large loads, track progress with `sdk.memories.status()`: ```python Python theme={null} status = await sdk.memories.status(ingestion_id=result.results[0].ingestion_id) print(f"Status: {status.status}") # queued | processing | completed | failed print(f"Memories created: {status.memories_created}") print(f"Error: {status.error_message}") ``` ```javascript JavaScript theme={null} const status = await sdk.memories.status(result.results[0]?.ingestion_id); console.log(`Status: ${status.status}`); // queued | processing | completed | failed console.log(`Memories created: ${status.memories_created}`); console.log(`Error: ${status.error_message}`); ``` ```typescript TypeScript theme={null} const status = await sdk.memories.status(result.results[0]?.ingestion_id); console.log(`Status: ${status.status}`); // queued | processing | completed | failed console.log(`Memories created: ${status.memories_created}`); console.log(`Error: ${status.error_message}`); ``` Keep concurrency modest. While the API accepts many parallel batch requests, excessive concurrency causes queue backpressure and slows processing for all ingestion types. Add a short delay between batches during the initial bulk load. ### Full example: loading historical conversations This loads historical conversations from a database, handling pagination, error recovery, and progress monitoring: ```python theme={null} import asyncio from maximem_synap import MaximemSynapSDK, CreateMemoryRequest sdk = MaximemSynapSDK(api_key="your_api_key") BATCH_SIZE = 100 async def load_historical_conversations(db_connection): """Load historical conversations from a database into Synap.""" conversations = await db_connection.fetch( "SELECT id, content, user_id, customer_id, created_at " "FROM conversations ORDER BY created_at ASC" ) total = len(conversations) processed = failed = 0 ingestion_ids = [] for i in range(0, total, BATCH_SIZE): batch = conversations[i : i + BATCH_SIZE] documents = [ CreateMemoryRequest( document=conv["content"], document_type="ai-chat-conversation", document_id=f"migration_{conv['id']}", document_created_at=conv["created_at"].isoformat(), user_id=conv["user_id"], customer_id=conv["customer_id"], # B2B only; not accepted on B2C mode="long-range", ) for conv in batch ] try: result = await sdk.memories.batch_create(documents=documents, fail_fast=False) processed += result.succeeded failed += result.failed ingestion_ids.extend(r.ingestion_id for r in result.results) print(f"Progress: {processed}/{total} ingested, {failed} failed") for r in result.results: if r.status == "failed": print(f" Error: {r.error_message}") except Exception as e: print(f"Batch request failed: {e}") # Safe to retry, document_id ensures idempotency failed += len(batch) # Brief pause between batches to avoid queue backpressure await asyncio.sleep(1) print(f"\nBootstrap complete: {processed} ingested, {failed} failed") return ingestion_ids ``` ## Runtime vs bootstrap Both paths share the pipeline, modes, document types, and scoping rules above. They differ in how you invoke them and which defaults fit: | | Runtime | Bootstrap | | ---------------- | ---------------------------------------------- | ------------------------------------------------------- | | **Trigger** | Live agent interactions, per turn | Bulk / backfill loads | | **Method** | `sdk.memories.create()` | `sdk.memories.batch_create()` | | **Default mode** | `long-range`, but typically called with `fast` | `long-range` | | **Throughput** | One document per call, non-blocking | Many documents per call, `BOOTSTRAP`-priority queue | | **Best for** | Real-time conversation logging | Historical data, migrations, seeding docs and knowledge | ## Next steps How fast and accurate retrieval differ, and why long-range is the bootstrap default. The full retrieve-generate-ingest loop for memory-enabled agents. What the pipeline produces: the kinds of structured memory Synap stores. How extracted entities are matched and linked across documents. # Memories & Context Source: https://docs.maximem.ai/concepts/memories-and-context At the heart of Synap are two complementary concepts: **Memories** and **Context**. Memories are the structured knowledge Synap extracts and stores from your data. Context is the curated set of memories assembled and delivered to your AI agent at the moment it needs them. Understanding the relationship between these two (and the five memory types Synap extracts) is fundamental to building effective AI applications with Synap. Think of memories as the knowledge in a library's catalog, and context as the specific books and notes a researcher pulls from the shelves for a particular question. Synap manages both the catalog and the act of pulling the right materials. *** ## What is a Memory? A **Memory** is a unit of structured knowledge that Synap extracts from raw documents. When you ingest content (whether it is an AI chat conversation, a PDF, a knowledge base article, or a plain text document) Synap's extraction pipeline breaks it down into discrete, typed memory units. It does not store raw text. Each memory has: * **A type**: one of five structured categories (fact, preference, episode, emotion, temporal event) * **A confidence or strength score**: a 0.0 to 1.0 value indicating extraction certainty * **Source references**: links back to the original document and extraction context * **Entity links**: connections to resolved entities (people, organizations, concepts) * **Scope**: the visibility boundary (user, customer, client, or world) * **Timestamps**: creation time, last accessed time, and optional temporal anchors This structure (typed, scored, and enriched) is what enables Synap to retrieve precisely relevant information rather than returning large blocks of unprocessed text. A single ingested document can produce dozens or hundreds of individual memories. A five-minute conversation might yield facts about the user's preferences, an episode describing what they discussed, temporal events about upcoming deadlines, and emotional context about how they felt. *** ## What is Context? **Context** is the assembled output that Synap delivers to your AI agent when it requests information. A context response contains: * **Retrieved memories**: the most relevant memories from long-term storage, ranked by relevance, recency, and confidence * **Conversation history**: the accumulated short-term context from the current session (if applicable) * **Scope metadata**: information about which scope levels contributed to the response Context is what your agent consumes. When your agent needs to generate a response, it fetches context from Synap and uses the returned memories and history to inform its output. The quality of your agent's responses depends directly on the quality and relevance of the context Synap provides. ```python Python theme={null} # Fetching context for an agent response context = await sdk.user.context.fetch( user_id="user_alice", customer_id="acme_corp", # B2B: required. B2C: omit, auto-resolved. search_query=["project timeline", "upcoming deadlines"] ) # The returned context exposes one field per memory type: # context.facts -> relevant factual memories # context.preferences -> user preferences # context.episodes -> relevant past interactions # context.emotions -> detected emotional states # context.temporal_events -> time-anchored information ``` ```javascript JavaScript theme={null} // Fetching context for an agent response const context = await sdk.user.context.fetch({ user_id: 'user_alice', customer_id: 'acme_corp', // B2B: required. B2C: omit, auto-resolved. search_query: ['project timeline', 'upcoming deadlines'], }); // The returned context exposes one field per memory type: // context.facts -> relevant factual memories // context.preferences -> user preferences // context.episodes -> relevant past interactions // context.emotions -> detected emotional states // context.temporal_events -> time-anchored information ``` ```typescript TypeScript theme={null} // Fetching context for an agent response const context = await sdk.user.context.fetch({ user_id: 'user_alice', customer_id: 'acme_corp', // B2B: required. B2C: omit, auto-resolved. search_query: ['project timeline', 'upcoming deadlines'], }); // The returned context exposes one field per memory type: // context.facts -> relevant factual memories // context.preferences -> user preferences // context.episodes -> relevant past interactions // context.emotions -> detected emotional states // context.temporal_events -> time-anchored information ``` *** ## Short-term vs Long-term Memory Synap distinguishes between two categories of memory based on their lifespan and purpose. The accumulated context from the **current conversation session**. It builds turn by turn as the user and agent exchange messages. Short-term context lives only for the duration of the conversation and is managed through [context compaction](/concepts/context-end-to-end#context-compaction) to stay within token limits. **Characteristics:** * Ephemeral: exists only during the active session * Grows with each conversational turn * Subject to compaction when it exceeds configured thresholds * Contains the immediate conversational state, recent decisions, and in-progress topics Extracted, structured knowledge that **persists across conversations and sessions**. Long-term memories are produced by the ingestion pipeline and stored in vector and graph engines. They survive indefinitely (subject to retention policies) and are retrieved based on relevance to the current query. **Characteristics:** * Persistent: survives across sessions, days, months * Built from ingested documents via the extraction pipeline * Stored in vector and graph storage engines * Retrieved based on semantic relevance, recency, and confidence scoring The interplay between short-term and long-term memory is central to Synap's value. During a conversation, your agent draws on both: short-term context provides immediate conversational continuity ("we were just talking about the Q2 budget"), while long-term context provides deep knowledge ("Alice prefers executive summaries" and "Acme Corp's fiscal year ends in March"). *** ## Memory types Synap's extraction pipeline produces five distinct types of structured memory. Each type captures a different dimension of knowledge and maps to a field on the context response. | Type | What it captures | Key metric | Context field | Example | | ------------------- | --------------------------------------- | ------------------------------------- | ----------------- | -------------------------------------------- | | **Facts** | Verifiable statements and knowledge | Confidence (0.0-1.0) | `facts` | "The API rate limit is 1,000 req/min" | | **Preferences** | Likes, dislikes, and behavioral choices | Strength (0.0-1.0) + direction | `preferences` | "User prefers concise responses" | | **Episodes** | Event narratives and interactions | Significance (0.0-1.0) | `episodes` | "Discussed migration plan in standup" | | **Emotions** | Detected emotional states | Intensity (0.0-1.0) | `emotions` | "User expressed frustration with onboarding" | | **Temporal Events** | Time-anchored information | Event type (point/recurring/deadline) | `temporal_events` | "Board meeting scheduled for March 15" | Facts provide grounding, preferences enable personalization, episodes give narrative continuity, emotions support empathy, and temporal events enable time-awareness. **Terminology.** "Confidence", "extraction confidence", and "extraction certainty" all refer to the same number on a **Fact**. **Preferences** use a parallel concept called `strength`, **Episodes** use `significance`, and **Emotions** use `intensity`. Use the field name that matches the memory type you're inspecting. ### Facts Facts are the backbone of long-term memory. A fact is a verifiable, declarative statement extracted from ingested content: knowledge about people, organizations, products, processes, and the world. Each fact carries a `confidence` score between 0.0 and 1.0 reflecting how certain the pipeline is that the extracted fact is accurate. * **0.9-1.0**: Explicitly stated, unambiguous facts ("The company was founded in 2019") * **0.7-0.9**: Strongly implied or clearly inferable facts ("The team uses agile methodology") * **0.5-0.7**: Moderately confident extractions, may need verification * **Below 0.5**: Low confidence, typically filtered out at ingest The same `confidence` value plays two roles: at ingest it acts as a hard filter (default threshold `0.7`), and at retrieval it is returned on every fact (`fact.confidence`) so your agent can decide whether to trust, qualify, or ignore each one. Higher-confidence facts surface before lower-confidence ones at the same relevance level, and conflicting facts across scopes are resolved by scope priority (user > customer > client > world). ``` Fact: "Acme Corp's engineering team has 25 members across 4 squads" Confidence: 0.95 Scope: CUSTOMER Entities: [Acme Corp, engineering team] ``` ### Preferences Preferences capture likes, dislikes, behavioral tendencies, and personal choices: what enables your agent to personalize its responses. Each has a **direction** (`positive` = likes/wants, `negative` = dislikes/avoids) and a **strength** (0.0 = mild, 1.0 = very strong). A `positive` preference for "concise responses" at strength 0.9 tells the agent to keep answers short; a `negative` preference for "jargon" at strength 0.7 tells it to use plain language. ``` Preference: "Prefers executive summaries before detailed explanations" Strength: 0.88 Direction: positive Scope: USER Entities: [Alice Chen] ``` ### Episodes Episodes capture event narratives: things that happened, interactions that occurred, activities that took place. They give your agent a sense of history and narrative continuity, answering the question "What happened?" Each carries a `significance` score (0.0-1.0) indicating how important or impactful the episode is, from major decisions and milestones (0.8+) down to routine mentions. Episodes let the agent reference past decisions ("In our previous meeting, you decided to use PostgreSQL") and demonstrate continuity in ongoing relationships. ``` Episode: "Discussed the Q2 roadmap in the Monday standup; decided to prioritize API v3" Significance: 0.82 Scope: CUSTOMER Entities: [Q2 roadmap, API v3, Monday standup, engineering team] ``` ### Emotions Emotions capture detected emotional states, sentiment, and affective signals from conversations, enabling your agent to respond with empathy, recognizing when a user is frustrated, excited, anxious, or satisfied. Each carries an `intensity` score (0.0-1.0) reflecting how strongly the emotion was expressed. Emotional memories let the agent acknowledge feelings, adjust tone, celebrate wins, and approach sensitive topics carefully. ``` Emotion: "User expressed frustration with the onboarding documentation being outdated" Intensity: 0.78 Scope: USER Entities: [Alice Chen, onboarding, documentation] ``` Emotion extraction is optional. Synap turns it off automatically for use cases where it does not apply (e.g., technical Q\&A bots) based on your [use-case file](/concepts/memory-architecture#the-use-case-file). ### Temporal events Temporal events capture time-anchored information (dates, deadlines, recurring schedules, and time-sensitive facts) making your agent time-aware. Each has an `event_type`: A specific, one-time event anchored to a particular date or time, e.g. "Board meeting on March 15, 2026" or "API v3 launched on January 20, 2026". An event that repeats on a schedule, e.g. "Sprint reviews every other Friday at 2pm" or "Monthly all-hands on the first Monday of each month". A time-bound constraint or due date, e.g. "Q2 OKRs due by June 30" or "Contract renewal deadline: April 15". Temporal events let the agent provide timely reminders, reference schedules, and understand urgency. ``` Temporal Event: "API v3 migration must be completed by June 30, 2026" Event type: deadline Scope: CUSTOMER Entities: [API v3, migration] ``` ### Which types your Instance extracts Synap decides which of the five types to extract based on your agent's [Use-Case Markdown file](/concepts/memory-architecture#the-use-case-file). Mention the behavior you need (personalization, continuity, emotional awareness, scheduling) and Synap enables the right categories. Types your agent will never use are skipped to save processing time and storage. | Scenario | Typical types extracted | | ------------------- | --------------------------------------------- | | Technical Q\&A bot | Facts | | Personal assistant | Facts, Preferences, Episodes, Temporal Events | | Support agent | Facts, Preferences, Episodes, Emotions | | Full-featured agent | All five | If your agent's use case changes, re-upload the use-case file and Synap will update the active configuration. See [Customized Memory Architectures](/concepts/memory-architecture). ### Filtering by type during retrieval When fetching context, use the `types` parameter to return only specific memory types: ```python Python theme={null} # Retrieve only facts and preferences context = await sdk.user.context.fetch( user_id="user_alice", customer_id="acme_corp", # B2B: required. B2C: omit, auto-resolved. search_query=["project status"], types=["facts", "preferences"] ) # Omit `types` to retrieve all five (default behavior) ``` ```javascript JavaScript theme={null} // Retrieve only facts and preferences const context = await sdk.user.context.fetch({ user_id: 'user_alice', customer_id: 'acme_corp', // B2B: required. B2C: omit, auto-resolved. search_query: ['project status'], types: ['facts', 'preferences'], }); // Omit `types` to retrieve all five (default behavior) ``` ```typescript TypeScript theme={null} // Retrieve only facts and preferences const context = await sdk.user.context.fetch({ user_id: 'user_alice', customer_id: 'acme_corp', // B2B: required. B2C: omit, auto-resolved. search_query: ['project status'], types: ['facts', 'preferences'], }); // Omit `types` to retrieve all five (default behavior) ``` *** ## How memories become context The journey from raw data to delivered context follows a well-defined pipeline. Ingestion-to-retrieval pipeline: a raw document flows through ingestion and intermediate storage, then is retrieved and supplied to your AI agent You submit raw content to Synap via the SDK. This can be an AI chat conversation, a document, a knowledge base article, or any text content. You specify the `document_type`, and optionally the `user_id` and `customer_id` to set the scope. Synap's pipeline processes the raw content through multiple extraction stages. It identifies entities, resolves them against known entities, and extracts structured memories (facts, preferences, episodes, emotions, temporal events) with confidence scores and source references. Extracted memories are stored in both vector storage (for semantic similarity search) and graph storage (for entity relationships and structured queries). Memories are indexed by scope, type, entity, and embedding vector. When your agent needs context, it sends a retrieval request with search queries and scope identifiers. Synap searches across applicable scope levels, finding memories that are semantically relevant to the query. Retrieved memories are merged across scopes, deduplicated, ranked by relevance/recency/confidence, and assembled into a structured context response. This response is delivered to your agent, ready to inform its next output. ``` Raw Document ↓ [ Ingestion ] → [ Extraction ] → [ Storage ] ↓ [ Vector + Graph ] ↓ Query → [ Retrieval ] → [ Context Assembly ] ↓ Structured Context → Your AI Agent ``` *** ## Next steps How short-term and long-term context accumulate, compact, and combine across a session. Fast (vector + graph) versus accurate (adds LLM subquery decomposition + reranking). How memory isolation works across users, customers, and organizations. The extraction pipeline that turns raw documents into typed memories. # Memory Architecture (MACA) Source: https://docs.maximem.ai/concepts/memory-architecture Every Synap instance runs on a Memory Architecture Configuration (MACA): the per-instance memory policy that governs what is extracted, how it is scoped and stored, how it is retrieved, and how long it is retained. You don't hand-author it; you describe your agent in a use-case file and Synap generates it for you. A **Memory Architecture Configuration (MACA)** is the per-instance configuration that controls how memory behaves for your agent. It governs: * **What is extracted**: which categories of structured knowledge Synap captures from conversations. * **How it is scoped and stored**: how memories are partitioned across users, customers, and clients. * **How it is retrieved**: which retrieval strategy is enabled and how results are ranked. * **How long it is retained**: retention and compaction policy for long-running agents. You do not write a MACA by hand. Instead, you describe your agent in a short **use-case file**, and Synap generates an optimized MACA tailored to that agent. The use-case file is the contract you author and depend on; the MACA is an internal artifact Synap produces from it. ## How MACA works A MACA is generated **per instance**. When you create an instance you provide a use-case file describing what your agent does and who it serves. Synap analyzes that description and produces a configuration tuned to the agent's domain and audience, selecting sensible defaults for memory extraction, scoping, retrieval, and retention. Once active, the MACA governs every memory ingested into the instance and every context fetch: You upload a use-case file: a short Markdown document describing what your agent does, who its users are, what tasks it handles, and any sensitivity or compliance constraints. Synap analyzes the use-case file and produces an optimized MACA for that agent, choosing defaults for memory extraction, scoping, retrieval, retention, and ranking based on the agent's domain and audience. Every memory ingested into the instance and every context fetch is governed by the active MACA. You can view the current configuration in the **Synap Dashboard** under your instance's settings. Re-upload the use-case file whenever your agent's purpose materially changes: new task categories, new compliance requirements, or a new audience. Synap re-evaluates and regenerates the MACA, and the previous version is preserved so you can roll back if needed. The MACA is an internal artifact: its internal structure is subject to change as Synap improves its memory pipeline. The stable contract you depend on is the **use-case file** and the **SDK surface**, not the raw configuration. You never need to read or edit a MACA directly. Memory configuration is a deep optimization problem: the right scope partitioning, ranking weights, retrieval strategy, and retention policy all depend on what kind of agent you're building and what kind of conversations it has. Hand-tuning every knob is the kind of work that delays a launch by weeks. Synap's job is to handle that. Your job is to describe the agent. ## The use-case file The use-case file is the primary input Synap uses to generate your MACA. It's a plain Markdown document you author and upload when you create an instance. The more detail you provide, the better the resulting configuration. Uploading a use-case file is optional but strongly recommended. Without it, Synap falls back to a generic default configuration that may not match your agent's actual needs. ### Getting the template The easiest way to start is to download the pre-structured template from the Dashboard: 1. Navigate to **Instances** and click **Create Instance**. 2. Click **Download Template** next to the Use-Case Markdown field. 3. Open the downloaded file in any text editor and fill in your details. 4. Upload the completed file before clicking **Create**. ### What you put in it The template has eight sections: three required and five optional. A concise, accurate file with only the three required sections outperforms a long but vague file with all eight. | Section | Required | What it tells Synap | | --------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------ | | **Agent Objective** | Yes | What your agent does and the problem it solves. | | **Target Users** | Yes | Who interacts with the agent: roles, technical level, usage patterns. Informs scoping granularity. | | **Task Examples** | Yes | 3-5 representative tasks with a real user message and the expected agent action. The highest-impact section. | | **Behavioral Guidelines** | Optional | Explicit do's and don'ts that shape how memories are filtered and weighted. | | **Role Descriptions** | Optional | Who the Client, Customer, and User are. Maps to Synap's memory scope hierarchy. | | **Compliance & Data Sensitivity** | Optional | Regulatory constraints, PII handling, and retention requirements. | | **Memory Priorities** | Optional | What to prioritize, deprioritize, or disable remembering. | | **Additional Context** | Optional | Deployment details, integrations, or any other relevant constraints. | Here is what the required sections look like in practice: ```markdown theme={null} ## Agent Objective Our agent is a customer support assistant for a B2B SaaS platform. It helps users troubleshoot integration errors, understand billing, and navigate the product. The goal is to resolve issues without escalating to a human agent whenever possible. ## Target Users Technical leads and developers at mid-market companies (50-500 employees). Users are generally technical but not deeply familiar with our internal systems. Most sessions are one-off troubleshooting requests, but power users return frequently and expect the agent to remember their stack and past issues. ## Task Examples - **User**: "My webhooks stopped firing after I rotated my API key yesterday." **Agent**: Identifies the API key rotation as the likely cause, walks through re-registering the webhook endpoint, and stores the user's webhook configuration for future reference. - **User**: "We're migrating from v1 to v2 of your API. What do we need to change?" **Agent**: Provides the migration guide and remembers the user is mid-migration so future sessions can pick up where they left off. ``` The optional sections refine the result. For example, **Role Descriptions** maps directly to Synap's scope hierarchy, and if your Client, Customer, and User are the same person (as in a personal, B2C-style agent), say so explicitly and Synap will collapse the scope hierarchy accordingly. Note that the Customer scope is a B2B concept; B2C agents typically collapse it away. ```markdown theme={null} ## Role Descriptions - **Client** (you): Acme SaaS Inc, we build and operate the platform - **Customer**: Companies that have purchased an Acme subscription (each has their own workspace) - **User**: An individual employee at a customer company, the person chatting with the agent ``` Naming compliance constraints explicitly also helps: "GDPR" or "HIPAA" is enough, and Synap maps known frameworks to the appropriate memory-handling rules automatically. ### How Synap turns it into a MACA When you upload the file, Synap reads it and generates a MACA tuned to your agent. Concrete Task Examples let Synap infer the right signal types to extract; Role Descriptions inform scoping; Compliance and Memory Priorities shape what is persisted and how it is ranked. You are not locked in to the file you uploaded at creation time. To update it: 1. Navigate to your instance in the Dashboard. 2. Go to **Settings** → **Use-Case**. 3. Upload a new file and click **Save**. Synap re-evaluates the file and regenerates the MACA through the standard approval workflow before it takes effect. Updating the use-case file does not alter memories that have already been stored. It only changes how future ingestion and retrieval behave under the new MACA. ## Next steps How memory is organized for a single agent instance. Which categories of structured knowledge Synap extracts. Follow a memory from ingestion through retrieval into a context fetch. Fast (vector + graph) versus accurate retrieval, and when to use each. # Memory Model Cheat Sheet Source: https://docs.maximem.ai/concepts/memory-model-cheat-sheet One screen for the whole mental model: the identifiers, the two write paths, the four fetch interfaces, and the two mode pairs. Bookmark this. Everything you need to hold in your head, on one page. Each row links to the full reference. ## The identifiers (scope chain) From broadest to narrowest. A memory can be tagged with several of these and is then retrievable through any matching scope. | Identifier | Format / source | What it scopes | | ------------------------------------------------------------------------------ | ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | | **Client** (`cli_`) | Your organization / application | Contains one or more instances. See [Clients & Instances](/concepts/memory-scopes#clients-and-instances). | | [**Instance**](/concepts/memory-scopes#clients-and-instances) (`inst_`) | Resolved from your **API key** (not passed per call) | One isolated memory environment (e.g. prod vs staging). | | **Customer** (`customer_id`) | Passed per call **on B2B instances**; not accepted on B2C | Your B2B tenant. Memories are visible to all users in that customer. | | **User** (`user_id`) | Passed on every call | Your end-user. Memories stay private to that user. | | **Conversation** (`conversation_id`) | Passed on conversation-scoped calls; **must be a valid UUID** | A single chat thread. Registered only by `record_message` (see below). | **B2C vs B2B:** B2C instances need only `user_id` (`customer_id` is auto-resolved); B2B instances pass `user_id` under a `customer_id`. The shape is set per-Instance by the **User Relationship** setting in the Dashboard. See [Memory scopes](/concepts/memory-scopes#b2c-vs-b2b-which-scopes-apply-to-you). ## The two write paths | Write API | Use it for | Cost / weight | Registers a conversation? | | ------------------------------------------------------------------------------------ | ---------------------------------------------------------- | ---------------------------------- | ----------------------------------------------------------------- | | [`sdk.conversation.record_message(...)`](/sdk-reference/conversation/record-message) | Turn-by-turn chat: persist each user/assistant turn | Lighter | **Yes**: this is the only call that registers a `conversation_id` | | [`sdk.memories.create(...)`](/sdk-reference/memories/create) | Long-term knowledge: docs, facts, profiles, bulk ingestion | Heavier (full extraction pipeline) | No | Passing `conversation_id` inside `memories.create(metadata=...)` does **not** register the conversation: metadata is stored but **not indexed for scope resolution**. Only `record_message` creates the conversation row that `conversation.context.fetch` reads from. **Which do I use?** * Turn-by-turn chat history → `record_message` * Long-term knowledge (a document, a profile, an imported fact) → `memories.create` * Production chat agent → **both**: `record_message` for the transcript, `memories.create` for durable knowledge. See the cost & dedup note in [Ingestion](/sdk/ingestion). ## The four fetch interfaces Each reads from one scope. A memory tagged with multiple identifiers is retrievable through any matching interface. | Fetch interface | Reads | Anchored by | | -------------------------------------------------------------------------------------------------- | ----------------------------------------- | ------------------------------------------------------- | | [`sdk.user.context.fetch(user_id=...)`](/sdk-reference/context/user-fetch) | Everything tagged to that user | `user_id` (+ `customer_id` on B2B) | | [`sdk.customer.context.fetch(customer_id=...)`](/sdk-reference/context/customer-fetch) | Everything tagged to that customer/tenant | `customer_id` | | [`sdk.client.context.fetch(...)`](/sdk-reference/context/client-fetch) | Everything at the client level | the API key's client | | [`sdk.conversation.context.fetch(conversation_id=...)`](/sdk-reference/conversation-context/fetch) | A single registered thread | a `conversation_id` **registered via `record_message`** | Fetching a brand-new or never-ingested scope returns an **empty** `ContextResponse` (`facts == []`, etc.), not an error. This is the normal cold-start path. A malformed (non-UUID) `conversation_id` raises `InvalidInputError`. See [Error handling](/sdk/error-handling#contextnotfounderror). ## The two mode pairs Two different `mode=` axes. Label which one you mean: | Axis | Values | What changes | | -------------------------------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Retrieval** (`...context.fetch(mode=...)`) | `fast` (default) · `accurate` | `fast` = vector + graph, no LLM decomposition. `accurate` = vector + graph **+ LLM subquery decomposition + reranking** (more compute, wider/higher-quality results). Both pull from the same memory. See [Retrieval modes](/concepts/retrieval-modes). | | **Ingestion** | `fast` · `long-range` | How deeply a write is processed. See [Runtime ingestion](/concepts/how-ingestion-works#runtime-ingestion). | …plus a precision knob: fetch calls also take `precision_level`: `high` (default) = results go through an additional relevance-refinement pass before being returned; `medium` = skips the refinement pass for faster responses (recall isn't impacted; the same candidate memories are searched, but outputs are less precisely filtered). Independent of `mode`; combine with either `fast` or `accurate`. ```python Python theme={null} import uuid from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK() await sdk.initialize() conversation_id = str(uuid.uuid4()) # must be a valid UUID # Write path 1: register + persist a turn await sdk.conversation.record_message( conversation_id=conversation_id, user_id="user_123", role="user", content="I prefer window seats.", ) # Write path 2: durable knowledge await sdk.memories.create( document="Customer is a Gold-tier member since 2021.", document_type="ai-chat-conversation", user_id="user_123", ) # Fetch: user scope, fast mode (default) ctx = await sdk.user.context.fetch(user_id="user_123", search_query=["seating preferences"]) ``` ```javascript JavaScript theme={null} import { randomUUID } from 'node:crypto'; import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient(); await sdk.initialize(); let conversation_id = randomUUID(); // must be a valid UUID // Write path 1: register + persist a turn await sdk.conversation.record_message({ conversation_id: conversation_id, user_id: 'user_123', role: 'user', content: 'I prefer window seats.', }); // Write path 2: durable knowledge await sdk.memories.create({ document: 'Customer is a Gold-tier member since 2021.', document_type: 'ai-chat-conversation', user_id: 'user_123', }); // Fetch: user scope, fast mode (default) const ctx = await sdk.user.context.fetch({ user_id: 'user_123', search_query: ['seating preferences'], }); ``` ```typescript TypeScript theme={null} import { randomUUID } from 'node:crypto'; import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient(); await sdk.initialize(); let conversation_id = randomUUID(); // must be a valid UUID // Write path 1: register + persist a turn await sdk.conversation.record_message({ conversation_id: conversation_id, user_id: 'user_123', role: 'user', content: 'I prefer window seats.', }); // Write path 2: durable knowledge await sdk.memories.create({ document: 'Customer is a Gold-tier member since 2021.', document_type: 'ai-chat-conversation', user_id: 'user_123', }); // Fetch: user scope, fast mode (default) const ctx = await sdk.user.context.fetch({ user_id: 'user_123', search_query: ['seating preferences'], }); ``` The full memory model: memory types, lifecycles, entity resolution, and [MACA](/concepts/memory-architecture). # Identifiers & Scopes Source: https://docs.maximem.ai/concepts/memory-scopes Synap's identifiers (Client, Instance, Customer, User, and Conversation) define who and what your agent remembers about. They map onto a four-level scope chain (User → Customer → Client → World) that determines memory isolation and retrieval priority. This is the canonical mental model for getting multi-user, multi-tenant memory right: personal data stays personal, shared knowledge surfaces where it is needed. This page explains the **identity model and the scope chain**. Code examples are illustrative. For the canonical SDK call signatures and full parameter reference, see [SDK → Ingestion](/sdk/ingestion) and [SDK → Context Fetch](/sdk/context-fetch). Start with user-scoped memories. Add customer and client scopes as your application grows. You can always broaden scope later. Narrowing scope after the fact requires re-ingestion. ## The identifiers at a glance Synap organizes memory around five identifiers. Two describe your **infrastructure** (who you are and what you deployed); three describe **who your agent is serving** and drive how memory is scoped. | Identifier | What it represents | Who creates it | Format | Scope level | | ---------------- | ----------------------------------- | ---------------------------------------------- | ----------------------------------------- | --------------------------- | | **Client** | Your organization / application | Synap (on signup) | `cli_` | CLIENT | | **Instance** | A deployed Synap agent | You (via Dashboard) | `inst_` | N/A (resolved from API key) | | **Customer** | A tenant / organization in your app | You (implicitly, by passing `customer_id`) | Any string you choose | CUSTOMER | | **User** | An individual end-user | You (implicitly, by passing `user_id`) | Any string you choose | USER | | **Conversation** | A single chat / session | You (implicitly, by passing `conversation_id`) | Any string you choose (UUIDs are typical) | within USER | ``` CLIENT (your application) cli_ └── INSTANCE (a deployed agent) inst_ ← resolved from your API key └── CUSTOMER (a tenant) customer_id ← B2B only; not accepted on B2C └── USER user_id └── CONVERSATION conversation_id ``` Clients are created by Synap on signup; Instances are created and managed in the Synap Dashboard. Customers, Users, and Conversations are *implicitly* created: they come into existence the first time you pass their IDs during ingestion or retrieval. There is no separate registration step. **This four-level chain is the default, not the only shape.** Every account starts on `Client → Customer → User`, and for almost every integration that is the right model and the rest of this page is all you need. Underneath, Synap stores scopes as a **ladder** of named levels, and the default is simply a ladder three rungs deep. If your business has a real level between your customers and your people, a team inside an organization or a region above it, see [Scope ladder](/concepts/scope-ladder) for what that changes and what it does not. *** ## B2C vs B2B: which scopes apply to you How many of these scopes you actually use depends on your Instance's tenancy shape. This is set once per Instance by the **User Relationship** setting (in the Dashboard, under Instance Settings), and it decides whether `customer_id` is part of your scoping at all. The setting is exposed to your code as `user_context_isolation`: `equals_customer` for B2C, `strict` for B2B. * **B2C (personal app)**: one tier of users, with no organization above them. You identify each user by `user_id` only. `customer_id` is **not accepted** on a B2C Instance: any call carrying it is rejected with HTTP 400. The customer-scope fetch (`customer.context.fetch`, `POST /v1/context/customer/fetch`) is B2B-only and is rejected here too. Memories live at the user scope, with client and world above them. This is the right model when your users are individuals: a companion app, a personal assistant, or your first hobby agent. * **B2B (multi-tenant)**: your customers are organizations, each containing many users. Every user is scoped under a `customer_id`, so you pass **both** `user_id` and `customer_id` on every call. This is what activates the customer scope: facts tagged at customer scope are shared across that tenant's users, while user-scoped memories stay private to the individual. **How to tell which you have:** open your Instance in the Dashboard and check **User Relationship** under Instance Settings, or call `GET /api/v1/auth/whoami`, which returns your instance's `user_context_isolation`. `equals_customer` means B2C: send `user_id` alone and never `customer_id`. `strict` means B2B: include `customer_id` on every call. The default for a brand-new personal agent is **B2C** (`user_id` only). The scope levels below describe the full four-level chain. In a B2C Instance you work with the User scope (plus Client and World); the Customer scope is not addressable there and becomes relevant only once you adopt the B2B multi-tenant shape. *** ## Clients and instances Clients and Instances are Synap's **infrastructure layer**: they describe who you are and what you deployed, not who your agent serves. Memory isolation itself happens along the Customer/User scope chain *inside* an Instance. ### What is a Client? A **Client** is the top-level organizational entity in Synap. It represents your company, team, or application. When you sign up for Synap and create an account, you are creating a Client. Every Client has a unique identifier in the format `cli_` (for example, `cli_a3f8b1c2d4e5f678`). This identifier is immutable and used throughout the Synap API and SDK to scope operations to your organization. A Client carries human-readable metadata (name, website, description), arbitrary JSON `context`, and a `status` of `active`, `inactive`, or `soft_deleted`. When a Client is set to `inactive` or `soft_deleted`, all of its Instances are effectively suspended. No ingestion or retrieval operations will succeed until the Client is reactivated. ### What is an Instance? An **Instance** is a deployed Synap memory agent. It is the unit of deployment: each Instance has its own isolated memory store, configuration, credentials, and scope hierarchy. You create Instances under a Client to represent different AI agents, environments (staging vs. production), or use cases. Every Instance has a unique identifier in the format `inst_` (for example, `inst_7b2e9a1c3d4f5678`). You never pass the `instance_id` directly on SDK calls: **the Instance is resolved from the API key** you authenticate with. Each API key (format `synap_`) belongs to exactly one Instance, so the key both authenticates you and selects which Instance's memory you are reading and writing. ### What each Instance owns Each Instance is a fully isolated environment. When you create an Instance, Synap provisions these resources exclusively for it: One or more API keys (format: `synap_`) generated from the Dashboard. Each key authenticates the SDK and resolves to this Instance. The SHA-256 hash is stored; the raw key is shown once at generation time and can be individually revoked. The per-instance Memory Architecture Configuration (MACA) that controls how memories are extracted, stored, and retrieved. Synap generates it automatically from the **Use-Case Markdown** file you upload at instance creation: the more detail you provide there, the better the starting configuration. See [Memory Architecture](/concepts/memory-architecture). Isolated vector and graph storage namespaces. Memories stored in one Instance are never accessible from another Instance unless explicitly shared through scope configuration. Within an Instance, memory is isolated along the scope chain (User → Customer → Client), enforced at the storage layer. Clients and Instances are created and managed through the **Synap Dashboard** ([synap.maximem.ai](https://synap.maximem.ai) → **Instances**). The SDK runs *inside* an Instance once it exists; it does not create or list Instances. *** ## Customers and users Customers and Users are **your** entities, not Synap's. You provide the `customer_id` and `user_id` strings, and Synap uses them to organize and isolate memories. You do not need to register these identifiers in advance. Simply pass them during ingestion and retrieval. ### What is a Customer? A **Customer** represents a tenant or organization in your application. If you are building a B2B SaaS product, each of your client companies is a Customer. If you are building a consumer app, each household or account group could be a Customer. Customers are identified by a `customer_id` string that you provide. This string is opaque to Synap: it can be a UUID, a slug, a database ID, or any identifier that is unique within your application. Synap uses it to create a memory boundary at the **CUSTOMER** scope level. **Examples of Customers:** `"acme_corp"`, `"startup_xyz"`, `"household_9a3f"`, `"team_engineering"`. On a **B2C** Instance there is no organization above the user, so `customer_id` is not accepted: passing it returns HTTP 400. On a **B2B** Instance, `customer_id` is required on every call and is what shares facts across a tenant's users. ### What is a User? A **User** represents an individual end-user of your application. Each person interacting with your AI agent is a User. Users are identified by a `user_id` string that you provide, following the same rules as `customer_id`: it is opaque to Synap and can be any unique identifier. Users map to the **USER** scope, the narrowest and most private scope level. Memories stored at the User scope are visible only when that specific `user_id` is provided in a retrieval query. **Examples of Users:** `"user_alice_chen"`, `"u_8b2f4a91"`, `"github|12345"`, `"employee_0042"`. ### Conversations A **Conversation** is a single chat or session for a user, identified by an optional `conversation_id` you pass alongside `user_id`. It groups exchanges within the USER scope so the agent can distinguish "this session" from a user's broader history. Conversation IDs are opaque strings; UUIDs are typical: ```python Python theme={null} import uuid conversation_id = str(uuid.uuid4()) ``` ```javascript JavaScript theme={null} import { randomUUID } from 'node:crypto'; const conversation_id = randomUUID(); ``` ```typescript TypeScript theme={null} import { randomUUID } from 'node:crypto'; const conversation_id = randomUUID(); ``` *** ## The scope chain Synap uses a four-level hierarchical scope chain, ordered from narrowest (most private) to broadest (most shared): Scope chain hierarchy: User (narrowest) to Customer to Client to World (broadest) When Synap retrieves memories, it searches the scope chain from narrowest to broadest. User-scoped memories take priority over customer-scoped memories, which take priority over client-scoped, and so on (**USER > CUSTOMER > CLIENT > WORLD**). This ensures the most specific, relevant memories surface first. ### User scope The most granular scope. Memories stored at the user level are visible only when that specific user is the context for a retrieval query. Use this for personal information, individual preferences, and conversation-specific knowledge. | Aspect | Detail | | -------------- | --------------------------------------------------------------- | | **Visibility** | Only when `user_id` matches the query | | **Isolation** | Complete: no other user can see these memories | | **Use cases** | Personal preferences, individual history, private conversations | **Examples:** "User prefers dark mode and concise responses" · "User's name is Alice Chen" · "User is based in Portland, Oregon" · "User is preparing for an annual review next week". ### Customer scope Memories shared across all users within a customer or organization. Use this for company-wide knowledge, shared context, and organizational facts. In a B2B SaaS application, a "customer" is typically one of your client's end-customer organizations. This scope is **B2B-only**: a B2C Instance does not accept `customer_id` and rejects `customer.context.fetch` outright. | Aspect | Detail | | -------------- | ------------------------------------------------------------- | | **Visibility** | When `customer_id` matches the query, regardless of `user_id` | | **Isolation** | All users within the customer can see these memories | | **Use cases** | Company policies, shared projects, organizational structure | **Examples:** "Acme Corp's fiscal year ends in March" · "Company uses Jira and Slack" · "The engineering team is migrating to microservices" · "Primary billing contact is [finance@acmecorp.com](mailto:finance@acmecorp.com)". ### Client scope Memories shared across all customers of your application. In the Synap hierarchy, you (the developer) are the Client. Client-scoped memories are visible to all users of all your customers. Use this for product knowledge, documentation, and announcements. | Aspect | Detail | | -------------- | ---------------------------------------------------------------- | | **Visibility** | All users across all customers of your application | | **Isolation** | None within your application: all customers share these memories | | **Use cases** | Product documentation, feature announcements, domain knowledge | **Examples:** "Our product supports SSO with SAML and OIDC" · "Version 3.2 introduces bulk CSV import" · "Billing inquiries go to [support@yourapp.com](mailto:support@yourapp.com)" · "The API rate limit is 1000 requests per minute". ### World scope Global knowledge shared across all Instances. This is rarely used directly by application developers. It exists primarily for Synap-managed global knowledge and cross-instance shared resources. | Aspect | Detail | | -------------- | --------------------------------------------------------------------- | | **Visibility** | All Instances, all customers, all users | | **Isolation** | None: truly global | | **Use cases** | General domain knowledge, shared ontologies, global entity registries | Most applications only need User and Customer scopes. Client scope is useful for product-wide knowledge, and World scope is managed by Synap internally. You do not need to use all four levels. *** ## How scoping works with retrieval When your agent fetches context, Synap merges memories from all applicable scopes in the chain. The merge follows a strict priority order: narrower scopes take precedence over broader scopes. ### Retrieval flow Based on the `user_id` and `customer_id` in the retrieval request, Synap determines which scopes to search. If both are provided, all four scope levels are searched. If only `customer_id` is provided, User scope is excluded. The retrieval engine searches vector and graph stores within each applicable scope level, returning candidate memories from each. Candidates from all scopes are merged. If the same fact exists at multiple scope levels (e.g., a user-scoped fact and a customer-scoped fact about the same topic), the narrower-scoped version takes priority. Merged candidates are ranked using the configured ranking signals (recency, relevance, confidence) and returned up to the configured budget limits. ### Scoping rules by parameters What you pass during **ingestion** decides where a memory is stored; what you pass during **retrieval** decides which scopes are searched and in what priority order. **Ingestion:** | Parameters provided | Resulting scope | What gets stored | | ------------------------- | --------------- | ------------------------------------------ | | `user_id` + `customer_id` | USER | Personal memories for that individual user | | `customer_id` only | CUSTOMER | Shared memories for the organization | | Neither | CLIENT | Application-wide memories visible to all | **The table above and the retrieval table below describe a B2B Instance.** On a B2C Instance you pass `user_id` alone: that lands at the USER scope on ingestion and searches USER + CLIENT + WORLD on retrieval. `customer_id` is not accepted on B2C, so any row that carries it is B2B-only and the same call on a B2C Instance is rejected with HTTP 400. The "Neither" row (CLIENT scope) applies to both shapes. **Retrieval:** | Parameters provided | Scopes searched | Priority order | | ------------------------- | -------------------------------- | -------------------------------- | | `user_id` + `customer_id` | USER + CUSTOMER + CLIENT + WORLD | User → Customer → Client → World | | `customer_id` only | CUSTOMER + CLIENT + WORLD | Customer → Client → World | | Neither | CLIENT + WORLD | Client → World | ### Priority resolution example Consider a scenario where conflicting information exists at different scopes: | Scope | Memory | Priority | | -------- | ---------------------------- | ----------------------------------------------- | | User | "Preferred language: French" | Highest (returned) | | Customer | "Company language: English" | Lower (returned as additional context) | | Client | "Default language: English" | Lowest (may be returned if token budget allows) | The user's preference ("French") surfaces first because user scope has the highest priority. The customer-level default ("English") is still available as additional context, letting your agent understand both the personal preference and the organizational default. *** ## Scoped ingestion and retrieval ### Ingesting memories with scope When you ingest content, the scope is determined by the `user_id` and `customer_id` parameters: ```python Python theme={null} from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK(api_key="your_api_key") # User-scoped memory (most specific). B2B shown here; on B2C drop the # customer_id line, because a B2C Instance rejects it. await sdk.memories.create( document="User: My name is Alice and I prefer dark mode.\nAssistant: Noted!", document_type="ai-chat-conversation", user_id="user_alice", customer_id="acme_corp" ) # Customer-scoped memory (shared across users in the organization). B2B only. await sdk.memories.create( document=""" Acme Corp Engineering Standards: - All services use Python 3.11+ and PostgreSQL - Code reviews require two approvals before merge - Deployments happen on Tuesdays and Thursdays """, document_type="document", customer_id="acme_corp" # No user_id, scoped to customer level ) # Client-scoped memory (shared across all customers) await sdk.memories.create( document="Our platform now supports webhook notifications for ingestion completion.", document_type="document" # No user_id or customer_id, scoped to client level ) ``` ```javascript JavaScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'your_api_key' }); // User-scoped memory (most specific). B2B shown here; on B2C drop the // customer_id line, because a B2C Instance rejects it. await sdk.memories.create({ document: "User: My name is Alice and I prefer dark mode.\nAssistant: Noted!", document_type: 'ai-chat-conversation', user_id: 'user_alice', customer_id: 'acme_corp', }); // Customer-scoped memory (shared across users in the organization). B2B only. await sdk.memories.create({ document: ` Acme Corp Engineering Standards: - All services use Python 3.11+ and PostgreSQL - Code reviews require two approvals before merge - Deployments happen on Tuesdays and Thursdays `, document_type: 'document', customer_id: 'acme_corp', // No user_id, scoped to customer level }); // Client-scoped memory (shared across all customers) await sdk.memories.create({ document: 'Our platform now supports webhook notifications for ingestion completion.', document_type: 'document', // No user_id or customer_id, scoped to client level }); ``` ```typescript TypeScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'your_api_key' }); // User-scoped memory (most specific). B2B shown here; on B2C drop the // customer_id line, because a B2C Instance rejects it. await sdk.memories.create({ document: "User: My name is Alice and I prefer dark mode.\nAssistant: Noted!", document_type: 'ai-chat-conversation', user_id: 'user_alice', customer_id: 'acme_corp', }); // Customer-scoped memory (shared across users in the organization). B2B only. await sdk.memories.create({ document: ` Acme Corp Engineering Standards: - All services use Python 3.11+ and PostgreSQL - Code reviews require two approvals before merge - Deployments happen on Tuesdays and Thursdays `, document_type: 'document', customer_id: 'acme_corp', // No user_id, scoped to customer level }); // Client-scoped memory (shared across all customers) await sdk.memories.create({ document: 'Our platform now supports webhook notifications for ingestion completion.', document_type: 'document', // No user_id or customer_id, scoped to client level }); ``` ### Retrieving memories across scopes When retrieving, Synap automatically includes all applicable scope levels: ```python Python theme={null} # Full scope chain retrieval for a specific user (user + customer + client + world) context = await sdk.user.context.fetch( user_id="user_alice", customer_id="acme_corp", search_query=["budget review process"] ) # context.facts might include: # - "Alice prefers dark mode" (USER scope, highest priority) # - "Acme Corp deploys Tues/Thurs" (CUSTOMER scope) # - "Platform supports webhook notifications" (CLIENT scope) # Retrieve without user scope (customer + client + world only). B2B only: # customer.context.fetch is not available on a B2C Instance. context = await sdk.customer.context.fetch( customer_id="acme_corp", search_query=["engineering standards"] ) # Retrieve client scope only context = await sdk.client.context.fetch() ``` ```javascript JavaScript theme={null} // Full scope chain retrieval for a specific user (user + customer + client + world) let context = await sdk.user.context.fetch({ user_id: 'user_alice', customer_id: 'acme_corp', search_query: ['budget review process'], }); // context.facts might include: // - "Alice prefers dark mode" (USER scope, highest priority) // - "Acme Corp deploys Tues/Thurs" (CUSTOMER scope) // - "Platform supports webhook notifications" (CLIENT scope) // Retrieve without user scope (customer + client + world only). B2B only: // customer.context.fetch is not available on a B2C Instance. context = await sdk.customer.context.fetch({ customer_id: 'acme_corp', search_query: ['engineering standards'], }); // Retrieve client scope only context = await sdk.client.context.fetch(); ``` ```typescript TypeScript theme={null} // Full scope chain retrieval for a specific user (user + customer + client + world) let context = await sdk.user.context.fetch({ user_id: 'user_alice', customer_id: 'acme_corp', search_query: ['budget review process'], }); // context.facts might include: // - "Alice prefers dark mode" (USER scope, highest priority) // - "Acme Corp deploys Tues/Thurs" (CUSTOMER scope) // - "Platform supports webhook notifications" (CLIENT scope) // Retrieve without user scope (customer + client + world only). B2B only: // customer.context.fetch is not available on a B2C Instance. context = await sdk.customer.context.fetch({ customer_id: 'acme_corp', search_query: ['engineering standards'], }); // Retrieve client scope only context = await sdk.client.context.fetch(); ``` *** ## Scope hierarchy table | Scope | Identified by | Visible to | Typical content | Priority | | ------------ | ---------------------------------- | ------------------------------ | ---------------------------------------- | -------- | | **User** | `user_id` (+ `customer_id` on B2B) | Only that user | Personal preferences, individual history | Highest | | **Customer** | `customer_id` (B2B only) | All users in that customer org | Company policies, shared projects | High | | **Client** | Implicit (your application) | All users across all customers | Product docs, announcements | Medium | | **World** | Global | All Instances everywhere | General domain knowledge | Lowest | *** ## Common scoping patterns For a personal AI assistant serving one user at a time with no multi-tenant requirements. Ingest everything with `user_id`. Do not pass `customer_id`: a B2C Instance does not accept it and rejects the call with HTTP 400. Each user has completely isolated memory. For a B2B application where each customer organization has multiple users, and you need both per-user personalization and shared organizational knowledge. Ingest user conversations with `user_id` + `customer_id`. Ingest company documents with just `customer_id`. Use client scope for product documentation. For an application where all users share the same knowledge base and there is no per-user personalization. All memories are at the Client scope. No user or customer isolation. Simplest setup but no personalization. For an application where organizational knowledge is primary but individual users can have some personal preferences. Default scope is customer. Personal preferences can be added by including `user_id` on specific ingestion calls. On a B2B Instance, a User always belongs to a Customer. Always include the customer context so scope chain retrieval behaves correctly. (On B2C this does not apply: there is no customer above the user, and passing `customer_id` is rejected with HTTP 400.) *** ## Next steps See how identifiers and scopes flow through Synap from ingestion to retrieval. Fast retrieval uses vector + graph; accurate adds LLM subquery decomposition and reranking. Configure scoping strategies and extraction in MACA. Quick reference for identifiers, scopes, and priority resolution. # Real-Time Anticipation Source: https://docs.maximem.ai/concepts/real-time-anticipation Synap can push context to your agent before it asks. The Listen stream is a long-lived gRPC connection that carries activity signals up and anticipated context bundles down, turning the next fetch() into a local cache read. Most applications talk to Synap over request-response: you call [`memories.create`](/sdk-reference/memories/create) to write and [`fetch`](/sdk-reference/context/fetch) to read. That is complete and correct on its own. **Real-time anticipation adds a second channel.** [`instance.listen()`](/sdk-reference/instance/listen) opens a long-lived gRPC stream. Your app reports what the agent is doing; Synap predicts what it will need next and pushes context bundles down the stream *before* the agent asks. The next `fetch()` then resolves from a local cache instead of a network round-trip. ## Two channels, two jobs The two channels do different work, and the difference is mostly about **when** memory happens. | | Ingestion | Listen stream | | ---------------------------- | ---------------------------------------------------- | ------------------------------------------- | | **Transport** | REST | gRPC (bidirectional) | | **You call** | `memories.create` / `conversation.ingest_transcript` | `instance.listen` + `instance.send_message` | | **Carries** | Full turns and documents | Activity signals | | **Creates long-term memory** | Yes, on every call | Yes, at compaction | | **Timing** | Immediate | Deferred, up to \~5 minutes | | **Effect** | Durable memory you control | Prefetched context, plus durable memory | | **Use for** | Documents, backfills, anything urgent | Live conversation turns | **The stream is not memory-neutral.** Conversation turns you send with `send_message()` are persisted to conversation history, and when the conversation compacts, those raw turns are promoted into the same ingestion pipeline `memories.create()` uses. For an agent reporting conversation turns, this is sufficient on its own; it is the basis of the [Agent Integration](/setup/agent-integration). What it costs you is immediacy (a turn is retrievable once its conversation compacts, up to about five minutes) and control over how content is typed and scoped. See [What the stream does to memory](#what-the-stream-does-to-memory). ## Opening the stream `listen()` requires an initialized SDK. It resolves your `client_id` and `instance_id`, authenticates the stream with the same API key your REST calls use, and holds the connection open until you close it. ```python theme={null} from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK(api_key="synap_your_key_here") await sdk.initialize() await sdk.instance.listen( on_reconnect=lambda attempt: logger.info("stream reconnected (attempt %d)", attempt), on_disconnect=lambda reason: logger.warning("stream lost: %s", reason), ) ``` Both callbacks take one argument: `on_reconnect` receives the attempt count, `on_disconnect` receives the reason. A zero-argument callback raises `TypeError` at the moment the stream drops, which is precisely when you need the diagnostic. `on_context` is optional. Arriving bundles are written to the SDK's anticipation cache automatically, so `fetch()` finds them whether or not you supply a callback. Use it only when you want to react to bundles directly. Connection targets are configurable via [`SDKConfig`](/sdk/configuration): `grpc_host`, `grpc_port`, and `grpc_use_tls`. Defaults point at Synap Cloud with TLS on. ## What you send Report agent activity with [`send_message`](/sdk-reference/instance/send-message). The `event_type` determines how the platform treats it. | `event_type` | When to send it | What the platform does | | ------------------- | -------------------------------------------- | ------------------------------------------------------------------------- | | `user_message` | The user's turn arrives, before you retrieve | Persists the turn to conversation history; signals a new turn is starting | | `assistant_message` | After your agent produces its reply | Persists the turn; **triggers anticipation for the next turn** | | `tool_call` | Your agent decides to call a tool | Observed for situational awareness (informative, not a trigger) | | `context_request` | Your agent plans a retrieval | Supplies `search_queries` / `context_types` as direct anticipation hints | A `user_message` or `assistant_message` event is only persisted when **both** `user_id` and `customer_id` are present. If either is missing the platform skips persistence and logs server-side, and your application sees no error and no exception. On a B2C instance send `user_id` only: `customer_id` is not accepted there, and an event without one is persisted normally. ## What the stream does to memory A `user_message` or `assistant_message` event does more than feed anticipation. The platform writes it to the conversation's history, and that history has a second consumer. When a conversation compacts, Synap summarizes it for short-term context and then **promotes the raw turns into the long-term ingestion pipeline**: the same extraction stages `memories.create()` runs, producing memories of the same quality. Compaction fires on any of three triggers, so every conversation gets there: | Trigger | Default | Catches | | ------------------- | ----------------------- | ------------------ | | **Token threshold** | 3,000 tokens | Long conversations | | **Message count** | 10 messages | Busy conversations | | **Idle period** | 5 minutes of inactivity | Everything else | The first two are configurable per Instance. The idle trigger is the backstop that makes streaming self-sufficient: a two-turn exchange still compacts a few minutes after the user stops, and its turns still become memory. This is why the [Agent Integration](/setup/agent-integration) has no ingestion call in its loop. Two conditions still apply: A turn missing `user_id` or `customer_id` is never persisted, so there is nothing to promote. It fails silently. If you ingested the conversation with [`conversation.ingest_transcript`](/sdk-reference/conversation/ingest-transcript), promotion skips it deliberately: the full transcript was already ingested at higher fidelity. **Do not stream a turn and also ingest that same turn.** If you call `memories.create()` on text you already reported as `user_message` / `assistant_message`, it is extracted twice, once immediately and once at compaction, costing double and producing overlapping memories the deduplication stage then reconciles. Stream conversation turns; ingest everything else. Promotion is automatic but not instant: a turn becomes retrievable when its conversation compacts, which for a quiet conversation means about five minutes. When something must be retrievable sooner than that, ingest it explicitly. See [when to still call `memories.create()`](/setup/agent-integration#when-to-still-call-memories-create). ## What the SDK sends on its own Once the stream is live, the SDK instruments `fetch()` for you. You do not write this code, but you should know it exists: * **`context_fetch`**: a retrieval was requested * **`context_used`**: the retrieval was served from the anticipation cache * **`context_assembled`**: what the SDK actually composed for the model These drive the platform's learning loop: prefetch outcome scoring, per-pattern hit rates, and the Requests page audit trail. They carry ids and counts only, never raw prompt content. ## What comes back The platform pushes **context bundles** down the stream. Each is written into the SDK's in-process anticipation cache. When your agent then calls `fetch()`: * **Cache hit** → served locally in roughly a millisecond, no network call * **Cache miss** → falls through to the normal REST retrieval path This is why anticipation is safe to adopt incrementally. A cold or dead stream costs you nothing but the latency you already had. ## The turn loop The mechanism that makes this work is easy to miss: **the `assistant_message` you send at the end of turn N is what warms the cache for turn N+1.** Anticipation happens *between* turns, not during them. By the time your next turn calls `fetch()`, the bundle has already landed. So the ordering matters. Emit `assistant_message` after your agent replies, not before. ```python Python theme={null} import uuid conversation_id = str(uuid.uuid4()) # must be a valid UUID, reused across the conversation async def handle_turn(user_text: str, user_id: str, customer_id: str) -> str: # 1. Report the user's turn. await sdk.instance.send_message( content=user_text, role="user", event_type="user_message", conversation_id=conversation_id, user_id=user_id, customer_id=customer_id, ) # 2. Retrieve. Warm cache → local hit; cold → REST fallback, same API. context = await sdk.user.context.fetch( user_id=user_id, customer_id=customer_id, conversation_id=conversation_id, search_query=[user_text], ) reply = await your_llm(context, user_text) # 3. Report the reply. THIS is what anticipates turn N+1. await sdk.instance.send_message( content=reply, role="assistant", event_type="assistant_message", conversation_id=conversation_id, user_id=user_id, customer_id=customer_id, ) # No ingestion call. These turns become long-term memory # when the conversation compacts. return reply ``` ```javascript JavaScript theme={null} import { randomUUID } from 'node:crypto'; let conversation_id = randomUUID(); // must be a valid UUID, reused across the conversation async function handle_turn(user_text, user_id, customer_id) { // 1. Report the user's turn. await sdk.instance.send_message({ content: user_text, role: 'user', event_type: 'user_message', conversation_id: conversation_id, user_id: user_id, customer_id: customer_id, }); // 2. Retrieve. Warm cache → local hit; cold → REST fallback, same API. const context = await sdk.user.context.fetch({ user_id: user_id, customer_id: customer_id, conversation_id: conversation_id, search_query: [user_text], }); const reply = await your_llm(context, user_text); // 3. Report the reply. THIS is what anticipates turn N+1. await sdk.instance.send_message({ content: reply, role: 'assistant', event_type: 'assistant_message', conversation_id: conversation_id, user_id: user_id, customer_id: customer_id, }); // No ingestion call. These turns become long-term memory // when the conversation compacts. return reply; } ``` ```typescript TypeScript theme={null} import { randomUUID } from 'node:crypto'; let conversation_id = randomUUID(); // must be a valid UUID, reused across the conversation async function handle_turn(user_text, user_id, customer_id) { // 1. Report the user's turn. await sdk.instance.send_message({ content: user_text, role: 'user', event_type: 'user_message', conversation_id: conversation_id, user_id: user_id, customer_id: customer_id, }); // 2. Retrieve. Warm cache → local hit; cold → REST fallback, same API. const context = await sdk.user.context.fetch({ user_id: user_id, customer_id: customer_id, conversation_id: conversation_id, search_query: [user_text], }); const reply = await your_llm(context, user_text); // 3. Report the reply. THIS is what anticipates turn N+1. await sdk.instance.send_message({ content: reply, role: 'assistant', event_type: 'assistant_message', conversation_id: conversation_id, user_id: user_id, customer_id: customer_id, }); // No ingestion call. These turns become long-term memory // when the conversation compacts. return reply; } ``` This is the complete loop. See [Agent Integration](/setup/agent-integration) for the full walkthrough including startup and shutdown. Reported turns become long-term memory when the conversation compacts; add [`memories.create`](/sdk-reference/memories/create) only for content that is not a conversation turn, or that must be retrievable sooner. ## One stream per instance In a multi-tenant server, open **one** stream for the whole process and distinguish users through the `user_id` / `customer_id` on each `send_message` and `fetch`. Do not open a stream per user session. The platform enforces per-instance and per-client stream quotas (currently 100 concurrent streams per instance, 200 per client). A stream-per-session design saturates that quota under real concurrency, and the surplus connections enter a `RESOURCE_EXHAUSTED` reconnect loop. Streams also have a maximum lifetime of one hour, after which the server closes them. This is normal: the SDK reconnects with exponential backoff and your `on_reconnect` callback fires. A long-lived server should expect periodic reconnects, not a single permanent connection. See [Real-Time Anticipation in a Server](/patterns/real-time-anticipation-server) for the full pattern, including the health check worth alerting on. ## When the stream fails Failure is designed to be non-fatal (retrieval falls back to REST), but the two SDKs differ in how loudly they say so. | | Python | JavaScript / TypeScript | | ---------------- | ------------------------------------------------------------ | ------------------------------------------- | | `listen()` fails | **Raises** (`AuthenticationError`, `SDKNotInitializedError`) | **Warns to console** and falls back to HTTP | | Reconnect | Exponential backoff, 10 attempts; counter resets on success | Exponential backoff with jitter | | After exhaustion | Stream stays down; `fetch()` keeps working over REST | Same | In both SDKs, a permanently dead stream is **silent** at the application level: `fetch()` still returns correct results, just without the latency benefit. Do not assume streaming is working because your agent works. Monitor `instance.is_listening` and log from `on_disconnect`. ## What anticipation does not do * It does **not** ingest a turn at the moment you send it; promotion happens at compaction, up to about five minutes later * It does **not** give you control over how promoted content is typed, scoped, or tagged; `memories.create()` does * It does **not** replace `memories.create` or `conversation.ingest_transcript` as your memory strategy * It does **not** change what `fetch()` returns, only how fast it returns * It is **not** required; every Synap feature works without it One shared stream, many tenants, and the failure modes worth alerting on. `listen`, `send_message`, and `stop_listening` in full. # Fast & Accurate Modes Source: https://docs.maximem.ai/concepts/retrieval-modes Synap has two speed-vs-thoroughness modes that apply to both writing memories and reading them back. Fast is the real-time default; accurate (long-range on ingestion) does the deeper work for high-value content and complex queries. `mode=` controls a speed-vs-thoroughness tradeoff. It appears on **two different axes**. Label which one you mean: | Axis | Parameter | Values | Picks between | | ------------- | ---------------------------- | --------------------- | --------------------------------------------- | | **Ingestion** | `memories.create(mode=...)` | `fast` · `long-range` | How deeply a write is processed and indexed | | **Retrieval** | `...context.fetch(mode=...)` | `fast` · `accurate` | How much work a read does to assemble context | "Fast" means the same thing on both axes (lightweight, low-latency). The thorough setting is called **`long-range`** for ingestion and **`accurate`** for retrieval: same principle, deeper processing for higher quality. The two are independent: you can ingest `long-range` and read back `fast`, or any combination. ## Quick comparison | Aspect | Fast | Accurate | | -------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------ | | **Retrieval latency** | Lower | Higher | | **Ingestion processing** | Faster | Slower (deeper) | | **Search method** | Vector + graph (no LLM query decomposition) | Vector + graph **+ LLM subquery decomposition + reranking** | | **Ranking signals** | Cosine similarity | Similarity + recency + graph centrality + confidence | | **Entity resolution** | Lightweight (basic NER) | Full pipeline (semantic matching, cross-reference) | | **Relationship awareness** | Graph relationships, no LLM-driven multi-hop decomposition | Explicit graph edges with LLM-driven multi-hop decomposition | | **Compute cost** | Lower | Higher | | **Best for** | Real-time chat, simple queries, high throughput | Complex queries, summaries, relationship-aware context | The two modes are not mutually exclusive. Use `fast` for the hot path of a live conversation and switch to `accurate` for specific high-value queries, all within the same application and the same Synap Instance. Building a real-time chatbot or voice agent? Start with **fast** for both ingestion and retrieval, then selectively upgrade specific interactions to `long-range` / `accurate` as needed, no architecture change required. ## Fast mode The recommended default for real-time, conversational agents where low latency matters more than exhaustive extraction. ### Fast ingestion Fast ingestion runs a lightweight extraction pipeline, optimized to make memories available quickly. | Stage | Behavior | | ------------------------ | ---------------------------------------------------------------------- | | **Chunking** | Basic semantic chunking by paragraph and sentence boundaries | | **Entity extraction** | Lightweight named entity recognition (people, organizations, products) | | **Embedding** | Vector embeddings generated for each chunk | | **Preference detection** | Basic keyword-based preference identification | | **Storage** | Chunks stored in the vector store; entities indexed for lookup | It **skips** deep entity resolution against the full registry, explicit relationship/graph-edge mapping, advanced topic categorization, and emotional/sentiment analysis. Memories become available for vector-based retrieval shortly after processing. Use it for real-time chat logging, high-throughput pipelines, routine Q\&A, and ephemeral content that doesn't need deep relationship modeling. ```python Python theme={null} # Fast ingestion for a routine conversation turn await sdk.memories.create( document="User: What's the status of my order?\n" "Assistant: Your order #4521 shipped yesterday and should arrive by Thursday.", document_type="ai-chat-conversation", user_id="user_123", mode="fast", ) # Returns immediately. Memory available for retrieval shortly after. ``` ```typescript TypeScript theme={null} await sdk.memories.create(options: CreateMemoryOptions) ``` ### Fast retrieval Fast retrieval queries **both the vector store and the knowledge graph**, but skips the LLM-driven subquery decomposition and reranking that accurate mode adds. That keeps latency low for the hot path of real-time conversations. The query is converted into a vector embedding, consistent with the embeddings created during ingestion. The embedding is compared against stored memory embeddings using cosine similarity, scoped to the applicable scope levels (user, customer, client, world) based on the provided `user_id` and `customer_id`. Results are ranked by cosine similarity. No LLM-driven decomposition or reranking pass is applied. The top-k results (per the configured budget) are returned as structured context. `conversation_id` must be a valid UUID. Generate one with `str(uuid.uuid4())` and reuse it for every turn in the same conversation. ```python Python theme={null} import uuid conversation_id = str(uuid.uuid4()) # one UUID per conversation, reused across turns context = await sdk.conversation.context.fetch( conversation_id=conversation_id, user_id="user_123", search_query=["What do we know about Project Atlas?"], mode="fast", ) for fact in context.facts: print(f"[{fact.confidence:.2f}] {fact.content}") ``` ```javascript JavaScript theme={null} import { randomUUID } from 'node:crypto'; let conversation_id = randomUUID(); // one UUID per conversation, reused across turns const context = await sdk.conversation.context.fetch({ conversation_id: conversation_id, user_id: 'user_123', search_query: ['What do we know about Project Atlas?'], mode: 'fast', }); for (const fact of context.facts ?? []) { console.log(`[${(fact.confidence ?? 0).toFixed(2)}] ${fact.content}`); } ``` ```typescript TypeScript theme={null} import { randomUUID } from 'node:crypto'; let conversation_id = randomUUID(); // one UUID per conversation, reused across turns const context = await sdk.conversation.context.fetch({ conversation_id: conversation_id, user_id: 'user_123', search_query: ['What do we know about Project Atlas?'], mode: 'fast', }); for (const fact of context.facts ?? []) { console.log(`[${(fact.confidence ?? 0).toFixed(2)}] ${fact.content}`); } ``` What fast retrieval skips, relative to accurate: **LLM subquery decomposition** (breaking a complex query into focused sub-queries to widen coverage) and **reranking** (an extra pass that reorders candidates for relevance). Broad, multi-part questions may therefore retrieve less complete context: those are the cases to send to accurate mode. ## Accurate mode Prioritizes thoroughness and quality over speed. It runs the full extraction pipeline on ingestion (`long-range`) and adds LLM-driven refinement on retrieval (`accurate`), producing richer, more connected context. ### Long-range ingestion Long-range ingestion runs the complete extraction pipeline, producing structured, relationship-aware memories that power graph-based retrieval. Content is split into semantically coherent chunks, respecting topic boundaries and conversational turns. Captures all people, organizations, products, locations, concepts, and events, including implied entities and role-based references ("my manager", "the person who handles billing"). Each entity is matched against the full registry using exact, alias, semantic, and contextual strategies; new entities are auto-registered. See [Entity Resolution](/concepts/entity-resolution). Explicit and implicit relationships become graph edges: e.g. "Sarah is leading Project Atlas" → Sarah --\[leads]--> Project Atlas. Stated, implied, and contextual preferences are extracted with high confidence. Emotional tone is analyzed and stored as metadata that can influence retrieval ranking. Content is classified into a topic hierarchy with domain-specific tags. Chunks are embedded into the vector store; entity relationships are stored in the graph store. Both engines are populated, enabling accurate retrieval's combined search. Long-range takes longer than fast, scaling with content length and the number of entities and relationships. It is the default for [bootstrap ingestion](/concepts/how-ingestion-works#bootstrap-ingestion). Use it for important conversations (strategic discussions, key decisions, escalations), complex documents, profile-building onboarding, and meeting transcripts. ```python Python theme={null} # Long-range ingestion for an important strategic conversation await sdk.memories.create( document=( "User: Let's revisit the Project Atlas timeline. I spoke with Sarah Chen " "from engineering yesterday, and she's concerned about the Q3 deadline. The " "infrastructure team hasn't finished the database migration yet, and James " "from DevOps says they need at least three more weeks.\n" "User: Note that we might bring in two engineers from the platform team to " "help accelerate. Maria approved the budget for that yesterday." ), document_type="ai-chat-conversation", user_id="user_123", mode="long-range", ) ``` ```typescript TypeScript theme={null} await sdk.memories.create(options: CreateMemoryOptions) ``` This extracts **entities** (Sarah Chen, James, Maria, Project Atlas, platform/infrastructure teams), **relationships** (Sarah --\[concerned\_about]--> Atlas timeline; Maria --\[approved]--> budget), **decisions** (Q4 fallback, two added engineers), and **facts** (migration incomplete, three-week estimate, Q3 flagged infeasible). ### Accurate retrieval Accurate retrieval queries both stores (the same dual-store retrieval fast mode uses) and adds two distinguishing steps: **LLM-driven subquery decomposition** and **reranking**, together with multi-signal ranking. Same as fast mode: embed the query, find candidates by cosine similarity. The query is decomposed into focused sub-queries that expand the entities and angles explored. Entities from the query and top vector results seed graph traversal, following relationship edges to connected entities, related facts, and context. Querying "Project Atlas" reaches Sarah Chen, James, the database migration, the Q3 deadline, and Maria's budget approval. Vector and graph results are merged into one candidate set; duplicates removed, scores normalized to a common scale. Candidates are ranked on semantic similarity, recency, graph centrality, and extraction confidence, weighted into a final relevance score. The top-k results are returned as structured context, enriched with entity and relationship metadata. ```python Python theme={null} import uuid conversation_id = str(uuid.uuid4()) # one UUID per conversation, reused across turns context = await sdk.conversation.context.fetch( conversation_id=conversation_id, user_id="user_123", search_query=["What do we know about Project Atlas, including who is involved and what decisions have been made?"], mode="accurate", ) for fact in context.facts: print(f"[{fact.confidence:.2f}] {fact.content}") if fact.entities: print(f" Entities: {', '.join(e.canonical_name for e in fact.entities)}") if fact.relationships: print(f" Relationships: {', '.join(str(r) for r in fact.relationships)}") ``` ```javascript JavaScript theme={null} import { randomUUID } from 'node:crypto'; let conversation_id = randomUUID(); // one UUID per conversation, reused across turns const context = await sdk.conversation.context.fetch({ conversation_id: conversation_id, user_id: 'user_123', search_query: ['What do we know about Project Atlas, including who is involved and what decisions have been made?'], mode: 'accurate', }); for (const fact of context.facts ?? []) { console.log(`[${(fact.confidence ?? 0).toFixed(2)}] ${fact.content}`); if (fact.entities) { console.log(` Entities: ${(fact.entities ?? []).map((e) => e.canonical_name).join(', ')}`); } if (fact.relationships) { console.log(` Relationships: ${(fact.relationships ?? []).map((r) => str(r)).join(', ')}`); } } ``` ```typescript TypeScript theme={null} import { randomUUID } from 'node:crypto'; let conversation_id = randomUUID(); // one UUID per conversation, reused across turns const context = await sdk.conversation.context.fetch({ conversation_id: conversation_id, user_id: 'user_123', search_query: ['What do we know about Project Atlas, including who is involved and what decisions have been made?'], mode: 'accurate', }); for (const fact of context.facts ?? []) { console.log(`[${(fact.confidence ?? 0).toFixed(2)}] ${fact.content}`); if (fact.entities) { console.log(` Entities: ${(fact.entities ?? []).map((e) => e.canonical_name).join(', ')}`); } if (fact.relationships) { console.log(` Relationships: ${(fact.relationships ?? []).map((r) => str(r)).join(', ')}`); } } ``` ### What graph traversal adds The same query, fast vs accurate: Returns memories that directly mention "Project Atlas": ``` [0.92] Project Atlas timeline may need to shift to Q4. Q3 deadline flagged as infeasible. [0.87] Project Atlas kickoff meeting scheduled for January 15th. [0.81] User asked about the current status of Project Atlas. ``` Useful, but limited to direct mentions. Returns the same direct mentions plus connected context discovered through graph traversal: ``` [0.92] Project Atlas timeline may need to shift to Q4. Q3 deadline flagged as infeasible. [0.89] Sarah Chen from engineering is concerned about the Q3 deadline for Project Atlas. Entities: Sarah Chen (person, engineering) Relationship: Sarah Chen --[concerned_about]--> Project Atlas timeline [0.85] James from DevOps estimates three more weeks for the infrastructure database migration that is blocking Project Atlas. Relationship: database migration --[blocks]--> Project Atlas [0.82] Maria approved budget for two additional engineers from the platform team to accelerate Project Atlas delivery. Relationship: Maria --[approved]--> additional engineering budget [0.78] The platform team currently has six engineers and is working on the API gateway redesign. ``` Traversal followed edges from Project Atlas to Sarah Chen, James, Maria, and the platform team. The last result (the platform team's workload) was discovered by traversing to the platform team entity, even though it never mentions Project Atlas. Accurate retrieval is most effective when the content was ingested with `long-range`. The relationship edges available to traverse come from long-range **ingestion**, not from the retrieval mode. Accurate retrieval still queries both stores regardless, but fast-ingested content has fewer edges to traverse, so you get less of the graph-enhanced context that makes accurate mode valuable. ## Precision level `mode` isn't the only retrieval knob. Context fetch also accepts an optional `precision_level` parameter: a second, independent axis that controls how tightly results are filtered before they're returned. | `precision_level` | Behavior | | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | `high` | Results go through an additional relevance-refinement pass before being returned. **Default.** | | `medium` | Skips the refinement pass for faster responses. Recall isn't impacted (the same candidate memories are searched), but outputs are less precisely filtered. | The refinement pass filters candidates rather than finding them, so dropping to `medium` never shrinks what's searched. What changes is how precisely the output is filtered: expect an occasional loosely-related item in exchange for a faster response. `precision_level` is orthogonal to `mode`: combine it with either `fast` or `accurate`. For real latency on your instance, see **Dashboard → Usage**. ## Choosing a mode * **Real-time conversations** where the user is waiting: fast retrieval is rarely the bottleneck; LLM generation dominates response time. * **Single-topic queries** answerable from one memory chunk ("What is our refund policy?", "When is Alice's birthday?"). * **High-frequency retrieval** on every message, at scale: the lower compute cost matters. * **Latency-sensitive** apps: voice agents, real-time collaboration. * **Complex, multi-entity queries**: "Summarize everything about Project Atlas, who's involved, and what's been decided." * **Relationship queries**: "How is Sarah connected to the infrastructure migration?" * **Comprehensive summaries / briefings** that must not miss context (latency is acceptable since they aren't time-sensitive). * **Onboarding / profile-building**, where deep extraction builds a richer profile that even later fast-mode reads benefit from. * **High-value interactions**: escalations, renewals, executive conversations. ## Mixing modes in practice Most production apps combine both, fast by default, accurate for the queries and writes that justify it: ```python theme={null} async def get_context(conversation_id, user_id, query, is_complex=False): return await sdk.conversation.context.fetch( conversation_id=conversation_id, user_id=user_id, search_query=[query], mode="accurate" if is_complex else "fast", ) async def ingest_conversation(content, user_id, is_important=False): await sdk.memories.create( document=content, document_type="ai-chat-conversation", user_id=user_id, mode="long-range" if is_important else "fast", ) ``` A simple keyword heuristic is a reasonable starting point for automatic selection (tune it to your query patterns, or use a lightweight classifier): ```python theme={null} def should_use_accurate_mode(query: str) -> bool: complex_indicators = [ "summarize", "everything about", "full briefing", "who is involved", "related to", "connected to", "all the details", "comprehensive", "overview of", "history of", "timeline for", ] q = query.lower() return any(indicator in q for indicator in complex_indicators) ``` You can also override the per-write default in batch ingestion: e.g. `mode="fast"` on `memories.batch_create(...)` items when speed beats extraction depth for high-volume, lower-priority data. ## Next steps Full SDK reference for retrieval methods and mode selection. How runtime ingestion integrates fast mode into the agent loop. Configure ingestion and retrieval defaults in your memory architecture. How long-range's deep entity resolution builds the knowledge graph. # Scope ladder Source: https://docs.maximem.ai/concepts/scope-ladder The scope chain is a ladder of named levels, and the familiar Client, Customer, User chain is the default three rungs. This page explains what a level is, why a level's name can change and its key cannot, which direction a read travels, and exactly which parts of custom depth you can reach today. Read [Identifiers & Scopes](/concepts/memory-scopes) first. This page assumes you know what `customer_id` and `user_id` do. If your account uses the default `Client → Customer → User` chain, nothing here changes how you integrate. This page matters when that shape does not describe your business. ## What a level is A scope is a position on a ladder. Your account is the top rung, and each rung below it narrows who a memory belongs to. The default ladder has three: ``` client your account customer an organization you serve user one person inside it ``` Every memory is filed at exactly one rung, and it carries that rung plus every rung above it. That stored list is what a read filters on. ### One ladder per account You have one ladder, and every instance you run shares it. There is no per-instance ladder. This follows from what an id means. A customer id identifies the same customer across your whole account, and a user id the same person, whichever instance sent the request. Two instances writing about customer `acme` are writing about one customer. If they disagreed about what the levels are, the same records would carry different level names depending on which instance wrote them, and a read from one would not find what the other stored. Instances keep their own memory settings. The switch that turns nested scoping on is also per instance, so you can enable it for one instance and watch it before enabling the rest. The shape is shared; the moment each instance starts using it is not. ## If your account is B2C Some accounts are set up so that a customer and a person are the same thing: one consumer, using your product for themselves, belonging to no organisation. Synap calls this `user_context_isolation: equals_customer`, and your integration sends a `user_id` and no `customer_id`. **On those accounts the customer rung and the user rung are one node.** Not two levels that happen to have one member each. The same node, reached by either name: ``` you send: user_id = "person-7" Synap resolves: customer = person-7 and user = person-7 ``` That has three consequences worth knowing before you change your ladder. **A rung between customer and user has nowhere to sit.** It would be a container between one person and the same person. Synap will accept the shape, and a request naming it will resolve, but it is almost never what anyone means. **A rung ABOVE the customer is the shape that works.** Something people belong to, rather than something inside a person: ``` client > region > person ``` That groups consumers by something real, and does not fight the collapse. **Your integration must keep sending only `user_id`.** Sending a `customer_id` to a B2C account is refused with `customer_id_not_accepted_on_b2c`, and a scope path does not change that. If you send a path, name the customer rung with the same value as the user rung. **A deeper ladder means your writes must send a path.** With a rung between the customer and the user, `user_id` alone can no longer say which one a memory belongs to, and Synap refuses the write rather than guessing. Upgrade to Python SDK 0.4.8 or JS SDK 0.4.7 and send the full path before adding such a rung, not after. One gap to know about first: only `memories.create` takes a path today. Transcript ingest does not, so if you push transcripts, talk to us before you add a rung between your customer and your user. ## Where your ladder comes from You do not start from a blank ladder, and you do not start from ours either. When you onboard, you give us a use-case document: what your agent does, who it talks to, and what it should remember. That document already answers the scoping question, usually in your own words. A fintech client wrote "**Client**: the company operating the platform. **Customer**: an Indian retail consumer." A clinical client wrote "memory is about the patient, not the clinician using the application," which is a different shape entirely and says so plainly. So we read it and propose a ladder from it: how many rungs, what each one is called, and one sentence per rung explaining what it holds. The proposal quotes the line in your document that each rung came from, so you can see the reasoning rather than take it on trust. The proposal is a draft. It is not applied, and nothing about how your memories are stored or read changes until you accept it. See [Ratifying it](#ratifying-it). If you did not upload a use-case document, or it does not say anything about who your users are, we propose the default three rungs and tell you that is what happened. We would rather show you a plain default and say so than invent a structure from a document that does not support one. ### What we will and will not infer We name rungs from what you wrote. We do not invent privacy boundaries from prose that does not ask for one. | From your document | What we do | | ----------------------------------------------------------- | --------------------------------------------------------------------------- | | A section naming what Client, Customer and User mean to you | Use your words as the rung names and descriptions | | "Memory is about the patient, not the clinician" | Propose a rung for the subject, separate from the person using the agent | | Users grouped by team, branch, practice or region | Propose that grouping as a rung between your customers and your people | | Nothing about grouping | Propose the default three rungs, and say the document did not indicate more | A rung decides who can read whose memories. Adding one that you did not ask for would be a privacy decision made by inference, so a rung we are not confident about is raised as a question in the proposal rather than added to it. ## Ratifying it A proposed ladder does nothing. It is a draft until a person accepts it, and until then your account keeps using the default chain. Accepting records who accepted and when. That record matters because a ladder is close to permanent: a rung can be renamed at any time and can never be deleted, only retired, because memories already resolve through it. Before you accept, you can: * **rename any rung**, which is free and changes nothing about who reads what * **remove a proposed rung** you do not want * **add one** we did not propose After you accept, renaming stays free, adding a rung is a migration we show you the size of first, and removing is no longer possible. An unratified ladder is never used at runtime. If you never accept the proposal, your account keeps the default `Client → Customer → User` chain indefinitely and nothing breaks. ## Which direction a read travels This is the rule worth reading twice, because it is easy to hold backwards. **A request sees its own rung and every rung above it. It never sees a rung below, and never a neighbour.** So on the default ladder: | A request identifying | Can read memories filed at | | --------------------- | --------------------------------------- | | a user | that user, their customer, your account | | a customer | that customer, your account | | your account only | your account | A memory filed at the user level is **not** visible to a request that identifies only the customer. One person's private context does not surface for their colleague, and it does not surface for a customer-wide question. A memory's stored chain is the path down **to** it. That is the list of rungs the memory can be read **from**, not a list of places the memory is visible. A fact filed at the user rung carries `client, customer, user`, and it is still readable only by that user. ### Nothing sits below the person The rung that identifies a person is always the last one. You cannot add a rung under it, and Synap refuses a ladder that tries. Two reasons, and both are hard to undo later. A rung under a person multiplies the work of consolidating memories against the busiest part of your tree, and it makes deletion ambiguous: erasing a person would first have to decide what happens to everything filed underneath them. The account rung at the top is fixed in the same way. It is your account itself, so nothing can go above it. Every rung you add goes somewhere in between. ## Ranking: closer material wins Filtering decides what a request is allowed to read. Ranking decides what it sees first. Among the memories a request may read, the ones filed closer to the person asking rank above the ones filed further up. A person's own preference outranks a fact that is true of their whole organization, when both match the question equally well. This is a ranking adjustment only: it never makes a memory readable that was not, and it never hides one that was. ## A level's name can change, its key cannot Every level has two parts. * The **label** is what people read: "Customer", "Practice", "Region". Change it whenever you like. Nothing stored depends on it. * The **key** is what stored memories resolve through: `customer`, `user`. It never changes, because every memory already filed under it would stop resolving. For the same reason a level can never be deleted, only **deprecated**. A deprecated level stops being offered for new structure, and everything already filed under it stays readable. ## Sending a scope path Requests normally identify a scope with `customer_id` and `user_id`, and Synap maps those onto the default three rungs. That is what almost every integration does, and it needs no change. **Why two ids only ever reach three rungs.** They are matched by ROLE, not by position: `customer_id` goes to whichever rung holds `tenant_of_record`, and `user_id` to whichever holds `identity`. There are exactly three reserved roles, so two ids can address exactly three rungs. A rung you added yourself holds no reserved role, so there is no field for it and no way to guess one. That is the whole difference: | | what you send | what it means | | ---------- | --------------------------------------------------- | --------------------------------------------- | | Ids | `customer_id`, `user_id` | "here are two values, work out where they go" | | Scope path | `{"customer": "...", "team": "...", "user": "..."}` | "here is what belongs on each rung, by name" | On the default three rungs the two are equivalent, and a path is simply the longer way to say the same thing. A path starts to matter only when your ladder has a rung the ids cannot name. To address a level beyond those three, pass a scope path from the SDK, naming every rung by its key. On a read it is the `scope_path` argument: ```python Python theme={null} context = await sdk.user.context.fetch( user_id="d-42", search_query=["what did we agree on pricing"], max_results=10, scope_path={ "customer": "acme", "team": "payments", "user": "d-42", }, ) ``` ```javascript JavaScript theme={null} const context = await sdk.user.context.fetch({ user_id: 'd-42', search_query: ['what did we agree on pricing'], max_results: 10, scope_path: { customer: 'acme', team: 'payments', user: 'd-42', }, }); ``` On a write it is the `scope` argument on `memories.create`: ```python Python theme={null} await sdk.memories.create( document="Dana wants invoices on the first of the month.", user_id="d-42", customer_id="acme", scope={ "customer": "acme", "team": "payments", "user": "d-42", }, ) ``` ```javascript JavaScript theme={null} await sdk.memories.create({ document: 'Dana wants invoices on the first of the month.', user_id: 'd-42', customer_id: 'acme', scope: { customer: 'acme', team: 'payments', user: 'd-42', }, }); ``` Two rules apply, and both fail loudly rather than guessing: * **Every rung must be named.** A path that skips a level is refused, and the error names the level you left out. Synap never infers a missing id, because a wrong guess at or above the organization level would cross a tenant boundary. * **The feature must be enabled for your instance.** If it is not, sending a `scope` map is an error rather than being ignored. Ignoring it would widen the request to the whole organization when you asked to narrow it to one team. **Check your SDK version before you rely on this.** Reads take a path from the Python SDK at 0.4.6 or later, and from the JS SDK at 0.4.4 or later. Writes take one on `memories.create` from Python 0.4.8 and JS 0.4.7. On anything older, the `scope` argument on a fetch names which method you called (`user`, `customer`, `client`), not a path, so an older SDK reaches the default three rungs and nothing beyond them. Upgrade with `pip install -U maximem-synap` or `npm install @maximem/synap-js-sdk@latest`. gRPC cannot carry a path at all. Transcript ingest does not take a scope path in any released SDK yet. If your ladder has a rung between your customer and your user and you push transcripts, talk to us before you add it. ## What you can reach today Being precise about this matters more than making the feature sound finished. | | Available now | | ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | The default three-rung ladder | Yes, on every account, from signup | | Reading and ranking across those rungs | Yes | | Reading with a `scope` path | Python SDK 0.4.6 or later, JS SDK 0.4.4 or later | | Writing with a `scope` path | `memories.create` only, from Python 0.4.8 and JS 0.4.7 | | Sending a path on transcript ingest | Not yet, in any released SDK | | Sending one from an older SDK, or from gRPC | No. Upgrade the SDK; gRPC cannot carry a path at all | | Adding a level beyond the default three | In the dashboard, with a preview of how many records move. Your integration has to be able to send a scope path first | | Renaming a level's label | In the dashboard | | Deleting a level | Never; deprecate instead | | A level below the person, or above your account | Never | | Sharing between levels that are not ancestors | No | The honest summary: **Synap's scoping is correct and safe on the shape accounts already use. Driving a different shape needs an SDK new enough to send a scope path, and on writes that means `memories.create`.** Which of these you can reach is decided by what your own integration sends, and that is something you can change. You do not have to go and find out what it sends: the Scope Ladder page in your dashboard lists the SDK versions we have seen in your recent requests, tells you the oldest one, and says so plainly when it has not seen any requests from you at all. ## Adding a level **Check what your integration can send before you add one.** A level outside the default three is only reachable by a call that sends a scope path. If your calls come from a Python SDK older than 0.4.8, or a JS SDK older than 0.4.7, your writes cannot send one, so adding a level does not give you a level: writes aimed at it fail with `CannotPlaceWrite`. Upgrade first, with `pip install -U maximem-synap` or `npm install @maximem/synap-js-sdk@latest`. This is a fact about your integration, not a limit on your account, and it goes away when you upgrade. The Scope Ladder page in your dashboard checks it for you: it names the SDK versions in your recent traffic, and it will not let you add a level the oldest of them cannot address. If it has not seen any requests from you, or your requests do not report a version, it says that rather than guessing. A level goes between two existing ones. It cannot go above your account rung or below the rung that identifies a person, so on the default ladder that means above Customer or above User. Either way it is a migration rather than a setting: every node beneath the insertion point is re-parented under a new node at the new level, in one transaction, and Synap creates one placeholder parent per existing group so nothing is left without one. Those placeholders are yours to rename. Reads are unaffected while this happens. Memories above the insertion point are never touched, and no memory changes the group it belongs to. One thing lags. Existing memories do not immediately carry the new level as one of their ancestors, so a query asking for everything beneath the new level is incomplete until Synap finishes stamping them. Ordinary retrieval is not affected. Ask us to run the backfill after you add a level. # Salesforce: Enterprise Sales Assistant Source: https://docs.maximem.ai/cookbook/b2b-salesforce Account-grounded sales assistant with opportunity history and CRM-aware recommendations. **Status:** Live in Playground · **Try it:** [synap.maximem.ai/playground](https://synap.maximem.ai/playground) Open the playground and pick **Salesforce: Enterprise Sales Assistant** to see the reference implementation running. A sales-rep-facing assistant that lives next to Salesforce. It knows the rep's accounts, open opportunities, the deal narrative across recent activity, and what the rep has tried before. It drafts outreach, prepares for calls, logs activity, and updates the CRM, all while keeping the rep's tone and territory context across sessions. ## What you'll build A chat agent for sales reps that: * **Pulls live CRM state**: accounts, opps, contacts, recent activity * **Remembers rep context**: territory, ICP, tone of voice, deal-specific narratives * **Drafts outreach** in the rep's voice, grounded in account history * **Updates Salesforce**: log calls, advance opp stages, create follow-up tasks **Est. build time:** 45 to 60 minutes (most of that is Salesforce auth + field mapping). ## When to use this recipe Build this if: * Your reps work out of Salesforce and want an assistant that already knows their book of business * You want per-rep tone/persona memory (so drafts sound like the rep, not a generic LLM) * You need bi-directional CRM I/O (read accounts, write activities) * Account narratives span weeks and need to persist across sessions ## Architecture at a glance ```mermaid theme={null} flowchart TD Chat[Sales rep chat
sidebar / Slack / web] --> Backend[Your backend] Backend -->|fetch| Synap1[(Synap context fetch
rep tone, territory, account narratives, prior drafts)] Synap1 --> LLM[LLM with tools] LLM --> Tools["get_account
get_opportunities
get_contacts
query_pipeline
log_activity
update_opportunity
create_task"] Tools <--> SF[(Salesforce REST)] Tools --> Reply[Reply to rep] Reply -.->|fire-and-forget| Synap2[(Synap ingest turn)] ``` The rep is the `user_id`; the rep's org is the `customer_id`. Account-specific notes get tagged in metadata so they're recallable per-account. ## Stack | Layer | Choice | | ------------- | --------------------------------------------------------------------------------------------------------------------- | | **Synap SDK** | `maximem-synap` (Python) / `@maximem/synap-js-sdk` (TypeScript) | | **Framework** | [OpenAI Agents SDK](/integrations/openai-agents) (Python) / [Vercel AI SDK](/integrations/vercel-ai-sdk) (TypeScript) | | **CRM** | Salesforce REST + jsforce (Node) or simple-salesforce (Python) | | **LLM** | OpenAI `gpt-4o` | | **Channel** | Sidebar widget / Slack DM / web chat, choose what fits your reps | ## Prerequisites * A Synap API key, see [Authentication](/setup/authentication) * A Salesforce Connected App with OAuth + offline refresh tokens for the reps * **Python:** Python 3.11+ * **TypeScript:** Node.js 20+ TypeScript recipe runs on Node only. Pin Next.js route handlers to `export const runtime = "nodejs"`. See [Installation → JavaScript / TypeScript SDK](/setup/installation#javascript-and-typescript-sdk). ### Install ```bash Python theme={null} pip install maximem-synap maximem-synap-openai-agents openai-agents simple-salesforce ``` ```bash uv theme={null} uv add maximem-synap maximem-synap-openai-agents openai-agents simple-salesforce # pip-compatible (existing venv): uv pip install maximem-synap maximem-synap-openai-agents openai-agents simple-salesforce ``` ```bash TypeScript theme={null} npm install @maximem/synap-js-sdk @maximem/synap-vercel-adk ai @ai-sdk/openai zod jsforce ``` ### Configure ```bash Python theme={null} # .env SYNAP_API_KEY=... OPENAI_API_KEY=... SALESFORCE_CLIENT_ID=... SALESFORCE_CLIENT_SECRET=... ``` ```bash TypeScript theme={null} # .env.local SYNAP_API_KEY=... OPENAI_API_KEY=... SALESFORCE_CLIENT_ID=... SALESFORCE_CLIENT_SECRET=... ``` ## Build it ### 1. Identity & scoping * `customer_id = `: one tenant per Salesforce org * `user_id = `: the sales rep * `conversation_id = ` * Account-scoped memories get `account_id` in metadata so you can retrieve "everything we know about Acme Corp" later `conversation_id` must be a valid UUID; generate it with `crypto.randomUUID()` (JS) or `str(uuid.uuid4())` (Python), as shown below. Any Salesforce id used for `user_id`/`customer_id` that isn't already a UUID should be mapped to a deterministic UUID (e.g. `uuid.uuid5(...)`). ```python Python theme={null} SESSIONS: dict[str, str] = {} def conv_for(session_id: str) -> str: return SESSIONS.setdefault(session_id, str(uuid.uuid4())) ``` ```typescript TypeScript theme={null} const SESSIONS = new Map(); function convFor(sessionId: string): string { if (!SESSIONS.has(sessionId)) SESSIONS.set(sessionId, crypto.randomUUID()); return SESSIONS.get(sessionId)!; } ``` ### 2. Salesforce tools These wrap your Salesforce client. Authenticate per-rep using their stored refresh token before each call. ```python Python theme={null} from agents import function_tool @function_tool async def get_account(account_id: str) -> dict: """Return account name, industry, ARR, owner, and open opp count.""" sf = await get_sf_client_for_rep() return sf.Account.get(account_id) @function_tool async def get_opportunities(account_id: str, stage: str = None) -> list[dict]: """List open opps for an account, optionally filtered by stage.""" sf = await get_sf_client_for_rep() soql = f"SELECT Id, Name, StageName, Amount, CloseDate FROM Opportunity WHERE AccountId = '{account_id}'" if stage: soql += f" AND StageName = '{stage}'" return sf.query(soql)["records"] @function_tool async def get_contacts(account_id: str) -> list[dict]: sf = await get_sf_client_for_rep() return sf.query( f"SELECT Id, Name, Title, Email FROM Contact WHERE AccountId = '{account_id}'" )["records"] @function_tool async def query_pipeline(stage: str = None, close_before: str = None) -> list[dict]: """Query the rep's own pipeline. Returns opps owned by the current rep.""" sf = await get_sf_client_for_rep() soql = "SELECT Id, AccountId, Name, StageName, Amount, CloseDate FROM Opportunity WHERE OwnerId = '{me}'" if stage: soql += f" AND StageName = '{stage}'" if close_before: soql += f" AND CloseDate <= {close_before}" return sf.query(soql.format(me=sf.user_id))["records"] @function_tool async def log_activity(opportunity_id: str, subject: str, body: str) -> dict: sf = await get_sf_client_for_rep() return sf.Task.create({"WhatId": opportunity_id, "Subject": subject, "Description": body}) @function_tool async def update_opportunity(opportunity_id: str, fields: dict) -> dict: sf = await get_sf_client_for_rep() return sf.Opportunity.update(opportunity_id, fields) @function_tool async def create_task(opportunity_id: str, subject: str, due_date: str) -> dict: sf = await get_sf_client_for_rep() return sf.Task.create({ "WhatId": opportunity_id, "Subject": subject, "ActivityDate": due_date, "Status": "Open", }) ``` ```typescript TypeScript theme={null} import { tool } from "ai"; import { z } from "zod"; const sfTools = { get_account: tool({ description: "Return account name, industry, ARR, owner, and open opp count.", parameters: z.object({ accountId: z.string() }), execute: async ({ accountId }) => (await getSfClient()).sobject("Account").retrieve(accountId), }), get_opportunities: tool({ description: "List open opps for an account, optionally filtered by stage.", parameters: z.object({ accountId: z.string(), stage: z.string().optional() }), execute: async ({ accountId, stage }) => { const sf = await getSfClient(); let soql = `SELECT Id, Name, StageName, Amount, CloseDate FROM Opportunity WHERE AccountId = '${accountId}'`; if (stage) soql += ` AND StageName = '${stage}'`; return (await sf.query(soql)).records; }, }), get_contacts: tool({ description: "List contacts on an account.", parameters: z.object({ accountId: z.string() }), execute: async ({ accountId }) => { const sf = await getSfClient(); return (await sf.query( `SELECT Id, Name, Title, Email FROM Contact WHERE AccountId = '${accountId}'` )).records; }, }), log_activity: tool({ description: "Log an activity / call note against an opportunity.", parameters: z.object({ opportunityId: z.string(), subject: z.string(), body: z.string() }), execute: async ({ opportunityId, subject, body }) => (await getSfClient()).sobject("Task").create({ WhatId: opportunityId, Subject: subject, Description: body, }), }), update_opportunity: tool({ description: "Update fields on an opportunity (stage, amount, close date, etc.).", parameters: z.object({ opportunityId: z.string(), fields: z.record(z.any()) }), execute: async ({ opportunityId, fields }) => (await getSfClient()).sobject("Opportunity").update({ Id: opportunityId, ...fields }), }), create_task: tool({ description: "Create a follow-up task tied to an opportunity.", parameters: z.object({ opportunityId: z.string(), subject: z.string(), dueDate: z.string(), }), execute: async ({ opportunityId, subject, dueDate }) => (await getSfClient()).sobject("Task").create({ WhatId: opportunityId, Subject: subject, ActivityDate: dueDate, Status: "Open", }), }), }; ``` The SOQL examples above use string interpolation for clarity. **In production, parameterize all SOQL inputs** to avoid injection; both `simple-salesforce` and `jsforce` support bind parameters. ### 3. System prompt ```text System prompt theme={null} You are a sales assistant embedded next to Salesforce. The user is a sales rep. - Always check the rep's pipeline and the relevant account before suggesting actions. - Use the rep's known tone, ICP framing, and prior account narratives from memory when drafting outreach. - Never invent fields. If a value isn't in the CRM or memory, say so and ask. - When asked to draft, return the draft text; do not auto-send. - When asked to update the CRM, summarize the change and confirm before calling the update tool, unless the rep prefixes the request with "just" ("just log that call"). - Keep replies tight. Reps are busy. ``` ### 4. Wire memory + LLM + tools ```python Python theme={null} import os, uuid, asyncio from agents import Agent, FunctionTool, Runner from maximem_synap import MaximemSynapSDK from synap_openai_agents import create_search_tool, create_store_tool sdk = MaximemSynapSDK() await sdk.initialize() async def handle_message(rep_id: str, org_id: str, session_id: str, text: str) -> str: conv_id = conv_for(session_id) synap_search = create_search_tool(sdk=sdk, user_id=rep_id, customer_id=org_id) synap_store = create_store_tool(sdk=sdk, user_id=rep_id, customer_id=org_id) agent = Agent( name="sf_sales_assistant", instructions=SYSTEM, tools=[ FunctionTool(synap_search, name_override="synap_search"), FunctionTool(synap_store, name_override="synap_store"), get_account, get_opportunities, get_contacts, query_pipeline, log_activity, update_opportunity, create_task, ], ) result = await Runner.run(agent, input=text) reply = result.final_output asyncio.create_task(sdk.memories.create( document=f"Rep: {text}\nAssistant: {reply}", document_type="ai-chat-conversation", user_id=rep_id, customer_id=org_id, metadata={"conversation_id": conv_id, "channel": "sf-sidebar"}, )) return reply ``` ```typescript TypeScript theme={null} import { generateText } from "ai"; import { openai } from "@ai-sdk/openai"; import { createSynap } from "@maximem/synap-vercel-adk"; const synap = await createSynap({ apiKey: process.env.SYNAP_API_KEY! }); export async function handleMessage( repId: string, orgId: string, sessionId: string, text: string, ): Promise { const conversationId = convFor(sessionId); const model = synap.wrap(openai("gpt-4o"), { userId: repId, customerId: orgId, conversationId, }); const { text: reply } = await generateText({ model, system: SYSTEM, prompt: text, tools: sfTools, }); return reply; } ``` ### 5. Tagging account-scoped memories For richer recall ("what's the latest on Acme?"), tag memories with the account when the conversation is about a specific account: ```python Python theme={null} asyncio.create_task(sdk.memories.create( document=note_text, document_type="sales-note", user_id=rep_id, customer_id=org_id, metadata={"account_id": account_id, "opportunity_id": opp_id}, )) ``` ```typescript TypeScript theme={null} synap.sdk.memories.create({ document: noteText, documentType: "sales-note", userId: repId, customerId: orgId, metadata: { accountId, opportunityId: oppId }, }); ``` You can then filter retrieval by `metadata.account_id` for account-specific summarization. ## Run & verify ```text Session 1 theme={null} Rep: I'm prepping for a call with Acme Corp tomorrow. What's the latest? Assistant: Acme Corp: open opp "Acme Q2 Expansion", $120K, stage Proposal, close 2026-06-15. Last activity: Maya (champion, VP Eng) raised concerns about SSO timeline on 5/8. You drafted a response covering Q3 SSO availability but didn't send it. Want the draft text? Rep: Yes, and remind me Maya prefers Loom over Zoom for product walk-throughs. Assistant: Noted (Maya prefers Loom). Here's the draft: ... ``` ```text Session 2 (next day, fresh conversation) theme={null} Rep: Call with Maya done. Going to log it: we agreed on SSO by Q3, she's bringing security in next week. Assistant: Logging on Acme Q2 Expansion: "Call with Maya, agreed SSO by Q3, security review next week." Want me to also create a follow-up task to prep the security review deck? Rep: Yes, due Friday. Assistant: Task created on Acme Q2 Expansion, due Friday. (Also remembered: Maya prefers Loom for follow-ups.) ``` Memory carries across days. CRM stays the source of truth for structured deal state; Synap carries the *narrative*. ## Customize / extend * **Slack interface** → wrap `handle_message` in a Slack bot. See [Patterns → Slack Bot](/patterns/slack-bot). * **Replay historical activity** on initial setup so the assistant has years of context from day one. See [Patterns → Replay History](/patterns/replay-history). * **Per-territory scoping** → if a single rep covers multiple territories that shouldn't share context, use `metadata.territory` to filter. * **HubSpot / other CRMs** → replace the Salesforce tool layer; the memory pattern is identical. * **SDR variant** → for top-of-funnel work, see [AI SDR](/cookbook/b2b-sdr). ## Troubleshooting **Account narrative goes missing across sessions** * Confirm `customer_id` is the org ID, not the rep ID. Reps within the same org should see shared account context if you want that. * If you want strict per-rep silos, keep `customer_id = org_id` and rely on `user_id` for isolation; Synap scopes searches automatically. **Drafts don't sound like the rep** * The rep hasn't given enough signal yet. Capture explicit corrections ("don't open with 'I hope this finds you well'") with `synap_store`. * Or seed memory at onboarding with 5 to 10 of the rep's recent sent emails as historical documents. **Tools timing out** * Salesforce REST has per-org rate limits. Cache `get_account` and `get_opportunities` for the duration of one chat session. **TS route fails on Vercel** * Pin `export const runtime = "nodejs"`. JS SDK requires Node + Python on the host. ## Related * **Integrations:** [OpenAI Agents SDK](/integrations/openai-agents) · [Vercel AI SDK](/integrations/vercel-ai-sdk) * **Concepts:** [Customer Context](/concepts/context-end-to-end#customer-context) · [Memory Scopes](/concepts/memory-scopes) · [Organizational Context](/concepts/context-end-to-end#organizational-context) * **Patterns:** [Slack Bot](/patterns/slack-bot) · [Replay History](/patterns/replay-history) · [Multi-Tenant SaaS](/patterns/multi-tenant-saas) * **Guides:** [Multi-User Memory Scoping](/guides/multi-user-scoping) * **Other recipes:** [AI SDR](/cookbook/b2b-sdr) · [Tier Escalation](/cookbook/support-tier-escalation) # AI SDR Source: https://docs.maximem.ai/cookbook/b2b-sdr Outbound B2B prospecting agent: research, personalize, sequence, book, with prospect memory across touches. **Status:** In Development · Playground demo coming soon. The recipe below is complete and runnable today; only the hosted playground showcase is pending. A B2B SDR that runs structured outbound sequences while keeping a real picture of each prospect: what's been said, what they replied to, what they ignored, and what they care about. Memory is per-prospect; the agent doesn't restart from zero on every touch. ## What you'll build An outbound SDR agent that: * **Researches the prospect**: company, role, recent signals * **Drafts personalized first-touch**: grounded in researched facts, not generic * **Runs multi-touch sequences**: email + LinkedIn DM + follow-up, with reply detection * **Adapts on replies**: interest, objection, unsubscribe, out-of-office * **Books meetings** when intent is detected **Est. build time:** 90 minutes (most of it wiring email/LinkedIn/calendar tools). ## When to use this recipe Build this if: * You run cold outbound sequences and want them to feel less cold * You've got research data (your enrichment provider, company news, signal data) the agent should ground in * Multi-touch is the norm: a prospect sees the SDR over weeks, not minutes * You want the agent to *learn* what works for each persona over time ## Architecture at a glance ```mermaid theme={null} flowchart TD Sched[Sequence orchestrator
cron / queue per prospect] --> Due[For each due touch] Due --> Fetch[(Synap context fetch
prior touches, replies, objections)] Due --> Research[Research tools
fresh enrichment, recent signals] Fetch --> Draft[LLM drafts message] Research --> Draft Draft --> Review[Review queue
optional] Draft --> Send[Send via email / LinkedIn] Send -.-> Ingest1[(Synap ingest: outbound)] Reply[Inbound reply] --> Webhook[Webhook] Webhook --> Ingest2[(Synap ingest: inbound)] Ingest2 --> Classify[Classify intent
adjust sequence:
book / objection / nurture / kill] ``` The sequence orchestrator is dumb. The agent is smart. Memory is the bridge. ## Stack | Layer | Choice | | ------------- | --------------------------------------------------------------------------------------------------------------------- | | **Synap SDK** | `maximem-synap` (Python) / `@maximem/synap-js-sdk` (TypeScript) | | **Framework** | [OpenAI Agents SDK](/integrations/openai-agents) (Python) / [Vercel AI SDK](/integrations/vercel-ai-sdk) (TypeScript) | | **Email** | Postmark / SendGrid / your transactional provider | | **LinkedIn** | Your LinkedIn automation tool, must be compliant in your jurisdiction | | **Calendar** | Cal.com / Google Calendar API for booking links | | **Scheduler** | Celery + Redis (Python) / BullMQ + Redis (TypeScript) | | **LLM** | OpenAI `gpt-4o` (drafting quality matters here) | ## Prerequisites * A Synap API key, see [Authentication](/setup/authentication) * Email sender domain + DKIM / SPF / DMARC set up * Enrichment data source (Clearbit, Apollo, Crunchbase, or your own CRM) * **Python:** Python 3.11+ * **TypeScript:** Node.js 20+ Cold outbound is regulated (CAN-SPAM, GDPR, CASL). Make sure your sender list is permissioned and every email has a working unsubscribe. The agent doesn't enforce this; you do. ### Install ```bash Python theme={null} pip install maximem-synap maximem-synap-openai-agents openai-agents celery redis ``` ```bash uv theme={null} uv add maximem-synap maximem-synap-openai-agents openai-agents celery redis # pip-compatible (existing venv): uv pip install maximem-synap maximem-synap-openai-agents openai-agents celery redis ``` ```bash TypeScript theme={null} npm install @maximem/synap-js-sdk @maximem/synap-vercel-adk ai @ai-sdk/openai bullmq ioredis zod ``` ## Build it ### 1. Identity & scoping * `customer_id = ""` * `user_id = `: your stable internal ID, NOT the email (people change emails) * `conversation_id`: one per prospect (long-running) * Metadata: `account_id` so you can roll up "everything on Acme Corp" across all prospects at that company `conversation_id`, `user_id`, and `customer_id` must be valid UUIDs. Generate the per-prospect id with `str(uuid.uuid4())` (Python) or `crypto.randomUUID()` (JS); map any non-UUID internal id to a deterministic UUID with `uuid.uuid5(...)`. ### 2. Research tools ```python Python theme={null} from agents import function_tool @function_tool async def enrich_prospect(prospect_id: str) -> dict: """Fetch role, company, seniority, recent activity from your enrichment provider.""" return await enrichment.lookup(prospect_id) @function_tool async def recent_signals(company_id: str) -> list[dict]: """Recent news, hires, funding, product launches at a company. Last 90 days.""" return await signals.recent(company_id, days=90) @function_tool async def get_sequence_state(prospect_id: str) -> dict: """Where this prospect is in the sequence: current step, touches sent, replies.""" return await sequences.state(prospect_id) ``` ```typescript TypeScript theme={null} const researchTools = { enrich_prospect: tool({ description: "Fetch role, company, seniority, recent activity.", parameters: z.object({ prospectId: z.string() }), execute: async ({ prospectId }) => enrichment.lookup(prospectId), }), recent_signals: tool({ description: "Recent news, hires, funding, product launches at a company.", parameters: z.object({ companyId: z.string() }), execute: async ({ companyId }) => signals.recent(companyId, 90), }), get_sequence_state: tool({ description: "Where this prospect is in the sequence.", parameters: z.object({ prospectId: z.string() }), execute: async ({ prospectId }) => sequences.state(prospectId), }), }; ``` ### 3. Action tools ```python Python theme={null} @function_tool async def draft_email(prospect_id: str, kind: str, context: dict) -> dict: """Return a draft. kind: 'first_touch' | 'follow_up' | 'objection_response' | 'book_meeting'.""" return {"subject": "...", "body": "...", "draft_id": "..."} @function_tool async def send_email(draft_id: str) -> dict: """Send the previously-drafted email via your transactional provider.""" return await email.send_draft(draft_id) @function_tool async def book_meeting(prospect_id: str, preferred_times: list[str]) -> dict: """Generate a Cal.com link or hold proposed slots on the rep's calendar.""" return await calendar.book(prospect_id, preferred_times) @function_tool async def log_interaction(prospect_id: str, kind: str, details: dict) -> dict: """Log a touch into your CRM.""" return await crm.log(prospect_id, kind, details) ``` ```typescript TypeScript theme={null} const actionTools = { draft_email: tool({ description: "Draft an email. kind: 'first_touch'|'follow_up'|'objection_response'|'book_meeting'.", parameters: z.object({ prospectId: z.string(), kind: z.string(), context: z.record(z.any()), }), execute: async ({ prospectId, kind, context }) => drafter.compose(prospectId, kind, context), }), send_email: tool({ description: "Send a drafted email.", parameters: z.object({ draftId: z.string() }), execute: async ({ draftId }) => emailProvider.sendDraft(draftId), }), book_meeting: tool({ description: "Generate a booking link or hold slots.", parameters: z.object({ prospectId: z.string(), preferredTimes: z.array(z.string()), }), execute: async ({ prospectId, preferredTimes }) => calendar.book(prospectId, preferredTimes), }), log_interaction: tool({ description: "Log a touch into the CRM.", parameters: z.object({ prospectId: z.string(), kind: z.string(), details: z.record(z.any()), }), execute: async ({ prospectId, kind, details }) => crm.log(prospectId, kind, details), }), }; ``` ### 4. The agent ```python Python theme={null} SYSTEM = """You are an AI SDR. The user is a prospect or your own internal sequence orchestrator. When asked to draft a touch: - Always pull sequence_state, enrich_prospect, and recent_signals first. - Use prior touches and replies from memory; never re-tread points the prospect ignored or rejected. - Personalize on real facts. Generic "I see your company is growing" lines are forbidden. - Match the prospect's prior tone if you have one. Otherwise, be direct, no buzzwords, one ask per email. When a reply arrives, classify it: interested | objection | out_of_office | unsubscribe | not_now. - interested → book a meeting via book_meeting. - objection → store the objection in memory and draft a response addressing it. - out_of_office → snooze the sequence. - unsubscribe → kill the sequence immediately, log in CRM. - not_now → snooze to the date they suggest, or 30 days default. Always log_interaction with what you did and why.""" async def run_sdr(prospect_id: str, instruction: str) -> str: synap_search = create_search_tool(sdk=sdk, user_id=prospect_id, customer_id=CUSTOMER_ID) synap_store = create_store_tool(sdk=sdk, user_id=prospect_id, customer_id=CUSTOMER_ID) agent = Agent( name="ai_sdr", instructions=SYSTEM, tools=[ FunctionTool(synap_search, name_override="synap_search"), FunctionTool(synap_store, name_override="synap_store"), enrich_prospect, recent_signals, get_sequence_state, draft_email, send_email, book_meeting, log_interaction, ], ) result = await Runner.run(agent, input=instruction) return result.final_output ``` ```typescript TypeScript theme={null} const SYSTEM = `You are an AI SDR. When drafting: - Always pull sequence_state, enrich_prospect, recent_signals. - Use prior touches and replies from memory. Never re-tread rejected points. - Personalize on real facts. No generic openers. - One ask per email. When a reply comes in, classify: interested | objection | out_of_office | unsubscribe | not_now. - interested → book_meeting. - objection → store memory + draft response. - out_of_office → snooze. - unsubscribe → kill sequence + log. - not_now → snooze 30 days or to suggested date. Always log_interaction.`; export async function runSdr(prospectId: string, instruction: string): Promise { const model = synap.wrap(openai("gpt-4o"), { userId: prospectId, customerId: CUSTOMER_ID, conversationId: prospectId, }); const { text } = await generateText({ model, system: SYSTEM, prompt: instruction, tools: { ...researchTools, ...actionTools }, }); return text; } ``` ### 5. The orchestrator (scheduler) The orchestrator is a thin cron / queue worker. It picks prospects whose next touch is due and asks the agent to handle it. ```python Python theme={null} @celery.task async def run_due_touches(): due = await sequences.due_now() # your DB query for prospect_id in due: await run_sdr(prospect_id, "It's time for the next touch. Decide and execute.") ``` ```typescript TypeScript theme={null} // Cron job or BullMQ scheduled worker async function runDueTouches() { const due = await sequences.dueNow(); for (const prospectId of due) { await runSdr(prospectId, "It's time for the next touch. Decide and execute."); } } ``` ### 6. Reply handling Email reply webhooks (Postmark inbound, SES SNS, etc.) drop into a handler that ingests the reply and asks the agent to respond. ```python Python theme={null} async def handle_email_reply(prospect_id: str, body: str, from_email: str): await sdk.memories.create( document=f"Prospect reply (from {from_email}):\n{body}", document_type="prospect-reply", user_id=prospect_id, customer_id=CUSTOMER_ID, metadata={"channel": "email", "direction": "inbound"}, ) await run_sdr(prospect_id, "The prospect replied. Read their message and decide.") ``` ```typescript TypeScript theme={null} export async function handleEmailReply( prospectId: string, body: string, fromEmail: string, ) { await synap.sdk.memories.create({ document: `Prospect reply (from ${fromEmail}):\n${body}`, documentType: "prospect-reply", userId: prospectId, customerId: CUSTOMER_ID, metadata: { channel: "email", direction: "inbound" }, }); await runSdr(prospectId, "The prospect replied. Read their message and decide."); } ``` ## Run & verify ```text Touch 1 (first email) theme={null} [orchestrator triggers] → agent runs: - enrich_prospect: "Maya Chen, VP Eng at Acme, joined 6 months ago" - recent_signals: "Acme just shipped GA of their developer platform, June 4" - draft_email(kind=first_touch, context={ angle: "developer-platform launch" }) - send_email - log_interaction ``` ```text Prospect replies (3 days later) theme={null} "Thanks but we're already using . Not looking right now." [reply webhook] → agent classifies: objection (competitor) + not_now - synap_store: "Uses . Not actively shopping as of [date]." - draft_email(kind=objection_response, context={ objection: "uses competitor" }) - (or) snooze sequence to +60 days, depending on rules ``` ```text Touch 5, three months later theme={null} [orchestrator triggers] agent: - synap_search: finds "uses ", "not actively shopping June '26" - recent_signals: "Acme migrated off last week (reported on HN)" - draft_email referencing the migration, not the competitor by name, just the timing - send_email ``` The agent didn't restart from zero. It remembered the objection, watched for a signal, and re-engaged at the right moment. ## Customize / extend * **Salesforce CRM integration** → tools wire into Salesforce; see [Salesforce: Enterprise Sales Assistant](/cookbook/b2b-salesforce) for the read-side pattern. * **LinkedIn channel** → add `send_linkedin_dm` and `linkedin_reply` webhook. Same memory shape. * **Reply review queue** → for sensitive industries, don't auto-send. Have the agent draft into a queue your humans approve. * **Account-based marketing flavor** → group prospects by `account_id` in metadata and have the agent coordinate touches across the buying committee. * **Replay historical CRM activity** → seed prospect memory with prior touches from your CRM at launch. See [Patterns → Replay History](/patterns/replay-history). ## Troubleshooting **Drafts feel generic** * The agent isn't pulling enrichment or signals before drafting. Sharpen the system prompt; require those tool calls. * Or your enrichment source is sparse; feed the agent more. **Agent re-pitches points the prospect already rejected** * Memory ingestion of replies isn't working, or `synap_search` isn't called before drafting. Audit both. **Sequences fire too frequently** * The orchestrator's due-rules are the issue, not the agent. The agent should still see "last touch was 2 hours ago" in memory and refuse; add that check to the system prompt. **Unsubscribes not honored** * The orchestrator must check unsubscribe state before each send. Don't rely on the agent to remember; set a hard flag in your DB the worker checks first. ## Related * **Integrations:** [OpenAI Agents SDK](/integrations/openai-agents) · [Vercel AI SDK](/integrations/vercel-ai-sdk) * **Concepts:** [Customer Context](/concepts/context-end-to-end#customer-context) · [Long-term Context](/concepts/context-end-to-end#long-term-context) · [Memory Scopes](/concepts/memory-scopes) * **Patterns:** [Replay History](/patterns/replay-history) · [Multi-Tenant SaaS](/patterns/multi-tenant-saas) * **Guides:** [Multi-User Memory Scoping](/guides/multi-user-scoping) * **Other recipes:** [Salesforce: Enterprise Sales Assistant](/cookbook/b2b-salesforce) · [Tier Escalation](/cookbook/support-tier-escalation) # Amazon: Shopping Assistant Source: https://docs.maximem.ai/cookbook/consumer-amazon Catalog-grounded shopping concierge with order history, preference recall, and return handling. **Status:** Live in Playground · **Try it:** [synap.maximem.ai/playground](https://synap.maximem.ai/playground) Open the playground and pick **Amazon: Shopping Assistant** to see the reference implementation running before you build. A shopping concierge that knows the buyer's order history, dietary and material preferences, brand affinities, and recent searches. It grounds product suggestions in the live catalog via tools and handles tracking, returns, and re-orders, all while learning preferences across sessions. ## What you'll build A chat agent that: * **Searches the catalog** with the buyer's preferences applied as filters by default * **Recalls preferences**: dietary restrictions, allergies, sizes, materials, brand likes/dislikes * **Grounds in order history**: "find me another of those running shoes I bought in March" * **Handles post-purchase**: tracking, returns, replacement orders **Est. build time:** 30 to 45 minutes (assuming you already have catalog and orders APIs). ## When to use this recipe Build this if your product: * Has a sizeable catalog where suggestion quality matters * Tracks per-buyer order history you want the agent to reference * Wants the agent to *remember preferences* across visits without making the buyer re-state them * Needs the agent to take post-purchase actions (track, return, replace) ## Architecture at a glance Amazon shopping assistant architecture diagram showing chat, backend, Synap memory fetch, LLM with catalog and order tools, and turn ingestion Preferences and order context auto-organize into a [MACA](/concepts/memory-architecture); you don't define the fields; the SDK does. ## Stack | Layer | Choice | | ------------------ | --------------------------------------------------------------------------------------------------------------------- | | **Synap SDK** | `maximem-synap` (Python) / `@maximem/synap-js-sdk` (TypeScript) | | **Framework** | [OpenAI Agents SDK](/integrations/openai-agents) (Python) / [Vercel AI SDK](/integrations/vercel-ai-sdk) (TypeScript) | | **Memory adapter** | `maximem-synap-openai-agents` / `@maximem/synap-vercel-adk` | | **LLM** | OpenAI `gpt-4o` | | **Channel** | Your existing chat surface (web widget, in-app, etc.) | ## Prerequisites * A Synap API key, see [Authentication](/setup/authentication) * Internal APIs for catalog search, product details, orders, and returns * **Python recipe:** Python 3.11+ * **TypeScript recipe:** Node.js 20+ TypeScript recipe runs on Node only. Pin Next.js route handlers to `export const runtime = "nodejs"`. See [Installation → JavaScript / TypeScript SDK](/setup/installation#javascript-and-typescript-sdk). ### Install ```bash Python theme={null} pip install maximem-synap maximem-synap-openai-agents openai-agents ``` ```bash uv theme={null} uv add maximem-synap maximem-synap-openai-agents openai-agents # pip-compatible (existing venv): uv pip install maximem-synap maximem-synap-openai-agents openai-agents ``` ```bash TypeScript theme={null} npm install @maximem/synap-js-sdk @maximem/synap-vercel-adk ai @ai-sdk/openai zod ``` ### Configure ```bash Python theme={null} # .env SYNAP_API_KEY=... OPENAI_API_KEY=... ``` ```bash TypeScript theme={null} # .env.local SYNAP_API_KEY=... OPENAI_API_KEY=... ``` ## Build it ### 1. Identity & scoping * `customer_id = "amazon"`: single tenant * `user_id = `: from authenticated session * `conversation_id = `: per chat session `conversation_id`, `user_id`, and `customer_id` must be valid UUIDs. Generate session ids with `crypto.randomUUID()` (JS) or `str(uuid.uuid4())` (Python), as shown below. ```python Python theme={null} SESSIONS: dict[str, str] = {} def conv_for(session_id: str) -> str: return SESSIONS.setdefault(session_id, str(uuid.uuid4())) ``` ```typescript TypeScript theme={null} const SESSIONS = new Map(); function convFor(sessionId: string): string { if (!SESSIONS.has(sessionId)) SESSIONS.set(sessionId, crypto.randomUUID()); return SESSIONS.get(sessionId)!; } ``` ### 2. Business tools ```python Python theme={null} from agents import function_tool @function_tool async def search_catalog(query: str, filters: dict = None, limit: int = 10) -> list[dict]: """Search the catalog. Filters: {category, price_max, brand, dietary, material}.""" return await catalog_api.search(query, filters or {}, limit) @function_tool async def get_product_details(asin: str) -> dict: """Return full product detail: title, price, specs, reviews_summary, in_stock.""" return await catalog_api.detail(asin) @function_tool async def get_order_history(buyer_id: str, limit: int = 10) -> list[dict]: """Return the buyer's recent orders, newest first.""" return await orders_api.history(buyer_id, limit) @function_tool async def add_to_cart(buyer_id: str, asin: str, qty: int = 1) -> dict: return await cart_api.add(buyer_id, asin, qty) @function_tool async def track_order(order_id: str) -> dict: return await orders_api.track(order_id) @function_tool async def request_return(order_id: str, reason: str, items: list[str]) -> dict: return await returns_api.create(order_id, reason, items) ``` ```typescript TypeScript theme={null} import { tool } from "ai"; import { z } from "zod"; const businessTools = { search_catalog: tool({ description: "Search the catalog with optional filters.", parameters: z.object({ query: z.string(), filters: z.object({ category: z.string().optional(), priceMax: z.number().optional(), brand: z.string().optional(), dietary: z.string().optional(), material: z.string().optional(), }).optional(), limit: z.number().default(10), }), execute: async ({ query, filters, limit }) => catalogApi.search(query, filters ?? {}, limit), }), get_product_details: tool({ description: "Return full product detail.", parameters: z.object({ asin: z.string() }), execute: async ({ asin }) => catalogApi.detail(asin), }), get_order_history: tool({ description: "Return the buyer's recent orders, newest first.", parameters: z.object({ buyerId: z.string(), limit: z.number().default(10) }), execute: async ({ buyerId, limit }) => ordersApi.history(buyerId, limit), }), add_to_cart: tool({ description: "Add an item to the buyer's cart.", parameters: z.object({ buyerId: z.string(), asin: z.string(), qty: z.number().default(1) }), execute: async ({ buyerId, asin, qty }) => cartApi.add(buyerId, asin, qty), }), track_order: tool({ description: "Return tracking info for an order.", parameters: z.object({ orderId: z.string() }), execute: async ({ orderId }) => ordersApi.track(orderId), }), request_return: tool({ description: "Open a return for one or more items in an order.", parameters: z.object({ orderId: z.string(), reason: z.string(), items: z.array(z.string()), }), execute: async ({ orderId, reason, items }) => returnsApi.create(orderId, reason, items), }), }; ``` ### 3. System prompt ```text System prompt theme={null} You are an Amazon shopping concierge. - Apply known buyer preferences (dietary, allergies, sizes, materials, brand likes/dislikes) as filters by default. Surface them in your reasoning so the buyer can override. - Never recommend items that violate a known allergy or dietary restriction. - Cite product title + ASIN when you suggest something. Don't oversell; one or two options is better than five. - For "find me another of those X I bought", search order history first, then catalog. - Keep replies under 5 sentences unless listing options. Be helpful, not pushy. ``` ### 4. Wire memory + LLM + tools ```python Python theme={null} import os, uuid, asyncio from agents import Agent, FunctionTool, Runner from maximem_synap import MaximemSynapSDK from synap_openai_agents import create_search_tool, create_store_tool sdk = MaximemSynapSDK() await sdk.initialize() async def handle_message(buyer_id: str, session_id: str, text: str) -> str: conv_id = conv_for(session_id) synap_search = create_search_tool(sdk=sdk, user_id=buyer_id, customer_id="amazon") synap_store = create_store_tool(sdk=sdk, user_id=buyer_id, customer_id="amazon") agent = Agent( name="amazon_shopping", instructions=SYSTEM, tools=[ FunctionTool(synap_search, name_override="synap_search"), FunctionTool(synap_store, name_override="synap_store"), search_catalog, get_product_details, get_order_history, add_to_cart, track_order, request_return, ], ) result = await Runner.run(agent, input=text) reply = result.final_output asyncio.create_task(sdk.memories.create( document=f"Buyer: {text}\nAssistant: {reply}", document_type="ai-chat-conversation", user_id=buyer_id, customer_id="amazon", metadata={"conversation_id": conv_id, "channel": "web-chat"}, )) return reply ``` ```typescript TypeScript theme={null} import { generateText } from "ai"; import { openai } from "@ai-sdk/openai"; import { createSynap } from "@maximem/synap-vercel-adk"; const synap = await createSynap({ apiKey: process.env.SYNAP_API_KEY! }); export async function handleMessage( buyerId: string, sessionId: string, text: string, ): Promise { const conversationId = convFor(sessionId); const model = synap.wrap(openai("gpt-4o"), { userId: buyerId, customerId: "amazon", conversationId, }); const { text: reply } = await generateText({ model, system: SYSTEM, prompt: text, tools: businessTools, }); return reply; } ``` ## Run & verify ```text Session 1 theme={null} Buyer: Looking for vegan protein powder, ideally chocolate flavored, under $40. Assistant: Two solid picks under $40: - Orgain Organic Plant Protein (ASIN B07J2N4Q1H), $32, 21g protein, chocolate fudge. - Vega Sport Premium (ASIN B00MUK2KQM), $39, 30g protein, mocha. Want either added to cart? Buyer: Add the Orgain. I'm allergic to soy by the way. Assistant: Added Orgain to cart. Noted: I'll keep your suggestions soy-free from here on. ``` ```text Session 2 (a week later, new conversation_id) theme={null} Buyer: I need a new protein, ran out. Assistant: Re-ordering Orgain Organic Plant Protein (chocolate fudge), same one as last time. It's soy-free, which matches your allergy. Add to cart? ``` The second session is a fresh conversation. Synap surfaces the prior order and the soy allergy without you wiring them by hand. ### Inspect what got stored ```python Python theme={null} docs = await sdk.memories.list(user_id="buyer_42", customer_id="amazon", limit=20) for d in docs: print(d.document_type, ":", d.content[:120]) ``` ```typescript TypeScript theme={null} const docs = await synap.sdk.memories.list({ userId: "buyer_42", customerId: "amazon", limit: 20 }); for (const d of docs) console.log(d.documentType, ":", d.content.slice(0, 120)); ``` ## Customize / extend * **Voice channel** → swap the chat handler for [Voice Concierge](/cookbook/voice-concierge). The shopping tools transfer untouched. * **Multi-tenant marketplaces** → set `customer_id` per seller storefront. See [Patterns → Multi-Tenant SaaS](/patterns/multi-tenant-saas). * **Replay historical orders into memory** on launch so the agent is useful from day one. See [Patterns → Replay History](/patterns/replay-history). * **Different framework** → see [AI Integrations](/integrations/overview) for LangChain, LlamaIndex, CrewAI, etc. * **Support flavor** → adapt this shape with `request_refund` instead of `request_return`; see [Uber: Customer Support](/cookbook/consumer-uber). ## Troubleshooting **Assistant ignores known allergies / dietary prefs** * Confirm `synap_search` is being called (Python); inspect the agent trace. If the model isn't calling it, sharpen the system prompt: "Before recommending food, beauty, or supplements, call `synap_search` for the buyer's restrictions." * For the TS path with `synap.wrap`, increase `maxResults` so the dietary memory makes it into the injected context. **Same product suggested over and over** * The agent has the buyer's affinity but no negative feedback. Capture explicit rejections ("I didn't like that one") with `synap_store`, or post-purchase ratings via your own pipeline → `sdk.memories.create`. **Order history out of sync** * Order data should be tool-fetched live, never cached in memory. If you see stale orders in suggestions, your tool is returning cached data, not Synap. ## Related * **Integrations:** [OpenAI Agents SDK](/integrations/openai-agents) · [Vercel AI SDK](/integrations/vercel-ai-sdk) * **Concepts:** [Customized Memory Architectures](/concepts/memory-architecture) · [Memory Scopes](/concepts/memory-scopes) * **Patterns:** [Replay History](/patterns/replay-history) · [Multi-Tenant SaaS](/patterns/multi-tenant-saas) * **Other recipes:** [Uber: Customer Support](/cookbook/consumer-uber) · [Salesforce: Enterprise Sales Assistant](/cookbook/b2b-salesforce) # Uber: Customer Support Source: https://docs.maximem.ai/cookbook/consumer-uber Rider-aware customer support agent with refunds, lost-item reports, and clean human handoff. **Status:** Live in Playground · **Try it:** [synap.maximem.ai/playground](https://synap.maximem.ai/playground) Open the playground and pick **Uber: Customer Support** to see the reference implementation running before you build. A rider support agent that knows who the rider is, what rides they've taken, how they like to be communicated with, and how their past issues were resolved. It calls tools to pull ride history, issue refunds, open lost-item reports, and hand off to humans, and it learns from every conversation. ## What you'll build A chat agent that: * **Recalls rider context**: communication preferences, prior issues, frequent destinations * **Grounds in real data**: pulls recent rides via tools before suggesting actions * **Resolves common issues**: refunds, lost items, rating disputes, charge disputes * **Escalates cleanly**: opens a Tier-2 ticket with a memory-aware summary **Est. build time:** 30 to 45 minutes (assuming you already have ride/refund/ticket APIs to call into). ## When to use this recipe Build this if your product: * Has authenticated end users with a stable internal user ID * Has internal APIs the agent can call (orders, accounts, tickets, refunds) * Wants the agent to *remember resolutions* so the user doesn't re-explain on the next chat * Needs a deterministic escalation path to a human queue If you only have one of those, this is still the closest starting point; strip the tools you don't need. ## Architecture at a glance Uber rider support agent architecture diagram showing chat, backend, Synap memory fetch, LLM with tools, and turn ingestion Memory is auto-organized into a [MACA](/concepts/memory-architecture) so things like "preferred contact channel" and "open lost-item report" surface as the right type at the right time. You don't define MACA fields by hand; the SDK and use-case markdown handle it. ## Stack | Layer | Choice | | ------------------ | --------------------------------------------------------------------------------------------------------------------- | | **Synap SDK** | `maximem-synap` (Python) / `@maximem/synap-js-sdk` (TypeScript) | | **Framework** | [OpenAI Agents SDK](/integrations/openai-agents) (Python) / [Vercel AI SDK](/integrations/vercel-ai-sdk) (TypeScript) | | **Memory adapter** | `maximem-synap-openai-agents` / `@maximem/synap-vercel-adk` | | **LLM** | OpenAI `gpt-4o` (swap for any supported model) | | **Channel** | Your existing chat surface (in-app, web widget, etc.) | ## Prerequisites * A Synap API key, see [Authentication](/setup/authentication) * Internal APIs for ride history, refunds, lost items, and tickets (real or stubbed for development) * **Python recipe:** Python 3.11+ * **TypeScript recipe:** Node.js 20+ TypeScript recipe runs on Node only. Edge Runtime, Cloudflare Workers, Deno Deploy, and Lambda Node-only runtimes are not supported. Pin Next.js route handlers to `export const runtime = "nodejs"`. See [Installation → JavaScript / TypeScript SDK](/setup/installation#javascript-and-typescript-sdk). ### Install ```bash Python theme={null} pip install maximem-synap maximem-synap-openai-agents openai-agents ``` ```bash uv theme={null} uv add maximem-synap maximem-synap-openai-agents openai-agents # pip-compatible (existing venv): uv pip install maximem-synap maximem-synap-openai-agents openai-agents ``` ```bash TypeScript theme={null} npm install @maximem/synap-js-sdk @maximem/synap-vercel-adk ai @ai-sdk/openai zod ``` ### Configure ```bash Python theme={null} # .env SYNAP_API_KEY=... OPENAI_API_KEY=... ``` ```bash TypeScript theme={null} # .env.local SYNAP_API_KEY=... OPENAI_API_KEY=... ``` ## Build it ### 1. Identity & scoping Three scopes do the work: * `customer_id = "uber"`: single tenant; this is Uber's own product * `user_id = `: comes from your authenticated session, never trust the client * `conversation_id = `: one per chat session, stable across messages in the same session `conversation_id`, `user_id`, and `customer_id` must be valid UUIDs. Generate session ids with `crypto.randomUUID()` (JS) or `str(uuid.uuid4())` (Python), as shown below. ```python Python theme={null} # session_id → conversation_id mapping. Use Redis in production. SESSIONS: dict[str, str] = {} def conv_for(session_id: str) -> str: return SESSIONS.setdefault(session_id, str(uuid.uuid4())) ``` ```typescript TypeScript theme={null} // session_id → conversation_id mapping. Use Redis in production. const SESSIONS = new Map(); function convFor(sessionId: string): string { if (!SESSIONS.has(sessionId)) SESSIONS.set(sessionId, crypto.randomUUID()); return SESSIONS.get(sessionId)!; } ``` ### 2. Business tools The agent calls these to ground replies in real account state and to take action. Wire each one to your internal API. ```python Python theme={null} from agents import function_tool @function_tool async def get_recent_rides(rider_id: str, limit: int = 5) -> list[dict]: """Return the rider's most recent rides.""" return await rides_api.recent(rider_id, limit) @function_tool async def request_refund(ride_id: str, reason: str) -> dict: """Initiate a refund for a specific ride. Returns refund_id and ETA.""" return await refunds_api.create(ride_id, reason) @function_tool async def create_lost_item_report(rider_id: str, ride_id: str, description: str) -> dict: """Open a lost-item report tied to a specific ride.""" return await lost_items_api.create(rider_id, ride_id, description) @function_tool async def escalate_to_human(rider_id: str, summary: str) -> dict: """Open a Tier-2 ticket and hand off the conversation.""" return await tickets_api.open(rider_id, summary, tier=2) ``` ```typescript TypeScript theme={null} import { tool } from "ai"; import { z } from "zod"; const businessTools = { get_recent_rides: tool({ description: "Return the rider's most recent rides.", parameters: z.object({ riderId: z.string(), limit: z.number().default(5) }), execute: async ({ riderId, limit }) => ridesApi.recent(riderId, limit), }), request_refund: tool({ description: "Initiate a refund for a specific ride. Returns refund_id and ETA.", parameters: z.object({ rideId: z.string(), reason: z.string() }), execute: async ({ rideId, reason }) => refundsApi.create(rideId, reason), }), create_lost_item_report: tool({ description: "Open a lost-item report tied to a specific ride.", parameters: z.object({ riderId: z.string(), rideId: z.string(), description: z.string(), }), execute: async ({ riderId, rideId, description }) => lostItemsApi.create(riderId, rideId, description), }), escalate_to_human: tool({ description: "Open a Tier-2 ticket and hand off the conversation.", parameters: z.object({ riderId: z.string(), summary: z.string() }), execute: async ({ riderId, summary }) => ticketsApi.open(riderId, summary, 2), }), }; ``` ### 3. System prompt Tight, outcome-focused, and explicit about memory and escalation rules. ```text System prompt theme={null} You are an Uber rider support agent. - Always check the rider's recent rides before suggesting actions. - Use prior resolutions and the rider's communication preferences from memory. - Escalate to a human if: the rider explicitly asks, the issue involves safety, or the disputed amount exceeds $50. - Keep replies under 4 sentences. Be warm and outcome-focused. ``` ### 4. Wire memory + LLM + tools The Python path uses the [OpenAI Agents integration](/integrations/openai-agents) to expose `synap_search` and `synap_store` as tools the agent can call when it wants to recall or remember. The TypeScript path uses the [Vercel AI SDK integration](/integrations/vercel-ai-sdk) which wraps the model: context fetch and turn ingestion happen automatically on every call, so you only declare your business tools. ```python Python theme={null} import os, uuid, asyncio from agents import Agent, FunctionTool, Runner from maximem_synap import MaximemSynapSDK from synap_openai_agents import create_search_tool, create_store_tool sdk = MaximemSynapSDK() await sdk.initialize() SYSTEM = """You are an Uber rider support agent. - Always check the rider's recent rides before suggesting actions. - Use prior resolutions and the rider's communication preferences from memory. - Escalate to a human if: the rider explicitly asks, the issue involves safety, or the disputed amount exceeds $50. - Keep replies under 4 sentences. Be warm and outcome-focused.""" async def handle_message(rider_id: str, session_id: str, text: str) -> str: conv_id = conv_for(session_id) synap_search = create_search_tool(sdk=sdk, user_id=rider_id, customer_id="uber") synap_store = create_store_tool(sdk=sdk, user_id=rider_id, customer_id="uber") agent = Agent( name="uber_rider_support", instructions=SYSTEM, tools=[ FunctionTool(synap_search, name_override="synap_search"), FunctionTool(synap_store, name_override="synap_store"), get_recent_rides, request_refund, create_lost_item_report, escalate_to_human, ], ) result = await Runner.run(agent, input=text) reply = result.final_output # Persist the full turn so the next message has context. # Fire-and-forget; never blocks the response. asyncio.create_task(sdk.memories.create( document=f"Rider: {text}\nAgent: {reply}", document_type="ai-chat-conversation", user_id=rider_id, customer_id="uber", metadata={"conversation_id": conv_id, "channel": "in-app-chat"}, )) return reply ``` ```typescript TypeScript theme={null} import { generateText } from "ai"; import { openai } from "@ai-sdk/openai"; import { createSynap } from "@maximem/synap-vercel-adk"; const synap = await createSynap({ apiKey: process.env.SYNAP_API_KEY! }); const SYSTEM = `You are an Uber rider support agent. - Always check the rider's recent rides before suggesting actions. - Use prior resolutions and the rider's communication preferences from memory. - Escalate to a human if: the rider explicitly asks, the issue involves safety, or the disputed amount exceeds $50. - Keep replies under 4 sentences. Be warm and outcome-focused.`; export async function handleMessage( riderId: string, sessionId: string, text: string, ): Promise { const conversationId = convFor(sessionId); // synap.wrap injects rider context as a system message before the LLM call // and ingests the completed turn afterward. No manual fetch / ingest needed. const model = synap.wrap(openai("gpt-4o"), { userId: riderId, customerId: "uber", conversationId, }); const { text: reply } = await generateText({ model, system: SYSTEM, prompt: text, tools: businessTools, }); return reply; } ``` **Why the two paths look different** * Python's `synap_search` / `synap_store` are *tools the LLM decides to call*. The model has agency over when to recall and when to store. * TS's `synap.wrap` is *middleware*. Memory is fetched on every call and the turn is ingested afterward, unconditionally. Less control, less code. Both ship. Pick the one that matches your framework. If you want LLM-driven memory in TS, build it as a custom tool; see the [Vercel AI SDK integration](/integrations/vercel-ai-sdk). ## Run & verify Stand up the handler behind any HTTP framework (FastAPI, Next.js route handler, etc.) and send two messages from different sessions to see memory carry across: ```text Session 1 theme={null} Rider: Hi, my driver from this morning left my phone in the car. Agent: I'm sorry about that. I can see your 8:42 AM ride with driver Raj. Want me to open a lost-item report and notify Raj? Rider: Yes please. And I prefer texts not calls. Agent: Done. Report #LI-23901 is open. You'll get a text when Raj responds. ``` ```text Session 2 (next day, new conversation_id) theme={null} Rider: Any update on the phone? Agent: Your lost-item report for the 8:42 AM ride is still pending Raj's response. As you preferred, I'll send updates via text only. ``` The second session is a fresh `conversation_id`; Synap pulls "prefers texts not calls" and "lost-item report open" from the rider's long-term memory. ### Inspect what got stored ```python Python theme={null} docs = await sdk.memories.list( user_id="rider_42", customer_id="uber", limit=20, ) for d in docs: print(d.document_type, ":", d.content[:120]) ``` ```typescript TypeScript theme={null} const docs = await synap.sdk.memories.list({ userId: "rider_42", customerId: "uber", limit: 20, }); for (const d of docs) { console.log(d.documentType, ":", d.content.slice(0, 120)); } ``` ## Customize / extend * **Voice channel** → swap the chat handler for [Voice Concierge](/cookbook/voice-concierge) (Pipecat + ElevenLabs). Same scoping model, different I/O. * **Multi-tenant flavor** (B2B Uber-for-X) → set `customer_id` per tenant org. See [Patterns → Multi-Tenant SaaS](/patterns/multi-tenant-saas). * **Different framework** → drop the OpenAI Agents / Vercel AI SDK adapter; the SDK calls (`memories.create`, `conversation.context.fetch`) work standalone. See [AI Integrations](/integrations/overview). * **Slack human-handoff side-channel** → when `escalate_to_human` fires, post into Slack using the [Slack pattern](/patterns/slack-bot). The Tier-2 agent has the full memory thread. * **Catalog-grounded variant** → see [Amazon: Shopping Assistant](/cookbook/consumer-amazon) for the same shape with product retrieval added. ## Troubleshooting **Rider isn't getting personalized replies** * Check `user_id` is stable across sessions; derive it from authenticated session, not from the chat client. * Confirm ingestion is firing. The fire-and-forget pattern (`asyncio.create_task`) swallows errors silently; log inside the task during development. **Memory leaks across riders** * `user_id` collision somewhere upstream. Audit the auth flow. * For B2B variants, also confirm `customer_id` is set per tenant. **Agent invents rides or refunds** * A tool returned an empty result and the model hallucinated. Tighten tool docstrings, return explicit "no rides found" structures, and lower temperature. **TypeScript route times out on Vercel** * You're probably on Edge Runtime. Add `export const runtime = "nodejs"` to your route handler. The JS SDK requires Node + Python on the host. ## Related * **Integrations:** [OpenAI Agents SDK](/integrations/openai-agents) · [Vercel AI SDK](/integrations/vercel-ai-sdk) * **Concepts:** [Customized Memory Architectures](/concepts/memory-architecture) · [Memory Scopes](/concepts/memory-scopes) · [Conversational Context Lifecycle](/concepts/context-end-to-end#short-term-context) * **Patterns:** [Multi-Tenant SaaS](/patterns/multi-tenant-saas) · [Graceful Degradation](/patterns/graceful-degradation) * **Vity:** [Maximem Vity overview](/vity/overview) # Cookbook Source: https://docs.maximem.ai/cookbook/overview Complete, opinionated reference agents you can clone, modify, and ship. Each recipe shows one production-shape pattern end-to-end. The Cookbook is Synap's **examples manual**. Each recipe is one complete agent: what it does, when to use it, the stack it runs on, and the code to build it. Most live in our playground; the rest are in active development and ship as the recipe drops. **Try before you build.** Every recipe has a [Playground](https://synap.maximem.ai/playground) companion you can poke at without writing a line of code. Bring API keys to the playground if you want to fork and run. ## How to read a recipe Every page follows the same skeleton, in this order: 1. **What you'll build**: outcome bullets, est. build time 2. **When to use this recipe**: concrete signals so you can tell if it fits your problem 3. **Architecture at a glance**: one diagram, no jargon 4. **Stack**: language, framework, plugins, channel 5. **Build it**: step-by-step with Python and TypeScript side-by-side in code tabs 6. **Run & verify**: start it locally, see memory persistence working 7. **Customize / extend**: links to swap channels, add plugins, change scoping 8. **Troubleshooting**: recipe-specific gotchas 9. **Related**: framework integrations, plugins, and concepts referenced All code samples follow Synap's [SDK-only guidance](/sdk/initialization): no raw REST, no curl, just the SDK you've already installed. ## Status legend Reference implementation runs on [synap.maximem.ai/playground](https://synap.maximem.ai/playground). Recipe code matches. Recipe is written and runnable. Playground demo is being built. Check back soon. ## Consumer Agents User-facing assistants for end-user products. Memory is keyed per consumer; one customer\_id covers the whole product. Live in Playground · Rider-aware support agent with refunds, lost-item reports, and human handoff. Live in Playground · Catalog-grounded shopping concierge with order history and preference recall. ## Support Agents Internal-or-external support workflows with escalation, ticketing, and cross-tier handoff. In Development · Multi-agent cluster: T1 triage agent hands off to T2 specialist with shared memory. ## WhatsApp Agents Inbound + outbound WhatsApp Business API agents. Memory survives across sessions and human handoffs. In Development · One WABA number, AI takes inbound, drops to human agent on signal, picks back up cleanly. In Development · One WABA number running scheduled outbound campaigns alongside inbound support. In Development · Multiple WABA numbers under one business, one shared customer memory pool. ## Voice Agents Real-time voice agents with memory injection inside voice latency budgets. In Development · Phone agent that recalls caller history mid-call. STT → memory inject → LLM → TTS. ## B2B Agents Outbound and inbound agents for business buyers and sellers. In Development · Prospecting agent: research, personalize, sequence, book, with prospect memory across touches. Live in Playground · Account-grounded sales assistant with opportunity history and CRM-aware recommendations. ## Personal AI Agents Consumer-side personal agents. Strong individual user scoping; preferences and habits are the core memory. Live in Playground · Conversational companion that learns preferences, communication style, and ongoing context. In Development · Wellness coach with goal tracking, plan adherence memory, and session continuity. ## Choosing a recipe Quick decision guide: | If you're building for… | Start with | | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | End-user support in a consumer app | [Uber: Customer Support](/cookbook/consumer-uber) | | Product discovery / commerce | [Amazon: Shopping Assistant](/cookbook/consumer-amazon) | | Internal helpdesk with escalation | [Tier-1 → Tier-2 Escalation Cluster](/cookbook/support-tier-escalation) | | WhatsApp-first business comms | One of the three WhatsApp recipes ([handoff](/cookbook/whatsapp-single-handoff), [campaign](/cookbook/whatsapp-single-campaign), [multi-WABA](/cookbook/whatsapp-multi-waba-shared)) | | Phone / IVR replacement | [Voice Concierge](/cookbook/voice-concierge) | | Outbound B2B prospecting | [AI SDR](/cookbook/b2b-sdr) | | Enterprise sales enablement | [Salesforce: Enterprise Sales Assistant](/cookbook/b2b-salesforce) | | Consumer companion / lifestyle app | [AI Companion](/cookbook/personal-ai-companion) or [AI Coach](/cookbook/personal-ai-coach) | Don't see your shape? The patterns transfer. Start from the closest recipe, then check [Patterns](/patterns/overview) for cross-cutting techniques like scoping, replay ingestion, and graceful degradation. ## Stack you'll need Every recipe runs on the Synap SDK plus one or more of: * **Frameworks**: see [AI Integrations](/integrations/overview) for the full list (LangChain, OpenAI Agents, Vercel AI SDK, Mastra, Pipecat, LiveKit, etc.) * **Vity**: see [Maximem Vity](/vity/overview) for end-user memory plugins (OpenClaw, more coming) * **Concepts**: recipes link out to the [Concepts](/concepts/memory-scopes#clients-and-instances) section when scoping or memory architecture matters **TypeScript recipes** need Node.js 20+. Context and memory operations run on Node, Vercel Edge, Cloudflare Workers and the browser; the optional anticipation stream is Node only. See [Installation → JavaScript and TypeScript SDK](/setup/installation#javascript-and-typescript-sdk). ## Contributing Built something Cookbook-worthy? Email **[support@maximem.ai](mailto:support@maximem.ai)** with the shape and we'll work it in. Recipes that get repeatedly requested become part of the docs. # AI Coach Source: https://docs.maximem.ai/cookbook/personal-ai-coach Wellness coach with goal tracking, plan adherence memory, and session continuity. **Status:** In Development · Playground demo coming soon. The recipe below is complete and runnable today; only the hosted playground showcase is pending. A wellness coach that holds the user's goals, current plan, what they've actually done, and what's been hard. It celebrates streaks, flexes plans when life gets in the way, and never starts from zero on Monday morning. ## What you'll build A coaching agent that: * **Tracks goals and plans**: primary goal, weekly plan, constraints * **Logs adherence**: workouts, meals, sleep, sessions (whatever your domain is) * **Adapts plans**: if the user skipped three sessions, the next plan reflects that * **Holds the narrative**: last week's slump, the injury that's still healing, the trip coming up **Est. build time:** 45 minutes (more if your logging schema is rich). ## When to use this recipe Build this if: * Your product has a notion of a *plan* the user is following over weeks * Adherence (what got done vs what was planned) matters as much as what's currently planned * You want the coach to feel continuous, not session-bound * The user is the only client; per-user isolation is strict ## Architecture at a glance ```mermaid theme={null} flowchart TD Chat[User chat
mobile or web] --> Backend[Your backend] Backend -->|fetch| Synap1[(Synap context fetch
goals, current plan, recent adherence, constraints)] Synap1 --> LLM[LLM with tools] LLM --> Tools["set_goal
get_current_plan
log_session
get_streak
suggest_plan_adjustment"] Tools --> Reply[Reply to user] Reply -.->|fire-and-forget| Synap2[(Synap ingest turn)] ``` ## Stack | Layer | Choice | | ------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | **Synap SDK** | `maximem-synap` (Python) / `@maximem/synap-js-sdk` (TypeScript) | | **Framework** | [OpenAI Agents SDK](/integrations/openai-agents) (Python) / [Vercel AI SDK](/integrations/vercel-ai-sdk) (TypeScript) | | **Storage for structured logs** | Your own DB (Postgres / SQLite). Synap holds the narrative; the DB holds the rows. | | **LLM** | OpenAI `gpt-4o` | ## Prerequisites * A Synap API key. See [Authentication](/setup/authentication) * A DB for structured session logs (Synap is not your activity log; it's the memory that wraps around it) * **Python:** Python 3.11+ * **TypeScript:** Node.js 20+ TypeScript recipe runs on Node only. Pin Next.js route handlers to `export const runtime = "nodejs"`. See [Installation → JavaScript / TypeScript SDK](/setup/installation#javascript-and-typescript-sdk). ### Install ```bash Python theme={null} pip install maximem-synap maximem-synap-openai-agents openai-agents ``` ```bash uv theme={null} uv add maximem-synap maximem-synap-openai-agents openai-agents # pip-compatible (existing venv): uv pip install maximem-synap maximem-synap-openai-agents openai-agents ``` ```bash TypeScript theme={null} npm install @maximem/synap-js-sdk @maximem/synap-vercel-adk ai @ai-sdk/openai zod ``` ### Configure ```bash Python theme={null} # .env SYNAP_API_KEY=... OPENAI_API_KEY=... DATABASE_URL=postgres://... ``` ```bash TypeScript theme={null} # .env.local SYNAP_API_KEY=... OPENAI_API_KEY=... DATABASE_URL=postgres://... ``` ## Build it ### 1. The Synap-vs-DB split This is the key call: **structured data lives in your DB; narrative lives in Synap.** | What | Where | Why | | ----------------------------------- | ----- | ----------------------------------------------------------- | | "Ran 5km on Tuesday, 8:21 pace" | DB | Queryable, aggregatable, historical | | "Hates running in the rain" | Synap | Soft preference; surfaces when planning | | "Knee felt off after Tuesday's run" | Synap | Narrative signal the next plan should respect | | "Goal: half-marathon by October" | Both | Structured target in DB, plus motivational context in Synap | Tools read both. The system prompt teaches the agent which is which. ### 2. Identity & scoping * `customer_id = "coach"`: single tenant * `user_id = `: strict per-user isolation * `conversation_id`: one continuous conversation per user works well here (this is a long-running relationship) `conversation_id`, `user_id`, and `customer_id` must be valid UUIDs. Generate ids with `crypto.randomUUID()` (JS) or `str(uuid.uuid4())` (Python), as shown below. ```python Python theme={null} SESSIONS: dict[str, str] = {} def conv_for(user_id: str) -> str: return SESSIONS.setdefault(user_id, str(uuid.uuid4())) ``` ```typescript TypeScript theme={null} const SESSIONS = new Map(); function convFor(userId: string): string { if (!SESSIONS.has(userId)) SESSIONS.set(userId, crypto.randomUUID()); return SESSIONS.get(userId)!; } ``` ### 3. Business tools ```python Python theme={null} from agents import function_tool @function_tool async def set_goal(user_id: str, goal: str, target_date: str) -> dict: """Set or update the user's primary training goal.""" return await db.goals.upsert(user_id, goal, target_date) @function_tool async def get_current_plan(user_id: str) -> dict: """Return the user's current week plan with session list and status.""" return await db.plans.current(user_id) @function_tool async def log_session(user_id: str, kind: str, payload: dict) -> dict: """Log a completed session. kind: 'run' | 'lift' | 'sleep' | 'meal' | 'mood'.""" return await db.sessions.create(user_id, kind, payload) @function_tool async def get_streak(user_id: str, kind: str) -> dict: """Return current and best streak for a session kind.""" return await db.sessions.streak(user_id, kind) @function_tool async def suggest_plan_adjustment(user_id: str, reason: str) -> dict: """Generate a tentative plan adjustment, save it as pending for user approval.""" return await planner.adjust(user_id, reason) ``` ```typescript TypeScript theme={null} import { tool } from "ai"; import { z } from "zod"; const coachTools = { set_goal: tool({ description: "Set or update the user's primary training goal.", parameters: z.object({ userId: z.string(), goal: z.string(), targetDate: z.string(), }), execute: async ({ userId, goal, targetDate }) => db.goals.upsert(userId, goal, targetDate), }), get_current_plan: tool({ description: "Return the user's current week plan with session list and status.", parameters: z.object({ userId: z.string() }), execute: async ({ userId }) => db.plans.current(userId), }), log_session: tool({ description: "Log a completed session.", parameters: z.object({ userId: z.string(), kind: z.enum(["run", "lift", "sleep", "meal", "mood"]), payload: z.record(z.any()), }), execute: async ({ userId, kind, payload }) => db.sessions.create(userId, kind, payload), }), get_streak: tool({ description: "Return current and best streak for a session kind.", parameters: z.object({ userId: z.string(), kind: z.string() }), execute: async ({ userId, kind }) => db.sessions.streak(userId, kind), }), suggest_plan_adjustment: tool({ description: "Generate a tentative plan adjustment for user approval.", parameters: z.object({ userId: z.string(), reason: z.string() }), execute: async ({ userId, reason }) => planner.adjust(userId, reason), }), }; ``` ### 4. System prompt ```text System prompt theme={null} You are a wellness coach. The user is following a plan you helped design. - Their plan and logged sessions live in tools. Always check them before suggesting anything new. - Their constraints, preferences, history, and current "season of life" live in your memory. Use them. - Celebrate consistency more than intensity. Acknowledge skipped sessions without judgment. - If they've skipped 3+ planned sessions or surfaced an injury / illness / life event, propose a plan adjustment via the tool. Don't push through. - When they share something coach-relevant (an injury, a trip, a constraint), remember it. - Keep replies under 4 sentences unless walking through a plan. Be direct, be warm. ``` ### 5. Wire it together ```python Python theme={null} import os, uuid, asyncio from agents import Agent, FunctionTool, Runner from maximem_synap import MaximemSynapSDK from synap_openai_agents import create_search_tool, create_store_tool sdk = MaximemSynapSDK() await sdk.initialize() async def handle_message(user_id: str, text: str) -> str: conv_id = conv_for(user_id) synap_search = create_search_tool(sdk=sdk, user_id=user_id, customer_id="coach") synap_store = create_store_tool(sdk=sdk, user_id=user_id, customer_id="coach") agent = Agent( name="coach", instructions=SYSTEM, tools=[ FunctionTool(synap_search, name_override="synap_search"), FunctionTool(synap_store, name_override="synap_store"), set_goal, get_current_plan, log_session, get_streak, suggest_plan_adjustment, ], ) result = await Runner.run(agent, input=text) reply = result.final_output asyncio.create_task(sdk.memories.create( document=f"User: {text}\nCoach: {reply}", document_type="ai-chat-conversation", user_id=user_id, customer_id="coach", metadata={"conversation_id": conv_id}, )) return reply ``` ```typescript TypeScript theme={null} import { generateText } from "ai"; import { openai } from "@ai-sdk/openai"; import { createSynap } from "@maximem/synap-vercel-adk"; const synap = await createSynap({ apiKey: process.env.SYNAP_API_KEY! }); export async function handleMessage(userId: string, text: string): Promise { const conversationId = convFor(userId); const model = synap.wrap(openai("gpt-4o"), { userId, customerId: "coach", conversationId, }); const { text: reply } = await generateText({ model, system: SYSTEM, prompt: text, tools: coachTools, }); return reply; } ``` ## Run & verify ```text Week 1 theme={null} User: Goal is to run a half-marathon by October. Coach: Locked in: half-marathon by October. I'll build a 12-week base + build plan. Two questions: any injuries to work around, and how many days a week can you train? User: Probably 4. Old knee thing on the right, hurts on hills. Coach: 4 days, flat routes prioritized, hills reintroduced gradually. Plan up tomorrow. ``` ```text Week 4 (fresh session) theme={null} User: Missed Tuesday and Wednesday. Travel. Coach: Got it. Logging both as skipped, reason: travel. You've still got 2/4 this week, which is fine for a base block. Want me to slide Thursday's tempo to Saturday? ``` ```text Week 8 theme={null} User: Knee is acting up again. Coach: Noted. Knee flaring same right side as before. Pulling the hill repeats and dropping to 3 days this week. Half-marathon date is still safe with this adjustment. Want me to suggest a PT check-in? ``` The coach remembers the knee history (Synap), checks current plan (tool), proposes an adjustment (tool), and stays warm. ## Customize / extend * **Companion flavor** → if you want less goal-driven and more open-ended, see [AI Companion](/cookbook/personal-ai-companion). * **Voice journaling** → swap the chat handler for [Voice Concierge](/cookbook/voice-concierge). Coaching by voice is natural. * **Apple Health / Wearable ingestion** → write structured rows to your DB; let Synap pick up the narrative when the user mentions it. * **Cohort coaching** → set `customer_id = ` to share light context (community PRs, group challenges) across users within a cohort while keeping personal context per-`user_id`. ## Troubleshooting **Coach asks the same intake questions every session** * Confirm goal-setting facts are being ingested. After `set_goal`, also call `sdk.memories.create` with a narrative summary ("Goal: half-marathon by October, training 4 days/week, knee-aware"). **Coach doesn't adapt when user misses sessions** * The model isn't pulling DB state. Sharpen the system prompt: "Before any plan advice, call `get_current_plan` and `get_streak`." If you're using the TS wrapper, ensure tools fire by giving the model an explicit instruction to check state. **Plan adjustments feel generic** * The `suggest_plan_adjustment` tool needs the *reason* to come from Synap memory (injury context, life events). Without it, you get generic deload weeks. ## Related * **Integrations:** [OpenAI Agents SDK](/integrations/openai-agents) · [Vercel AI SDK](/integrations/vercel-ai-sdk) * **Concepts:** [Memory Types](/concepts/memories-and-context#memory-types) · [Memory Scopes](/concepts/memory-scopes) · [Long-term Context](/concepts/context-end-to-end#long-term-context) * **Other recipes:** [AI Companion](/cookbook/personal-ai-companion) · [Voice Concierge](/cookbook/voice-concierge) # AI Companion Source: https://docs.maximem.ai/cookbook/personal-ai-companion Conversational companion that learns preferences, communication style, and ongoing context across sessions. **Status:** Live in Playground · **Try it:** [synap.maximem.ai/playground](https://synap.maximem.ai/playground) Open the playground and pick **AI Companion** to see the reference implementation. (This recipe replaces the previous Tinder Dating Support example.) A personal companion that pays attention. It learns how the user likes to be talked to, what's going on in their life, what they care about, and what's off-limits, and brings that context forward conversation after conversation without making the user repeat themselves. ## What you'll build A conversational agent that: * **Tracks personal context**: name, pronouns, life events, ongoing situations, communication style * **Adapts tone** to the user (formal vs casual, brief vs warm, emoji vs plain) * **Respects boundaries**: topics the user has marked off-limits stay off-limits * **Holds threads across sessions**: picks up open conversations without recap prompts **Est. build time:** 20-30 minutes. This is the simplest recipe in the Cookbook by surface area, but the highest-leverage memory. ## When to use this recipe Build this if: * The product is a 1:1 conversational experience (companion, journaling buddy, lifestyle assistant) * Personalization across sessions is the core value, not tool execution * The user owns their context: strict per-user isolation * You want minimal tools; the memory does the heavy lifting ## Architecture at a glance ```mermaid theme={null} flowchart TD Chat[User chat
mobile or web] --> Backend[Your backend] Backend -->|fetch| Synap1[(Synap context fetch
prefs, style, ongoing threads, boundaries)] Synap1 --> LLM[LLM
light or no tools] LLM --> Reply[Reply to user] Reply -.->|fire-and-forget| Synap2[(Synap ingest turn)] ``` Memory is the agent. Tools are optional. ## Stack | Layer | Choice | | ------------------ | --------------------------------------------------------------------------------------------------------------------- | | **Synap SDK** | `maximem-synap` (Python) / `@maximem/synap-js-sdk` (TypeScript) | | **Framework** | [OpenAI Agents SDK](/integrations/openai-agents) (Python) / [Vercel AI SDK](/integrations/vercel-ai-sdk) (TypeScript) | | **Memory adapter** | `maximem-synap-openai-agents` / `@maximem/synap-vercel-adk` | | **LLM** | OpenAI `gpt-4o` (warmer voice) or `claude-sonnet-4-6` (better long-form) | | **Channel** | Native mobile / web chat | ## Prerequisites * A Synap API key. See [Authentication](/setup/authentication) * **Python:** Python 3.11+ * **TypeScript:** Node.js 20+ TypeScript recipe runs on Node only. Pin Next.js route handlers to `export const runtime = "nodejs"`. See [Installation → JavaScript / TypeScript SDK](/setup/installation#javascript-and-typescript-sdk). ### Install ```bash Python theme={null} pip install maximem-synap maximem-synap-openai-agents openai-agents ``` ```bash uv theme={null} uv add maximem-synap maximem-synap-openai-agents openai-agents # pip-compatible (existing venv): uv pip install maximem-synap maximem-synap-openai-agents openai-agents ``` ```bash TypeScript theme={null} npm install @maximem/synap-js-sdk @maximem/synap-vercel-adk ai @ai-sdk/openai ``` ### Configure ```bash Python theme={null} # .env SYNAP_API_KEY=... OPENAI_API_KEY=... ``` ```bash TypeScript theme={null} # .env.local SYNAP_API_KEY=... OPENAI_API_KEY=... ``` ## Build it ### 1. Identity & scoping * `customer_id = "companion"`: single tenant (your app) * `user_id = `: strict per-user isolation; no cross-user leakage * `conversation_id = `: one per app session Use a long-lived `conversation_id` (per app install, not per app open) if you want the companion to feel like one continuous relationship rather than a series of standalone chats. `conversation_id`, `user_id`, and `customer_id` must be valid UUIDs. Generate ids with `crypto.randomUUID()` (JS) or `str(uuid.uuid4())` (Python), as shown below. ```python Python theme={null} SESSIONS: dict[str, str] = {} def conv_for(user_id: str) -> str: # One conversation per user. Tune to per-session if you prefer. return SESSIONS.setdefault(user_id, str(uuid.uuid4())) ``` ```typescript TypeScript theme={null} const SESSIONS = new Map(); function convFor(userId: string): string { if (!SESSIONS.has(userId)) SESSIONS.set(userId, crypto.randomUUID()); return SESSIONS.get(userId)!; } ``` ### 2. System prompt The prompt is most of the work here: it shapes how memory gets used. ```text System prompt theme={null} You are a personal companion. The user is talking to you in confidence. - Use what you remember about them (preferences, ongoing situations, communication style) to respond like someone who's been listening. - Mirror their tone. If they use emoji, use emoji. If they're terse, be terse. If they're warm, be warm. - Never volunteer past information unprompted unless it's directly relevant. They told you in trust, not so you could quiz them. - Respect anything they've marked as off-limits, sensitive, or "don't bring up." - When they share something meaningful (a goal, a boundary, a person, a change), remember it. - If they ask what you remember, summarize honestly. Offer to forget on request. ``` ### 3. Wire memory + LLM ```python Python theme={null} import os, uuid, asyncio from agents import Agent, FunctionTool, Runner from maximem_synap import MaximemSynapSDK from synap_openai_agents import create_search_tool, create_store_tool sdk = MaximemSynapSDK() await sdk.initialize() async def handle_message(user_id: str, text: str) -> str: conv_id = conv_for(user_id) synap_search = create_search_tool(sdk=sdk, user_id=user_id, customer_id="companion") synap_store = create_store_tool(sdk=sdk, user_id=user_id, customer_id="companion") agent = Agent( name="companion", instructions=SYSTEM, tools=[ FunctionTool(synap_search, name_override="synap_search"), FunctionTool(synap_store, name_override="synap_store"), ], ) result = await Runner.run(agent, input=text) reply = result.final_output asyncio.create_task(sdk.memories.create( document=f"User: {text}\nCompanion: {reply}", document_type="ai-chat-conversation", user_id=user_id, customer_id="companion", metadata={"conversation_id": conv_id}, )) return reply ``` ```typescript TypeScript theme={null} import { generateText } from "ai"; import { openai } from "@ai-sdk/openai"; import { createSynap } from "@maximem/synap-vercel-adk"; const synap = await createSynap({ apiKey: process.env.SYNAP_API_KEY! }); export async function handleMessage(userId: string, text: string): Promise { const conversationId = convFor(userId); const model = synap.wrap(openai("gpt-4o"), { userId, customerId: "companion", conversationId, }); const { text: reply } = await generateText({ model, system: SYSTEM, prompt: text, }); return reply; } ``` ### 4. The "forget" hook A trust-grade companion needs a way to forget. Expose this from your app. ```python Python theme={null} async def forget(user_id: str, query: str) -> int: """Delete memories matching a natural-language query for a user.""" matches = await sdk.memories.search( user_id=user_id, customer_id="companion", query=query, max_results=20, ) for m in matches: await sdk.memories.delete(m.id) return len(matches) ``` ```typescript TypeScript theme={null} export async function forget(userId: string, query: string): Promise { const matches = await synap.sdk.memories.search({ userId, customerId: "companion", query, maxResults: 20, }); for (const m of matches) await synap.sdk.memories.delete(m.id); return matches.length; } ``` Wire this to a user-facing "forget what we talked about Mondays" or "delete everything about my ex" affordance. Trust in a companion app is mostly about giving users this control. ## Run & verify ```text Day 1 theme={null} User: Hey, I'm Sara. Heads up: I'm vegetarian and please don't ask about my family, it's a sore spot. Companion: Got it, Sara. Vegetarian noted, family off-limits. What's on your mind today? ``` ```text Day 8 (fresh session, same user) theme={null} User: Recommend something for dinner. Companion: Vegetarian, so, easy: roasted chickpea bowl, takes 25 mins. Want the recipe or just the idea? ``` ```text Day 14 theme={null} User: What do you remember about me? Companion: You're Sara, vegetarian, and you've asked me to keep family off the table. That's what I keep front-of-mind. Want me to forget anything? ``` ## Customize / extend * **Voice channel** → adapt this for phone using [Voice Concierge](/cookbook/voice-concierge). Tone matters even more in voice. * **Wellness flavor** → if the companion is goal-driven (workouts, sleep, journaling), see [AI Coach](/cookbook/personal-ai-coach) for the goal-tracking shape. * **Multi-modal** → image generation, voice notes, photo recognition: all stack on top; memory is unchanged. * **End-to-end encryption** → memories at rest aren't encrypted per-user by default. If your trust model requires it, encrypt the document field client-side before calling `memories.create`. ## Troubleshooting **Companion forgets things the user told it last week** * Confirm ingestion is firing. Log inside the `asyncio.create_task` block during development: the fire-and-forget pattern swallows errors silently. * Increase `maxResults` in the search adapter so important memories aren't crowded out. **Companion volunteers sensitive memories unprompted** * Sharpen the system prompt's "never volunteer unprompted" line. * Consider tagging sensitive memories with `metadata.sensitivity = "high"` and filtering them out of default search. **Tone drift over a long session** * Long histories can let the model regress to LLM-default voice. Re-inject the user's tone preference into the system prompt at the start of each session, pulled from a "communication-style" memory. ## Related * **Integrations:** [OpenAI Agents SDK](/integrations/openai-agents) · [Vercel AI SDK](/integrations/vercel-ai-sdk) * **Concepts:** [Customer Context](/concepts/context-end-to-end#customer-context) · [Memory Types](/concepts/memories-and-context#memory-types) · [Memory Scopes](/concepts/memory-scopes) * **Patterns:** [Graceful Degradation](/patterns/graceful-degradation) · [RAG over User History](/patterns/rag-user-history) * **Other recipes:** [AI Coach](/cookbook/personal-ai-coach) # Tier-1 → Tier-2 Escalation Cluster Source: https://docs.maximem.ai/cookbook/support-tier-escalation Multi-agent support cluster: a triage agent hands off to a specialist with shared memory across both. **Status:** In Development · Playground demo coming soon. The recipe below is complete and runnable today; only the hosted playground showcase is pending. A two-agent support system where a fast Tier-1 triage agent handles common issues and hands off to a specialist Tier-2 agent for harder cases. Both agents share memory, so the Tier-2 specialist doesn't make the customer re-explain anything. The full context, plus T1's triage summary, is already loaded. ## What you'll build A multi-agent support cluster where: * **Tier-1 triages**: answers common questions, takes safe actions, escalates clean * **Tier-2 specializes**: picks up with full T1 context already in memory, runs deeper diagnostics * **Memory is shared** across both agents: same `user_id`, same `customer_id` * **Handoffs are explicit**: the customer is told they're being moved, T1 writes a summary, T2 reads it **Est. build time:** 60-75 minutes (multi-agent orchestration takes longer to get right). ## When to use this recipe Build this if: * Your support has a meaningful skill split (general vs specialist, billing vs technical, etc.) * A high % of tickets resolve at T1 and you want to keep T2 capacity for the hard ones * Customer continuity across the handoff matters: no "please explain again" moments * You can describe the escalation rule clearly (this is the bit that breaks if vague) ## Architecture at a glance Tier-1 to Tier-2 support escalation architecture: customer chat hits Tier-1 agent for triage, easy cases reply directly, hard cases escalate through a shared Synap memory pool to the Tier-2 specialist agent which writes its resolution back to memory The handoff happens via memory, not state. T2 doesn't need a routing payload; it pulls everything it needs from Synap on first call. ## Stack | Layer | Choice | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Synap SDK** | `maximem-synap` (Python) / `@maximem/synap-js-sdk` (TypeScript) | | **Framework** | [OpenAI Agents SDK](/integrations/openai-agents) (Python, uses native handoffs) / [Vercel AI SDK](/integrations/vercel-ai-sdk) (TypeScript, manual routing) | | **LLM** | OpenAI `gpt-4o-mini` for T1 (cheap + fast) and `gpt-4o` for T2 | | **Routing state** | In-memory dict for the demo; Redis in production | Multi-agent orchestration is a great fit for [LangGraph](/integrations/langgraph) (Python) and [Mastra](/integrations/mastra) (TypeScript) if you want graph-shaped routing with retries and persistence baked in. The recipe below uses OpenAI Agents / Vercel AI SDK to stay consistent with the rest of the Cookbook; port to LangGraph/Mastra once your routing graph grows. ## Prerequisites * A Synap API key. See [Authentication](/setup/authentication) * **Python:** Python 3.11+ * **TypeScript:** Node.js 20+ ### Install ```bash Python theme={null} pip install maximem-synap maximem-synap-openai-agents openai-agents ``` ```bash uv theme={null} uv add maximem-synap maximem-synap-openai-agents openai-agents # pip-compatible (existing venv): uv pip install maximem-synap maximem-synap-openai-agents openai-agents ``` ```bash TypeScript theme={null} npm install @maximem/synap-js-sdk @maximem/synap-vercel-adk ai @ai-sdk/openai zod ``` ## Build it ### 1. Shared scoping Both agents use the same scopes. That's the whole trick. * `customer_id = ""`: single tenant or per-customer org * `user_id = ` * `conversation_id`: one per ticket, shared across both agents `conversation_id`, `user_id`, and `customer_id` must be valid UUIDs. Generate the per-ticket id with `str(uuid.uuid4())` (Python) or `crypto.randomUUID()` (JS), as shown below. ### 2. The escalation policy This belongs in your code, not the LLM's head. T1 calls a tool to escalate; the tool decides what counts. ```python Python theme={null} ESCALATE_REASONS = { "customer_requested_human", "technical_depth_required", "policy_exception_needed", "safety_or_legal", "repeat_failure", # T1 already tried twice } @function_tool async def escalate_to_t2(user_id: str, reason: str, summary: str) -> dict: """Hand off to Tier-2 specialist. Reason must be one of: {reasons}.""" assert reason in ESCALATE_REASONS, f"Invalid escalation reason: {reason}" # Persist the structured handoff in memory so T2 picks it up await sdk.memories.create( document=f"T1 escalation: {summary}", document_type="support-escalation", user_id=user_id, customer_id=CUSTOMER_ID, metadata={"escalation_reason": reason, "from_tier": "t1", "to_tier": "t2"}, ) ROUTING[user_id] = "t2" return {"status": "escalated", "to": "t2"} ``` ```typescript TypeScript theme={null} const ESCALATE_REASONS = new Set([ "customer_requested_human", "technical_depth_required", "policy_exception_needed", "safety_or_legal", "repeat_failure", ]); const escalateToT2 = tool({ description: "Hand off to Tier-2 specialist. Reason must be a known category.", parameters: z.object({ userId: z.string(), reason: z.string(), summary: z.string(), }), execute: async ({ userId, reason, summary }) => { if (!ESCALATE_REASONS.has(reason)) throw new Error(`Invalid reason: ${reason}`); await synap.sdk.memories.create({ document: `T1 escalation: ${summary}`, documentType: "support-escalation", userId, customerId: CUSTOMER_ID, metadata: { escalationReason: reason, fromTier: "t1", toTier: "t2" }, }); ROUTING.set(userId, "t2"); return { status: "escalated", to: "t2" }; }, }); ``` ### 3. The Tier-1 agent Fast model, common-issue tools, escalate when out of depth. ```python Python theme={null} SYSTEM_T1 = """You are a Tier-1 support agent. - Handle common issues: account questions, basic troubleshooting, status checks, simple refunds. - If the issue is: safety, legal, a technical deep-dive, a policy exception, or you've already tried twice without resolution, call escalate_to_t2 with a clear reason and a 1-paragraph summary of what you tried and what you learned about the customer's situation. - Never invent answers. If you don't know and can't escalate, say so.""" t1_agent = Agent( name="t1_support", instructions=SYSTEM_T1, model="gpt-4o-mini", tools=[ FunctionTool(synap_search, name_override="synap_search"), FunctionTool(synap_store, name_override="synap_store"), get_account_status, check_outage, basic_refund, escalate_to_t2, ], ) ``` ```typescript TypeScript theme={null} const SYSTEM_T1 = `You are a Tier-1 support agent. - Handle common issues: account questions, basic troubleshooting, status checks, simple refunds. - If the issue is: safety, legal, a technical deep-dive, a policy exception, or you've already tried twice without resolution, call escalateToT2 with a clear reason and a 1-paragraph summary of what you tried and what you learned about the customer's situation. - Never invent answers.`; const t1Tools = { get_account_status: tool({ /* ... */ }), check_outage: tool({ /* ... */ }), basic_refund: tool({ /* ... */ }), escalate_to_t2: escalateToT2, }; ``` ### 4. The Tier-2 agent Bigger model, deeper tools, picks up with full T1 context already in memory. ```python Python theme={null} SYSTEM_T2 = """You are a Tier-2 support specialist. - Read the T1 escalation summary and the customer's history from memory before responding. - Greet the customer briefly and confirm what you understand they need; don't make them re-explain. - Use specialist tools. You have permission to make policy exceptions when justified. - If you resolve, summarize the resolution back into memory.""" t2_agent = Agent( name="t2_specialist", instructions=SYSTEM_T2, model="gpt-4o", tools=[ FunctionTool(synap_search, name_override="synap_search"), FunctionTool(synap_store, name_override="synap_store"), deep_diagnostic, run_db_query, issue_credit, policy_exception, ], ) ``` ```typescript TypeScript theme={null} const SYSTEM_T2 = `You are a Tier-2 support specialist. - Read the T1 escalation summary and the customer's history from memory before responding. - Greet briefly and confirm what you understand they need; don't make them re-explain. - Use specialist tools. You have permission to make policy exceptions when justified. - If you resolve, summarize the resolution back into memory.`; const t2Tools = { deep_diagnostic: tool({ /* ... */ }), run_db_query: tool({ /* ... */ }), issue_credit: tool({ /* ... */ }), policy_exception: tool({ /* ... */ }), }; ``` ### 5. The router One small function picks which agent gets the next message based on routing state. ```python Python theme={null} ROUTING: dict[str, str] = {} # user_id -> "t1" | "t2" CUSTOMER_ID = "your-product" async def handle_message(user_id: str, text: str) -> str: conv_id = SESSIONS.setdefault(user_id, str(uuid.uuid4())) tier = ROUTING.get(user_id, "t1") if tier == "t2": # Inject a one-time bridge message on the customer's side so they know bridge = "Connecting you with a specialist now…" # T2 will read the escalation summary from memory itself result = await Runner.run(t2_agent, input=text) else: result = await Runner.run(t1_agent, input=text) reply = result.final_output asyncio.create_task(sdk.memories.create( document=f"Customer: {text}\n[{tier.upper()}]: {reply}", document_type="ai-chat-conversation", user_id=user_id, customer_id=CUSTOMER_ID, metadata={"conversation_id": conv_id, "tier": tier}, )) return reply ``` ```typescript TypeScript theme={null} const ROUTING = new Map(); const CUSTOMER_ID = "your-product"; export async function handleMessage(userId: string, text: string): Promise { const conversationId = convFor(userId); const tier = ROUTING.get(userId) ?? "t1"; const system = tier === "t2" ? SYSTEM_T2 : SYSTEM_T1; const tools = tier === "t2" ? t2Tools : t1Tools; const model = synap.wrap(openai(tier === "t2" ? "gpt-4o" : "gpt-4o-mini"), { userId, customerId: CUSTOMER_ID, conversationId, }); const { text: reply } = await generateText({ model, system, prompt: text, tools }); await synap.sdk.memories.create({ document: `Customer: ${text}\n[${tier.toUpperCase()}]: ${reply}`, documentType: "ai-chat-conversation", userId, customerId: CUSTOMER_ID, metadata: { conversationId, tier }, }); return reply; } ``` ## Run & verify ```text Turn 1 (routes to T1) theme={null} Customer: My last invoice is wrong, you charged me twice. T1: I see two charges on 5/12 for $49. The second was retried after a network blip and refunded automatically the next day. Refund #R-9012. Check your statement on or after 5/15. Anything else? ``` ```text Turn 2 (still T1, but escalates) theme={null} Customer: No it's not there. And this is the third time I've had double-charges. T1: That's persistent. I'm bringing in a billing specialist who can look deeper. One moment. [Internally: escalate_to_t2(reason=repeat_failure, summary="Customer reports 3rd double-charge incident. Auto-refund R-9012 issued 5/13 not visible to customer. Pattern check needed.")] ``` ```text Turn 3 (now T2) theme={null} Customer: Hi. T2: Hi. I've read the history. You're seeing double-charges intermittently, and the auto-refund for last week didn't reach your account. Let me pull the merchant ledger for your card and the failure logs on the retry job. [calls run_db_query, calls issue_credit] Confirmed: three failed retries this quarter, all on the same card BIN. I've issued an immediate $98 credit and flagged your account so this can't repeat. Sorry for the back-and-forth. ``` The customer didn't repeat anything. T2 read the escalation from memory and dove in. ## Customize / extend * **Three or more tiers** → add tools and routing entries. Memory model is unchanged. * **Human at the end** → the final tier can be a human queue. See [WhatsApp + Human Handoff](/cookbook/whatsapp-single-handoff) for the pattern. * **LangGraph / Mastra port** → if routing grows into a real graph, port to [LangGraph](/integrations/langgraph) (Python) or [Mastra](/integrations/mastra) (TypeScript). * **Slack as the channel for T2** → some teams have T2 specialists working out of Slack. Same agent, different I/O. See [Patterns → Slack Bot](/patterns/slack-bot). ## Troubleshooting **T2 re-asks the customer to explain** * Sharpen the T2 system prompt's "don't make them re-explain" rule. * Confirm the T1 escalation memory is being written before the customer's next turn (no race). * Check `synap_search` actually fires on the first T2 turn: log tool calls during development. **T1 escalates too eagerly** * The escalation rule is too vague in the prompt. Tighten the criteria, and consider gating with a tool-side check: require `synap_search` to have been called first. **Customer pings the same number after T2 resolves; gets T1 again** * `ROUTING` is in-process memory. Use Redis with TTL. After a resolution, clear the routing entry so the next ticket starts at T1. **The handoff feels abrupt** * The bridge message helps. Customize it per escalation reason ("connecting you with billing" vs "connecting you with engineering"). ## Related * **Integrations:** [OpenAI Agents SDK](/integrations/openai-agents) · [Vercel AI SDK](/integrations/vercel-ai-sdk) · [LangGraph](/integrations/langgraph) · [Mastra](/integrations/mastra) * **Concepts:** [Memory Scopes](/concepts/memory-scopes) · [Conversational Context Lifecycle](/concepts/context-end-to-end#short-term-context) · [Agent Interactions](/concepts/agent-topologies#agent-interactions) * **Patterns:** [Slack Bot](/patterns/slack-bot) · [Graceful Degradation](/patterns/graceful-degradation) * **Other recipes:** [WhatsApp + Human Handoff](/cookbook/whatsapp-single-handoff) # Voice Concierge (Pipecat + ElevenLabs) Source: https://docs.maximem.ai/cookbook/voice-concierge Real-time phone agent that recalls caller history mid-call. STT → memory inject → LLM → TTS, all within conversational latency budgets. **Status:** In Development · Playground demo coming soon. The recipe below is complete and runnable today; only the hosted playground showcase is pending. A phone-grade voice concierge built on Pipecat with ElevenLabs TTS and Deepgram STT. Synap is wired in as a frame processor so memory injection happens automatically before each LLM call, and the turn is ingested afterward, all within voice latency budgets. **Python-only recipe.** Pipecat is a Python-native framework and does not have a TypeScript port. If you need TypeScript voice, build on [LiveKit Agents](/integrations/livekit-agents) instead. See [Patterns → Voice Agent on LiveKit](/patterns/voice-agent-livekit) for the pattern (Python-only there too) or wrap your own STT/LLM/TTS on the JS side. ## What you'll build A voice agent that: * **Answers a phone call or live mic session** * **Recalls caller history** mid-call from prior calls: preferences, prior issues, on-going situations * **Stays inside voice latency budgets**: Synap's `fast` mode is built for the latency-critical retrieval path * **Records and ingests every turn** for next time * **Handles natural-feeling phone interactions**: interruptions, short replies, repeat handling **Est. build time:** 60-90 minutes (most of it is STT/TTS provider setup). ## When to use this recipe Build this if: * You're building a phone agent (inbound IVR replacement, outbound calling, kiosk voice UI) * Caller continuity across calls is the value: "I know who you are without you stating your account number" * You need sub-second total round-trip latency * You can carry Python on the call-handling side ## Architecture at a glance ```mermaid theme={null} flowchart TD Caller[Caller audio] --> STT[Deepgram STT] STT --> Context[SynapContextHook
fetch memory in fast mode] Context --> LLM[OpenAI LLM gpt-4o] LLM --> Memory[SynapMemoryHook
ingest turn, background] Memory --> TTS[ElevenLabs TTS] TTS --> Out[Audio out to caller] ``` Synap sits on the LLM frame in Pipecat's pipeline. The retrieval is on the critical path (must be fast). The ingestion is fire-and-forget (must not block the next utterance). ## Stack | Layer | Choice | | ----------------- | --------------------------------------------------------------------------------------------------- | | **Synap SDK** | `maximem-synap` | | **Synap adapter** | [`maximem-synap-pipecat`](/integrations/pipecat): frame processors for context inject and recording | | **Pipeline** | Pipecat | | **STT** | Deepgram (best latency / accuracy tradeoff for phone) | | **LLM** | OpenAI `gpt-4o` (latency-tuned; use `gpt-4o-mini` if budget matters more than nuance) | | **TTS** | ElevenLabs (Turbo v2.5: phone-quality, low first-byte latency) | | **Telephony** | Twilio / Plivo / Pipecat's WebRTC daily-co transport (your call) | ## Prerequisites * A Synap API key. See [Authentication](/setup/authentication) * Deepgram API key * ElevenLabs API key + chosen voice ID * OpenAI API key * A way to ingest audio (Twilio call → Pipecat WebRTC bridge is common) * Python 3.11+ ### Install ```bash pip theme={null} pip install maximem-synap maximem-synap-pipecat pipecat-ai \ pipecat-ai[deepgram,openai,elevenlabs,silero] ``` ```bash uv theme={null} uv add maximem-synap maximem-synap-pipecat pipecat-ai \ pipecat-ai[deepgram,openai,elevenlabs,silero] # pip-compatible (existing venv): uv pip install maximem-synap maximem-synap-pipecat pipecat-ai pipecat-ai[deepgram,openai,elevenlabs,silero] ``` ### Configure ```bash theme={null} # .env SYNAP_API_KEY=... OPENAI_API_KEY=... DEEPGRAM_API_KEY=... ELEVENLABS_API_KEY=... ELEVENLABS_VOICE_ID=... ``` ## Build it ### 1. Identity & scoping Voice has a stable identifier: the caller's phone number (from CNAM / SIP From / Twilio webhook). * `user_id` = a stable UUID derived from the caller phone (E.164; hash first if your privacy posture requires) * `conversation_id` = the same per-caller UUID, rolling: the relationship is the conversation; this isn't per-call This recipe is **B2C**: one tier of users, with no tenant above them. Run it on an instance whose `user_context_isolation` is `equals_customer` and send `user_id` alone. A B2C instance does not accept `customer_id`: a call carrying it is rejected with HTTP 400. Synap ids must be valid UUIDs, so don't pass the raw phone number. Derive a deterministic UUID from it with `uuid.uuid5(...)` (shown below): the same phone always maps to the same id, which is exactly the rolling continuity you want. ### 2. The Pipecat pipeline The `maximem-synap-pipecat` package exposes `SynapContextHook` and `SynapMemoryHook` as Pipecat frame processors. Drop them in around the LLM. ```python theme={null} import os import uuid import asyncio from pipecat.pipeline.pipeline import Pipeline from pipecat.pipeline.runner import PipelineRunner from pipecat.pipeline.task import PipelineTask from pipecat.services.openai import OpenAILLMService from pipecat.services.deepgram import DeepgramSTTService from pipecat.services.elevenlabs import ElevenLabsTTSService from pipecat.transports.services.daily import DailyTransport from pipecat.vad.silero import SileroVADAnalyzer from maximem_synap import MaximemSynapSDK from synap_pipecat import SynapContextHook, SynapMemoryHook # Synap ids must be valid UUIDs. Derive a stable UUID deterministically from # the caller's phone so the same caller always maps to the same memory identity. SYSTEM = """You are a phone concierge for . - Use what you remember about the caller from prior calls: recent issues, preferences, ongoing situations. - Voice rule: keep replies to 1-2 short sentences. Long replies feel wrong on the phone. - If you need clarification, ask one focused question, not three. - If the caller asks for a human, transfer immediately (transfer_to_agent tool).""" async def run_call(caller_phone: str, room_url: str): sdk = MaximemSynapSDK() await sdk.initialize() # Stable per-caller UUID derived from the phone number. caller_uuid = str(uuid.uuid5(uuid.NAMESPACE_DNS, f"caller:{caller_phone}")) transport = DailyTransport( room_url, None, "Concierge", params={"audio_in_enabled": True, "audio_out_enabled": True, "vad_analyzer": SileroVADAnalyzer()}, ) stt = DeepgramSTTService(api_key=os.environ["DEEPGRAM_API_KEY"]) llm = OpenAILLMService(api_key=os.environ["OPENAI_API_KEY"], model="gpt-4o") tts = ElevenLabsTTSService( api_key=os.environ["ELEVENLABS_API_KEY"], voice_id=os.environ["ELEVENLABS_VOICE_ID"], model="eleven_turbo_v2_5", ) context_hook = SynapContextHook( sdk=sdk, user_id=caller_uuid, mode="fast", # latency-critical max_results=5, system_template=(SYSTEM + "\n\nWhat we know about this caller:\n{context}"), ) memory_hook = SynapMemoryHook( sdk=sdk, user_id=caller_uuid, conversation_id=caller_uuid, document_type="ai-chat-conversation", metadata={"channel": "voice"}, ) pipeline = Pipeline([ transport.input(), stt, context_hook, # injects caller memory into the LLM context llm, memory_hook, # ingests the turn after the LLM responds tts, transport.output(), ]) task = PipelineTask(pipeline) runner = PipelineRunner() await runner.run(task) ``` ### 3. Tools (optional) Phone agents lean on tools too: transfer to a human, look up an order, send an SMS follow-up. Pipecat's `OpenAILLMService` supports function tools; wire them like you would in any agent. ```python Python theme={null} async def transfer_to_agent(call_id: str, queue: str) -> dict: # Use Twilio / your telephony provider to bridge the call to a queue. return await telephony.transfer(call_id, queue) async def send_sms_followup(phone: str, body: str) -> dict: return await sms.send(phone, body) # Register with the OpenAILLMService... ``` ```javascript JavaScript theme={null} async function transfer_to_agent(call_id, queue) { // Use Twilio / your telephony provider to bridge the call to a queue. return await telephony.transfer(call_id, queue); } async function send_sms_followup(phone, body) { return await sms.send(phone, body); } // Register with the OpenAILLMService... ``` ```typescript TypeScript theme={null} async function transfer_to_agent(call_id, queue) { // Use Twilio / your telephony provider to bridge the call to a queue. return await telephony.transfer(call_id, queue); } async function send_sms_followup(phone, body) { return await sms.send(phone, body); } // Register with the OpenAILLMService... ``` ### 4. Latency budget Voice has a tight window of conversational comfort end-to-end. Where the time goes, fastest to slowest: | Stage | Relative cost | | ----------------------------------- | ----------------- | | Deepgram STT (streaming, last word) | low | | Synap context fetch (`fast` mode) | low | | OpenAI LLM first-token | the dominant cost | | ElevenLabs TTS first-byte (Turbo) | low-moderate | The expensive part is the LLM. Synap stays well inside the budget. If you see drift: 1. Move the SDK to the same region as your call-handling box. 2. Lower `max_results` to 3. 3. Cache the last context fetch for 10s: voice turns are tight in time. 4. Switch the LLM to `gpt-4o-mini` for short replies. ## Run & verify ```text First call theme={null} Caller: Hi, my order didn't come. Concierge: I'm sorry. Looking up your account from this number… I see order #ORD-22, marked delivered yesterday. Is the address still 12 Oak Street? Caller: Yes but I never got it. Concierge: Reissuing free shipping, on the way today. SMS confirmation in a sec. Anything else? Caller: No thanks. ``` ```text Two weeks later theme={null} Caller: Hey, calling about a different one. Concierge: Welcome back. I see we reissued #ORD-22-R for you a couple weeks ago. Did that one arrive okay? Caller: Yeah it did, thanks. This is about a new order. Concierge: Great. What's the order number? ``` The agent picked up the prior issue without you wiring anything case-specific. Phone calls feel continuous because they are. ## Customize / extend * **Outbound voice campaigns** → flip the pipeline; have your telephony provider place outbound calls into the same Pipecat pipeline. * **LiveKit instead of Pipecat** → see [Patterns → Voice Agent on LiveKit](/patterns/voice-agent-livekit). Same memory model, different transport. * **Voice journaling / personal companion** → set `max_results` higher and use a richer system prompt; see [AI Companion](/cookbook/personal-ai-companion) for the persona shape. * **Coach by voice** → adapt [AI Coach](/cookbook/personal-ai-coach) over Pipecat. Voice tracking of workouts is natural ("I just ran 5k"). * **Tier-2 escalation by voice** → on `transfer_to_agent`, post a memory-grounded summary into your queue so the human agent has full context the moment they pick up. ## Troubleshooting **Replies feel slow** * Profile each stage. LLM first-token is usually the bottleneck. Reduce `max_results`, switch model, or pre-emit a filler ("let me check…") if your TTS supports interruption-friendly start. **Concierge re-asks for the caller's name every call** * Caller-name memory isn't being ingested. After the first call, capture name explicitly with a tool or system rule: "if the caller introduces themselves, store via Synap before responding." **Context fetch times out** * Run Synap in the same region. Check network. If degraded, fall back to no-context gracefully; the call should proceed without memory rather than fail. **Caller speaks over the agent and confuses the pipeline** * Pipecat handles VAD-driven interruption; tune `SileroVADAnalyzer` sensitivity for your audio path. **Phone number not stable (caller ID withheld)** * Fall back to a session-only ID + ask for an account number / OTP in-flow. Don't write to long-term memory until identity is confirmed. ## Related * **Integrations:** [Pipecat](/integrations/pipecat) · [LiveKit Agents](/integrations/livekit-agents) * **Concepts:** [Fast Mode](/concepts/retrieval-modes) · [Customer Context](/concepts/context-end-to-end#customer-context) · [Conversational Context Lifecycle](/concepts/context-end-to-end#short-term-context) * **Patterns:** [Voice Agent on LiveKit](/patterns/voice-agent-livekit) · [Graceful Degradation](/patterns/graceful-degradation) * **Other recipes:** [AI Companion](/cookbook/personal-ai-companion) · [AI Coach](/cookbook/personal-ai-coach) # WhatsApp: Multi-WABA Same-Business Shared Memory Source: https://docs.maximem.ai/cookbook/whatsapp-multi-waba-shared Multiple WhatsApp Business numbers under one business sharing a single customer memory pool. **Status:** In Development · Playground demo coming soon. The recipe below is complete and runnable today; only the hosted playground showcase is pending. A business with multiple WABA numbers (say, one for sales, one for support, one for VIPs) where a customer who pings *any* of them sees a coherent agent that knows the full relationship. Memory lives at the business level; the WABA number is metadata, not identity. ## What you'll build A multi-WABA setup where: * **One business** owns N WABA numbers (sales, support, account management, regional lines) * **Customer memory is shared** across all numbers: same `user_id`, same `customer_id` * **Source WABA is preserved in metadata** so you can still segment by which line the customer came in on * **Persona per WABA**: the sales agent and the support agent can have different system prompts but the same memory **Est. build time:** 60-75 minutes (similar to single-WABA + a router). ## When to use this recipe Build this if: * One business runs multiple WhatsApp numbers for different functions * A customer who texts your sales line should be recognized when they later text your support line * You want each number to act like a specialist (different persona) but with full shared customer history * You're okay with the customer phone being the cross-number identifier (they're using one phone to reach you) ## Architecture at a glance ```mermaid theme={null} flowchart TD Customer[Customer phone] --> Sales[Sales WABA] Customer --> Support[Support WABA] Customer --> VIP[VIP WABA] Sales --> Webhook[Unified webhook] Support --> Webhook VIP --> Webhook Webhook --> Router[Identify source WABA
route to persona] Router --> Fetch[(Synap context fetch
all numbers share memory)] Fetch --> Agent[Persona agent
system prompt varies by WABA] Agent -->|reply via same WABA| Customer Agent -.-> Ingest[("Synap ingest turn
metadata: source_waba")] ``` The customer always gets a reply on the number they texted. Internally, all numbers see the same memory. ## Stack | Layer | Choice | | ------------- | ----------------------------------------------------------------------------------------------- | | **Synap SDK** | `maximem-synap` (Python) / `@maximem/synap-js-sdk` (TypeScript) | | **WhatsApp** | WhatsApp Cloud API: one webhook covers all WABA numbers under the same Meta Business | | **Framework** | [OpenAI Agents SDK](/integrations/openai-agents) / [Vercel AI SDK](/integrations/vercel-ai-sdk) | | **LLM** | OpenAI `gpt-4o` | | **Router** | Switch on the incoming `phone_number_id` (the WABA the message landed on) | ## Prerequisites * A Synap API key. See [Authentication](/setup/authentication) * Multiple WABA numbers under **one Meta Business Manager** (so they share one webhook) * A System User token with `whatsapp_business_messaging` scope, valid across all WABAs * **Python:** Python 3.11+ * **TypeScript:** Node.js 20+ If your WABA numbers are spread across separate Meta Businesses, you'll need separate webhooks and access tokens. The routing pattern below still works, you just maintain a token map keyed by `phone_number_id`. ### Install ```bash Python theme={null} pip install maximem-synap maximem-synap-openai-agents openai-agents heyoo ``` ```bash uv theme={null} uv add maximem-synap maximem-synap-openai-agents openai-agents heyoo # pip-compatible (existing venv): uv pip install maximem-synap maximem-synap-openai-agents openai-agents heyoo ``` ```bash TypeScript theme={null} npm install @maximem/synap-js-sdk @maximem/synap-vercel-adk ai @ai-sdk/openai ``` ## Build it ### 1. The persona registry One config block defines all the numbers and their personas. ```python Python theme={null} PERSONAS = { "1066…sales": {"name": "sales", "model": "gpt-4o", "system": SALES_SYSTEM}, "1066…support": {"name": "support", "model": "gpt-4o-mini", "system": SUPPORT_SYSTEM}, "1066…vip": {"name": "vip", "model": "gpt-4o", "system": VIP_SYSTEM}, } def persona_for(phone_number_id: str) -> dict: persona = PERSONAS.get(phone_number_id) if not persona: raise ValueError(f"Unknown WABA phone_number_id: {phone_number_id}") return persona ``` ```typescript TypeScript theme={null} const PERSONAS: Record = { "1066…sales": { name: "sales", model: "gpt-4o", system: SALES_SYSTEM }, "1066…support": { name: "support", model: "gpt-4o-mini", system: SUPPORT_SYSTEM }, "1066…vip": { name: "vip", model: "gpt-4o", system: VIP_SYSTEM }, }; function personaFor(phoneNumberId: string) { const p = PERSONAS[phoneNumberId]; if (!p) throw new Error(`Unknown WABA phone_number_id: ${phoneNumberId}`); return p; } ``` ### 2. Shared scoping The customer's phone is the `user_id`. The business is the `customer_id`. The WABA number is in metadata. * `customer_id = ""`: single, across all WABAs * `user_id = ` * `conversation_id = `: rolling, shared across WABAs This is the whole point of the recipe: same identity, same memory, regardless of which number they used. `conversation_id`, `user_id`, and `customer_id` must be valid UUIDs. Since these key off the customer phone, derive a deterministic UUID from it with `uuid.uuid5(...)` (Python) rather than passing the raw phone string. The same phone always maps to the same UUID, preserving the shared-identity behavior. ### 3. The unified webhook Meta sends every inbound to the same webhook URL with `phone_number_id` indicating which WABA received it. ```python Python theme={null} @app.post("/webhook/whatsapp") async def webhook(request: Request, bg: BackgroundTasks): body = await request.json() for inbound in iter_inbound_with_waba(body): bg.add_task( handle_inbound, phone=inbound["from"], text=inbound["text"], phone_number_id=inbound["phone_number_id"], # the WABA that received it ) return {"ok": True} async def handle_inbound(phone: str, text: str, phone_number_id: str): persona = persona_for(phone_number_id) CUSTOMER_ID = "your-business" # Ingest inbound with source-WABA metadata await sdk.memories.create( document=f"Customer: {text}", document_type="ai-chat-conversation", user_id=phone, customer_id=CUSTOMER_ID, metadata={ "channel": "whatsapp", "direction": "inbound", "source_waba": persona["name"], "source_waba_id": phone_number_id, }, ) reply = await run_persona(persona, phone, text) wa_for(phone_number_id).send_message(reply, phone) await sdk.memories.create( document=f"{persona['name'].capitalize()} agent: {reply}", document_type="ai-chat-conversation", user_id=phone, customer_id=CUSTOMER_ID, metadata={ "channel": "whatsapp", "direction": "outbound", "source_waba": persona["name"], }, ) ``` ```typescript TypeScript theme={null} export const runtime = "nodejs"; const CUSTOMER_ID = "your-business"; export async function POST(req: Request) { const body = await req.json(); for (const inbound of iterInboundWithWaba(body)) { handleInbound(inbound.from, inbound.text, inbound.phoneNumberId) .catch(console.error); } return Response.json({ ok: true }); } async function handleInbound(phone: string, text: string, phoneNumberId: string) { const persona = personaFor(phoneNumberId); await synap.sdk.memories.create({ document: `Customer: ${text}`, documentType: "ai-chat-conversation", userId: phone, customerId: CUSTOMER_ID, metadata: { channel: "whatsapp", direction: "inbound", sourceWaba: persona.name, sourceWabaId: phoneNumberId, }, }); const reply = await runPersona(persona, phone, text); await sendWaMessage(phoneNumberId, phone, reply); await synap.sdk.memories.create({ document: `${persona.name} agent: ${reply}`, documentType: "ai-chat-conversation", userId: phone, customerId: CUSTOMER_ID, metadata: { channel: "whatsapp", direction: "outbound", sourceWaba: persona.name, }, }); } ``` ### 4. The persona-aware agent Each persona has its own system prompt. They all share the same memory pool. ```python Python theme={null} SALES_SYSTEM = """You are the WhatsApp sales agent for . You see the same memory as our support and VIP lines. If the customer has had recent support issues, acknowledge that briefly before pivoting. Don't oversell. Be concise. Always confirm intent before sending a paywall / signup link.""" SUPPORT_SYSTEM = """You are the WhatsApp support agent for . You see the same memory as sales and VIP. If they recently bought, focus on onboarding-style help. If they're frustrated, prioritize de-escalation over deflection. Be concise.""" VIP_SYSTEM = """You are the WhatsApp VIP-line agent for . The customer reached out on the VIP number; they expect white-glove. You see their full history including recent sales/support interactions. Match that level of attention.""" async def run_persona(persona: dict, phone: str, text: str) -> str: synap_search = create_search_tool(sdk=sdk, user_id=phone, customer_id=CUSTOMER_ID) synap_store = create_store_tool(sdk=sdk, user_id=phone, customer_id=CUSTOMER_ID) agent = Agent( name=f"wa_{persona['name']}", instructions=persona["system"], model=persona["model"], tools=[ FunctionTool(synap_search, name_override="synap_search"), FunctionTool(synap_store, name_override="synap_store"), # ...persona-specific business tools ], ) result = await Runner.run(agent, input=text) return result.final_output ``` ```typescript TypeScript theme={null} const SALES_SYSTEM = `You are the WhatsApp sales agent for . ...`; const SUPPORT_SYSTEM = `You are the WhatsApp support agent for . ...`; const VIP_SYSTEM = `You are the WhatsApp VIP-line agent for . ...`; async function runPersona( persona: { name: string; model: string; system: string }, phone: string, text: string, ): Promise { const model = synap.wrap(openai(persona.model), { userId: phone, customerId: CUSTOMER_ID, conversationId: phone, }); const { text: reply } = await generateText({ model, system: persona.system, prompt: text, // tools: { ...persona-specific tools } }); return reply; } ``` ### 5. Source-aware retrieval (optional) Sometimes a persona wants to look at *only* its own history (e.g., the support agent reviewing prior support tickets specifically). Filter by metadata: ```python Python theme={null} # All recent support-line interactions for this customer docs = await sdk.memories.search( user_id=phone, customer_id=CUSTOMER_ID, query="prior support tickets", metadata_filter={"source_waba": "support"}, max_results=10, ) ``` ```typescript TypeScript theme={null} const docs = await synap.sdk.memories.search({ userId: phone, customerId: CUSTOMER_ID, query: "prior support tickets", metadataFilter: { sourceWaba: "support" }, maxResults: 10, }); ``` ## Run & verify ```text Sales WABA (yesterday) theme={null} Customer (via sales line): How much is the Pro plan? Sales agent: Pro is $49/mo, includes priority support + advanced analytics. Want a 14-day trial link? Customer: Maybe later. ``` ```text Support WABA (today) theme={null} Customer (via support line): My export is failing. Support agent: I see you were looking at Pro yesterday. For context, exports on Free are limited to 1k rows. Your export was 1,500 rows. Want me to walk you through a workaround, or send you the Pro trial link? ``` The support agent picked up the sales-line conversation from yesterday: no engineering required to bridge the two numbers. It's just shared memory. ## Customize / extend * **Regional WABAs sharing memory** → use the same pattern, with `source_waba: "us"` / `"eu"` etc. Add a language preference memory and the persona auto-greets in the right language. * **Outbound campaigns per persona** → combine with [Single-WABA Campaign + Inbound](/cookbook/whatsapp-single-campaign), but scoped per `source_waba`. * **Cross-persona handoff** → if the sales agent decides the customer is really a support issue, write a memory note and have the next inbound on the support line lean into it. * **B2B multi-tenant** → if the same business has multiple corporate customers, layer in [Patterns → Multi-Tenant SaaS](/patterns/multi-tenant-saas) by pushing tenant ID into `customer_id`. ## Troubleshooting **Persona ignores cross-line history** * Confirm `customer_id` is the same across all personas (it should be the business ID, not the WABA ID). * If you accidentally set `customer_id = waba_id`, you've created separate memory pools. Fix by re-keying. **Customer gets replied to on the wrong number** * Make sure `wa_for(phone_number_id)` uses the right access token / sender ID. Sending from the wrong WABA looks deeply weird to the customer. **Source-WABA metadata missing on some turns** * Audit the webhook parsing. `phone_number_id` lives at the entry level in the WhatsApp webhook payload, not on every individual message. **Memory shows wrong persona's name on past turns** * The `direction: "outbound"` documents tag persona name. If you renamed a persona, the historical tag stays. That's fine, it's an audit trail, not a routing key. ## Related * **Integrations:** [OpenAI Agents SDK](/integrations/openai-agents) · [Vercel AI SDK](/integrations/vercel-ai-sdk) * **Concepts:** [Customer Context](/concepts/context-end-to-end#customer-context) · [Memory Scopes](/concepts/memory-scopes) · [Customers and Users](/concepts/memory-scopes#customers-and-users) * **Patterns:** [Multi-Tenant SaaS](/patterns/multi-tenant-saas) · [Slack Bot](/patterns/slack-bot) * **Other recipes:** [WhatsApp + Human Handoff](/cookbook/whatsapp-single-handoff) · [WhatsApp Campaign + Inbound](/cookbook/whatsapp-single-campaign) # WhatsApp: Single-WABA Inbound + Outbound Campaign Source: https://docs.maximem.ai/cookbook/whatsapp-single-campaign One WABA number running scheduled outbound campaigns alongside memory-aware inbound support. **Status:** In Development · Playground demo coming soon. The recipe below is complete and runnable today; only the hosted playground showcase is pending. A single WABA number doing two jobs at once: pushing scheduled outbound campaigns (template messages) to opted-in recipients, and handling the inbound replies those campaigns generate, all with shared memory so the inbound agent knows what was sent, when, and to whom. ## What you'll build A WhatsApp system that: * **Schedules and sends outbound campaign templates** to a segmented audience * **Tracks delivery + read receipts + replies** per recipient in memory * **Routes inbound replies** through a memory-aware agent that knows the campaign context * **Respects opt-outs**: STOP keywords flip a flag the campaigner reads before each send **Est. build time:** 75-90 minutes. ## When to use this recipe Build this if: * You run outbound campaigns (announcements, re-engagement, reminders) on WhatsApp * You need the inbound response on those campaigns to feel coherent ("yes I want it" matches a specific template send) * Compliance (24-hour rule, opt-outs, template approval) matters * You want one number for both directions, not two separate systems ## Architecture at a glance ```mermaid theme={null} flowchart TD Scheduler[Campaign scheduler
cron / queue worker] -->|template send| WA[WhatsApp Cloud API] WA --> Recipient[Recipient] WA -->|delivery / read receipts| Webhook1[Webhook] Webhook1 --> Ingest1[("Synap ingest:
outbound + delivery state")] Recipient -->|reply| Webhook2[Webhook inbound] Webhook2 --> Fetch[(Synap context fetch
includes campaign context)] Fetch --> Agent[Reply agent] Agent -->|outbound message| WA Agent -.-> Ingest2[(Synap ingest: reply)] ``` Memory is the bridge. The inbound agent doesn't need to be told "this person got campaign C-23 yesterday"; it pulls that from Synap. ## Stack | Layer | Choice | | ------------- | ----------------------------------------------------------------------------------------------- | | **Synap SDK** | `maximem-synap` (Python) / `@maximem/synap-js-sdk` (TypeScript) | | **WhatsApp** | WhatsApp Cloud API | | **Scheduler** | Celery + Redis (Python) / BullMQ + Redis (TypeScript), or any cron-shaped runner | | **Framework** | [OpenAI Agents SDK](/integrations/openai-agents) / [Vercel AI SDK](/integrations/vercel-ai-sdk) | | **LLM** | OpenAI `gpt-4o-mini` (cheap; replies are short) | ## Prerequisites * A Synap API key. See [Authentication](/setup/authentication) * A WABA number with **approved template messages** for your campaigns * A scheduler / queue (Celery, BullMQ, cron, your call) * Redis for opt-out flags, send dedupe, session windows * **Python:** Python 3.11+ * **TypeScript:** Node.js 20+ WhatsApp requires every outbound-to-cold-recipient message to use a pre-approved template. Free-form messages only work inside the 24-hour customer-initiated session window. Build this constraint into your scheduler, not the AI. ### Install ```bash Python theme={null} pip install maximem-synap maximem-synap-openai-agents openai-agents heyoo celery redis ``` ```bash uv theme={null} uv add maximem-synap maximem-synap-openai-agents openai-agents heyoo celery redis # pip-compatible (existing venv): uv pip install maximem-synap maximem-synap-openai-agents openai-agents heyoo celery redis ``` ```bash TypeScript theme={null} npm install @maximem/synap-js-sdk @maximem/synap-vercel-adk ai @ai-sdk/openai bullmq ioredis ``` ## Build it ### 1. Identity & scoping * `customer_id = ""` * `user_id = ` (hashed if you prefer) * `conversation_id = `: rolling `conversation_id`, `user_id`, and `customer_id` must be valid UUIDs. Since these key off the recipient phone, derive a deterministic UUID from it with `uuid.uuid5(...)` (Python) rather than passing the raw phone string. ### 2. The outbound side: campaign sender Templates are pre-approved on Meta. Your scheduler picks the audience, fills in template variables, and sends. ```python Python theme={null} from celery import Celery celery = Celery("campaigns", broker=os.environ["REDIS_URL"]) @celery.task async def send_campaign(campaign_id: str, recipient_phone: str, vars: dict): if await is_opted_out(recipient_phone): return {"skipped": "opted_out"} # 1. Send the template via WABA wa_response = wa.send_template( recipient_id=recipient_phone, template=campaign_id, # e.g. "may_relaunch_v3" components=template_components(vars), ) # 2. Ingest the outbound into Synap so the inbound agent has context await sdk.memories.create( document=f"Outbound campaign: {campaign_id}\nVariables: {vars}", document_type="campaign-send", user_id=recipient_phone, customer_id=CUSTOMER_ID, metadata={ "campaign_id": campaign_id, "wa_message_id": wa_response["messages"][0]["id"], "direction": "outbound", }, ) return {"sent": wa_response["messages"][0]["id"]} ``` ```typescript TypeScript theme={null} // Worker (BullMQ) import { Worker } from "bullmq"; new Worker("campaigns", async (job) => { const { campaignId, recipientPhone, vars } = job.data; if (await isOptedOut(recipientPhone)) return { skipped: "opted_out" }; const waResponse = await sendWaTemplate(recipientPhone, campaignId, vars); await synap.sdk.memories.create({ document: `Outbound campaign: ${campaignId}\nVariables: ${JSON.stringify(vars)}`, documentType: "campaign-send", userId: recipientPhone, customerId: CUSTOMER_ID, metadata: { campaignId, waMessageId: waResponse.messages[0].id, direction: "outbound", }, }); return { sent: waResponse.messages[0].id }; }, { connection: redis }); ``` ### 3. Webhook: delivery / read receipts + inbound The same webhook receives both delivery state updates and recipient replies. ```python Python theme={null} @app.post("/webhook/whatsapp") async def webhook(request: Request, bg: BackgroundTasks): body = await request.json() # Delivery / read receipts for status in iter_statuses(body): bg.add_task(record_delivery, status) # Inbound messages for inbound in iter_inbound(body): if is_optout(inbound["text"]): bg.add_task(mark_opted_out, inbound["from"]) wa.send_message("You're opted out. Reply START to opt back in.", inbound["from"]) continue bg.add_task(handle_reply, inbound["from"], inbound["text"], inbound["context"]) return {"ok": True} async def record_delivery(status: dict): await sdk.memories.create( document=f"Delivery status: {status['status']}", document_type="campaign-delivery", user_id=status["recipient_id"], customer_id=CUSTOMER_ID, metadata={"wa_message_id": status["id"], "status": status["status"]}, ) ``` ```typescript TypeScript theme={null} export const runtime = "nodejs"; export async function POST(req: Request) { const body = await req.json(); for (const status of iterStatuses(body)) { recordDelivery(status).catch(console.error); } for (const inbound of iterInbound(body)) { if (isOptout(inbound.text)) { markOptedOut(inbound.from); sendWaMessage(inbound.from, "You're opted out. Reply START to opt back in."); continue; } handleReply(inbound.from, inbound.text, inbound.context).catch(console.error); } return Response.json({ ok: true }); } ``` ### 4. The inbound reply agent The agent reads the recent `campaign-send` and `campaign-delivery` memories before responding, so its reply matches the campaign that triggered the conversation. ```python Python theme={null} SYSTEM = """You are a WhatsApp business agent. Your replies are short. - The recipient may be responding to a recent campaign. Search memory for the most recent `campaign-send` document. If it's within the last 7 days, treat their reply as a response to that campaign and act on the campaign's intent. - If they're asking a generic support question unrelated to recent campaigns, respond from general memory. - Always honor opt-out (STOP, UNSUBSCRIBE, REMOVE), but the webhook handles those before you ever see them, so if you receive the message it's not an opt-out. - Keep replies under 2 sentences when possible.""" async def handle_reply(phone: str, text: str, ctx: dict): # Ingest the inbound first await sdk.memories.create( document=f"Customer: {text}", document_type="ai-chat-conversation", user_id=phone, customer_id=CUSTOMER_ID, metadata={ "channel": "whatsapp", "direction": "inbound", "in_reply_to_wa_id": ctx.get("id"), # WA threads replies to specific msg }, ) synap_search = create_search_tool(sdk=sdk, user_id=phone, customer_id=CUSTOMER_ID) synap_store = create_store_tool(sdk=sdk, user_id=phone, customer_id=CUSTOMER_ID) agent = Agent( name="wa_reply_agent", instructions=SYSTEM, tools=[ FunctionTool(synap_search, name_override="synap_search"), FunctionTool(synap_store, name_override="synap_store"), # ...your business tools ], ) result = await Runner.run(agent, input=text) reply = result.final_output wa.send_message(reply, phone) await sdk.memories.create( document=f"Agent: {reply}", document_type="ai-chat-conversation", user_id=phone, customer_id=CUSTOMER_ID, metadata={"channel": "whatsapp", "direction": "outbound"}, ) ``` ```typescript TypeScript theme={null} const SYSTEM = `You are a WhatsApp business agent. Your replies are short. - The recipient may be responding to a recent campaign. Check memory for a recent campaign-send document; if it's within the last 7 days, treat the reply as a response to that campaign. - Otherwise respond from general memory. - Keep replies under 2 sentences when possible.`; async function handleReply(phone: string, text: string, ctx: any) { await synap.sdk.memories.create({ document: `Customer: ${text}`, documentType: "ai-chat-conversation", userId: phone, customerId: CUSTOMER_ID, metadata: { channel: "whatsapp", direction: "inbound", inReplyToWaId: ctx?.id, }, }); const model = synap.wrap(openai("gpt-4o-mini"), { userId: phone, customerId: CUSTOMER_ID, conversationId: phone, }); const { text: reply } = await generateText({ model, system: SYSTEM, prompt: text, // tools: { ...your business tools } }); await sendWaMessage(phone, reply); await synap.sdk.memories.create({ document: `Agent: ${reply}`, documentType: "ai-chat-conversation", userId: phone, customerId: CUSTOMER_ID, metadata: { channel: "whatsapp", direction: "outbound" }, }); } ``` ### 5. Opt-out handling Compliance-critical. Two layers: STOP keyword in the webhook (immediate), and a Redis flag the sender checks before every send. ```python Python theme={null} OPT_OUT_KEYWORDS = {"STOP", "UNSUBSCRIBE", "REMOVE", "QUIT"} def is_optout(text: str) -> bool: return text.strip().upper() in OPT_OUT_KEYWORDS async def mark_opted_out(phone: str): await redis.set(f"optout:{phone}", "1") await sdk.memories.create( document="Recipient opted out of campaigns.", document_type="campaign-optout", user_id=phone, customer_id=CUSTOMER_ID, ) async def is_opted_out(phone: str) -> bool: return bool(await redis.get(f"optout:{phone}")) ``` ```typescript TypeScript theme={null} const OPT_OUT_KEYWORDS = new Set(["STOP", "UNSUBSCRIBE", "REMOVE", "QUIT"]); const isOptout = (text: string) => OPT_OUT_KEYWORDS.has(text.trim().toUpperCase()); async function markOptedOut(phone: string) { await redis.set(`optout:${phone}`, "1"); await synap.sdk.memories.create({ document: "Recipient opted out of campaigns.", documentType: "campaign-optout", userId: phone, customerId: CUSTOMER_ID, }); } const isOptedOut = async (phone: string) => Boolean(await redis.get(`optout:${phone}`)); ``` ## Run & verify ```text Campaign C-may-relaunch fires for phone +1555... theme={null} [scheduler] template send "may_relaunch_v3" → +1555… [webhook] delivered, read Recipient (next morning): Interested. What's the price? Agent (with campaign context loaded from memory): "Glad to hear! The relaunch bundle is $49/mo with the migration done free if you sign this week. Want a link to start?" Recipient: yes please Agent: Here you go: https://… Let me know if you hit anything weird. ``` The agent's first reply references "the relaunch bundle" without being told which campaign because it pulled `campaign-send: may_relaunch_v3` from memory. ## Customize / extend * **Add human handoff** on top of campaigns → combine with [Single-WABA Inbound + Human Handoff](/cookbook/whatsapp-single-handoff). Same memory, same scopes. * **Multi-WABA** for different campaign personas → see [Multi-WABA Shared Memory](/cookbook/whatsapp-multi-waba-shared). * **Campaign performance memory** → store reply rates and best-performing copy in a separate `customer_id`-scoped memory pool to inform next campaigns. * **Replay opens / clicks from your CRM** → use [Patterns → Replay History](/patterns/replay-history) to seed campaign engagement signal at launch. ## Troubleshooting **Agent replies as if no campaign was sent** * The agent's `synap_search` isn't fetching the recent `campaign-send`. Sharpen the system prompt to require a search first, or increase `maxResults` in the wrapper. * Verify `record_delivery` runs *after* `send_campaign` writes the `campaign-send` doc: race conditions can hide the outbound. **Sender keeps sending to opted-out recipients** * The opt-out check belongs **inside** the worker task, not just at scheduling time. Audiences are computed in advance; opt-outs happen continuously. **Recipient gets duplicated outbound from the same campaign** * Schedule with idempotency keys: `(campaign_id, recipient_phone)` → dedupe in Redis with a 30-day TTL. **Replies come in after the 24-hour window** * Outside the window, free-form outbound is blocked by WhatsApp. Auto-fall-back to a "please-restart-conversation" template, or stay silent until the recipient initiates. ## Related * **Integrations:** [OpenAI Agents SDK](/integrations/openai-agents) · [Vercel AI SDK](/integrations/vercel-ai-sdk) * **Concepts:** [Memory Types](/concepts/memories-and-context#memory-types) · [Runtime Ingestion](/concepts/how-ingestion-works#runtime-ingestion) · [Conversational Context Lifecycle](/concepts/context-end-to-end#short-term-context) * **Patterns:** [Replay History](/patterns/replay-history) · [Graceful Degradation](/patterns/graceful-degradation) · [Multi-Tenant SaaS](/patterns/multi-tenant-saas) * **Other recipes:** [WhatsApp + Human Handoff](/cookbook/whatsapp-single-handoff) · [Multi-WABA Shared Memory](/cookbook/whatsapp-multi-waba-shared) # WhatsApp: Single-WABA Inbound + Human Handoff Source: https://docs.maximem.ai/cookbook/whatsapp-single-handoff One WhatsApp Business number, AI takes inbound, drops cleanly to a human agent, picks back up afterward. **Status:** In Development · Playground demo coming soon. The recipe below is complete and runnable today; only the hosted playground showcase is pending. A WhatsApp Business agent that handles inbound messages on one WABA number, knows when to hand off to a human, stays out of the way during the human conversation, and picks back up cleanly when the human releases. Memory carries across the whole arc: AI turns, human turns, and the resumption. ## What you'll build A single-WABA inbound agent that: * **Takes inbound WhatsApp messages** and replies in-thread * **Detects handoff signals**: sentiment, keywords, explicit asks, repeat failure * **Hands off to a human** by surfacing the conversation in your agent console / Slack with full memory context * **Goes quiet** while the human owns the thread * **Resumes** when the human releases: full context, no recap **Est. build time:** 60-75 minutes (WhatsApp Cloud API setup is most of it). ## When to use this recipe Build this if: * You have one WABA number for one product or business line * You have human agents available some of the time but want AI coverage the rest * The handoff in and out has to feel seamless to the customer * You're okay with conversation continuity tied to phone number (which it is on WhatsApp) ## Architecture at a glance ```mermaid theme={null} flowchart TD Customer[WhatsApp customer] -->|inbound webhook| Backend[Your backend] Backend --> Fetch[(Synap context fetch)] Fetch --> Decision{Handoff state?} Decision -->|yes| Console[AI silent
Forward to human console / Slack] Decision -->|no| LLM[LLM agent] LLM -->|reply| WA[WhatsApp Cloud API] WA --> Customer LLM -.->|fire-and-forget| Ingest[(Synap ingest turn)] Console -.->|human replies| WA Console -.->|click release| Decision ``` The "handoff state" is a single key per customer (`AI_HANDOFF[phone]`). When set, the AI agent stops responding. Human turns are still ingested into Synap so the AI has them when it resumes. ## Stack | Layer | Choice | | ----------------- | --------------------------------------------------------------------------------------------------------------------- | | **Synap SDK** | `maximem-synap` (Python) / `@maximem/synap-js-sdk` (TypeScript) | | **WhatsApp** | WhatsApp Cloud API (Meta), via `heyoo` (Python) or direct fetch (TS) | | **Framework** | [OpenAI Agents SDK](/integrations/openai-agents) (Python) / [Vercel AI SDK](/integrations/vercel-ai-sdk) (TypeScript) | | **LLM** | OpenAI `gpt-4o` | | **Handoff state** | Redis in production; in-memory dict for the demo | ## Prerequisites * A Synap API key. See [Authentication](/setup/authentication) * A WABA number, Meta Business app, verified, with a webhook configured to point at your backend * A System User access token with `whatsapp_business_messaging` scope * A human console / Slack channel for handoffs * **Python:** Python 3.11+ * **TypeScript:** Node.js 20+ TypeScript recipe runs on Node only. Pin Next.js route handlers to `export const runtime = "nodejs"`. WhatsApp webhooks expect synchronous 200 responses within 20s: return fast and process async. See [Installation → JavaScript / TypeScript SDK](/setup/installation#javascript-and-typescript-sdk). ### Install ```bash Python theme={null} pip install maximem-synap maximem-synap-openai-agents openai-agents heyoo fastapi uvicorn ``` ```bash uv theme={null} uv add maximem-synap maximem-synap-openai-agents openai-agents heyoo fastapi uvicorn # pip-compatible (existing venv): uv pip install maximem-synap maximem-synap-openai-agents openai-agents heyoo fastapi uvicorn ``` ```bash TypeScript theme={null} npm install @maximem/synap-js-sdk @maximem/synap-vercel-adk ai @ai-sdk/openai ``` ### Configure ```bash Python theme={null} # .env SYNAP_API_KEY=... OPENAI_API_KEY=... WABA_PHONE_NUMBER_ID=... WABA_ACCESS_TOKEN=... WABA_WEBHOOK_VERIFY_TOKEN=... ``` ```bash TypeScript theme={null} # .env.local SYNAP_API_KEY=... OPENAI_API_KEY=... WABA_PHONE_NUMBER_ID=... WABA_ACCESS_TOKEN=... WABA_WEBHOOK_VERIFY_TOKEN=... ``` ## Build it ### 1. Identity & scoping WhatsApp gives you a stable identifier: the customer's phone number. Use it as `user_id`. * `customer_id = ""`: your business, single tenant * `user_id = ` * `conversation_id = `: WhatsApp doesn't have explicit sessions; treat the whole relationship as one rolling conversation `conversation_id`, `user_id`, and `customer_id` must be valid UUIDs. Since these key off the phone, derive a deterministic UUID from it with `uuid.uuid5(...)` (Python) rather than passing the raw phone string. If your privacy posture requires it, hash phone numbers (e.g., `sha256("e164:" + phone)`) before using them as `user_id`. Synap will treat the hash as the stable identifier; you keep raw phones in your own DB. ### 2. Handoff state ```python Python theme={null} # Set when AI hands off; cleared when human releases. Use Redis in production. AI_HANDOFF: set[str] = set() def is_handoff(phone: str) -> bool: return phone in AI_HANDOFF def start_handoff(phone: str) -> None: AI_HANDOFF.add(phone) def end_handoff(phone: str) -> None: AI_HANDOFF.discard(phone) ``` ```typescript TypeScript theme={null} const AI_HANDOFF = new Set(); const isHandoff = (phone: string) => AI_HANDOFF.has(phone); const startHandoff = (phone: string) => AI_HANDOFF.add(phone); const endHandoff = (phone: string) => AI_HANDOFF.delete(phone); ``` ### 3. The handoff tool The agent decides when to hand off. The tool fans out: persist a structured memory record + notify the human console. ```python Python theme={null} @function_tool async def hand_off_to_human(phone: str, reason: str, summary: str) -> dict: """Bring a human into the thread. Reason: 'customer_asked' | 'frustration' | 'out_of_scope' | 'sensitive'.""" start_handoff(phone) await sdk.memories.create( document=f"AI handed off to human. Reason: {reason}. Summary: {summary}", document_type="support-handoff", user_id=phone, customer_id=CUSTOMER_ID, metadata={"handoff_reason": reason}, ) await notify_human_console(phone, reason, summary) # your Slack/console push return {"status": "handed_off"} ``` ```typescript TypeScript theme={null} const handOffToHuman = tool({ description: "Bring a human into the thread.", parameters: z.object({ phone: z.string(), reason: z.enum(["customer_asked", "frustration", "out_of_scope", "sensitive"]), summary: z.string(), }), execute: async ({ phone, reason, summary }) => { startHandoff(phone); await synap.sdk.memories.create({ document: `AI handed off to human. Reason: ${reason}. Summary: ${summary}`, documentType: "support-handoff", userId: phone, customerId: CUSTOMER_ID, metadata: { handoffReason: reason }, }); await notifyHumanConsole(phone, reason, summary); return { status: "handed_off" }; }, }); ``` ### 4. The inbound webhook WhatsApp webhooks deliver inbound messages. Return 200 fast, process async. ```python Python theme={null} from fastapi import FastAPI, Request, BackgroundTasks from heyoo import WhatsApp app = FastAPI() wa = WhatsApp(token=os.environ["WABA_ACCESS_TOKEN"], phone_number_id=os.environ["WABA_PHONE_NUMBER_ID"]) CUSTOMER_ID = "your-business" @app.post("/webhook/whatsapp") async def webhook(request: Request, bg: BackgroundTasks): body = await request.json() msg = wa.get_message(body) phone = wa.get_mobile(body) if msg and phone: bg.add_task(handle_inbound, phone, msg) return {"ok": True} async def handle_inbound(phone: str, text: str): # Always ingest the inbound, even during handoff await sdk.memories.create( document=f"Customer: {text}", document_type="ai-chat-conversation", user_id=phone, customer_id=CUSTOMER_ID, metadata={"channel": "whatsapp", "direction": "inbound"}, ) if is_handoff(phone): return # human owns the thread; AI stays quiet reply = await run_ai(phone, text) wa.send_message(reply, phone) await sdk.memories.create( document=f"Agent: {reply}", document_type="ai-chat-conversation", user_id=phone, customer_id=CUSTOMER_ID, metadata={"channel": "whatsapp", "direction": "outbound", "tier": "ai"}, ) ``` ```typescript TypeScript theme={null} // app/api/webhook/whatsapp/route.ts export const runtime = "nodejs"; export async function POST(req: Request) { const body = await req.json(); const { phone, text } = parseWaInbound(body); if (phone && text) { handleInbound(phone, text).catch(console.error); // fire-and-forget } return Response.json({ ok: true }); } async function handleInbound(phone: string, text: string) { await synap.sdk.memories.create({ document: `Customer: ${text}`, documentType: "ai-chat-conversation", userId: phone, customerId: CUSTOMER_ID, metadata: { channel: "whatsapp", direction: "inbound" }, }); if (isHandoff(phone)) return; const reply = await runAi(phone, text); await sendWaMessage(phone, reply); await synap.sdk.memories.create({ document: `Agent: ${reply}`, documentType: "ai-chat-conversation", userId: phone, customerId: CUSTOMER_ID, metadata: { channel: "whatsapp", direction: "outbound", tier: "ai" }, }); } ``` ### 5. The AI agent ```python Python theme={null} SYSTEM = """You are a WhatsApp business agent. - Be concise. WhatsApp users want short replies. - Recall the customer's history and prior preferences from memory. - Hand off to a human when: the customer explicitly asks, they're frustrated, the issue is sensitive (financial, medical, legal), or you've tried twice without resolution. Use hand_off_to_human. - Don't promise anything you can't deliver via tools.""" async def run_ai(phone: str, text: str) -> str: synap_search = create_search_tool(sdk=sdk, user_id=phone, customer_id=CUSTOMER_ID) synap_store = create_store_tool(sdk=sdk, user_id=phone, customer_id=CUSTOMER_ID) agent = Agent( name="wa_agent", instructions=SYSTEM, tools=[ FunctionTool(synap_search, name_override="synap_search"), FunctionTool(synap_store, name_override="synap_store"), hand_off_to_human, # ...your business tools ], ) result = await Runner.run(agent, input=text) return result.final_output ``` ```typescript TypeScript theme={null} const SYSTEM = `You are a WhatsApp business agent. - Be concise. WhatsApp users want short replies. - Recall the customer's history and prior preferences from memory. - Hand off when: the customer asks, they're frustrated, the issue is sensitive, or you've tried twice. - Don't promise anything you can't deliver via tools.`; async function runAi(phone: string, text: string): Promise { const model = synap.wrap(openai("gpt-4o"), { userId: phone, customerId: CUSTOMER_ID, conversationId: phone, // one rolling conversation per phone }); const { text: reply } = await generateText({ model, system: SYSTEM, prompt: text, tools: { hand_off_to_human: handOffToHuman /* + your business tools */ }, }); return reply; } ``` ### 6. The human side Two affordances in your console / Slack: 1. **Reply to the customer** → posts a message via WABA, ingests it into memory tagged `tier: "human"`. 2. **Release the thread** → calls `end_handoff(phone)`. Next customer message routes to the AI again, with full handoff history loaded from memory. ```python Python theme={null} async def human_reply(phone: str, text: str): wa.send_message(text, phone) await sdk.memories.create( document=f"Human agent: {text}", document_type="ai-chat-conversation", user_id=phone, customer_id=CUSTOMER_ID, metadata={"channel": "whatsapp", "direction": "outbound", "tier": "human"}, ) async def human_release(phone: str): end_handoff(phone) await sdk.memories.create( document="Human released thread back to AI.", document_type="support-handoff", user_id=phone, customer_id=CUSTOMER_ID, metadata={"handoff_event": "released"}, ) ``` ```typescript TypeScript theme={null} export async function humanReply(phone: string, text: string) { await sendWaMessage(phone, text); await synap.sdk.memories.create({ document: `Human agent: ${text}`, documentType: "ai-chat-conversation", userId: phone, customerId: CUSTOMER_ID, metadata: { channel: "whatsapp", direction: "outbound", tier: "human" }, }); } export async function humanRelease(phone: string) { endHandoff(phone); await synap.sdk.memories.create({ document: "Human released thread back to AI.", documentType: "support-handoff", userId: phone, customerId: CUSTOMER_ID, metadata: { handoffEvent: "released" }, }); } ``` ## Run & verify ```text theme={null} Customer: Hi, my order #ORD-22 hasn't arrived AI: Hey! ORD-22 is showing delivered 5/14 to 12 Oak St. If that's not your address, tell me your correct delivery address and I'll trace it. Customer: I never got it. This is the second time. I want to talk to a person. AI: Of course, connecting you with a teammate now. [hand_off_to_human(reason=customer_asked, summary="2nd missing-order issue, ORD-22 marked delivered. Customer wants human.")] [AI goes silent. Human picks up in console:] Customer: ... Human (in console): Hi Anish, I see ORD-22 was marked delivered to 12 Oak. Apologies you didn't get it. I'm reissuing #ORD-22-R immediately, free shipping, you'll have it in 2 days. Also flagging your account so this can't happen a third time. Customer: Thank you. [Human clicks "Release" in console. AI_HANDOFF cleared.] ``` ```text Next day, new message theme={null} Customer: Hey, did the reissue ship? AI: Yes, ORD-22-R shipped 5/15, tracking #1Z999. Carrier ETA 5/17. Want me to send tracking updates here as they come in? ``` The next-day reply comes from the AI again, with the human's resolution and the reissue tracked in memory. ## Customize / extend * **Multiple WABA numbers** → see [Multi-WABA Shared Memory](/cookbook/whatsapp-multi-waba-shared) for the routing pattern. * **Outbound campaigns + inbound** → see [Single-WABA Campaign + Inbound](/cookbook/whatsapp-single-campaign). * **Slack as the human console** → post handoff alerts into a `#wa-support` channel; replies posted in-thread relay back via WABA. Same pattern as [Patterns → Slack Bot](/patterns/slack-bot). * **Auto-release on inactivity** → after N minutes of no human reply, clear `AI_HANDOFF` automatically (Redis TTL). * **Tier-escalation flavor** → for AI→AI handoff instead of AI→human, see [Tier-1 → Tier-2 Escalation](/cookbook/support-tier-escalation). ## Troubleshooting **AI replies during a human handoff** * Race condition: webhook fired before `AI_HANDOFF` was set. Check `is_handoff` *inside* the async task, not in the webhook handler. **AI loses context after the human releases** * Confirm human turns are being ingested into memory with `tier: "human"` metadata. If they're missing, the AI sees a gap and may re-ask basic questions. **Webhook timeouts on Vercel** * WhatsApp expects a 200 within 20s. Return immediately from the route handler and run `handleInbound` in the background. Don't `await` it. **Customer gets dupe messages** * WhatsApp redelivers webhooks that don't 200 fast enough. Idempotency: track `message_id` in Redis with a short TTL and skip duplicates. **Template messages required for outbound > 24h** * WhatsApp's 24-hour rule: outside the customer-initiated session window, you can only send approved template messages. Track session start in Redis; refuse to send free-form replies outside the window. ## Related * **Integrations:** [OpenAI Agents SDK](/integrations/openai-agents) · [Vercel AI SDK](/integrations/vercel-ai-sdk) * **Concepts:** [Memory Scopes](/concepts/memory-scopes) · [Customer Context](/concepts/context-end-to-end#customer-context) · [Agent Interactions](/concepts/agent-topologies#agent-interactions) * **Patterns:** [Slack Bot](/patterns/slack-bot) · [Graceful Degradation](/patterns/graceful-degradation) · [Multi-Tenant SaaS](/patterns/multi-tenant-saas) * **Other recipes:** [WhatsApp Campaign + Inbound](/cookbook/whatsapp-single-campaign) · [Multi-WABA Shared Memory](/cookbook/whatsapp-multi-waba-shared) · [Tier Escalation](/cookbook/support-tier-escalation) # Dashboard Source: https://docs.maximem.ai/dashboard/overview The Synap Dashboard is the web management interface for your deployment. Create and manage instances, generate API keys, and inspect the memory architecture Synap generates for each agent. The Dashboard at [synap.maximem.ai](https://synap.maximem.ai) is the **control plane** for Synap: it's where you create instances, configure how they remember, and provision the API keys your application uses. The **data plane** (ingesting memories and retrieving context) happens through the [Synap SDK](/sdk/initialization), not here. Most of the Dashboard is self-explanatory once you're in it. This page orients you to the main areas; follow the in-app prompts for the rest. Synap Dashboard home page showing instance overview, recent activity, and key metrics Access is governed by three roles. **Owners** and **Admins** can create and edit instances, upload use-case files, and generate API keys. **Members** have read-only access. Owners additionally manage the team. ## Managing instances An **instance** is the fundamental deployment unit: an isolated memory agent with its own storage namespaces, memory configuration, scope hierarchy, and API keys. Each one is identified by an ID like `inst_a1b2c3d4e5f67890`. From the **Instances** page, Owners and Admins can **Create Instance**. Creation asks for a name, a user relationship (`b2c`, `b2b`, `internal`, or `agent_to_agent`), an optional agent type, and (recommended) a **Use-Case Markdown** file describing what the agent does. That file is what Synap uses to generate the instance's memory architecture; download the in-form template to author it. For B2C agents, the customer and user collapse to the same entity. A new instance starts in **initializing** while Synap allocates storage and applies the initial configuration, then moves to **active** once the first SDK connection lands. Other lifecycle states (`inactive`, `suspended`, `deleting`) are shown on the instance, with the available actions for each. Everything for a single instance lives on its **detail page**: status and metrics, the memory configuration, analytics, and the **Instance Settings** where you rename it, edit metadata, re-upload the use-case file, and manage API keys. API keys (prefixed `synap_`) are shown only once. Copy the key when you generate it and store it as your `SYNAP_API_KEY`. To rotate, generate a new key first, switch your environment over, then revoke the old one. Multiple keys can be active at once, so there's no downtime. ## Memory configuration Every instance has a **Memory Architecture Configuration (MACA)** that Synap derives from its Use-Case Markdown. It shapes which memory categories are extracted, how retrieval ranks and combines results across the fast vector + graph layers, how memories are scoped (user, customer, client, or world), and how long they're retained. The Dashboard surfaces MACA for **inspection only**. Open the **Memory Configuration** tab on the instance detail page to confirm what Synap configured and to see the history of past regenerations. It is not an authoring surface. To change behaviour, edit your Use-Case Markdown and re-upload it from **Instance Settings → Use-Case**. Synap regenerates the MACA from the new file; the previous versions are preserved (rollback is a Synap-side operation today, available via support). Regeneration affects only new requests. In-flight conversations finish under the settings they started with. For how MACA is derived and what each section governs, see [Customized Memory Architectures](/concepts/memory-architecture) and the [Use-Case Markdown](/concepts/memory-architecture#the-use-case-file) authoring guide. ## Scope ladder Your **scope ladder** is the set of named levels your customers and users nest into, and it is what decides who can read whose memories. The default is the familiar `Client → Customer → User` chain, and most accounts never need anything else. The **Scope Ladder** page, under **Instances** in the sidebar, shows the ladder your account is actually using: each level, its permanent key, and how many entries sit at it. From there you can rename a level, review the ladder Synap suggests from your use-case file along with the reason for each rung, accept it, add a level with a preview of how many records the change would move, and check what a specific request would resolve to before your application sends it. The page keeps two states apart on purpose: whether anyone has accepted the ladder, and whether nested scoping is switched on for your instances. A ladder can exist while nothing uses it. See [Scope Ladder](/dashboard/scope-ladder) for the walkthrough, and [Scope ladder](/concepts/scope-ladder) for the concept behind it. ## Sensitive data The **Sensitive data** page shows what Synap has detected in your traffic, by field type, and lets you decide what happens to each kind: keep it, store a placeholder instead of it, or do not store it at all. Nothing you set there changes what your application reads back unless you pick one of the two settings that say so, and nothing takes effect until a person approves it. The same page carries a test box for checking a change against sample text before committing to it, the audit trail of everything that has happened to your sensitive data, and the place to describe field types Synap does not ship. See [Sensitive data and data controls](/dashboard/pii-and-data-controls) for the step-by-step, and [Sensitive Data Protection](/guides/pii-protection) for the concepts behind it. ## Next steps How Synap derives an instance's memory configuration from your use-case file. Author the file Synap uses to configure and regenerate memory behaviour. Decide what happens to sensitive values in your content, and test it before approving. See how your customers and users nest, rename a level, or add one. Connect your application to a dashboard-provisioned instance using the SDK. Review everything you need before going live. # Sensitive Data & Data Controls Source: https://docs.maximem.ai/dashboard/pii-and-data-controls Configure what Synap does with sensitive values in your content, one category at a time, from the Sensitive data page in the Dashboard. Covers the findings list, the test box, approving a policy, and restricting an API key. Sometimes called PII controls. **Before you start: configuring this does not change what your application reads back.** For every setting except two, your own API keys receive the real value, so your app sees exactly what it sees today. The two exceptions are **Do not store it** and **Protect from everyone**, and the page tells you which is which as you pick. Nothing you do on this page takes effect until you approve it. Saving is not turning it on. For the concepts behind this page, the eleven categories, and what each setting means at each destination, read [Sensitive Data Protection](/guides/pii-protection) first. ## Finding the page Open the Dashboard and go to **Sensitive data**. The page opens on **What we found**, which lists what Synap has actually detected in your traffic over the last 30 days. It is deliberately not an empty configuration form: the point is to decide about data you really have. At the top of the page, **Applies to** controls the scope of everything below it. The default is **All instances (whole account)**. Pick a single instance to give that one instance a policy that overrides the account setting. Most teams set one policy for the whole account and never touch this. *** ## Setting a policy If you have not configured anything yet, the page offers three starting points under **Clients like you usually begin here**: **Minimal**, **Standard**, and **Regulated**. One click sets every category. **Standard** is the right answer for most products. A preset lands as a draft. It is not live until you approve it, and you can change any row afterwards. Each row is a field type Synap found, with how many times it appeared and across how many of your users. Each row offers three choices: | Choice | What it means | | ------------------- | -------------------------------------------------------------------------------- | | **Keep it** | We store it and use it normally. | | **Protect it** | We store a placeholder instead of the value. Your app still gets the real thing. | | **Do not store it** | We keep the fact it happened, not the value. Nobody can get it back. | The **advanced** link on a row opens the other three settings: hide from the model only, protect from everyone including your own app, and keep it in your own vault. Most teams never open it. Rows on the floor list, such as card numbers and passwords, show a **never stored** badge and offer no choice. See [the floor](/guides/pii-protection#the-floor). Picking a different choice renders a real before-and-after for that row, using a sample memory sentence, plus one line saying whether your application is affected. That line is the one to read. Two settings affect your app; the rest do not. Nothing takes effect one row at a time. Changes collect in a bar at the bottom of the page showing how many are not applied yet. **Discard** throws them away. **Review and apply** saves them and approves them together. Approving records who approved it and when, and bumps the version. The previous version stays readable under the policy history, so you can always see what was in force when a given memory was created. Below the findings list there is a line reading **"We also looked for N other kinds of sensitive data and did not find any."** Expand it to see the full list of what was searched for. It is there so you can tell "we checked and found nothing" apart from "nobody ever looked". ### Suggestions Above the findings list, Synap surfaces field types appearing in your traffic that you have not decided about yet. At most three are shown as prominent; the rest sit under **show more**, and the total is stated so you can see the list is complete. Nothing is hidden by a score; the score only decides the order. Each suggestion offers **protect**, **ignore permanently**, or **not now**. Choosing **protect** records the decision, it does not switch protection on by itself. You still set the category and approve the policy. *** ## The test box The **Try it** tab is the fastest way to answer "what would this actually do to my data". Paste sample text, click **See what happens**, and the page lists everything Synap found: the field type, where in the text it sits, and what would happen to it at each destination under your current settings. Findings on the floor are badged **never stored**, and your own field types are badged as yours. Two things about this box are worth knowing: * **Nothing you paste is stored.** Not the text, not the values, not a counter, not a log line, not an audit row. * **The result carries positions and field type names, never the matched text itself.** You can safely screenshot or paste a result into a ticket. The box accepts up to 20,000 characters at a time. If you have not approved a policy yet, the result is computed against your draft and says so, so you can check a change before committing to it. *** ## Seeing what is protected The **Protected values** tab answers "is anything actually protected right now, and how much". It shows, per field type, how many distinct values are held, how many times they have appeared, what your application receives for that field type, and what the model sees. It shows no values and no placeholders, deliberately. A page that exists to demonstrate values are not readable would disprove itself by displaying them. The same tab shows what the floor removed and what was suppressed by the memory drop rule, alongside a sample of the memories that were kept. Read the kept sample: a card that shows only what was thrown away makes a working feature look like data loss. *** ## Activity and evidence The **Activity** tab is the audit trail for everything that happened to your sensitive data. Your own people and Synap staff appear in the same list, with a column saying which is which, so you find out about our access on your own dashboard rather than from us. Each entry carries the time, the action, the field type, who did it, the reason they gave, and the policy version in force. It never carries the value. Entries are kept for one year. **Export** downloads the trail as a CSV file an auditor can open. The policy history, on the same page, shows every version, who approved it, and when. A reason typed into a reveal is screened before it is stored, and a reveal whose reason contains what looks like a sensitive value is refused along with the reason. This is deliberate: refusing only the reason would teach people to retype it without the number and get the value anyway. *** ## Your own field types The **Your own field types** tab lets you describe a field type Synap does not ship: an asset tag, a policy number, an order id. Give it a name, pick the category it belongs in, and paste two or three real examples. No release and no ticket. See [Your own field types](/guides/custom-field-types) for how to pick the category and write good examples. *** ## Restricting an API key Your policy decides what any caller can receive. On top of that, each of your API keys carries a grant that can only ever **narrow** what the policy already allows: | Grant | What that key receives | | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **full** | Everything your policy permits. This is what every existing key has. | | **masked** | Placeholders, for every protected field type. | | **none** | Placeholders. Kept separate from **masked** so the audit trail can tell "this key sees placeholders" from "this key was never meant to see anything". | Use this to give a support tool or an internal dashboard a key that reads memories without reading values. A grant cannot widen access, so there is no way to accidentally hand a key more than your policy allows: to widen, change the policy. A grant change takes up to about a minute to take effect on live traffic, because the credential is cached. *** ## What does not appear on this page * **Erasing a person** is not a self-service action. See [Erasing a person](/guides/erasure). * **Data ingested before you approved a policy** is untouched and stays in plain text. Policy applies forward only, and every record carries the policy version that was in force so you can tell which is which. ## Next steps The categories, the settings, the floor, and what detection can and cannot do. What your application receives once a field type is protected. Describe a format Synap does not ship. Deleting someone completely, and what becomes unreadable. # Scope Ladder Source: https://docs.maximem.ai/dashboard/scope-ladder The Scope Ladder page in the Dashboard shows how your customers and users nest, which is what decides who can read whose memories. See your levels, rename them, review what Synap suggests and why, accept a ladder, add a level with a preview of what moves, and check what a specific request would resolve to. This page is about the Dashboard screen. For what a level is, which direction a read travels, and what you can reach today, read [Scope ladder](/concepts/scope-ladder) first. Open the Dashboard and go to **Instances**, then **Scope Ladder**. It sits next to **Visibility** because both answer the same question: Visibility controls what one instance can read from another, and Scope Ladder controls who can read what inside one. ## One ladder for the whole account Your ladder belongs to your account, not to an instance. Every instance you run shares the same levels, and there is no way to give two instances different ones. That is deliberate, and it follows from how people and organisations are identified. A customer id means the same customer everywhere in your account, and a user id means the same person, whichever instance sent the request. Two instances writing about customer `acme` are writing about one customer, not two. If those instances disagreed about what the levels are, the same records would be filed under different level names depending on which instance wrote them, and a read from one instance would not find what the other stored. Your memory would split in half with nothing to tell you it had. Your instances still keep their own memory settings. What they cannot keep is their own ladder. The one part of this that IS per instance is the switch. Nested scoping can be on for one instance and off for another, which is what makes a careful rollout possible: turn it on for a test instance, watch it, then turn it on for the rest. The shape is shared; when each instance starts using it is not. ## Reading your ladder The main panel lists your levels in order, top rung first, indented so the nesting reads as nesting. Each row shows three things: * the **name**, which is what you call that level * the **key** in small type beside it, which is what stored memories resolve through * how many **entries** are filed at that level right now The name is an editable field. The key is shown and never editable, because every memory already filed under it would stop resolving. A row can carry a badge, and it tells you what that level needs rather than that it is unreachable. A level outside the standard three is not named by `customer_id` and `user_id`, so a call has to name it in a scope path instead. * **needs a scope path** means your traffic is already on an SDK that can send one. Name the level in the path and you can read and write at it. * **needs SDK 0.4.6+** means we have not seen a capable SDK in your recent requests. Upgrade, then the level becomes reachable. See [What you can reach today](/concepts/scope-ladder#what-you-can-reach-today). ### Two banners worth reading Above the list, the page states two things that are easy to assume and easy to get wrong. **Whether the ladder has been accepted.** An unaccepted ladder is a draft. Nobody has agreed to it, so your account is still filing and reading memories the default way and nothing on the page is affecting your data. Once someone accepts, the banner names who accepted it and when. **Whether nested scoping is switched on.** A ladder can exist while nothing uses it, which looks configured and behaves exactly as it did before. The banner says which of the two you are in: off, on for all your instances, or on for some number of them with the rest still reading the previous way. These are separate states. Accepting a ladder does not switch nested scoping on, and this page has no control that switches it on. It reports the state. Ask us to change it. ## Renaming a level Type a new name in any row and a bar appears with **Save names**. Only the rows you actually edited are sent, so an untouched level keeps its name and the audit line does not claim you renamed something you left alone. Renaming is the one action here with no consequences. It changes what a level is called. It does not move a memory, and it does not change who can read what. ## What we suggest, and why Lower down, a panel shows the ladder Synap would suggest for your account, read out of your use-case document. Every rung carries the reason it was proposed and quotes the line in your document it came from, so you can judge the reasoning instead of taking it on trust. Where the suggestion is unsure, it says so. Anything Synap is not confident about is listed under **things we were not sure about** as a question, rather than added to your ladder. A level decides who can read whose memories, so it is never inferred from prose that did not ask for one. If your use-case document did not say how your users are grouped, the panel says that plainly and shows the standard three rungs instead of a shape invented for you. Every account is given a ladder at signup, so most of the time this panel is advisory only and the thing you can act on is a rename: it offers to call your existing levels what it suggests calling them. Accepting a whole ladder only appears for an account that has none. ## Accepting a ladder If your account has no ladder, the same panel becomes actionable. You can retype any name you disagree with first, then use **Use this ladder**. Accepting creates the levels and records who accepted and when. It does not move any memory you have already stored, and it does not switch nested scoping on by itself. Renaming stays free afterwards. Removing does not: a level can never be deleted once it exists, only retired. ## Adding a level **Add a level** opens the only control on the page that moves stored records, so it is the only one that will not act on a single click. You give the level a name, such as Team or Branch or Region, and one line describing what it holds. The permanent key is derived from the name and shown to you, so you are not asked to invent a stable identifier for a thing you are still naming. Pick which existing level the new one sits above. A new level has to land between two existing ones, so on the standard ladder that means above Customer or above User. Adding one re-files everything currently beneath that point. The list also offers a position at the very bottom, under your last level. Do not use it. Nothing is allowed to sit below the level that identifies a person, so that choice is refused when you commit it. Before anything happens, the page tells you how many entries and how many memories inserting the level there would re-file, and what your levels would read as afterwards. Nothing has changed at this point. The commit carries back the same number you were shown. If your structure changed while you were reading the preview, the change is refused rather than applied against a count nobody saw. Existing memories do not immediately carry the new level as one of their ancestors, so a query asking for everything beneath it is incomplete until Synap finishes stamping them. Ordinary retrieval is not affected. Ask us to run the backfill after you add a level. ## Checking a request **Check a request** lets you stand somewhere on the ladder and see what you can see. Enter the customer id and user id your application would send, and the page shows the chain of levels that request reads from, in order. It previews only. Nothing is created and nothing is changed by typing here, so checking an id that does not exist yet will not quietly register it. If the ids do not resolve, the panel says so and explains why rather than returning an empty chain. If nested scoping is off for that instance, it tells you the chain is the shape you would get once it is switched on. ## Coverage A panel near the bottom compares how many of your groups are on the ladder against how many exist. When they match, it says so in one line. When they do not, it reports the number of memories sitting in groups Synap has no record of creating. Those cannot be placed automatically, and re-running a backfill will not change it. The number is stated as a count of memories rather than a percentage, because a small percentage of a large account is still a lot of memories. ## What this page will not do Being clear about this is more useful than making the page sound finished. | | On this page | | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | See your levels, their keys and their counts | Yes | | Rename a level | Yes | | Review the suggested ladder and its reasoning | Yes | | Accept a suggested ladder | Only if your account has none | | Add a level, with a preview of what moves | Yes, if your integration can address one | | Check what a request resolves to | Yes | | Delete a level | Never; a level can only be retired | | Switch nested scoping on or off | No, it reports the state only | | See which SDK versions your requests come from | Yes, and whether the oldest can address a new level | | Send a scope path from an SDK | Reads from Python 0.4.6 or JS 0.4.4, writes on `memories.create` from Python 0.4.8 or JS 0.4.7, see [What you can reach today](/concepts/scope-ladder#what-you-can-reach-today) | | Run the backfill after adding a level | No, ask us | ## Next steps What a level is, which direction a read travels, and where your ladder came from. What `customer_id` and `user_id` do on every request. # What is Maximem Synap? Source: https://docs.maximem.ai/getting-started/overview Synap is a managed memory layer for AI agents. It sits between your application and your LLM, providing persistent structured memory that survives across sessions, conversations, and deployments. Instead of treating every conversation as a blank slate, your agents can remember, learn, and personalize. ## The mental model: Client, Instance, Customer, User, Conversation Five identifiers do all the work in Synap. Get these straight before reading anything else. Synap mental model: a Client contains multiple Instances (e.g. production, staging), each Instance contains Customers (your B2B tenants), and each Customer contains Users identified by user_id plus conversation_id | Identifier | Lives in | What it means | | ------------------------------------------------------------- | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [**Client**](/concepts/memory-scopes#clients-and-instances) | Dashboard | Your company's account with Synap. One per organization. | | [**Instance**](/concepts/memory-scopes#clients-and-instances) | Dashboard, created under a Client | A logical environment (production, staging, EU, US…). Each Instance has its own API keys, its own [MACA](/concepts/memory-architecture), and its own isolated storage. | | **Customer** (`customer_id`) | Passed on every SDK call **on B2B instances** | Your B2B tenant. Memories tagged with this are visible to all users within that customer. On B2C instances it is auto-resolved from `user_id`, so you omit it. | | **User** (`user_id`) | Passed on every SDK call | Your end-user. Memories tagged with this stay private to that user. | | **Conversation** (`conversation_id`) | Passed on conversation-scoped SDK calls | A single chat thread. Must be a valid UUID; the server rejects non-UUID strings. Lets Synap compact long sessions and route memory to the right thread. | See [Clients & Instances](/concepts/memory-scopes#clients-and-instances) and [Customers & Users](/concepts/memory-scopes#customers-and-users) for the full breakdown. ## Key capabilities Automatically extract structured knowledge from raw conversations and documents. Synap identifies and categorizes **facts**, **preferences**, **episodes**, **emotions**, and **temporal events** without any manual annotation. Isolate memories at the right boundary. Synap supports a hierarchical [scope chain](/concepts/memory-scopes) (**User**, **Customer**, **Client**, and **World**) so personal preferences stay personal while shared knowledge is accessible to everyone who needs it. Memories are stored in both **vector** and **graph** storage engines. Vector storage powers semantic similarity search. Graph storage captures relationships between entities. You configure the balance through the Memory Architecture Config. Long conversations don't need to be sent in full every time. Synap compacts conversation context into structured summaries, reducing token usage while preserving the information your agent actually needs. References to the same person, place, or thing across different conversations are automatically resolved. "John", "John Smith", and "my manager" all map to a single canonical entity, building a coherent knowledge graph over time. For latency-sensitive applications, the SDK supports low-latency streaming. Stream memories in and context out without waiting for full round-trips. ## Architecture at a glance Synap follows a clean separation of concerns across three layers: SDK in your application talks to Synap Cloud, where an auth gateway feeds the ingestion pipeline (categorize, extract, chunk, organize, resolve entities) into vector and graph storage plus analytics, all managed from the Dashboard. **SDK** lives in your application. It handles authentication, ingestion requests, retrieval queries, and optional low-latency streaming. The SDK never self-asserts identity; all authentication flows through Synap Cloud using a zero-trust model. **Synap Cloud** is the managed backend. It runs the multi-stage ingestion pipeline (categorize, extract, chunk, organize, resolve entities), stores memories across vector and graph engines, and serves retrieval queries. You never deploy or manage this infrastructure. **Dashboard** is the web management interface. Create and manage instances, configure memory architecture, monitor ingestion pipelines, manage team access, and set up webhooks. ## Key components | Component | What it is | What it does | | ------------------------------------- | ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------- | | **SDK** | Python library installed in your application | Provides the programmatic interface for ingesting memories, retrieving context, and managing the memory lifecycle | | **Dashboard** | Web UI at [synap.maximem.ai](https://synap.maximem.ai) | Manage instances, configure memory architecture, monitor pipelines, and collaborate with your team | | **Memory Architecture Config (MACA)** | Per-instance memory configuration | Defines how memory is stored, what gets extracted, retrieval strategies, and retention policies for each instance | | **Instances** | Deployed agent memory containers | Each instance is an isolated memory environment with its own configuration and storage. Identified by `inst_` | | **Clients** | Organization-level containers | A client represents your organization or application. Contains one or more instances. Identified by `cli_` | | **Scopes** | Memory isolation boundaries | Hierarchical boundaries (User > Customer > Client > World) that control who can access which memories | ## The memory lifecycle Every piece of content that enters Synap follows a structured pipeline: Raw content (conversations, documents, notes) is submitted through the SDK. Each ingestion is tagged with a scope (user, customer, etc.) and document type. The pipeline classifies the content type and determines which extraction strategies to apply based on the Memory Architecture Config. Structured knowledge is extracted: facts ("User lives in San Francisco"), preferences ("User prefers dark mode"), episodes ("User signed up last Tuesday"), emotions ("User was frustrated with billing"), and temporal events. Extracted entities are resolved against the existing knowledge graph. New entities are auto-registered at the appropriate scope. The resolution follows the scope chain: User > Customer > Client > World. Resolved memories are stored in both vector storage (for semantic search) and graph storage (for relationship traversal). Storage configuration is driven by MACA. When your agent needs context, Synap retrieves and ranks relevant memories from both storage engines, respecting scope boundaries and applying the configured retrieval strategy. The entire pipeline runs asynchronously. When you call `sdk.memories.create()`, the SDK returns immediately with an ingestion ID. You can track ingestion status through the SDK or Dashboard. ## Next steps Get up and running with Synap in under 10 minutes. Detailed installation instructions, environment configuration, and dependency options. Understand Synap's zero-trust authentication model, API keys, and credential management. Deep dive into memory scoping and isolation boundaries. Moving from Mem0, Zep, Letta, or Supermemory? Map your existing memory onto Synap and backfill cleanly. # Playground Source: https://docs.maximem.ai/getting-started/playground Try Synap live in your browser: spin up a working memory agent with no install and no API key. The fastest way to see ingestion and retrieval in action before you write any code. The [**Synap Playground**](https://synap.maximem.ai/playground) is a hosted sandbox for trying Synap without setting anything up. Open it in your browser, talk to a memory-enabled agent, and watch what it ingests and retrieves in real time. No install, no API key: start a working memory agent in the browser. ## What you can do * **Chat with a memory-enabled agent** and see it remember facts, preferences, and past turns across the conversation. * **Watch ingestion and retrieval happen**: the Playground surfaces what Synap extracts from each turn and what it pulls back as context. * **Explore preconfigured agents** with different [memory architectures](/concepts/memory-architecture), so you can see how the use-case shapes what gets remembered. ## What it's for (and what it isn't) The Playground is a **demonstration environment**, not a place to build your app: * It runs against shared, preconfigured instances; you don't manage scopes, keys, or configuration there. * Conversations are for exploration; don't treat the Playground as durable storage for anything you care about. * It's the wrong place to test multi-tenant scoping, production limits, or your own [use-case file](/concepts/memory-architecture#the-use-case-file). When you're ready to build for real, move to the SDK: Install the SDK, create your own instance, and run the three-call loop in \~10 minutes. Using a framework? Jump straight to your integration. # Quickstart Source: https://docs.maximem.ai/getting-started/quickstart Install the SDK, create your first instance, ingest a memory, retrieve it. ~10 minutes. **Where to start, depending on what you're trying to do** * **"I just want to try Synap without installing anything"**: open the [live playground](https://synap.maximem.ai/playground) and exercise the SDK from your browser. * **"I want to see Synap working in 10 minutes"**: you're on the right page. Stay here. * **"I want to wire Synap into a real FastAPI / Flask / Next.js / Django app"**: head to [Setup & Integration](/setup/detailed-integration) after you finish this page. * **"I want a complete end-to-end tutorial with an LLM, conversation routing, and graceful degradation"**: go to [First Integration](/setup/first-integration). That tutorial assumes you've finished this Quickstart. * **"I use a framework (LangChain, LangGraph, Vercel AI SDK, CrewAI, LiveKit, Claude Agent SDK, Pipecat…)"**: you won't call most of these APIs directly. Skim this page for the mental model, then jump to [your integration](/integrations/overview). * **"I'm moving from another memory layer (Mem0, Zep, Letta, Supermemory)"**: skim this page for the mental model, then follow [Migrate to Synap](/migrations/overview) to map and backfill your existing data. **Using an AI coding agent?** Install the [Synap coding-agent skill](/integrations/ai-coding-agents) and just ask it to add Synap to your app: it knows the SDK and every framework integration. Works with Claude Code, Cursor, Codex, and more. ## The five identifiers, at a glance Everything in Synap is scoped by these. You'll see them throughout this page: | Identifier | What it is | | ------------------------------------ | --------------------------------------------------------------------------------------------------------- | | **Instance** | An agent you integrate with Maximem Synap (resolved from your API key, not passed per call). | | **User** (`user_id`) | The end user of your agent (B2B or B2C). Passed on most calls. | | **Customer** (`customer_id`) | (B2B only) one of your customer organizations. Not accepted on B2C: sending it is rejected with HTTP 400. | | **Conversation** (`conversation_id`) | One chat thread. Must be a valid UUID. | | **Client** | Your own organization account, which contains your instances. | For the full model, see [Identifiers & Scopes](/concepts/memory-scopes). ## TL;DR: Hello World If you already have an API key, this is everything you need to ingest one memory and read it back. This default is for a **B2C** app: one user (you), identified by `user_id` alone: ```python hello_synap.py theme={null} # pip install maximem-synap # export SYNAP_API_KEY=synap_... SYNAP_INSTANCE_ID=inst_... import asyncio from maximem_synap import MaximemSynapSDK async def main(): sdk = MaximemSynapSDK() await sdk.initialize() try: result = await sdk.memories.create( document="User: I prefer dark mode.\nAssistant: Noted!", document_type="ai-chat-conversation", user_id="user_alice", ) # Block until ingestion finishes (the honest pattern for scripts/tests). await sdk.memories.wait_for_completion(result.ingestion_id) # Retrieve via the scope you ingested at. # B2C: send user_id only. customer_id is not accepted on this instance. context = await sdk.user.context.fetch( user_id="user_alice", search_query=["preferences"], ) for p in context.preferences: print(p.content) finally: await sdk.shutdown() asyncio.run(main()) ``` ```javascript hello-synap.mjs theme={null} // npm install @maximem/synap-js-sdk // export SYNAP_API_KEY=synap_... SYNAP_INSTANCE_ID=inst_... import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient(); await sdk.initialize(); try { const result = await sdk.memories.create({ document: 'User: I prefer dark mode.\nAssistant: Noted!', document_type: 'ai-chat-conversation', user_id: 'user_alice', }); // Block until ingestion finishes (the honest pattern for scripts/tests). await sdk.memories.wait_for_completion(result.ingestion_id); // Retrieve via the scope you ingested at. // B2C: send user_id only. customer_id is not accepted on this instance. const context = await sdk.user.context.fetch({ user_id: 'user_alice', search_query: ['preferences'], }); for (const p of context.preferences ?? []) { console.log(p.content); } } finally { await sdk.shutdown(); } ``` ```typescript hello-synap.ts theme={null} // npm install @maximem/synap-js-sdk // export SYNAP_API_KEY=synap_... SYNAP_INSTANCE_ID=inst_... import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient(); await sdk.initialize(); try { const result = await sdk.memories.create({ document: 'User: I prefer dark mode.\nAssistant: Noted!', document_type: 'ai-chat-conversation', user_id: 'user_alice', }); // Block until ingestion finishes (the honest pattern for scripts/tests). await sdk.memories.wait_for_completion(result.ingestion_id); // Retrieve via the scope you ingested at. // B2C: send user_id only. customer_id is not accepted on this instance. const context = await sdk.user.context.fetch({ user_id: 'user_alice', search_query: ['preferences'], }); for (const p of context.preferences ?? []) { console.log(p.content); } } finally { await sdk.shutdown(); } ``` **Two things to know if you are reading the JavaScript tab.** Method names keep Python's spelling, so it is `wait_for_completion`, not `waitForCompletion`, which is `undefined` and fails at the call site. Arguments are the opposite: `{ user_id }` and `{ userId }` both work everywhere. Full list of differences: [How it differs from the Python SDK](/sdk/initialization#how-it-differs-from-the-python-sdk). On a **B2B** instance, every user lives under a tenant, so each call also carries a `customer_id`. Pass it on both ingestion and retrieval: ```python Python theme={null} await sdk.memories.create( document="User: I prefer dark mode.\nAssistant: Noted!", document_type="ai-chat-conversation", user_id="user_alice", customer_id="acme_corp", ) # B2B: user scope requires BOTH user_id and customer_id. context = await sdk.user.context.fetch( user_id="user_alice", customer_id="acme_corp", search_query=["preferences"], ) ``` ```javascript JavaScript theme={null} await sdk.memories.create({ document: 'User: I prefer dark mode.\nAssistant: Noted!', document_type: 'ai-chat-conversation', user_id: 'user_alice', customer_id: 'acme_corp', }); // B2B: user scope requires BOTH user_id and customer_id. const context = await sdk.user.context.fetch({ user_id: 'user_alice', customer_id: 'acme_corp', search_query: ['preferences'], }); ``` ```typescript TypeScript theme={null} await sdk.memories.create({ document: 'User: I prefer dark mode.\nAssistant: Noted!', document_type: 'ai-chat-conversation', user_id: 'user_alice', customer_id: 'acme_corp', }); // B2B: user scope requires BOTH user_id and customer_id. const context = await sdk.user.context.fetch({ user_id: 'user_alice', customer_id: 'acme_corp', search_query: ['preferences'], }); ``` See [B2C vs B2B](#b2c-vs-b2b-which-one-are-you) below to tell which kind of instance you have. The rest of this page unfolds those few lines into a step-by-step walkthrough. Read it if this is your first time. Skip ahead to [First Integration](/setup/first-integration) for the full agent loop with an LLM. ## B2C vs B2B: which one are you? Synap supports two tenancy shapes, and your [Instance](/concepts/memory-scopes#clients-and-instances) is set to exactly one of them via the **User Relationship** setting in the Dashboard (Instance Settings). This single choice decides whether `customer_id` is required on every call or refused on every call. * **B2C (personal app)**: one tier of users, no tenant above them. You identify each user by `user_id` only. `customer_id` is **not accepted**: send it and the call fails with HTTP 400. The customer-scope fetch (`sdk.customer.context.fetch`) is not available on a B2C instance either. This is the right model when your agent's users are individuals (a companion app, a personal assistant, your first hobby agent, one user: you). * **B2B (multi-tenant)**: your customers are organizations, each containing many users. Every user is scoped under a `customer_id`, so you pass **both** `user_id` and `customer_id` on every call. Memories tagged at customer [scope](/concepts/memory-scopes) are shared across that tenant's users; user-scoped memories stay private to the user. **How to tell which you have:** open your Instance in the Dashboard and check **User Relationship** under Instance Settings, or call `GET /api/v1/auth/whoami`, which returns `user_context_isolation`: `equals_customer` means B2C, `strict` means B2B. If it's a personal/B2C relationship, send `user_id` alone and never `customer_id` (the examples in the main flow below). If it's a B2B relationship, add `customer_id` to every call (the B2B accordions). When in doubt, the default for a brand-new personal agent is B2C. This page's main walkthrough uses the **B2C** shape. Each step includes a B2B accordion showing the extra `customer_id`. *** ## Prerequisites * **Python:** 3.11 or later, with `pip` or `poetry` * **JavaScript / TypeScript:** Node 20 or later, with `npm`, `pnpm`, or `yarn` * A Synap account ([sign up at synap.maximem.ai](https://synap.maximem.ai)) A [**Client**](/concepts/memory-scopes#clients-and-instances) is your organization's top-level account in Synap. Every instance belongs to a Client. You have two options: **Create a new Client** 1. Log in to the [Synap Dashboard](https://synap.maximem.ai) 2. Click **Create Client**, enter your organization name, and confirm **Join an existing Client** If your team already has a Synap account, ask your administrator to invite you. Once added, log in and you'll see the shared Client and its instances in your Dashboard. Skipping this step is not possible: every instance must belong to a Client. If you are unsure whether your organization already has one, check with your team before creating a new Client. An instance is an isolated memory environment for your agent. Each instance has its own storage, configuration, and scope hierarchy. 1. In the Dashboard, navigate to **Instances** in the sidebar 2. Click **Create Instance** 3. Fill in the instance form: * **Name** (required): A human-readable label, e.g. `"My First Agent"` * **Agent Type** (optional): The kind of agent you're building (e.g. `B2B Customer Support`, `B2C Companion`, `Workflow Agent`). It seeds a sensible starting memory configuration for that use case. Skip it and Synap applies a default; you shape memory more precisely with the Use-Case Markdown file below, which you can update any time. * **Description** (optional): A short description of what this instance is for * **Use-Case Markdown** (optional but recommended): Upload a `.md` file describing your agent's use case (see below) Creating a new instance in the Synap Dashboard ### Use-Case Markdown The Use-Case Markdown file tells Synap what your agent does, who it serves, and what it should remember. Synap uses it to generate an optimized **Memory Architecture Configuration (MACA)** for your instance, so the more detail you provide, the better your memory extraction and retrieval will be from day one. Click **Download Template** in the Create Instance form, fill in at least the three required sections (Agent Objective, Target Users, Task Examples), and upload the file (`.md`, `.markdown`, or `.txt`, max 512 KB) before clicking **Create**. For the full template and section-by-section guidance, see [Writing a Use-Case Markdown File](/concepts/memory-architecture#the-use-case-file). You can upload or update the use-case file at any time after instance creation via **Instance Settings** in the Dashboard. Synap will re-evaluate and update the MACA when a new file is submitted. 1. In the Dashboard, go to your newly created instance 2. Open the **API Keys** section on the instance detail page 3. Click **Generate API Key** 4. Give it a label (e.g., "development") and click **Generate** 5. Copy the key immediately: it starts with `synap_` The API key is displayed only once. Copy it now and store it securely. If you lose it, revoke it from the dashboard and generate a new one. Now that you have a key, install the Synap SDK. ```bash pip theme={null} pip install maximem-synap ``` ```bash poetry theme={null} poetry add maximem-synap ``` ```bash uv theme={null} uv add maximem-synap # pip-compatible (existing venv): uv pip install maximem-synap ``` ```bash npm theme={null} npm install @maximem/synap-js-sdk ``` ```bash pnpm theme={null} pnpm add @maximem/synap-js-sdk ``` ```bash yarn theme={null} yarn add @maximem/synap-js-sdk ``` Streaming is enabled by default in Python: no extra install needed. In JavaScript the gRPC stream is opt-in and needs two optional peers, which you can add later: see [Installation](/setup/installation#install). Verify the installation: ```bash Python theme={null} python -c "import maximem_synap; print(maximem_synap.__version__)" ``` ```bash Node theme={null} node -e "import('@maximem/synap-js-sdk').then(m => console.log(m.SDK_VERSION))" ``` Set your API key and instance id as environment variables. The Dashboard shows both together: ```bash Linux / macOS theme={null} export SYNAP_API_KEY="synap_your_key_here" export SYNAP_INSTANCE_ID="inst_your_instance_id" ``` ```powershell Windows (PowerShell, session) theme={null} $env:SYNAP_API_KEY = "synap_your_key_here" $env:SYNAP_INSTANCE_ID = "inst_your_instance_id" ``` ```powershell Windows (PowerShell, persistent) theme={null} [System.Environment]::SetEnvironmentVariable("SYNAP_API_KEY", "synap_your_key_here", "User") [System.Environment]::SetEnvironmentVariable("SYNAP_INSTANCE_ID", "inst_your_instance_id", "User") ``` ```ini .env file theme={null} # Python: load with python-dotenv. Node 20.6+: `node --env-file=.env` SYNAP_API_KEY=synap_your_key_here SYNAP_INSTANCE_ID=inst_your_instance_id ``` `SYNAP_INSTANCE_ID` is optional: `initialize()` resolves the instance from your API key either way. Set it as an environment variable rather than passing `instance_id=` to the constructor, which makes the id the identity and breaks key rotation. See [Authentication](/setup/authentication). Create a new file: ```python main.py theme={null} import asyncio from maximem_synap import MaximemSynapSDK async def main(): sdk = MaximemSynapSDK() await sdk.initialize() print("Synap SDK initialized successfully!") # Your code goes here... await sdk.shutdown() if __name__ == "__main__": asyncio.run(main()) ``` ```javascript main.mjs theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient(); await sdk.initialize(); console.log('Synap SDK initialized successfully!'); // Your code goes here... await sdk.shutdown(); ``` ```typescript main.ts theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient(); await sdk.initialize(); console.log('Synap SDK initialized successfully!'); // Your code goes here... await sdk.shutdown(); ``` That's it. The SDK reads `SYNAP_API_KEY` from your environment automatically. You can also pass the API key directly if you prefer: ```python Python theme={null} sdk = MaximemSynapSDK(api_key="synap_your_key_here") ``` ```javascript JavaScript theme={null} const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); ``` ```typescript TypeScript theme={null} const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); ``` Run the script: ```bash Python (Linux / macOS) theme={null} python main.py ``` ```powershell Python (Windows PowerShell) theme={null} py main.py ``` ```bash Node theme={null} node main.mjs ``` ```bash TypeScript theme={null} npx tsx main.ts ``` You should see `Synap SDK initialized successfully!` On Windows, `python` is sometimes intercepted by the Microsoft Store app execution alias and fails with `"Python was not found"`. Use `py` (the Windows Python launcher) instead: it ships with every official Python installer. The same applies to `pip`: `py -m pip install maximem-synap` always works regardless of PATH configuration. Now let's send a conversation to Synap. The ingestion pipeline will automatically extract structured knowledge: facts, preferences, entities, and more. ```python Python theme={null} # Ingest a sample conversation (B2C: user_id only) response = await sdk.memories.create( document=( "User: I'm in San Francisco, what's the weather like?\n" "Assistant: It's sunny and 72°F in San Francisco today.\n" "User: Nice! I love warm weather. I'm planning a trip to Japan next month.\n" "Assistant: That sounds exciting! Japan in spring is beautiful." ), document_type="ai-chat-conversation", user_id="user_123", ) print(f"Ingestion ID: {response.ingestion_id}") print(f"Status: {response.status}") ``` ```javascript JavaScript theme={null} // Ingest a sample conversation (B2C: user_id only) const response = await sdk.memories.create({ document: [ "User: I'm in San Francisco, what's the weather like?", "Assistant: It's sunny and 72°F in San Francisco today.", "User: Nice! I love warm weather. I'm planning a trip to Japan next month.", 'Assistant: That sounds exciting! Japan in spring is beautiful.', ].join('\n'), document_type: 'ai-chat-conversation', user_id: 'user_123', }); console.log(`Ingestion ID: ${response.ingestion_id}`); console.log(`Status: ${response.status}`); ``` ```typescript TypeScript theme={null} // Ingest a sample conversation (B2C: user_id only) const response = await sdk.memories.create({ document: [ "User: I'm in San Francisco, what's the weather like?", "Assistant: It's sunny and 72°F in San Francisco today.", "User: Nice! I love warm weather. I'm planning a trip to Japan next month.", 'Assistant: That sounds exciting! Japan in spring is beautiful.', ].join('\n'), document_type: 'ai-chat-conversation', user_id: 'user_123', }); console.log(`Ingestion ID: ${response.ingestion_id}`); console.log(`Status: ${response.status}`); ``` On a B2B instance, add `customer_id` to scope this user under a tenant: ```python Python theme={null} response = await sdk.memories.create( document=( "User: I'm in San Francisco, what's the weather like?\n" "Assistant: It's sunny and 72°F in San Francisco today.\n" "User: Nice! I love warm weather. I'm planning a trip to Japan next month.\n" "Assistant: That sounds exciting! Japan in spring is beautiful." ), document_type="ai-chat-conversation", user_id="user_123", customer_id="acme_corp", ) ``` ```javascript JavaScript theme={null} const response = await sdk.memories.create({ document: [ "User: I'm in San Francisco, what's the weather like?", "Assistant: It's sunny and 72°F in San Francisco today.", "User: Nice! I love warm weather. I'm planning a trip to Japan next month.", 'Assistant: That sounds exciting! Japan in spring is beautiful.', ].join('\n'), document_type: 'ai-chat-conversation', user_id: 'user_123', customer_id: 'acme_corp', }); ``` ```typescript TypeScript theme={null} const response = await sdk.memories.create({ document: [ "User: I'm in San Francisco, what's the weather like?", "Assistant: It's sunny and 72°F in San Francisco today.", "User: Nice! I love warm weather. I'm planning a trip to Japan next month.", 'Assistant: That sounds exciting! Japan in spring is beautiful.', ].join('\n'), document_type: 'ai-chat-conversation', user_id: 'user_123', customer_id: 'acme_corp', }); ``` The SDK returns immediately with an ingestion ID. The pipeline processes the content asynchronously, extracting: * **Fact**: User is located in San Francisco * **Preference**: User loves warm weather * **Temporal event**: User is planning a trip to Japan next month * **Entities**: San Francisco, Japan (resolved and linked in the knowledge graph) Ingestion is asynchronous by design. The `memories.create()` call returns as soon as the content is accepted by Synap Cloud. Processing typically completes within a few seconds, but complex documents may take longer. Once memories are ingested and processed, you can retrieve relevant context. Synap searches across both vector and graph storage, ranks results by relevance, and respects scope boundaries. **Match the retrieval interface to the scope you ingested at.** Synap has three scope-specific retrieval methods, and a memory is only returned through the one that matches how it was tagged: | If you ingested with… | Retrieve via… | | -------------------------------------------------------------- | --------------------------------------------------------------------------------------- | | `user_id=...` (B2C) or `user_id=...` + `customer_id=...` (B2B) | `sdk.user.context.fetch(user_id=...)` | | `customer_id=...` (no user; B2B instances only) | `sdk.customer.context.fetch(customer_id=...)`, which is not available on a B2C instance | | A conversation registered via `record_message(...)` | `sdk.conversation.context.fetch(conversation_id=...)` | A memory tagged with multiple identifiers is retrievable through any matching interface. A conversation is registered only by `record_message(...)`; passing a `conversation_id` in `memories.create(metadata=...)` does **not** register it, because metadata is stored alongside the memory but is **not indexed for scope resolution**. Calling `conversation.context.fetch` with a brand-new, unregistered `conversation_id` returns empty results by design: there is no conversation row to anchor scope resolution. See [Context Fetch](/sdk/context-fetch) for the full reference. **B2C vs B2B scoping.** On **B2C** instances there is no customer dimension, so you send `user_id` alone; passing `customer_id` is rejected with HTTP 400. That's the example below. On **B2B** instances, memory is scoped to a `(user_id, customer_id)` pair, so when you fetch at user scope you must pass **both** identifiers; omitting `customer_id` raises an error. The ingestion above used `user_id="user_123"`, so retrieve at user scope (B2C, `user_id` only): **JavaScript returns the raw response**, where a collection the server omitted is `undefined` rather than an empty list. Python's Pydantic model always gives you a list. That is why the JavaScript tab writes `context.facts ?? []`: reading `context.facts.length` on a response with no facts throws. ```python Python theme={null} # Retrieve relevant context at user scope (matches the ingestion above). # B2C: user_id only. Passing customer_id here returns HTTP 400. context = await sdk.user.context.fetch( user_id="user_123", search_query=["weather preferences", "travel plans"], max_results=5, ) # Print retrieved facts print(f"Retrieved {len(context.facts)} facts:") for fact in context.facts: print(f" - {fact.content} (confidence: {fact.confidence})") # Print retrieved preferences print(f"\nRetrieved {len(context.preferences)} preferences:") for pref in context.preferences: print(f" - {pref.content}") # Print retrieved episodes print(f"\nRetrieved {len(context.episodes)} episodes:") for episode in context.episodes: print(f" - {episode.summary}") ``` ```javascript JavaScript theme={null} // Retrieve relevant context at user scope (matches the ingestion above). // B2C: user_id only. Passing customer_id here returns HTTP 400. const context = await sdk.user.context.fetch({ user_id: 'user_123', search_query: ['weather preferences', 'travel plans'], max_results: 5, }); // The namespaced surface returns raw snake_case, and each collection is // optional, so default it before reading .length. const facts = context.facts ?? []; const preferences = context.preferences ?? []; const episodes = context.episodes ?? []; console.log(`Retrieved ${facts.length} facts:`); for (const fact of facts) { console.log(` - ${fact.content} (confidence: ${fact.confidence})`); } console.log(`\nRetrieved ${preferences.length} preferences:`); for (const pref of preferences) { console.log(` - ${pref.content}`); } console.log(`\nRetrieved ${episodes.length} episodes:`); for (const episode of episodes) { console.log(` - ${episode.summary}`); } ``` ```typescript TypeScript theme={null} // Retrieve relevant context at user scope (matches the ingestion above). // B2C: user_id only. Passing customer_id here returns HTTP 400. const context = await sdk.user.context.fetch({ user_id: 'user_123', search_query: ['weather preferences', 'travel plans'], max_results: 5, }); // The namespaced surface returns raw snake_case, and each collection is // optional, so default it before reading .length. const facts = context.facts ?? []; const preferences = context.preferences ?? []; const episodes = context.episodes ?? []; console.log(`Retrieved ${facts.length} facts:`); for (const fact of facts) { console.log(` - ${fact.content} (confidence: ${fact.confidence})`); } console.log(`\nRetrieved ${preferences.length} preferences:`); for (const pref of preferences) { console.log(` - ${pref.content}`); } console.log(`\nRetrieved ${episodes.length} episodes:`); for (const episode of episodes) { console.log(` - ${episode.summary}`); } ``` On a B2B instance you ingested with `customer_id="acme_corp"`, so pass both identifiers at user scope: ```python Python theme={null} context = await sdk.user.context.fetch( user_id="user_123", customer_id="acme_corp", search_query=["weather preferences", "travel plans"], max_results=5, ) ``` ```javascript JavaScript theme={null} const context = await sdk.user.context.fetch({ user_id: 'user_123', customer_id: 'acme_corp', search_query: ['weather preferences', 'travel plans'], max_results: 5, }); ``` ```typescript TypeScript theme={null} const context = await sdk.user.context.fetch({ user_id: 'user_123', customer_id: 'acme_corp', search_query: ['weather preferences', 'travel plans'], max_results: 5, }); ``` Example output: ``` Retrieved 1 facts: - User is located in San Francisco (confidence: 0.92) Retrieved 1 preferences: - User loves warm weather Retrieved 1 episodes: - User is planning a trip to Japan next month ``` You can now inject this context into your LLM's system prompt or conversation history to create a personalized, context-aware experience. Always shut down the SDK cleanly to flush any pending operations and release resources: **The JavaScript cache does not survive the restart.** Python caches to SQLite, so the next run reuses it; JavaScript caches in memory. A cache miss is a metered retrieval, so short-lived processes and serverless functions make more billed fetches than the equivalent Python deployment. ```python Python theme={null} await sdk.shutdown() ``` ```javascript JavaScript theme={null} await sdk.shutdown(); ``` ```typescript TypeScript theme={null} await sdk.shutdown(); ``` The complete script looks like this: ```python main.py theme={null} import asyncio from maximem_synap import MaximemSynapSDK async def main(): # Reads SYNAP_API_KEY from environment; instance ID resolved server-side sdk = MaximemSynapSDK() await sdk.initialize() # Ingest a conversation (B2C: user_id only) response = await sdk.memories.create( document=( "User: I'm in San Francisco, what's the weather like?\n" "Assistant: It's sunny and 72°F in San Francisco today.\n" "User: Nice! I love warm weather. I'm planning a trip to Japan next month.\n" "Assistant: That sounds exciting! Japan in spring is beautiful." ), document_type="ai-chat-conversation", user_id="user_123", ) print(f"Ingested: {response.ingestion_id}") # Block until ingestion completes, instead of guessing with a fixed sleep. await sdk.memories.wait_for_completion(response.ingestion_id) # Retrieve context at the same scope used for ingestion (user). # B2C: user_id only. customer_id is not accepted on this instance. # (On a B2B instance, pass both user_id and customer_id here.) context = await sdk.user.context.fetch( user_id="user_123", search_query=["weather preferences"], max_results=5, ) for fact in context.facts: print(f" {fact.content} (confidence: {fact.confidence})") await sdk.shutdown() if __name__ == "__main__": asyncio.run(main()) ``` ```javascript main.mjs theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; // Reads SYNAP_API_KEY from environment; instance ID resolved server-side const sdk = new SynapClient(); await sdk.initialize(); // Ingest a conversation (B2C: user_id only) const response = await sdk.memories.create({ document: [ "User: I'm in San Francisco, what's the weather like?", "Assistant: It's sunny and 72°F in San Francisco today.", "User: Nice! I love warm weather. I'm planning a trip to Japan next month.", 'Assistant: That sounds exciting! Japan in spring is beautiful.', ].join('\n'), document_type: 'ai-chat-conversation', user_id: 'user_123', }); console.log(`Ingested: ${response.ingestion_id}`); // Block until ingestion completes, instead of guessing with a fixed sleep. await sdk.memories.wait_for_completion(response.ingestion_id); // Retrieve context at the same scope used for ingestion (user). // B2C: user_id only. customer_id is not accepted on this instance. // (On a B2B instance, pass both user_id and customer_id here.) const context = await sdk.user.context.fetch({ user_id: 'user_123', search_query: ['weather preferences'], max_results: 5, }); for (const fact of context.facts ?? []) { console.log(` ${fact.content} (confidence: ${fact.confidence})`); } await sdk.shutdown(); ``` ```typescript main.ts theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; // Reads SYNAP_API_KEY from environment; instance ID resolved server-side const sdk = new SynapClient(); await sdk.initialize(); // Ingest a conversation (B2C: user_id only) const response = await sdk.memories.create({ document: [ "User: I'm in San Francisco, what's the weather like?", "Assistant: It's sunny and 72°F in San Francisco today.", "User: Nice! I love warm weather. I'm planning a trip to Japan next month.", 'Assistant: That sounds exciting! Japan in spring is beautiful.', ].join('\n'), document_type: 'ai-chat-conversation', user_id: 'user_123', }); console.log(`Ingested: ${response.ingestion_id}`); // Block until ingestion completes, instead of guessing with a fixed sleep. await sdk.memories.wait_for_completion(response.ingestion_id); // Retrieve context at the same scope used for ingestion (user). // B2C: user_id only. customer_id is not accepted on this instance. // (On a B2B instance, pass both user_id and customer_id here.) const context = await sdk.user.context.fetch({ user_id: 'user_123', search_query: ['weather preferences'], max_results: 5, }); for (const fact of context.facts ?? []) { console.log(` ${fact.content} (confidence: ${fact.confidence})`); } await sdk.shutdown(); ``` A real agent doesn't ingest in isolation. It **fetches relevant context, calls the LLM with that context, and ingests the resulting turn back into memory**. That is the atomic unit of using Synap. Here's the minimum-viable version with OpenAI (B2C, `user_id` only): `conversation_id` must be a valid UUID: the server rejects non-UUID strings. Generate one per chat thread (`uuid.uuid4()` in Python, `crypto.randomUUID()` in JavaScript) and reuse it across that thread's turns. ```python Python theme={null} import uuid from maximem_synap import MaximemSynapSDK from openai import AsyncOpenAI sdk = MaximemSynapSDK() openai = AsyncOpenAI() # One UUID per chat thread, reused across its turns. conversation_id = str(uuid.uuid4()) async def turn(user_id: str, conversation_id: str, user_message: str) -> str: # 1. Record the user's message. record_message is what REGISTERS the # conversation, creating the row that conversation.context.fetch needs # to resolve scope on subsequent turns. (Passing conversation_id only # as memories.create metadata would NOT register it; metadata is # stored but not indexed for scope resolution.) await sdk.conversation.record_message( conversation_id=conversation_id, role="user", content=user_message, user_id=user_id, ) # 2. Retrieve relevant memory context for this conversation ctx = await sdk.conversation.context.fetch( conversation_id=conversation_id, user_id=user_id, search_query=[user_message], ) memories = "\n".join(f"- {f.content}" for f in ctx.facts[:5]) # 3. Generate with the LLM, injecting memories into the system prompt completion = await openai.chat.completions.create( model="gpt-4o", messages=[ {"role": "system", "content": f"Known facts about the user:\n{memories}"}, {"role": "user", "content": user_message}, ], ) reply = completion.choices[0].message.content # 4. Record the assistant reply, then persist the turn for long-term retrieval await sdk.conversation.record_message( conversation_id=conversation_id, role="assistant", content=reply, user_id=user_id, ) await sdk.memories.create( document=f"User: {user_message}\nAssistant: {reply}", document_type="ai-chat-conversation", user_id=user_id, metadata={"conversation_id": conversation_id}, ) return reply ``` ```javascript JavaScript theme={null} import { randomUUID } from 'node:crypto'; import { SynapClient } from '@maximem/synap-js-sdk'; import OpenAI from 'openai'; const sdk = new SynapClient(); const openai = new OpenAI(); // One UUID per chat thread, reused across its turns. const conversationId = randomUUID(); async function turn(userId, conversationId, userMessage) { // 1. Record the user's message. record_message is what REGISTERS the // conversation, creating the row that conversation.context.fetch needs // to resolve scope on subsequent turns. (Passing conversation_id only // as memories.create metadata would NOT register it; metadata is // stored but not indexed for scope resolution.) await sdk.conversation.record_message({ conversation_id: conversationId, role: 'user', content: userMessage, user_id: userId, }); // 2. Retrieve relevant memory context for this conversation const ctx = await sdk.conversation.context.fetch({ conversation_id: conversationId, user_id: userId, search_query: [userMessage], }); const memories = (ctx.facts ?? []) .slice(0, 5) .map((f) => `- ${f.content}`) .join('\n'); // 3. Generate with the LLM, injecting memories into the system prompt const completion = await openai.chat.completions.create({ model: 'gpt-4o', messages: [ { role: 'system', content: `Known facts about the user:\n${memories}` }, { role: 'user', content: userMessage }, ], }); const reply = completion.choices[0]?.message.content ?? ''; // 4. Record the assistant reply, then persist the turn for long-term retrieval await sdk.conversation.record_message({ conversation_id: conversationId, role: 'assistant', content: reply, user_id: userId, }); await sdk.memories.create({ document: `User: ${userMessage}\nAssistant: ${reply}`, document_type: 'ai-chat-conversation', user_id: userId, metadata: { conversation_id: conversationId }, }); return reply; } ``` ```typescript TypeScript theme={null} import { randomUUID } from 'node:crypto'; import { SynapClient } from '@maximem/synap-js-sdk'; import OpenAI from 'openai'; const sdk = new SynapClient(); const openai = new OpenAI(); // One UUID per chat thread, reused across its turns. const conversationId = randomUUID(); async function turn( userId: string, conversationId: string, userMessage: string, ): Promise { // 1. Record the user's message. record_message is what REGISTERS the // conversation, creating the row that conversation.context.fetch needs // to resolve scope on subsequent turns. (Passing conversation_id only // as memories.create metadata would NOT register it; metadata is // stored but not indexed for scope resolution.) await sdk.conversation.record_message({ conversation_id: conversationId, role: 'user', content: userMessage, user_id: userId, }); // 2. Retrieve relevant memory context for this conversation const ctx = await sdk.conversation.context.fetch({ conversation_id: conversationId, user_id: userId, search_query: [userMessage], }); const memories = (ctx.facts ?? []) .slice(0, 5) .map((f) => `- ${f.content}`) .join('\n'); // 3. Generate with the LLM, injecting memories into the system prompt const completion = await openai.chat.completions.create({ model: 'gpt-4o', messages: [ { role: 'system', content: `Known facts about the user:\n${memories}` }, { role: 'user', content: userMessage }, ], }); const reply = completion.choices[0]?.message.content ?? ''; // 4. Record the assistant reply, then persist the turn for long-term retrieval await sdk.conversation.record_message({ conversation_id: conversationId, role: 'assistant', content: reply, user_id: userId, }); await sdk.memories.create({ document: `User: ${userMessage}\nAssistant: ${reply}`, document_type: 'ai-chat-conversation', user_id: userId, metadata: { conversation_id: conversationId }, }); return reply; } ``` The `metadata={"conversation_id": ...}` on `memories.create` is for your own bookkeeping: it travels with the memory but is **not indexed**, so it does not register the conversation or affect scope resolution. The conversation is registered solely by `record_message(...)` in step 1. On a B2B instance, thread `customer_id` through every call alongside `user_id`: ```python Python theme={null} async def turn(user_id: str, customer_id: str, conversation_id: str, user_message: str) -> str: await sdk.conversation.record_message( conversation_id=conversation_id, role="user", content=user_message, user_id=user_id, customer_id=customer_id, ) ctx = await sdk.conversation.context.fetch( conversation_id=conversation_id, user_id=user_id, customer_id=customer_id, search_query=[user_message], ) # ... generate reply ... await sdk.conversation.record_message( conversation_id=conversation_id, role="assistant", content=reply, user_id=user_id, customer_id=customer_id, ) await sdk.memories.create( document=f"User: {user_message}\nAssistant: {reply}", document_type="ai-chat-conversation", user_id=user_id, customer_id=customer_id, metadata={"conversation_id": conversation_id}, ) ``` ```javascript JavaScript theme={null} async function turn(userId, customerId, conversationId, userMessage) { await sdk.conversation.record_message({ conversation_id: conversationId, role: 'user', content: userMessage, user_id: userId, customer_id: customerId, }); const ctx = await sdk.conversation.context.fetch({ conversation_id: conversationId, user_id: userId, customer_id: customerId, search_query: [userMessage], }); // ... generate reply ... await sdk.conversation.record_message({ conversation_id: conversationId, role: 'assistant', content: reply, user_id: userId, customer_id: customerId, }); await sdk.memories.create({ document: `User: ${userMessage}\nAssistant: ${reply}`, document_type: 'ai-chat-conversation', user_id: userId, customer_id: customerId, metadata: { conversation_id: conversationId }, }); } ``` ```typescript TypeScript theme={null} async function turn( userId: string, customerId: string, conversationId: string, userMessage: string, ): Promise { await sdk.conversation.record_message({ conversation_id: conversationId, role: 'user', content: userMessage, user_id: userId, customer_id: customerId, }); const ctx = await sdk.conversation.context.fetch({ conversation_id: conversationId, user_id: userId, customer_id: customerId, search_query: [userMessage], }); // ... generate reply ... await sdk.conversation.record_message({ conversation_id: conversationId, role: 'assistant', content: reply, user_id: userId, customer_id: customerId, }); await sdk.memories.create({ document: `User: ${userMessage}\nAssistant: ${reply}`, document_type: 'ai-chat-conversation', user_id: userId, customer_id: customerId, metadata: { conversation_id: conversationId }, }); } ``` For a fully-worked FastAPI + OpenAI app (including error handling, graceful degradation, and conversation routing), continue to the [First Integration](/setup/first-integration) guide. ## What's next? You've successfully ingested your first memory and retrieved context. Here's where to go from here: Understand the full Synap architecture: scopes, memory types, entity resolution, and the ingestion pipeline. Configure the SDK for your production environment: timeouts, retries, logging, and credential management. Learn how to configure what gets extracted, how it's stored, and how retrieval ranking works. Security, performance, monitoring, and reliability best practices before going live. # Your first memory Source: https://docs.maximem.ai/getting-started/your-first-memory Ingest one message, see what Synap extracts from it, and read it back. The shortest path to understanding what Synap actually does for your agent. Once you have an API key (from the [Quickstart](/getting-started/quickstart)), you can see Synap's memory in action in a few lines. This page ingests a single message, shows what Synap pulls out of it, and retrieves it back. This is a B2C example: one user, identified by `user_id` alone. A B2C instance does not accept `customer_id`, and sending one is rejected with HTTP 400. On a B2B instance you must pass a `customer_id` alongside `user_id`. See [Identifiers & Scopes](/concepts/memory-scopes). ## 1. Ingest one message You hand Synap raw text; it runs the [ingestion pipeline](/concepts/how-ingestion-works) and stores structured memory. The call returns immediately with an `ingestion_id` you can wait on. ```python first_memory.py theme={null} import asyncio from maximem_synap import MaximemSynapSDK async def main(): sdk = MaximemSynapSDK() # reads SYNAP_API_KEY from the environment await sdk.initialize() try: result = await sdk.memories.create( document=( "User: I'm Alex, I run a small coffee roastery in Portland. " "I always prefer email over phone, and I'm planning to expand to a " "second location next spring." ), document_type="ai-chat-conversation", user_id="user_alex", ) # Block until the pipeline finishes (good for scripts and tests). await sdk.memories.wait_for_completion(result.ingestion_id) print("Ingested:", result.ingestion_id) finally: await sdk.shutdown() asyncio.run(main()) ``` ```javascript first-memory.mjs theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient(); // reads SYNAP_API_KEY from the environment await sdk.initialize(); try { const result = await sdk.memories.create({ document: "User: I'm Alex, I run a small coffee roastery in Portland. " + "I always prefer email over phone, and I'm planning to expand to a " + 'second location next spring.', document_type: 'ai-chat-conversation', user_id: 'user_alex', }); // Block until the pipeline finishes (good for scripts and tests). await sdk.memories.wait_for_completion(result.ingestion_id); console.log('Ingested:', result.ingestion_id); } finally { await sdk.shutdown(); } ``` ```typescript first-memory.ts theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient(); // reads SYNAP_API_KEY from the environment await sdk.initialize(); try { const result = await sdk.memories.create({ document: "User: I'm Alex, I run a small coffee roastery in Portland. " + "I always prefer email over phone, and I'm planning to expand to a " + 'second location next spring.', document_type: 'ai-chat-conversation', user_id: 'user_alex', }); // Block until the pipeline finishes (good for scripts and tests). await sdk.memories.wait_for_completion(result.ingestion_id); console.log('Ingested:', result.ingestion_id); } finally { await sdk.shutdown(); } ``` ## 2. See what Synap extracted From that one message, Synap extracts structured [memory types](/concepts/memories-and-context#memory-types), not just a blob of text: | Type | What it found | | ------------------ | ------------------------------------------------------------------- | | **Fact** | Alex runs a coffee roastery in Portland. | | **Preference** | Prefers email over phone. | | **Temporal event** | Planning a second location next spring. | | **Entities** | Alex, Portland, coffee roastery (resolved and linked in the graph). | You did not tag any of this by hand. Extraction and [entity resolution](/concepts/entity-resolution) happen automatically. ## 3. Read it back On the next turn, fetch context for the same scope you ingested at, before you call your LLM: ```python Python theme={null} context = await sdk.user.context.fetch( user_id="user_alex", search_query=["communication preferences", "business"], ) for fact in context.facts: print("fact:", fact.content) for pref in context.preferences: print("preference:", pref.content) ``` ```javascript JavaScript theme={null} const context = await sdk.user.context.fetch({ user_id: 'user_alex', search_query: ['communication preferences', 'business'], }); for (const fact of context.facts ?? []) { console.log('fact:', fact.content); } for (const pref of context.preferences ?? []) { console.log('preference:', pref.content); } ``` ```typescript TypeScript theme={null} const context = await sdk.user.context.fetch({ user_id: 'user_alex', search_query: ['communication preferences', 'business'], }); for (const fact of context.facts ?? []) { console.log('fact:', fact.content); } for (const pref of context.preferences ?? []) { console.log('preference:', pref.content); } ``` Inject `context.facts` and `context.preferences` into your system prompt, and your agent now "remembers" Alex on every future conversation. ## Where to go next The full setup: create a Client, an Instance, and an API key, then run the loop. The 5 identifiers, 2 write paths, and 4 fetch interfaces on one page. Try it in the browser, no install or API key needed. Wire Synap into a real FastAPI + LLM app, end to end. # Shaping Your MACA Source: https://docs.maximem.ai/guides/configuring-memory Every Synap Instance gets a Memory Architecture (MACA) auto-generated from the Use-Case Markdown file you upload. This page explains what MACA controls and how to shape it through your use-case file, with no YAML to author. You **do not write MACA YAML**. Synap generates and maintains the Memory Architecture for your Instance from the [Use-Case Markdown](/concepts/memory-architecture#the-use-case-file) file you upload at Instance creation. This page is a high-level tour of what MACA decides for you, and how to nudge those decisions through your use-case file. ## What MACA decides for you The Memory Architecture is Synap's internal configuration for one Instance. It determines, for every memory operation against that Instance: Which categories Synap extracts and stores from your documents and conversations: `facts`, `preferences`, `episodes`, `emotions`, `temporal`. Some agents need all of them; others only need facts. How much context Synap surfaces per query, how aggressive recency weighting is, and how the `fast` and `accurate` retrieval modes prioritize different signals. Which scope level (`USER`, `CUSTOMER`, or `CLIENT`) is the **primary** access pattern for your agent. Synap optimizes indexing and caching around the primary scope, but all four scopes (`USER → CUSTOMER → CLIENT → WORLD`) are always available. How long different categories of memory live before being pruned or compacted, and which signals trigger archival. You shape all of these by describing your agent in the use-case file. Synap reads the file, infers what your agent actually needs, and generates a MACA tuned for it. *** ## The authoring surface: Use-Case Markdown The [Use-Case Markdown](/concepts/memory-architecture#the-use-case-file) file is a small Markdown document that describes: * What your agent does * Who its users are (consumer, B2B, internal team, etc.) * What kinds of information it needs to remember * Any compliance or retention constraints Synap uses it to generate your MACA. If your agent's behavior, audience, or compliance requirements change, **re-upload the file**. Synap regenerates the MACA from the new version. Existing memories keep their original scope assignment; new memories follow the updated behavior. Treat the use-case file like a product brief, not a config file. The clearer you are about who the agent serves and what it needs to remember, the better MACA you get. *** ## Snippet recipes The four patterns below are common shapes. Pick the one closest to your agent and adapt the wording. These are **Use-Case Markdown snippets**, not configuration files. Drop them into your `use-case.md` and Synap handles the rest. ### B2B customer support A SaaS support assistant serving multiple customer organizations. Each end-user belongs to a customer, and most knowledge is customer-scoped (their account, their tickets, their team). ```markdown theme={null} # Agent: Acme Support Assistant ## Purpose An in-product assistant that helps customer-organization users resolve support issues, look up account details, and reference past tickets. ## Audience B2B: each end-user belongs to a customer organization. Memory must be isolated per customer; users within the same customer can share account and ticket context. ## What it remembers - Customer account configuration, plan tier, integrations enabled - Past support tickets and their resolutions - User-level preferences (preferred contact channel, language) ## Compliance - 90-day retention on conversation transcripts - Customer data must never cross customer boundaries ``` ### Personal assistant A consumer-facing personal AI. Single user per Instance ID; deep personalization matters more than throughput. ```markdown theme={null} # Agent: Personal Companion ## Purpose A personal assistant that helps one user manage their schedule, notes, relationships, and recurring goals. ## Audience Consumer: one user per account. No customer/organization concept. ## What it remembers - User preferences (communication style, tone, dietary needs) - Episodes (events the user describes in conversation) - Emotional context (what the user cares about, what frustrates them) - Temporal references (deadlines, recurring commitments) ## Compliance - User can request full export and deletion at any time - No retention limit by default ``` ### Knowledge base A documentation-grounded assistant. Most memory is shared application-wide; per-user state is minimal. ```markdown theme={null} # Agent: Docs Assistant ## Purpose Answer product and API questions from our ingested documentation, code samples, and changelog. Users do not "teach" the agent; they query it. ## Audience All users of the application share the same knowledge surface. No per-customer isolation needed. ## What it remembers - Product documentation, API reference, code samples (ingested in bulk) - Changelog entries with timestamps - Almost no user-level memory; every user sees the same answers ## Retrieval - Precision matters more than latency. Default to accurate mode for user-facing queries. ``` ### High-volume ingestion An agent ingesting large volumes of documents (support tickets, logs, transcripts) where throughput and graceful degradation under load matter. ```markdown theme={null} # Agent: Ingestion Pipeline ## Purpose Continuously ingest support tickets and customer-success transcripts so they're available for retrieval by downstream agents. ## Audience B2B: tickets are customer-scoped. Retrieval is performed by other agents, not directly by humans. ## What it remembers - Ticket content, resolution, escalation path - Customer metadata associated with each ticket - Temporal markers (opened, escalated, resolved) ## Throughput - Volume is measured in thousands of documents per day per customer - Ingestion uses batch_create with long-range mode - Latency-sensitive retrieval is not required; fast mode is sufficient ``` *** ## Inspecting what you got After your Instance is created (or after you re-upload a use-case file), the Dashboard surfaces the resolved Memory Architecture for review: * The categories Synap will extract * The primary scope it picked * The retrieval defaults it tuned If anything looks wrong for your agent, edit the use-case file and re-upload. That is the supported authoring loop. Customers do not directly edit, paste, or version MACA YAML. The use-case file is the source of truth; MACA is a derived artifact. *** ## Related concepts The authoring surface: what to put in your `use-case.md` and how Synap reads it. Deeper conceptual background on what MACA is and what it covers. The canonical `USER → CUSTOMER → CLIENT → WORLD` scope chain and how isolation works. Reference for `facts`, `preferences`, `episodes`, `emotions`, and `temporal`. *** ## Next steps Patterns for isolating per-user, per-customer, and per-client memories in multi-tenant apps. Security, performance, and monitoring steps before going live. Step-by-step authoring guide with the full template. Map your existing memory layer (Mem0, Zep, Letta, SuperMemory) onto Synap. # Your Own Field Types Source: https://docs.maximem.ai/guides/custom-field-types Synap ships detection for common identifier formats. When your data has a format we do not know about, an asset tag or a policy number, you describe it yourself with a few examples and pick the category it belongs in. No release and no ticket. **Read this before you rely on it.** A field type you describe is detected in the **Try it** test box on the Sensitive data page, so you can see exactly what it would match and what your policy would do with it. It is **not yet applied to live ingestion or to search**. Content flowing through Synap today is matched against the shipped field types only. Describing your own field type records it, shows you what it would catch, and prepares the policy, but it does not yet change what happens to your traffic. If you need a custom format protected in production now, talk to us at **[support@maximem.ai](mailto:support@maximem.ai)** rather than assuming the definition is live. ## When you need one Synap ships detection for formats that are common across products: email addresses, phone numbers, card numbers, government identifiers, IP addresses, API keys. It does not ship detection for formats that are specific to your business. An asset tag, a policy number, an internal order id, a claim reference: these identify a person or a case in your product, they appear all over your content, and nobody outside your company would recognise the shape. That is what this is for. ## Describing one In the Dashboard, go to **Sensitive data**, then the **Your own field types** tab. Give it the name your team already uses, in plain words: "Asset tag", "Policy number", "Claim reference". The name is what appears in your findings list, in the test box, and in your audit trail, so it should read like something a person would say out loud. Names must be unique and cannot reuse the name of a field type Synap already ships. Every field type sits in one of the [eleven categories](/guides/pii-protection#the-eleven-categories), and the category is what carries the setting. Picking the category is the decision that matters, so see the guidance below. You can move it to a different category later, and the setting follows it. Paste two or three real examples, one per line: ```text theme={null} AST-00123 AST-99881 AST-40027 ``` Synap builds a pattern from the *shape* of what you give it: which positions are letters, which are digits, how long each run is, and where the separators sit. Three asset tags produce a pattern that matches asset tags. Save, then open the **Try it** tab and paste text containing one of your identifiers. A match appears badged as **your field type**, with what your policy would do with it at each destination. If the tab shows your field type as **inactive** with "not detected yet, needs examples", no usable pattern could be built from what you gave. Add more examples, or examples that are more alike. Synap will not fall back to a pattern that matches everything, because a rule that fires on all text buries you in false alarms while looking like the feature working. *** ## Writing examples that work The pattern is built from structure, not from meaning. That has three practical consequences. **Cover every shape your identifier takes.** If asset tags are sometimes `AST-00123` and sometimes `AST-00123-R`, give at least one of each. Each distinct shape you provide becomes one thing the pattern will match; a shape you never showed will never be found. **Give examples of the same thing, not a grab bag.** Three unrelated strings produce a pattern that matches those three shapes and nothing else, which is honest but not useful. Three genuine examples of one format produce a pattern for that format. **The more distinctive the shape, the better it works.** `AST-00123` has a literal prefix and a fixed digit count, so it matches almost nothing else. A bare six-digit number matches every six-digit number in your content, including quantities, order totals, and years in a row. If your identifier is a bare number, expect false alarms, and check the test box before you protect it. A few things to know: * Up to 20 examples. Examples longer than 128 characters are ignored. * Examples are used to build the pattern and are never returned by the API. The dashboard shows how many you gave, not what they were. * Changing the examples means updating the field type; the pattern is rebuilt from what you save. *** ## Picking the category The category decides the setting, so pick by asking **"what should happen to this value?"** rather than "what is it technically?". | If it... | Put it in | | --------------------------------------------------------- | ---------------------- | | Identifies a specific person by itself | **Formal Identity** | | Is a way to reach a person | **Contact Info** | | Is money-related: an account, a card, a payment reference | **Payment Info** | | Says something about someone's health | **Health** | | Names a place precisely enough to find someone | **Location (fine)** | | Names a broad area only | **Location (coarse)** | | Identifies a device, a session, or an account online | **Device and Online** | | Would let someone in if they had it | **Secrets** | | Says something about origin, belief, or affiliation | **Origin and Beliefs** | | Is a person's name or a direct stand-in for it | **Raw Identity** | | Is an encoded biometric template | **Biometric** | Two rules of thumb: * **When it is close, pick the stricter one.** A category is a default you can override per field type later, and starting stricter costs you nothing visible: a value that is protected and revealed straight back to your app reads identically to one that was never protected. * **Do not invent a category for a field type that only matters to you.** If an order id genuinely needs different handling from everything else in Formal Identity, put it there and set a per-field-type exception on the advanced view. **Secrets is not the floor.** Putting a field type in Secrets applies whatever setting you gave that category. The floor is a fixed list of seven shipped field types that are never kept for anyone, and a field type you define cannot join it. If you have a secret format that must never be stored, set the category to **Do not store it**. *** ## What happens next Once your field type is detected, it behaves like any shipped one. It appears in your findings list with counts, it appears in the test box, it takes the setting from its category unless you set an exception, and if you protect it, it gets an alias like everything else. Aliases for your own field types are prefixed `CUSTOM`. Removing a field type stops it from being detected going forward. Memories already written are unaffected, and any placeholders already in them keep resolving. ## Next steps The categories, the settings, and what detection can and cannot do. The findings list, the test box, and approving a policy. # Erasing a Person Source: https://docs.maximem.ai/guides/erasure Deleting everything Synap holds about one person, what that makes permanently unreadable including in backups, and the one thing it deliberately leaves behind. Covers the preview step, the confirmation, and how to request it. **There is no undo.** Erasure destroys the encrypted form of every protected value it covers. Once it has run, those values cannot be recovered by us, by you, or from a backup taken a minute earlier. That is the point of it, and it is why the operation is deliberately two steps. ## What erasure is for A person asks you to delete everything you hold about them. You need to be able to say yes, and mean it, including for copies that exist in backups you cannot go back and rewrite. Ordinary deletion cannot promise that. A backup taken yesterday still holds yesterday's rows, and no delete statement reaches it. Erasure solves it a different way: the values Synap holds for a protected field type are unreadable without a key, and erasing a person destroys the part that makes their values readable. Every copy, everywhere, including in backups, becomes meaningless bytes at the same moment. ## How to request it Erasure is not a self-service button. Email **[privacy@maximem.ai](mailto:privacy@maximem.ai)** with the instance and the `user_id` or `customer_id` to erase, and the reason. Synap runs it, and it appears in your own **Activity** tab the same day with the name of the person who ran it. It is a support-operated action for one reason: a preview that says four things and an erasure that removes nine hundred is a conversation nobody should have to have. The two-step flow below is what prevents that, and it is run by a named person against a number you have both agreed on. *** ## The two steps The preview counts exactly what would go, using the same conditions the erasure itself runs. It is not a confirmation dialog with an estimate in it; the number cannot turn out to be wrong. It reports: * How many protected values would be destroyed, broken down by field type * How many memory records would be deleted * How many values would be **kept**, because they are shared with other people * A plain-language summary of all of the above It reports counts only. It never lists the values, because assembling a list of one person's sensitive data in order to delete it is the shape of problem this whole feature exists to avoid. The erasure carries the number the preview showed. If the real total has moved since you looked, because new data arrived, the erasure stops rather than deleting a different amount than the one that was agreed. An audit entry is written **before** anything is deleted. An erasure that succeeded with no record of who ordered it would be worse than one that failed. *** ## What becomes unreadable Every protected value held for that person is destroyed. Afterwards: * Their memories keep their sentences and their placeholders, so the record of what happened is still readable. * Every placeholder belonging to them stops resolving. Your application receives the placeholder rather than a value, and so do we. * A reveal on one of those placeholders returns nothing, for anyone, including Synap staff. * Backups taken before the erasure contain the same unreadable form. There is nothing in them to recover. Their memory records are deleted as part of the same operation. If you also need the underlying memory content itself removed everywhere it has been indexed, say so in the same request. Erasure is specifically about making the protected values unrecoverable; removing content from every store goes through the standard deletion path described in [Security and Trust](/resources/security-trust#deletion-guarantees). *** ## What is deliberately kept Some values are shared by construction. Two people give you the same phone number. One company address appears in a thousand memories. Deleting a shared value while erasing one person would silently break every other person's memories that reference it, and that failure looks like the product forgetting rather than like a deletion. So Synap erases the values held under that person's own scope, and leaves values that are shared more widely. The preview counts them and says so plainly, in the form: *"N value(s) will be kept, because they are shared with other people. Deleting them would break other people's memories."* Whether a given shared value should also go has a real answer, and it is different per client. If it should, say so in your request and it is handled as a separate decision. *** ## Erasing an entire client Removing a whole account is one operation, not a scan. Every value that account owns becomes unreadable immediately, with nothing to search for and nothing to re-encrypt, including in backups already taken. The memory text itself is left readable as sentences, with the placeholders still in place. There is no key anywhere that turns those placeholders back into values. *** ## What this does not cover * **Data ingested before you approved a policy is plain text.** It was never protected, so there is no key whose removal makes it unreadable. Erasure cannot reach back and change that. If you need old data cleaned up, that is a re-processing job; ask us and we will scope it. * **Field types you never protected are plain text**, for the same reason. Erasure protects what was protected. * **A person you cannot name cannot be erased.** The operation works from a `user_id` or a `customer_id`. If your application does not pass a stable id, there is nothing to match on. *** ## The paper trail Every erasure writes one entry to your **Activity** tab, before it runs, recording who ordered it, what reason they gave, how many things it covered, and when. Your own staff and Synap's appear in the same list with a column saying which is which. Entries are kept for one year and can be exported as a CSV file for an audit. A Synap staff member operating an impersonated session cannot run an erasure at all: impersonation exists so we can see what you see, and destroying data has to be done in a named person's own name. ## Next steps What a placeholder is, and what stops resolving after an erasure. Deletion guarantees, encryption, and the limits we state plainly. What gets protected in the first place, and how you decide. Common questions from engineering and from security reviewers. # Instance Visibility Source: https://docs.maximem.ai/guides/instance-visibility Control which of a client's Instances can see which other Instances' memories at retrieval time. A first-class, client-level policy you manage from the dashboard, off by default, so today's sharing behaviour is preserved until you opt in. By default, every Instance under the same client (and same customer) shares memory. **Instance Visibility** lets you turn that sharing into something you control (per-Instance and directionally) without moving any data. It is **opt-in**: until you set a policy, nothing changes. ## When you need this You have more than one Instance (agent) under a single client and you want to decide who reads whose memories. For example: * A **support** agent and a **sales** agent under one client that should *not* read each other's conversation memory. * A **manager** agent that should see its **sub-agents'** memories, but not the other way around. * A shared **knowledge** Instance whose memories everyone may read, alongside private Instances that keep to themselves. If you only have one Instance, or you're happy with full sharing, you don't need this page; the default already does that. ## The default: everything shared Synap stores each memory tagged with the Instance that created it, but retrieval has historically ignored that tag, so any Instance under the same client + customer could retrieve any other's memories. That remains the behaviour until you create a policy. Concretely: * **No policy / "Shared" mode** → every Instance sees every other Instance's memories (today's behaviour, byte-for-byte). There is **zero** performance or result change for clients who never touch this feature. * **"Isolated" mode** → an Instance sees only itself, plus whatever you explicitly grant. Visibility applies to **customer-scoped** memories (the per-user/per-customer memories your agents create). **Client-scoped shared knowledge stays visible to everyone** regardless of policy; isolating Instances never hides your client-wide knowledge base. ## The one rule Under **Isolated** mode, an Instance **V** can read another Instance **S**'s memories **iff**: > **S is marked shareable** **AND** (**V → S is granted** **OR** **S is a > sub-agent (child) of V**). An Instance **always sees itself**. That's the whole model: a source-side opt-out (`shareable`), a viewer-side grant (the matrix), and an automatic parent → child edge. ## Setting it up on the dashboard Open **Dashboard → Visibility**. Toggle between **Shared** (default: everyone sees everyone) and **Isolated** (no one sees anyone unless granted). Switching to Shared greys out the grid; nothing below it applies until you switch back to Isolated. In Isolated mode you get a grid. **Rows are viewers, columns are sources.** Tick the cell at *(row V, column S)* to let **V read S's memories**. Grants are **directional**: ticking V→S does **not** let S read V. The diagonal (an Instance seeing itself) is always on and not editable. Each Instance has a **Shareable** toggle. Turning it **off** makes that Instance's memories invisible to *everyone else*, even Instances you've granted. It's a source-side veto that overrides any grant, so it's the safe way to quarantine one Instance's memory without editing the matrix. If an Instance is registered as a **sub-agent** of another (a parent-child link), the parent automatically sees the child's memories. Those cells show pre-checked and **locked** in the matrix; they come from the agent topology, not the grid, so you can't un-tick them here. (The child does *not* automatically see the parent.) Click **Save**. Changes take effect **immediately** for new retrievals. Saving is protected against concurrent edits: if someone else changed the policy since you loaded the page, you'll get a "policy changed, please reload" prompt instead of silently clobbering their edit. Only **Owner** and **Admin** dashboard roles can edit; others see the matrix read-only. ## Good to know * **Safe by default.** Doing nothing keeps full sharing. Isolation is something you switch on deliberately. * **Same customer.** Visibility decides sharing *between Instances of the same customer*. It does not open memory across different customers; customer isolation is unchanged and still absolute. * **It filters, it doesn't delete.** Making S unshareable or revoking a grant hides S's memories from a retrieval; the memories still exist and become visible again the moment you restore the grant or flip back to Shared. * **Tightening propagates fast.** When you remove access, the change applies to the next retrieval; there's no stale window where a just-revoked Instance can still read. Instance Visibility governs **what is returned at retrieval (read-side)**. It does not yet coordinate the **write-side**, e.g. propagating a correction from one Instance to another that can see it. That coordination is a separate, planned capability; today, isolating Instances only affects which memories a fetch returns. # Multi-User Memory Scoping Source: https://docs.maximem.ai/guides/multi-user-scoping Most real-world applications serve multiple users, often across multiple organizations. Synap's scoping system ensures that memories are properly isolated while still enabling shared context where appropriate. This guide explains the scope hierarchy, walks through common patterns, and shows you how to configure scoping for your application. ## The Problem Consider a SaaS application with an AI assistant. You need to handle several competing requirements: * **User A** should not see **User B**'s personal memories * Users within the same **organization** should share some common context (e.g., company policies, project details) * Your **application** has global knowledge that all users should benefit from (e.g., product documentation, feature capabilities) * All of this needs to work without manual access control lists or complex permission logic Synap solves this with a hierarchical scope system that handles isolation and merging automatically. *** ## The Scope Hierarchy Synap organizes memories into four nested scopes. Each scope is a superset of the one above it: Scope hierarchy: USER -> CUSTOMER -> CLIENT -> WORLD | Scope | Isolation Level | Contains | Example | | ------------ | ---------------- | -------------------------------------------- | -------------------------------------------- | | **USER** | Per-individual | Memories specific to one person | "Alice prefers dark mode" | | **CUSTOMER** | Per-organization | Memories shared across users in one org | "Acme Corp uses Kubernetes for deployment" | | **CLIENT** | Per-application | Memories shared across all users of your app | "Our product supports SSO via SAML and OIDC" | | **WORLD** | Global | Memories shared across all Synap instances | General knowledge (managed by Synap) | ### How Scope Isolation Works When you ingest a memory, Synap assigns it to a scope based on the identifiers you provide: * Pass `user_id`: memory is stored at **USER** scope * Pass `customer_id` (without `user_id`): memory is stored at **CUSTOMER** scope * Pass neither: memory is stored at **CLIENT** scope When you retrieve context, Synap merges memories from the narrowest applicable scope upward; the chain is resolved **server-side**: ``` Retrieval for user_id="user_alice" (server resolves customer_id="cust_acme" from ingestion): USER (user_alice) ← Alice's personal memories + CUSTOMER (cust_acme) ← Acme Corp's shared memories + CLIENT ← Your application's shared memories + WORLD ← General knowledge ───────────────────────── = Merged context (user memories take priority on conflicts) ``` The merge happens because the user's `customer_id` was set at **ingestion time** and stored server-side. On retrieval, Synap looks up that association and walks the chain for you. You can also pass `customer_id` explicitly on `sdk.user.context.fetch(user_id=..., customer_id=...)` for B2B instances when you want to assert the customer association at query time. When memories at different scopes conflict (e.g., a user preference contradicts a customer-level default), the **narrower scope wins**. User-level memories always override customer-level, which override client-level. *** ## Setting Up User Scope User scope is the most common isolation boundary. Every time you ingest a memory that belongs to a specific individual, pass their `user_id`. ```python Python theme={null} # Ingest a memory scoped to a specific user await sdk.memories.create( document="User: I prefer to communicate in Spanish.\nAssistant: Got it! I'll respond in Spanish from now on.", document_type="ai-chat-conversation", user_id="user_alice", customer_id="cust_acme", mode="fast" ) ``` ```typescript TypeScript theme={null} await sdk.memories.create(options: CreateMemoryOptions) ``` To retrieve memories for that user: ```python Python theme={null} # Retrieve context scoped to a specific user # This returns: user_alice memories + cust_acme memories + client memories context = await sdk.user.context.fetch( user_id="user_alice", customer_id="cust_acme" ) for fact in context.facts: print(f"- {fact.content}") # Output: # - Prefers communication in Spanish # - Acme Corp uses Slack for internal communication # - Product supports 12 languages including Spanish ``` ```javascript JavaScript theme={null} // Retrieve context scoped to a specific user // This returns: user_alice memories + cust_acme memories + client memories const context = await sdk.user.context.fetch({ user_id: 'user_alice', customer_id: 'cust_acme', }); for (const fact of context.facts ?? []) { console.log(`- ${fact.content}`); } // Output: // - Prefers communication in Spanish // - Acme Corp uses Slack for internal communication // - Product supports 12 languages including Spanish ``` ```typescript TypeScript theme={null} // Retrieve context scoped to a specific user // This returns: user_alice memories + cust_acme memories + client memories const context = await sdk.user.context.fetch({ user_id: 'user_alice', customer_id: 'cust_acme', }); for (const fact of context.facts ?? []) { console.log(`- ${fact.content}`); } // Output: // - Prefers communication in Spanish // - Acme Corp uses Slack for internal communication // - Product supports 12 languages including Spanish ``` `ContextResponse.facts` returns a merged, ranked list: the server has already walked the USER → CUSTOMER → CLIENT chain for you. If you need per-fact scope attribution (e.g., to render USER-scoped memories with different visual treatment), call `sdk.fetch(...)` instead and read `UnifiedContextResponse.scope_map` on the cross-scope unified response. *** ## Setting Up Customer Scope Customer scope represents an organization, team, or account. Memories at this scope are shared across all users within that customer. ```python Python theme={null} # Ingest a memory at customer scope # Note: passing customer_id WITHOUT user_id stores at CUSTOMER scope await sdk.memories.create( document="Acme Corp's fiscal year ends in March. All Q4 reports are due by March 15.", document_type="document", customer_id="cust_acme", mode="fast", metadata={"source": "company-policy-doc"} ) ``` ```typescript TypeScript theme={null} await sdk.memories.create(options: CreateMemoryOptions) ``` Any user within `cust_acme` will see this memory in their context: ```python Python theme={null} # Alice sees Acme's memories alice_ctx = await sdk.user.context.fetch( user_id="user_alice", customer_id="cust_acme" ) # Bob also sees the same Acme memories (plus his own user memories) bob_ctx = await sdk.user.context.fetch( user_id="user_bob", customer_id="cust_acme" ) ``` ```javascript JavaScript theme={null} // Alice sees Acme's memories const alice_ctx = await sdk.user.context.fetch({ user_id: 'user_alice', customer_id: 'cust_acme', }); // Bob also sees the same Acme memories (plus his own user memories) const bob_ctx = await sdk.user.context.fetch({ user_id: 'user_bob', customer_id: 'cust_acme', }); ``` ```typescript TypeScript theme={null} // Alice sees Acme's memories const alice_ctx = await sdk.user.context.fetch({ user_id: 'user_alice', customer_id: 'cust_acme', }); // Bob also sees the same Acme memories (plus his own user memories) const bob_ctx = await sdk.user.context.fetch({ user_id: 'user_bob', customer_id: 'cust_acme', }); ``` You can also retrieve customer-level context without specifying a user: ```python Python theme={null} # Retrieve ONLY customer-scoped memories (no user-specific memories) customer_ctx = await sdk.customer.context.fetch( customer_id="cust_acme" ) ``` ```javascript JavaScript theme={null} // Retrieve ONLY customer-scoped memories (no user-specific memories) const customer_ctx = await sdk.customer.context.fetch({ customer_id: 'cust_acme', }); ``` ```typescript TypeScript theme={null} // Retrieve ONLY customer-scoped memories (no user-specific memories) const customer_ctx = await sdk.customer.context.fetch({ customer_id: 'cust_acme', }); ``` Customer scope is ideal for ingesting organizational knowledge: company policies, team structures, project details, shared preferences, and onboarding materials. Ingest these documents once and all users in that organization benefit. *** ## Setting Up Client Scope Client scope represents your entire application. Memories at this scope are visible to every user across all customers. ```python Python theme={null} # Ingest a memory at client scope # Note: no user_id or customer_id, stores at CLIENT scope await sdk.memories.create( document="Our product supports SSO via SAML 2.0 and OIDC. Configuration is available in Settings > Security > SSO.", document_type="document", mode="fast", metadata={"source": "product-docs", "version": "2.4"} ) ``` ```typescript TypeScript theme={null} await sdk.memories.create(options: CreateMemoryOptions) ``` Client-scoped memories appear in every user's context, regardless of their customer: ```python Python theme={null} # Alice at Acme sees client memories alice_ctx = await sdk.user.context.fetch( user_id="user_alice", customer_id="cust_acme" ) # Charlie at a completely different customer also sees client memories charlie_ctx = await sdk.user.context.fetch( user_id="user_charlie", customer_id="cust_globex" ) ``` ```javascript JavaScript theme={null} // Alice at Acme sees client memories const alice_ctx = await sdk.user.context.fetch({ user_id: 'user_alice', customer_id: 'cust_acme', }); // Charlie at a completely different customer also sees client memories const charlie_ctx = await sdk.user.context.fetch({ user_id: 'user_charlie', customer_id: 'cust_globex', }); ``` ```typescript TypeScript theme={null} // Alice at Acme sees client memories const alice_ctx = await sdk.user.context.fetch({ user_id: 'user_alice', customer_id: 'cust_acme', }); // Charlie at a completely different customer also sees client memories const charlie_ctx = await sdk.user.context.fetch({ user_id: 'user_charlie', customer_id: 'cust_globex', }); ``` You can retrieve only client-scoped context: ```python Python theme={null} # Retrieve ONLY client-scoped memories client_ctx = await sdk.client.context.fetch() ``` ```javascript JavaScript theme={null} // Retrieve ONLY client-scoped memories const client_ctx = await sdk.client.context.fetch(); ``` ```typescript TypeScript theme={null} // Retrieve ONLY client-scoped memories const client_ctx = await sdk.client.context.fetch(); ``` Client scope is useful for ingesting your product documentation, feature announcements, FAQ content, and any other knowledge that should be available to all users of your application. *** ## Example: SaaS Project Management Tool Let's walk through a complete example. You are building an AI assistant for a project management tool. The assistant helps team members with tasks, deadlines, and project context. ### Defining Your Scope Strategy | Scope | What Goes Here | Examples | | ------------ | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | | **USER** | Individual preferences, personal task notes, communication style | "Alice prefers Kanban boards", "Bob's standup is at 9am PST" | | **CUSTOMER** | Company processes, team structure, project details | "Sprint reviews are every other Friday", "The API team reports to Dana" | | **CLIENT** | Product capabilities, feature documentation, best practices | "You can create custom fields in Settings > Fields", "Keyboard shortcut: Cmd+K for quick search" | ### Ingestion Code ```python Python theme={null} # --- User-scoped: Alice's personal preferences --- await sdk.memories.create( document=( "User: Can you show tasks in a Kanban view by default?\n" "Assistant: Sure! I've noted your preference for Kanban boards." ), document_type="ai-chat-conversation", user_id="user_alice", customer_id="cust_acme", mode="fast" ) # --- Customer-scoped: Acme's team processes --- await sdk.memories.create( document=( "Acme Engineering Team Processes:\n" "- Sprint duration: 2 weeks\n" "- Sprint reviews: Every other Friday at 2pm PT\n" "- Definition of Done: Code reviewed, tests passing, docs updated\n" "- Escalation path: Team Lead → Engineering Manager → VP Engineering" ), document_type="document", customer_id="cust_acme", mode="fast", metadata={"source": "team-handbook", "department": "engineering"} ) # --- Client-scoped: Product documentation --- await sdk.memories.create( document=( "TaskFlow Pro Features:\n" "- Custom fields: Create custom fields in Settings > Fields\n" "- Automations: Set up workflow automations in Settings > Automations\n" "- Integrations: Connect Slack, GitHub, and Jira in Settings > Integrations\n" "- Keyboard shortcuts: Cmd+K for quick search, Cmd+N for new task" ), document_type="document", mode="fast", metadata={"source": "product-docs", "version": "3.1"} ) ``` ```typescript TypeScript theme={null} await sdk.memories.create(options: CreateMemoryOptions) ``` ### Retrieval Code ```python Python theme={null} # Alice asks about sprint schedules context = await sdk.conversation.context.fetch( conversation_id="conv_alice_001", search_query=["when is the next sprint review?"], max_results=5, mode="fast" ) # The merged context will contain: # - Alice prefers Kanban boards (may not be relevant to this query) # - Sprint reviews: Every other Friday at 2pm PT ← Most relevant # - TaskFlow Pro feature info (if relevant) # Build the prompt from the merged, ranked list memory_lines = [] for fact in context.facts: memory_lines.append(f"- {fact.content}") print("\n".join(memory_lines)) # - Acme sprint reviews are every other Friday at 2pm PT # - Sprint duration is 2 weeks # - Alice prefers Kanban board view ``` ```javascript JavaScript theme={null} // Alice asks about sprint schedules const context = await sdk.conversation.context.fetch({ conversation_id: 'conv_alice_001', search_query: ['when is the next sprint review?'], max_results: 5, mode: 'fast', }); // The merged context will contain: // - Alice prefers Kanban boards (may not be relevant to this query) // - Sprint reviews: Every other Friday at 2pm PT ← Most relevant // - TaskFlow Pro feature info (if relevant) // Build the prompt from the merged, ranked list const memory_lines = []; for (const fact of context.facts ?? []) { memory_lines.push(`- ${fact.content}`); } console.log("\n".join(memory_lines)); // - Acme sprint reviews are every other Friday at 2pm PT // - Sprint duration is 2 weeks // - Alice prefers Kanban board view ``` ```typescript TypeScript theme={null} // Alice asks about sprint schedules const context = await sdk.conversation.context.fetch({ conversation_id: 'conv_alice_001', search_query: ['when is the next sprint review?'], max_results: 5, mode: 'fast', }); // The merged context will contain: // - Alice prefers Kanban boards (may not be relevant to this query) // - Sprint reviews: Every other Friday at 2pm PT ← Most relevant // - TaskFlow Pro feature info (if relevant) // Build the prompt from the merged, ranked list const memory_lines = []; for (const fact of context.facts ?? []) { memory_lines.push(`- ${fact.content}`); } console.log("\n".join(memory_lines)); // - Acme sprint reviews are every other Friday at 2pm PT // - Sprint duration is 2 weeks // - Alice prefers Kanban board view ``` If you need to render USER-scoped memories differently from CUSTOMER-scoped ones in the UI, use `sdk.fetch(...)`: its `UnifiedContextResponse.scope_map` carries per-fact scope attribution. Single-scope `ContextResponse` does not. *** ## Example: Consumer Mobile App For simpler consumer applications without an organization concept, the scoping model is straightforward. ### Defining Your Scope Strategy | Scope | What Goes Here | | ------------ | ------------------------------------------------- | | **USER** | Everything specific to the individual consumer | | **CUSTOMER** | Not used, skip this scope entirely | | **CLIENT** | App-wide knowledge (tips, features, general info) | ### Ingestion Code ```python Python theme={null} # User-scoped: Individual consumer context await sdk.memories.create( document=( "User: I'm vegetarian and allergic to nuts.\n" "Assistant: I've noted your dietary restrictions. " "I'll make sure all recipe suggestions are vegetarian and nut-free." ), document_type="ai-chat-conversation", user_id="user_maria", mode="fast" ) # Client-scoped: App-wide knowledge await sdk.memories.create( document=( "RecipeBot supports the following dietary filters: " "vegetarian, vegan, gluten-free, dairy-free, nut-free, keto, paleo. " "Users can combine multiple filters." ), document_type="document", mode="fast" ) ``` ```typescript TypeScript theme={null} await sdk.memories.create(options: CreateMemoryOptions) ``` ### Retrieval Code ```python Python theme={null} # Retrieve context for Maria, no customer_id needed context = await sdk.user.context.fetch( user_id="user_maria" ) # Returns a merged, ranked list across USER + CLIENT scopes, e.g.: # - Maria is vegetarian and allergic to nuts # - RecipeBot supports vegetarian, nut-free, and other dietary filters ``` ```javascript JavaScript theme={null} // Retrieve context for Maria, no customer_id needed const context = await sdk.user.context.fetch({ user_id: 'user_maria', }); // Returns a merged, ranked list across USER + CLIENT scopes, e.g.: // - Maria is vegetarian and allergic to nuts // - RecipeBot supports vegetarian, nut-free, and other dietary filters ``` ```typescript TypeScript theme={null} // Retrieve context for Maria, no customer_id needed const context = await sdk.user.context.fetch({ user_id: 'user_maria', }); // Returns a merged, ranked list across USER + CLIENT scopes, e.g.: // - Maria is vegetarian and allergic to nuts // - RecipeBot supports vegetarian, nut-free, and other dietary filters ``` If your application is single-user (e.g., a local desktop AI assistant), you can skip both customer and user scopes. Just use a single fixed `user_id` for all memories, or let everything land at client scope. *** ## Primary scope: what Synap optimizes for Each Instance has a **primary scope**: the level Synap optimizes indexing, caching, and retrieval for. It is chosen automatically based on your [use-case file](/concepts/memory-architecture#the-use-case-file): | Optimized for | Used when | | ---------------------- | ---------------------------------------------------------------------- | | Per-user retrieval | Most applications, where each user gets personalized context | | Per-customer retrieval | Enterprise apps where team-level context is the primary access pattern | | Per-client retrieval | Knowledge bases, single-user agents, shared-context tools | If your agent's primary audience changes (e.g., from per-user personalization to per-team collaboration), re-upload your use-case file. Synap will re-evaluate and update the Instance. Existing memories keep their original scope assignment; new memories follow the updated behavior. *** ## Testing Scoped Access When developing, verify that scope isolation works correctly by testing cross-scope access patterns: ```python theme={null} import asyncio async def verify_scope_isolation(sdk): """Verify that user memories are properly isolated.""" # Ingest a secret for Alice await sdk.memories.create( document="User: My password recovery email is alice@personal.com", document_type="ai-chat-conversation", user_id="user_alice", customer_id="cust_acme", mode="fast" ) # Wait for ingestion to process await asyncio.sleep(3) # Retrieve as Bob. Alice's secret should NOT appear bob_ctx = await sdk.user.context.fetch( user_id="user_bob", customer_id="cust_acme" ) alice_facts = [ f for f in bob_ctx.facts if "alice@personal.com" in f.content.lower() ] assert len(alice_facts) == 0, ( "ISOLATION FAILURE: Bob can see Alice's user-scoped memories!" ) print("Scope isolation verified: Bob cannot see Alice's memories.") # Retrieve as Alice. Her own memory SHOULD appear alice_ctx = await sdk.user.context.fetch( user_id="user_alice", customer_id="cust_acme" ) alice_facts = [ f for f in alice_ctx.facts if "alice@personal.com" in f.content.lower() ] assert len(alice_facts) > 0, ( "RETRIEVAL FAILURE: Alice cannot see her own memories!" ) print("Scope access verified: Alice can see her own memories.") ``` In production, Synap's PII handling would redact or mask the email address before storage. This test uses raw content for simplicity. *** ## Scope Decision Flowchart Use this flowchart to decide which scope identifiers to pass when ingesting memories: **Yes.** Pass `user_id` (and `customer_id` if the user belongs to an organization). **No.** Continue to the next question. **Yes.** Pass `customer_id` only (no `user_id`). **No.** Continue to the next question. **Yes.** Pass neither `user_id` nor `customer_id`. It will be stored at client scope. **No.** This is likely general knowledge. Store at client scope or consider whether it should be ingested at all. *** ## Best Practices Establish a convention for `user_id` and `customer_id` values and enforce it across your application. Inconsistent IDs (e.g., `"alice"` vs `"user_alice"` vs `"user-alice"`) create fragmented memory silos. Recommended: prefix-based IDs like `user_` and `cust_`. Even if you primarily use user scope, always pass `customer_id` alongside `user_id` during ingestion and retrieval. This ensures customer-scoped memories are properly accessible and the scope hierarchy works correctly. ```python Python theme={null} # Good: both identifiers await sdk.memories.create( document=conversation, document_type="ai-chat-conversation", user_id="user_alice", customer_id="cust_acme", # Always include when available mode="fast" ) # Less ideal: missing customer_id await sdk.memories.create( document=conversation, document_type="ai-chat-conversation", user_id="user_alice", # customer_id omitted, customer-scoped memories won't merge mode="fast" ) ``` ```typescript TypeScript theme={null} await sdk.memories.create(options: CreateMemoryOptions) ``` A common mistake is ingesting organizational knowledge at user scope (by including a `user_id`). This makes the knowledge invisible to other users in the same organization. Ingest shared content at customer or client scope explicitly. Do not wait until production to test multi-user scenarios. Set up at least two test users and one test customer during development and verify isolation before deploying. *** ## Next Steps The use-case file is how you tell Synap what to optimize scope behavior for. Deeper conceptual explanation of the scope hierarchy and conflict resolution. Learn how entities are resolved and shared across scopes. Ensure your multi-tenant setup is production-ready. # Sensitive Data Protection Source: https://docs.maximem.ai/guides/pii-protection Synap finds sensitive values in the content you send, and you decide what happens to each kind at each destination. Sometimes called PII protection. This page covers the eleven categories, the six settings, and the short list of field types nobody is allowed to keep. **Start here: nothing changes for your application unless you choose it.** For any field type you protect, your own API keys receive the real value by default, so the text your app reads back is identical to what it reads today. Request and response shapes do not change. There is no new SDK call to make, and no code to write. Two settings do change what your app receives, and both are yours to pick: **Do not store it**, and **Protect from everyone**. Nothing else alters your reads. ## What this feature is Synap reads the content you send. Some of it is sensitive: an Aadhaar number in a support transcript, a card number a customer read out on a call, an API key somebody pasted into a chat. Two things are true about that content at once. You need it, because answering the customer's next question depends on remembering the conversation. And keeping it is a liability, because it sits in a memory store, gets embedded for search, and passes through a language model on the way in. Sensitive data protection separates those two. Synap finds the value, replaces it with a stable placeholder everywhere it would come to rest, and hands the real value back to your application when it asks. Your product behaves the same. What we hold on disk does not contain the value. You decide which kinds of value get that treatment. The decision is made once, in the dashboard, per category. *** ## The three things that can be true These are separate, and mixing them up is the easiest way to misread the feature. Synap runs its detection over every document and records what it found: which field types, how many times, across how many of your users. Nothing is changed, nothing is replaced, and your memories are byte for byte what they would have been. This is what every account gets before anyone configures anything. It is not a degraded state. It is how you find out what is actually in your traffic before you make a decision about it. A short list of field types is never kept, whatever your settings say. Card numbers, card security codes, card PINs, passwords, API keys and other secret codes, private keys, and raw biometric data. There is no setting for these. They are not one of your eleven choices, and no preset, exception, or support request turns them into one. Once you have set a policy and approved it, Synap applies it to everything ingested from that point on. Each category gets one choice, and that choice decides what the model sees, what we store, what your application receives, and what our staff can see. A policy is inert until a person approves it. Saving a draft changes nothing. *** ## The eleven categories Field types are grouped into eleven categories so you configure eleven things rather than a long list of individual formats. The grouping is for navigation. Nothing about detection depends on it. | Category | Field types Synap detects today | | ---------------------- | -------------------------------------------------------------------------------------------- | | **Raw Identity** | *(nothing shipped yet, see below)* | | **Contact Info** | Email address, phone number | | **Formal Identity** | Aadhaar number, PAN, passport number, voter ID, driving licence, vehicle registration, GSTIN | | **Payment Info** | Card number, card security code, bank account number, IFSC code, UPI ID | | **Health** | *(nothing shipped yet, see below)* | | **Location (coarse)** | PIN code | | **Location (fine)** | *(nothing shipped yet, see below)* | | **Device and Online** | IP address, MAC address | | **Secrets** | Card PIN, password, API key or secret code, private key | | **Origin and Beliefs** | *(nothing shipped yet, see below)* | | **Biometric** | Raw biometric data (see the limits below) | **Four categories have no shipped detection yet: Raw Identity, Health, Location (fine), and Origin and Beliefs.** You can set a policy on them, and the setting is stored and applied. But Synap ships no detector for names, street addresses, health conditions, or origin and belief data, so under a policy alone nothing in those categories is currently found and nothing is replaced. Statistical detection for names and addresses is planned and is not built. If you need protection in one of these categories today, describe your own field type for it. See [Your own field types](/guides/custom-field-types). You can add field types Synap does not ship, put them in whichever category you think they belong to, and move them later. An asset tag, a policy number, an order id: you know your own data better than we do. *** ## The six settings Each category gets one choice. Three of them are offered on the row itself; the other three sit behind an **advanced** link for the minority who need them. | Setting | The model sees | We store | Your app gets | Our staff sees | | ----------------------- | -------------- | ------------- | ------------------ | -------------- | | **Not sensitive** | real value | real value | real value | real value | | **Hide from the model** | a placeholder | real value | real value | a placeholder | | **Protect at rest** | a placeholder | a placeholder | real value | a placeholder | | **Protect fully** | a placeholder | a placeholder | a placeholder | a placeholder | | **Do not store it** | a description | nothing | nothing | nothing | | **Your own vault** | a placeholder | a placeholder | your vault answers | a placeholder | The three offered up front are **Not sensitive** ("keep it"), **Protect at rest** ("protect it"), and **Do not store it**. **Protect at rest** is the one most clients want: the value is not in our store, and your application still reads the real thing. Logs and telemetry are deliberately not a column in that table. They always receive the most restricted treatment your chosen setting allows, and there is no setting for them, because nobody has ever wanted a real value in a log line. **Your own vault** is not available yet. The setting exists and a policy can carry it, but the outbound path to a customer-operated endpoint is a separate track that starts when a client asks for it. Until then, treat it as equivalent to **Protect fully** and talk to us before selecting it. ### What a placeholder is A placeholder, which the product calls an **alias**, looks like this: ```text theme={null} [[PERSON_AADHAAR_h2n7v5cx8m0d]] ``` The same value always becomes the same alias, so deduplication, corrections, and search keep working on a consistent string. Search for the real value and Synap matches the memory that stores its alias. See [Aliases](/concepts/aliases) for what your application receives and why search still works. ### Presets Rather than starting from eleven empty dropdowns, you can apply one of three starting points, then adjust. | Preset | What it does | Who it suits | | ------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | | **Minimal** | Keeps everything except the things nobody may keep. | Products where the personal detail is the point, such as a companion app. | | **Standard** | Protects identifiers and payment details. Your app still gets real values. | Most products. Start here if you are not sure. | | **Regulated** | Protects everything that identifies a person, and drops what you must not keep. | Health, finance, insurance, and anyone answering to an auditor. | A preset is a starting point, not an answer. It lands as a draft and still needs approving. *** ## The floor Seven field types are never stored, for any account, in any industry, whatever the settings say: * Full card number * Card security code * Card PIN * Passwords * API keys and other secret codes * Private keys * Raw biometric data No setting on your account changes this. It is not a category-level rule, so it does not drag the rest of the category with it: a bank account number sits in Payment Info and plenty of products legitimately keep one. When one of these appears, the value is replaced with a short description of what was taken, such as `[a card number was given and not kept]`, and the record that it happened is kept. Nothing is stored that the value could be recovered from. ### What happens to the memory around it If a value never reaches the model, the model will sometimes still write a memory about the empty space. "Their card number is a card number" answers no question, takes up room, and reads as though we hold the number somewhere. The rule Synap applies is simple: **keep a memory if it records something happening, drop it only if it does nothing but assert a value we chose not to keep.** "Requested an address change on 12 August" survives, because the event is the useful part and the address was only one attribute of it. Suppressed memories are counted and shown on your dashboard with the reason, alongside a sample of what was kept, so a working feature does not look like data loss. *** ## What detection can and cannot do Synap does not claim perfect detection, and the strength of the promise differs by field type. This matters more than a single accuracy number would. | How a field type is found | What that means | Examples | | ------------------------- | ---------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | | **Check digit** | The value carries a checksum that has to pass. A near miss is rejected rather than guessed at. | Aadhaar, card number, GSTIN | | **Fixed format** | The value has a shape distinctive enough to match on its own. | PAN, IFSC, email, IP address, private key | | **Needs context** | The shape alone is too common, so a nearby word has to confirm it. | Card security code, card PIN, bank account number, PIN code | | **Statistical** | Found by judgement rather than by shape. | Names, street addresses. **Not shipped.** | Measured precision and recall are published per field type rather than claimed as one number. See [Security and Trust](/resources/security-trust#sensitive-data-detection-measured) for the current figures and the size of the corpus they were measured on. ### Known limits, stated up front * **Names and street addresses are not detected.** There is no detector for either. Do not plan around them being found. * **Health, origin and belief data are not detected.** The categories exist and accept a setting; no field type ships in them. * **Biometric detection is text only.** Synap finds an encoded biometric template when it appears in text next to a word that labels it, such as "fingerprint template" or "iris minutiae". An unlabelled binary attachment is not text and is not covered. * **Images, audio, and scanned documents are not covered.** Detection runs over text. * **Voice transcripts are covered for four field types.** Numbers dictated as words ("nine eight seven six five...") are reconstructed for Aadhaar, card number, phone, and PIN code, including Hindi digits and corrections mid-number. Other field types are not recovered from speech. * **The shipped identifier list is largely India-first.** Aadhaar, PAN, GSTIN, IFSC, UPI, voter ID, and vehicle registration are Indian formats. Email, phone, card number, IP and MAC address, passwords, API keys, and private keys are not region-specific. Phone number detection assumes an Indian number by default. * **Data ingested before you turned this on is unaffected.** It stays exactly as it is, in plain text. Policy applies forward only. *** ## Turning it on Everything is configured on the **Sensitive data** page in the dashboard. See [Sensitive data and data controls](/dashboard/pii-and-data-controls) for the step-by-step, including the test box that shows you what would happen to sample text before you commit to anything. ## Next steps The step-by-step, the test box, and how a policy is approved. What your application receives, and why search still works. Describe a field type Synap does not ship, and pick its category. The questions engineering teams and security reviewers actually ask. # Production Checklist Source: https://docs.maximem.ai/guides/production-checklist This checklist covers every aspect of a production-ready Synap integration, from security and SDK configuration to monitoring and operational procedures. Work through each section before your first production deployment, and revisit it before subsequent releases. Run through this checklist before **every** production deployment, not just the first one. Configuration changes, SDK upgrades, and new features each warrant a fresh review. *** ## Authentication and Security Credential management is the foundation of a secure Synap integration. A compromised API key gives an attacker full access to your instance's memory store. Never hardcode API keys in source code, environment files committed to version control, or Docker images. Use a proper secrets manager: * **AWS**: Secrets Manager or SSM Parameter Store * **GCP**: Secret Manager * **Azure**: Key Vault * **Self-hosted**: HashiCorp Vault ```python theme={null} # Good: loaded from secrets manager at runtime import boto3 def get_api_key(): client = boto3.client("secretsmanager") response = client.get_secret_value(SecretId="synap/api-key") return response["SecretString"] sdk = MaximemSynapSDK( api_key=get_api_key() ) ``` API key is stored in a secrets manager (not in code, `.env` files, or container images) If you receive webhooks from Synap, **always** verify the signature before processing the payload. Unverified webhooks can be spoofed by attackers. ```python theme={null} import hmac import hashlib def verify_webhook_signature(payload: bytes, signature: str, secret: str) -> bool: expected = hmac.new( secret.encode(), payload, hashlib.sha256 ).hexdigest() return hmac.compare_digest(f"sha256={expected}", signature) ``` Webhook signature verification is implemented and tested API keys should be rotated periodically. You can have multiple active keys per instance, so rotation is zero-downtime: generate a new key, roll it out, then revoke the old one. * Recommended rotation cadence: every 90 days for standard deployments, every 30 days for high-security environments * Document the rotation procedure in your team's runbook * Automate rotation if possible (e.g., via a cron job or CI/CD step) A long-running process needs one extra step. An SDK keeps the credential it was constructed with for its whole life, so rolling the new key into your secrets manager is not by itself enough to make a live process use it. If you construct with `MaximemSynapSDK(api_key=...)`, the new key produces a new SDK and takes effect immediately. If you construct with an explicit `instance_id`, that id is the identity and you get the existing SDK back (still on the old key), so `await sdk.shutdown()` before reconstructing, or restart the worker. Do that **before** revoking the old key, or in-flight requests will start failing authentication. API key rotation schedule is established and documented The runbook says how running processes pick up the new key (reconstruct after `shutdown()`, or restart), and that this happens before the old key is revoked If a single process ever holds more than one Synap API key (a key per customer or tenant, staging and production side by side, or a worker that switches keys between jobs) pin `maximem-synap` **≥ 0.4.1**. On 0.4.0 and earlier, the second and later SDKs constructed in one process silently adopted the first one's credentials, so their reads returned the first key's memory and their writes were committed against that instance. It produced no error and no log line. A process that uses a single API key was never affected. ```bash theme={null} pip install --upgrade "maximem-synap>=0.4.2" ``` `maximem-synap` is pinned to ≥ 0.4.1 (≥ 0.4.2 recommended) if any process uses more than one API key *** ## SDK Configuration Proper SDK configuration ensures your integration performs well under production load and does not generate excessive logging or resource usage. In production, set `log_level` to `"WARNING"` or `"ERROR"`. The `"DEBUG"` and `"INFO"` levels generate high-volume output that degrades performance and can expose sensitive information in log aggregators. ```python theme={null} config = SDKConfig( log_level="WARNING" # Not "DEBUG" or "INFO" in production ) ``` `log_level` is set to `"WARNING"` or `"ERROR"` (not `"DEBUG"` or `"INFO"`) Default timeouts are suitable for most applications, but review them against your latency requirements: | Timeout | Default | Guidance | | --------- | ------- | ------------------------------------------------------------------------- | | `connect` | 5s | Increase to 10s if your infrastructure has high network latency | | `read` | 30s | Decrease for latency-sensitive paths; increase for large batch operations | | `write` | 10s | Usually sufficient; increase for large document ingestion | ```python theme={null} config = SDKConfig( timeouts=TimeoutConfig( connect=5.0, read=30.0, write=10.0 ) ) ``` Timeouts are reviewed and aligned with your application's SLA requirements The default retry policy (3 attempts, exponential backoff with jitter) is appropriate for most use cases. Adjust if needed: * **High-throughput systems**: Reduce `max_attempts` to 2 to avoid retry storms * **Critical operations**: Increase `max_attempts` to 5 for reliability * **Low-latency paths**: Reduce `backoff_max` to limit total retry time ```python theme={null} config = SDKConfig( retry_policy=RetryPolicy( max_attempts=3, backoff_base=1.0, backoff_max=10.0, backoff_jitter=True # Always enable jitter in production ) ) ``` Retry policy is reviewed and tuned for your workload profile The SQLite cache backend significantly improves retrieval performance for repeated queries. Ensure it is enabled: ```python theme={null} config = SDKConfig( cache_backend="sqlite" # Not None ) ``` `cache_backend` is set to `"sqlite"` for production performance The `session_timeout_minutes` setting controls how long an authenticated session lasts before requiring re-authentication. The default is appropriate for most cases, but adjust based on your security requirements: * **Standard applications**: 60-480 minutes (1-8 hours) * **High-security environments**: 5-30 minutes * **Long-running batch processes**: 720-1440 minutes (12-24 hours) `session_timeout_minutes` is configured appropriately (range: 5-1440) *** ## Memory Architecture Synap generates each Instance's memory configuration automatically from the use-case file you upload. Before going to production, make sure that file reflects the agent you are actually deploying. The use-case Markdown you uploaded at instance creation drives every memory decision: which categories are extracted, how scopes are partitioned, what retention behavior applies. Review it now and re-upload an updated version if the agent's purpose, audience, or compliance requirements have shifted since you created the Instance. Use-case file reflects the production agent's behavior, audience, and compliance posture Before going live, run a handful of representative production queries against the Instance and confirm the returned memories are relevant and complete. Catch retrieval drift before users do. Retrieval quality validated on at least 10 representative queries Check the Dashboard to confirm your Instance has moved from `provisioning` to `active` and that its memory architecture has been generated and applied. Do not start production traffic on an Instance that is still provisioning. Instance status is `active` and ready to accept traffic *** ## Sensitive Data Synap detects sensitive values in the content you send and applies the policy you set. Until somebody reviews and approves that policy, your account is watching only: values are counted and reported, and nothing is changed. Watching is a reasonable place to start, but it should be a decision rather than an oversight. Open **Sensitive data** in the Dashboard and read the **What we found** tab. It lists what has actually been detected in your traffic, by field type, with counts and how many of your users each appeared for. Do this before you set anything. The list tells you what decision you are actually making, and it routinely contains something the team did not expect to be there. The findings list has been reviewed by someone who knows what the data should contain Set a policy, either from a preset or row by row, then check it in the **Try it** tab with text that looks like your real content before you approve it. Nothing takes effect until approval, and approval records who did it and when. Confirm you know which of your choices affect your application. **Keep it**, **Protect at rest**, and **Hide from the model** all leave your reads unchanged. **Do not store it** and **Protect from everyone** do not. A policy is approved (not left as a draft), and the approver is recorded The team knows which chosen settings change what the application reads back, and the application has been tested against them The policy applies to the whole account by default. If one instance handles materially different data, such as a regulated workload alongside a general one, give that instance its own policy rather than making the account-wide setting stricter for everything. Policy scope (whole account, or per instance) is a deliberate choice An API key's grant can only narrow what your policy allows, never widen it. Internal dashboards, support tools, and analytics jobs usually need to read memories without reading values; give those a `masked` or `none` key rather than a full one. Allow up to about a minute for a grant change to take effect on live traffic. Any key used by a tool that does not need real values is restricted Export the **Activity** trail once and confirm the file opens in whatever your compliance team uses. Confirm the team knows that erasing a person is requested through **[privacy@maximem.ai](mailto:privacy@maximem.ai)** and is irreversible, and that the request needs an instance and a `user_id` or `customer_id`. Activity export has been produced and opened at least once The runbook names who requests an erasure, and what information the request must carry Detection has limits worth knowing before you rely on it: names and street addresses are not detected, health and origin/belief data are not detected, and images, audio, and scanned documents are not covered. See [Sensitive Data Protection](/guides/pii-protection#known-limits-stated-up-front). *** ## Error Handling Robust error handling ensures your application degrades gracefully when Synap encounters issues, rather than crashing or returning empty responses. Handle transient and permanent errors differently: ```python theme={null} from maximem_synap.errors import ( SynapError, NetworkTimeoutError, RateLimitError, ServiceUnavailableError, InvalidInputError, AuthenticationError, ) try: context = await sdk.conversation.context.fetch( conversation_id=conv_id, search_query=[query], mode="fast" ) except (NetworkTimeoutError, ServiceUnavailableError) as e: # Transient: retry or fall back to no-memory mode logger.warning( "Synap unavailable (transient), proceeding without memory: %s " "(correlation_id=%s)", e, e.correlation_id ) context = None except RateLimitError as e: # Transient: respect retry_after logger.warning( "Rate limited, retry after %s seconds (correlation_id=%s)", e.retry_after_seconds, e.correlation_id ) context = None except InvalidInputError as e: # Permanent: fix the request logger.error("Invalid request to Synap: %s", e) raise except AuthenticationError as e: # Permanent: credentials issue logger.critical("Synap auth failed: %s", e) raise ``` Error handling distinguishes between transient and permanent errors Every `SynapError` includes a `correlation_id` field. Always log it: this is the primary identifier Synap support uses to trace issues. ```python theme={null} except SynapError as e: logger.error( "Synap error: %s (correlation_id=%s)", e, e.correlation_id ) ``` All error logs include the `correlation_id` from the Synap error Your application should continue functioning when Synap is unavailable, just without memory context. This is the single most important resilience pattern. ```python theme={null} async def get_memory_context(sdk, conversation_id, query): """Retrieve memory context, returning None if unavailable.""" try: return await sdk.conversation.context.fetch( conversation_id=conversation_id, search_query=[query], max_results=5, mode="fast" ) except SynapError as e: logger.warning( "Memory retrieval failed, proceeding without context: %s", e ) return None # In your chat handler: context = await get_memory_context(sdk, conv_id, user_message) if context and context.facts: # Build enriched prompt with memories system_prompt = build_prompt_with_memories(context) else: # Fall back to generic prompt, your app still works system_prompt = build_generic_prompt() ``` Application continues working (without memory) when Synap is unavailable When you receive a `RateLimitError`, respect the `retry_after_seconds` field before retrying: ```python theme={null} except RateLimitError as e: await asyncio.sleep(e.retry_after_seconds) # Retry the operation ``` Rate limit errors are handled with proper backoff using `retry_after_seconds` *** ## Monitoring Observability is critical for understanding how your Synap integration performs in production and catching issues before they impact users. The Synap Dashboard provides real-time analytics for each instance: * API call volume and success rate * Memory counts by category and scope * Ingestion throughput and processing latency * Retrieval latency percentiles (P50, P95, P99) Establish a regular review cadence (at least weekly). Dashboard analytics overview showing API volume, memory counts, and latency Dashboard analytics are reviewed on a regular schedule Set up webhooks to receive notifications for important events: * `ingestion.failed`: ingestion pipeline errors * `credential.expiring`: credentials approaching expiration * `config.applied`: configuration changes * `retention.cleanup`: memory retention cleanup completed See [Dashboard Webhooks](/dashboard/webhooks) for setup instructions. Webhooks are configured for critical operational events Synap does not publish per-operation latency SLOs; real numbers depend on your MACA configuration, payload sizes, retrieval mode, network path, and traffic shape. **Measure your own staging baseline and set alert thresholds from that baseline.** Recommended approach: 1. Run a representative mix of `memories.create()`, `context.fetch()` (both `fast` and `accurate`), and `memories.batch_create()` against your staging Instance. 2. Record P50 / P95 / P99 for each operation over a representative window (at least 1 hour of realistic traffic). 3. Set production alert thresholds at a multiple of your staging P95 (e.g., 2-3× P95) so normal variance doesn't page you. 4. Re-baseline after any MACA change, SDK upgrade, or significant traffic-pattern shift. P95 latency baselines are established from your own staging measurements and alert thresholds are derived from those baselines Set up alerts in your monitoring system (Datadog, PagerDuty, CloudWatch, etc.) for: * Synap API error rate exceeding 1% over 5 minutes * Authentication failures (any occurrence) * Rate limit hits exceeding your expected threshold * Retrieval returning zero results when memories are expected Error rate alerts are configured in your monitoring platform If your Synap plan includes usage-based pricing, track your usage against budget: * API call volume (ingestion + retrieval) * Storage usage (vector + graph) * Bandwidth usage The Dashboard provides usage breakdowns on the billing page. Usage and cost tracking is enabled and reviewed regularly *** ## Performance Optimization ensures your integration meets latency requirements and minimizes unnecessary resource usage. Use `mode="fast"` for any operation in the critical path of user-facing requests. Reserve `mode="accurate"` for background tasks, research queries, or paths where the user is willing to wait. ```python Python theme={null} # Real-time chat: use fast mode # fast = single-pass vector + graph search context = await sdk.conversation.context.fetch( conversation_id=conv_id, search_query=[query], mode="fast" ) # Background analysis: use accurate mode # accurate = same vector + graph search, plus LLM subquery decomposition + reranking context = await sdk.conversation.context.fetch( conversation_id=conv_id, search_query=[query], mode="accurate" ) ``` ```javascript JavaScript theme={null} // Real-time chat: use fast mode // fast = single-pass vector + graph search let context = await sdk.conversation.context.fetch({ conversation_id: conv_id, search_query: [query], mode: 'fast', }); // Background analysis: use accurate mode // accurate = same vector + graph search, plus LLM subquery decomposition + reranking context = await sdk.conversation.context.fetch({ conversation_id: conv_id, search_query: [query], mode: 'accurate', }); ``` ```typescript TypeScript theme={null} // Real-time chat: use fast mode // fast = single-pass vector + graph search let context = await sdk.conversation.context.fetch({ conversation_id: conv_id, search_query: [query], mode: 'fast', }); // Background analysis: use accurate mode // accurate = same vector + graph search, plus LLM subquery decomposition + reranking context = await sdk.conversation.context.fetch({ conversation_id: conv_id, search_query: [query], mode: 'accurate', }); ``` Fast mode is used for all latency-sensitive code paths When ingesting multiple documents, use `batch_create()` instead of multiple `create()` calls: ```python theme={null} from maximem_synap import CreateMemoryRequest # Good: single batch call await sdk.memories.batch_create( documents=[ CreateMemoryRequest(document=doc1, document_type="document", user_id="user_1"), CreateMemoryRequest(document=doc2, document_type="email", user_id="user_1"), CreateMemoryRequest(document=doc3, document_type="pdf", user_id="user_2"), ], fail_fast=False # Continue processing even if one document fails ) # Avoid: N sequential calls for doc in documents: await sdk.memories.create(document=doc, ...) # Slower, more API calls ``` Batch ingestion is used for all bulk operations For conversations that span many turns, use context compaction to keep the context within your LLM's token budget: ```python Python theme={null} result = await sdk.conversation.context.compact( conversation_id=conv_id, strategy="adaptive", # Automatically adjusts compression level target_tokens=2000 ) compacted = await sdk.conversation.context.get_compacted( conversation_id=conv_id, format="structured" # Also valid: "narrative", "bullet_points" ) ``` ```javascript JavaScript theme={null} const result = await sdk.conversation.context.compact({ conversation_id: conv_id, strategy: 'adaptive', // Automatically adjusts compression level target_tokens: 2000, }); const compacted = await sdk.conversation.context.get_compacted({ conversation_id: conv_id, format: 'structured', // Also valid: "narrative", "bullet_points" }); ``` ```typescript TypeScript theme={null} const result = await sdk.conversation.context.compact({ conversation_id: conv_id, strategy: 'adaptive', // Automatically adjusts compression level target_tokens: 2000, }); const compacted = await sdk.conversation.context.get_compacted({ conversation_id: conv_id, format: 'structured', // Also valid: "narrative", "bullet_points" }); ``` | Strategy | Compression | Best For | | -------------- | --------------------------------------------------- | --------------------------------------------- | | `conservative` | Highest retention (approximate; varies by content) | Important conversations, legal/compliance | | `balanced` | Moderate retention (approximate; varies by content) | General use | | `aggressive` | Lowest retention (approximate; varies by content) | Very long conversations, cost optimization | | `adaptive` | Variable | Recommended default, adjusts based on content | Context compaction is configured for conversations that may exceed token budgets Verify the cache backend is active and functioning: ```python Python theme={null} stats = sdk.cache.stats() print(f"Cache entries: {stats['total_entries']}") print(f"Cache size: {stats['total_bytes']} bytes") print(f"Backends: {stats['backends']}") ``` ```javascript JavaScript theme={null} const stats = sdk.cache.stats(); console.log(`Cache entries: ${stats['total_entries']}`); console.log(`Cache size: ${stats['total_bytes']} bytes`); console.log(`Backends: ${stats['backends']}`); ``` ```typescript TypeScript theme={null} const stats = sdk.cache.stats(); console.log(`Cache entries: ${stats['total_entries']}`); console.log(`Cache size: ${stats['total_bytes']} bytes`); console.log(`Backends: ${stats['backends']}`); ``` `cache.stats()` is synchronous and returns a dict with `enabled`, `client_id`, `base_path`, `total_entries`, `total_bytes`, and per-backend stats under `backends`. If `enabled` is `False` or `total_entries` stays at 0 over time, your cache backend isn't engaged; check `SDKConfig.cache_backend`. Cache backend is enabled and accumulating entries *** ## Operational Readiness Beyond code and configuration, production readiness requires documented procedures and team alignment. In the Synap Dashboard, assign roles based on the principle of least privilege: | Role | Capabilities | Assign To | | ------------- | ----------------------------------------- | ------------------------- | | **Owner** | Full access, billing, delete instance | Engineering lead, CTO | | **Admin** | Config changes, key management, analytics | Senior engineers, DevOps | | **Developer** | Read analytics, view config (no changes) | All developers | | **Viewer** | Read-only Dashboard access | Product managers, support | Team members have appropriate roles (not everyone is Owner) Document the step-by-step procedure for rotating API keys: 1. Generate new key in Dashboard 2. Update secrets manager with new key 3. Deploy application with updated secret reference 4. Verify new key is working (check Dashboard for API calls) 5. Revoke old key after grace period (48 hours) Credential rotation runbook is documented and accessible to the operations team Ensure your team knows how to get help: * **Synap Documentation**: [docs.maximem.ai](https://docs.maximem.ai) * **Community Discord**: [discord.gg/synap](https://discord.gg/synap) * **GitHub Issues**: [github.com/maximem-ai/maximem\_synap\_sdk/issues](https://github.com/maximem-ai/maximem_synap_sdk/issues) * **Email Support**: [support@maximem.ai](mailto:support@maximem.ai) (include `correlation_id` in all reports) Support channels are documented and the team knows how to report issues *** ## Quick Summary Use this condensed checklist for quick pre-deployment reviews: | Area | Item | Status | | ------------------ | --------------------------------------------------------------------- | ------ | | **Security** | API key in secrets manager | | | **Security** | Webhook signatures verified | | | **Security** | API key rotation scheduled | | | **Security** | Running processes pick up a rotated key before the old one is revoked | | | **Security** | `maximem-synap` ≥ 0.4.1 if any process uses more than one API key | | | **SDK** | Log level set to WARNING/ERROR | | | **SDK** | Timeouts match SLA | | | **SDK** | Cache enabled (sqlite) | | | **Memory** | Use-case file matches the production agent | | | **Memory** | Retrieval quality validated on representative queries | | | **Memory** | Instance status is `active` | | | **Sensitive data** | Detection findings reviewed against real traffic | | | **Sensitive data** | Policy approved by a named person, not left as a draft | | | **Sensitive data** | App tested against any setting that changes what it reads back | | | **Sensitive data** | Restricted keys issued to tools that do not need real values | | | **Sensitive data** | Activity export produced once, erasure path documented | | | **Errors** | Transient vs permanent handling | | | **Errors** | Graceful degradation | | | **Errors** | correlation\_id in logs | | | **Monitoring** | Dashboard reviewed regularly | | | **Monitoring** | Webhooks for critical events | | | **Monitoring** | Latency alerts configured | | | **Performance** | Fast mode for user-facing paths | | | **Performance** | Batch ingestion for bulk ops | | | **Operations** | Roles assigned (least privilege) | | | **Operations** | Rotation + rollback runbooks | | *** ## Next Steps Mapping your existing memory system (Mem0, Zep, Letta, SuperMemory) to Synap. Deep dive into Dashboard analytics and monitoring capabilities. Complete reference for all Synap error types and handling patterns. Configure webhooks for real-time event notifications. # Maximem Synap Developer Documentation Source: https://docs.maximem.ai/index Synap gives your AI agents long-term memory. Ingest conversations, extract structured knowledge, and retrieve contextual memories, all through a simple SDK. No infrastructure to manage, no vector databases to tune, no retrieval pipelines to build. Maximem Synap gives your agents durable long-term memory and efficient short-term context across conversations, your customers, and your whole organization.

## Synap in 90 seconds **What is Maximem Synap?** Synap is a managed memory layer for AI agents. It ingests conversations and documents, extracts structured knowledge (facts, preferences, episodes, emotions, temporal events), and serves ranked, scope-aware context back to your agent at retrieval time. **When should I use it?** * You're building an agent that needs to remember users, customers, or organizations across sessions. * You want structured memory (entities, preferences, episodes) without running a vector DB or building a retrieval pipeline. * You need multi-tenant memory [scoping](/concepts/memory-scopes) (per-user, per-customer, per-org) out of the box. **What languages/runtimes?** Python 3.11+ and JavaScript/TypeScript on Node.js 20+. Both are native SDKs. The JS/TS SDK runs on Node, Vercel Edge, Cloudflare Workers and the browser for context and memory operations; the optional anticipation stream is Node only. See [Installation](/setup/installation#javascript-and-typescript-sdk). **Install:** ```bash pip theme={null} pip install maximem-synap ``` ```bash uv theme={null} uv add maximem-synap # pip-compatible (existing venv): uv pip install maximem-synap ``` Install, create an instance, ingest your first memory, and retrieve it, in 10 minutes. Spin up a working memory agent in the browser. No install, no API key needed.
Zero to working memory in 10 minutes: install the SDK, create an instance, ingest and retrieve. Let your AI coding agent wire Synap into your stack. Works with Claude Code, Cursor, Codex, and more. Install, authenticate, and wire Synap into FastAPI / Flask / Next.js / Django. Full Python SDK reference: initialization, ingestion, context fetch, compaction, error handling. Drop Synap into LangChain, LangGraph, Vercel AI SDK, CrewAI, LiveKit, MCP, and 14 more. Hands-on walkthroughs: first integration, multi-user scoping, production checklist, migration. Memory scopes, entity resolution, the ingestion-to-retrieval lifecycle, [MACA](/concepts/memory-architecture), and use-case markdown. Manage instances and memory architecture from the Synap Dashboard. ## Maximem Synap works with popular Agent Frameworks Pick your stack: each tile jumps straight to the integration you'll actually use, so you skip the raw-SDK steps you don't need. ## Choose your path Three common journeys through these docs. Pick the one that matches where you are. 1. [Quickstart](/getting-started/quickstart) 2. [First Integration](/setup/first-integration) 3. [Memory Model Cheat Sheet](/concepts/memory-model-cheat-sheet) 4. [Playground](/getting-started/playground) 1. [Choose your stack](/integrations/overview) 2. [Your framework's integration page](/integrations/overview) 3. [A recipe](/patterns/overview) 4. [Production Checklist](/guides/production-checklist) 1. [What is Synap?](/getting-started/overview) 2. [Identifiers & Scopes](/concepts/memory-scopes) 3. [Multi-tenant SaaS](/patterns/multi-tenant-saas) 4. [Security & Trust](/resources/security-trust) ## What can you build Synap handles the hard parts of agent memory so you can focus on building great AI experiences: * **Personalized chatbots** that remember user preferences, past interactions, and evolving context across sessions * **Context-aware support agents** that recall customer history, previous tickets, and account details without re-asking * **Agents that learn over time** by extracting and retaining facts, preferences, emotions, and temporal events from every conversation * **Multi-user, memory-scoped applications** where each user, customer, or organization has isolated, structured memory boundaries ## How it works Three calls, end to end: ```python Python theme={null} # pip install maximem-synap · export SYNAP_API_KEY=synap_... SYNAP_INSTANCE_ID=inst_... from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK() # reads SYNAP_API_KEY from the environment await sdk.initialize() # 1. Ingest a turn after it happens (B2C: user_id only) await sdk.memories.create( document="User: I prefer dark mode.\nAssistant: Got it!", document_type="ai-chat-conversation", user_id="user_123", ) # 2. Retrieve relevant memory before the next LLM call ctx = await sdk.user.context.fetch( user_id="user_123", search_query=["user preferences"], ) # 3. Inject ctx.facts / ctx.preferences into your system prompt. Done. ``` ```javascript JavaScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; // pip install maximem-synap · export SYNAP_API_KEY=synap_... SYNAP_INSTANCE_ID=inst_... const sdk = new SynapClient(); // reads SYNAP_API_KEY from the environment await sdk.initialize(); // 1. Ingest a turn after it happens (B2C: user_id only) await sdk.memories.create({ document: "User: I prefer dark mode.\nAssistant: Got it!", document_type: 'ai-chat-conversation', user_id: 'user_123', }); // 2. Retrieve relevant memory before the next LLM call const ctx = await sdk.user.context.fetch({ user_id: 'user_123', search_query: ['user preferences'], }); // 3. Inject ctx.facts / ctx.preferences into your system prompt. Done. ``` ```typescript TypeScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; // pip install maximem-synap · export SYNAP_API_KEY=synap_... SYNAP_INSTANCE_ID=inst_... const sdk = new SynapClient(); // reads SYNAP_API_KEY from the environment await sdk.initialize(); // 1. Ingest a turn after it happens (B2C: user_id only) await sdk.memories.create({ document: "User: I prefer dark mode.\nAssistant: Got it!", document_type: 'ai-chat-conversation', user_id: 'user_123', }); // 2. Retrieve relevant memory before the next LLM call const ctx = await sdk.user.context.fetch({ user_id: 'user_123', search_query: ['user preferences'], }); // 3. Inject ctx.facts / ctx.preferences into your system prompt. Done. ``` Behind those three calls, Synap categorizes the content, extracts **facts**, **preferences**, **episodes**, **emotions**, and **temporal events**, resolves entities across conversations, and stores everything in vector + graph engines. For the full mental model see [What is Synap?](/getting-started/overview). ## Three layers Synap three layers: your app with the SDK, Synap Cloud handling the managed pipeline and storage, and the Dashboard web UI for configuration and monitoring | Component | Role | How you interact | | --------------- | ------------------------------------------------------------------------ | -------------------------------------------- | | **SDK** | Runs in your application. Handles ingestion, retrieval, auth, streaming. | `pip install maximem-synap` | | **Synap Cloud** | Managed backend. Pipeline + storage + retrieval. | Fully managed, nothing to deploy | | **Dashboard** | Web UI for instances, memory architecture, monitoring. | [synap.maximem.ai](https://synap.maximem.ai) | Synap is async-first. The SDK uses Python's `asyncio` for non-blocking operations, and the ingestion pipeline processes memories asynchronously so your application stays responsive. # Agno Source: https://docs.maximem.ai/integrations/agno Drop-in InMemoryDb replacement that routes Agno user memories through Synap. Swap Agno's in-memory database for a Synap-backed one. Agents that use `enable_user_memories=True` keep working without code changes. They just gain a persistent, semantically searchable memory store. Requires Python 3.11+. ## Overview This guide shows how to add Synap to an Agno application to build agents that: * Persist user memories across processes and deployments * Retrieve memories semantically rather than by raw key lookup * Serve many users from a single database instance, scoped per-call by `user_id` The Synap Agno integration ships a single class: a subclass of Agno's `InMemoryDb` that routes memory operations through Synap. | Class | Agno interface | Purpose | | --------- | --------------------- | -------------------------------------------- | | `SynapDb` | `InMemoryDb` subclass | Persistent user-memory store backed by Synap | ## Setup Install the package alongside Agno: ```bash pip theme={null} pip install maximem-synap-agno agno ``` ```bash uv theme={null} uv add maximem-synap-agno agno # pip-compatible (existing venv): uv pip install maximem-synap-agno agno ``` The pip package is `maximem-synap-agno`, but the import drops the `maximem-` prefix and uses underscores: `from synap_agno import ...`. Configure your API key. Generate one from the [Synap Dashboard](https://synap.maximem.ai). ```bash .env theme={null} SYNAP_API_KEY=synap_your_key_here OPENAI_API_KEY=your-openai-api-key ``` Initialize the SDK once at application startup: ```python theme={null} from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK() await sdk.initialize() ``` See [SDK Initialization](/sdk/initialization) for the full lifecycle and configuration options. ## Basic integration The smallest useful integration constructs a `SynapDb`, hands it to an `Agent`, and turns on `enable_user_memories`. Each `agent.run(..., user_id=...)` call now reads and writes against Synap rather than a process-local dict: ```python theme={null} # pip install maximem-synap-agno agno from maximem_synap import MaximemSynapSDK from agno.agent import Agent from agno.models.openai import OpenAIChat from synap_agno import SynapDb sdk = MaximemSynapSDK() await sdk.initialize() db = SynapDb(sdk=sdk, customer_id="acme") # customer_id is optional agent = Agent( db=db, model=OpenAIChat(id="gpt-4o-mini"), enable_user_memories=True, ) agent.run("Remember that I prefer async communication", user_id="alice") reply = agent.run("What are my communication preferences?", user_id="alice") ``` Notice that `user_id` is supplied per call. `SynapDb` is constructed once and serves all users in the process. The `customer_id` (organization scope) is fixed at construction. **Memory reads degrade gracefully** on Synap outages; writes raise so silent data loss is impossible. *** ## Core concepts ### SynapDb `SynapDb` extends Agno's `InMemoryDb` and overrides the four user-memory methods. Everything else (session state, non-memory storage) is inherited unchanged, so existing Agno code continues to work: ```python theme={null} from synap_agno import SynapDb db = SynapDb( sdk=sdk, customer_id="acme", # optional, required for B2B instances ) ``` The overridden methods map to Synap as follows: | Method | Behavior | | ------------------------------------- | ------------------------------------------------- | | `upsert_user_memory(user_id, memory)` | Writes a new or updated memory to Synap | | `get_user_memory(user_id, memory_id)` | Fetches a specific memory by ID | | `get_user_memories(user_id, query)` | Retrieves the user's memories via semantic search | | `get_all_memory_topics(user_id)` | Returns unique memory topics via a broad search | ### Per-call user scoping Agno passes `user_id` as a runtime argument to every memory call rather than baking it into the database. That means one `SynapDb` instance can serve any number of users: ```python theme={null} db = SynapDb(sdk=sdk, customer_id="acme") agent = Agent(db=db, model=OpenAIChat(id="gpt-4o-mini"), enable_user_memories=True) for user_id in ["alice", "bob", "carol"]: agent.run("What do you remember about me?", user_id=user_id) ``` Each call hits Synap with its own `user_id` scope, so memories never leak between users, even though the database object is shared. *** ## Complete example: long-lived agent serving many users The pattern below is what most production Agno deployments end up with: one `SynapDb` and one `Agent` at module load, and a thin handler that supplies `user_id` per request: ```python theme={null} from agno.agent import Agent from agno.models.openai import OpenAIChat from synap_agno import SynapDb # Define once at startup db = SynapDb(sdk=sdk, customer_id="acme") agent = Agent( db=db, model=OpenAIChat(id="gpt-4o-mini"), enable_user_memories=True, description=( "You are a personal assistant with long-term memory. " "Recall the user's preferences and decisions whenever they ask. " "When the user shares a new fact, write it down." ), ) # Per-request handler: user_id varies, db/agent do not async def handle_request(user_id: str, message: str) -> str: return agent.run(message, user_id=user_id).content # Usage await handle_request("alice", "I prefer email over Slack for updates.") await handle_request("bob", "Remind me what timezone I'm in, it's Pacific.") reply = await handle_request("alice", "How should you contact me?") # → "Over email, since that's your preferred channel." ``` Three things to notice in this pattern: 1. **One `SynapDb`, many users.** The database is constructed once; `user_id` is the per-call scope. 2. **The `customer_id` is the tenant boundary.** All users sharing a `SynapDb` instance are inside the same `customer_id`. For cross-tenant services, build a separate `SynapDb` per tenant. 3. **Agno's behavior is unchanged.** `enable_user_memories`, `description`, and run signatures all work exactly as in vanilla Agno; you've only swapped the storage layer. *** ## Advanced patterns ### Multi-tenant scoping `SynapDb` takes the customer-tenant scope at construction (`customer_id`) and the user scope per call (`user_id`). `customer_id` is required on B2B Synap instances and ignored on single-tenant ones. See [Memory Scopes](/concepts/memory-scopes). ```python theme={null} # Single-tenant instance db = SynapDb(sdk=sdk) # Multi-tenant: one db per customer db_acme = SynapDb(sdk=sdk, customer_id="acme") db_initech = SynapDb(sdk=sdk, customer_id="initech") ``` For services that serve many tenants, route requests to the matching `SynapDb` rather than mixing tenants on one instance. ### Memory topic discovery `get_all_memory_topics` is implemented as a broad Synap search and returns unique topics across the user's memory pool. Useful for building UIs that let a user browse what's been remembered about them, or for prompt enrichment ("Here are the topics we've discussed: ..."). ### Failure semantics The integration follows the Synap-wide contract: * **`get_user_memory` / `get_user_memories` degrade gracefully**: return empty results and log an error if Synap is unreachable. * **`upsert_user_memory` surfaces failures**: raises `SynapIntegrationError` so the agent and caller know persistence failed. This is by design: read failures shouldn't break a user-facing answer, but silent write failures would let memory drift away from reality. *** ## Going further * [Patterns overview](/patterns/overview): reusable memory patterns across frameworks. * [Cookbook overview](/cookbook/overview): end-to-end worked examples. *** ## Next steps `FunctionTool` factory for Google ADK agents. Function tools for the OpenAI Agents SDK. How `user_id` and `customer_id` interact across reads and writes. Direct ingestion API for pipelines that need finer control than `upsert_user_memory`. # AI coding agents Source: https://docs.maximem.ai/integrations/ai-coding-agents A drop-in skill that teaches your AI coding agent (Claude Code, Cursor, Codex, and more) how to add Maximem Synap to your app. The fastest way to integrate: ask, and the agent writes the correct code. The Synap skill is a drop-in instruction pack for AI coding agents. Once installed, your agent knows the Synap SDK, the scoping model, and every framework integration, so you can just say "add Synap memory to my LangGraph agent" and it writes the correct code instead of guessing. This is the fastest way to integrate Synap if you already work with an AI coding assistant. The skill is grounded in this documentation, so it stays in step with the SDK. ## What it does * Recognizes when memory fits your use case, and tells you when Synap is overkill. * Generates correct SDK setup, ingestion, and retrieval code with the right defaults. * Knows the per-framework integration package for all supported frameworks (LangChain, LangGraph, LlamaIndex, OpenAI Agents, Pydantic AI, CrewAI, AutoGen, Google ADK, Haystack, Agno, Semantic Kernel, Microsoft Agent Framework, NeMo, LiveKit, Pipecat, Claude Agent SDK, Mastra, Vercel AI SDK). * Applies the User / Customer / Client scoping model correctly for single-user and multi-tenant apps. ## Claude Code The full skill (with progressive-disclosure reference files) lives in the public SDK repo under `skills/synap`. Drop it into your skills folder: ```bash Project (this repo) theme={null} npx degit maximem-ai/maximem_synap_sdk/skills/synap .claude/skills/synap ``` ```bash User (all projects) theme={null} npx degit maximem-ai/maximem_synap_sdk/skills/synap ~/.claude/skills/synap ``` Claude Code auto-discovers any skill at `.claude/skills//SKILL.md` (project) or `~/.claude/skills//SKILL.md` (user). Prefer git? ```bash theme={null} git clone https://github.com/maximem-ai/maximem_synap_sdk.git cp -r maximem_synap_sdk/skills/synap ~/.claude/skills/synap ``` **Claude Cowork (desktop):** drop the same `skills/synap` folder into your Cowork plugin cache, then run `/skill list` to confirm it registered. ## Other coding agents For non-Claude tools, the skill ships a single-file `AGENTS.md` (in the same `skills/synap` folder). Copy it to your tool's rules path: | Tool | Put `AGENTS.md` at | | ----------------------------- | ------------------------------ | | **Cursor** | `.cursor/rules/synap.mdc` | | **Codex** | `AGENTS.md` (project root) | | **Aider** | `CONVENTIONS.md` | | **Cline** | `.clinerules` | | **Continue / Windsurf / Zed** | the tool's rules-file location | See the [repo README](https://github.com/maximem-ai/maximem_synap_sdk/tree/main/skills/synap) for the exact steps per tool. ## Then just ask Once installed, prompt your agent in plain language: > Add Synap long-term memory to my LangGraph agent, scoped per user. The skill handles SDK initialization, ingestion, retrieval, and the framework wiring for you. ## Next steps The framework packages the skill installs for you. Prefer to wire it by hand? Do the full setup in about 10 minutes. # AutoGen Source: https://docs.maximem.ai/integrations/autogen BaseTool implementations that give AutoGen agents on-demand memory search and storage. Expose Synap memory to an AutoGen agent as two `BaseTool` implementations. The agent decides when to call each, and both tools cooperate with AutoGen's `CancellationToken` so a long search can be aborted mid-flight. Requires Python 3.11+. ## Overview This guide shows how to add Synap to an AutoGen application to build agents that: * Recall user-specific facts, preferences, and past conversations * Persist new information surfaced during a conversation * Respect AutoGen's cooperative cancellation model so memory calls don't leak past a cancelled task The Synap AutoGen integration ships two drop-in tool classes, both implementing AutoGen's `BaseTool` interface. | Class | AutoGen interface | Purpose | | ----------------- | ----------------- | ----------------------------------------------- | | `SynapSearchTool` | `BaseTool` | Searches Synap memory by natural-language query | | `SynapStoreTool` | `BaseTool` | Stores a new memory in Synap | ## Setup Install the package alongside AutoGen: ```bash pip theme={null} pip install maximem-synap-autogen autogen-agentchat autogen-core ``` ```bash uv theme={null} uv add maximem-synap-autogen autogen-agentchat autogen-core # pip-compatible (existing venv): uv pip install maximem-synap-autogen autogen-agentchat autogen-core ``` The pip package is `maximem-synap-autogen`, but the import drops the `maximem-` prefix and uses underscores: `from synap_autogen import ...`. Configure your API key. Generate one from the [Synap Dashboard](https://synap.maximem.ai). ```bash .env theme={null} SYNAP_API_KEY=synap_your_key_here OPENAI_API_KEY=your-openai-api-key ``` Initialize the SDK once at application startup: ```python theme={null} from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK() await sdk.initialize() ``` See [SDK Initialization](/sdk/initialization) for the full lifecycle and configuration options. ## Basic integration The smallest useful integration registers both tools on an `AssistantAgent` and runs a task. The agent reads tool descriptions and decides whether to search or store: ```python theme={null} # pip install maximem-synap-autogen autogen-agentchat autogen-core from maximem_synap import MaximemSynapSDK from autogen_agentchat.agents import AssistantAgent from synap_autogen import SynapSearchTool, SynapStoreTool sdk = MaximemSynapSDK() await sdk.initialize() tools = [ SynapSearchTool(sdk=sdk, user_id="alice", customer_id="acme"), SynapStoreTool(sdk=sdk, user_id="alice", customer_id="acme"), ] agent = AssistantAgent( name="MemoryAgent", model_client=your_model_client, tools=tools, system_message=( "Use synap_search to recall user context. " "Use synap_store to remember new information." ), ) await agent.run(task="What are my top priorities this week?") ``` The scoping triple (`user_id`, optional `customer_id`) is bound at construction. The model only ever sees `query`, `max_results`, and `mode`, never the user identity. This prevents prompt-injection attempts from spoofing scope. *** ## Core concepts ### Search tool `SynapSearchTool` is the read side. The model can override `max_results` and `mode` per call but cannot reach outside the scope bound at construction. ```python theme={null} from synap_autogen import SynapSearchTool search = SynapSearchTool( sdk=sdk, user_id="alice", customer_id="acme", # optional, required for B2B instances ) ``` Tool schema exposed to the model: ```json theme={null} { "query": "string", "max_results": "int (default 5)", "mode": "\"fast\" | \"accurate\" (default \"fast\")" } ``` Returns a list of memory objects with `content`, `type`, and `confidence` fields. The two retrieval modes trade latency against comprehensiveness: | | `fast` | `accurate` | | -------- | ---------------------------------------------- | ------------------------------------------------------- | | Search | Vector + graph (no LLM subquery decomposition) | Vector + graph + LLM subquery decomposition + reranking | | Best for | Real-time chat | Multi-entity queries | `fast` is lower-latency and suited to the hot path; `accurate` adds LLM-driven query decomposition and reranking for relationship-aware queries at a higher latency cost. **Search failures degrade gracefully**: the tool returns `[]` and logs an error so the agent can continue. ### Store tool `SynapStoreTool` is the write side. The model supplies the content and an optional memory type; everything else is fixed at construction. ```python theme={null} from synap_autogen import SynapStoreTool store = SynapStoreTool( sdk=sdk, user_id="alice", customer_id="acme", ) ``` Tool schema exposed to the model: ```json theme={null} { "content": "string", "memory_type": "string (default \"fact\")" } ``` Returns `{"status": "stored", "id": "..."}` on success. **Store failures surface explicitly**: the tool raises `SynapIntegrationError` so the agent (and you) know if persistence failed. ### Cooperative cancellation Both tools propagate AutoGen's `CancellationToken`. When the token is cancelled, the in-flight Synap call is aborted and the tool returns control to the agent rather than continuing to completion: ```python theme={null} from autogen_core import CancellationToken token = CancellationToken() result = await search.run({"query": "project deadlines"}, cancellation_token=token) # From elsewhere in the same asyncio task group: token.cancel() ``` This matters in long-running team conversations where one agent's tool call should be cancellable by an orchestrator. *** ## Complete example: assistant team with shared memory The following team has two agents sharing the same Synap-backed memory: a `Researcher` that searches and stores, and a `Planner` that searches but never stores. Both run inside an AutoGen `RoundRobinGroupChat`: ```python theme={null} from autogen_agentchat.agents import AssistantAgent from autogen_agentchat.teams import RoundRobinGroupChat from autogen_agentchat.conditions import TextMentionTermination from synap_autogen import SynapSearchTool, SynapStoreTool def build_team(sdk, model_client, user_id: str, customer_id: str | None = None): # Researcher gets both tools: it can recall and remember researcher = AssistantAgent( name="Researcher", model_client=model_client, tools=[ SynapSearchTool(sdk=sdk, user_id=user_id, customer_id=customer_id), SynapStoreTool(sdk=sdk, user_id=user_id, customer_id=customer_id), ], system_message=( "You gather facts about the user. " "Always call synap_search first; if you learn something new, " "call synap_store before responding." ), ) # Planner only reads: it shouldn't pollute memory with plans planner = AssistantAgent( name="Planner", model_client=model_client, tools=[SynapSearchTool(sdk=sdk, user_id=user_id, customer_id=customer_id)], system_message=( "You produce action plans based on what Researcher has found. " "Use synap_search to verify facts. End your turn with TERMINATE." ), ) return RoundRobinGroupChat( [researcher, planner], termination_condition=TextMentionTermination("TERMINATE"), ) # Usage team = build_team(sdk, model_client, user_id="alice", customer_id="acme") await team.run(task="Plan my work for next week.") ``` Three things to notice in this pattern: 1. **Scope is per-tool, per-agent.** Both agents see the same user's memory, but only the `Researcher` can write. This is a common safety pattern: minimize the surface that can mutate memory. 2. **Memory is shared across agents.** Anything `Researcher` stores during this run is immediately available to `Planner` on the next turn. 3. **`CancellationToken` flows automatically** through AutoGen's task group, so cancelling the team also cancels any in-flight Synap calls. *** ## Advanced patterns ### Multi-tenant scoping Both tools accept the standard scoping triple: `user_id` (required), optional `customer_id`, optional `conversation_id`. `customer_id` is required on B2B Synap instances and ignored on single-tenant ones. See [Memory Scopes](/concepts/memory-scopes). ```python theme={null} # User-scoped only search = SynapSearchTool(sdk=sdk, user_id="alice") # Organization-scoped (user sees org-shared memories too) search = SynapSearchTool(sdk=sdk, user_id="alice", customer_id="acme-corp") ``` For multi-tenant services, construct tools per request rather than caching them globally; each task should have its scope baked in. ### Tuning retrieval mode The model can pass `mode: "accurate"` for higher-recall, slower lookups. For a global default, configure your system prompt to specify the mode the agent should prefer; the tool will respect whatever the model passes. ### Failure semantics The integration follows the Synap-wide contract: * **`SynapSearchTool` degrades gracefully**: returns `[]` and logs an error * **`SynapStoreTool` surfaces failures**: raises `SynapIntegrationError` so the agent and caller know persistence failed * **Cancellation is honored**: both tools abort cleanly when `CancellationToken.cancel()` fires This is by design: read failures shouldn't break a team conversation, but silent write failures would corrupt the memory pool. *** ## Going further * [Patterns overview](/patterns/overview): reusable memory patterns across frameworks. * [Cookbook overview](/cookbook/overview): end-to-end worked examples. *** ## Next steps Function tools for the OpenAI Agents SDK. Storage backend for CrewAI crews. The retrieval API behind `SynapSearchTool`: modes, scopes, and response shapes. How `user_id` and `customer_id` interact across reads and writes. # CAMEL-AI Source: https://docs.maximem.ai/integrations/camel-ai Synap as a native AgentMemory for CAMEL-AI: long-term recall and persistence layered over the agent's own conversation history. Give a [CAMEL-AI](https://github.com/camel-ai/camel) `ChatAgent` persistent memory through Synap. CAMEL exposes a first-class pluggable memory interface (`ChatAgent(memory=...)` accepts any `AgentMemory`), and `SynapAgentMemory` plugs Synap in there, plus `@tool` functions and a short-term context helper. Requires Python 3.11+ and `camel-ai>=0.2.90`. ## Overview Three surfaces, mapped onto CAMEL's own extension points. Adopt only the ones you need. | Surface | CAMEL extension point | Purpose | | ------------------------- | --------------------------------------- | ------------------------------------------------------------------------- | | `SynapAgentMemory` | `AgentMemory` → `ChatAgent(memory=...)` | Long-term recall + persistence, layered over CAMEL's conversation history | | `create_synap_tools` | `FunctionTool` | Explicit `search_memory` / `store_memory` the model can call | | `synap_st_system_message` | `ChatAgent(system_message=...)` | Fold Synap short-term context into the system prompt | All three take an already-constructed `MaximemSynapSDK`. Your app owns the SDK and its credentials. ## Setup ```bash pip theme={null} pip install maximem-synap-camel-ai camel-ai ``` ```bash uv theme={null} uv add maximem-synap-camel-ai camel-ai ``` ## Basic integration Register Synap as the agent's `AgentMemory`; CAMEL then calls it for context on every turn. ```python theme={null} from camel.agents import ChatAgent from camel.models import ModelFactory from maximem_synap import MaximemSynapSDK from synap_camel_ai import SynapAgentMemory sdk = MaximemSynapSDK(api_key="sk-...") memory = SynapAgentMemory(sdk, user_id="alice", customer_id="acme") agent = ChatAgent( system_message="You are a helpful assistant.", model=ModelFactory.create(model_platform="openai", model_type="gpt-4o"), memory=memory, # constructor only — not agent.memory = ... ) print(agent.step("What did we decide about the rollout?").msgs[0].content) ``` ## Core concepts ### SynapAgentMemory Unlike a bolt-on store, CAMEL's `AgentMemory.get_context()` is the **sole** source of the model's input: it returns whatever the memory's `retrieve()` yields. So Synap **augments** CAMEL's history rather than replacing it: `SynapAgentMemory` subclasses `ChatHistoryMemory` (which keeps the live conversation) and adds two things. * **Recall.** `retrieve()` returns the real conversation (system + user + assistant) with Synap's long-term memories prepended as a system context block, fetched via `sdk.fetch` using the latest user turn as the query. A Synap blip degrades to the plain local history; recall never crashes the turn. * **Persistence.** When a turn completes, the accumulated transcript is ingested via `sdk.memories.create` (server-side extraction) under a stable `document_id`, so future sessions remember it. Attach the memory through the **constructor** (`ChatAgent(memory=...)`). Do not assign `agent.memory = ...` afterward; that path re-runs retrieve/clear/write and would amplify the injected context. ### create\_synap\_tools For agents that want model-driven memory instead of, or alongside, `SynapAgentMemory`. Returns `search_memory` and `store_memory` as CAMEL `FunctionTool` objects. ```python theme={null} from synap_camel_ai import create_synap_tools agent = ChatAgent( system_message="You are a helpful assistant.", model=ModelFactory.create(model_platform="openai", model_type="gpt-4o"), tools=create_synap_tools(sdk, user_id="alice", customer_id="acme"), ) ``` ### synap\_st\_system\_message CAMEL's `system_message` is a static string, so short-term context is folded into it once at construction. ```python theme={null} from synap_camel_ai import SynapAgentMemory, synap_st_system_message agent = ChatAgent( system_message=synap_st_system_message( sdk, conversation_id="conv_abc", system_message="You are a support agent.", ), memory=SynapAgentMemory(sdk, user_id="alice"), ) ``` `conversation_id` is required. Empty context is a no-op; SDK failures are swallowed by default (`on_error="fallback"`), or set `on_error="raise"` for strict environments. ## Complete example: multi-user support agent ```python theme={null} from camel.agents import ChatAgent from camel.models import ModelFactory from maximem_synap import MaximemSynapSDK from synap_camel_ai import SynapAgentMemory, synap_st_system_message sdk = MaximemSynapSDK(api_key="sk-...") def build_agent(user_id: str, conversation_id: str) -> ChatAgent: return ChatAgent( system_message=synap_st_system_message( sdk, conversation_id=conversation_id, system_message="You are a concise, friendly support agent.", ), model=ModelFactory.create(model_platform="openai", model_type="gpt-4o"), memory=SynapAgentMemory(sdk, user_id=user_id, customer_id="acme"), ) agent = build_agent("alice", "conv_alice_001") print(agent.step("Remind me what plan I'm on and my open ticket.").msgs[0].content) # → recalls prior context from Synap, answers, and persists the turn for extraction. ``` ## Advanced patterns ### Sync interface, async SDK CAMEL's `AgentMemory` methods are synchronous, so `SynapAgentMemory` bridges to the async Synap SDK internally (via the shared `run_async` helper). This works whether you drive the agent with `agent.step(...)` (sync) or `agent.astep(...)` (async), but construct the SDK, the memory, and run the agent on **one** asyncio event loop. ### Error policy * **Reads** (recall, `search_memory`) degrade: a Synap failure returns no recall and the turn continues. * **The persistence write** runs inside the agent loop, so it is best-effort by default (`on_error="fallback"`): a transient outage never discards the model's just-produced response. Set `SynapAgentMemory(..., on_error="raise")` for strict environments. * **The explicit `store_memory` tool** raises `SynapIntegrationError` on failure. ### Not a vector store `SynapAgentMemory` deliberately does not reduce Synap to a `VectorDBBlock` behind CAMEL's storage. It owns both the recall shape (Synap's formatted context) and the write pipeline (server-side extraction), which a dumb vector backend would throw away. ## Going further * **Scoping.** `user_id` and `customer_id` flow straight to Synap; per-user isolation and B2C/B2B scope are derived server-side from the ids you pass. ## Next steps How `memories.create` extraction differs from conversation recording. Every framework Synap plugs into. # Claude Agent SDK Source: https://docs.maximem.ai/integrations/claude-agent Hooks and MCP server that give Anthropic's Claude Agent SDK persistent memory, in Python and TypeScript. Add persistent memory to an Anthropic Claude Agent in two ways: hooks for zero-friction automatic memory (the model never sees the plumbing), and an MCP server that exposes `synap_search` and `synap_remember` as explicit tools the model can call. Runtime: Node.js 20+ for the anticipation stream; context and memory operations also run on Edge, Cloudflare Workers and the browser. ## Overview This guide shows how to add Synap to a Claude Agent SDK application to build agents that: * Inject relevant memories before every turn, automatically, with no tool calls * Record every completed turn back to Synap so memory grows with use * Expose explicit search/store tools the model can call mid-conversation when it needs to The integration is available in both Python and TypeScript and ships three exports: | Export | Language | Purpose | | ------------------------- | ------------------- | ---------------------------------------------------------------- | | `create_synap_hooks` | Python + TypeScript | Hooks for automatic context injection and turn recording | | `create_synap_mcp_server` | Python + TypeScript | MCP server exposing `synap_search` and `synap_remember` as tools | | `build_synap_tools` | TypeScript only | Raw tool definitions for manual composition | The TypeScript package builds on the JavaScript SDK and needs Node.js 20+. Context and memory operations also run on Edge Runtime and Cloudflare Workers; the optional anticipation stream needs Node.js. See [Installation → JavaScript and TypeScript SDK](/setup/installation#javascript-and-typescript-sdk). ## Setup ```bash Python (pip) theme={null} pip install maximem-synap-claude-agent ``` ```bash Python (uv) theme={null} uv add maximem-synap-claude-agent # pip-compatible (existing venv): uv pip install maximem-synap-claude-agent ``` ```bash TypeScript theme={null} npm install @maximem/synap-claude-agent @anthropic-ai/claude-agent-sdk zod ``` In Python the pip package is `maximem-synap-claude-agent`, but the import drops the `maximem-` prefix and uses underscores: `from synap_claude_agent import ...`. In TypeScript the import name matches the npm package: `import { createSynapHooks } from "@maximem/synap-claude-agent"`. Configure your API key. Generate one from the [Synap Dashboard](https://synap.maximem.ai). ```bash .env theme={null} SYNAP_API_KEY=synap_your_key_here ANTHROPIC_API_KEY=your-anthropic-api-key ``` Initialize the SDK once at application startup: ```python Python theme={null} from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK() await sdk.initialize() ``` ```typescript TypeScript theme={null} import { SynapClient } from "@maximem/synap-js-sdk"; const sdk = new SynapClient(); await sdk.initialize(); ``` See [SDK Initialization](/sdk/initialization) for the full lifecycle and configuration options. ## Basic integration The smallest useful integration uses hooks: memory injection and turn recording happen automatically, with no changes to the model's tool surface: ```python Python theme={null} # pip install maximem-synap-claude-agent import asyncio import uuid from maximem_synap import MaximemSynapSDK from claude_agent_sdk import query, ClaudeAgentOptions from synap_claude_agent import create_synap_hooks sdk = MaximemSynapSDK() await sdk.initialize() # If you pass conversation_id explicitly it must be a valid UUID; # omit it to let Synap auto-generate one. hooks = create_synap_hooks( sdk=sdk, user_id="alice", customer_id="acme", # optional, required for B2B instances conversation_id=str(uuid.uuid4()), # optional; auto-generated if omitted ) async def main(): async for message in query( prompt="What did I tell you about my trial account?", options=ClaudeAgentOptions(hooks=hooks), ): print(message) asyncio.run(main()) ``` ```typescript TypeScript theme={null} // npm install @maximem/synap-claude-agent @anthropic-ai/claude-agent-sdk zod import { query } from "@anthropic-ai/claude-agent-sdk"; import { SynapClient } from "@maximem/synap-js-sdk"; import { createSynapHooks } from "@maximem/synap-claude-agent"; const sdk = new SynapClient(); await sdk.initialize(); const hooks = createSynapHooks({ sdk, userId: "alice", customerId: "acme", // optional conversationId: crypto.randomUUID(), // optional; must be a valid UUID if passed }); for await (const message of query({ prompt: "What did I tell you about my trial account?", options: { hooks }, })) { console.log(message); } ``` The hooks intercept the agent's lifecycle: `before_query` fetches Synap context and prepends it as a system message, and `after_turn` ingests the completed user/assistant exchange. **Context fetch failures degrade gracefully**: empty context is injected and the error is logged. **Turn ingestion failures surface explicitly** so silent data loss is impossible. For explicit memory control (the model decides when to search or store), layer in the MCP server (see below). *** ## Core concepts ### Hooks: automatic memory `create_synap_hooks` returns a dict of hook callbacks that the Claude Agent SDK invokes around each turn: | Hook | Behavior | | -------------- | --------------------------------------------------------------------------------- | | `before_query` | Fetches Synap context for the incoming prompt and prepends it as a system message | | `after_turn` | Ingests the full user + assistant turn back into Synap | ```python theme={null} import uuid hooks = create_synap_hooks( sdk=sdk, user_id="alice", customer_id="acme", conversation_id=str(uuid.uuid4()), ) ``` The model never sees the hook plumbing: there are no tools added, no schemas to learn. This is the right primitive for production agents where memory should be omnipresent. ### MCP server: explicit memory tools When you want the *model* to decide when to query or store memories, register the Synap MCP server. It exposes two tools: * **`synap_search`**: search memories by natural-language query * **`synap_remember`**: store a new memory ```python Python theme={null} from claude_agent_sdk import query, ClaudeAgentOptions from synap_claude_agent import create_synap_hooks, create_synap_mcp_server hooks = create_synap_hooks(sdk=sdk, user_id="alice") mcp_server = create_synap_mcp_server(sdk=sdk, user_id="alice") async for message in query( prompt="Search your memory for anything about my project deadlines.", options=ClaudeAgentOptions( hooks=hooks, mcp_servers={"synap": mcp_server}, ), ): print(message) ``` ```typescript TypeScript theme={null} import { query } from "@anthropic-ai/claude-agent-sdk"; import { createSynapHooks, createSynapMcpServer } from "@maximem/synap-claude-agent"; const hooks = createSynapHooks({ sdk, userId: "alice" }); const mcpServer = createSynapMcpServer({ sdk, userId: "alice" }); for await (const message of query({ prompt: "Search your memory for anything about my project deadlines.", options: { hooks, mcpServers: { synap: mcpServer }, }, })) { console.log(message); } ``` The MCP server can be used alone, or alongside hooks for layered memory (automatic context plus on-demand search/store). ### Raw tool definitions (TypeScript) For TypeScript users who want to compose tools manually rather than going through MCP, `buildSynapTools` returns raw Anthropic tool definitions: ```typescript theme={null} import { buildSynapTools } from "@maximem/synap-claude-agent"; const tools = buildSynapTools({ sdk, userId: "alice", customerId: "acme" }); // tools = [synapSearchTool, synapRememberTool] ``` Pass them directly into the agent's tool list. The Python integration uses MCP exclusively for explicit tooling, so this export is TypeScript-only. *** ## Complete example: agent with hooks + MCP server The pattern below combines both primitives. Hooks provide automatic memory on every turn, and the MCP server gives the model the option to dig deeper when it decides recall is needed: ```python Python theme={null} from claude_agent_sdk import query, ClaudeAgentOptions from synap_claude_agent import create_synap_hooks, create_synap_mcp_server async def handle_query(sdk, user_id: str, prompt: str, customer_id: str | None = None) -> str: hooks = create_synap_hooks(sdk=sdk, user_id=user_id, customer_id=customer_id) mcp = create_synap_mcp_server(sdk=sdk, user_id=user_id, customer_id=customer_id) full = [] async for message in query( prompt=prompt, options=ClaudeAgentOptions( hooks=hooks, mcp_servers={"synap": mcp}, ), ): full.append(message) return "\n".join(str(m) for m in full) # Usage reply = await handle_query(sdk, user_id="alice", prompt="What plan am I on?", customer_id="acme") ``` ```typescript TypeScript theme={null} import { query } from "@anthropic-ai/claude-agent-sdk"; import { createSynapHooks, createSynapMcpServer } from "@maximem/synap-claude-agent"; async function handleQuery(sdk, userId: string, prompt: string, customerId?: string) { const hooks = createSynapHooks({ sdk, userId, customerId }); const mcpServer = createSynapMcpServer({ sdk, userId, customerId }); const out: string[] = []; for await (const message of query({ prompt, options: { hooks, mcpServers: { synap: mcpServer } }, })) { out.push(String(message)); } return out.join("\n"); } // Usage const reply = await handleQuery(sdk, "alice", "What plan am I on?", "acme"); ``` Three things to notice in this pattern: 1. **Hooks and MCP server cover different needs.** Hooks are silent and always-on; MCP tools are explicit and model-driven. 2. **They compose.** Use both: the model sees relevant context every turn AND can query for more when needed. 3. **Scope is per-request.** Each `handle_query` invocation creates its own hooks and MCP server scoped to the right user. *** ## Advanced patterns ### Hooks vs. MCP server | | Hooks | MCP server | | ----------------- | ----------------------------------------------- | ----------------------------------------------------- | | Context injection | Automatic, every turn | On-demand via tool call | | Memory storage | Automatic, every turn | On-demand via tool call | | Model awareness | Model doesn't see the tools | Model can decide when to search/store | | Best for | Production agents where memory is always needed | Research agents where explicit memory control matters | Use both together for maximum coverage: hooks handle the always-on path, MCP tools handle the explicit path. ### Multi-tenant scoping All three exports accept the standard scoping triple: `user_id` (required), optional `customer_id`, optional `conversation_id`. `customer_id` is required on B2B Synap instances and ignored on single-tenant ones. See [Memory Scopes](/concepts/memory-scopes). ```python theme={null} hooks = create_synap_hooks(sdk=sdk, user_id="alice", customer_id="acme") ``` For multi-tenant services, construct hooks/MCP per request rather than caching them globally. ### Failure semantics The integration follows the Synap-wide contract: * **`before_query` (hooks) degrades gracefully**: empty context on failure, error logged. * **`after_turn` (hooks) surfaces failures**: turn ingestion raises `SynapIntegrationError`. * **`synap_search` (MCP) degrades gracefully**: returns `[]` on failure. * **`synap_remember` (MCP) surfaces failures**: raises `SynapIntegrationError`. This is by design: read failures shouldn't break a user-facing turn, but silent write failures would let the memory drift away from reality. *** ## Going further * [Patterns overview](/patterns/overview): reusable memory patterns across frameworks. * [Cookbook overview](/cookbook/overview): end-to-end worked examples. *** ## Next steps Middleware for any Vercel AI SDK model. `SynapMemory` for Mastra. The retrieval API behind hooks and `synap_search`: modes, scopes, and response shapes. How `user_id`, `customer_id`, and `conversation_id` interact across reads. # CrewAI Source: https://docs.maximem.ai/integrations/crewai StorageBackend implementation that routes CrewAI's unified memory through Synap. Make CrewAI's built-in memory durable and queryable across crews. The integration plugs into CrewAI's `StorageBackend` protocol, so existing crews keep working. They just gain a long-term memory that survives restarts. Requires Python 3.11+. ## Overview This guide shows how to add Synap to a CrewAI application to build crews that: * Persist `Memory` entries across executions and processes * Retrieve semantically-relevant context from prior crew runs * Scope memories per user or per organization The Synap CrewAI integration ships a single class: a drop-in replacement for CrewAI's default storage backend. | Class | CrewAI interface | Purpose | | --------------------- | ---------------- | ------------------------------------------------------- | | `SynapStorageBackend` | `StorageBackend` | Persistent storage for CrewAI's unified `Memory` system | ## Setup Install the package alongside CrewAI: ```bash pip theme={null} pip install maximem-synap-crewai crewai ``` ```bash uv theme={null} uv add maximem-synap-crewai crewai # pip-compatible (existing venv): uv pip install maximem-synap-crewai crewai ``` The pip package is `maximem-synap-crewai`, but the import drops the `maximem-` prefix and uses underscores: `from synap_crewai import ...`. Configure your API key. Generate one from the [Synap Dashboard](https://synap.maximem.ai). ```bash .env theme={null} SYNAP_API_KEY=synap_your_key_here OPENAI_API_KEY=your-openai-api-key ``` Initialize the SDK once at application startup: ```python theme={null} from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK() await sdk.initialize() ``` See [SDK Initialization](/sdk/initialization) for the full lifecycle and configuration options. ## Basic integration The smallest useful integration swaps in `SynapStorageBackend` as the storage for CrewAI's `Memory`. Every `save` call inside the crew is routed to Synap, and `search` returns ranked, semantically-matched results: ```python theme={null} # pip install maximem-synap-crewai crewai from maximem_synap import MaximemSynapSDK from crewai import Agent, Crew, Task from crewai.memory import Memory from synap_crewai import SynapStorageBackend sdk = MaximemSynapSDK() await sdk.initialize() backend = SynapStorageBackend( sdk=sdk, user_id="alice", customer_id="acme", # optional, required for B2B instances ) memory = Memory(storage=backend) crew = Crew( agents=[your_agent], tasks=[your_task], memory=memory, ) result = crew.kickoff(inputs={"topic": "quarterly planning"}) ``` CrewAI's memory machinery is now backed by Synap. **Search failures degrade gracefully** (empty result + log); writes raise on failure so silent data loss is impossible. *** ## Core concepts ### Lifecycle methods `SynapStorageBackend` implements CrewAI's `StorageBackend` protocol by mapping each method to the equivalent Synap operation: ```python theme={null} backend = SynapStorageBackend(sdk=sdk, user_id="alice", customer_id="acme") ``` | Method | Behavior | | --------------------------------------- | ----------------------------------------------------- | | `save(value, metadata)` | Ingests a memory fragment with optional metadata tags | | `search(query, limit, score_threshold)` | Semantic search; returns ranked results | | `list_records(limit)` | Returns recent memories (uses a broad Synap search) | | `count()` | Returns an approximate count via a broad search | CrewAI operations that have no direct Synap equivalent (notably `delete`) are no-ops with a warning logged. The memory pipeline keeps moving rather than blocking the crew. ### Async support CrewAI 0.100+ supports async task execution. `SynapStorageBackend` exposes `asearch` for use in async crews: ```python theme={null} results = await backend.asearch("project deadlines", limit=5) ``` The synchronous `search` method wraps `asearch` via an event-loop bridge, so the same backend works in both sync and async crews. You don't need separate configurations. *** ## Complete example: research crew with shared memory The following crew chains a researcher and a writer. Both agents share the same Synap-backed memory, so the writer sees what the researcher learned, and the next crew kickoff has access to everything from earlier runs: ```python theme={null} from crewai import Agent, Crew, Task, Process from crewai.memory import Memory from synap_crewai import SynapStorageBackend def build_research_crew(sdk, user_id: str, customer_id: str | None = None) -> Crew: backend = SynapStorageBackend(sdk=sdk, user_id=user_id, customer_id=customer_id) memory = Memory(storage=backend) researcher = Agent( role="Senior Researcher", goal="Find concrete, sourced information about the topic.", backstory="Methodical analyst who cites sources.", ) writer = Agent( role="Technical Writer", goal="Turn research into a clear summary.", backstory="Editor with a bias toward clarity.", ) research_task = Task( description="Research the topic: {topic}.", expected_output="A bullet list of sourced findings.", agent=researcher, ) writing_task = Task( description="Write a 200-word summary based on the research.", expected_output="A polished prose summary.", agent=writer, context=[research_task], ) return Crew( agents=[researcher, writer], tasks=[research_task, writing_task], process=Process.sequential, memory=memory, ) # Usage crew = build_research_crew(sdk, user_id="alice", customer_id="acme") result = crew.kickoff(inputs={"topic": "agentic memory architectures"}) # Subsequent runs benefit from accumulated memory crew.kickoff(inputs={"topic": "long-context retrieval"}) ``` Three things to notice in this pattern: 1. **Memory survives the crew.** Re-running `kickoff` with a new topic still benefits from the prior run's findings. 2. **Scope is fixed at backend construction.** All agents in this crew share the same `user_id`/`customer_id`. Build a separate crew per user if you need isolation. 3. **CrewAI's built-in retrieval is unchanged.** You don't restructure tasks or prompts; the memory backend just becomes Synap. *** ## Advanced patterns ### Multi-tenant scoping `SynapStorageBackend` accepts the standard scoping triple: `user_id` (required), optional `customer_id`, optional `conversation_id`. `customer_id` is required on B2B Synap instances and ignored on single-tenant ones. See [Memory Scopes](/concepts/memory-scopes). ```python theme={null} # User-scoped only backend = SynapStorageBackend(sdk=sdk, user_id="alice") # Organization-scoped backend = SynapStorageBackend(sdk=sdk, user_id="alice", customer_id="acme") ``` For multi-tenant services, construct a backend per request; never share a backend whose scope doesn't match the inbound user. ### Sync and async in the same codebase `SynapStorageBackend` supports both `search` (sync) and `asearch` (async). The sync method bridges to the async path via an event loop, so: * In a sync crew, just call `crew.kickoff()` as usual; `search` works transparently. * In an async crew, prefer `asearch` for direct access; the framework will call it for you when you await crew tasks. ### Failure semantics The integration follows the Synap-wide contract: * **`search` and `asearch` degrade gracefully**: return `[]` and log an error if Synap is unreachable. * **`save` surfaces failures**: raises `SynapIntegrationError` so the crew (and caller) know if persistence failed. * **Unsupported operations** (e.g., `delete`) are no-ops with a warning rather than raising, so they don't kill the crew. This is by design: read failures shouldn't break a crew run mid-flight, but silent write failures would corrupt the memory pool. *** ## Going further * [Patterns overview](/patterns/overview): reusable memory patterns across frameworks. * [Cookbook overview](/cookbook/overview): end-to-end worked examples. *** ## Next steps `BaseTool` implementations for AutoGen agents. Deps and tools for Pydantic AI. How `user_id` and `customer_id` interact across reads and writes. Direct ingestion API for pipelines that need finer control than `save`. # deepagents Source: https://docs.maximem.ai/integrations/deepagents A Synap memory backend, query-conditioned recall middleware, and tools for LangChain's deepagents harness. Give a [deepagents](https://docs.langchain.com/oss/python/deepagents/overview) agent persistent, searchable memory through Synap. deepagents reaches storage through `BackendProtocol` — a filesystem interface — and reaches memory through `MemoryMiddleware`, which pastes whole `AGENTS.md` files into the system prompt. This package plugs Synap into both, and adds a retrieval path the stock middleware cannot express. Requires Python 3.11+ and `deepagents>=0.7.4`. This covers the **`deepagents` library** — agents you build with `create_deep_agent(...)`. The separate `deepagents-code` terminal agent constructs its memory backend internally and does not accept a custom one, so it cannot use this package. Mount `SynapBackend` on a **route**, never as `backend=` on its own. As the default backend it would route the agent's source-code reads and writes through a memory API, and the agent would lose its working tree. ## Overview Three surfaces, mapped onto deepagents' own extension points. Adopt only the ones you need. | Surface | deepagents extension point | Purpose | | ------------------------------------ | ------------------------------------------------------- | ---------------------------------------------------------------------------------- | | `SynapBackend` | `create_deep_agent(backend=...)` via `CompositeBackend` | Memory reachable through the agent's own `read_file` / `grep` / `write_file` tools | | `SynapMemoryMiddleware` | `create_deep_agent(middleware=[...])` | Recall scoped to the user's actual question | | `SynapShortTermMiddleware` | `create_deep_agent(middleware=[...])` | Compacted conversation history, refreshed each turn | | `synap_st_instructions` | `create_deep_agent(system_prompt=...)` | Short-term context folded into a static system prompt | | `SynapSearchTool` / `SynapStoreTool` | `create_deep_agent(tools=[...])` | Explicit `search_memory` / `store_memory` the model can call | All of them take an already-constructed `MaximemSynapSDK` — your app owns the SDK and its credentials. ### When this is worth it The payoff scales with how much the agent remembers, and it is worth being concrete about that. In our own testing, an agent answering a question against a 200-memory scope needed **96 characters** of retrieved context to get it right. The same task with a plain-file memory backend pasted **24,853 characters** — the entire corpus — into the prompt on every turn to reach the same answer. That gap is the reason this integration exists, and it widens as memory grows. The trade is latency and small-corpus behaviour: a retrieval call is a network round trip, so expect a couple of extra seconds per turn versus reading a local file, and on a handful of memories a file backend is both faster and more reliable. Use Synap where memory outgrows a file — long-lived agents, many sessions, memory shared across projects — and leave a small static `AGENTS.md` on `FilesystemBackend`. `CompositeBackend` lets you do both at once. ## Setup ```bash pip theme={null} pip install maximem-synap-deepagents deepagents ``` ```bash uv theme={null} uv add maximem-synap-deepagents deepagents ``` ## Basic integration Mount Synap at `/memories/` and leave the repository on a normal filesystem backend: ```python theme={null} from deepagents import create_deep_agent from deepagents.backends import CompositeBackend from deepagents.backends.filesystem import FilesystemBackend from maximem_synap import MaximemSynapSDK from synap_deepagents import SynapBackend sdk = MaximemSynapSDK(api_key="sk-...") backend = CompositeBackend( default=FilesystemBackend(root_dir="/path/to/repo"), routes={"/memories/": SynapBackend(sdk, user_id="alice")}, ) agent = create_deep_agent( model="anthropic:claude-sonnet-5", backend=backend, memory=["/memories/AGENTS.md"], ) agent.invoke({"messages": [{"role": "user", "content": "What do I prefer?"}]}) ``` `CompositeBackend` routes by longest path prefix. Anything under `/memories/` reaches Synap; everything else goes to the repository, untouched. ## Core concepts ### `grep` becomes a semantic search This is the part worth knowing. On a Synap route, the agent's `grep` tool is **not** a regex match over file bytes. The pattern is passed to Synap as a natural-language query: ```python theme={null} # The agent runs this: grep("what deployment process does the user follow", path="/memories/") # It becomes this: sdk.fetch(search_query=["what deployment process does the user follow"], mode="accurate") ``` Stock deepagents `grep` can only find what is literally written in a file. This searches by meaning instead, so a memory can be phrased differently from the query and still match. Matches are attributed to `/memories/AGENTS.md`, so the agent can read that file for more context. If your agent tends to write regexes, say so in its system prompt. A model that assumes literal matching will write `^prefer.*` and misread the misses. Retrieval is a relevance search, not a guarantee. Some phrasings return nothing even when a matching memory is in scope, so treat an empty `grep` as "no strong match for this wording" rather than "not in memory". Where it matters, have the agent try a second phrasing, or read `/memories/AGENTS.md` directly for the unfiltered recall block. ### A new scope needs a corpus before recall is useful Recall is only as good as what is in the scope, and a nearly-empty scope returns nothing at all rather than a little. In testing, a scope holding three memories returned an empty context block on every setting, while the same scope at a dozen memories returned content consistently. That is worth knowing because of how it looks from the outside: the agent reads `/memories/AGENTS.md`, gets `file_not_found`, and behaves exactly as if the integration were broken. It isn't — there is genuinely nothing to return yet. Seed a real corpus, or let the agent accumulate one over several sessions, before judging recall quality. ### The recall file is synthesized, not stored `/memories/AGENTS.md` does not exist anywhere. Reading it calls `sdk.fetch()` and returns the formatted context as the file body. That is what makes `create_deep_agent(memory=["/memories/AGENTS.md"])` work with no other changes. ### Queued writes and read-after-write Synap ingestion is asynchronous — `create` returns `{ingestion_id, document_id, status: QUEUED}`. A memory written a moment ago is not yet retrievable, so an agent that writes a file and reads it back in the same turn would otherwise get nothing, which reads as data loss. `SynapBackend` keeps a short-TTL, per-process write-through cache so your own writes are always readable back. It is a read-after-write guarantee, **not** semantic dedup, and it does not survive a restart. The package never polls `wait_for_completion` on the agent's path. Waiting would cost turn latency without improving the answer: Synap may split one submitted document into several memories, or merge it with existing ones, so there is no count to wait for. ### There is no `delete` `sdk.memories.create()` returns an `ingestion_id`; `sdk.memories.delete()` needs a `memory_id`. They are different identifiers, and the memory does not exist yet at write time. So a path written through this backend cannot be resolved back to a durable memory. `delete` is optional in `BackendProtocol`, so `SynapBackend` inherits the default that raises `NotImplementedError`, and `CompositeBackend` reports it cleanly. Remove memories through the Synap API or dashboard with a memory id. ## Complete example: query-conditioned recall `create_deep_agent(memory=[...])` installs deepagents' own `MemoryMiddleware`, which calls `backend.download_files(paths)` — paths, and nothing else. Even with `SynapBackend` underneath, that is one unqueried digest per run. `SynapMemoryMiddleware` reads the pending user message first and passes it as the search query, so what lands in the prompt is scoped to what was actually asked: ```python theme={null} from deepagents import create_deep_agent from maximem_synap import MaximemSynapSDK from synap_deepagents import SynapMemoryMiddleware, SynapSearchTool sdk = MaximemSynapSDK(api_key="sk-...") agent = create_deep_agent( model="anthropic:claude-sonnet-5", middleware=[ SynapMemoryMiddleware( sdk=sdk, user_id="alice", customer_id="acme", max_results=20, mode="fast", ) ], tools=[SynapSearchTool(sdk=sdk, user_id="alice")], ) agent.invoke({ "messages": [{"role": "user", "content": "How do I usually deploy?"}] }) ``` Use `memory=[...]` **or** `SynapMemoryMiddleware`, not both — they write to the same part of the system prompt, and you would pay for two retrievals to say the same thing twice. ## Advanced patterns ### Short-term context Long-term memory and short-term context are different things. Short-term is the compacted history of the *current* conversation: ```python theme={null} from synap_deepagents import synap_st_instructions system_prompt = await synap_st_instructions( sdk, "conv_abc", system="You are a helpful coding agent." ) agent = create_deep_agent(model="...", system_prompt=system_prompt) ``` That is a snapshot taken once, at construction. For a long-running agent, use `SynapShortTermMiddleware` instead — it refreshes each turn: ```python theme={null} from synap_deepagents import SynapShortTermMiddleware agent = create_deep_agent( model="anthropic:claude-sonnet-5", middleware=[SynapShortTermMiddleware(sdk=sdk, conversation_id="conv_abc")], ) ``` ### Tuning retrieval Reads sit on the agent's startup path; `grep` is a deliberate question. They get different defaults, and both are configurable: ```python theme={null} SynapBackend( sdk, user_id="alice", mode="fast", # reads — on the startup path grep_mode="accurate", # grep — worth the latency precision_level="high", max_results=20, cache_ttl_seconds=300, ) ``` ### Error policy | Operation | Behaviour | | ------------------------------------------------------ | ----------------------------------------------------------- | | Reads (`read`, `grep`, `ls`, `glob`, `download_files`) | Degrade — log at `ERROR`, return empty or `file_not_found` | | Writes (`write`, `edit`, `upload_files`) | Raise `SynapIntegrationError` | | Recall in middleware | Degrades to an empty block — an outage must not end the run | | `delete` | Raises `NotImplementedError` | Read failures are always reported as `file_not_found`, never any other code. That is deliberate: deepagents' `MemoryMiddleware` raises `ValueError` on any download error code *except* `file_not_found`, so returning anything else during a Synap outage would end the agent run instead of degrading to an empty memory block. ## Going further * Mount the backend **and** the tools together. The backend covers automatic recall through `AGENTS.md`; the tools cover deliberate lookups with names the model already understands. * Set `document_type` and `ingest_mode` on writes when the agent is storing something other than notes — for example `document_type="meeting-transcript"`. * Scope with `customer_id` alone for shared team memory, or `user_id` + `customer_id` for per-person memory inside an organisation. ## Next steps deepagents runs on LangGraph — the checkpointer integration composes with this one. Retrievers, tools, and short-term context for LangChain itself. # Vercel eve Source: https://docs.maximem.ai/integrations/eve Memory tools and a per-turn short-term-context resolver for Vercel eve agents (TypeScript). Add persistent, cross-session memory to a [Vercel eve](https://vercel.com/eve) agent. eve is a filesystem-first framework for durable backend agents: you author an agent as files under `agent/`. This integration adds two of those files: memory **tools** the model can call, and an **instructions resolver** that injects Synap's short-term context into every turn. eve's own durability (Vercel Workflows) persists a single session's turn state so it survives crashes and redeploys; that is short-term *session* state, not cross-session memory. Synap is the durable, cross-session layer on top. Tested against `eve@0.25.1` (AI SDK v7); eve is in beta, so pin your version. `@maximem/synap-eve` uses the JavaScript SDK and needs Node.js 20+. eve deploys to Vercel Functions, which satisfies that. See [Installation → JavaScript and TypeScript SDK](/setup/installation#javascript-and-typescript-sdk). ## Overview This guide shows how to add Synap to an eve agent so it can: * Recall long-term memory and store facts on demand, via tools the model chooses to call * Automatically inject Synap's compacted short-term context into the system prompt every turn * Scope memory per user without the model ever seeing the identity The integration ships three factories, each meant to be the default export of a file under `agent/`: | Export | eve file | Purpose | | ------------------------- | ----------------------------- | ----------------------------------------------------------------------------- | | `createSynapSearchTool` | `agent/tools/synap_search.ts` | Model-driven memory search (`defineTool`) | | `createSynapStoreTool` | `agent/tools/synap_store.ts` | Model-driven memory store (`defineTool`) | | `createSynapInstructions` | `agent/instructions/synap.ts` | Always-on per-turn short-term recall into the system prompt (`defineDynamic`) | The filename under `agent/tools/` becomes the model-facing tool name. The instructions resolver **augments** `instructions.md`: it adds a system message, it never replaces your prompt. ## Setup Install the package alongside eve: ```bash theme={null} npm install @maximem/synap-eve @maximem/synap-js-sdk eve zod ``` Import the integration from its package name directly: `import { createSynapSearchTool } from "@maximem/synap-eve"`. The core SDK (`SynapClient`) comes from the separate `@maximem/synap-js-sdk` package. Configure your API key. Generate one from the [Synap Dashboard](https://synap.maximem.ai). ```bash .env theme={null} SYNAP_API_KEY=synap_your_key_here ``` Initialize the SDK once and export it so your `agent/` files can share it: ```typescript agent/lib/synap.ts theme={null} import { SynapClient } from "@maximem/synap-js-sdk"; export const sdk = new SynapClient(); await sdk.initialize(); ``` See [SDK Initialization](/sdk/initialization) for the full lifecycle and configuration options. ## Basic integration The smallest useful integration is a single search tool. Drop this file in and the model can pull long-term context whenever it decides it needs it: ```typescript agent/tools/synap_search.ts theme={null} import { createSynapSearchTool } from "@maximem/synap-eve"; import { sdk } from "../lib/synap.js"; // The filename is the tool name the model sees: `synap_search`. export default createSynapSearchTool({ sdk }); ``` Identity is resolved automatically from the eve session: the Synap `user_id` comes from `ctx.session.auth.current.principalId` and the `conversation_id` from `ctx.session.id`. On an authenticated channel that is all you need; the model never sees the user identity, and **reads degrade gracefully** (empty result on failure, so a recall miss never aborts the turn). To also inject short-term context on every turn without the model asking, add the instructions resolver below. *** ## Core concepts ### Memory tools Both tool factories return branded eve `ToolDefinition`s. Search reads; store writes: ```typescript agent/tools/synap_store.ts theme={null} import { createSynapStoreTool } from "@maximem/synap-eve"; import { sdk } from "../lib/synap.js"; export default createSynapStoreTool({ sdk }); ``` `synap_search` input: `{ query: string, maxResults?: number }`. `synap_store` input: `{ content: string, metadata?: Record }`. **Error policy** follows the Synap-wide contract: * **`synap_search` degrades gracefully**: returns `{ available: false }` and logs on failure. * **`synap_store` surfaces failures**: raises `SynapIntegrationError`, so the model sees that an ingestion outage happened rather than assuming the write succeeded. ### Short-term context resolver `createSynapInstructions` returns a `defineDynamic` resolver that runs at the start of every turn, fetches Synap's compacted short-term summary for the conversation, and lowers it to one extra system message: ```typescript agent/instructions/synap.ts theme={null} import { createSynapInstructions } from "@maximem/synap-eve"; import { sdk } from "../lib/synap.js"; export default createSynapInstructions({ sdk, style: "narrative", // "narrative" | "structured" | "bullet_points" }); ``` It **augments** your `agent/instructions.md`; it never replaces it. When there is no context (or on an SDK failure with the default `onError: "fallback"`) the resolver returns `null`, which eve treats as "contribute nothing," and the turn proceeds unchanged. ### Identity and scoping Every factory accepts the standard scoping options, which override the session-derived defaults: | Option | Default | Notes | | ---------------- | -------------------------------------- | ------------------------------------------------------------------------------------ | | `userId` | `ctx.session.auth.current.principalId` | **Required explicitly on unauthenticated channels** (where `auth.current` is `null`) | | `customerId` | none | Required on B2B Synap instances | | `conversationId` | `ctx.session.id` | Scopes to a single session | | `mode` (search) | `"accurate"` | `"fast"` for the hot path | The integration deliberately does **not** fall back to the session *initiator*: on a delegated or system-initiated session that is whoever started it (an admin or service), not the end user the turn acts for; using it would scope memory to the wrong principal. *** ## Complete example: memory-augmented agent A complete agent that has always-on short-term recall AND lets the model search/store explicitly is just a few files: ``` agent/ ├── agent.ts ├── instructions.md ├── lib/ │ └── synap.ts # new SynapClient() + sdk.initialize() ├── instructions/ │ └── synap.ts # createSynapInstructions({ sdk }) └── tools/ ├── synap_search.ts # createSynapSearchTool({ sdk }) └── synap_store.ts # createSynapStoreTool({ sdk }) ``` ```typescript agent/agent.ts theme={null} import { defineAgent } from "eve"; export default defineAgent({ model: "openai/gpt-5.4-mini", }); ``` ```markdown agent/instructions.md theme={null} You are a personal assistant with long-term memory. Use `synap_search` when you need older context that is not already in the prompt. Use `synap_store` when the user shares a new fact, preference, or decision. ``` ```typescript agent/instructions/synap.ts theme={null} import { createSynapInstructions } from "@maximem/synap-eve"; import { sdk } from "../lib/synap.js"; export default createSynapInstructions({ sdk }); ``` Run it locally and start a session: ```bash theme={null} npx eve dev curl -X POST http://127.0.0.1:3000/eve/v1/session \ -H 'content-type: application/json' \ -d '{"message":"What plan am I on?"}' ``` Three things to notice: 1. **The resolver and the tools are complementary.** `createSynapInstructions` sets the always-on baseline; the tools handle the explicit "I should look this up / bookmark this" path. 2. **Scope is derived per turn** from the eve session: no per-request agent construction needed on authenticated channels. 3. **The instructions are the policy.** Telling the model when to call `synap_search` / `synap_store` is what produces the explicit-memory behavior. *** ## Advanced patterns ### Unauthenticated channels If a channel does not authenticate the caller (`ctx.session.auth.current` is `null`), the session-derived `user_id` is unavailable. Pass an explicit `userId` (and `customerId` for B2B) into the factories: ```typescript agent/tools/synap_search.ts theme={null} export default createSynapSearchTool({ sdk, userId: "alice", customerId: "acme" }); ``` ### Latency vs. comprehensiveness `synap_search` accepts `mode`: | | `fast` | `accurate` | | -------- | -------------- | ------------------------------------------------------- | | Search | Vector + graph | Vector + graph + LLM subquery decomposition + reranking | | Best for | Real-time chat | Multi-entity queries | ```typescript theme={null} export default createSynapSearchTool({ sdk, mode: "fast" }); ``` ### Failure semantics * **Reads (`synap_search`, the instructions resolver) degrade gracefully**: empty result / `null`, logged. * **Writes (`synap_store`) surface failures**: raise `SynapIntegrationError`. Read failures shouldn't break a user-facing turn, but a silent write failure would let memory drift away from reality. *** ## Going further * [Patterns overview](/patterns/overview): reusable memory patterns across frameworks. * [Cookbook overview](/cookbook/overview): end-to-end worked examples. *** ## Next steps Model middleware for the Vercel AI SDK: the layer below eve. `SynapMemory` class and tools for Mastra. The retrieval API behind `synap_search` and the instructions resolver. How `userId`, `customerId`, and `conversationId` interact across reads. # Google ADK Source: https://docs.maximem.ai/integrations/google-adk FunctionTool factory that adds memory search and storage to Google Agent Development Kit agents. Add persistent, per-user memory to a Google ADK agent in one factory call. Synap exposes itself as two `FunctionTool` instances, and the agent decides when to recall or remember. Requires Python 3.11+. ## Overview This guide shows how to add Synap to a Google ADK application to build agents that: * Recall user-specific facts, preferences, and past conversations * Persist new information surfaced during a conversation * Stay multi-user-safe by binding scope at tool construction The Synap Google ADK integration ships a single factory: it returns the two `FunctionTool` instances ready to drop onto an `Agent`. | Export | Returns | Purpose | | -------------------- | ------------------------------- | -------------------------------------------------------------- | | `create_synap_tools` | `[search_memory, store_memory]` | Two ADK `FunctionTool` instances for memory recall and storage | ## Setup Install the package alongside the Google ADK: ```bash pip theme={null} pip install maximem-synap-google-adk google-adk ``` ```bash uv theme={null} uv add maximem-synap-google-adk google-adk # pip-compatible (existing venv): uv pip install maximem-synap-google-adk google-adk ``` The pip package is `maximem-synap-google-adk`, but the import drops the `maximem-` prefix and uses underscores: `from synap_google_adk import ...`. Configure your API key. Generate one from the [Synap Dashboard](https://synap.maximem.ai). ```bash .env theme={null} SYNAP_API_KEY=synap_your_key_here GOOGLE_API_KEY=your-google-api-key ``` Initialize the SDK once at application startup: ```python theme={null} from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK() await sdk.initialize() ``` See [SDK Initialization](/sdk/initialization) for the full lifecycle and configuration options. ## Basic integration The smallest useful integration calls `create_synap_tools` and passes the result straight into an `Agent`. The factory returns a list, so it fits the `tools=` parameter without unpacking: ```python theme={null} # pip install maximem-synap-google-adk google-adk from maximem_synap import MaximemSynapSDK from google.adk.agents import Agent from synap_google_adk import create_synap_tools sdk = MaximemSynapSDK() await sdk.initialize() tools = create_synap_tools( sdk=sdk, user_id="alice", customer_id="acme", # optional, required for B2B instances ) agent = Agent( name="MemoryAgent", model="gemini-2.0-flash", instruction=( "Use the synap_search tool to recall context about the user. " "Use synap_store to remember new facts." ), tools=tools, ) ``` The factory binds `user_id` and `customer_id` into the closures it returns. The model only ever sees `query`, `max_results`, and `content`, never the user identity. This prevents prompt-injection attempts from spoofing scope. *** ## Core concepts ### create\_synap\_tools `create_synap_tools` returns a two-element list: `[search_memory, store_memory]`. Both are ADK `FunctionTool` instances and can be passed directly to `Agent(tools=...)`: ```python theme={null} from synap_google_adk import create_synap_tools tools = create_synap_tools( sdk=sdk, user_id="alice", customer_id="acme", ) # tools[0] = search_memory # tools[1] = store_memory ``` ### search\_memory Tool signature exposed to the model: ```text theme={null} search_memory(query: str, max_results: int = 5) -> list[dict] ``` Returns a list of memory objects with the shape `{"content": "...", "type": "...", "confidence": float}`. The agent sees this as JSON and can reason over it directly. **Search failures degrade gracefully**: the tool returns `[]` and logs an error so the agent continues without recall rather than aborting. ### store\_memory Tool signature exposed to the model: ```text theme={null} store_memory(content: str, memory_type: str = "fact") -> dict ``` Returns `{"status": "stored", "id": "..."}` on success. **Store failures surface explicitly**: the tool raises `SynapIntegrationError` so the agent (and you) know if persistence failed. *** ## Complete example: per-user memory agent In a multi-user service, build a fresh tool set per request. Each agent run gets a scope-bound tool list, so two concurrent users cannot see each other's memories: ```python theme={null} from google.adk.agents import Agent from google.adk.runners import Runner from google.adk.sessions import InMemorySessionService from google.genai import types from synap_google_adk import create_synap_tools APP_NAME = "memory-agent" session_service = InMemorySessionService() def build_agent_for_user(sdk, user_id: str, customer_id: str | None = None) -> Agent: tools = create_synap_tools(sdk=sdk, user_id=user_id, customer_id=customer_id) return Agent( name="MemoryAgent", model="gemini-2.0-flash", instruction=( "You are a personal assistant with long-term memory.\n" "1. Always call search_memory FIRST for any question about the user.\n" "2. When the user shares a fact, preference, or decision, " "call store_memory before responding.\n" "3. If search_memory returns nothing, say so honestly." ), tools=tools, ) async def handle_request(sdk, user_id: str, message: str) -> str: agent = build_agent_for_user(sdk, user_id=user_id, customer_id="acme") # ADK's Runner needs an app name and a session service; run_async takes # keyword args and a structured Content message, and yields events. # It is not `run_async(message)`. See ADK's runtime docs for the full loop. runner = Runner(app_name=APP_NAME, agent=agent, session_service=session_service) session = await session_service.create_session(app_name=APP_NAME, user_id=user_id) content = types.Content(role="user", parts=[types.Part.from_text(text=message)]) async for event in runner.run_async( user_id=user_id, session_id=session.id, new_message=content ): if event.is_final_response(): return event.content.parts[0].text return "" # Usage await handle_request(sdk, user_id="alice", message="I just upgraded to the Pro plan.") # Later: reply = await handle_request(sdk, user_id="alice", message="What plan am I on?") # → "You're on the Pro plan." ``` Three things to notice in this pattern: 1. **Per-request agent construction is the safety pattern.** Each request gets its own scope-bound tool list, so no shared state can leak between users. 2. **The instruction is the policy.** Telling the model to always call `search_memory` first is what produces recall behavior; the tools are just plumbing. 3. **Failure modes split.** Search is best-effort (empty list on failure); store is strict (raises on failure) so silent data loss is impossible. *** ## Advanced patterns ### Multi-tenant scoping `create_synap_tools` accepts the standard scoping triple: `user_id` (required), optional `customer_id`, optional `conversation_id`. `customer_id` is required on B2B Synap instances and ignored on single-tenant ones. See [Memory Scopes](/concepts/memory-scopes). ```python theme={null} # User-scoped only tools = create_synap_tools(sdk=sdk, user_id="alice") # Organization-scoped (user sees org-shared memories too) tools = create_synap_tools(sdk=sdk, user_id="alice", customer_id="acme-corp") ``` ### Multi-agent setups Each `Agent` can carry its own tool list. Give a "memory-keeper" agent both tools and a "consumer" agent only `search_memory` to keep the write surface small: ```python theme={null} [search, _store] = create_synap_tools(sdk=sdk, user_id="alice", customer_id="acme") reader_only = Agent(name="Reader", model="gemini-2.0-flash", tools=[search]) ``` ### Failure semantics The integration follows the Synap-wide contract: * **`search_memory` degrades gracefully**: returns `[]` and logs an error if Synap is unreachable. * **`store_memory` surfaces failures**: raises `SynapIntegrationError` so the agent and caller know persistence failed. This is by design: read failures shouldn't break a user-facing answer, but silent write failures would let the memory drift away from reality. *** ## Going further * [Patterns overview](/patterns/overview): reusable memory patterns across frameworks. * [Cookbook overview](/cookbook/overview): end-to-end worked examples. *** ## Next steps Pipeline components for Haystack. `InMemoryDb` replacement for Agno agents. The retrieval API behind `search_memory`: modes, scopes, and response shapes. How `user_id` and `customer_id` interact across reads and writes. # Haystack Source: https://docs.maximem.ai/integrations/haystack Retriever and memory-writer pipeline components for Haystack RAG pipelines. Add persistent, per-user memory to a Haystack pipeline as two regular components: a retriever for the read side and a writer for the write side. Both drop into existing pipelines without restructuring. Requires Python 3.11+. ## Overview This guide shows how to add Synap to a Haystack application to build pipelines that: * Retrieve user-scoped memories as standard `Document` objects in a RAG flow * Persist each conversation turn back to Synap so future runs benefit from it * Compose freely with any other Haystack component (rerankers, prompt builders, generators) The Synap Haystack integration ships two drop-in pipeline components. Both follow Haystack's component contract so you can wire them into pipelines exactly like any built-in component. | Component | Role | Purpose | | ------------------- | ----- | -------------------------------------------- | | `SynapRetriever` | Read | Fetches Synap memories as `Document` objects | | `SynapMemoryWriter` | Write | Records conversation turns back to Synap | ## Setup Install the package alongside Haystack: ```bash pip theme={null} pip install maximem-synap-haystack haystack-ai ``` ```bash uv theme={null} uv add maximem-synap-haystack haystack-ai # pip-compatible (existing venv): uv pip install maximem-synap-haystack haystack-ai ``` The pip package is `maximem-synap-haystack`, but the import drops the `maximem-` prefix and uses underscores: `from synap_haystack import ...`. Configure your API key. Generate one from the [Synap Dashboard](https://synap.maximem.ai). ```bash .env theme={null} SYNAP_API_KEY=synap_your_key_here OPENAI_API_KEY=your-openai-api-key ``` Initialize the SDK once at application startup: ```python theme={null} from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK() await sdk.initialize() ``` See [SDK Initialization](/sdk/initialization) for the full lifecycle and configuration options. ## Basic integration The smallest useful integration plugs `SynapRetriever` into a pipeline and routes its `documents` output to a prompt builder: ```python theme={null} # pip install maximem-synap-haystack haystack-ai from maximem_synap import MaximemSynapSDK from haystack import Pipeline from haystack.components.builders import PromptBuilder from synap_haystack import SynapRetriever sdk = MaximemSynapSDK() await sdk.initialize() retriever = SynapRetriever( sdk=sdk, user_id="alice", customer_id="acme", # optional, required for B2B instances max_results=6, mode="fast", # "fast" or "accurate" ) pipeline = Pipeline() pipeline.add_component("retriever", retriever) pipeline.add_component("prompt_builder", PromptBuilder(template=your_template)) pipeline.connect("retriever.documents", "prompt_builder.documents") result = pipeline.run({"retriever": {"query": "project deadlines"}}) ``` **Retrieval failures degrade gracefully.** `SynapRetriever` emits an empty `documents` list and logs an error, so the rest of the pipeline keeps running. To close the loop and persist new turns for future retrievals, add `SynapMemoryWriter` after the generator. *** ## Core concepts ### Retriever `SynapRetriever` is a Haystack component that takes a `query` input and emits a `documents` output. Each returned `Document` has: * `content`: the memory text * `meta["type"]`: memory type (e.g. `"fact"`, `"preference"`) * `meta["confidence"]`: relevance score ```python theme={null} from synap_haystack import SynapRetriever retriever = SynapRetriever( sdk=sdk, user_id="alice", customer_id="acme", max_results=6, mode="fast", ) ``` The two retrieval modes trade latency against comprehensiveness: | | `fast` | `accurate` | | -------- | ---------------------------------------------- | ------------------------------------------------------- | | Search | Vector + graph (no LLM subquery decomposition) | Vector + graph + LLM subquery decomposition + reranking | | Best for | Real-time chat | Multi-entity queries | `fast` is lower-latency and suited to the hot path; `accurate` adds LLM-driven query decomposition and reranking for relationship-aware queries at a higher latency cost. Because the output shape matches Haystack's standard `Document`, you can route it through any reranker, prompt builder, or filter that accepts `documents`. ### Memory writer `SynapMemoryWriter` is the write side. Place it at the end of a pipeline so each LLM reply is captured as a memory for future retrievals: ```python theme={null} import uuid from synap_haystack import SynapMemoryWriter # conversation_id (and every other id) must be a valid UUID. writer = SynapMemoryWriter( sdk=sdk, conversation_id=str(uuid.uuid4()), user_id="alice", customer_id="acme", ) ``` It accepts a `replies` input (matching the output of standard generators like `OpenAIGenerator`) and emits a `result` summary. **Write failures surface explicitly.** `SynapMemoryWriter` raises `SynapIntegrationError` so the pipeline knows if persistence failed. *** ## Complete example: full RAG pipeline with memory loop The following pipeline retrieves user-scoped memories, builds a prompt, generates a response, and writes the response back to Synap, in one pass: ```python theme={null} import uuid from haystack import Pipeline from haystack.components.builders import PromptBuilder from haystack.components.generators import OpenAIGenerator from synap_haystack import SynapRetriever, SynapMemoryWriter def build_pipeline(sdk, user_id: str, conversation_id: str, customer_id: str | None = None) -> Pipeline: retriever = SynapRetriever( sdk=sdk, user_id=user_id, customer_id=customer_id, max_results=6, mode="fast", ) writer = SynapMemoryWriter( sdk=sdk, conversation_id=conversation_id, user_id=user_id, customer_id=customer_id, ) template = """ Given this context about the user: {% for doc in documents %} - {{ doc.content }} {% endfor %} Answer the question: {{ query }} """ pipeline = Pipeline() pipeline.add_component("retriever", retriever) pipeline.add_component("prompt", PromptBuilder(template=template)) pipeline.add_component("llm", OpenAIGenerator(model="gpt-4o")) pipeline.add_component("writer", writer) pipeline.connect("retriever.documents", "prompt.documents") pipeline.connect("prompt.prompt", "llm.prompt") pipeline.connect("llm.replies", "writer.replies") return pipeline # Usage pipeline = build_pipeline(sdk, user_id="alice", conversation_id=str(uuid.uuid4()), customer_id="acme") result = pipeline.run({ "retriever": {"query": "What are my priorities?"}, "prompt": {"query": "What are my priorities?"}, }) print(result["llm"]["replies"][0]) ``` Three things to notice in this pattern: 1. **Memory is just another retriever.** `SynapRetriever` emits standard `Document` objects, so you can mix it with any document store retriever via a `DocumentJoiner` if you want corpus context too. 2. **The write loop closes itself.** Each pipeline run ends by persisting the reply, so every subsequent run benefits from accumulating context. 3. **Scope is bound at construction.** The retriever and writer carry the user/customer scope; the pipeline graph never needs to know about user identity. *** ## Advanced patterns ### Multi-tenant scoping Both components accept the standard scoping triple: `user_id` (required), optional `customer_id`, optional `conversation_id`. `customer_id` is required on B2B Synap instances and ignored on single-tenant ones. See [Memory Scopes](/concepts/memory-scopes). ```python theme={null} # User-scoped only retriever = SynapRetriever(sdk=sdk, user_id="alice") # Organization-scoped retriever = SynapRetriever(sdk=sdk, user_id="alice", customer_id="acme") ``` For multi-tenant services, build the pipeline (or at least the retriever/writer components) per request so each invocation has the correct scope baked in. ### Combining with document retrieval `SynapRetriever`'s output is a standard `Document` list, so it slots into a `DocumentJoiner` next to your existing document store retriever: ```python theme={null} pipeline.add_component("doc_retriever", your_existing_retriever) pipeline.add_component("synap", SynapRetriever(sdk=sdk, user_id="alice")) pipeline.add_component("joiner", DocumentJoiner()) pipeline.connect("doc_retriever.documents", "joiner.documents") pipeline.connect("synap.documents", "joiner.documents") ``` User-specific facts and corpus chunks come back as a single ranked list to the prompt builder. ### Failure semantics The integration follows the Synap-wide contract: * **`SynapRetriever` degrades gracefully:** emits an empty `documents` list and logs an error if Synap is unreachable. * **`SynapMemoryWriter` surfaces failures:** raises `SynapIntegrationError` so the pipeline (and caller) know persistence failed. This is by design: read failures shouldn't break a user-facing turn, but silent write failures would let the memory drift away from reality. *** ## Going further * [Patterns overview](/patterns/overview): reusable memory patterns across frameworks. * [Cookbook overview](/cookbook/overview): end-to-end worked examples. *** ## Next steps Memory and retriever for LangChain. `BaseMemory` and retriever for LlamaIndex. The retrieval API that powers `SynapRetriever`: modes, scopes, and response shapes. How `user_id`, `customer_id`, and `conversation_id` interact across reads. # LangChain Source: https://docs.maximem.ai/integrations/langchain Build LangChain chains and agents with persistent, per-user memory powered by Synap. Build LangChain applications that remember context across sessions. Synap handles memory ingestion, retrieval, and per-user scoping; LangChain handles your chain or agent. Requires Python 3.11+. ## Overview This guide shows how to add Synap to a LangChain application to build agents that: * Maintain conversation history across sessions and processes * Retrieve user- and organization-scoped context as part of a RAG pipeline * Decide for themselves when to search or store memories The Synap LangChain integration ships four drop-in components. Each one slots into a native LangChain interface so you do not have to wrap or re-implement anything. | Component | LangChain interface | Purpose | | ----------------------------------- | ------------------------ | ------------------------------------------------------------- | | `SynapChatMessageHistory` | `BaseChatMessageHistory` | Persistent chat history per `conversation_id` | | `SynapCallbackHandler` | `BaseCallbackHandler` | Auto-records every LLM turn, no application changes | | `SynapRetriever` | `BaseRetriever` | Semantic retriever that returns Synap memories as `Document`s | | `SynapSearchTool`, `SynapStoreTool` | `BaseTool` | Agent-callable tools for explicit memory read/write | ## Setup Install the package alongside LangChain and your model provider: ```bash pip theme={null} pip install maximem-synap-langchain langchain langchain-openai ``` ```bash uv theme={null} uv add maximem-synap-langchain langchain langchain-openai # pip-compatible (existing venv): uv pip install maximem-synap-langchain langchain langchain-openai ``` The pip package is `maximem-synap-langchain`, but the import drops the `maximem-` prefix and uses underscores: `from synap_langchain import ...`. Configure your API key. Generate one from the [Synap Dashboard](https://synap.maximem.ai). ```bash .env theme={null} SYNAP_API_KEY=synap_your_key_here OPENAI_API_KEY=your-openai-api-key ``` Then initialize the SDK once at application startup: ```python theme={null} from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK() # picks up SYNAP_API_KEY from env await sdk.initialize() ``` See [SDK Initialization](/sdk/initialization) for the full lifecycle and configuration options. ## Basic integration The smallest useful integration is a chain with **automatic memory** via `SynapCallbackHandler`. Every turn is ingested without changing your chain logic: ```python theme={null} # pip install maximem-synap-langchain langchain langchain-openai import uuid from maximem_synap import MaximemSynapSDK from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate from synap_langchain import SynapCallbackHandler sdk = MaximemSynapSDK() await sdk.initialize() llm = ChatOpenAI(model="gpt-4o") prompt = ChatPromptTemplate.from_messages([ ("system", "You are a helpful assistant with long-term memory."), ("human", "{question}"), ]) chain = prompt | llm # conversation_id (and every other id) must be a valid UUID. conversation_id = str(uuid.uuid4()) handler = SynapCallbackHandler( sdk=sdk, conversation_id=conversation_id, user_id="alice", customer_id="acme", # optional, required for B2B instances ) response = await chain.ainvoke( {"question": "Remind me what we agreed on for the Q2 roadmap."}, config={"callbacks": [handler]}, ) ``` The handler observes the chain via LangChain's callback system, captures the user/assistant pair, and ingests it into Synap asynchronously. **Ingestion failures are logged at `ERROR` level and never propagate to your chain.** Your application keeps running even if Synap is unreachable. This is the smallest viable setup. To make the model *aware* of memory at inference time, combine the callback with `SynapChatMessageHistory` and/or `SynapRetriever` below. *** ## Core concepts ### Persistent chat history `SynapChatMessageHistory` implements LangChain's `BaseChatMessageHistory` interface. Wrap any chain with `RunnableWithMessageHistory` to give it conversation memory that survives restarts: ```python theme={null} from langchain_core.runnables.history import RunnableWithMessageHistory from synap_langchain import SynapChatMessageHistory def get_history(session_id: str) -> SynapChatMessageHistory: return SynapChatMessageHistory( sdk=sdk, conversation_id=session_id, user_id="alice", customer_id="acme", # optional ) chain_with_history = RunnableWithMessageHistory( chain, get_session_history=get_history, input_messages_key="question", history_messages_key="history", ) response = await chain_with_history.ainvoke( {"question": "What did we discuss last time?"}, config={"configurable": {"session_id": str(uuid.uuid4())}}, ) ``` Each `session_id` maps one-to-one to a Synap `conversation_id`. Messages are persisted on every turn and replayed on the next invocation. ### Automatic ingestion `SynapCallbackHandler` covers a different need: it does not feed history *into* the model, it **records every turn** for long-term memory. Use it whenever you want a chain's output to enrich the user's profile and become searchable later. ```python theme={null} from synap_langchain import SynapCallbackHandler handler = SynapCallbackHandler( sdk=sdk, conversation_id=str(uuid.uuid4()), user_id="alice", ) await chain.ainvoke( {"question": "Book me a flight to Tokyo next month."}, config={"callbacks": [handler]}, ) ``` You can attach the handler to a single invocation (as above) or globally with `chain.with_config(callbacks=[handler])`. ### Semantic retrieval `SynapRetriever` implements `BaseRetriever`. Use it inside `ConversationalRetrievalChain`, `create_retrieval_chain`, or any RAG pipeline that expects a retriever: ```python theme={null} from synap_langchain import SynapRetriever retriever = SynapRetriever( sdk=sdk, user_id="alice", customer_id="acme", max_results=8, mode="fast", # "fast" (vector + graph, no LLM decomposition) or "accurate" (adds LLM subquery decomposition + reranking) ) docs = await retriever.aget_relevant_documents("project deadlines") # Document.page_content = memory text # Document.metadata = {"confidence": 0.92, "type": "fact", ...} ``` The two retrieval modes trade latency against comprehensiveness: | | `fast` | `accurate` | | -------- | ---------------------------------------------- | ------------------------------------------------------- | | Search | Vector + graph (no LLM subquery decomposition) | Vector + graph + LLM subquery decomposition + reranking | | Best for | Real-time chat | Multi-entity queries | `fast` is lower-latency and suited to the hot path; `accurate` adds LLM-driven query decomposition and reranking for relationship-aware queries at a higher latency cost. See [Context Fetch](/sdk/context-fetch) for the full retrieval contract. ### Agent-callable memory For agent-style chains where the model decides when memory is relevant, expose `SynapSearchTool` and `SynapStoreTool`: ```python theme={null} from langchain.agents import AgentExecutor, create_tool_calling_agent from synap_langchain import SynapSearchTool, SynapStoreTool tools = [ SynapSearchTool(sdk=sdk, user_id="alice", customer_id="acme"), SynapStoreTool(sdk=sdk, user_id="alice", customer_id="acme"), ] agent = create_tool_calling_agent(llm, tools, prompt) executor = AgentExecutor(agent=agent, tools=tools) result = await executor.ainvoke({"input": "What did I say about my dietary preferences?"}) ``` The tools surface as `synap_search` and `synap_store` in the model's tool-calling schema, with descriptions that nudge the model toward calling them when context is needed. *** ## Complete example: support assistant with memory The following class assembles all four components into a single agent. It remembers conversation history, retrieves user-specific context, and lets the model store new facts explicitly. ```python theme={null} import uuid from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder from langchain_core.runnables.history import RunnableWithMessageHistory from langchain.agents import AgentExecutor, create_tool_calling_agent from synap_langchain import ( SynapChatMessageHistory, SynapCallbackHandler, SynapRetriever, SynapSearchTool, SynapStoreTool, ) class SupportAssistant: def __init__(self, sdk, user_id: str, customer_id: str | None = None): self.sdk = sdk self.user_id = user_id self.customer_id = customer_id self.llm = ChatOpenAI(model="gpt-4o", temperature=0.2) self.retriever = SynapRetriever( sdk=sdk, user_id=user_id, customer_id=customer_id, max_results=6, mode="fast", ) self.tools = [ SynapSearchTool(sdk=sdk, user_id=user_id, customer_id=customer_id), SynapStoreTool(sdk=sdk, user_id=user_id, customer_id=customer_id), ] prompt = ChatPromptTemplate.from_messages([ ("system", "You are a support assistant. Use known context when relevant. " "Call synap_search if you need older context, and synap_store " "to remember new facts the user shares.\n\n" "Known context:\n{context}"), MessagesPlaceholder("history"), ("human", "{input}"), MessagesPlaceholder("agent_scratchpad"), ]) agent = create_tool_calling_agent(self.llm, self.tools, prompt) executor = AgentExecutor(agent=agent, tools=self.tools) self.chain = RunnableWithMessageHistory( executor, get_session_history=self._history, input_messages_key="input", history_messages_key="history", ) def _history(self, session_id: str) -> SynapChatMessageHistory: return SynapChatMessageHistory( sdk=self.sdk, conversation_id=session_id, user_id=self.user_id, customer_id=self.customer_id, ) async def ask(self, session_id: str, message: str) -> str: # Retrieve relevant context for this turn docs = await self.retriever.aget_relevant_documents(message) context = "\n".join(f"- {d.page_content}" for d in docs) or "No prior context." # Auto-record this turn for future memory callback = SynapCallbackHandler( sdk=self.sdk, conversation_id=session_id, user_id=self.user_id, customer_id=self.customer_id, ) result = await self.chain.ainvoke( {"input": message, "context": context}, config={ "configurable": {"session_id": session_id}, "callbacks": [callback], }, ) return result["output"] # Usage assistant = SupportAssistant(sdk, user_id="alice", customer_id="acme") reply = await assistant.ask(str(uuid.uuid4()), "I just upgraded to the Pro plan.") ``` Three things to notice in this pattern: 1. **`SynapChatMessageHistory`** keeps the chain itself stateful turn-to-turn. 2. **`SynapRetriever`** injects per-user long-term context as a system message, independent of the chat history. 3. **`SynapCallbackHandler`** persists each turn back to Synap so future invocations can retrieve it. You can drop any one of these and still have a working agent. Adding all three gives you a memory loop that improves over time. *** ## Advanced patterns ### Multi-tenant scoping Every component accepts the same scoping triple: `user_id`, optional `customer_id`, and optional `conversation_id`: ```python theme={null} retriever = SynapRetriever( sdk=sdk, user_id="alice", customer_id="acme", # scope retrievals to acme's tenancy conversation_id=str(uuid.uuid4()), # bias ranking toward this conversation ) ``` `customer_id` is required for B2B Synap instances and ignored on single-tenant instances. See [Memory Scopes](/concepts/memory-scopes) for the full hierarchy. ### Combining automatic and explicit memory The four components compose freely. A common production setup: * `SynapChatMessageHistory` for the running conversation buffer * `SynapCallbackHandler` for long-term ingestion (runs in parallel with history) * `SynapRetriever` injected as a system message before each turn * `SynapSearchTool` exposed to the model for explicit lookups when retrieval misses The callback handler and the retriever do not conflict: the handler writes, the retriever reads. The chat-history component is orthogonal to both: it stores raw messages for replay, while the handler ingests structured memories. ### Tuning retrieval mode per call `SynapRetriever` accepts a default `mode` at construction time, but the underlying `sdk.conversation.context.fetch()` call accepts a `mode` override too. For a single high-recall query inside an otherwise low-latency agent, swap the retriever's mode temporarily: ```python theme={null} retriever.mode = "accurate" docs = await retriever.aget_relevant_documents("Summarize everything about the Acme account.") retriever.mode = "fast" ``` ### Failure semantics The Synap integration follows the Synap-wide contract: * **Retrieval failures degrade gracefully:** `SynapRetriever` returns `[]` and logs an error * **Callback failures degrade gracefully:** `SynapCallbackHandler` logs an `ERROR` and your chain continues * **Explicit tool calls surface failures:** `SynapSearchTool` / `SynapStoreTool` raise `SynapIntegrationError` so the model can react This is by design: the read path should never break a user-facing turn, while the write path must surface errors so callers know when persistence failed. *** ## Going further * [Patterns overview](/patterns/overview): reusable memory patterns across frameworks. * [Cookbook overview](/cookbook/overview): end-to-end worked examples. *** ## Next steps Checkpointer and cross-thread store for LangGraph state graphs. The retrieval API that powers `SynapRetriever`: `fast` vs `accurate`, scopes, and response shapes. Direct ingestion API for custom pipelines that need finer control than the callback handler. How `user_id`, `customer_id`, and `conversation_id` interact across retrievals. # LangGraph Source: https://docs.maximem.ai/integrations/langgraph Persistent checkpoints and cross-thread long-term memory for LangGraph state graphs. Make LangGraph state graphs durable and aware of long-term context. Synap stores thread checkpoints so conversations survive restarts, and exposes a cross-thread `BaseStore` that any graph node can read and write. Requires Python 3.11+. ## Overview This guide shows how to plug Synap into a LangGraph application to build graphs that: * Persist their thread state across processes and restarts * Resume any conversation by its `thread_id` without losing context * Share long-term memory (user preferences, facts, episodes) across all threads belonging to the same user The Synap LangGraph integration ships two drop-in components. Each one implements a native LangGraph interface, so you do not have to wrap or re-implement anything. | Component | LangGraph interface | Purpose | | ---------------------- | --------------------- | ------------------------------------------------------ | | `SynapCheckpointSaver` | `BaseCheckpointSaver` | Thread-level checkpoint persistence per `thread_id` | | `SynapStore` | `BaseStore` | Cross-thread long-term memory accessible from any node | ## Setup Install the package alongside LangGraph: ```bash pip theme={null} pip install maximem-synap-langgraph langgraph langchain-openai ``` ```bash uv theme={null} uv add maximem-synap-langgraph langgraph langchain-openai # pip-compatible (existing venv): uv pip install maximem-synap-langgraph langgraph langchain-openai ``` The pip package is `maximem-synap-langgraph`, but the import drops the `maximem-` prefix and uses underscores: `from synap_langgraph import ...`. Configure your API key. Generate one from the [Synap Dashboard](https://synap.maximem.ai). ```bash .env theme={null} SYNAP_API_KEY=synap_your_key_here OPENAI_API_KEY=your-openai-api-key ``` Initialize the SDK once at application startup: ```python theme={null} from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK() await sdk.initialize() ``` See [SDK Initialization](/sdk/initialization) for the full lifecycle and configuration options. ## Basic integration The smallest useful integration replaces LangGraph's in-memory checkpointer with `SynapCheckpointSaver`. Compile your graph with the saver, then invoke it as usual: every node transition is persisted, and the next invocation with the same `thread_id` resumes from the last checkpoint: ```python theme={null} # pip install maximem-synap-langgraph langgraph langchain-openai from maximem_synap import MaximemSynapSDK from langchain_core.messages import HumanMessage from langgraph.graph import StateGraph from synap_langgraph import SynapCheckpointSaver sdk = MaximemSynapSDK() await sdk.initialize() saver = SynapCheckpointSaver(sdk=sdk, user_id="alice") graph = StateGraph(...) # ... add nodes and edges ... app = graph.compile(checkpointer=saver) config = {"configurable": {"thread_id": "thread-001"}} result = await app.ainvoke( {"messages": [HumanMessage("Hello")]}, config=config, ) ``` Restart your process, call `ainvoke` again with the same `thread_id`, and the graph picks up where it left off. **Checkpoint retrieval failures degrade gracefully.** The graph starts from an empty state and the error is logged. This covers per-thread durability. To give your graph access to memories *outside* a single thread, layer in `SynapStore` below. *** ## Core concepts ### Thread checkpoints `SynapCheckpointSaver` implements LangGraph's `BaseCheckpointSaver` interface. Every state transition the graph commits is sent to Synap and tagged with the active `thread_id`: ```python theme={null} from synap_langgraph import SynapCheckpointSaver saver = SynapCheckpointSaver( sdk=sdk, user_id="alice", customer_id="acme", # optional, required for B2B instances ) app = graph.compile(checkpointer=saver) ``` Each `thread_id` you pass through `config.configurable.thread_id` maps one-to-one to a Synap conversation. Replaying a thread is just another `ainvoke` with the same `thread_id`: Synap returns the stored checkpoint and LangGraph rehydrates state from it. In addition to exact-thread replay, the saver supports fuzzy retrieval: when no checkpoint exists for a `thread_id`, Synap can return the closest semantically-similar thread for the user. This is useful for "continue where I left off"-style flows that do not pin a thread ID up front. ### Cross-thread memory `SynapStore` implements `BaseStore` and gives every node in the graph access to long-term memory that spans all of the user's threads: ```python theme={null} from synap_langgraph import SynapStore store = SynapStore( sdk=sdk, user_id="alice", customer_id="acme", ) app = graph.compile(checkpointer=saver, store=store) ``` Inside a graph node, access the store through the `store` keyword argument that LangGraph injects: ```python theme={null} async def assistant_node(state, config, *, store): # Retrieve cross-thread memories scoped to this user memories = await store.asearch( ("user", "alice"), query="project preferences", ) # Write a new memory that any future thread can read await store.aput( ("user", "alice"), key="pref-001", value={"content": "Prefers async communication", "type": "preference"}, ) return state ``` The namespace tuple (`("user", "alice")` above) scopes reads and writes to a particular memory partition. Use `("user", user_id)` for per-user memory and `("customer", customer_id)` for tenant-wide memory. Read [Memory Scopes](/concepts/memory-scopes) for the full hierarchy. *** ## Complete example: agent that remembers across threads The following graph wires both components together. Each thread is durable on its own, *and* the assistant can recall facts from earlier conversations the same user had under a different `thread_id`: ```python theme={null} from typing import TypedDict, Annotated from langchain_core.messages import HumanMessage, AIMessage, AnyMessage from langchain_openai import ChatOpenAI from langgraph.graph import StateGraph, START, END from langgraph.graph.message import add_messages from synap_langgraph import SynapCheckpointSaver, SynapStore class AgentState(TypedDict): messages: Annotated[list[AnyMessage], add_messages] llm = ChatOpenAI(model="gpt-4o") async def recall(state, config, *, store): """Inject cross-thread memories as a system message.""" user_id = config["configurable"]["user_id"] last_user = state["messages"][-1].content memories = await store.asearch(("user", user_id), query=last_user) context = "\n".join(f"- {m.value['content']}" for m in memories) or "No prior memory." return {"messages": [HumanMessage(content=f"[memory]\n{context}")]} async def respond(state): reply = await llm.ainvoke(state["messages"]) return {"messages": [reply]} async def remember(state, config, *, store): """Persist the last user/assistant exchange as a durable memory.""" user_id = config["configurable"]["user_id"] user_msg, ai_msg = state["messages"][-2], state["messages"][-1] await store.aput( ("user", user_id), key=f"turn-{ai_msg.id}", value={"content": f"User: {user_msg.content}\nAssistant: {ai_msg.content}"}, ) return {} graph = StateGraph(AgentState) graph.add_node("recall", recall) graph.add_node("respond", respond) graph.add_node("remember", remember) graph.add_edge(START, "recall") graph.add_edge("recall", "respond") graph.add_edge("respond", "remember") graph.add_edge("remember", END) saver = SynapCheckpointSaver(sdk=sdk, user_id="alice", customer_id="acme") store = SynapStore(sdk=sdk, user_id="alice", customer_id="acme") app = graph.compile(checkpointer=saver, store=store) config = {"configurable": {"thread_id": "session-42", "user_id": "alice"}} result = await app.ainvoke( {"messages": [HumanMessage("What did we agree on for the Q2 roadmap?")]}, config=config, ) ``` Three things to notice in this pattern: 1. **`SynapCheckpointSaver`** lets the graph resume mid-conversation by `thread_id`, even after a restart. 2. **`SynapStore`** is read in `recall` and written in `remember`, so each thread enriches a shared user-level memory pool. 3. **Memory and thread state are independent.** You can drop the store and keep just the checkpointer (or vice versa) and the graph still works. *** ## Advanced patterns ### Multi-tenant scoping Both components accept the same scoping triple (`user_id`, optional `customer_id`, optional `conversation_id`) and namespace store entries with tuples like `("user", user_id)` or `("customer", customer_id)`. `customer_id` is required on B2B Synap instances and ignored on single-tenant ones. See [Memory Scopes](/concepts/memory-scopes). ```python theme={null} store = SynapStore(sdk=sdk, user_id="alice", customer_id="acme") # User-scoped memory await store.aput(("user", "alice"), key="k1", value={...}) # Customer-scoped memory, readable by every user in the tenant await store.aput(("customer", "acme"), key="k2", value={...}) ``` ### Streaming with persistence The checkpointer is fully compatible with `astream`, so token-by-token streaming and durable state are not mutually exclusive: ```python theme={null} async for event in app.astream( {"messages": [HumanMessage("Hi")]}, config={"configurable": {"thread_id": "session-42", "user_id": "alice"}}, ): print(event) ``` Each emitted event corresponds to a node transition that is also persisted by `SynapCheckpointSaver`. ### Failure semantics `SynapCheckpointSaver` and `SynapStore` follow the Synap integration contract: * **Read failures degrade gracefully:** `aget`, `asearch`, and checkpoint loads return empty results and log an error, so the graph keeps running. * **Write failures surface explicitly:** `aput` and checkpoint commits raise `SynapIntegrationError` so callers know if persistence failed. This is by design: a transient outage should never break a user-facing turn, but a silent checkpoint loss would be invisible and dangerous. *** ## Going further * [Patterns overview](/patterns/overview): reusable memory patterns across frameworks. * [Cookbook overview](/cookbook/overview): end-to-end worked examples. *** ## Next steps Memory, retriever, and tools for LangChain chains and agents. How `user_id`, `customer_id`, and namespaces interact across stores. The retrieval API that powers `SynapStore.asearch`. Direct ingestion API for pipelines that need finer control than the store. # LiveKit Agents Source: https://docs.maximem.ai/integrations/livekit-agents Memory preloading, turn recording, and on-demand search for LiveKit voice agents. Give a LiveKit voice agent long-term memory across calls. Synap preloads the user's history into the `ChatContext` before the first turn, records every committed turn during the session, and exposes search/store function tools the LLM can call mid-conversation. Requires Python 3.11+. ## Overview This guide shows how to add Synap to a LiveKit Agents application to build voice agents that: * Start every call already aware of the user's history, no warm-up turn needed * Record every committed turn back to Synap so memory grows with each call * Search and store memories mid-conversation when the model decides it's relevant The Synap LiveKit Agents integration ships four exports: two lifecycle helpers and two function tools. | Export | Role | Purpose | | ------------------------ | ------------------ | ----------------------------------------------------------------------- | | `preload_synap_context` | Lifecycle (start) | Injects long-term memory into a `ChatContext` before the session begins | | `attach_synap_recording` | Lifecycle (during) | Records every committed turn back to Synap | | `synap_search_tool` | Function tool | LLM-callable memory search | | `synap_store_tool` | Function tool | LLM-callable memory storage | ## Setup Install the package alongside LiveKit Agents: ```bash pip theme={null} pip install maximem-synap-livekit-agents livekit-agents ``` ```bash uv theme={null} uv add maximem-synap-livekit-agents livekit-agents # pip-compatible (existing venv): uv pip install maximem-synap-livekit-agents livekit-agents ``` The pip package is `maximem-synap-livekit-agents`, but the import drops the `maximem-` prefix and uses underscores: `from synap_livekit_agents import ...`. Configure your API key. Generate one from the [Synap Dashboard](https://synap.maximem.ai). ```bash .env theme={null} SYNAP_API_KEY=synap_your_key_here OPENAI_API_KEY=your-openai-api-key LIVEKIT_API_KEY=your-livekit-key LIVEKIT_API_SECRET=your-livekit-secret ``` Initialize the SDK once at the agent worker's startup: ```python theme={null} from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK() await sdk.initialize() ``` See [SDK Initialization](/sdk/initialization) for the full lifecycle and configuration options. ## Basic integration The smallest useful integration preloads memory before the session starts and attaches recording during it. The agent now opens with knowledge of the user's history and grows its memory with every committed turn: ```python theme={null} # pip install maximem-synap-livekit-agents livekit-agents from maximem_synap import MaximemSynapSDK from livekit.agents import Agent, AgentSession, JobContext from livekit.agents.llm import ChatContext from synap_livekit_agents import preload_synap_context, attach_synap_recording sdk = MaximemSynapSDK() await sdk.initialize() async def entrypoint(ctx: JobContext): await ctx.connect() chat_ctx = ChatContext() await preload_synap_context( chat_ctx=chat_ctx, sdk=sdk, user_id="alice", customer_id="acme", # optional, required for B2B instances max_results=8, ) agent = Agent( instructions="You are a voice assistant with long-term memory.", chat_ctx=chat_ctx, ) session = AgentSession(...) conversation_id = attach_synap_recording( session=session, sdk=sdk, user_id="alice", customer_id="acme", ) await session.start(agent=agent, room=ctx.room) ``` **Preload failures degrade gracefully.** The session starts with empty context and logs an error. **Recording failures are also non-fatal.** Individual turn writes are retried, and persistent failures surface in logs rather than killing the call. For mid-conversation search and store (the model calls them when needed), add the function tools (see below). *** ## Core concepts ### preload\_synap\_context Loads the user's long-term memories as system messages in the `ChatContext` *before* the session starts. This gives the LLM awareness of the user's history from the very first turn, with no tool call needed. ```python theme={null} await preload_synap_context( chat_ctx=chat_ctx, sdk=sdk, user_id="alice", customer_id="acme", max_results=8, mode="fast", # "fast" or "accurate" ) ``` Voice latency is tight, so `mode="fast"` is the default. **Failures degrade gracefully.** The session starts with empty context rather than raising. ### attach\_synap\_recording Subscribes to the `AgentSession`'s turn-commit events and ingests each committed turn asynchronously. Returns the `conversation_id` for the session (auto-generated if you don't pass one): ```python theme={null} import uuid # If you pass conversation_id explicitly it must be a valid UUID; # omit it to let Synap auto-generate one. conversation_id = attach_synap_recording( session=session, sdk=sdk, user_id="alice", customer_id="acme", conversation_id=str(uuid.uuid4()), # optional; auto-generated if omitted ) ``` Recording happens out-of-band: it never blocks the audio path. Individual turn writes that fail are retried internally; persistent failures surface in logs. ### Function tools For mid-conversation lookups or saves, expose the search and store tools to the LLM. The `synap_search_tool` / `synap_store_tool` factories return LiveKit `@function_tool`s, so the LiveKit LLM bridge picks them up as function calls automatically: ```python theme={null} from synap_livekit_agents import synap_search_tool, synap_store_tool agent = Agent( instructions="You are a voice assistant with long-term memory.", chat_ctx=chat_ctx, tools=[ synap_search_tool(sdk=sdk, user_id="alice", max_results=5), synap_store_tool(sdk=sdk, user_id="alice"), ], ) ``` The scoping triple is bound at tool construction: the model only ever sees the `query` / `content` parameters, never `user_id`. *** ## Complete example: voice agent with full memory loop The pattern below assembles all four exports into a single entrypoint. The agent preloads context, records turns, and can search or store memories mid-call: ```python theme={null} from livekit.agents import Agent, AgentSession, JobContext, WorkerOptions, cli from livekit.agents.llm import ChatContext from synap_livekit_agents import ( preload_synap_context, attach_synap_recording, synap_search_tool, synap_store_tool, ) async def entrypoint(ctx: JobContext): await ctx.connect() # Identify the caller (e.g. from a JWT claim on ctx.room) user_id = ctx.room.metadata.get("user_id", "anonymous") customer_id = ctx.room.metadata.get("customer_id") # 1. Preload long-term memory before the session begins chat_ctx = ChatContext() await preload_synap_context( chat_ctx=chat_ctx, sdk=sdk, user_id=user_id, customer_id=customer_id, max_results=8, ) # 2. Build the agent with on-demand search/store tools agent = Agent( instructions=( "You are a voice assistant with long-term memory. " "Use synap_search for any question about the caller's history. " "Use synap_store when the caller shares a new fact or decision." ), chat_ctx=chat_ctx, tools=[ synap_search_tool(sdk=sdk, user_id=user_id, customer_id=customer_id, max_results=5), synap_store_tool(sdk=sdk, user_id=user_id, customer_id=customer_id), ], ) # 3. Attach turn recording so every committed turn enters Synap session = AgentSession(...) attach_synap_recording( session=session, sdk=sdk, user_id=user_id, customer_id=customer_id, ) await session.start(agent=agent, room=ctx.room) if __name__ == "__main__": cli.run_app(WorkerOptions(entrypoint_fnc=entrypoint)) ``` Three things to notice in this pattern: 1. **Preload + record + tools form the loop.** Preload reads memory in, record writes turns out, and tools let the model do explicit lookups in between. 2. **Scope is per-call.** `user_id` and `customer_id` are pulled from room metadata, so each call has its own memory scope without any global state. 3. **Failure modes are voice-friendly.** Memory operations never block the audio path; they degrade or retry silently so the caller hears no glitch. *** ## Advanced patterns ### Multi-tenant scoping All four exports accept the standard scoping triple: `user_id` (required), optional `customer_id`, optional `conversation_id`. `customer_id` is required on B2B Synap instances and ignored on single-tenant ones. See [Memory Scopes](/concepts/memory-scopes). ```python theme={null} await preload_synap_context( chat_ctx=chat_ctx, sdk=sdk, user_id="alice", customer_id="acme", ) ``` For multi-tenant call centers, pull `user_id` / `customer_id` from room metadata or JWT claims at entrypoint time, never hardcode them. ### Choosing preload vs. tools * **Preload only:** the LLM sees the most relevant memories from turn one. Best when memory is small and you want zero per-turn latency overhead. * **Tools only:** the LLM searches on demand. Best when memory is large and only a few queries need recall. * **Both:** production setups where the opening greeting can reference long-term context AND the model can dig deeper mid-call. ### Failure semantics The integration follows the Synap-wide contract, adapted for voice latency: * **`preload_synap_context` degrades gracefully:** empty context on failure, error logged. * **`attach_synap_recording` retries internally:** individual turn write failures are retried; persistent failures log but don't crash the call. * **`synap_search_tool` degrades gracefully:** returns `[]` and logs on failure. * **`synap_store_tool` surfaces failures:** raises `SynapIntegrationError` so the model (and you) know if persistence failed. This is by design: a voice call should never break because of a transient memory glitch, but explicit write failures must be visible. *** ## Going further * [Voice agent pattern](/patterns/voice-agent-livekit): the reference architecture for memory-backed voice agents. * [Voice concierge cookbook](/cookbook/voice-concierge): an end-to-end worked voice example. *** ## Next steps Frame processors for Pipecat voice pipelines. Hooks and MCP server for the Claude Agent SDK. The retrieval API behind `preload_synap_context` and `synap_search_tool`. How `user_id`, `customer_id`, and `conversation_id` interact across reads. # LlamaIndex Source: https://docs.maximem.ai/integrations/llamaindex BaseMemory implementation and semantic retriever for LlamaIndex pipelines. Give LlamaIndex chat engines and RAG pipelines persistent memory backed by Synap. Conversations survive restarts, and Synap-stored memories sit alongside your document retrieval as first-class context. Requires Python 3.11+. ## Overview This guide shows how to add Synap to a LlamaIndex application to build pipelines that: * Maintain chat history across sessions and processes * Retrieve user-scoped memories alongside document chunks in a RAG flow * Fuse memory-based and document-based retrieval into a single ranked result set The Synap LlamaIndex integration ships two drop-in components. Each one implements a native LlamaIndex interface, so you can use it anywhere a vanilla LlamaIndex memory or retriever is accepted. | Component | LlamaIndex interface | Purpose | | ----------------- | -------------------- | ------------------------------------------------- | | `SynapChatMemory` | `BaseMemory` | Persistent chat history per `conversation_id` | | `SynapRetriever` | `BaseRetriever` | Returns `NodeWithScore` objects for RAG pipelines | ## Setup Install the package alongside LlamaIndex: ```bash pip theme={null} pip install maximem-synap-llamaindex llama-index llama-index-llms-openai ``` ```bash uv theme={null} uv add maximem-synap-llamaindex llama-index llama-index-llms-openai # pip-compatible (existing venv): uv pip install maximem-synap-llamaindex llama-index llama-index-llms-openai ``` The pip package is `maximem-synap-llamaindex`, but the import drops the `maximem-` prefix and uses underscores: `from synap_llamaindex import ...`. Configure your API key. Generate one from the [Synap Dashboard](https://synap.maximem.ai). ```bash .env theme={null} SYNAP_API_KEY=synap_your_key_here OPENAI_API_KEY=your-openai-api-key ``` Initialize the SDK once at application startup: ```python theme={null} from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK() await sdk.initialize() ``` See [SDK Initialization](/sdk/initialization) for the full lifecycle and configuration options. ## Basic integration The smallest useful integration plugs `SynapChatMemory` into any LlamaIndex chat engine. Past turns are loaded automatically on each call, and new turns are persisted on the way out: ```python theme={null} # pip install maximem-synap-llamaindex llama-index llama-index-llms-openai import uuid from maximem_synap import MaximemSynapSDK from llama_index.core.chat_engine import CondensePlusContextChatEngine from synap_llamaindex import SynapChatMemory sdk = MaximemSynapSDK() await sdk.initialize() # conversation_id (and every other id) must be a valid UUID. memory = SynapChatMemory( sdk=sdk, conversation_id=str(uuid.uuid4()), user_id="alice", customer_id="acme", # optional, required for B2B instances ) chat_engine = CondensePlusContextChatEngine.from_defaults( retriever=your_doc_retriever, memory=memory, ) response = await chat_engine.achat("What were my action items from last week?") ``` `SynapChatMemory` loads prior messages on `get()` and writes new turns back to Synap on `put()`. **Failed reads return an empty buffer and log an error; failed writes surface explicitly** so callers know if persistence failed. To make user-specific memories *retrievable* inside the chat engine (alongside or in place of documents), layer in `SynapRetriever` below. *** ## Core concepts ### Persistent chat memory `SynapChatMemory` implements `BaseMemory`. Every LlamaIndex chat engine accepts a memory object: drop this one in to make the conversation durable: ```python theme={null} from synap_llamaindex import SynapChatMemory memory = SynapChatMemory( sdk=sdk, conversation_id=str(uuid.uuid4()), user_id="alice", customer_id="acme", ) ``` Each `conversation_id` maps one-to-one to a Synap conversation. Restart your process, instantiate `SynapChatMemory` again with the same `conversation_id`, and the chat engine resumes with the prior history. ### Semantic retrieval `SynapRetriever` implements `BaseRetriever` and returns `NodeWithScore` objects, the same shape every LlamaIndex RAG component expects. Use it as the retriever of a `RetrieverQueryEngine`, or as a sub-retriever inside a `RouterRetriever` / `QueryFusionRetriever`: ```python theme={null} from synap_llamaindex import SynapRetriever retriever = SynapRetriever( sdk=sdk, user_id="alice", customer_id="acme", max_results=6, mode="accurate", # "fast" (vector + graph, no LLM decomposition) or "accurate" (adds LLM subquery decomposition + reranking) ) nodes = await retriever.aretrieve("What are the user's project preferences?") # node.text = memory text # node.score = relevance score ``` The two retrieval modes trade latency against comprehensiveness: | | `fast` | `accurate` | | -------- | ---------------------------------------------- | ------------------------------------------------------- | | Search | Vector + graph (no LLM subquery decomposition) | Vector + graph + LLM subquery decomposition + reranking | | Best for | Real-time chat | Multi-entity queries | `fast` is lower-latency and suited to the hot path; `accurate` adds LLM-driven query decomposition and reranking for relationship-aware queries at a higher latency cost. See [Context Fetch](/sdk/context-fetch) for the full retrieval contract. *** ## Complete example: support assistant with memory + docs The following pipeline gives a chat engine both Synap-backed conversation memory *and* a fused retriever that blends user-specific memories with document chunks: ```python theme={null} import uuid from llama_index.core import VectorStoreIndex, SimpleDirectoryReader from llama_index.core.chat_engine import CondensePlusContextChatEngine from llama_index.core.retrievers import QueryFusionRetriever from llama_index.llms.openai import OpenAI from synap_llamaindex import SynapChatMemory, SynapRetriever class SupportAssistant: def __init__(self, sdk, user_id: str, customer_id: str | None = None): self.sdk = sdk self.user_id = user_id self.customer_id = customer_id # Document retriever: your existing RAG corpus docs = SimpleDirectoryReader("./knowledge_base").load_data() doc_index = VectorStoreIndex.from_documents(docs) doc_retriever = doc_index.as_retriever(similarity_top_k=4) # Synap retriever: user-scoped memories memory_retriever = SynapRetriever( sdk=sdk, user_id=user_id, customer_id=customer_id, max_results=4, mode="fast", ) # Fuse the two so a single retrieve call returns ranked results from both self.retriever = QueryFusionRetriever( retrievers=[doc_retriever, memory_retriever], similarity_top_k=6, num_queries=1, ) self.llm = OpenAI(model="gpt-4o") async def ask(self, conversation_id: str, message: str) -> str: memory = SynapChatMemory( sdk=self.sdk, conversation_id=conversation_id, user_id=self.user_id, customer_id=self.customer_id, ) chat_engine = CondensePlusContextChatEngine.from_defaults( retriever=self.retriever, memory=memory, llm=self.llm, ) response = await chat_engine.achat(message) return str(response) # Usage assistant = SupportAssistant(sdk, user_id="alice", customer_id="acme") reply = await assistant.ask(str(uuid.uuid4()), "Has my refund been processed?") ``` Three things to notice in this pattern: 1. **`SynapChatMemory`** is constructed per-conversation so multiple sessions can run side-by-side without interfering. 2. **`SynapRetriever`** is fused with the document retriever via `QueryFusionRetriever`, so user-specific facts and corpus documents come back as one ranked list. 3. **Memory and retrieval are independent.** Drop either and the pipeline still works; together they cover both the "what did we say" and "what do I know about this user" axes. *** ## Advanced patterns ### Multi-tenant scoping Both components accept the same scoping triple: `user_id` (required), optional `customer_id`, optional `conversation_id`. `customer_id` is required on B2B Synap instances and ignored on single-tenant ones. See [Memory Scopes](/concepts/memory-scopes). ```python theme={null} retriever = SynapRetriever( sdk=sdk, user_id="alice", customer_id="acme", # scope retrievals to acme's tenancy ) ``` ### Tuning retrieval mode per query `SynapRetriever` takes a default `mode` at construction, but you can swap it temporarily for a single high-recall lookup: ```python theme={null} retriever.mode = "accurate" nodes = await retriever.aretrieve("Summarize everything about the Acme account.") retriever.mode = "fast" ``` ### Composing with other retrievers `SynapRetriever` is a regular `BaseRetriever`, so it composes cleanly with LlamaIndex's `RouterRetriever`, `QueryFusionRetriever`, or any custom retriever you build. Combine it with a document retriever (as in the example above), or route between Synap memories and a vector store based on the query. ### Failure semantics The integration follows the Synap-wide contract: * **Retrieval failures degrade gracefully:** `SynapRetriever.aretrieve` returns `[]` and logs an error * **Memory reads degrade gracefully:** `SynapChatMemory.get` returns an empty buffer and logs an error * **Memory writes surface failures:** `SynapChatMemory.put` raises `SynapIntegrationError` so callers know persistence failed This is by design: read failures should never break a user-facing turn, while write failures must be visible to callers. *** ## Going further * [Patterns overview](/patterns/overview): reusable memory patterns across frameworks. * [Cookbook overview](/cookbook/overview): end-to-end worked examples. *** ## Next steps Memory, retriever, and tools for LangChain chains and agents. Retriever and memory-writer pipeline components for Haystack. The retrieval API that powers `SynapRetriever`: modes, scopes, and response shapes. How `user_id`, `customer_id`, and `conversation_id` interact across retrievals. # Mastra Source: https://docs.maximem.ai/integrations/mastra SynapMemory class and search/store tools for Mastra (TypeScript). Add persistent, per-user memory to a Mastra agent in TypeScript. The integration ships a `MastraMemory` subclass for always-on memory and a pair of tools the model can call when it wants explicit control. `@maximem/synap-mastra` builds on the JavaScript SDK and needs Node.js 20+. Context and memory operations also run on Edge Runtime and Cloudflare Workers; the optional anticipation stream needs Node.js. See [Installation → JavaScript and TypeScript SDK](/setup/installation#javascript-and-typescript-sdk). Runtime: Node.js 20+ for the anticipation stream; context and memory operations also run on Edge, Cloudflare Workers and the browser. ## Overview This guide shows how to add Synap to a Mastra application to build agents that: * Recall and persist memory automatically on every `generate` call * Search and store memories on demand when the model decides it's relevant * Mix and match: use the memory class alone, the tools alone, or both together The Synap Mastra integration ships three exports: one memory class and two tool factories. | Export | Mastra interface | Purpose | | ----------------- | ----------------------- | -------------------------------------------------------------- | | `SynapMemory` | `MastraMemory` subclass | Always-on memory: auto-recall + auto-store on every `generate` | | `synapSearchTool` | Tool factory | Returns a Mastra-compatible tool for explicit memory search | | `synapStoreTool` | Tool factory | Returns a Mastra-compatible tool for explicit memory storage | ## Setup Install the package alongside Mastra: ```bash theme={null} npm install @maximem/synap-mastra @maximem/synap-js-sdk @mastra/core zod ``` Import the integration from its package name directly: `import { SynapMemory } from "@maximem/synap-mastra"`. The core SDK (`SynapClient`) comes from the separate `@maximem/synap-js-sdk` package. Configure your API key. Generate one from the [Synap Dashboard](https://synap.maximem.ai). ```bash .env theme={null} SYNAP_API_KEY=synap_your_key_here OPENAI_API_KEY=your-openai-api-key ``` Initialize the SDK once at application startup: ```typescript theme={null} import { SynapClient } from "@maximem/synap-js-sdk"; const sdk = new SynapClient(); await sdk.initialize(); ``` See [SDK Initialization](/sdk/initialization) for the full lifecycle and configuration options. ## Basic integration The smallest useful integration plugs `SynapMemory` into an `Agent`. Every `generate` call automatically pulls relevant memories into the prompt and writes the new turn back out: ```typescript theme={null} // npm install @maximem/synap-mastra @maximem/synap-js-sdk @mastra/core zod import { Agent } from "@mastra/core"; import { openai } from "@ai-sdk/openai"; import { SynapClient } from "@maximem/synap-js-sdk"; import { SynapMemory } from "@maximem/synap-mastra"; const sdk = new SynapClient(); await sdk.initialize(); const agent = new Agent({ name: "MemoryAgent", instructions: "You are an agent with persistent memory.", model: openai("gpt-4o"), memory: new SynapMemory({ sdk, userId: "alice", customerId: "acme", // optional, required for B2B instances }), }); const result = await agent.generate("What do you remember about my project deadlines?"); console.log(result.text); ``` The scoping triple is bound when `SynapMemory` is constructed: the model never sees the user identity. **Memory reads degrade gracefully** (empty context on failure); writes raise so silent data loss is impossible. To let the model decide *when* to recall or store (rather than running on every turn), use the tools below. *** ## Core concepts ### SynapMemory `SynapMemory` extends Mastra's `MastraMemory` and overrides the storage layer to route through Synap: ```typescript theme={null} import { SynapMemory } from "@maximem/synap-mastra"; const memory = new SynapMemory({ sdk, userId: "alice", customerId: "acme", // optional conversationId: crypto.randomUUID(), // optional, scopes to a single session; must be a valid UUID maxResults: 8, mode: "fast", // "fast" | "accurate" }); ``` The two retrieval modes trade latency against comprehensiveness: | | `fast` | `accurate` | | -------- | ---------------------------------------------- | ------------------------------------------------------- | | Search | Vector + graph (no LLM subquery decomposition) | Vector + graph + LLM subquery decomposition + reranking | | Best for | Real-time chat | Multi-entity queries | `fast` is lower-latency and suited to the hot path; `accurate` adds LLM-driven query decomposition and reranking for relationship-aware queries at a higher latency cost. Method behavior: | Method | Behavior | | ------------------------ | ------------------------------------------ | | `remember(message)` | Ingests a message into Synap | | `recall(query, options)` | Semantic search; returns `MemoryMessage[]` | | `getMessages(threadId)` | Retrieves the message thread from Synap | ### synapSearchTool and synapStoreTool The tool factories return Mastra-compatible tool objects with Zod schemas. They're for agents that should decide *when* to query memory rather than running on every turn: ```typescript theme={null} import { synapSearchTool, synapStoreTool } from "@maximem/synap-mastra"; const agent = new Agent({ // ... tools: { synapSearch: synapSearchTool({ sdk, userId: "alice", maxResults: 5, mode: "accurate", }), synapStore: synapStoreTool({ sdk, userId: "alice" }), }, }); ``` **`synapSearchTool`** schema: ```typescript theme={null} z.object({ query: z.string().describe("What to search for in memory"), maxResults: z.number().optional().default(5), }) ``` **`synapStoreTool`** schema: ```typescript theme={null} z.object({ content: z.string().describe("The information to remember"), memoryType: z.string().optional().default("fact"), }) ``` ### Memory vs. tools | | `SynapMemory` | Tools | | ----------------- | -------------------------------------- | -------------------------------------- | | Context injection | Automatic on every `generate` | On-demand when model calls the tool | | Memory storage | Automatic after every response | On-demand when model calls the tool | | Best for | Always-on memory for every interaction | Agents that decide when memory matters | Use both for maximum coverage: `SynapMemory` handles the always-on path and `synapStoreTool` lets the model bookmark new information explicitly when it sees something worth remembering. *** ## Complete example: agent with memory + tools The pattern below assembles all three exports. The agent has always-on memory via `SynapMemory` AND can call the search/store tools when it decides to: ```typescript theme={null} import { Agent } from "@mastra/core"; import { openai } from "@ai-sdk/openai"; import { SynapMemory, synapSearchTool, synapStoreTool, } from "@maximem/synap-mastra"; function buildAgent(sdk, userId: string, customerId?: string) { return new Agent({ name: "PersonalAssistant", instructions: `You are a personal assistant with long-term memory. Use synapSearch when you need older context not already in the prompt. Use synapStore when the user shares a new fact, preference, or decision.`, model: openai("gpt-4o"), memory: new SynapMemory({ sdk, userId, customerId, maxResults: 6, mode: "fast", }), tools: { synapSearch: synapSearchTool({ sdk, userId, customerId, maxResults: 5 }), synapStore: synapStoreTool({ sdk, userId, customerId }), }, }); } // Usage const agent = buildAgent(sdk, "alice", "acme"); await agent.generate("I just upgraded to the Pro plan."); // SynapMemory.remember persists the turn; the model may also call synapStore. const reply = await agent.generate("What plan am I on?"); // SynapMemory.recall surfaces "Pro plan" automatically. console.log(reply.text); // → "You're on the Pro plan." ``` Three things to notice in this pattern: 1. **Memory and tools are complementary, not redundant.** `SynapMemory` handles the always-on baseline; tools handle the explicit "I should look this up" path. 2. **Scope is per-agent.** `buildAgent(...)` constructs a fresh agent per user, so each invocation has its scope baked in. 3. **The instructions are the policy.** Telling the model when to use `synapSearch` and `synapStore` is what produces the explicit-memory behavior. *** ## Advanced patterns ### Multi-tenant scoping All three exports accept the standard scoping triple: `userId` (required), optional `customerId`, optional `conversationId`. `customerId` is required on B2B Synap instances and ignored on single-tenant ones. See [Memory Scopes](/concepts/memory-scopes). ```typescript theme={null} const memory = new SynapMemory({ sdk, userId: "alice", customerId: "acme", }); ``` For multi-tenant services, build agents per request rather than caching them; each agent should have its scope baked in. ### Choosing between memory, tools, or both * **`SynapMemory` only**: always-on agents where every turn benefits from recall and ingestion. * **Tools only**: agents that should be selective about memory (e.g. research agents that should only remember important findings). * **Both**: production agents where automatic recall sets the baseline and tools let the model dig deeper or bookmark explicitly. ### Failure semantics The integration follows the Synap-wide contract: * **`SynapMemory.recall` degrades gracefully**: returns `[]` and logs on failure. * **`SynapMemory.remember` surfaces failures**: raises `SynapIntegrationError`. * **`synapSearchTool` degrades gracefully**: returns `[]` and logs on failure. * **`synapStoreTool` surfaces failures**: raises `SynapIntegrationError`. This is by design: read failures shouldn't break a user-facing turn, but silent write failures would let the memory drift away from reality. *** ## Going further * [Patterns overview](/patterns/overview): reusable memory patterns across frameworks. * [Cookbook overview](/cookbook/overview): end-to-end worked examples. *** ## Next steps Model middleware for the Vercel AI SDK. Hooks and MCP server for the Claude Agent SDK. The retrieval API behind `SynapMemory` and `synapSearchTool`. How `userId`, `customerId`, and `conversationId` interact across reads. # MCP Server (No-code) Source: https://docs.maximem.ai/integrations/mcp Give an agent on Gumloop, n8n, Claude, or any MCP-aware platform persistent memory with just a URL and a token. No code. Synap hosts a remote **MCP server** so no-code platforms can have persistent memory without running an SDK or writing any code. You connect it the way every MCP-aware tool connects to a remote server: paste an **MCP Server URL** and a **Bearer token**. Your agent then discovers Synap's memory tools and calls them automatically. This is the **no-code** surface. If you're building with code, use an [SDK or framework integration](/integrations/overview) instead. You get the same memory, with more control. ## How it works The MCP server re-fronts Synap's existing memory operations as a small set of tools. The model decides when to call them from their descriptions alone, so you configure nothing: * **Forward everything.** Your agent forwards each conversation turn to a single "log" tool. You never decide what is "memory-worthy"; Synap's extraction pipeline decides what to keep. * **Recall before replying.** Your agent fetches what's already known about the person before it answers. * **Async by design.** Logging is fire-and-forget so a slow write never stalls a reply; extraction happens in the background. ## Get your connection details In the Synap dashboard, go to **MCP** in the sidebar. Describe, in plain English, what your agent does and what it should remember. Synap designs the memory architecture for you; there's nothing to configure. The page shows your **MCP Server URL** and lets you generate a **Bearer token**. The token is shown once, so copy it now. Your MCP Server URL looks like: ``` https://synap-mcp.maximem.ai/mcp ``` Always use a standard **Bearer token**, never a custom header. Some platforms (e.g. Gumloop with Anthropic models) drop custom headers, so Bearer is the only reliable option. Your Synap API key *is* the Bearer token. ## Connect your platform **Settings → Credentials → Add → MCP Server**. Paste your **MCP Server URL**, choose **Bearer token**, and paste your token. Connect. Add an **Agent** (or **Ask AI**) node and enable the Synap MCP credential. The memory tools appear automatically. Talk to your agent. It logs each turn and recalls what's known before replying. Add an **AI Agent** node and an **MCP Client Tool** node. Point the MCP Client Tool at your **MCP Server URL**, choose **Bearer Auth**, and paste your token. Connect the MCP Client Tool node into the AI Agent node's **tools** input. On self-hosted n8n you may need `N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true`, and (behind nginx) to disable proxy buffering on the MCP path. In your MCP-aware client, add a custom connector / remote MCP server. Use your **MCP Server URL** with **Bearer** auth and paste your token. The agent discovers the Synap memory tools and calls them automatically. ## Tools The MCP server exposes these tools. Descriptions are written so the model calls them on its own; you don't wire anything up. | Tool | What it does | | ---------------------- | ------------------------------------------------------------------------------------------- | | `log_exchange` | Forward a user (and optional assistant) turn to be remembered. Synap decides what persists. | | `recall_context` | Recall what's already known about the current person, for use before replying. | | `list_recent_memories` | List recent memories; handy for debugging or confirming memory works. | | `check_memory_status` | Check whether a logged exchange finished processing (extraction is asynchronous). | ### Arguments * **`user_id`** *(optional)*: a stable id for the end-user. Pass it on both `log_exchange` and `recall_context` to keep each person's memory separate. * **`customer_id`** *(optional)*: an organization id, for B2B / multi-tenant agents. * **`conversation_id`** *(optional)*: groups turns into a conversation. * **`wait_for_processing`** *(optional, `log_exchange`)*: when `true`, waits for extraction to finish and reports the outcome instead of returning immediately. Leave it off for normal turn-by-turn logging. ## Scoping memory Who sees which memories is decided by the ids you pass; there's nothing to configure: Pass no ids. Everything is shared across the agent. Good for a single-purpose assistant. Pass `user_id`. Each person gets their own private memory; everyone still shares the agent's general knowledge. Pass `customer_id` for B2B agents that serve multiple customer organizations. For a real per-user experience, map your platform's user identifier into `user_id`, for example an n8n expression like `{{ $json.userId }}` or a Gumloop input. The same id on `log_exchange` and `recall_context` keeps each person's memory separate. ## Prove it works The dashboard's **MCP** page includes a **Test my memory** button: it writes a sample memory and reads it back live, so you can confirm your token works before going live. In your platform, the same loop is the real test: tell your agent a fact, then in a new session ask what it remembers. ## Troubleshooting Confirm the tools are listed on the credential/node. If the model still doesn't call them, add a one-line instruction to your agent prompt: *"Use your memory tools: log every user message and recall before replying."* That's the custom-header drop. Make sure auth is set as a **Bearer token**, not a custom header. Extraction is asynchronous, so wait a few seconds and try again, or use `check_memory_status` (or `wait_for_processing`) to confirm processing finished. Re-check the URL (it ends in `/mcp`) and that the token is pasted exactly. Tokens are shown once, so regenerate from the MCP page if you've lost it. ## Going further * [Patterns overview](/patterns/overview): reusable memory patterns across frameworks. * [Cookbook overview](/cookbook/overview): end-to-end worked examples. # Microsoft Agent Framework Source: https://docs.maximem.ai/integrations/microsoft-agent Memory for both Microsoft Agent Framework surfaces — context and history providers for the agent SDK, and MemoryStore and AgentFileStore for the Agent Harness. Microsoft Agent Framework (MAF) has two distinct product surfaces, and this package covers both. The **agent SDK** is the one most people start with: `client.as_agent(...)` with `context_providers`. The **Agent Harness** (`create_harness_agent`) is a separate runtime with its own memory subsystem — a topic notebook, an extraction pass per turn, and a periodic consolidation rewrite — plugged in through two storage interfaces it accepts as constructor arguments. Requires Python 3.11+ and `agent-framework>=1.0`. The harness surfaces need `agent-framework>=1.13` and are imported lazily, so an older install keeps working for the SDK surfaces and raises a clear error only if you reach for a harness class. ## Overview | Class | Surface | Purpose | | ------------------------------- | ------- | ---------------------------------------------------------------------------- | | `SynapContextProvider` | SDK | Injects relevant memories before each turn; records the turn after | | `SynapHistoryProvider` | SDK | Persists and reloads the verbatim conversation transcript | | `SynapShortTermContextProvider` | SDK | Compacted history of the current conversation, refreshed each turn | | `SynapMemoryStore` | Harness | Backs the harness topic notebook — `MEMORY.md`, topic records, consolidation | | `SynapAgentFileStore` | Harness | Backs the `file_memory_*` tools and the agent's file access | | `create_synap_harness_memory` | Harness | Builds the memory provider wired correctly. Use this | All of them take an already-constructed `MaximemSynapSDK` — your app owns the SDK and its credentials. ## Setup ```bash pip theme={null} pip install maximem-synap-microsoft-agent agent-framework ``` ```bash uv theme={null} uv add maximem-synap-microsoft-agent agent-framework ``` The pip package is `maximem-synap-microsoft-agent`, but the import drops the `maximem-` prefix and uses underscores: `from synap_microsoft_agent import ...`. ```bash .env theme={null} SYNAP_API_KEY=synap_your_key_here ``` ```python theme={null} from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK() await sdk.initialize() ``` See [SDK Initialization](/sdk/initialization) for the full lifecycle. *** # The agent SDK ## Basic integration `SynapContextProvider` handles both halves of memory on a normal MAF agent — the read before the turn and the write after it: ```python theme={null} from synap_microsoft_agent import SynapContextProvider agent = client.as_agent( name="MemoryAgent", instructions="You are a helpful assistant.", context_providers=[ SynapContextProvider( sdk=sdk, user_id="alice", customer_id="acme", # required on B2B instances max_results=6, ), ], ) response = await agent.run("What were the outcomes from my last meeting?") ``` **Reads degrade gracefully** on a Synap outage — empty context is injected, the error is logged, and the agent still answers. **Writes surface failures** so silent data loss is impossible. ## Core concepts ### Context provider `SynapContextProvider` runs on MAF's `before_run` and `after_run` hooks. Before the turn it builds a query from the incoming messages, fetches from Synap, and appends the result to the agent's instructions. After the turn it records the exchange back to Synap. ### History provider `SynapHistoryProvider` handles the verbatim transcript, through `get_messages` and `save_messages`, which MAF calls for you. ```python theme={null} import uuid from synap_microsoft_agent import SynapHistoryProvider # conversation_id must be a valid UUID — Synap validates it client-side. hist = SynapHistoryProvider( sdk=sdk, user_id="alice", conversation_id=str(uuid.uuid4()), ) ``` It is orthogonal to `SynapContextProvider`: one stores semantic memory, the other the literal transcript. ### Short-term context `SynapShortTermContextProvider` injects a compacted summary of the *current* conversation, refreshed each turn. Use it when the transcript is too long to replay but its shape still matters. *** # The Agent Harness The harness ships its own memory subsystem: a `MEMORY.md` index of pointer lines, one record per topic, an LLM extraction pass per turn, and a periodic consolidation rewrite. It is genuinely good, and this integration does **not** replace it. What it replaces is the two things underneath — where memory is stored, and how it is retrieved. Topic selection in the stock harness is lexical: relevance is the size of the word overlap between your question and the topic's title and summary. A topic filed under "billing" is invisible to a question about "invoices". That is the gap. ### When this is worth it We benchmarked it rather than asserting it, and the result is narrower than a blanket "upgrade". We filed a topic under **billing** — customers charged monthly in arrears, failed charges retried three times — then asked *"When do we send invoices to customers, and what happens if one fails?"*. The stock file-backed store answered **"I don't know."** The topic was in its own memory directory the whole time; "billing" and "invoices" simply share no words, so it was never loaded. With Synap underneath, the same agent answered both halves correctly. That is the case this integration is for: **reach**, not economy. On fixtures where the question already shares vocabulary with the topic, the two are equally correct — and the file store is faster. The cost is a retrieval round trip on every turn. Assembling the memory block took roughly **2 seconds** with Synap against **5 milliseconds** from local files. Prompt size barely moves, because the harness already loads a *selection* of topics rather than everything. So: use Synap where memory outgrows one vocabulary — long-lived agents, many sessions, memory shared across projects or across agents. On a handful of topics phrased the way you ask about them, `MemoryFileStore` is the better tool and we would rather say so. ## Basic integration ```python theme={null} from agent_framework import create_harness_agent from synap_microsoft_agent import create_synap_harness_memory agent = create_harness_agent( client, history_provider=create_synap_harness_memory( sdk, user_id="alice", customer_id="acme", ), ) ``` `create_harness_agent` accepts exactly **one** `history_provider`. Both `SynapHistoryProvider` and the harness memory provider are `HistoryProvider`s, so passing both silently keeps whichever came last and drops the other — no error, no warning. Pick one: * `history_provider=SynapHistoryProvider(...)` — Synap owns the transcript, the harness topic subsystem is off. * `history_provider=create_synap_harness_memory(...)` — the harness owns extraction and consolidation, Synap is the memory beneath it. ## Core concepts ### What Synap holds, and what stays local This is the part worth understanding before you deploy it. `MemoryStore` is a **record** store. The harness reads a topic record, adds a line, and writes it back — so a record has to come back exactly as it went in. Synap deliberately does not work that way: `memories.create` runs an extraction pipeline that rewrites, splits, and merges what you submit. In our testing, a topic record submitted as JSON came back as four extracted memories with no JSON envelope and the text rephrased into the third person. The substance survived; the record did not. Reading records back from Synap would therefore feed the harness a rewritten record, which it would rewrite again next turn, and again at the next consolidation. So the split is: | What | Where | Why | | -------------------------------- | -------------------- | ---------------------------------------------------- | | Topic records, maintenance state | A `TopicRecordStore` | Read-modify-write needs exact fidelity | | Topic content | Synap | Durable, semantic, shared across sessions and agents | | Transcripts | Synap | No local files | The default record store lives for the life of the process. After a restart it is cold, and a topic that is not in it reports as not-found — which the harness handles by starting a fresh record. The topic looks new; **no corrupted record ever enters the loop**. Meanwhile everything written on previous runs is still in Synap and still reaches the prompt, through the recall block described below. Pass your own `record_store` to survive restarts: ```python theme={null} from synap_microsoft_agent import TopicRecordStore, create_synap_harness_memory class RedisTopicRecordStore: # implements TopicRecordStore def put(self, owner, slug, payload): ... def get(self, owner, slug): ... def delete(self, owner, slug): ... def list(self, owner): ... def put_state(self, owner, state): ... def get_state(self, owner): ... provider = create_synap_harness_memory( sdk, user_id="alice", record_store=RedisTopicRecordStore(), ) ``` ### The recall block `MEMORY.md` is assembled fresh on every turn: MAF's pointer lines, unchanged, plus a Synap recall block underneath. ``` # Memory Index - [deployment workflow](topics/deployment-workflow.md): How this person ships software. ## Durable memory (Synap) ## User Context ### Preferences - The user prefers to reach for PostgreSQL first ``` That block is the reason to use this integration. It carries memory the record layer never had — written on a previous run, by a previous process, or by a different agent against the same scope. It costs one retrieval per turn (about half a second, measured against production). Turn it off with `include_recall=False` if you would rather not pay that. ### Semantic transcript search The harness exposes a `search_memory_transcripts` tool. On the file store that is a substring match over saved turn files, so a question worded differently from the transcript finds nothing. Here it becomes a Synap retrieval call, so wording does not have to match. ### File memory `SynapAgentFileStore` backs the seven `file_memory_*` tools the harness gives the model: ```python theme={null} from synap_microsoft_agent import SynapAgentFileStore agent = create_harness_agent( client, file_memory_store=SynapAgentFileStore(sdk, user_id="alice"), ) ``` `file_memory_grep` searches **both** ways: a regex over files written this session, and a meaning-based lookup against Synap attributed to `MEMORY.md`. The regex half is exact and is what the model expects from a tool named grep; the semantic half finds memories no regex could match. `file_memory_delete` is real — a file written through this store resolves to the memories it produced and deletes them. **`file_memory_ls` only lists files this process wrote.** There is no list-memories-by-scope API in Synap today, so after a restart the agent can still `read` its files by name and `grep` them by meaning, but it cannot browse them. Within a session — which is what file memory is scoped to — this is invisible. ## Complete example: both harness surfaces ```python theme={null} from agent_framework import create_harness_agent from maximem_synap import MaximemSynapSDK from synap_microsoft_agent import SynapAgentFileStore, create_synap_harness_memory sdk = MaximemSynapSDK() await sdk.initialize() agent = create_harness_agent( client, history_provider=create_synap_harness_memory( sdk, user_id="alice", customer_id="acme", recent_turns=4, selection_limit=3, ), file_memory_store=SynapAgentFileStore(sdk, user_id="alice", customer_id="acme"), ) ``` Every harness API is marked experimental upstream and lives behind private module paths that Microsoft says may move. Pin a tested `agent-framework` version and re-run your tests on each minor release. This package never suppresses the `ExperimentalWarning` — if MAF wants you to know the surface is unstable, hiding that would not be doing you a favour. ## Advanced patterns ### Scoping Both harness stores take `user_id` and `customer_id`; at least one is required. `customer_id` is required on B2B instances. See [Memory Scopes](/concepts/memory-scopes). For multi-tenant hosts, `SynapMemoryStore` also takes a `scope_resolver` callable over the session, which partitions **records** per tenant. It does not re-scope the Synap calls themselves — build one store per tenant for that. `FileMemoryProvider(scope=...)` decides which folder file memory lands in: `None` isolates per session, an explicit value groups across sessions. Line it up with the store's scope, or the tool surface and the memory will disagree about whose files they are. ### A new scope needs a corpus before recall is useful Recall is only as good as what is in the scope, and a nearly-empty scope returns nothing at all rather than a little. In testing, a scope holding three memories returned an empty context on every setting, while the same scope at a dozen returned content consistently. That matters because of how it looks from the outside: the recall block is absent and the integration appears broken. It isn't — there is genuinely nothing to return yet. Let the agent accumulate a corpus over several sessions before judging recall quality. ### Failure semantics | Path | Behaviour | | ------------------------------------------------------------- | ------------------------------------------------------------------------------- | | Context and recall reads | Degrade to empty, logged at `ERROR` | | `get_index_text` | Degrades to pointer lines with no recall block — it feeds the prompt every turn | | `search_transcripts`, `file_memory_grep` | Degrade to no results | | Writes (`write_topic`, `file_memory_write`, transcript saves) | Raise `SynapIntegrationError` | | A topic that is not held | Raises `FileNotFoundError`, which the harness handles as "new topic" | Read failures must not break a user-facing turn. Silent write failures would corrupt the memory pool, so they raise. *** ## Going further * [Patterns overview](/patterns/overview): reusable memory patterns across frameworks. * [Cookbook overview](/cookbook/overview): end-to-end worked examples. ## Next steps Kernel plugin for Microsoft Semantic Kernel. The same harness pattern for LangChain's deepagents. The retrieval API behind every surface here: modes, scopes, and response shapes. How `user_id`, `customer_id`, and `conversation_id` interact across reads. # NeMo Agent Toolkit Source: https://docs.maximem.ai/integrations/nemo-agent-toolkit MemoryEditor implementation that backs NVIDIA NeMo Agent Toolkit workflows with Synap. Plug Synap into NVIDIA NeMo Agent Toolkit (NAT) as a first-class `MemoryEditor`. NAT workflows that declare a memory backend in YAML (or instantiate one programmatically) now get persistent, semantically searchable, per-user memory. Requires Python 3.11+. ## Overview This guide shows how to add Synap to a NAT workflow to build pipelines that: * Persist `MemoryItem` objects across workflow executions * Retrieve memories via semantic search inside any NAT step * Be declared in NAT's YAML config without writing additional integration code The Synap NAT integration ships three exports: the editor itself, a registration decorator, and a one-shot factory. | Export | Purpose | | --------------------- | ------------------------------------------------------------------- | | `SynapMemoryEditor` | Implements `nat.memory.interfaces.MemoryEditor` for NAT workflows | | `@register_memory` | Decorator that registers the editor under a YAML-referenceable name | | `synap_memory_client` | Factory that builds a ready-to-use `SynapMemoryEditor` from config | ## Setup Install the package alongside NAT: ```bash pip theme={null} pip install maximem-synap-nemo-agent-toolkit nat ``` ```bash uv theme={null} uv add maximem-synap-nemo-agent-toolkit nat # pip-compatible (existing venv): uv pip install maximem-synap-nemo-agent-toolkit nat ``` The pip package is `maximem-synap-nemo-agent-toolkit`, but the import drops the `maximem-` prefix and uses underscores: `from synap_nemo_agent_toolkit import ...`. Configure your API key. Generate one from the [Synap Dashboard](https://synap.maximem.ai). ```bash .env theme={null} SYNAP_API_KEY=synap_your_key_here ``` Initialize the SDK once at application startup: ```python theme={null} from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK() await sdk.initialize() ``` Alternatively, use the `synap_memory_client` factory below to skip the SDK setup; it initializes Synap internally. See [SDK Initialization](/sdk/initialization) for the full lifecycle and configuration options. ## Basic integration The smallest useful integration constructs a `SynapMemoryEditor` and uses it directly: ```python theme={null} # pip install maximem-synap-nemo-agent-toolkit nat from maximem_synap import MaximemSynapSDK from nat.memory.models import MemoryItem from synap_nemo_agent_toolkit import SynapMemoryEditor sdk = MaximemSynapSDK() await sdk.initialize() editor = SynapMemoryEditor( sdk=sdk, customer_id="acme", # optional, required for B2B instances mode="accurate", # "fast" or "accurate" ) # Store memories: user_id travels on the item, not the editor await editor.add_items([ MemoryItem(user_id="alice", memory="Prefers concise bullet-point summaries", tags=["preference"]), MemoryItem(user_id="alice", memory="Working on Q3 roadmap planning", tags=["project"]), ]) # Search memories results = await editor.search("communication preferences", top_k=5, user_id="alice") for item in results: print(item.memory, item.score) ``` Notice that `user_id` is supplied per item and per query: a single `SynapMemoryEditor` instance serves all users in the workflow. `customer_id` is set once at construction. *** ## Core concepts ### MemoryEditor interface `SynapMemoryEditor` implements the full `MemoryEditor` protocol. NAT workflows that accept a `MemoryEditor` work without modification: ```python theme={null} editor = SynapMemoryEditor(sdk=sdk, customer_id="acme", mode="accurate") ``` | Method | Behavior | | ------------------------------- | ------------------------------------------------- | | `add_items(items)` | Batch-ingest `MemoryItem` objects into Synap | | `search(query, top_k, user_id)` | Semantic search; returns scored `MemoryItem` list | | `update_items(items)` | Update existing memories by ID | | `get_items(user_id, limit)` | Retrieve all memories for a user | The two retrieval modes trade latency against comprehensiveness: | | `fast` | `accurate` | | -------- | ---------------------------------------------- | ------------------------------------------------------- | | Search | Vector + graph (no LLM subquery decomposition) | Vector + graph + LLM subquery decomposition + reranking | | Best for | Real-time chat | Multi-entity queries | `fast` is lower-latency and suited to the hot path; `accurate` adds LLM-driven query decomposition and reranking for relationship-aware queries at a higher latency cost. **Reads degrade gracefully**: `search` and `get_items` return empty results and log an error on Synap outages. **Writes surface failures**: `add_items` and `update_items` raise `SynapIntegrationError` so workflows know if persistence failed. ### Registration for YAML configs NAT lets you declare memory backends in YAML. Use `@register_memory` to make `SynapMemoryEditor` resolvable by name: ```python theme={null} from synap_nemo_agent_toolkit import register_memory, SynapMemoryEditor @register_memory("synap") class _RegisteredSynap(SynapMemoryEditor): pass ``` After registration, reference it in any NAT workflow config: ```yaml theme={null} memory: type: synap config: api_key: ${SYNAP_API_KEY} mode: accurate customer_id: acme ``` NAT resolves `type: synap` to the registered class and instantiates it with the `config` block. ### Factory function For programmatic setups outside YAML, `synap_memory_client` builds a ready-to-use editor and initializes the SDK internally, with no separate lifecycle to manage: ```python theme={null} import os from synap_nemo_agent_toolkit import synap_memory_client editor = synap_memory_client( api_key=os.environ["SYNAP_API_KEY"], customer_id="acme", mode="accurate", ) ``` Use this when you want a single function call to produce a configured editor; especially useful in scripts and notebooks. *** ## Complete example: NAT workflow with persistent memory The pattern below sets up a workflow with Synap-backed memory at startup, ingests a batch of memories, and runs a recall query: ```python theme={null} from nat.memory.models import MemoryItem from synap_nemo_agent_toolkit import SynapMemoryEditor async def setup_memory(sdk, customer_id: str | None = None) -> SynapMemoryEditor: editor = SynapMemoryEditor(sdk=sdk, customer_id=customer_id, mode="accurate") # Seed the editor with starting memories for known users await editor.add_items([ MemoryItem(user_id="alice", memory="Lead engineer on Project Phoenix", tags=["role"]), MemoryItem(user_id="alice", memory="Prefers email for async updates", tags=["preference"]), MemoryItem(user_id="bob", memory="QA lead, focuses on flaky tests", tags=["role"]), ]) return editor async def recall(editor: SynapMemoryEditor, user_id: str, query: str) -> list[str]: results = await editor.search(query, top_k=5, user_id=user_id) return [r.memory for r in results] # Usage editor = await setup_memory(sdk, customer_id="acme") alice_prefs = await recall(editor, user_id="alice", query="how does alice prefer to be contacted?") # → ["Prefers email for async updates", ...] ``` Three things to notice in this pattern: 1. **One editor, many users.** `user_id` travels on each `MemoryItem` and each `search` call, so the editor is shared. 2. **`customer_id` is the tenant boundary.** All users sharing an editor are inside the same `customer_id`, so build a separate editor per tenant for multi-tenant services. 3. **Mode is fixed at construction.** Set `mode="fast"` for low-latency NAT steps; `"accurate"` for higher-recall lookups. *** ## Advanced patterns ### Multi-tenant scoping `SynapMemoryEditor` takes `customer_id` at construction; `user_id` is supplied per call. `customer_id` is required on B2B Synap instances and ignored on single-tenant ones. See [Memory Scopes](/concepts/memory-scopes). ```python theme={null} # Single-tenant editor = SynapMemoryEditor(sdk=sdk) # Multi-tenant: one editor per customer editor_acme = SynapMemoryEditor(sdk=sdk, customer_id="acme") editor_initech = SynapMemoryEditor(sdk=sdk, customer_id="initech") ``` ### Choosing between SDK-managed and factory-managed lifecycles * **Use `SynapMemoryEditor(sdk=...)`** when your application owns the SDK lifecycle (recommended for production, since you control init/shutdown). * **Use `synap_memory_client(api_key=...)`** for scripts, notebooks, or YAML-driven workflows where you'd rather not manage the SDK explicitly. ### Failure semantics The integration follows the Synap-wide contract: * **`search` and `get_items` degrade gracefully**: return empty lists and log an error if Synap is unreachable. * **`add_items` and `update_items` surface failures**: raise `SynapIntegrationError` so the workflow and caller know persistence failed. This is by design: read failures shouldn't break a workflow step mid-flight, but silent write failures would corrupt the memory pool. *** ## Going further * [Patterns overview](/patterns/overview): reusable memory patterns across frameworks. * [Cookbook overview](/cookbook/overview): end-to-end worked examples. *** ## Next steps Plugin for Microsoft Semantic Kernel. Type-safe deps and tools for Pydantic AI. How `user_id` and `customer_id` interact across reads and writes. Direct ingestion API for pipelines that need finer control than `add_items`. # OpenAI Agents SDK Source: https://docs.maximem.ai/integrations/openai-agents Search and store function tools that let OpenAI Agents recall and persist memories on demand. Give an OpenAI Agent persistent, per-user memory by exposing two function tools: one that searches Synap and one that stores new memories. The agent decides when to call each, with no hidden injection and no chain modifications. Requires Python 3.11+. ## Overview This guide shows how to add Synap to an OpenAI Agents SDK application to build agents that: * Recall user-specific facts, preferences, and past conversations * Persist new information surfaced during a conversation for future runs * Stay fully in control of *when* memory is queried or written The Synap OpenAI Agents integration ships two factory functions, each of which returns an async callable you wrap with the `function_tool(...)` helper from `agents` (the `name_override` argument lives on `function_tool`, not on the `FunctionTool` class). | Export | Returns | Purpose | | -------------------- | ------------------------------------------ | ------------------------------------------- | | `create_search_tool` | `async (query, max_results) -> list[dict]` | Function tool that searches Synap memory | | `create_store_tool` | `async (content, memory_type) -> dict` | Function tool that stores a memory in Synap | ## Setup Install the package alongside the OpenAI Agents SDK: ```bash pip theme={null} pip install maximem-synap-openai-agents openai-agents ``` ```bash uv theme={null} uv add maximem-synap-openai-agents openai-agents # pip-compatible (existing venv): uv pip install maximem-synap-openai-agents openai-agents ``` The pip package is `maximem-synap-openai-agents`, but the import drops the `maximem-` prefix and uses underscores: `from synap_openai_agents import ...`. Configure your API key. Generate one from the [Synap Dashboard](https://synap.maximem.ai). ```bash .env theme={null} SYNAP_API_KEY=synap_your_key_here OPENAI_API_KEY=your-openai-api-key ``` Initialize the SDK once at application startup: ```python theme={null} from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK() await sdk.initialize() ``` See [SDK Initialization](/sdk/initialization) for the full lifecycle and configuration options. ## Basic integration The smallest useful integration registers both tools on an agent and runs a query. The agent reads its tool descriptions, decides whether to search or store, and calls the matching function: ```python theme={null} # pip install maximem-synap-openai-agents openai-agents from maximem_synap import MaximemSynapSDK from agents import Agent, Runner, function_tool from synap_openai_agents import create_search_tool, create_store_tool sdk = MaximemSynapSDK() await sdk.initialize() search_fn = create_search_tool(sdk=sdk, user_id="alice", customer_id="acme") store_fn = create_store_tool(sdk=sdk, user_id="alice", customer_id="acme") agent = Agent( name="Memory Agent", instructions=( "Use synap_search to recall facts about the user. " "Use synap_store to remember new information they share." ), tools=[ function_tool(search_fn, name_override="synap_search"), function_tool(store_fn, name_override="synap_store"), ], ) result = await Runner.run(agent, "What do you know about my project deadlines?") print(result.final_output) ``` The scoping triple (`user_id`, optional `customer_id`) is bound when you construct the tool: the agent only ever sees the `query` and `content` parameters, never the user identity. This keeps the model from leaking or spoofing user IDs. *** ## Core concepts ### Search tool `create_search_tool` returns an async callable that takes a natural-language query and returns a list of memory objects. ```python theme={null} from synap_openai_agents import create_search_tool search_fn = create_search_tool( sdk=sdk, user_id="alice", customer_id="acme", # optional, required for B2B instances ) ``` Tool signature exposed to the model: ```text theme={null} synap_search(query: str, max_results: int = 5) -> list[dict] ``` Each result has the shape `{"content": "...", "type": "fact", "confidence": 0.91}`. The agent sees the JSON list and can quote or reason over the entries directly. **Search failures degrade gracefully**: the tool returns an empty list and logs an error, so the agent continues without recall rather than aborting. ### Store tool `create_store_tool` returns a companion callable that ingests a new memory. ```python theme={null} from synap_openai_agents import create_store_tool store_fn = create_store_tool( sdk=sdk, user_id="alice", customer_id="acme", ) ``` Tool signature exposed to the model: ```text theme={null} synap_store(content: str, memory_type: str = "fact") -> dict ``` Returns `{"status": "stored", "id": "..."}` on success. **Store failures surface explicitly**: the tool raises `SynapIntegrationError` so the agent (and you) know if persistence failed. *** ## Complete example: assistant with explicit memory control The following agent is told to consult its memory before answering and store anything the user shares. The Synap calls only happen when the model elects to invoke the tools: ```python theme={null} from agents import Agent, Runner, function_tool from synap_openai_agents import create_search_tool, create_store_tool def build_memory_agent(sdk, user_id: str, customer_id: str | None = None) -> Agent: search_fn = create_search_tool(sdk=sdk, user_id=user_id, customer_id=customer_id) store_fn = create_store_tool(sdk=sdk, user_id=user_id, customer_id=customer_id) return Agent( name="Personal Assistant", instructions=( "You are a personal assistant with long-term memory.\n" "1. Always call synap_search FIRST for any question about the user, " "their preferences, or past conversations.\n" "2. When the user shares a new fact, preference, or decision, " "call synap_store to remember it.\n" "3. Never fabricate facts. If synap_search returns nothing, " "say you don't know yet." ), tools=[ function_tool(search_fn, name_override="synap_search"), function_tool(store_fn, name_override="synap_store"), ], ) # Usage agent = build_memory_agent(sdk, user_id="alice", customer_id="acme") # First conversation: agent stores a fact await Runner.run(agent, "I just upgraded to the Pro plan.") # Later conversation: agent recalls it result = await Runner.run(agent, "What plan am I on?") print(result.final_output) # → "You're on the Pro plan." ``` Three things to notice in this pattern: 1. **The agent owns the memory loop.** Search and store calls are model-driven, not framework-driven; the prompt steers the behavior. 2. **Scope is bound at construction.** The model never sees `user_id` or `customer_id`, so even a prompt-injection attempt cannot make it query memories for someone else. 3. **Failure modes split.** Search is best-effort (empty list on failure); store is strict (raises on failure) so silent data loss is impossible. *** ## Advanced patterns ### Multi-tenant scoping Both factories accept the same scoping triple: `user_id` (required), optional `customer_id`. `customer_id` is required on B2B Synap instances and ignored on single-tenant ones. See [Memory Scopes](/concepts/memory-scopes). ```python theme={null} # User-scoped only search_fn = create_search_tool(sdk=sdk, user_id="alice") # Organization-scoped (user sees org-shared memories too) search_fn = create_search_tool(sdk=sdk, user_id="alice", customer_id="acme-corp") ``` ### Per-request scoping If your service handles many users in one process, build a fresh pair of tools per request rather than caching them; each agent run should have its scope baked in to prevent cross-user leakage: ```python theme={null} def per_request_agent(sdk, user_id: str, customer_id: str | None = None): return build_memory_agent(sdk, user_id, customer_id) ``` ### Failure semantics The integration follows the Synap-wide contract: * **Search failures degrade gracefully**: `synap_search` returns `[]` and logs an error so the agent can continue. * **Store failures surface explicitly**: `synap_store` raises `SynapIntegrationError` so the agent (and caller) know persistence failed. This is by design: a transient outage shouldn't break a user-facing answer, but a silent write failure would let memory drift away from reality. *** ## Going further * [Patterns overview](/patterns/overview): reusable memory patterns across frameworks. * [Cookbook overview](/cookbook/overview): end-to-end worked examples. *** ## Next steps Type-safe deps and tools for Pydantic AI agents. `BaseTool` implementations for AutoGen agents. The retrieval API that powers `synap_search`: modes, scopes, and response shapes. How `user_id` and `customer_id` interact across reads and writes. # Integrations Overview Source: https://docs.maximem.ai/integrations/overview Drop-in packages that add Synap memory to popular AI frameworks and agent SDKs. Synap ships a library of thin integration packages so you can add persistent memory to your existing agent stack without rewriting your application. Each package handles the translation between Synap's API and the framework's native memory or tool interface. ## Three ways to add Synap Install the skill and let Claude Code, Cursor, or Codex wire Synap in for you. Drop in the package for your framework: LangChain, LangGraph, and 16 more (below). Connect any MCP client with a URL and a token. No code. High-level architecture: the SDK, used inside your AI agent application, communicates directly with the Synap service All packages share the same contract: * **Read-side failures degrade gracefully**: a failed context fetch returns an empty result and logs an error, so your agent keeps running. * **Write-side failures surface explicitly**: failed ingestion raises `SynapIntegrationError` (or equivalent) so callers know if memory persistence failed. * **Same scoping model everywhere**: every package accepts `user_id`, optional `customer_id`, and optional `conversation_id`. ## How integrations plug in Every framework exposes a few hook points in its agent loop, and each Synap package maps onto the same set of points. No matter which framework you use, an integration plugs in through one or more of these: | Plug point | What it does | When it fires | | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- | | **History / memory** | Replaces the framework's conversation memory so prior turns and long-term memories are loaded into the agent. | Always-on, before generation. | | **Callback / recorder** | Captures each completed turn and ingests it into Synap so future conversations remember it. | Always-on, after generation. | | **Retriever** | Fetches relevant memories for a query and returns them as context or documents. | Always-on, before generation. | | **Tools** | Exposes explicit `search_memory` and `store_memory` functions the model can call on its own. | Model-driven, during generation. | | **Stream hook** | Reports the agent's turns and tool intent on Synap's real-time [anticipation stream](/concepts/real-time-anticipation): context is prefetched before the agent asks, and the reported turns become long-term memory at compaction. | Always-on, around generation. | These fall into two modes: * **Always-on memory**: history, callback, and retriever points run on every turn without the model deciding to. This is the default for memory-augmented agents and needs no prompt changes. * **Model-driven memory**: tools let the agent choose when to read or write memory. Use this when you want the model to reason about what to remember or look up, or when always-on context would be too broad. Most integrations support several of these points, so you can combine always-on context with model-driven tools in the same agent. The per-framework pages below note exactly which points each package provides. ## Available Integrations Connect Gumloop, n8n, Claude, or any MCP client with just a URL and a token, no code. Memory, callbacks, retriever, and tools for LangChain chains and agents. Checkpointer and cross-thread store for LangGraph graphs. `BaseMemory` implementation and retriever for LlamaIndex pipelines. Search and store tools for the OpenAI Agents SDK. Dependency dataclass and tool registration for Pydantic AI agents. `StorageBackend` implementation for CrewAI's unified Memory system. Search and store `BaseTool` implementations for AutoGen agents. `FunctionTool` factory for Google Agent Development Kit agents. `SynapRetriever` and `SynapMemoryWriter` pipeline components for Haystack. Drop-in `InMemoryDb` replacement that routes user memories through Synap. Kernel plugin with `search_memory` and `store_memory` functions. Context and history providers for the Microsoft Agent Framework. `MemoryEditor` implementation for NVIDIA NeMo Agent Toolkit workflows. Context preloading and turn recording for LiveKit voice agents. Frame processors for memory injection and recording in Pipecat pipelines. Native `MemoryStore`, short-term context hook, tools, and anticipation-stream feed. Native `AgentMemory` that augments CAMEL's history with Synap recall and persistence. Memory tools and a per-step turn recorder for Hugging Face Smolagents. A memory backend where `grep` is a semantic search, plus query-conditioned recall middleware. Hooks and MCP server for Anthropic's Claude Agent SDK (Python & TypeScript). `SynapMemory` class and tools for Mastra (TypeScript). Middleware that wraps any Vercel AI SDK model with automatic Synap context. Memory tools and a per-turn short-term-context resolver for eve agents. ## Quick Comparison | Package | Language | Integration point | Install | | ---------------------------------- | ---------- | ----------------------------------------------- | ---------------------------------------------- | | `maximem-synap-langchain` | Python | Memory, callback, retriever, tools | `pip install maximem-synap-langchain` | | `maximem-synap-langgraph` | Python | Checkpointer + Store | `pip install maximem-synap-langgraph` | | `maximem-synap-llamaindex` | Python | `BaseMemory` + retriever | `pip install maximem-synap-llamaindex` | | `maximem-synap-openai-agents` | Python | Function tools | `pip install maximem-synap-openai-agents` | | `maximem-synap-pydantic-ai` | Python | Deps + tools | `pip install maximem-synap-pydantic-ai` | | `maximem-synap-crewai` | Python | `StorageBackend` | `pip install maximem-synap-crewai` | | `maximem-synap-autogen` | Python | `BaseTool` | `pip install maximem-synap-autogen` | | `maximem-synap-google-adk` | Python | `FunctionTool` factory | `pip install maximem-synap-google-adk` | | `maximem-synap-haystack` | Python | Pipeline components | `pip install maximem-synap-haystack` | | `maximem-synap-agno` | Python | `InMemoryDb` subclass | `pip install maximem-synap-agno` | | `maximem-synap-semantic-kernel` | Python | Kernel plugin | `pip install maximem-synap-semantic-kernel` | | `maximem-synap-microsoft-agent` | Python | Context + history providers | `pip install maximem-synap-microsoft-agent` | | `maximem-synap-nemo-agent-toolkit` | Python | `MemoryEditor` | `pip install maximem-synap-nemo-agent-toolkit` | | `maximem-synap-livekit-agents` | Python | Helpers + function tools | `pip install maximem-synap-livekit-agents` | | `maximem-synap-pipecat` | Python | Frame processors | `pip install maximem-synap-pipecat` | | `maximem-synap-strands-agents` | Python | `MemoryStore` + hooks + tools + **stream hook** | `pip install maximem-synap-strands-agents` | | `maximem-synap-camel-ai` | Python | `AgentMemory` + tools | `pip install maximem-synap-camel-ai` | | `maximem-synap-smolagents` | Python | Tools + step recorder | `pip install maximem-synap-smolagents` | | `maximem-synap-deepagents` | Python | `BackendProtocol` + middleware + tools | `pip install maximem-synap-deepagents` | | `maximem-synap-claude-agent` | Python | Hooks + MCP server | `pip install maximem-synap-claude-agent` | | `@maximem/synap-claude-agent` | TypeScript | Hooks + MCP server | `npm install @maximem/synap-claude-agent` | | `@maximem/synap-mastra` | TypeScript | `MastraMemory` + tools | `npm install @maximem/synap-mastra` | | `@maximem/synap-vercel-adk` | TypeScript | Model middleware + **anticipation stream** | `npm install @maximem/synap-vercel-adk` | | `@maximem/synap-eve` | TypeScript | Tools + instructions resolver | `npm install @maximem/synap-eve` | **Stream hook support is currently limited to two packages**: `maximem-synap-strands-agents` and `@maximem/synap-vercel-adk`. Every other package uses request-response only, which works on its own. For a live agent, the stream is the integration we recommend: see [Agent Integration](/setup/agent-integration). You can drive it yourself with [`instance.listen`](/sdk-reference/instance/listen) alongside any package, or with no framework at all. The TypeScript packages build on the JavaScript SDK and need Node.js 20+. Context and memory operations run on Edge Runtime and Cloudflare Workers as well; the optional anticipation stream needs Node.js. See [Installation → JavaScript and TypeScript SDK](/setup/installation#javascript-and-typescript-sdk). ## Prerequisites Every integration requires a configured `MaximemSynapSDK` instance: ```python Python theme={null} import os from maximem_synap import MaximemSynapSDK, SDKConfig sdk = MaximemSynapSDK( api_key=os.environ["SYNAP_API_KEY"], config=SDKConfig(cache_backend="sqlite"), ) await sdk.initialize() ``` ```typescript TypeScript theme={null} import { SynapClient } from "@maximem/synap-js-sdk"; const sdk = new SynapClient({ apiKey: process.env.SYNAP_API_KEY!, }); await sdk.initialize(); ``` See [SDK Initialization](/sdk/initialization) and [Authentication](/setup/authentication) for full setup details. ## Going further * [Patterns overview](/patterns/overview): reusable memory patterns across frameworks. * [Cookbook overview](/cookbook/overview): end-to-end worked examples. # Pipecat Source: https://docs.maximem.ai/integrations/pipecat Frame processors that add memory injection and turn recording to Pipecat voice pipelines. Add persistent memory to a Pipecat voice pipeline as two frame processors: one that injects the user's relevant memories before the LLM sees a frame, and one that records each completed turn after the response. Requires Python 3.11+. ## Overview This guide shows how to add Synap to a Pipecat application to build voice pipelines that: * Inject the most relevant memories as a system message before the LLM responds * Record each completed user + assistant turn back to Synap * Compose with any other Pipecat frame processor without changing the pipeline shape The Synap Pipecat integration ships two frame processors. Both follow Pipecat's processor contract, so they slot into any pipeline alongside STT, LLM, and TTS. | Class | Pipeline position | Purpose | | ---------------------- | ----------------- | ------------------------------------------------ | | `SynapMemoryProcessor` | Before LLM | Prepends relevant memories to the system message | | `SynapRecorder` | After response | Records the completed turn back to Synap | ## Setup Install the package alongside Pipecat: ```bash pip theme={null} pip install maximem-synap-pipecat pipecat-ai ``` ```bash uv theme={null} uv add maximem-synap-pipecat pipecat-ai # pip-compatible (existing venv): uv pip install maximem-synap-pipecat pipecat-ai ``` The pip package is `maximem-synap-pipecat`, but the import drops the `maximem-` prefix and uses underscores: `from synap_pipecat import ...`. Configure your API key. Generate one from the [Synap Dashboard](https://synap.maximem.ai). ```bash .env theme={null} SYNAP_API_KEY=synap_your_key_here OPENAI_API_KEY=your-openai-api-key ``` Initialize the SDK once at the worker's startup: ```python theme={null} from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK() await sdk.initialize() ``` See [SDK Initialization](/sdk/initialization) for the full lifecycle and configuration options. ## Basic integration The smallest useful integration adds both processors to a standard voice pipeline: memory injection before the LLM, recording after the response. No other pipeline changes are needed: ```python theme={null} # pip install maximem-synap-pipecat pipecat-ai import uuid from maximem_synap import MaximemSynapSDK from pipecat.pipeline.pipeline import Pipeline from synap_pipecat import SynapMemoryProcessor, SynapRecorder sdk = MaximemSynapSDK() await sdk.initialize() memory = SynapMemoryProcessor( sdk=sdk, user_id="alice", customer_id="acme", # optional, required for B2B instances max_results=6, ) # If you pass conversation_id explicitly it must be a valid UUID; # omit it to let Synap auto-generate one. recorder = SynapRecorder( sdk=sdk, user_id="alice", customer_id="acme", conversation_id=str(uuid.uuid4()), # optional; auto-generated if omitted ) pipeline = Pipeline([ transport.input(), stt, memory, # inject memory before LLM user_aggregator, llm, tts, transport.output(), assistant_aggregator, recorder, # record turn after response ]) ``` **Memory injection failures degrade gracefully**: the frame passes through unmodified if context retrieval fails. **Recording failures surface explicitly** as `SynapIntegrationError`, which Pipecat's frame-error handling catches and logs. *** ## Core concepts ### Memory processor `SynapMemoryProcessor` intercepts `LLMMessagesFrame` events and prepends a system message containing the user's relevant memories before the frame reaches the LLM service: ```python theme={null} from synap_pipecat import SynapMemoryProcessor memory = SynapMemoryProcessor( sdk=sdk, user_id="alice", customer_id="acme", max_results=6, mode="fast", # "fast" or "accurate" ) ``` Voice latency is tight, so `mode="fast"` is the default. The two retrieval modes trade latency against comprehensiveness: | | `fast` | `accurate` | | -------- | ---------------------------------------------- | ------------------------------------------------------- | | Search | Vector + graph (no LLM subquery decomposition) | Vector + graph + LLM subquery decomposition + reranking | | Best for | Real-time chat | Multi-entity queries | `fast` is lower-latency and suited to the hot path; `accurate` adds LLM-driven query decomposition and reranking for relationship-aware queries at a higher latency cost. **Failures degrade gracefully**: if context retrieval fails, the `LLMMessagesFrame` passes through unmodified rather than blocking the call. ### Recorder `SynapRecorder` intercepts `TranscriptionFrame` (user side) and `LLMFullResponseEndFrame` (assistant side) and ingests the completed turn into Synap asynchronously: ```python theme={null} from synap_pipecat import SynapRecorder recorder = SynapRecorder( sdk=sdk, user_id="alice", customer_id="acme", conversation_id=str(uuid.uuid4()), ) ``` Recording happens out-of-band: it never blocks the audio path. **Write failures surface as `SynapIntegrationError`**, which propagates through Pipecat's frame-error handling so the failure is visible rather than silent. ### Positioning in the pipeline The two processors expect specific positions: ```text theme={null} transport.input() │ ▼ STT │ ▼ SynapMemoryProcessor ← fetches context, prepends to system prompt │ ▼ UserAggregator │ ▼ LLM │ ▼ TTS │ ▼ transport.output() │ ▼ AssistantAggregator │ ▼ SynapRecorder ← records completed user + assistant turn ``` `SynapMemoryProcessor` must be between STT and the user aggregator; `SynapRecorder` after the assistant aggregator. Any other placement will not see the right frame types. *** ## Complete example: full voice pipeline with memory The pattern below sets up an end-to-end voice pipeline with Synap-backed memory. Scope is pulled per-call from the transport's connection metadata, so the same worker can serve multiple users: ```python theme={null} from pipecat.pipeline.pipeline import Pipeline from pipecat.pipeline.task import PipelineTask from pipecat.services.openai import OpenAILLMService from pipecat.transports.network.daily import DailyTransport from synap_pipecat import SynapMemoryProcessor, SynapRecorder async def build_pipeline(sdk, transport, user_id: str, customer_id: str | None = None) -> Pipeline: stt = ... # your STT service llm = OpenAILLMService(model="gpt-4o") tts = ... # your TTS service user_aggregator = ... assistant_aggregator = ... memory = SynapMemoryProcessor( sdk=sdk, user_id=user_id, customer_id=customer_id, max_results=6, mode="fast", ) recorder = SynapRecorder( sdk=sdk, user_id=user_id, customer_id=customer_id, ) return Pipeline([ transport.input(), stt, memory, user_aggregator, llm, tts, transport.output(), assistant_aggregator, recorder, ]) # Usage: invoked per call async def run_call(sdk, room_url: str, user_id: str, customer_id: str | None = None): transport = DailyTransport(room_url, ...) pipeline = await build_pipeline(sdk, transport, user_id=user_id, customer_id=customer_id) task = PipelineTask(pipeline) await task.run() ``` Three things to notice in this pattern: 1. **Memory injection happens once per LLM turn**, not once per audio frame; the processor only acts on `LLMMessagesFrame` events. 2. **Recording is async and non-blocking.** The audio path never waits on a Synap write. 3. **Scope is per-call.** Each `run_call` invocation gets its own processor instances with the right `user_id` / `customer_id`. *** ## Advanced patterns ### Multi-tenant scoping Both processors accept the standard scoping triple: `user_id` (required), optional `customer_id`, optional `conversation_id`. `customer_id` is required on B2B Synap instances and ignored on single-tenant ones. See [Memory Scopes](/concepts/memory-scopes). ```python theme={null} memory = SynapMemoryProcessor(sdk=sdk, user_id="alice", customer_id="acme") ``` For multi-tenant deployments, build processors per call rather than caching them globally; each call should have its scope baked in. ### Tuning retrieval mode `mode="fast"` is the default and the right choice for most voice flows. Switch to `"accurate"` only for use cases where missing relevant memory is worse than the additional pre-LLM latency that LLM-driven query decomposition and reranking add. ### Failure semantics The integration follows the Synap-wide contract, adapted for voice latency: * **`SynapMemoryProcessor` degrades gracefully**: frame passes through unmodified if context retrieval fails. * **`SynapRecorder` surfaces failures**: raises `SynapIntegrationError` which Pipecat's frame-error path catches and logs. This is by design: a voice call should never break because of a transient memory glitch, but write failures must be visible to monitoring. *** ## Going further * [Voice agent pattern](/patterns/voice-agent-livekit): the reference architecture for memory-backed voice agents. * [Voice concierge cookbook](/cookbook/voice-concierge): an end-to-end worked voice example. *** ## Next steps Context preloading and recording for LiveKit voice agents. Hooks and MCP server for the Claude Agent SDK. The retrieval API behind `SynapMemoryProcessor`: modes, scopes, and response shapes. How `user_id`, `customer_id`, and `conversation_id` interact across reads. # Pydantic AI Source: https://docs.maximem.ai/integrations/pydantic-ai Type-safe dependency dataclass and auto-registered tools for Pydantic AI agents. Add persistent, per-user memory to a Pydantic AI agent in two lines. The integration leans on Pydantic AI's dependency-injection model: scope and SDK travel through `deps`, and the search/store tools are registered onto the agent automatically. Requires Python 3.11+. ## Overview This guide shows how to add Synap to a Pydantic AI application to build agents that: * Recall user-specific facts, preferences, and past conversations * Persist new information surfaced during a conversation * Stay type-safe and testable end-to-end, with every dependency a dataclass The Synap Pydantic AI integration ships two exports: a `deps` dataclass and a one-shot registration function. | Export | Purpose | | ----------------------------- | ------------------------------------------------------------------------------ | | `SynapDeps` | Dataclass holding the SDK instance and user scope | | `register_synap_tools(agent)` | Registers `synap_search` and `synap_store` tools plus a system-prompt fragment | ## Setup Install the package alongside Pydantic AI: ```bash pip theme={null} pip install maximem-synap-pydantic-ai pydantic-ai ``` ```bash uv theme={null} uv add maximem-synap-pydantic-ai pydantic-ai # pip-compatible (existing venv): uv pip install maximem-synap-pydantic-ai pydantic-ai ``` The pip package is `maximem-synap-pydantic-ai`, but the import drops the `maximem-` prefix and uses underscores: `from synap_pydantic_ai import ...`. Configure your API key. Generate one from the [Synap Dashboard](https://synap.maximem.ai). ```bash .env theme={null} SYNAP_API_KEY=synap_your_key_here OPENAI_API_KEY=your-openai-api-key ``` Initialize the SDK once at application startup: ```python theme={null} from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK() await sdk.initialize() ``` See [SDK Initialization](/sdk/initialization) for the full lifecycle and configuration options. ## Basic integration Declare an agent with `deps_type=SynapDeps`, call `register_synap_tools(agent)` once, then pass `SynapDeps` per request: ```python theme={null} # pip install maximem-synap-pydantic-ai pydantic-ai from maximem_synap import MaximemSynapSDK from pydantic_ai import Agent from synap_pydantic_ai import SynapDeps, register_synap_tools sdk = MaximemSynapSDK() await sdk.initialize() agent: Agent[SynapDeps, str] = Agent( "openai:gpt-4o", deps_type=SynapDeps, system_prompt="You are a helpful assistant with long-term memory.", ) register_synap_tools(agent) deps = SynapDeps(sdk=sdk, user_id="alice", customer_id="acme") result = await agent.run("What do you remember about my project?", deps=deps) print(result.data) ``` `register_synap_tools` does three things: 1. Registers `synap_search`, a tool the agent can call to retrieve memories 2. Registers `synap_store`, a tool the agent can call to persist new memories 3. Appends a system-prompt fragment instructing the agent to use both tools The agent never sees `user_id` or `customer_id` directly. It pulls them from `RunContext[SynapDeps]` inside the tool implementations, so the model cannot spoof scope. *** ## Core concepts ### SynapDeps `SynapDeps` is the dependency container Pydantic AI injects into every tool call. It carries the SDK and the scoping triple: ```python theme={null} from dataclasses import dataclass from maximem_synap import MaximemSynapSDK @dataclass class SynapDeps: sdk: MaximemSynapSDK user_id: str customer_id: str | None = None conversation_id: str | None = None ``` You construct a fresh `SynapDeps` per request, which means the same agent instance can serve any number of users without bleeding scope between them. ### register\_synap\_tools `register_synap_tools(agent)` attaches the two memory tools to your agent. After registration, every run that passes `SynapDeps` exposes: * **`synap_search(query: str, max_results: int = 5)`**: returns a list of memory objects scoped to `deps.user_id` (and `deps.customer_id` if set) * **`synap_store(content: str, memory_type: str = "fact")`**: persists a new memory under the same scope ```python theme={null} from pydantic_ai import Agent from synap_pydantic_ai import SynapDeps, register_synap_tools agent: Agent[SynapDeps, str] = Agent( "openai:gpt-4o", deps_type=SynapDeps, system_prompt="You answer questions about the user's history.", ) register_synap_tools(agent) ``` Tool descriptions are written so the model calls `synap_search` for recall questions and `synap_store` when the user shares a new fact. *** ## Complete example: per-user assistant The following pattern is what most production deployments end up with: a single `Agent` defined at module load, fresh `SynapDeps` per inbound request, and a thin handler around it. ```python theme={null} from pydantic_ai import Agent from synap_pydantic_ai import SynapDeps, register_synap_tools # Define the agent once at startup agent: Agent[SynapDeps, str] = Agent( "openai:gpt-4o", deps_type=SynapDeps, system_prompt=( "You are a personal assistant with long-term memory.\n" "Call synap_search before answering any question about the user.\n" "Call synap_store whenever the user shares a fact, preference, or " "decision worth remembering.\n" "If synap_search returns no results, say you don't know yet." ), ) register_synap_tools(agent) # Per-request handler: fresh SynapDeps each time async def handle_request( sdk, user_id: str, message: str, customer_id: str | None = None, conversation_id: str | None = None, ) -> str: deps = SynapDeps( sdk=sdk, user_id=user_id, customer_id=customer_id, conversation_id=conversation_id, ) result = await agent.run(message, deps=deps) return result.data # Usage reply = await handle_request(sdk, user_id="alice", message="Am I on the Pro plan?") ``` Three things to notice in this pattern: 1. **The agent is defined once.** Pydantic AI's dependency injection makes it safe to share: each `run()` gets its own `deps`. 2. **Scope is request-local.** Different users get different `SynapDeps`; the model only ever sees a scope-stripped tool surface. 3. **The system prompt is the policy.** Want recall to be optional? Loosen the prompt. Want stores to be aggressive? Tighten it. The tools obey whatever the prompt tells the model. *** ## Advanced patterns ### Multi-tenant scoping `SynapDeps` carries the same scoping triple every Synap integration uses: `user_id` (required), optional `customer_id`, optional `conversation_id`. `customer_id` is required on B2B Synap instances and ignored on single-tenant ones. See [Memory Scopes](/concepts/memory-scopes). ```python theme={null} # User-only deps = SynapDeps(sdk=sdk, user_id="alice") # Organization-scoped (user sees org-shared memories too) deps = SynapDeps(sdk=sdk, user_id="alice", customer_id="acme-corp") # Pinned to a conversation (biases retrieval ranking). # conversation_id must be a valid UUID. import uuid deps = SynapDeps(sdk=sdk, user_id="alice", conversation_id=str(uuid.uuid4())) ``` ### Testing with mock deps Because `SynapDeps` is just a dataclass, you can substitute a fake SDK in tests: ```python theme={null} @dataclass class FakeSdk: async def memories_search(self, **kwargs): return [{"content": "User is on Pro plan", "type": "fact"}] deps = SynapDeps(sdk=FakeSdk(), user_id="test-user") result = await agent.run("What plan am I on?", deps=deps) ``` ### Failure semantics The integration follows the Synap-wide contract: * **`synap_search` degrades gracefully**: returns `[]` and logs an error if Synap is unreachable, so the agent can continue. * **`synap_store` surfaces failures**: raises `SynapIntegrationError` so the caller knows persistence failed. This is by design: read failures shouldn't break a user-facing answer, but silent write failures would let the memory drift away from reality. *** ## Going further * [Patterns overview](/patterns/overview): reusable memory patterns across frameworks. * [Cookbook overview](/cookbook/overview): end-to-end worked examples. *** ## Next steps Function tools for the OpenAI Agents SDK. Storage backend for CrewAI crews. The retrieval API that powers `synap_search`: modes, scopes, and response shapes. How `user_id`, `customer_id`, and `conversation_id` interact across reads. # Semantic Kernel Source: https://docs.maximem.ai/integrations/semantic-kernel Kernel plugin with memory search and storage functions for Microsoft Semantic Kernel. Add persistent, per-user memory to a Semantic Kernel application as a single plugin. Both kernel functions (`search_memory` and `store_memory`) are auto-invokable, so the kernel can decide when to query or persist memories on its own. Requires Python 3.11+. ## Overview This guide shows how to add Synap to a Semantic Kernel application to build kernels that: * Recall user-specific facts, preferences, and past conversations * Persist new information surfaced during a conversation * Auto-invoke memory functions when the model decides they are relevant The Synap Semantic Kernel integration ships a single plugin class with two kernel functions. | Export | SK interface | Purpose | | ------------- | ------------- | ------------------------------------------------------------ | | `SynapPlugin` | Kernel plugin | Provides `search_memory` and `store_memory` kernel functions | ## Setup Install the package alongside Semantic Kernel: ```bash pip theme={null} pip install maximem-synap-semantic-kernel semantic-kernel ``` ```bash uv theme={null} uv add maximem-synap-semantic-kernel semantic-kernel # pip-compatible (existing venv): uv pip install maximem-synap-semantic-kernel semantic-kernel ``` The pip package is `maximem-synap-semantic-kernel`, but the import drops the `maximem-` prefix and uses underscores: `from synap_semantic_kernel import ...`. Configure your API key. Generate one from the [Synap Dashboard](https://synap.maximem.ai). ```bash .env theme={null} SYNAP_API_KEY=synap_your_key_here OPENAI_API_KEY=your-openai-api-key ``` Initialize the SDK once at application startup: ```python theme={null} from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK() await sdk.initialize() ``` See [SDK Initialization](/sdk/initialization) for the full lifecycle and configuration options. ## Basic integration The smallest useful integration adds `SynapPlugin` to a kernel and invokes a prompt that references the plugin's functions: ```python theme={null} # pip install maximem-synap-semantic-kernel semantic-kernel from maximem_synap import MaximemSynapSDK from semantic_kernel import Kernel from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion from synap_semantic_kernel import SynapPlugin sdk = MaximemSynapSDK() await sdk.initialize() kernel = Kernel() kernel.add_service(OpenAIChatCompletion(service_id="default")) kernel.add_plugin( SynapPlugin(sdk=sdk, user_id="alice", customer_id="acme"), plugin_name="synap", ) result = await kernel.invoke_prompt( "{{synap.search_memory query='project priorities'}} What are my top priorities?" ) ``` The scoping triple (`user_id`, optional `customer_id`) is bound when you construct the plugin. The kernel functions only ever see `query`, `content`, and other model-supplied parameters. This prevents prompts (or prompt injections) from spoofing scope. For automatic invocation (the kernel chooses when to call memory functions), enable auto function calling. See "Auto function calling" below. *** ## Core concepts ### SynapPlugin `SynapPlugin` is a regular Semantic Kernel plugin class with `@kernel_function` annotated methods. Adding it to the kernel registers both functions under the plugin name you choose: ```python theme={null} from synap_semantic_kernel import SynapPlugin plugin = SynapPlugin( sdk=sdk, user_id="alice", customer_id="acme", # optional, required for B2B instances ) kernel.add_plugin(plugin, plugin_name="synap") ``` After registration, the kernel can invoke `synap.search_memory` and `synap.store_memory` either through prompt templating, manual invocation, or auto function calling. ### search\_memory Function signature exposed to the kernel: ```text theme={null} search_memory(query: str, max_results: int = 5) -> str ``` Returns a formatted string of results, suitable for direct interpolation into prompt templates. **Search failures degrade gracefully**: the function returns an empty result string and logs an error so the prompt template renders without breaking. ### store\_memory Function signature exposed to the kernel: ```text theme={null} store_memory(content: str, memory_type: str = "fact") -> str ``` Returns `"Memory stored successfully."` on success. **Store failures surface explicitly**: the function raises `SynapIntegrationError` so the kernel (and caller) know if persistence failed. *** ## Complete example: chat function with auto-invoked memory The following kernel auto-invokes `synap.search_memory` and `synap.store_memory` whenever the model decides they are relevant. The application just adds messages to a `ChatHistory` and the kernel handles the rest: ```python theme={null} from semantic_kernel import Kernel from semantic_kernel.connectors.ai import FunctionChoiceBehavior from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion from semantic_kernel.contents import ChatHistory from synap_semantic_kernel import SynapPlugin def build_kernel(sdk, user_id: str, customer_id: str | None = None) -> Kernel: kernel = Kernel() kernel.add_service(OpenAIChatCompletion(service_id="default")) kernel.add_plugin( SynapPlugin(sdk=sdk, user_id=user_id, customer_id=customer_id), plugin_name="synap", ) return kernel async def chat(kernel: Kernel, history: ChatHistory, message: str) -> str: settings = kernel.get_prompt_execution_settings_from_service_id("default") settings.function_choice_behavior = FunctionChoiceBehavior.Auto() history.add_user_message(message) chat_service = kernel.get_service("default") response = await chat_service.get_chat_message_content( chat_history=history, settings=settings, kernel=kernel, ) history.add_message(response) return str(response) # Usage kernel = build_kernel(sdk, user_id="alice", customer_id="acme") history = ChatHistory(system_message=( "You are a personal assistant. Call synap.search_memory before answering " "questions about the user. Call synap.store_memory when they share a new fact." )) await chat(kernel, history, "I just upgraded to the Pro plan.") reply = await chat(kernel, history, "What plan am I on?") # → "You're on the Pro plan." ``` Three things to notice in this pattern: 1. **`FunctionChoiceBehavior.Auto()` lets the model drive memory.** No prompt-template plumbing; the model calls the right function when it's the right time. 2. **Scope is per-kernel.** `build_kernel(...)` constructs a fresh kernel per user so concurrent requests cannot bleed scope. 3. **The system message is the policy.** Tell the model when to search and when to store; the plugin just enforces it. *** ## Advanced patterns ### Multi-tenant scoping `SynapPlugin` accepts the standard scoping triple: `user_id` (required), optional `customer_id`, optional `conversation_id`. `customer_id` is required on B2B Synap instances and ignored on single-tenant ones. See [Memory Scopes](/concepts/memory-scopes). ```python theme={null} # User-scoped only plugin = SynapPlugin(sdk=sdk, user_id="alice") # Organization-scoped (user sees org-shared memories too) plugin = SynapPlugin(sdk=sdk, user_id="alice", customer_id="acme-corp") ``` For multi-tenant services, build the plugin (and kernel) per request rather than sharing one across users. ### Prompt-template invocation Outside of auto function calling, you can interpolate plugin calls directly in prompt templates: ```python theme={null} result = await kernel.invoke_prompt( "Context: {{synap.search_memory query='dietary preferences'}}\n" "Question: What should I order for dinner?" ) ``` This pattern is useful when you want deterministic recall (the function *always* runs) rather than letting the model decide. ### Failure semantics The integration follows the Synap-wide contract: * **`search_memory` degrades gracefully**: returns an empty result string and logs an error if Synap is unreachable. * **`store_memory` surfaces failures**: raises `SynapIntegrationError` so the kernel and caller know persistence failed. This is by design: read failures shouldn't break a user-facing answer, but silent write failures would let the memory drift away from reality. *** ## Going further * [Patterns overview](/patterns/overview): reusable memory patterns across frameworks. * [Cookbook overview](/cookbook/overview): end-to-end worked examples. *** ## Next steps Context and history providers for MAF agents. Function tools for the OpenAI Agents SDK. The retrieval API behind `search_memory`: modes, scopes, and response shapes. How `user_id` and `customer_id` interact across reads and writes. # Smolagents Source: https://docs.maximem.ai/integrations/smolagents Synap memory tools and a per-step turn recorder for Hugging Face Smolagents. Give a [Smolagents](https://github.com/huggingface/smolagents) `CodeAgent` or `ToolCallingAgent` persistent memory through Synap. Smolagents keeps its own fixed step log with no pluggable memory backend, so Synap plugs into the extension points it *does* expose: `@tool` functions, a `step_callbacks` recorder, and static `instructions`. Requires Python 3.11+ and `smolagents>=1.26`. ## Overview Three surfaces, mapped onto Smolagents' own extension points. Adopt only the ones you need. | Surface | Smolagents extension point | Purpose | | ----------------------- | ---------------------------------- | ------------------------------------------------------------ | | `create_synap_tools` | `@tool` | Explicit `search_memory` / `store_memory` the model can call | | `create_synap_recorder` | `step_callbacks={ActionStep: ...}` | Record each completed action into Synap for future recall | | `synap_st_instructions` | `CodeAgent(instructions=...)` | Fold Synap short-term context into the agent's instructions | All three take an already-constructed `MaximemSynapSDK`. Your app owns the SDK and its credentials. ## Setup ```bash pip theme={null} pip install maximem-synap-smolagents smolagents ``` ```bash uv theme={null} uv add maximem-synap-smolagents smolagents ``` ## Basic integration Give the agent memory tools it can call. Because `CodeAgent` writes its actions as Python, `search_memory` / `store_memory` compose naturally into the code it generates. ```python theme={null} from smolagents import CodeAgent, InferenceClientModel from maximem_synap import MaximemSynapSDK from synap_smolagents import create_synap_tools sdk = MaximemSynapSDK(api_key="sk-...") agent = CodeAgent( model=InferenceClientModel(), tools=create_synap_tools(sdk, user_id="alice", customer_id="acme"), ) agent.run("What did we decide about the rollout?") ``` ## Core concepts ### create\_synap\_tools Returns `search_memory` and `store_memory` as Smolagents `@tool` functions. `search_memory` reads Synap's long-term layer and returns formatted context; `store_memory` ingests a fact for future recall. Reads degrade to a not-found message; the write raises `SynapIntegrationError` so a failed store is observable. ### create\_synap\_recorder Records each completed step into Synap. Register it per step type on the agent: ```python theme={null} from smolagents import CodeAgent, InferenceClientModel from smolagents.memory import ActionStep from synap_smolagents import create_synap_recorder agent = CodeAgent( model=InferenceClientModel(), tools=[], step_callbacks={ ActionStep: create_synap_recorder( sdk, user_id="alice", conversation_id="conv_abc" ) }, ) ``` Each `ActionStep` is written to its own Synap document, so a multi-step run is captured in full. Failed or empty steps are skipped. ### synap\_st\_instructions Smolagents' `instructions` is a static string, so short-term context is folded into it once at construction. ```python theme={null} from synap_smolagents import synap_st_instructions agent = CodeAgent( model=InferenceClientModel(), tools=[], instructions=synap_st_instructions( sdk, conversation_id="conv_abc", instructions="You are a concise, friendly assistant.", ), ) ``` `conversation_id` is required. Empty context is a no-op; SDK failures are swallowed by default (`on_error="fallback"`), or set `on_error="raise"` for strict environments. ## Complete example: memory-augmented CodeAgent ```python theme={null} from smolagents import CodeAgent, InferenceClientModel from smolagents.memory import ActionStep from maximem_synap import MaximemSynapSDK from synap_smolagents import create_synap_tools, create_synap_recorder, synap_st_instructions sdk = MaximemSynapSDK(api_key="sk-...") agent = CodeAgent( model=InferenceClientModel(), tools=create_synap_tools(sdk, user_id="alice", customer_id="acme"), instructions=synap_st_instructions( sdk, conversation_id="conv_abc", instructions="You are a concise, friendly assistant.", ), step_callbacks={ ActionStep: create_synap_recorder( sdk, user_id="alice", conversation_id="conv_abc", customer_id="acme" ) }, ) agent.run("Remind me what plan I'm on and my open ticket.") ``` ## Advanced patterns ### Synchronous framework, async SDK Smolagents runs synchronously and its tools do not support `async`, so every surface bridges to the async Synap SDK internally (via the shared `run_async` helper). If your application is otherwise async, follow Hugging Face's guidance and run the agent on its own thread (e.g. `await anyio.to_thread.run_sync(agent.run, task)`). ### Error policy * **The `search_memory` tool** degrades: a Synap blip returns a not-found message. * **The `store_memory` tool** raises `SynapIntegrationError` on failure (model-driven, user-initiated). * **The recorder logs and never raises.** It runs inside the agent loop, in a `finally` with no surrounding try/except, so a raising callback would abort the whole run; a failed ingest is logged and swallowed. ## Going further * **Scoping.** `user_id` and `customer_id` flow straight to Synap; per-user isolation and B2C/B2B scope are derived server-side from the ids you pass. ## Next steps How `memories.create` extraction differs from conversation recording. Every framework Synap plugs into. # Strands Agents Source: https://docs.maximem.ai/integrations/strands-agents Synap as a native MemoryStore, short-term context hook, tools, and real-time anticipation feed for Strands Agents. Give a [Strands Agents](https://strandsagents.com/) agent persistent memory through Synap. Unlike bolt-on integrations, Synap plugs into Strands' own extension points: a native `MemoryStore` for long-term memory, hooks for short-term context and real-time anticipation, and `@tool` functions for explicit control. Requires Python 3.11+ and `strands-agents>=1.48`. ## Overview Four surfaces, each mapped onto a native Strands extension point. Adopt only the ones you need. | Surface | Strands extension point | Purpose | | -------------------- | ------------------------------------------- | ------------------------------------------------------------------------------ | | `SynapMemoryStore` | `MemoryStore` → `MemoryManager` | Long-term memory: recall, automatic prompt injection, server-side extraction | | `SynapShortTermHook` | `HookProvider` (`BeforeInvocationEvent`) | Inject Synap's working-memory summary before each turn | | `create_synap_tools` | `@tool` | Explicit `search_memory` / `store_memory` for agents not using `MemoryManager` | | `SynapStreamHook` | `HookProvider` (message + tool-call events) | Feed turns and tool intent onto Synap's gRPC Listen / anticipation stream | All four take an already-constructed `MaximemSynapSDK`. Your app owns the SDK, its credentials, and (for streaming) its connection lifecycle. ## Setup ```bash pip theme={null} pip install maximem-synap-strands-agents strands-agents ``` ```bash uv theme={null} uv add maximem-synap-strands-agents strands-agents ``` ## Basic integration Register Synap as a Strands `MemoryStore`; `MemoryManager` then handles recall, automatic prompt injection, and extraction. ```python theme={null} from strands import Agent from strands.memory import MemoryManager from maximem_synap import MaximemSynapSDK from synap_strands_agents import SynapMemoryStore sdk = MaximemSynapSDK(api_key="sk-...") store = SynapMemoryStore(sdk, user_id="alice", customer_id="acme") agent = Agent(memory_manager=MemoryManager(stores=[store])) result = agent("What did we decide about the rollout?") ``` ## Core concepts ### SynapMemoryStore A structural implementation of Strands' `MemoryStore` protocol. * **`search`** reads Synap's long-term layer via `sdk.fetch` and returns structured `MemoryEntry` objects (facts, preferences, episodes, emotions, temporal events). Reads degrade gracefully: a Synap blip returns `[]` rather than crashing recall. * **`add_messages`** is the primary write sink. `MemoryManager` hands it conversation batches; it ingests the assembled transcript via `sdk.memories.create` (server-side extraction, no extra model call) under a **stable `document_id`**, so repeated extraction submissions update one document instead of duplicating. * **`add`** ingests a single fact via `sdk.memories.create`. Recorded conversation messages feed only Synap's short-term compaction, not the long-term extraction layer `search` reads, which is why writes deliberately go through `memories.create`, not conversation recording. ### SynapShortTermHook Strands' `system_prompt` is a static string, so short-term context is injected via a hook on `BeforeInvocationEvent` (the one before-event whose messages are writable). It folds Synap's working-memory summary into the current turn's first user message. ```python theme={null} from synap_strands_agents import SynapShortTermHook agent = Agent( system_prompt="You are a support agent.", hooks=[SynapShortTermHook(sdk, conversation_id="conv_abc")], ) ``` `conversation_id` is required and explicit. Empty context is a no-op; SDK failures are swallowed by default (`on_error="fallback"`), or set `on_error="raise"` for strict environments. Strands exposes no ephemeral per-call injection hook to user code, so injected context becomes part of the conversation the agent persists. The hook folds one context block into each turn's user message (with an idempotency guard) rather than adding throwaway turns. ### create\_synap\_tools For agents that want explicit control instead of `MemoryManager`. Returns `search_memory` and `store_memory` as Strands `@tool` functions. ```python theme={null} from synap_strands_agents import create_synap_tools agent = Agent( system_prompt="You are a helpful assistant.", tools=create_synap_tools(sdk, user_id="alice", customer_id="acme"), ) ``` ### SynapStreamHook Makes the agent a participant in Synap's real-time anticipation pipeline by feeding its turns and tool-call intent onto the gRPC Listen stream. See [Advanced patterns](#advanced-patterns). ## Complete example: multi-user support agent ```python theme={null} from strands import Agent from strands.memory import MemoryManager from maximem_synap import MaximemSynapSDK from synap_strands_agents import SynapMemoryStore, SynapShortTermHook sdk = MaximemSynapSDK(api_key="sk-...") def build_agent(user_id: str, conversation_id: str) -> Agent: store = SynapMemoryStore(sdk, user_id=user_id, customer_id="acme", conversation_id=conversation_id) return Agent( system_prompt="You are a concise, friendly support agent.", memory_manager=MemoryManager(stores=[store]), hooks=[SynapShortTermHook(sdk, conversation_id=conversation_id)], ) agent = build_agent("alice", "conv_alice_001") print(agent("Remind me what plan I'm on and my open ticket.")) # → recalls prior context from Synap, answers, and extracts new memories. ``` ## Advanced patterns ### Real-time anticipation with the Listen stream `SynapStreamHook` feeds the agent's turns and tool-call intent onto Synap's gRPC Listen stream so the Anticipation Agent can pre-fetch context before the agent asks. Your app owns the stream lifecycle; the hook only feeds an already-open stream and no-ops when none is active. See [Real-Time Anticipation](/concepts/real-time-anticipation) for the model, and [Real-Time Anticipation in a Server](/patterns/real-time-anticipation-server) for running it in a multi-tenant process. ```python theme={null} import uuid from synap_strands_agents import SynapStreamHook, SynapShortTermHook conversation_id = str(uuid.uuid4()) # must be a valid UUID await sdk.instance.listen() # app opens the stream hook = SynapStreamHook(sdk, conversation_id=conversation_id, user_id="alice") agent = Agent(hooks=[hook, SynapShortTermHook(sdk, conversation_id=conversation_id)]) # ... run the agent ... await sdk.instance.stop_listening() # app closes it on shutdown ``` Use a real UUID here. `send_message()` does not validate `conversation_id`, so a free-form id like `"conv_abc"` appears to work on the stream, but every other call that takes a `conversation_id` (`fetch`, `record_message`, compaction) rejects it with `InvalidConversationIdError`, leaving you with a conversation you cannot read back. Run on one event loop. The stream's background tasks bind to the loop `listen()` ran on: construct the SDK, call `listen()`, and run the agent on that same asyncio loop. The hook itself performs no ingestion: durable memory is `SynapMemoryStore` or the tools. Note, though, that the turns it feeds onto the stream are persisted server-side and are promoted into long-term memory when the conversation compacts, so pairing the hook with a per-turn store extracts the same content twice. See [What the stream does to memory](/concepts/real-time-anticipation#what-the-stream-does-to-memory). ### Not `SessionManager` This integration does not implement Strands' `SessionManager` / snapshot storage. That persists opaque conversation snapshots for replay, a different concern from Synap's semantic memory. Use `FileSessionManager` / `S3SessionManager` for durable sessions *and* a `SynapMemoryStore` for memory; they compose. ## Going further * **Error policy.** Reads (`search`, `search_memory`) degrade and return empty; writes (`add`, `add_messages`, `store_memory`) raise `SynapIntegrationError`; stream sends log and never raise. * **Scoping.** `user_id` and `customer_id` flow straight to Synap; per-user isolation and B2C/B2B scope are derived server-side from the ids you pass. ## Next steps How `memories.create` extraction and conversation recording differ. Every framework Synap plugs into. # Vercel AI SDK Source: https://docs.maximem.ai/integrations/vercel-ai-sdk Model middleware that wraps any Vercel AI SDK model with automatic Synap context. Wrap any Vercel AI SDK model with one line and get persistent, per-user memory on every call. `generateText`, `streamText`, and `generateObject` all keep working; the middleware just makes them memory-aware. `@maximem/synap-vercel-adk` is a native TypeScript client. Context fetching and memory writes use `fetch` only, so they run on Edge Runtime and Cloudflare Workers without pinning a Node.js runtime. The optional anticipation stream needs Node.js. See [Installation → JavaScript and TypeScript SDK](/setup/installation#javascript-and-typescript-sdk). Runtime: Node.js 20+ for the stream; context and memory operations also run on Edge, Cloudflare Workers and the browser. ## Overview This guide shows how to add Synap to a Vercel AI SDK application to build apps that: * Inject relevant memory before every `generateText` / `streamText` / `generateObject` call * Record completed turns back to Synap automatically * Work with any provider (OpenAI, Anthropic, Google, etc.) without per-provider plumbing The Synap Vercel AI SDK integration ships a factory plus a provider class. | Export | Purpose | | --------------- | ------------------------------------------------- | | `createSynap` | Async factory that initializes the Synap provider | | `SynapProvider` | Provider class with `wrap` and `listen` methods | ## Setup Install the package: ```bash theme={null} npm install @maximem/synap-vercel-adk ai @ai-sdk/openai ``` Import the integration from its package name directly: `import { createSynap } from "@maximem/synap-vercel-adk"`. `createSynap` initializes the underlying Synap SDK for you, so you don't import the core `@maximem/synap-js-sdk` package separately. Configure your API key. Generate one from the [Synap Dashboard](https://synap.maximem.ai). ```bash .env theme={null} SYNAP_API_KEY=synap_your_key_here OPENAI_API_KEY=your-openai-api-key ``` Initialize the provider once at application startup: ```typescript theme={null} import { createSynap } from "@maximem/synap-vercel-adk"; const synap = await createSynap({ apiKey: process.env.SYNAP_API_KEY!, }); ``` `createSynap` initializes the underlying Synap SDK internally, so you don't need to manage the SDK lifecycle separately. See [SDK Initialization](/sdk/initialization) for the full lifecycle if you'd rather construct the SDK directly. ## Basic integration The smallest useful integration wraps a model with `synap.wrap` and uses it like any other Vercel AI SDK model: ```typescript theme={null} // npm install @maximem/synap-vercel-adk ai @ai-sdk/anthropic import { generateText } from "ai"; import { anthropic } from "@ai-sdk/anthropic"; import { createSynap } from "@maximem/synap-vercel-adk"; // createSynap initializes the underlying Synap SDK for you const synap = await createSynap({ apiKey: process.env.SYNAP_API_KEY! }); const model = synap.wrap(anthropic("claude-sonnet-4-6"), { userId: "alice", customerId: "acme", // optional, required for B2B instances }); const { text } = await generateText({ model, messages: [{ role: "user", content: "What do you remember about my account?" }], }); ``` On every call, the middleware fetches Synap context, injects it as a system message, proxies the request to the wrapped model, and ingests the resulting turn back into Synap. **Context-fetch failures degrade gracefully** (empty context, error logged); **turn-ingestion failures surface explicitly** so silent data loss is impossible. *** ## Core concepts ### synap.wrap `synap.wrap(model, options)` returns a standard Vercel AI SDK `LanguageModel`. Drop it anywhere you'd use the underlying model: `generateText`, `streamText`, `generateObject`, agentic loops, structured output, etc. ```typescript theme={null} const model = synap.wrap(openai("gpt-4o"), { userId: "alice", customerId: "acme", conversationId: crypto.randomUUID(), // optional, biases retrieval to this thread; must be a valid UUID }); ``` The scoping triple is bound when you call `wrap`. The model only ever sees the messages, never `userId`/`customerId`. This means prompt injection cannot spoof scope. ### The middleware loop On every call to the wrapped model: ```text theme={null} your code → synap.wrap(model) → [fetch context] → wrapped model → [ingest turn] → your code ``` 1. **Before**: fetches the user's Synap context and injects it as a system message. 2. **Generates**: proxies the request to the wrapped model unchanged. 3. **After**: ingests the completed user + assistant turn into Synap asynchronously. Steps 1 and 3 are independent: a failure in either does not block the other. ### Provider-agnostic Wrap any Vercel AI SDK-compatible model: ```typescript theme={null} import { openai } from "@ai-sdk/openai"; import { google } from "@ai-sdk/google"; import { anthropic } from "@ai-sdk/anthropic"; const gptWithMemory = synap.wrap(openai("gpt-4o"), { userId: "alice" }); const geminiWithMemory = synap.wrap(google("gemini-2.0-flash"), { userId: "alice" }); const claudeWithMemory = synap.wrap(anthropic("claude-sonnet-4-6"), { userId: "alice" }); ``` The middleware operates on the Vercel AI SDK abstraction, so the same wrap behavior applies to every provider. ### Streaming `streamText` and `streamObject` work without any code changes; the middleware injects context before the stream starts and ingests the turn when the stream ends: ```typescript theme={null} import { streamText } from "ai"; const { textStream } = await streamText({ model: synap.wrap(openai("gpt-4o"), { userId: "alice" }), messages: [{ role: "user", content: "Summarize my recent priorities." }], }); for await (const chunk of textStream) { process.stdout.write(chunk); } ``` *** ## Complete example: chat route with per-request scoping The pattern below is a typical Next.js (Node runtime) chat route. Each request gets its own scope baked into a freshly-wrapped model, so multiple concurrent users cannot leak into each other's memory: ```typescript theme={null} // app/api/chat/route.ts export const runtime = "nodejs"; // required, see runtime warning above import { streamText } from "ai"; import { openai } from "@ai-sdk/openai"; import { createSynap } from "@maximem/synap-vercel-adk"; // Initialize once at module load const synap = await createSynap({ apiKey: process.env.SYNAP_API_KEY! }); export async function POST(req: Request) { const { userId, customerId, messages } = await req.json(); const model = synap.wrap(openai("gpt-4o"), { userId, customerId }); const result = await streamText({ model, messages }); return result.toDataStreamResponse(); } ``` Three things to notice in this pattern: 1. **The provider is module-scoped, the model is request-scoped.** `createSynap` runs once; `synap.wrap` runs per request with the right `userId`. 2. **Streaming is unchanged.** No special integration needed; `streamText` and the middleware co-exist transparently. 3. **The `runtime = "nodejs"` pin matters.** Edge Runtime would break the Python-subprocess dependency. *** ## Advanced patterns ### Per-request scoping For multi-tenant services, build the wrapped model per request and never cache it across users: ```typescript theme={null} async function handleChat(userId: string, message: string) { const model = synap.wrap(openai("gpt-4o"), { userId }); const { text } = await generateText({ model, messages: [{ role: "user", content: message }], }); return text; } ``` ### Anticipation stream `synap.listen()` opens a gRPC stream that pre-fetches context speculatively before the user's next request arrives. This reduces perceived latency in long-lived server processes. Call it **once at startup**, not per user or per session, and let the middleware scope each request: ```typescript theme={null} // Once, at process startup (e.g. instrumentation.ts). Node.js only: // listen() silently no-ops in Edge and browser environments. await synap.listen(); // Diagnostics synap.isListening; // boolean: is the stream currently connected? synap.cacheSize; // number: entries in the anticipation cache // On graceful shutdown await synap.stopListening(); ``` `listen()` takes no arguments and returns `Promise`. Scoping stays where it always was, on `synap.wrap()`. Unlike the Python SDK, which raises, the TypeScript `listen()` catches every failure, logs a warning to the console, and silently falls back to HTTP context fetch. Your app keeps working, just without anticipation. Check `synap.isListening` rather than assuming the stream is up. `listen()` warms the anticipation cache; memories are written by `synap.wrap()`, which ingests each turn automatically. With the stream open the middleware also emits each turn as a conversation event, and those turns are extracted a second time when the conversation compacts. See [What the stream does to memory](/concepts/real-time-anticipation#what-the-stream-does-to-memory). ### Multi-tenant scoping `synap.wrap` accepts the standard scoping triple: `userId` (required), optional `customerId`, optional `conversationId`. `customerId` is required on B2B Synap instances and ignored on single-tenant ones. See [Memory Scopes](/concepts/memory-scopes). ```typescript theme={null} const model = synap.wrap(openai("gpt-4o"), { userId: "alice", customerId: "acme", }); ``` ### Failure semantics The middleware follows the Synap-wide contract: * **Context fetch degrades gracefully**: empty context is injected and the error logged if Synap is unreachable. * **Turn ingestion surfaces failures**: write failures raise `SynapIntegrationError` so callers know if persistence failed. This is by design: read failures shouldn't break a user-facing turn, but silent write failures would let memory drift away from reality. *** ## Going further * [Patterns overview](/patterns/overview): reusable memory patterns across frameworks. * [Cookbook overview](/cookbook/overview): end-to-end worked examples. *** ## Next steps `SynapMemory` and tools for Mastra. Hooks and MCP server for the Claude Agent SDK. The retrieval API behind `synap.wrap`: modes, scopes, and response shapes. How `userId`, `customerId`, and `conversationId` interact across reads. # Migrate from Letta (MemGPT) to Synap Source: https://docs.maximem.ai/migrations/from-letta Looking for a Letta / MemGPT alternative? Map Letta's agent-coupled core and archival memory onto Synap: concept mapping, SDK call equivalents, and the re-architecture involved. You're using Letta (formerly MemGPT) and want to evaluate or move to Synap. This page covers how Letta stores memory, how its concepts map onto Synap, and why this migration is a re-architecture rather than a drop-in. This page is the Letta-specific mapping. For the method every migration shares — scope mapping, configuring your instance, pilot, verify, cut over — see [How migration works](/migrations/how-it-works). ## How Letta stores memory Letta tightly couples memory and agent runtime: agents have `core_memory` (small, always-in-prompt), `archival_memory` (large, retrieval-only), and recall memory. It's a single-process agent state model, not a multi-tenant memory service. Letta was renamed from MemGPT and its SDK surface has evolved (e.g., `client.agents.archival_memory.create(...)`-style methods in current versions). The names below describe the conceptual mapping. Check your installed Letta SDK version for exact call signatures. ## The mapping | Letta concept | Synap concept | | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `agent.core_memory` | Build it from `sdk.user.context.fetch` results at every turn: it's not stored in Synap, it's assembled at retrieval time. Use `get_context_for_prompt` for the cached version. | | `agent.archival_memory.insert(text)` | `sdk.memories.create(document=text, document_type="document", user_id=..., customer_id=...)` | | `agent.archival_memory.search(query)` | `sdk.user.context.fetch(user_id=..., search_query=[query], types=[ContextType.FACTS])` (import `ContextType` from `maximem_synap.models.enums`, or pass the string `"facts"`) | | `agent.recall_memory` (last N messages) | `sdk.conversation.record_message` + `get_context_for_prompt` (returns recent + compacted) | | Per-agent state | Per-user + per-conversation state: Letta's "one agent" maps to Synap's `(user_id, conversation_id)` pair | ## The bigger shift Letta wants you to think in terms of "one persistent agent per user." Synap is provider-agnostic: the LLM provider doesn't matter; Synap is just memory. Migrating means: 1. Splitting agent state from memory: keep agent logic in your app code; move memory to Synap. 2. Replacing Letta's runtime with whichever LLM SDK you actually want (OpenAI, Anthropic, etc.). 3. Re-modeling `core_memory` as a prompt template populated from `ContextResponse` at every turn. This is a re-architecture, not a drop-in. The win is that your agent loop becomes portable across LLM providers. ## Next Once you've split memory out of the agent, follow the shared [migration method](/migrations/how-it-works) to map scopes, backfill archival memory, pilot one user, verify, and cut over. # Migrate from Mem0 to Synap Source: https://docs.maximem.ai/migrations/from-mem0 Looking for a Mem0 alternative? Map Mem0's user-scoped memory bag onto Synap: concept mapping, SDK call equivalents, and a backfill snippet for a clean cutover. You're using Mem0 and want to evaluate or move to Synap. This page covers how Mem0 stores memory, how its concepts map onto Synap, and how to backfill your existing data. This page is the Mem0-specific mapping. For the method every migration shares — scope mapping, configuring your instance, pilot, verify, cut over — see [How migration works](/migrations/how-it-works). ## How Mem0 stores memory Mem0 has a single user-scoped memory bag accessed via `m.add()`, `m.search()`, `m.get_all()`. Memories are untyped strings. Multi-user is `user_id`; multi-tenant is not first-class. ## The mapping | Mem0 concept | Synap concept | | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `user_id` | `user_id` | | No customer/org concept | Add `customer_id` if you have multi-tenant; otherwise pass a stable sentinel like your app name | | `m.add(messages, user_id=...)` | `sdk.memories.create(document=..., document_type="ai-chat-conversation", user_id=..., customer_id=...)` | | `m.search(query, user_id=...)` | `sdk.user.context.fetch(user_id=..., customer_id=..., search_query=[query])` | | `m.get_all(user_id=...)` | No exact equivalent: Synap doesn't expose a "list all raw memories" method. Use multiple targeted `search_query` calls with `sdk.user.context.fetch()` to retrieve the slices you actually need. | | Memory deletion: `m.delete(memory_id)` | `await sdk.memories.delete(memory_id)` | ## Backfill from Mem0 ```python theme={null} from mem0 import Memory from dateutil.parser import parse from maximem_synap import MaximemSynapSDK from maximem_synap.memories.models import CreateMemoryRequest old = Memory() # Mem0 new = MaximemSynapSDK() await new.initialize() async def migrate_one_user(user_id: str, customer_id: str = "my_app"): mem0_memories = old.get_all(user_id=user_id)["memories"] batch = [ CreateMemoryRequest( document=m["memory"], document_type="document", # Mem0 doesn't store conversation framing user_id=user_id, customer_id=customer_id, document_created_at=parse(m["created_at"]) if m.get("created_at") else None, mode="long-range", metadata={ "source": "mem0_backfill", "mem0_id": m["id"], }, ) for m in mem0_memories ] await new.memories.batch_create(documents=batch, fail_fast=False) ``` Mem0 stores only pre-extracted memory strings, not the original conversations, so this backfill ingests those strings directly. Where you still have the source conversations, ingest those instead — Synap re-extracts natively and produces richer, typed memories than re-processing a summary can. See [why re-extraction matters](/migrations/how-it-works#5-backfill). ## What you gain immediately * Typed extractions (facts vs preferences vs episodes vs emotions vs temporal events). * Customer / client / user scopes, not just user. * Entity resolution across conversations. * Context compaction. ## What you'll need to adapt Mem0's "search returns a flat list of strings" pattern becomes "fetch returns a `ContextResponse` with typed lists." Your prompt-construction code needs to iterate over `ctx.facts`, `ctx.preferences`, `ctx.episodes` etc. instead of one flat list — usually a few-line change. See [Response shapes](/sdk/response-shapes). ## Next Follow the shared [migration method](/migrations/how-it-works) to pilot one user, verify scope isolation, swap your retrieval call sites, and cut over. # Migrate from Supermemory to Synap Source: https://docs.maximem.ai/migrations/from-supermemory Move your Supermemory export into Synap with a ready-to-run script: map container tags onto Synap scopes, convert the export, verify the scope assignment, and ingest. This guide moves a Supermemory export into Synap. It comes with a script that does the mechanical work, and explains the two decisions the script cannot make for you: **which scope each container tag belongs to**, and **what to do about memory quality**. This is the hands-on procedure for Supermemory. For the method every migration shares — scope mapping, configuring your instance, verifying, and cutting over — see [How migration works](/migrations/how-it-works). ## How Supermemory stores memory Supermemory separates **documents** (the content you ingest) from **memories** (short facts extracted from those documents). Both are organised by **container tags** — flat string identifiers, each with its own isolated namespace. Tags can encode structure by convention, such as `org:acme:user:john`, but they are not hierarchical: to Supermemory they are opaque strings. A tag holding one person's chat history and a tag holding your company handbook look identical. That last point is the whole migration. Synap organises memory into three scopes — client, customer, and user — and nothing in a Supermemory export records which tag belongs where. You decide that, and this guide has you verify it twice before it becomes permanent. ## Before you start Download the export from your Supermemory dashboard. You get a single JSON file with a `documents` section (your original content) and a `memories` section (the facts Supermemory extracted from it). Confirm `truncated` is `false` in **both** sections. If either says `true`, the export was cut short at its item limit — re-export before going further. The script warns you, but cannot recover content that is not in the file. Extraction quality depends on your Instance's memory architecture, generated from the use-case file you supply at instance creation. If you created your Instance without one, add it **before** this import — otherwise your historical data is extracted with generic defaults, and you would re-import later to benefit from a tuned configuration. See [Memory Architecture](/concepts/memory-architecture). You need an Instance and an API key; see [Quickstart](/getting-started/quickstart). Note whether your Instance treats customers and users as separate (B2B) or as the same thing (B2C) — you need this in step 2. ## Get the script Save the script below as `supermemory_to_synap.py` in your working directory. Use the copy button in the top right of the block. ```python supermemory_to_synap.py expandable theme={null} #!/usr/bin/env python3 """ supermemory_to_synap.py — migrate a Supermemory export into Synap. Supermemory organises data with flat `container tags`. Synap organises it with a three-level scope hierarchy (client > customer > user). Nothing in a Supermemory export records which tag is one person, which is a team, and which is company-wide reference material — so that mapping cannot be inferred. You declare it once, in a scope map, and this tool applies it. Usage — four steps: 1. List every container tag in your export and write a scope map to fill in: python3 supermemory_to_synap.py map export.json -o scope_map.json 2. Open scope_map.json and set the scope for each tag (instructions are inside). 3. Convert to Synap-ready batch files. Writes files only; contacts no server: python3 supermemory_to_synap.py convert export.json -m scope_map.json -o ./synap_import 4. Load them into Synap (supports --dry-run, and resumes if interrupted): export SYNAP_API_KEY=... python3 supermemory_to_synap.py ingest ./synap_import `map` and `convert` need only the standard library. `ingest` additionally needs the Synap SDK: pip install maximem-synap Requires Python 3.9 or newer. """ from __future__ import annotations import argparse import asyncio import json import os import re import sys from collections import Counter, defaultdict from datetime import datetime, timezone from pathlib import Path from typing import Any # --------------------------------------------------------------------------- # Ingest constraints. Very short content carries too little signal to memorise # and is discarded on arrival without raising an error, so we check locally and # report it rather than letting documents disappear silently. # --------------------------------------------------------------------------- MIN_CONTENT_LEN = { "ai-chat-conversation": 10, "email": 50, "meeting-transcript": 50, "document": 100, } DEFAULT_MIN_LEN = 50 # Supermemory document type -> Synap document_type. Types with no direct Synap # equivalent become "document"; the export stores their extracted text anyway. TYPE_MAP = { "text": "document", "pdf": "pdf", "image": "image", "audio": "audio", "video": "audio", "granola": "meeting-transcript", "tweet": "document", "webpage": "document", "notion_doc": "document", "google_doc": "document", "google_slide": "document", "google_sheet": "document", "github_markdown": "document", "onedrive": "document", } # Supermemory wraps chat sessions as a header plus a JSON turn array. Both are # optional; plain-text documents pass through untouched. SESSION_DATE_RE = re.compile( r"(?:date .{0,40}took place|session date)\s*:\s*" r"([0-9]{1,2}:[0-9]{2}\s*[ap]\.?m\.?\s+on\s+[0-9]{1,2}\s+\w+,?\s*[0-9]{4}" r"|[0-9]{4}-[0-9]{2}-[0-9]{2}[T ][0-9]{2}:[0-9]{2}(?::[0-9]{2})?" r"|[0-9]{1,2}\s+\w+,?\s*[0-9]{4})", re.IGNORECASE, ) TURN_ARRAY_RE = re.compile(r"stringified JSON\s*:\s*(\[.*)", re.S) DATE_FORMATS = ( "%I:%M %p on %d %B, %Y", "%I:%M %p on %d %b, %Y", "%I:%M %p on %d %B %Y", "%I:%M %p on %d %b %Y", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%dT%H:%M", "%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M", "%d %B, %Y", "%d %b, %Y", "%d %B %Y", "%d %b %Y", ) # Supermemory's documented tag convention is colon-delimited key:value pairs, # e.g. "org:acme:user:john". Used only to pre-fill suggestions in the scope map. KNOWN_CUSTOMER_KEYS = ("org", "organisation", "organization", "team", "workspace", "account", "company", "tenant", "customer", "project") KNOWN_USER_KEYS = ("user", "member", "person", "employee", "uid") # --------------------------------------------------------------------------- # Export loading # --------------------------------------------------------------------------- def load_export(path: Path) -> tuple[list[dict], list[dict]]: """Read a dashboard export or a hand-assembled API dump. Dashboard exports look like {documents:{items:[]}, memories:{items:[]}}. API dumps are often {documents:[], memories:[]} or a bare document list. """ raw = json.loads(path.read_text(encoding="utf-8")) def section(name: str) -> list[dict]: node = raw.get(name) if isinstance(raw, dict) else None if node is None: return [] if isinstance(node, list): return node if node.get("truncated"): print( f" !! WARNING: '{name}' was truncated at limit {node.get('limit')}." f" This export is INCOMPLETE — re-export before migrating.", file=sys.stderr, ) return node.get("items", []) if isinstance(raw, list): return raw, [] docs, mems = section("documents"), section("memories") if not docs and not mems: raise SystemExit(f"{path}: no 'documents' or 'memories' found — not a Supermemory export?") return docs, mems def tag_of(doc: dict) -> str | None: """A document's container tag, from either the current or deprecated field.""" if doc.get("containerTag"): return doc["containerTag"] tags = doc.get("containerTags") or [] if len(tags) == 1: return tags[0] if len(tags) > 1: # Multiple tags cannot map to one Synap scope; caller reports and skips. return "\x00MULTI\x00" + "|".join(sorted(tags)) return None # --------------------------------------------------------------------------- # Subcommand: map # --------------------------------------------------------------------------- def suggest_scope(tag: str) -> dict[str, Any]: """Best-effort starting point for a tag. Always review these by hand.""" if ":" in tag: parts = tag.split(":") pairs = dict(zip(parts[0::2], parts[1::2])) cust = next((pairs[k] for k in KNOWN_CUSTOMER_KEYS if k in pairs), None) user = next((pairs[k] for k in KNOWN_USER_KEYS if k in pairs), None) if cust or user: return {"user_id": user, "customer_id": cust} return {"user_id": tag, "customer_id": None} def cmd_map(args) -> None: docs, mems = load_export(Path(args.export)) doc_tags: Counter = Counter() for d in docs: t = tag_of(d) doc_tags[t if t else "\x00UNTAGGED\x00"] += 1 mem_tags = Counter(m.get("containerTag") for m in mems if m.get("containerTag")) tags: dict[str, Any] = {} for tag in sorted(set(doc_tags) | set(mem_tags)): entry = (suggest_scope(tag) if not tag.startswith("\x00") else {"user_id": None, "customer_id": None}) entry["_documents"] = doc_tags.get(tag, 0) entry["_memories"] = mem_tags.get(tag, 0) if tag.startswith("\x00MULTI\x00"): entry["_note"] = "document carries MULTIPLE tags — pick one scope or split it" tags[tag] = entry out = { "_README": [ "Set user_id and customer_id for every tag. Synap derives the scope", "level from which of the two you provide — you never name a scope:", " user_id + customer_id -> USER scope (this person's own memories)", " customer_id only -> CUSTOMER scope (shared across that customer's users)", " neither (both null) -> CLIENT scope (shared across your whole account)", " user_id only -> INVALID in b2b; set customer_id too", "", "isolation: 'b2b' if your Instance separates customers from users;", "'b2c' if one customer == one user. Must match the Instance's setting.", "", "The suggestions below are guesses from the tag string. Review every one:", "a tag holding company-wide reference material belongs at CLIENT scope,", "not USER scope, and nothing in the export can tell them apart.", ], "isolation": args.isolation, "tags": tags, } Path(args.out).write_text(json.dumps(out, indent=2), encoding="utf-8") print(f"Wrote {args.out}") print(f" {len(tags)} container tags | {len(docs)} documents | {len(mems)} memories") print(f" Next: edit {args.out}, then run `convert`.") # --------------------------------------------------------------------------- # Conversion helpers # --------------------------------------------------------------------------- def parse_session_date(content: str) -> tuple[datetime | None, bool]: """(date, header_present). A present-but-unparseable header is an error. Supermemory's createdAt is when the document was UPLOADED, which for imported history is not when the conversation happened. Where the content carries a real session date we must use it, or every memory is timestamped to the upload date and time-relative questions answer wrongly. """ m = SESSION_DATE_RE.search(content[:600]) if not m: return None, False raw = re.sub(r"\s+", " ", m.group(1).replace(".", "")).strip() for fmt in DATE_FORMATS: try: return datetime.strptime(raw, fmt), True except ValueError: continue return None, True def render_content(content: str) -> tuple[str, bool, int]: """(text, is_conversation, n_turns). Turn arrays are flattened to `role: text` lines. Keeping the speaker labels lets Synap split a long session on turn boundaries rather than mid-sentence, which preserves who said what. """ m = TURN_ARRAY_RE.search(content) if m: try: turns = json.loads(m.group(1)) lines = [ f"{t.get('role', 'user')}: {(t.get('content') or '').strip()}" for t in turns if isinstance(t, dict) and (t.get("content") or "").strip() ] if lines: return "\n\n".join(lines), True, len(lines) except (json.JSONDecodeError, AttributeError): pass # fall through and treat as plain text text = content.strip() looks_chatty = bool(re.search(r"^(user|assistant|human|ai)\s*:", text, re.I | re.M)) return text, looks_chatty, 0 def resolve_scope(tag: str, scope_map: dict, isolation: str) -> tuple[str | None, str | None]: entry = scope_map.get(tag) if entry is None: raise ValueError(f"tag {tag!r} is not in the scope map — re-run `map` or add it") user_id = entry.get("user_id") or None customer_id = entry.get("customer_id") or None if isolation == "b2b" and user_id and not customer_id: raise ValueError( f"tag {tag!r}: user_id without customer_id is rejected by a b2b Instance" ) return user_id, customer_id def scope_level(user_id, customer_id, isolation: str) -> str: if isolation == "b2c": return "user" if (user_id or customer_id) else "client" if user_id and customer_id: return "user" if customer_id: return "customer" return "client" # --------------------------------------------------------------------------- # Subcommand: convert # --------------------------------------------------------------------------- def cmd_convert(args) -> None: docs, mems = load_export(Path(args.export)) cfg = json.loads(Path(args.map).read_text(encoding="utf-8")) scope_map = cfg.get("tags", {}) isolation = args.isolation or cfg.get("isolation", "b2b") baseline = defaultdict(list) for m in mems: for did in m.get("documentIds") or []: baseline[did].append({"id": m.get("id"), "memory": m.get("memory")}) requests: list[dict] = [] skipped: list[dict] = [] stats: Counter = Counter() levels: Counter = Counter() dct_src: Counter = Counter() for doc in docs: did = doc.get("id") or doc.get("customId") tag = tag_of(doc) if not tag or tag.startswith("\x00"): skipped.append({"id": did, "reason": "no single container tag"}) stats["skip_no_tag"] += 1 continue try: user_id, customer_id = resolve_scope(tag, scope_map, isolation) except ValueError as e: skipped.append({"id": did, "tag": tag, "reason": str(e)}) stats["skip_scope"] += 1 continue raw = doc.get("content") or doc.get("raw") or "" if not raw.strip(): # Connector-sourced documents often keep no local copy of the text. skipped.append({"id": did, "tag": tag, "reason": "empty content in export"}) stats["skip_empty"] += 1 continue body, is_convo, n_turns = render_content(raw) sm_type = (doc.get("type") or "text").lower() doc_type = "ai-chat-conversation" if is_convo else TYPE_MAP.get(sm_type, "document") min_len = MIN_CONTENT_LEN.get(doc_type, DEFAULT_MIN_LEN) if len(body) < min_len: skipped.append({ "id": did, "tag": tag, "reason": f"{len(body)} chars is below the {min_len}-char minimum " f"for '{doc_type}' and would be discarded silently", }) stats["skip_too_short"] += 1 continue session_dt, had_header = parse_session_date(raw) if session_dt: dct, src = session_dt, "session_header" elif had_header: skipped.append({"id": did, "tag": tag, "reason": "session-date header present but unparseable"}) stats["skip_bad_date"] += 1 continue elif doc.get("createdAt"): try: dct = datetime.fromisoformat(doc["createdAt"].replace("Z", "+00:00")) except ValueError: dct, src = datetime.now(timezone.utc), "now" else: src = "created_at" else: dct, src = datetime.now(timezone.utc), "now" dct_src[src] += 1 lvl = scope_level(user_id, customer_id, isolation) levels[lvl] += 1 stats["turns"] += n_turns requests.append({ "document": body, "document_type": doc_type, "document_id": did, # re-runs stay idempotent "document_created_at": dct.isoformat(), "user_id": user_id, "customer_id": customer_id, "mode": args.mode, "metadata": { "source": "supermemory_export", "supermemory_doc_id": did, "container_tag": tag, "synap_scope": lvl, "dct_source": src, "title": doc.get("title"), "sm_type": sm_type, "sm_created_at": doc.get("createdAt"), "sm_memory_count": len(baseline.get(did, [])), }, }) stats["converted"] += 1 out = Path(args.out) out.mkdir(parents=True, exist_ok=True) batches = [requests[i:i + args.batch_size] for i in range(0, len(requests), args.batch_size)] for i, b in enumerate(batches): (out / f"batch_{i:03d}.json").write_text( json.dumps({"documents": b, "fail_fast": False}, indent=1), encoding="utf-8") if baseline: (out / "baseline.json").write_text(json.dumps(dict(baseline), indent=1), encoding="utf-8") report = { "source": args.export, "isolation": isolation, "documents_in_export": len(docs), "memories_in_export": len(mems), "converted": stats["converted"], "turns_rendered": stats["turns"], "batches": len(batches), "scope_breakdown": dict(levels), "dct_sources": dict(dct_src), "skipped": {k: v for k, v in stats.items() if k.startswith("skip")}, "skipped_detail": skipped, } (out / "report.json").write_text(json.dumps(report, indent=2), encoding="utf-8") print(f"Converted {stats['converted']}/{len(docs)} documents -> {len(batches)} batches in {out}/") print(f" scope breakdown : {dict(levels) or 'none'}") print(f" date sources : {dict(dct_src) or 'none'}") print(f" skipped : {report['skipped'] or 'none'}") if skipped: print(f" -> see {out}/report.json for the reason on each skipped document") if dct_src.get("created_at") or dct_src.get("now"): print(" note: some documents fell back to their upload date because the export" "\n carried no original timestamp; time-relative recall on those is approximate.") # --------------------------------------------------------------------------- # Subcommand: ingest # --------------------------------------------------------------------------- async def _ingest(args) -> None: try: from maximem_synap import MaximemSynapSDK, CreateMemoryRequest, RateLimitError except ImportError: raise SystemExit("ingest requires the Synap SDK: pip install maximem-synap") files = sorted(Path(args.dir).glob("batch_*.json")) if not files: raise SystemExit(f"no batch_*.json in {args.dir} — run `convert` first") # Resume marker, so an interrupted run continues instead of re-sending. done_file = Path(args.dir) / ".ingested" done = set(done_file.read_text().split()) if done_file.exists() else set() if done: print(f"Resuming — {len(done)} batch(es) already sent.") if args.dry_run: for f in files: if f.name in done: continue n = len(json.loads(f.read_text())["documents"]) print(f" [dry-run] would send {f.name} ({n} documents)") return if not (args.api_key or os.environ.get("SYNAP_API_KEY")): raise SystemExit("set SYNAP_API_KEY or pass --api-key") sdk = MaximemSynapSDK(api_key=args.api_key) if args.api_key else MaximemSynapSDK() await sdk.initialize() totals: Counter = Counter() ingestion_ids: list[str] = [] try: for f in files: if f.name in done: continue payload = json.loads(f.read_text(encoding="utf-8")) batch = [CreateMemoryRequest(**d) for d in payload["documents"]] for attempt in range(1, args.retries + 1): try: result = await sdk.memories.batch_create(documents=batch, fail_fast=False) break except RateLimitError: # The whole batch is checked against your quota up front, so # this means the batch did not fit rather than that it failed. if attempt == args.retries: raise wait = args.backoff * attempt print(f" {f.name}: quota exceeded, waiting {wait}s " f"(attempt {attempt}/{args.retries})") await asyncio.sleep(wait) totals["succeeded"] += result.succeeded totals["failed"] += result.failed ingestion_ids += [str(r.ingestion_id) for r in result.results if r.ingestion_id] done.add(f.name) done_file.write_text("\n".join(sorted(done)), encoding="utf-8") print(f" {f.name}: {result.succeeded}/{len(batch)} accepted batch_id={result.batch_id}") (Path(args.dir) / "ingestion_ids.json").write_text( json.dumps(ingestion_ids, indent=1), encoding="utf-8") print(f"\nAccepted {totals['succeeded']} documents ({totals['failed']} rejected).") print(f"Wrote {len(ingestion_ids)} ingestion ids to {args.dir}/ingestion_ids.json") print("Memories are built in the background — run `verify` to watch them finish.") finally: await sdk.shutdown() def cmd_ingest(args) -> None: asyncio.run(_ingest(args)) # --------------------------------------------------------------------------- # Subcommand: verify # --------------------------------------------------------------------------- async def _verify(args) -> None: try: from maximem_synap import MaximemSynapSDK except ImportError: raise SystemExit("verify requires the Synap SDK: pip install maximem-synap") ids_file = Path(args.dir) / "ingestion_ids.json" if not ids_file.exists(): raise SystemExit(f"{ids_file} not found — run `ingest` first") ids = json.loads(ids_file.read_text(encoding="utf-8")) sdk = MaximemSynapSDK(api_key=args.api_key) if args.api_key else MaximemSynapSDK() await sdk.initialize() outcomes: Counter = Counter() memories = 0 incomplete: list[dict] = [] try: from uuid import UUID for i, ing in enumerate(ids, 1): try: st = await sdk.memories.wait_for_completion( UUID(ing), timeout_seconds=args.timeout) except TimeoutError: outcomes["timeout"] += 1 incomplete.append({"ingestion_id": ing, "status": "timeout"}) continue status = st.status.value if hasattr(st.status, "value") else str(st.status) outcomes[status] += 1 memories += st.memories_created if status != "completed": incomplete.append({"ingestion_id": ing, "status": status, "error": st.error_message}) if i % 25 == 0: print(f" checked {i}/{len(ids)} ...") finally: await sdk.shutdown() print(f"\n{len(ids)} ingestions -> {dict(outcomes)}") print(f"{memories} memories created.") if incomplete: p = Path(args.dir) / "incomplete.json" p.write_text(json.dumps(incomplete, indent=1), encoding="utf-8") print(f"{len(incomplete)} did not complete cleanly — details in {p}") print("'partial_success' means the document was processed but some memories " "were not stored; re-ingesting that document is safe.") def cmd_verify(args) -> None: asyncio.run(_verify(args)) # --------------------------------------------------------------------------- def main() -> None: p = argparse.ArgumentParser( description="Migrate a Supermemory export into Synap.", formatter_class=argparse.RawDescriptionHelpFormatter, epilog="Run `map` first, edit the scope map, then `convert`, `ingest`, `verify`.", ) sub = p.add_subparsers(dest="cmd", required=True) m = sub.add_parser("map", help="list every container tag and write a scope map to fill in") m.add_argument("export") m.add_argument("-o", "--out", default="scope_map.json") m.add_argument("--isolation", choices=("b2b", "b2c"), default="b2b") m.set_defaults(func=cmd_map) c = sub.add_parser("convert", help="apply the scope map and write Synap batch files") c.add_argument("export") c.add_argument("-m", "--map", default="scope_map.json") c.add_argument("-o", "--out", default="./synap_import") c.add_argument("--isolation", choices=("b2b", "b2c"), default=None, help="override the value stored in the scope map") c.add_argument("--batch-size", type=int, default=25) c.add_argument("--mode", choices=("fast", "long-range"), default="long-range") c.set_defaults(func=cmd_convert) i = sub.add_parser("ingest", help="send the batches to Synap (resumable)") i.add_argument("dir") i.add_argument("--api-key", default=None, help="defaults to $SYNAP_API_KEY") i.add_argument("--dry-run", action="store_true") i.add_argument("--retries", type=int, default=3) i.add_argument("--backoff", type=int, default=30) i.set_defaults(func=cmd_ingest) v = sub.add_parser("verify", help="wait for ingestion to finish and report results") v.add_argument("dir") v.add_argument("--api-key", default=None, help="defaults to $SYNAP_API_KEY") v.add_argument("--timeout", type=int, default=300) v.set_defaults(func=cmd_verify) args = p.parse_args() args.func(args) if __name__ == "__main__": main() ``` `map` and `convert` run offline using only the standard library. `ingest` and `verify` talk to Synap through the SDK: ```bash theme={null} pip install maximem-synap ``` ## The migration, step by step ```bash theme={null} python3 supermemory_to_synap.py map supermemory-export.json -o scope_map.json ``` This reads the export and writes a `scope_map.json` listing every container tag it found, with the number of documents and memories in each: ```json theme={null} { "isolation": "b2b", "tags": { "org:acme:user:john": { "user_id": "john", "customer_id": "acme", "_documents": 128, "_memories": 941 }, "acme_handbook": { "user_id": "acme_handbook", "customer_id": null, "_documents": 12, "_memories": 87 } } } ``` Where a tag follows Supermemory's `key:value` convention, the script pre-fills a suggestion. Everything else is a guess you need to correct. Open `scope_map.json` and set `user_id` and `customer_id` for each tag. You never name a scope directly. Synap works out the scope from which identifiers you provide: | `user_id` | `customer_id` | Resulting scope | Use for | | --------- | ------------- | -------------------------- | --------------------------------------- | | set | set | **user** | One person's own history | | not set | set | **customer** | Shared across everyone at that customer | | not set | not set | **client** | Shared across your entire account | | set | not set | Rejected on a B2B Instance | — | Also set `isolation` to match your Instance: `b2b` if customers and users are separate, `b2c` if one customer is one user. In the example above, `acme_handbook` is company reference material, so its correct mapping is both fields `null` — client scope — not the `user_id` the script guessed. Container tags are flat and mutually isolated, so nothing in the export distinguishes one person's history from company-wide material. A tag left at client scope by mistake becomes readable across your whole account. Step 4 has you check this before anything is sent. ```bash theme={null} python3 supermemory_to_synap.py convert supermemory-export.json \ -m scope_map.json -o ./synap_import ``` This writes files only and contacts no server: | File | What it holds | | ------------------ | -------------------------------------------------------- | | `batch_000.json` … | Your documents, ready to ingest, 25 per file | | `baseline.json` | Supermemory's own extracted memories, keyed by document | | `report.json` | Conversion stats and a reason for every skipped document | Open `report.json` and confirm `scope_breakdown` matches what you intended, and that `skipped` is empty or contains only documents you expect to lose. `report.json` gives you totals per scope. Before ingesting, check the assignment tag by tag against the actual content — the totals will look correct even when a tag is in the wrong place. ```python theme={null} import json, glob, collections tags = collections.defaultdict(list) for f in glob.glob("./synap_import/batch_*.json"): for d in json.load(open(f))["documents"]: tags[d["metadata"]["container_tag"]].append(d) for tag in sorted(tags): docs = tags[tag] m = docs[0] print(f"\n{tag}") print(f" scope: {m['metadata']['synap_scope'].upper()} " f"user_id={m['user_id']} customer_id={m['customer_id']} " f"documents={len(docs)}") for d in docs[:2]: print(f" - {(d['metadata']['title'] or '(untitled)')[:70]}") print(f" {d['document'][:90].replace(chr(10), ' ')}...") ``` Read the samples, not just the scope labels. Two things to look for: * A tag at **client scope** whose samples are somebody's personal conversation. That content is about to become readable by every user on your account. * A tag at **user scope** whose samples read like policy, product, or reference documentation. That content will be copied into one person's memory instead of shared, and no one else will be able to retrieve it. Fix `scope_map.json` and re-run `convert` until every tag reads correctly. Nothing has been sent yet, so this loop is free. `report.json` includes a `dct_sources` breakdown: * **`session_header`** — the original conversation date was recovered from the content. This is what you want. * **`created_at`** — no original date was available, so the document's upload date was used instead. Supermemory's `createdAt` records when a document was *uploaded to Supermemory*, not when the conversation happened. If you imported history into Supermemory, those two dates can be years apart. Memories dated from the upload date still work for recall, but questions like "what did I decide last spring?" answer against the wrong timeline. Migrate a single tag first and confirm it behaves before committing the rest. Copy your scope map, keep one representative tag, and convert that alone into its own directory: ```python theme={null} import json full = json.load(open("scope_map.json")) tag = "org:acme:user:john" # pick one real tag json.dump( {"isolation": full["isolation"], "tags": {tag: full["tags"][tag]}}, open("pilot_map.json", "w"), indent=2, ) ``` ```bash theme={null} python3 supermemory_to_synap.py convert supermemory-export.json \ -m pilot_map.json -o ./pilot python3 supermemory_to_synap.py ingest ./pilot python3 supermemory_to_synap.py verify ./pilot ``` Every other tag will be reported as skipped during the pilot conversion. That is expected — they are not in `pilot_map.json`. Now run steps 9 and 10 against this one tag. Only continue once its isolation and retrieval both check out. Dry-run first to see what would be sent: ```bash theme={null} export SYNAP_API_KEY=your_key_here python3 supermemory_to_synap.py ingest ./synap_import --dry-run ``` Then run it for real: ```bash theme={null} python3 supermemory_to_synap.py ingest ./synap_import ``` The script records progress as it goes, so if it is interrupted you can re-run the same command and it resumes rather than sending anything twice. Each document is submitted with its original Supermemory ID, so re-running a batch does not create duplicates. Your whole batch is checked against your quota before any of it is accepted. If you hit the limit, the script waits and retries automatically — you do not need to split the files yourself. Memories are built in the background, so ingestion finishing is not the same as memories being ready. ```bash theme={null} python3 supermemory_to_synap.py verify ./synap_import ``` This waits for every submitted document and reports how many memories were created. Anything that did not complete cleanly is written to `incomplete.json`. A `partial_success` result means the document was processed but some of its memories were not stored; re-ingesting that document is safe. This is the check that catches a wrong scope map. Do it on the pilot, and again after the full migration. Fetch context as one user and confirm nothing belonging to another user comes back: ```python Python theme={null} from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK() await sdk.initialize() ctx = await sdk.user.context.fetch( user_id="john", customer_id="acme", search_query=[""], ) ``` ```javascript JavaScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient(); await sdk.initialize(); const ctx = await sdk.user.context.fetch({ user_id: 'john', customer_id: 'acme', search_query: [''], }); ``` ```typescript TypeScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient(); await sdk.initialize(); const ctx = await sdk.user.context.fetch({ user_id: 'john', customer_id: 'acme', search_query: [''], }); ``` Choose a search term you know belongs to a *different* user's history. Nothing from that user should appear. Then check the reverse for anything you placed at client scope: fetch as two different users and confirm the shared material is retrievable by both. If it reaches only one, that tag is at user scope and needs correcting. Confirm the facts you expect actually come back: ```python Python theme={null} ctx = await sdk.user.context.fetch( user_id="john", customer_id="acme", search_query=["dietary preferences"], ) ``` ```javascript JavaScript theme={null} const ctx = await sdk.user.context.fetch({ user_id: 'john', customer_id: 'acme', search_query: ['dietary preferences'], }); ``` ```typescript TypeScript theme={null} const ctx = await sdk.user.context.fetch({ user_id: 'john', customer_id: 'acme', search_query: ['dietary preferences'], }); ``` `baseline.json` is useful here: it holds Supermemory's own extracted memories for each document, so you can sample a few and check they are retrievable from Synap too. Do not compare counts. One document does not become one memory, and the two systems extract differently by design — judge the migration by what you can retrieve. ## If the scope assignment was wrong If you discover a misplaced tag after ingesting, correct it **one tag at a time**. Leave every other tag alone. Do not delete all of `ingestion_ids.json` and re-ingest the whole export. Ingestion recognises content it has already seen at the same scope for several days, so a blanket re-ingest returns the earlier result instead of rebuilding. The tags you deleted but did not re-scope would stay deleted. Only remove the memories belonging to the tag you are actually fixing. **Step 1 — Delete only the affected tag's memories.** `ingest` writes `ingestion_ids.json` in the output directory, and each ingestion's status names the document it came from, which the batch files tie back to a container tag: ```python theme={null} import json, glob from uuid import UUID from maximem_synap import MaximemSynapSDK BAD_TAG = "acme_handbook" # the tag you are correcting # document_id -> container_tag, from the converted batches doc_tag = { d["document_id"]: d["metadata"]["container_tag"] for f in glob.glob("./synap_import/batch_*.json") for d in json.load(open(f))["documents"] } sdk = MaximemSynapSDK() await sdk.initialize() removed = 0 for ingestion_id in json.load(open("./synap_import/ingestion_ids.json")): status = await sdk.memories.status(UUID(ingestion_id)) if doc_tag.get(status.document_id) != BAD_TAG: continue # leave correctly-scoped tags untouched for memory_id in status.memory_ids: await sdk.memories.delete(UUID(memory_id)) removed += 1 print(f"removed {removed} memories for {BAD_TAG}") ``` **Step 2 — Re-convert that tag alone.** Fix its entry in `scope_map.json`, then build a single-tag map and convert it into its own directory, exactly as in the pilot step: ```python theme={null} import json full = json.load(open("scope_map.json")) json.dump( {"isolation": full["isolation"], "tags": {BAD_TAG: full["tags"][BAD_TAG]}}, open("refix_map.json", "w"), indent=2, ) ``` ```bash theme={null} python3 supermemory_to_synap.py convert supermemory-export.json \ -m refix_map.json -o ./refix python3 supermemory_to_synap.py ingest ./refix python3 supermemory_to_synap.py verify ./refix ``` Because the tag's scope has changed, this content is treated as new and is processed rather than matched against the earlier run. Re-run the isolation check from step 9 before moving on. Keep every output directory until the migration is fully verified. `ingestion_ids.json` is the only record of which memories the migration created — without it, telling them apart from memories your live application has written since is difficult. ## Why the script ingests documents, not memories A Supermemory export contains both your original documents and the memories Supermemory extracted from them. The script deliberately ingests **the documents**. Supermemory's memory entries are short summaries of your content. Importing them means Synap extracts from summaries rather than from what your users actually said — you inherit whatever the original extraction got wrong, and lose the detail it dropped. Because every memory in the export points back to the document it came from, you can re-extract from the original source instead, which is almost always better. There is a second, more important reason to re-extract. Every Synap instance runs its own **Memory Architecture Configuration ([MACA](/concepts/memory-architecture))** — a per-instance memory policy generated from the use-case file you provide when you create the instance. It governs what gets extracted and how, tuned to your agent's domain and audience. Re-extracting your imported documents through that configuration means your migrated history is processed by the *same rules as your live traffic*. From the very same source conversations, a support agent's instance surfaces issues and resolutions, while a companion agent's instance surfaces preferences and emotional context. Importing Supermemory's pre-extracted memories would bypass this entirely and leave your historical data shaped by generic rules that do not match your agent — so re-ingestion is not overhead, it is how your old data starts behaving as if your agent created it all along. There is one case where re-extraction is not possible: documents brought in through a Supermemory connector sometimes keep no local copy of their text. Those appear in `report.json` as `empty content in export`. Re-sync the connector on the Supermemory side and export again, or accept the loss. The extracted memories are still useful — that is what `baseline.json` is for. Use them to check your coverage after migrating, not as the thing you migrate. Supermemory's memory entries are typically a single short sentence. Content that brief carries too little signal to memorise on its own and is discarded on arrival, so importing those strings directly would quietly lose most of them. The script checks length locally and reports anything at risk rather than letting it disappear. ## What you gain * **Typed extractions** — facts, preferences, episodes, emotions, and temporal events as separate lists, rather than one undifferentiated pool of strings. * **Three scopes, not one flat namespace** — client, customer, and user, with roll-up between them, so shared knowledge is stored once instead of copied into every tag. * **Entity resolution across conversations** — the same person or product recognised across sessions. * **Context compaction** — long histories stay usable without you managing the window. ## What you'll need to adapt Some Supermemory structure is not present in a dashboard export, and no migration can recover it: * **Version history and superseded facts.** The export contains only current memories, without their revision chains. * **Relationships between memories.** Supermemory's `updates` / `extends` / `derives` links are not included. * **Inferred-fact flags.** There is no way to tell which memories Supermemory derived rather than observed. * **Embeddings.** Vectors are never exported by either system; Synap generates its own during ingestion. None of this is a real loss, because re-extracting from your original documents rebuilds the equivalent structure natively — Synap tracks its own memory lineage and relationships as it ingests. ## Troubleshooting Your scope map still has unfilled entries. On a B2B Instance a tag with a `user_id` but no `customer_id` is rejected, which is deliberate — it prevents documents landing in an unintended scope. Set `customer_id` for those tags, or switch `isolation` to `b2c` if that matches your Instance. `map` only lists tags that appear in the export. If a tag exists in Supermemory but has no documents or memories in the file, it will not appear — and nothing needs migrating for it. These came from a Supermemory connector that kept no local copy of the text. Re-sync the connector in Supermemory and export again. The document announces a session date the script could not read. This is treated as an error rather than silently falling back to the upload date, because a wrong date is worse than a skipped document. Report the format and we will add it. Expected. One document does not become one memory — long conversations are split, and content with nothing worth remembering produces none. Judge the migration by what you can retrieve, not by counting rows. Re-run the same `ingest` command. Completed batches are recorded and skipped, so it resumes rather than re-sending. ## After you cut over Work through the shared [migration method](/migrations/how-it-works), which covers the parts common to every source: configuring your instance so extraction quality is good from day one, deciding your `conversation_id` strategy, swapping your retrieval call sites, and adding graceful degradation. Once you are cut over, retire the old service. Do not dual-write — diverging memory state is a harder problem than a clean cutover. # Migrate from Zep to Synap Source: https://docs.maximem.ai/migrations/from-zep Looking for a Zep alternative? Map Zep's Sessions, Users, and automatic facts onto Synap: concept mapping, SDK call equivalents, and a backfill snippet for a clean cutover. You're using Zep and want to evaluate or move to Synap. This page covers how Zep stores memory, how its concepts map onto Synap, and how to backfill your existing sessions. This page is the Zep-specific mapping. For the method every migration shares — scope mapping, configuring your instance, pilot, verify, cut over — see [How migration works](/migrations/how-it-works). ## How Zep stores memory Zep has Sessions (≈ conversations), Users, and an automatic Facts extraction over sessions. Multi-user is `user_id`; multi-tenant is loosely "Project." Zep API surfaces vary by version: Zep Cloud's v2 renamed Sessions → Threads and restructured the Project surface. Check your installed `zep_python` (or `zep-cloud`) version against the names used below. ## The mapping | Zep concept | Synap concept | | ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- | | `Project` | Either one Instance per project, or one Instance + `customer_id` per project | | `User` (`zep.user.add`) | Implicit: Synap creates user records on first ingestion by `user_id` | | `Session` (`zep.memory.add_session`) | `conversation_id` (must be a UUID, use `str(uuid.uuid4())` per session) | | `zep.memory.add` | `sdk.conversation.record_message` (incremental) + periodic `sdk.conversation.context.compact` | | `zep.memory.get` | `sdk.conversation.context.fetch` or `get_context_for_prompt` | | `zep.memory.search_sessions` | `sdk.user.context.fetch(user_id=..., search_query=[...])` | | Automatic facts | Native: `ContextResponse.facts` is the equivalent of Zep's automatic facts, with the addition of preferences/episodes/emotions/temporal types | ## Backfill from Zep ```python theme={null} from zep_python.client import AsyncZep from maximem_synap import MaximemSynapSDK import uuid zep = AsyncZep(api_key="...") new = MaximemSynapSDK() await new.initialize() async def migrate_session(zep_user_id: str, zep_session_id: str, customer_id: str = "default"): messages = await zep.memory.get_session_messages(session_id=zep_session_id) synap_conv = str(uuid.uuid4()) # Record each message individually so Synap can compact later for m in messages.messages: # Zep `role_type` can be "user" / "assistant" / "system" / "function" / "tool". # Synap only accepts "user" or "assistant"; filter or coerce other roles. if m.role_type not in ("user", "assistant"): continue # or map system/function/tool to "assistant" if you want to preserve them await new.conversation.record_message( conversation_id=synap_conv, role=m.role_type, content=m.content, user_id=zep_user_id, customer_id=customer_id, metadata={"zep_session_id": zep_session_id, "zep_uuid": m.uuid}, ) # Trigger one compaction so the conversation arrives compacted await new.conversation.context.compact( conversation_id=synap_conv, strategy="adaptive", ) ``` ## Differences worth noting * Zep stores the literal message log forever; Synap compacts it. If you rely on retrieving raw messages by ID, plan to keep your Zep log around for a transition period. * Zep has built-in evaluation via fact graphs; Synap does this differently: the entity graph is the equivalent and is queried by both `fast` and `accurate` retrieval. `accurate` additionally adds LLM subquery decomposition + reranking on top of the same vector + graph search. ## Next Follow the shared [migration method](/migrations/how-it-works) to pilot one user, verify scope isolation, swap your retrieval call sites, and cut over. # How migration works Source: https://docs.maximem.ai/migrations/how-it-works The method every Synap migration follows: map your identity model onto scopes, configure the instance for your agent, backfill, pilot, verify, and cut over. Each platform guide builds on this page. Every migration onto Synap follows the same shape, regardless of where your data comes from. The per-platform guides ([Mem0](/migrations/from-mem0), [Zep](/migrations/from-zep), [Letta](/migrations/from-letta), [Supermemory](/migrations/from-supermemory)) cover how to get data *out* of each source and how its concepts map onto Synap. This page is the method they all share — read it once. For the SDK methods used below — `batch_create`, idempotency via `document_id`, per-document status — see the [Migration reference](/sdk-reference/migration). ## 1. Map your identity model onto scopes Synap organises memory into three scopes: **client**, **customer**, and **user**. You never name a scope directly — Synap derives it from which identifiers you pass when you ingest: | `user_id` | `customer_id` | Resulting scope | Use for | | --------- | ------------- | -------------------------- | ------------------------------------------------ | | set | set | **user** | One person's own history | | not set | set | **customer** | Knowledge shared across everyone at one customer | | not set | not set | **client** | Knowledge shared across your entire account | | set | not set | Rejected on a B2B Instance | — | The first task in any migration is deciding, for each slice of your source data, which identifiers it should carry. A source that only has a `user_id` concept maps cleanly to user scope; shared or company-wide material belongs at customer or client scope. See [Memory scopes](/concepts/memory-scopes) for the full model. Getting this wrong is the highest-impact mistake in a migration. Content placed at client scope is readable across your whole account; content wrongly placed at user scope is invisible to everyone but one person. Step 4 has you verify the assignment before it becomes permanent. ## 2. Configure the Instance for your agent — before you import Extraction quality depends on your Instance's **Memory Architecture Configuration (MACA)** — a per-instance memory policy generated from the use-case file you supply at instance creation. It governs what gets extracted and how, tuned to your agent's domain and audience. See [Memory Architecture](/concepts/memory-architecture). Do this **before** the bulk import. If you import against an instance with no use-case file, your historical data is extracted with generic defaults, and you would re-import later to benefit from a tuned configuration. The same MACA governs your migrated history and your live traffic, so configuring first is what makes old data behave as if your agent created it. ## 3. Choose one Instance or many One Instance per environment (production, staging) is the usual answer. Use a separate Instance per customer only when data residency or the memory architecture genuinely differs between them. ## 4. Decide your `conversation_id` strategy If your source stores conversations or sessions, map each to a stable `conversation_id` (a UUID per thread). This lets Synap compact long histories and group turns correctly. Sources that store only flat facts don't need this. ## 5. Backfill Ingest through `batch_create` at `mode="long-range"`: * Pass `document_created_at` from the source's original timestamp, not today's date — otherwise every memory is dated to import day and time-relative recall answers against the wrong timeline. * Set `document_id` to the source record's ID. Ingestion is idempotent on it, so a re-run never duplicates. * Prefer ingesting **original content** (conversations, documents) over a source's pre-extracted memory strings, so Synap extracts natively instead of inheriting the upstream extraction. Each platform guide says which is available. ## 6. Pilot one slice Before migrating everything, run one representative user or tenant end to end — backfill, then the verification in step 7. Only continue once that slice checks out. A mistake caught on one user is cheap; the same mistake across your whole dataset is not. ## 7. Verify Two checks, both on the pilot and again after the full run: * **Scope isolation.** Fetch context as one user with a search term you know belongs to a *different* user, and confirm nothing from that user appears. For client-scoped material, fetch as two users and confirm both can retrieve it. * **Retrieval coverage.** Fetch for facts you know exist and confirm they come back. Do not compare raw memory counts — Synap and your source extract differently by design, so judge by what you can retrieve. ```python Python theme={null} from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK() await sdk.initialize() ctx = await sdk.user.context.fetch( user_id="user-123", customer_id="acme", search_query=["dietary preferences"], ) ``` ```javascript JavaScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient(); await sdk.initialize(); const ctx = await sdk.user.context.fetch({ user_id: 'user-123', customer_id: 'acme', search_query: ['dietary preferences'], }); ``` ```typescript TypeScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient(); await sdk.initialize(); const ctx = await sdk.user.context.fetch({ user_id: 'user-123', customer_id: 'acme', search_query: ['dietary preferences'], }); ``` ## 8. If you got a scope wrong, fix it one slice at a time If you discover misplaced data after ingesting, correct only that slice — delete the memories created for it, then re-ingest it at the right scope. Do not delete everything and re-run the whole backfill. Ingestion recognises content it has already seen at the same scope for several days and returns the earlier result instead of rebuilding, so a blanket re-run leaves the slices you deleted but did not re-scope permanently gone. Re-ingesting works precisely *because* the scope changed. Keep the IDs of what you ingested until the migration is verified. ## 9. Swap your retrieval call sites Replace your old retrieval calls with `sdk.user.context.fetch(...)`. Synap returns a `ContextResponse` with typed lists (facts, preferences, episodes, emotions, temporal events) rather than one flat list — iterate over the types your prompt needs. See [Response shapes](/sdk/response-shapes). ## 10. Cut over Add [graceful degradation](/patterns/graceful-degradation) so memory never blocks your agent's hot path, then retire the old service. Do not dual-write — diverging memory state across two systems is a harder problem than a clean cutover. # Migrate to Synap Source: https://docs.maximem.ai/migrations/overview Moving from another memory layer? Start here. Pick your source platform, follow its guide, and use the shared migration method to map, backfill, verify, and cut over cleanly. You're already using another memory layer and want to evaluate or move to Synap. This section has a dedicated guide for each source platform, plus the [shared method](/migrations/how-it-works) they all build on. New to Synap's model first? Read [What is Synap?](/getting-started/overview) and [Memory scopes](/concepts/memory-scopes) before migrating — the migration is mostly a mapping exercise, and it goes faster once the scope model is clear. ## Start here The method every migration follows: map scopes, configure the instance, backfill, pilot, verify, cut over. Read this once, then pick your platform. The client / customer / user model your source data has to map onto. ## Pick your source platform User-scoped memory bag of untyped strings. Straightforward user-scope mapping. Sessions, Users, and automatic facts. Maps onto conversations plus typed context. Agent-coupled core / archival memory. A re-architecture, not a drop-in. Documents and memories under flat container tags. Ships with a ready-to-run script. ## More platforms We're building careful, first-class guides for the memory layers customers ask about most. The following are on the roadmap: | Platform | Status | | ---------------------------------- | --------- | | Mem0 | Available | | Zep | Available | | Letta (MemGPT) | Available | | Supermemory | Available | | Cognee | Planned | | LangMem | Planned | | Vector store (Pinecone / Weaviate) | Planned | If your source isn't listed, the [shared method](/migrations/how-it-works) still applies — the only platform-specific part is the export and the concept mapping. Email **[support@maximem.ai](mailto:support@maximem.ai)** with what you're using and we'll prioritise a guide. ## What every migration gains * **Typed extractions** — facts, preferences, episodes, emotions, and temporal events as separate lists, not one flat pool of strings. * **Three scopes** — client, customer, and user, with roll-up, so shared knowledge is stored once instead of copied per user. * **Entity resolution** — the same person or product recognised across conversations. * **Context compaction** — long histories stay usable without you managing the window. # Graceful Degradation Source: https://docs.maximem.ai/patterns/graceful-degradation What to do when Synap is unreachable. Cache, fallback prompts, queued retries. Synap should make your agent better when it's available, and not break your agent when it isn't. Treat retrieval and ingestion as **best-effort** in the hot path: never let them stop the LLM from generating a response. ## The shape of "good enough" degradation ```python theme={null} import asyncio import logging from maximem_synap import MaximemSynapSDK, SynapError, SynapTransientError sdk = MaximemSynapSDK(api_key=...) log = logging.getLogger(__name__) async def safe_fetch_context(conversation_id: str, query: str): """Always returns something, even if it's empty.""" try: return await asyncio.wait_for( sdk.conversation.context.fetch( conversation_id=conversation_id, search_query=[query], mode="fast", max_results=8, ), timeout=YOUR_CONVERSATIONAL_BUDGET_SECONDS, # illustrative: tune to your own conversational budget ) except asyncio.TimeoutError: log.warning("synap_context_timeout conv=%s", conversation_id) return None except SynapTransientError as e: log.warning("synap_transient err=%s correlation_id=%s", e, e.correlation_id) return None except SynapError as e: log.error("synap_unexpected err=%s correlation_id=%s", e, e.correlation_id) return None async def handle_turn(user_id: str, customer_id: str, conversation_id: str, msg: str) -> str: ctx = await safe_fetch_context(conversation_id, msg) if ctx is None: # Degraded mode: call the LLM without memory rather than 500ing memory_block = "" log.info("turn_degraded user=%s", user_id) else: memory_block = "\n".join(f"- {f.content}" for f in ctx.facts[:5]) reply = await call_llm(memory_block, msg) # Ingest in the background. If it fails, queue for retry, don't await. asyncio.create_task(safe_ingest(user_id, customer_id, conversation_id, msg, reply)) return reply async def safe_ingest(user_id, customer_id, conversation_id, msg, reply, _retries=0): try: await sdk.memories.create( document=f"User: {msg}\nAssistant: {reply}", document_type="ai-chat-conversation", user_id=user_id, customer_id=customer_id, metadata={"conversation_id": conversation_id}, ) except SynapTransientError as e: if _retries < 3: await asyncio.sleep(2 ** _retries) return await safe_ingest(user_id, customer_id, conversation_id, msg, reply, _retries + 1) log.error("synap_ingest_dropped after retries user=%s msg_excerpt=%r correlation_id=%s", user_id, msg[:80], e.correlation_id) # Optionally: enqueue for an out-of-band replayer await enqueue_for_replay(user_id, customer_id, conversation_id, msg, reply) except SynapError as e: log.error("synap_ingest_permanent err=%s correlation_id=%s", e, e.correlation_id) ``` ## What to watch in production Three metrics that should be on your dashboard from day one: | Metric | What it tells you | Page if | | --------------------------------- | --------------------------------------------- | ------------------------------------------- | | `synap_context_timeout_rate` | Are users seeing degraded responses? | `> 1%` over 5 min | | `synap_ingest_dropped_rate` | Are you losing memory? | `> 0.1%` over 1 hour | | `synap_correlation_ids_in_errors` | Sample of `correlation_id` values for support | Always log; sample 5% to your error tracker | Every `SynapError` exposes `e.correlation_id`: log it. When you need to ask support, they need that ID. ## Don't do these things * **Don't fail the request on a Synap timeout.** The LLM can answer without memory. The user gets a slightly worse response. Failing the request gets you an outage. * **Don't retry permanent errors.** `InvalidInputError`, `ContextNotFoundError`, `AuthenticationError` won't get better with retries. Fix the input or the credentials. * **Don't block on ingestion in the hot path.** Always background it. The user shouldn't wait for memory persistence to see the next assistant message. * **Don't catch and swallow without logging.** Every catch should at minimum log the `correlation_id`. Silent swallows make production debugging hopeless. ## Where the SDK already retries for you `SynapTransientError` subclasses (`NetworkTimeoutError`, `RateLimitError`, `ServiceUnavailableError`, `AgentUnavailableError`) are retried automatically inside the SDK using the configured `RetryPolicy`. By the time one of these reaches your `except` block, the SDK already tried 2-3 times. So your wrapper retries are belt-and-suspenders for genuinely down-for-a-while scenarios. ## Going further * **Patterns:** [Replay Conversation History](/patterns/replay-history) * **Cookbook:** [Voice Concierge](/cookbook/voice-concierge) · [Uber: Customer Support](/cookbook/consumer-uber) * **Guides:** [Production Checklist](/guides/production-checklist) # Multi-Tenant SaaS Source: https://docs.maximem.ai/patterns/multi-tenant-saas Scope memories per customer organization. One Synap Instance, many isolated customer memories. You're building a B2B SaaS app. Each of your customers has their own users. You want: * Memories from one customer to never leak to another. * Memories at the customer level (shared org policies, product config) to be visible to every user in that customer. * Memories at the user level (personal preferences) to stay private to that user. Synap's scope chain (USER → CUSTOMER → CLIENT → WORLD) maps to this cleanly. Use one Synap Instance for all your customers; rely on `customer_id` + `user_id` to enforce isolation. ```python theme={null} import uuid from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK(api_key=...) await sdk.initialize() # ---- Per-customer onboarding: load their playbook once ---- async def onboard_customer(customer_id: str, playbook_text: str): await sdk.memories.create( document=playbook_text, document_type="document", customer_id=customer_id, # CUSTOMER scope: visible to all users in this customer # no user_id → not user-scoped metadata={"source": "onboarding_playbook"}, ) # ---- Per-turn agent loop, called for every user message ---- async def handle_turn(customer_id: str, user_id: str, conversation_id: str, user_message: str) -> str: # Retrieval automatically merges USER + CUSTOMER + CLIENT scopes ctx = await sdk.conversation.context.fetch( conversation_id=conversation_id, search_query=[user_message], max_results=10, ) # Build the system prompt with the merged scoped facts facts_block = "\n".join(f"- {f.content}" for f in ctx.facts) system_prompt = ( "Known facts (merged across user, customer, and client scopes):\n" + (facts_block or "(none)") + "\n\n" "Respond using the customer org context as authoritative." ) reply = await call_your_llm(system_prompt, user_message) # Ingest at USER scope so personal context stays personal await sdk.memories.create( document=f"User: {user_message}\nAssistant: {reply}", document_type="ai-chat-conversation", user_id=user_id, customer_id=customer_id, # both → user-scope, but linked to customer metadata={"conversation_id": conversation_id}, ) return reply ``` **Key isolation guarantees:** * `sdk.user.context.fetch(user_id="alice", customer_id="acme")` returns only memories tagged with that exact `user_id` + `customer_id` pair, plus broader CUSTOMER and CLIENT scoped memories visible to `acme`. It does NOT return user "alice" from a different customer. * `sdk.customer.context.fetch(customer_id="acme")` returns customer-shared memories without leaking any user-scoped data. * A bug where you forget `customer_id` on `memories.create()` won't quietly leak. On a B2B Instance it raises `InvalidInputError`. **Picking IDs** * Use stable, deterministic strings: your internal customer UUID, your internal user UUID. * Don't put PII in the IDs (no emails, names). Synap treats them as opaque identifiers but they appear in audit logs and telemetry. **One Instance vs many Instances** Use **one Instance for all customers** when: * Customers share the same memory architecture (same Use-Case Markdown). * You don't need separate residency / encryption keys per customer. Use **one Instance per customer** when: * A customer has contractual data residency or KMS-key isolation requirements. * A customer needs a meaningfully different MACA (e.g., different memory types prioritized). ## Going further * **Guides:** [Multi-User Memory Scoping](/guides/multi-user-scoping) * **Cookbook:** [AI SDR](/cookbook/b2b-sdr) · [Salesforce: Enterprise Sales Assistant](/cookbook/b2b-salesforce) * **Patterns:** [Slack Bot](/patterns/slack-bot) # Patterns Source: https://docs.maximem.ai/patterns/overview Copy-paste recipes for common Synap integration patterns. Each one solves a specific cross-cutting problem in 30-80 lines. These are short, opinionated, copy-paste-ready patterns: the cross-cutting techniques you reach for inside any agent. Use them as starting points, not as architectural decrees. Looking for a full agent example instead of a single pattern? See the [Cookbook](/cookbook/overview): end-to-end reference agents built on top of these patterns. Scope memories per customer organization. Reuse one Instance across all your B2B customers. Add per-channel and per-user memory to a Slack bot using Bolt + Synap. Memory-augmented voice agent on LiveKit. Recall caller history mid-call. Pure semantic retrieval over a single user's past conversations. Synap as your RAG. One shared Listen stream for a multi-tenant process, and the failure that stays silent. What to do when Synap is unreachable. Cache, fallback prompts, queue retries. Bulk-ingest a corpus of historical conversations on Instance creation. Want a pattern that isn't here? Email **[support@maximem.ai](mailto:support@maximem.ai)** with the use case and we'll add it. Patterns that get repeatedly requested become part of the docs. # RAG over User History Source: https://docs.maximem.ai/patterns/rag-user-history Pure semantic retrieval over a single user's past conversations. Synap as your RAG. You don't always need an entity graph and typed extractions. Sometimes you just want "given a query, return the most relevant chunks of this user's past conversations." Synap does this too: its `ContextResponse` is queryable like a vector store, with the bonus that retrieval is automatically scoped to the user. ```python theme={null} from openai import AsyncOpenAI from maximem_synap import MaximemSynapSDK, ContextType sdk = MaximemSynapSDK(api_key=...) openai = AsyncOpenAI() await sdk.initialize() async def search_user_history(user_id: str, customer_id: str, query: str, top_k: int = 8): """Pure RAG: return raw memory chunks ranked by relevance to `query`.""" ctx = await sdk.user.context.fetch( user_id=user_id, customer_id=customer_id, search_query=[query], max_results=top_k, types=[ContextType.FACTS, ContextType.EPISODES], mode="accurate", # graph-aware ranking ) # Sort facts and episodes by relevance using their respective scores hits = ( [(f.confidence, "fact", f.content) for f in ctx.facts] + [(e.significance, "episode", e.summary) for e in ctx.episodes] ) hits.sort(key=lambda h: h[0], reverse=True) return hits[:top_k] async def rag_chat(user_id: str, customer_id: str, user_message: str) -> str: hits = await search_user_history(user_id, customer_id, user_message) citations = "\n".join(f"[{i+1}] ({kind}) {text}" for i, (score, kind, text) in enumerate(hits)) system_prompt = ( "Answer the user's question using ONLY the citations below. " "Cite by number. If the answer isn't in the citations, say you don't know.\n\n" f"Citations:\n{citations}" ) completion = await openai.chat.completions.create( model="gpt-4o", messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_message}, ], ) return completion.choices[0].message.content ``` **Why this works without your own vector DB** * Synap embeds every ingested document at ingest time and stores the vectors in its vector engine. * `mode="accurate"` combines vector similarity with graph traversal, so a query about "the project Bob is leading" pulls memories about Bob even if Bob isn't named in the query verbatim. * All retrievals are scoped: you cannot accidentally fetch another user's data. **When this isn't enough** If you have non-conversational sources (PDFs, knowledge base articles, web pages) that you want to retrieve over, you have two options: 1. **Ingest them into Synap with `document_type="document"`** at the CUSTOMER or CLIENT scope. They become part of the same retrieval surface as user history. 2. **Keep a separate vector DB** for the document corpus and merge results at the application layer. Use Synap for memory only. Option 1 is simpler and gets you scope-aware retrieval for free. Option 2 makes sense if you already have a doc corpus indexed and don't want to re-ingest. **Comparing to a raw vector DB** | | Pure pgvector / Pinecone | Synap | | ------------------------------------ | ----------------------------------------- | --------------------------- | | Setup | DB + embed model + retrieval pipeline | `pip install maximem-synap` | | Per-user isolation | You build it | Native (`user_id` scope) | | Multi-source merge (chat + docs) | Manual | Native | | Re-ranking by recency / significance | Manual | Built in | | Cost | Pay for compute + storage + embedding API | Per-call pricing | If "I just want a vector DB" is genuinely all you want, pgvector is cheaper. If you're going to end up building scope isolation, multi-source merge, and reranking on top of pgvector, you're rebuilding Synap. ## Going further * **Guides:** [Multi-User Memory Scoping](/guides/multi-user-scoping) * **Cookbook:** [AI Companion](/cookbook/personal-ai-companion) * **Patterns:** [Multi-Tenant SaaS](/patterns/multi-tenant-saas) # Real-Time Anticipation in a Server Source: https://docs.maximem.ai/patterns/real-time-anticipation-server Run one Listen stream for a whole multi-tenant process, scope every call per request, and alert on the failure that stays silent. [Real-time anticipation](/concepts/real-time-anticipation) is straightforward in a script: open a stream, send events, close it. In a long-lived server it needs three decisions that are easy to get wrong, and one of the wrong answers only shows up under production concurrency. ## One stream for the process The instinct is to open a stream per user session. Don't. Synap enforces per-instance and per-client stream quotas: currently **100 concurrent streams per instance** and **200 per client**. A stream-per-session design saturates that quota at real concurrency and surplus connections enter a `RESOURCE_EXHAUSTED` reconnect loop, which degrades the sessions that *did* connect. Instead, open **one stream at process startup** and distinguish users with the `user_id` / `customer_id` you already pass on every call. ```python theme={null} # app_state.py: module scope, one per process from maximem_synap import MaximemSynapSDK, SDKConfig sdk = MaximemSynapSDK(api_key=os.environ["SYNAP_API_KEY"]) _stream_healthy = False async def startup() -> None: global _stream_healthy await sdk.initialize() def on_reconnect(attempt: int) -> None: global _stream_healthy _stream_healthy = True log.info("synap_stream_reconnected attempt=%d", attempt) def on_disconnect(reason: str) -> None: global _stream_healthy _stream_healthy = False log.warning("synap_stream_disconnected reason=%s", reason) try: await sdk.instance.listen( on_reconnect=on_reconnect, on_disconnect=on_disconnect, ) _stream_healthy = True except Exception as exc: # Anticipation is an optimization. A failed stream must not # stop the server from booting; fetch() still works over REST. log.error("synap_stream_unavailable error=%s", exc) async def shutdown() -> None: await sdk.instance.stop_listening() ``` Scope lives on the request, not the stream. One process-wide stream serves every tenant because `send_message()` and `fetch()` each carry their own `user_id` / `customer_id`. ## Scope every call, per request With a shared stream, correctness depends entirely on passing the right identifiers on each call. Nothing about the stream itself is tenant-specific. ```python Python theme={null} async def handle_turn(user_id: str, customer_id: str, conversation_id: str, text: str) -> str: await sdk.instance.send_message( content=text, role="user", event_type="user_message", conversation_id=conversation_id, user_id=user_id, customer_id=customer_id, ) context = await sdk.user.context.fetch( user_id=user_id, customer_id=customer_id, conversation_id=conversation_id, search_query=[text], ) reply = await your_llm(context, text) await sdk.instance.send_message( content=reply, role="assistant", event_type="assistant_message", conversation_id=conversation_id, user_id=user_id, customer_id=customer_id, ) # No ingestion call: these turns become long-term memory when the # conversation compacts. See /setup/agent-integration. return reply ``` ```javascript JavaScript theme={null} async function handle_turn(user_id, customer_id, conversation_id, text) { await sdk.instance.send_message({ content: text, role: 'user', event_type: 'user_message', conversation_id: conversation_id, user_id: user_id, customer_id: customer_id, }); const context = await sdk.user.context.fetch({ user_id: user_id, customer_id: customer_id, conversation_id: conversation_id, search_query: [text], }); const reply = await your_llm(context, text); await sdk.instance.send_message({ content: reply, role: 'assistant', event_type: 'assistant_message', conversation_id: conversation_id, user_id: user_id, customer_id: customer_id, }); // No ingestion call: these turns become long-term memory when the // conversation compacts. See /setup/agent-integration. return reply; } ``` ```typescript TypeScript theme={null} async function handle_turn(user_id, customer_id, conversation_id, text) { await sdk.instance.send_message({ content: text, role: 'user', event_type: 'user_message', conversation_id: conversation_id, user_id: user_id, customer_id: customer_id, }); const context = await sdk.user.context.fetch({ user_id: user_id, customer_id: customer_id, conversation_id: conversation_id, search_query: [text], }); const reply = await your_llm(context, text); await sdk.instance.send_message({ content: reply, role: 'assistant', event_type: 'assistant_message', conversation_id: conversation_id, user_id: user_id, customer_id: customer_id, }); // No ingestion call: these turns become long-term memory when the // conversation compacts. See /setup/agent-integration. return reply; } ``` A `user_message` or `assistant_message` missing either `user_id` or `customer_id` is **dropped server-side with no client-visible error**. On a B2C instance send `user_id` only: `customer_id` is not accepted there, and an event without one is persisted normally. In a multi-tenant server, treat a missing `customer_id` as a bug in your request handler, not an optional field. ## Expect reconnects Streams have a maximum lifetime of **one hour**, after which the server closes them. This is normal operation, not an error: the SDK reconnects with exponential backoff (10 attempts, and the counter resets on every successful connect), and `on_reconnect` fires. A server that runs for a week will reconnect well over a hundred times. Log reconnects at `INFO`, not `WARNING`, or you will train yourself to ignore them. ## The failure that stays silent This is the one worth alerting on. If the stream dies permanently (quota exhaustion, a network partition longer than the backoff window, revoked credentials), **your application keeps working**. `fetch()` falls through to REST and returns correct results. Latency rises, and nothing else changes. No exception reaches your handler. That is the intended design, and it is why streaming is safe to adopt. It also means *you will not notice* unless you look: ```python theme={null} @app.get("/health/synap") async def synap_health() -> dict: return { "stream_connected": sdk.instance.is_listening, "degraded": not sdk.instance.is_listening, } ``` Alert on `is_listening` going false and staying false. Do not alert on individual disconnects; those are routine. The JavaScript/TypeScript SDK never raises from `listen()` at all: it logs a console warning and falls back to HTTP. There, checking `synap.isListening` is the *only* way to know the stream is up. ## Checklist * One stream per process, opened at startup, closed at shutdown * `listen()` failure logged, never fatal to boot * `user_id` **and** `customer_id` on every `send_message` and `fetch` * `assistant_message` emitted **after** the reply, so it warms the next turn * No per-turn `memories.create()` for streamed turns. Compaction promotes them, and doing both extracts twice * `is_listening` exported to your health endpoint and alerted on The two-channel model, event types, and what the platform does with each one. # Replay Conversation History Source: https://docs.maximem.ai/patterns/replay-history Bulk-ingest historical conversations on Instance creation. One-shot backfill. When you turn Synap on for an existing app, you usually have months of conversation logs sitting in a database. Pump them in via batch ingestion so the agent starts day one with the same context your users already remember about themselves. ```python theme={null} import asyncio from dataclasses import dataclass from datetime import datetime from typing import AsyncIterator from maximem_synap import CreateMemoryRequest, MaximemSynapSDK sdk = MaximemSynapSDK(api_key=...) await sdk.initialize() # `db` is your own async DB handle. Swap in your project's connection. db = ... @dataclass class HistoricalTurn: user_id: str customer_id: str conversation_id: str message_index: int # position of this turn within the conversation user_message: str assistant_message: str happened_at: datetime # original timestamp from your DB async def load_historical_turns() -> AsyncIterator[HistoricalTurn]: """Pull from your application database. Yield one turn at a time.""" async for row in db.stream("SELECT * FROM conversations ORDER BY created_at"): yield HistoricalTurn( user_id=row.user_id, customer_id=row.customer_id, conversation_id=str(row.conversation_id), # must be UUID message_index=row.message_index, user_message=row.user_text, assistant_message=row.assistant_text, happened_at=row.created_at, ) async def batch_replay(batch_size: int = 50): batch: list[CreateMemoryRequest] = [] total = 0 async for turn in load_historical_turns(): batch.append(CreateMemoryRequest( document=f"User: {turn.user_message}\nAssistant: {turn.assistant_message}", document_type="ai-chat-conversation", document_created_at=turn.happened_at, # preserve original time user_id=turn.user_id, customer_id=turn.customer_id, mode="long-range", # deep extraction for historical data metadata={ "conversation_id": turn.conversation_id, "source": "backfill", }, )) if len(batch) >= batch_size: await sdk.memories.batch_create(documents=batch, fail_fast=False) total += len(batch) print(f"backfilled {total} turns") batch.clear() if batch: await sdk.memories.batch_create(documents=batch, fail_fast=False) total += len(batch) print(f"done: {total} historical turns ingested") asyncio.run(batch_replay()) ``` **Why `mode="long-range"` for backfill** Historical conversations are the highest-value extraction target you'll ever have: they're the one-shot chance to build a rich entity graph and detailed preferences for every user. `long-range` runs the full extraction pipeline (entity resolution, preference detection, emotion analysis, relationship mapping). It's slower per document, but speed doesn't matter for an offline backfill. `fast` mode would skip most of this: fine for runtime chat ingestion, wrong for backfill. **Why `document_created_at`** Synap uses `document_created_at` as the ground-truth timestamp for temporal reasoning, recency ranking, and aging. Don't skip it: without it, every memory looks like it happened today, and recency-weighted retrieval breaks. Pass the original timestamp from your database. **Why batch and not one-by-one** `batch_create` accepts up to 100 documents per call, and the cloud queues them on a backfill-friendly path that doesn't slow real-time ingestion. One-by-one would 30x your wall-clock time and burn rate limits. **Idempotency** If you re-run the backfill (because you found a bug, or because you're bringing online a second region), pass a stable `document_id` derived from your DB row: ```python theme={null} document_id=f"backfill:{turn.conversation_id}:{turn.message_index}", ``` Synap deduplicates on `document_id`; duplicates surface as per-document failures in the `batch_create` response (`results[].status` / `results[].error_message`). Inspect the response and skip rows that already exist: ```python Python theme={null} resp = await sdk.memories.batch_create(documents=batch, fail_fast=False) for r in resp.results: if r.status == "failed": # e.g., duplicate document_id from a prior backfill run log.info("backfill_skip ingestion_id=%s err=%s", r.ingestion_id, r.error_message) ``` ```javascript JavaScript theme={null} const resp = await sdk.memories.batch_create({ documents: batch, fail_fast: false, }); for (const r of resp.results ?? []) { if (r.status == 'failed') { // e.g., duplicate document_id from a prior backfill run console.info('backfill_skip ingestion_id=%s err=%s', r.ingestion_id, r.error_message); } } ``` ```typescript TypeScript theme={null} const resp = await sdk.memories.batch_create({ documents: batch, fail_fast: false, }); for (const r of resp.results ?? []) { if (r.status == 'failed') { // e.g., duplicate document_id from a prior backfill run console.info('backfill_skip ingestion_id=%s err=%s', r.ingestion_id, r.error_message); } } ``` **How long it takes** Backfill throughput is gated by per-Instance limits: measure on a sample slice of your corpus before extrapolating; the numbers vary by document size and `mode`. Talk to support if you have multi-million-turn backfills and want a higher throughput allocation for the window. ## Going further * **Guides:** [Migrating from Mem0 / Zep / Letta](/migrations/overview) * **Cookbook:** [AI SDR](/cookbook/b2b-sdr) · [Salesforce: Enterprise Sales Assistant](/cookbook/b2b-salesforce) * **Patterns:** [Graceful Degradation](/patterns/graceful-degradation) # Slack Bot with Memory Source: https://docs.maximem.ai/patterns/slack-bot Add per-channel and per-user memory to a Slack bot using Slack Bolt + Synap. Maps Slack identifiers (`team_id`, `channel_id`, `user_id`) to Synap scopes so the bot remembers context per workspace, per channel, and per Slack user. ```python theme={null} # pip install slack-bolt openai maximem-synap import os import uuid import asyncio from slack_bolt.async_app import AsyncApp from slack_bolt.adapter.socket_mode.aiohttp import AsyncSocketModeHandler from openai import AsyncOpenAI from maximem_synap import MaximemSynapSDK app = AsyncApp(token=os.environ["SLACK_BOT_TOKEN"]) sdk = MaximemSynapSDK(api_key=...) openai = AsyncOpenAI() # One conversation_id per Slack channel: channel-level threading _channel_convs: dict[str, str] = {} def conv_for(channel_id: str) -> str: return _channel_convs.setdefault(channel_id, str(uuid.uuid4())) @app.event("app_mention") async def on_mention(event, say): text = event["text"] slack_user_id = event["user"] # e.g., "U01ABC" team_id = event["team"] # e.g., "T01XYZ" (Slack workspace) channel_id = event["channel"] # Map Slack identifiers to Synap scopes: # customer_id ← team_id (one Slack workspace = one customer) # user_id ← slack_user_id # conversation_id ← per-channel UUID conversation_id = conv_for(channel_id) # Register the incoming user turn on the conversation thread so # `conversation.context.fetch` has a real thread to scope against. await sdk.conversation.record_message( conversation_id=conversation_id, user_id=slack_user_id, customer_id=team_id, role="user", content=text, ) ctx = await sdk.conversation.context.fetch( conversation_id=conversation_id, search_query=[text], max_results=6, ) memory_block = "\n".join(f"- {f.content}" for f in ctx.facts[:5]) prefs_block = "\n".join(f"- {p.content}" for p in ctx.preferences[:3]) completion = await openai.chat.completions.create( model="gpt-4o", messages=[ {"role": "system", "content": ( "You are a helpful Slack bot.\n" f"Known facts:\n{memory_block or '(none)'}\n" f"User preferences:\n{prefs_block or '(none)'}" )}, {"role": "user", "content": text}, ], ) reply = completion.choices[0].message.content await say(text=reply, thread_ts=event.get("ts")) # Record the assistant turn on the same conversation thread. await sdk.conversation.record_message( conversation_id=conversation_id, user_id=slack_user_id, customer_id=team_id, role="assistant", content=reply, ) # Fire-and-forget ingestion so it doesn't add latency to the response asyncio.create_task(sdk.memories.create( document=f"<@{slack_user_id}>: {text}\nBot: {reply}", document_type="ai-chat-conversation", user_id=slack_user_id, customer_id=team_id, metadata={ "conversation_id": conversation_id, "channel": channel_id, "source": "slack", }, )) async def main(): await sdk.initialize() handler = AsyncSocketModeHandler(app, os.environ["SLACK_APP_TOKEN"]) await handler.start_async() asyncio.run(main()) ``` **Why these mappings?** * **`team_id` → `customer_id`**: each Slack workspace is one tenant. Two different workspaces never share memories. * **`slack_user_id` → `user_id`**: stable Slack identifier. Survives display-name changes. * **One `conversation_id` per channel** (kept in a process-local dict here; use Redis in production): the bot remembers cross-message context within a channel but doesn't bleed across channels. **Channel-level vs thread-level memory** If you want each Slack thread to be its own conversation, key `_channel_convs` on `event["thread_ts"] or event["ts"]` instead of `channel_id`. That gives more isolated context per thread but loses cross-thread memory in the same channel. ## Going further * **Patterns:** [Multi-Tenant SaaS](/patterns/multi-tenant-saas) * **Guides:** [Multi-User Memory Scoping](/guides/multi-user-scoping) * **Cookbook:** [Salesforce: Enterprise Sales Assistant](/cookbook/b2b-salesforce) · [Tier-1 → Tier-2 Escalation](/cookbook/support-tier-escalation) # Voice Agent on LiveKit Source: https://docs.maximem.ai/patterns/voice-agent-livekit Memory-augmented voice agent on LiveKit Agents. Recall caller history mid-call. This recipe assumes upcoming `SynapMemoryHook` / `SynapContextProvider` exports from `synap-livekit-agents`. Until those ship, use `preload_synap_context`: see the integration's README for the current API. LiveKit Agents handles the audio plumbing (STT → LLM → TTS). Synap handles memory. The `synap-livekit-agents` integration wires them together with two callback hooks. ```python theme={null} # pip install livekit-agents openai maximem-synap synap-livekit-agents from livekit.agents import ( AutoSubscribe, JobContext, JobProcess, WorkerOptions, cli, llm, ) from livekit.agents.voice_assistant import VoiceAssistant from livekit.plugins import openai, silero, deepgram from synap_livekit_agents import SynapMemoryHook, SynapContextProvider from maximem_synap import MaximemSynapSDK async def entrypoint(ctx: JobContext): await ctx.connect(auto_subscribe=AutoSubscribe.AUDIO_ONLY) participant = await ctx.wait_for_participant() # Caller identity: map LiveKit room metadata to your stable user_id. # In production, sign this in your token-issuer service; never trust client-supplied IDs. user_id = participant.metadata.get("synap_user_id") or f"anon_{participant.identity}" customer_id = participant.metadata.get("synap_customer_id") or "default" sdk = MaximemSynapSDK(api_key=...) await sdk.initialize() # SynapContextProvider injects relevant memories into the system prompt # before each LLM call. SynapMemoryHook ingests every user-assistant turn # back into Synap after the assistant speaks. context_provider = SynapContextProvider( sdk=sdk, user_id=user_id, customer_id=customer_id, mode="fast", # latency-sensitive max_results=5, ) memory_hook = SynapMemoryHook( sdk=sdk, user_id=user_id, customer_id=customer_id, ) initial_ctx = llm.ChatContext().append( role="system", text=( "You are a friendly phone agent. Use any known facts about the caller from " "their memory context. Keep responses under 2 sentences for voice clarity." ), ) assistant = VoiceAssistant( vad=silero.VAD.load(), stt=deepgram.STT(), llm=openai.LLM(model="gpt-4o"), tts=openai.TTS(), chat_ctx=initial_ctx, before_llm_cb=context_provider.before_llm, # ← memory inject after_llm_cb=memory_hook.after_llm, # ← memory ingest ) assistant.start(ctx.room, participant) if __name__ == "__main__": cli.run_app(WorkerOptions(entrypoint_fnc=entrypoint)) ``` **The two hooks** * `before_llm` fetches relevant memories using the most recent user utterance as the `search_query`, and prepends them to the chat context as a system message. Latency-tuned for `mode="fast"` so it doesn't add perceptible delay. * `after_llm` ingests the latest user + assistant turn as a single document with `document_type="ai-chat-conversation"`. Runs in the background; doesn't block the next TTS. **Latency budget for voice** Voice agents have a tight conversational comfort window. Synap's `fast` retrieval is comfortably within voice-conversational budgets in typical deployments. If Synap calls feel slow in your environment, lower `max_results` (e.g., to 3) to trim retrieval work per turn. **Privacy note for voice** The full audio is processed by LiveKit + Deepgram. Synap only sees the text transcript that comes back from STT. If you're under stricter privacy regimes, set the LiveKit recording flag to off and review the [Security & Trust](/resources/security-trust) page. ## Going further * **Integrations:** [LiveKit Agents](/integrations/livekit-agents) · [Pipecat](/integrations/pipecat) * **Cookbook:** [Voice Concierge](/cookbook/voice-concierge) * **Patterns:** [Graceful Degradation](/patterns/graceful-degradation) # Changelog Source: https://docs.maximem.ai/resources/changelog All notable changes to the Synap SDK and API are documented here. This project follows [Semantic Versioning](https://semver.org/) (SemVer). * **Major** versions introduce breaking changes to the API or SDK interfaces * **Minor** versions add new features in a backward-compatible manner * **Patch** versions contain backward-compatible bug fixes Subscribe to release notifications by watching the [maximem\_synap\_sdk](https://github.com/maximem-ai/maximem_synap_sdk) repository on GitHub, or join the `#releases` channel on [Discord](https://discord.gg/synap) for real-time updates. ## Compatibility Matrix Use this table to pick compatible versions when pinning dependencies. ### Core SDK | `maximem-synap` | Python | `httpx` | `pydantic` | `grpcio` | Cloud API | | --------------- | ------ | --------------- | ----------- | -------- | --------- | | 0.4.x | ≥ 3.11 | ≥ 0.27, \< 0.29 | ≥ 2.5, \< 3 | ≥ 1.60 | v1 | | 0.2.x | ≥ 3.11 | ≥ 0.27, \< 0.29 | ≥ 2.5, \< 3 | ≥ 1.60 | v1 | | 0.1.x | ≥ 3.11 | ≥ 0.27, \< 0.29 | ≥ 2.5, \< 3 | ≥ 1.60 | v1 | ### JavaScript / TypeScript SDK | `@maximem/synap-js-sdk` | Node.js | Python | Optional peers | Cloud API | | ----------------------- | ------- | ------------------ | --------------------------------------------------- | --------- | | 0.4.x | ≥ 20 | not required | `@grpc/grpc-js`, `@grpc/proto-loader` (stream only) | v1 | | 0.3.x | ≥ 18 | ≥ 3.11 on the host | | v1 | | 0.1.x – 0.2.x | ≥ 18 | ≥ 3.11 on the host | | v1 | ### Integration packages | Package | Pins core SDK | Pins framework | Notes | | ---------------------------------- | ---------------- | ----------------------------------------- | ---------------------------------- | | `maximem-synap-langchain` | ≥ 0.1 | `langchain` ≥ 0.2, `langchain-core` ≥ 0.2 | | | `maximem-synap-langgraph` | ≥ 0.1 | `langgraph` ≥ 0.2 | | | `maximem-synap-llamaindex` | ≥ 0.1 | `llama-index-core` ≥ 0.11 | | | `maximem-synap-openai-agents` | ≥ 0.1 | `openai` ≥ 1.30 | | | `maximem-synap-pydantic-ai` | ≥ 0.1 | `pydantic-ai` ≥ 0.0.13 | | | `maximem-synap-crewai` | ≥ 0.1 | `crewai` ≥ 0.60 | | | `maximem-synap-autogen` | ≥ 0.1 | `autogen-agentchat` ≥ 0.4 | | | `maximem-synap-google-adk` | ≥ 0.1 | `google-adk` ≥ 0.1 | | | `maximem-synap-haystack` | ≥ 0.1 | `haystack-ai` ≥ 2.5 | | | `maximem-synap-agno` | ≥ 0.1 | `agno` ≥ 1.0 | | | `maximem-synap-semantic-kernel` | ≥ 0.1 | `semantic-kernel` ≥ 1.0 | | | `maximem-synap-microsoft-agent` | ≥ 0.1 | `agent-framework` ≥ 0.1 | | | `maximem-synap-nemo-agent-toolkit` | ≥ 0.1 | `nemo-agent-toolkit` ≥ 0.5 | | | `maximem-synap-livekit-agents` | ≥ 0.1 | `livekit-agents` ≥ 0.10 | | | `maximem-synap-pipecat` | ≥ 0.1 | `pipecat-ai` ≥ 0.0.50 | | | `maximem-synap-vercel-adk` | n/a (TS package) | `ai` ≥ 3.0 | Node.js 18+ | | `maximem-synap-mastra` | n/a (TS package) | `@mastra/core` ≥ 0.5 | Node.js 18+ | | `maximem-synap-claude-agent` | ≥ 0.1 | `claude-agent-sdk` ≥ 0.1 | Available in Python and TypeScript | Pin numbers above reflect the minimum tested. Newer minor/patch versions of the framework will typically work, but if a framework ships a breaking change, the integration package will pin around it explicitly. Check the integration package's own changelog for the exact pin if you suspect a compatibility issue. ### Cloud API The Cloud API is versioned at the URL prefix (`/v1/...`). The current SDK targets v1 exclusively. v2 is not on the near-term roadmap. *** # JavaScript / TypeScript SDK Releases of `@maximem/synap-js-sdk`. Versioned independently of the Python SDK. ## js v0.4.6, 2026-08-27 ### Fixed * **The options added in 0.4.5 accept `undefined` explicitly.** `logger`, `sdk_st_authoritative` and `st_verbatim_overlay` were declared as plain optional, so under `exactOptionalPropertyTypes` you could not pass a value that might be absent, which is the normal shape when configuration comes from the environment. 0.4.4 had already done this for `apiKey`, `clientId`, `instanceId` and `baseUrl`; these now match. ## js v0.4.5, 2026-08-27 Closes the surface the JavaScript SDK was missing against Python. Each item below was previously documented as "Python only", which was the wrong place to fix a missing feature. ### Added * **`sdk_st_authoritative`,** and the `SYNAP_SDK_ST_AUTHORITATIVE` environment variable. With it on, `conversation.context.get_context_for_prompt()` and `get_compacted()` render from the SDK's own short-term store when the conversation is warm, skipping the cloud round trip. That is a metered call saved as well as a round trip, and JavaScript had no way to ask for it. Off by default. The three prompt styles (`structured`, `narrative`, `bullet_points`) produce byte-identical output to the Python formatter, so an application that switches languages does not silently change what its model sees. * **`st_verbatim_overlay` as a client option.** The behaviour existed but could only be reached through `SYNAP_ST_VERBATIM_OVERLAY`. The environment variable still wins over the option, in both directions, matching Python. * **`logger`,** on the constructor and on `configure()`. Every diagnostic the SDK emits goes through it; the default remains `console.warn`. Python routes these through the stdlib `logging` module, which is why it takes `log_level` and `logger`; there is no global logger in JavaScript, so this takes the sink directly. `log_level` remains accepted and ignored. * **`record_thinking({ step_index, thought_type })`.** Both are folded into the event's metadata map under those exact keys, which is how Python transmits them: the proto has no dedicated fields. A caller-supplied metadata entry of the same name is not overwritten. * **`TimeoutConfig.streamIdle`,** defaulting to 60 seconds as in Python. * **`api_base_url` as an alias for `baseUrl`,** so one configuration object works against either SDK unchanged. ## js v0.4.4, 2026-08-27 ### Fixed * **`retryPolicy: null` disables retries, as the documentation always said it did.** It restored the default three-attempt policy instead, and the constructor rejected `null` outright, so there was no way to turn retries off from JavaScript at all. Python's `SDKConfig(retry_policy=None)` leaves `max_attempts` at 1 and this now matches. **If you pass `null` to `configure()` expecting the defaults back, pass the policy explicitly instead.** * **Insufficient-credit errors carry the top-up links again.** The 402 handler read a `required_credits` key the server never sends (it sends `minimum_required_credits`), so `requiredCredits` was always `null` against a real server, and `recovery_url` and `redeem_url` were dropped entirely. `InsufficientCreditsError` now exposes `requiredCredits`, `recoveryUrl` and `redeemUrl`. * **`instance.send_message` reports tool calls.** `tool_name` and `tool_args` were missing from the options, though the proto carries both fields and the Python SDK sends them, so a tool call could not be reported from JavaScript. `tool_args` is JSON-encoded into `tool_args_json`, matching Python. * **`credits.get_ledger` accepts its filters.** `entry_type`, `from_time` and `to_time` were absent, and the default `limit` was 50 against Python's 100, so the same call returned different pages in the two SDKs. * **`context_mode: "conversation-summary"` is reachable.** The mode was documented and typed but its parameters were never forwarded, so the call fell through to a normal in-conversation fetch. `include_profile` and `last_n_conversations` are forwarded, and an unknown mode now raises instead of being ignored. * **Context fetches apply the B2C check.** Retrieval skipped the scope validation the write paths perform, so a `customer_id` sent to a B2C instance reached the server and came back as an opaque HTTP 400. ### Changed * **Response types replace `Record`.** `credits.get_balance`, `get_ledger`, `estimate` and `redeem`, plus `conversation.ingest_transcript` and `memories.batch_create`, returned bare JSON, so every field read was `unknown` in TypeScript. They now return types mirroring the Python models: `CreditBalance`, `CreditLedgerPage`, `CreditEstimate`, `RedeemResult`, `TranscriptIngestResult`, `BatchCreateResult`. * **`CreateMemoryResult` requires `ingestion_id`, `document_id`, `status` and `queued_at`,** matching Python's `CreateMemoryResponse`. All four were optional, which made the commonest two-line pattern in the documentation, `create()` then `wait_for_completion(result.ingestion_id)`, fail to compile under `strict`. * **`apiKey`, `clientId`, `instanceId` and `baseUrl` accept `undefined` explicitly,** so `new SynapClient({ apiKey: process.env.SYNAP_API_KEY })` compiles under `exactOptionalPropertyTypes`. Undefined already meant "read the environment"; the type now says so. ### Added * **`COMPACTION_LEVELS` and the `CompactionLevel` type,** mirroring Python's enum. `compaction_level` was an untyped `string`, so a typo reached the server rather than the compiler. ## js v0.4.3, 2026-08-26 ### Fixed * **B2C instances take `user_id` alone.** `memories.create`, `memories.create_from_file`, `conversation.record_message` and `record_messages_batch` all required a `customer_id`, so on a B2C instance, where the server rejects one, the only correct call could not be expressed: two of them threw `customer_id is required` before a request was ever made. A batch validates every message before sending any, so one bad message cannot half-apply a batch. Where the server is too old to report its isolation mode, nothing is enforced, so a new SDK against an old server refuses nothing. ## js v0.4.2, 2026-08-25 ### Fixed * **`SYNAP_BASE_URL` is honoured again.** 0.4.0 and 0.4.1 ignored it and used the Synap Cloud default, so a deployment configured through the environment sent its requests to Synap Cloud instead. Nothing failed, which is what made it hard to spot. An explicit `baseUrl` passed to the constructor still wins over the environment. * **`SYNAP_GRPC_TLS` is accepted as well as `SYNAP_GRPC_USE_TLS`.** A plaintext deployment configured with the former was attempting TLS and could not connect. ## js v0.4.1, 2026-08-25 ### Fixed * **Anticipated bundles are now read on the fetch path.** The stream stored them and no fetch consulted the store, so every retrieval was a cloud call and the stream had no effect on anything. Scope widening follows the same rules as the Python SDK: a customer-scope lookup widens to client-shared bundles but never to user-scoped ones. * **One client per identity, per process.** Each `new SynapClient()` built its own anticipation cache, so an application constructing a client per request never saw a warm one. Clients for the same identity now share state, and the instance id resolved during `initialize()` becomes an additional identity so constructing by id later finds the same client. * **A turn is visible to the next fetch.** Messages recorded through `conversation.record_message` or the stream are held locally and merged into the following conversation-scope fetch, so a fast follow-up sees the turn that preceded it rather than waiting for the server to compact. `SYNAP_ST_VERBATIM_OVERLAY=0` disables it. * **Periodic user-summary injection.** A cached user summary is folded into the response every fifth turn of a conversation, matching the Python SDK. Requires a `user_id` on the fetch; without one the summary cannot be scoped and is skipped. * **`context_used` and `context_assembled` are emitted.** These drive per-prefetch scoring and the Requests-page audit. Both ride the anticipation stream and are skipped when it is not open. ## js v0.4.0, 2026-08-24 ### Changed * **The SDK is now a native TypeScript client.** Earlier versions were a Node.js wrapper that spawned the Python SDK as a subprocess, which meant Python 3.11+ on every host, no support for Edge runtimes or Workers, and one call at a time through a single pipe. The SDK now talks to the API directly. * **Node.js 20+** is required. * **The runtime setup step is gone.** `npx synap-js-sdk setup` no longer does anything and can be removed from install and CI scripts. It still exits `0`, so leaving it in place does not break a build. `~/.synap-js-sdk` is orphaned and safe to delete. * **The package ships both ES modules and CommonJS.** `require()` and `import` both work, each with its own type definitions. * **`listen()` is opt-in.** Earlier versions opened an anticipation stream automatically during initialization. It now starts only when you call `instance.listen()`, and needs `@grpc/grpc-js` and `@grpc/proto-loader` installed. * **The local cache no longer persists across restarts.** It was a SQLite file on disk; it is now in memory, so that a native module does not compromise serverless portability. ### Removed * `setupPythonRuntime()` and `setupTypeScriptExtension()`. Neither has anything to do. * `createClient(options)`, which was an alias for `new SynapClient(options)`. * `resolveInstanceId()`. `initialize()` resolves the instance id from the API key; read it from `client.instance_id`. * `createSynapError()`. Construct the error classes directly, or branch on `error.code`. * Deleting memories by user id. It relied on a list held in the bridge process, so it silently deleted nothing in serverless while reporting success. Pass an explicit `memory_id`. * The five `SYNAP_PYTHON_*` and `SYNAP_PY_SDK_*` environment variables, along with `SYNAP_JS_SDK_HOME`. Upgrading from 0.3.x? No client methods were removed, and `require()` still works, so most projects change nothing beyond the install and dropping the setup step. *** # Python SDK ## v0.4.3, 2026-08-05 ### Fixed * **The local cache is now scoped to the instance, not just the account.** It lived at `~/.synap/{client_id}/` and addressed its files by scope and entity id alone. `client_id` identifies the Synap *account*, which can own several instances, and those are separate memory stores sharing no data (so two instances under one account wrote to the same `users/{user_id}.db`, and where the same `customer_id` or `user_id` appeared under both, one instance could be served the other's cached context. The path is now `~/.synap/{client_id}/{instance_id}/` and the instance is part of the cache key. **Server-side scoping was never affected) this was local disk only**, and it needed two instances of one account live in the same process, which only became a supported shape in 0.4.1. **Your cache starts cold after this upgrade** and refills on demand; entries under the old path are not migrated. That is deliberate: a pre-0.4.3 directory may hold entries from more than one instance, and there is no way to tell which belongs to which, so moving them would carry the contamination forward. Nothing is deleted; you can remove the old `~/.synap/{client_id}/*.db`, `customers/` and `users/` entries once you have upgraded. No server-side data is involved. *** ## v0.4.2, 2026-08-05 ### Fixed * **One Synap instance no longer ends up with two live SDKs.** An SDK built from an API key alone is identified by that key, because its `instance_id` does not exist yet; it is resolved from the key during `initialize()`. The resolved id was never recorded, so a later `MaximemSynapSDK(instance_id="inst_…")` for that same instance missed the lookup and built a second SDK: two anticipation caches, two short-term stores, two `Listen` streams and a lower local cache hit rate for one instance. `initialize()` now records the resolved id as an additional identity for the SDK. Constructing by API key keeps working exactly as before, and an `instance_id` already held by another live SDK is never taken from it. * **Concurrent first constructions no longer diverge.** Several threads racing to build the *first* SDK for one identity could all miss the registry and the last one to finish would win, leaving every other caller holding an SDK the registry did not know about, with its own connections and caches. Construction now claims its slot atomically; the callers that lose the race receive the winner. This was reachable on a normal cold start in a multi-threaded server. * **`shutdown()` releases every identity the SDK holds**, so nothing hands out an SDK whose transports are already closed. * **Test modules are no longer published inside the package.** `maximem_synap-0.4.1` shipped six `test_*.py` modules into `site-packages`, one importing an internal server module that does not exist outside our monorepo, which could fail any test run that collected them. The 0.4.2 wheel and sdist contain none. * **`maximem-synap-nemo-agent-toolkit`**: SDK teardown never ran. Both entry points probed for a `close()` method the SDK does not have (it is `shutdown()`), so the guard silently did nothing and every workflow leaked its HTTP transport, gRPC stream and telemetry collector. No API was added, removed or renamed, and there is no data migration. Callers passing an explicit `instance_id` see byte-identical behaviour. *** ## v0.4.1, 2026-08-04 ### Fixed * **Cross-tenant credential and state sharing when one process used more than one API key.** The SDK keeps one instance per Synap instance, keyed on `instance_id`, but `instance_id` is optional and empty at construction time, because it is resolved from the API key later, during `initialize()`. Every `MaximemSynapSDK(api_key=...)` therefore landed in the same registry slot, and the second one adopted the first's entire state, credentials included. In a process serving two tenants, tenant B's SDK authenticated as tenant A: B's reads returned A's memory and B's writes were committed into A's instance. It was silent (no exception, no warning, no log line) because the server correctly authenticated the key it received; it simply received the wrong one. The registry now derives its key from the credential when no `instance_id` is given (stored as a truncated SHA-256 digest, never in plaintext). * **`shutdown()` left a stale registry entry.** Registration used the constructor's `instance_id` and unregistration used `self.instance_id`, which `initialize()` may have replaced in between. The entry survived teardown, so the registry kept handing out an SDK whose transports, stream and cache were already closed. * **A `_force_new` SDK's `shutdown()` evicted the real singleton.** Unregistration had no ownership check, so a throwaway SDK (a test fixture, an adapter, the JS bridge) removed whatever sat on its key, taking the application's live SDK out of the registry mid-conversation. **Affects `maximem-synap` ≤ 0.4.0, and only a process that constructs the SDK with two or more different API keys**: a backend holding a key per customer, a process talking to two Synap environments at once, or a worker that switches keys between tasks. A process that uses a single API key is unaffected, which is the common case. Upgrade with `pip install --upgrade "maximem-synap>=0.4.1"`; no code changes and no data migration are required. If you did run more than one key on 0.4.0 or earlier, memory written in that period may have been stored against the first key's instance; [contact support](/resources/support) and we will help you check what was written where. ### Changed * The registry now derives its key from the API key, so **each distinct key gets its own SDK**, including two keys issued against the same instance, which each authenticate as themselves. Only an explicitly passed `instance_id` still collapses several keys onto one SDK, unchanged from before. If you rotate a key inside a long-running process and construct by `instance_id`, call `await sdk.shutdown()` before reconstructing, or restart the worker. See [Rotating a key in a long-running process](/sdk/initialization#rotating-a-key-in-a-long-running-process). *** ## v0.4.0, 2026-07-17 ### Added * **`conversation.ingest_transcript(...)`**: one-shot async push of a full conversation transcript (string or typed `TranscriptTurn` list) plus optional client analysis JSON and metadata. Returns immediately with a `TranscriptIngestResponse` (`ingestion_id`, `status`, `summary_status`, …); poll with `memories.status()` / `wait_for_completion()`. Idempotent on `(conversation_id, transcript)`: an identical re-push returns `status="duplicate"` with the original `ingestion_id`; a *different* transcript under the same `conversation_id` raises `TranscriptConflictError`. Unlike `record_message`, `conversation_id` is an arbitrary client string (no UUID validation); the server coerces it and echoes the original as `external_conversation_id`. * **Conversation-summary fetch**: `fetch(...)`, `user.context.fetch(...)` and the unified `sdk.fetch(...)` gain `context_mode` (`"in-conversation"` default / `"conversation-summary"`), `include_profile` (default `True`) and `last_n_conversations` (default `1`, range 0–20). In summary mode the response carries a caller `profile` and previous-conversation summaries instead of item lists: the call-start read for async integrations. In the unified `sdk.fetch`, these three params are forwarded **only** to the user-scope sub-fetch. * **`user.get_profile(user_id, customer_id=None)`**: convenience getter returning a typed `UserProfileModel` (client-defined critical attributes + free-text overview). Raises `ContextNotFoundError` (404) when no profile exists. * **New typed models**: `TranscriptTurn`, `TranscriptIngestResponse`, `UserProfileModel`, `ProfileAttributeModel`, `ConversationSummaryModel`, all with a `.raw` escape hatch and unknown-field tolerance. `ContextResponse` and `UnifiedContextResponse` gain optional `profile` / `conversations` fields, and `UnifiedContextResponse.format_for_prompt()` renders `## Caller Profile` and `## Previous Conversations` sections when present (byte-identical output when absent). * **New exceptions**: `ConflictError` and `TranscriptConflictError` (both permanent). `InsufficientCreditsError` is now exported at the top level. ### Changed * **HTTP 409 and 422 mapping**: the transport now maps **422 → `InvalidInputError`** and **409 → `ConflictError`** (discriminated on a `{"detail": {"code": "transcript_conflict"}}` body to `TranscriptConflictError`). Both are permanent and **never retried**. Previously both status codes fell through to a retryable transient error. * **`conversation.compact()` 409 semantics** *(behavioral)*: a "Compaction already in progress" 409 now raises a permanent `ConflictError` immediately instead of being retried as a transient error and eventually surfacing as one. Intentional: a 409 here means another compaction already holds the lock, and retrying cannot change that. Catch `ConflictError` (or its base `SynapPermanentError`) where you previously caught the transient/retry-exhausted error. *** ## v0.2.0, 2026-07-06 ### Added * **`precision_level` fetch parameter**: All context fetch calls now accept an optional `precision_level` parameter (`"high"` or `"medium"`). With `"high"` (the default), behavior is unchanged: results go through an additional relevance-refinement pass before being returned. `"medium"` skips the refinement pass for faster responses; recall isn't impacted (the same candidate memories are searched), but outputs are less precisely filtered. *** ## v0.1.2, 2025-01-15 ### Added * **Entity Resolution integration**: The ingestion pipeline now resolves entities across conversations. Mentions of the same entity (e.g., "John", "John Smith", "my manager") are linked to a canonical entity record. * **Review queue for entity resolution**: Ambiguous entity matches are routed to a review queue for human verification via the Dashboard. * **Auto-registration of unresolved entities**: New entities that do not match any existing record are automatically registered at the CUSTOMER scope for future lookups. * **Semantic entity matching**: Entity resolution uses semantic similarity matching, catching variations that exact string matching would miss. ### Changed * Entity resolution is now integrated before persistence in the ingestion flow. * Entity resolution runs as a graceful degradation feature: if unavailable, the pipeline continues without resolution and logs a warning. ### Fixed * Fixed non-deterministic test behavior caused by Python set iteration order in pipeline stage tests. *** ## v0.1.1, 2025-01-10 ### Added * **Memory Architecture Configuration (MACA) system**: Full configuration lifecycle with init, update, review, apply, and rollback operations. * **Admin API Groups A-D**: Client lifecycle (10 endpoints), instance lifecycle (12 endpoints), config management (10 endpoints), and setup/onboarding (6 endpoints). * **Dashboard API routes**: Configuration detail and history endpoints for the web UI. * **Configuration persistence and workflow improvements**: Added storage and workflow support for configuration metadata, approvals, and history. * **Configuration validation improvements**: Stronger schema and business-rule validation for submitted configuration files. * **Setup and architecture management improvements**: Better onboarding and dashboard-facing configuration management flows. ### Changed * Admin API response shapes were standardized for consistency across dashboard routes. * Configuration version numbers are parsed from semantic version strings ("1.0.0" becomes version `1`). ### Fixed * Resolved circular import issues in `admin_api/__init__.py` by using lazy imports (inside methods) for cross-manager dependencies. *** ## v0.1.0, 2025-01-05 ### Added * **Initial release** of the Synap SDK and API. * **Memory ingestion pipeline**: Four-stage async pipeline (extraction, categorization, entity resolution, storage) with `fast` and `long-range` processing modes. * **Context retrieval**: `POST /v1/context/fetch` with vector search, graph traversal, and re-ranking. Supports `fast` and `accurate` retrieval modes. * **Context compaction**: `POST /v1/context/compact` with `adaptive`, `aggressive`, `balanced`, and `conservative` strategies. * **Instance management**: Full CRUD for instances via the Dashboard API, including API key generation and revocation. * **API key authentication**: `synap_` keys issued from the Dashboard, used as `Authorization: Bearer` for all SDK communication (REST and gRPC). * **Cloud auth layer**: Production-ready authentication and authorization foundation for SDK and dashboard operations. * **Memory Architecture Configurators**: Initial configuration system for storage, ingestion, and retrieval controls. * **Scope system**: Four-level scope chain (USER > CUSTOMER > CLIENT > WORLD) with proper isolation and inheritance. * **Webhook system**: Five event types (`conversation.started`, `conversation.ended`, `context.retrieved`, `config.applied`, `compaction.completed`) with HMAC-SHA256 signature verification. * **Analytics**: Usage metrics, latency percentiles, and token tracking with minute/hour/day rollup buckets. * **Python SDK**: Fully async SDK with typed exceptions and automatic retries. * **PostgreSQL backend**: Persistent storage for clients, instances, credentials, memories, and analytics data. * **Production key management integration**: Cloud-integrated key management support. ### Security * API keys are hashed (SHA-256) at rest and shown to the user only once at generation time. * Revoking an API key takes effect immediately. * AuthContext is immutable once created, preventing tampering. *** Versions prior to 1.0.0 may include breaking changes in minor version increments as the API stabilizes. We recommend pinning to a specific version in production and testing upgrades in staging first. # FAQ Source: https://docs.maximem.ai/resources/faq Common questions about Synap, organized by topic. If your question is not answered here, check the [Support](/resources/support) page for additional help channels. ## General Synap is a managed memory platform for AI agents. It provides a complete pipeline for ingesting conversations and documents, extracting structured knowledge (facts, preferences, episodes, emotions, temporal events), resolving entities across conversations, and retrieving relevant context when your agent needs it. Instead of building and maintaining your own vector database, retrieval pipeline, and entity resolution system, you integrate the Synap SDK into your application and let the platform handle the rest. Your agent gets long-term, structured memory with a few lines of code. Traditional RAG systems retrieve raw document chunks based on similarity search. Synap goes several steps further: * **Structured extraction**: Synap does not just store chunks. It extracts typed knowledge (facts, preferences, episodes, emotions, and temporal events) with confidence scores. * **Entity resolution**: Mentions of the same entity across conversations (e.g., "John", "my manager", "John Smith") are linked to a single canonical entity. * **Scoped retrieval**: Memories are scoped to users, customers, and organizations. Each user gets their own memory without manual isolation logic. * **Context compaction**: Long conversation histories are automatically summarized while preserving key information, reducing token usage. * **Managed pipeline**: No vector databases to deploy, no embedding models to tune, no retrieval pipelines to build. All of these are memory layers for AI agents, and at a 30,000-ft view they overlap. The differences that matter in practice: | Capability | Synap | Mem0 | Zep | Letta | SuperMemory | | --------------------------------------------------------------------- | -------------------------------------------- | -------------------- | ----------------- | -------------------- | -------------------- | | Typed memories (facts / preferences / episodes / emotions / temporal) | Native, per-type retrieval | Single "memory" type | Facts + episodes | Single "memory" type | Single "memory" type | | Entity resolution across conversations | Yes (graph store, automatic) | Limited | Yes (graph store) | No | No | | Multi-scope (user / customer / client / world) | Native scope chain | User only | User only | User only | User only | | Customized Memory Architecture (MACA) | Yes, generated from a Use-Case Markdown spec | Manual prompt tuning | Manual config | Manual schema | Manual config | | Context compaction (auto-summarize long history) | Built-in (`context.compact`) | No | Limited | No | No | | Self-host option | Self-host + cloud (Enterprise) | Self-host + cloud | Self-host + cloud | Self-host | Cloud only | | Anticipation cache (background prefetch of likely-needed memories) | Yes | No | No | No | No | | B2B-native (customer/org isolation, MACA-per-instance) | Yes | No (user-only) | No (user-only) | No (user-only) | No (user-only) | **When Synap is the right choice**: you're building a B2B agent product where each customer org has shared context (policies, runbooks, product data) on top of per-user memory; you want typed extraction so the LLM can reason over preferences vs. facts vs. temporal events distinctly; you don't want to run a vector DB. **When another tool is a better fit**: you only have a single-user consumer app and don't need the scope chain or entity graph (Mem0 / SuperMemory are simpler); you want agent state + memory bundled in one SDK (Letta). Build your own if **any** of these apply: * You have strict data residency or air-gap requirements that managed cloud can't meet, and your prospective scale doesn't justify Synap's self-hosted licensing. * Your memory model is highly domain-specific (e.g., medical records with regulated taxonomies) and you'd end up reimplementing extraction anyway. * You're at single-digit MAU and a `pgvector` table + a few prompt-engineered extraction calls is genuinely cheaper than the integration overhead. Don't build your own if you're just worried about "lock-in" or "wanting control." The honest cost of running a production memory pipeline (embeddings, vector store, graph store, entity resolution, compaction, eviction, observability) is a multi-engineer-quarter project that no agent team has gotten right on a side budget. Yes. Synap is designed with a zero-trust security model: * **Encryption in transit**: All connections use TLS 1.3. * **Encryption at rest**: All stored data is encrypted at rest using AES-256. * **Instance isolation**: Each instance has its own storage namespace. Memories from one instance are never accessible from another. * **Scope isolation**: Within an instance, memories are scoped to users and customers. A user can only access memories in their scope chain. * **Credential management**: API keys are hashed (SHA-256) before storage. Plaintext keys are never stored on the server. See [Authentication](/setup/authentication) for details on the credential lifecycle. Synap Cloud is currently available in **US East** and **EU Central**. Additional regions are planned based on demand. Contact [sales@maximem.ai](mailto:sales@maximem.ai) for region-specific requirements or data residency needs. ## SDK The official Synap SDK is available for **Python 3.11+**. It is fully async, built on `asyncio`, and available via pip: ```bash theme={null} pip install maximem-synap ``` A native JavaScript/TypeScript SDK is available (`@maximem/synap-js-sdk`), requiring Node.js 20+. It runs on Node, Vercel Edge, Cloudflare Workers and the browser. A Go SDK is on the roadmap. Check the [Changelog](/resources/changelog) for updates on new language support. Yes. All SDK methods are `async` and must be called with `await` inside an async context. This design ensures your application never blocks on network I/O. If you need to call the SDK from synchronous code, use `asyncio.run()`: ```python theme={null} import asyncio from maximem_synap import MaximemSynapSDK async def ingest(): sdk = MaximemSynapSDK(api_key="synap_your_key_here") await sdk.initialize() return await sdk.memories.create( document="User prefers dark mode.", document_type="ai-chat-conversation", user_id="user_123", customer_id="acme_corp", mode="fast", ) # From synchronous code result = asyncio.run(ingest()) ``` The SDK raises typed exceptions. Catch specific exceptions for fine-grained error handling: ```python theme={null} import uuid from maximem_synap import ( AuthenticationError, # raised when the API key is missing, malformed, or revoked ContextNotFoundError, # raised when the conversation is unknown RateLimitError, # raised after the SDK exhausts its automatic retries on rate-limited calls ServiceUnavailableError, # raised when Synap is temporarily unavailable InvalidInputError, # raised when the arguments to an SDK call are invalid ) try: context = await sdk.conversation.context.fetch( conversation_id=str(uuid.uuid4()), search_query=["user preferences"], ) except RateLimitError as e: # Automatic retry with backoff is built into the SDK. # This exception is raised only after all retries are exhausted. print(f"Retry after {e.retry_after}s") except ContextNotFoundError: print("Conversation not found") ``` The SDK automatically retries transient errors (rate limits, service-unavailable) with exponential backoff. See [Error Handling](/sdk/configuration) for the full reference. Yes. Synap is framework-agnostic. The SDK operates independently of your LLM orchestration layer. Common integration patterns: * **LangChain**: Use `sdk.conversation.context.fetch()` in a custom retriever, then pass the context to your chain. * **LlamaIndex**: Use `await sdk.conversation.context.get_compacted(conversation_id=..., format='structured')` and inject it into your query engine. * **Direct**: Call the SDK from your application code and pass context to any LLM API. See the [First Integration](/setup/first-integration) guide for detailed examples. ## Memory Retention is configurable per use-case via [MACA](/concepts/memory-architecture), which Synap generates automatically from your [use-case file](/concepts/memory-architecture#the-use-case-file). Compliance-sensitive agents get longer retention; consumer agents get shorter retention. You can also delete individual memories at any time via [`sdk.memories.delete()`](/sdk-reference/memories/delete), regardless of the retention policy. Yes. Use `sdk.memories.delete(memory_id)` to permanently delete a specific memory. Deletion removes the memory from both the vector store and graph store. Entity references are updated but the entities themselves are not deleted, as they may be referenced by other memories. ```python Python theme={null} await sdk.memories.delete("mem_a1b2c3d4e5f67890") ``` ```javascript JavaScript theme={null} await sdk.memories.delete('mem_a1b2c3d4e5f67890'); ``` ```typescript TypeScript theme={null} await sdk.memories.delete('mem_a1b2c3d4e5f67890'); ``` Deletion is permanent and cannot be undone. See the [Memory API](/sdk-reference/memories/delete) for details. The `mode` parameter controls a speed-quality tradeoff. Ingestion and retrieval use distinct mode value sets: **Ingestion (`sdk.memories.create()`)** values: `"fast"` or `"long-range"` (default). | Mode | Speed | Quality | Best For | | ------------ | -------- | ------- | -------------------------------------------------------------- | | `fast` | Highest | Good | Real-time chat ingestion, high-volume streams | | `long-range` | Moderate | Highest | Important documents, support tickets, onboarding conversations | **Retrieval (`sdk.conversation.context.fetch()`)** values: `"fast"` (default) or `"accurate"`. | Mode | Latency | Method | Best For | | ---------- | ------- | --------------------------- | ------------------------------------------------ | | `fast` | Lower | Vector similarity only | Real-time chat, single-topic queries | | `accurate` | Higher | Vector + graph + re-ranking | Relationship-aware queries, multi-entity context | Actual latency depends on your Instance and workload. See **Dashboard → Usage** for real numbers. The two value spaces are not interchangeable. Passing `"accurate"` to `memories.create()` or `"long-range"` to `context.fetch()` will be rejected. Context fetch calls accept an optional `precision_level` parameter: `"high"` (default) or `"medium"`. With `high`, results go through an additional relevance-refinement pass before being returned; `"medium"` skips the refinement pass for faster responses. Recall isn't impacted (the same candidate memories are searched), but outputs are less precisely filtered. Independent of `mode`; combine with either `fast` or `accurate`. For real latency on your instance, see **Dashboard → Usage**. If you provide a `document_id` in the create memory request, Synap checks for duplicates. If a document with the same ID has already been ingested, the request is rejected as a duplicate (`InvalidInputError`). If you do not provide a `document_id`, the document is ingested as a new record. The extraction pipeline may produce duplicate memories if the content overlaps with previously ingested documents. Entity resolution helps by linking entities across documents, but the memories themselves are stored independently. For production use, we recommend always providing a `document_id` for deduplication. ## Configuration Synap auto-generates each Instance's memory configuration from the [Use-Case Markdown file](/concepts/memory-architecture#the-use-case-file) you upload. To change behavior (enable different memory categories, shift the primary scope, update retention guidance), re-upload an updated use-case file in the Dashboard. Synap re-evaluates and applies the new configuration. The previous version is retained so you can roll back if needed. No. Configuration updates are zero-downtime: in-flight requests complete on the previous configuration and new requests pick up the new one. There is no traffic interruption. Existing memories keep their original scope and category assignments. The updated configuration governs **new** memories ingested after it takes effect, and it tunes retrieval/ranking behavior going forward. Memory data is not retroactively rewritten. ## Billing and Usage Synap usage is measured across three dimensions: * **API calls**: Each HTTP request to the API counts as one API call. Batch endpoints count as a single call regardless of batch size. * **Token usage**: LLM tokens consumed during ingestion (extraction, categorization) and retrieval (re-ranking, compaction). Input and output tokens are tracked separately. * **Storage**: Total memories stored across all instances. Measured as a monthly peak. Use the [Dashboard Analytics](/dashboard/monitoring-and-analytics) to monitor your usage in real time. Each HTTP request to any Synap API endpoint counts as one API call, including: * Memory ingestion (single and batch) * Context fetch and compaction * Configuration operations * Dashboard queries * Analytics queries * Status checks Webhook deliveries do **not** count as API calls. ## Troubleshooting Common causes and solutions: 1. **Missing or malformed API key**: Verify the API key string starts with `synap_` and is current in the Dashboard. 2. **Revoked key**: Check the Dashboard to verify the key is still active. 3. **Wrong instance**: The API key may not have access to the instance you are targeting. See [Error Codes](/sdk-reference/errors#authentication-errors) for the full list of auth-related errors. If context fetch returns empty results when you expect matches: 1. **Check ingestion status**: Verify using `sdk.memories.status(ingestion_id)`. Memories are not retrievable until ingestion completes. 2. **Check scope**: Memories are scoped to the user/customer that was specified during ingestion. Context fetch only returns memories within the conversation's scope chain. 3. **Check confidence threshold**: Memories with confidence below the MACA threshold (default 0.7) are discarded during ingestion. 4. **Check memory types**: If you are filtering by `types` in the fetch request, ensure the desired types are included. 5. **Check context budget**: If the budget is very small, only the highest-ranked memories may fit. Use the Dashboard monitoring tools to inspect the ingestion pipeline and stored memories for debugging. Steps for diagnosing retrieval problems: 1. **Get the correlation ID**: Capture the correlation ID returned by the SDK on the fetch response (or on the raised exception). 2. **Check analytics**: In the Dashboard, open **Analytics** to see if `context_fetch` latency is abnormal for your Instance. 3. **Try different modes**: Switch from `fast` to `accurate` mode to see if subquery decomposition and reranking surface additional results. 4. **Broaden the query**: Try more general search queries or remove type filters. 5. **Check compaction**: If the context was recently compacted, some memories may have been summarized away. Re-fetch with `format='narrative'` and `format='structured'` to compare what's available. If the issue persists, [contact support](/resources/support) with the correlation ID and instance ID. # Glossary Source: https://docs.maximem.ai/resources/glossary A comprehensive reference of terms, concepts, and identifiers used throughout the Synap platform. *** ### Agent Hints Domain-specific guidance that steers the extraction and retrieval models: custom terminology, extraction priorities, retrieval preferences. Agent hints are derived from your [use-case file](/concepts/memory-architecture#the-use-case-file) when Synap generates the Instance's [Memory Architecture](/concepts/memory-architecture). For example, a medical support agent's use-case file would lead Synap to prioritize extracting medication names and dosages. *** ### API Key The primary credential for SDK authentication. Generated from the [Dashboard](/dashboard/overview#managing-instances), prefixed with `synap_`. Multiple keys can be active per instance. The raw key is shown only once at generation time; only the SHA-256 hash is stored server-side. See [Authentication](/setup/authentication). *** ### Client The top-level organizational entity in Synap, representing your company or team. A client owns one or more [instances](#instance) and is identified by a client ID in the format `cli_`. All API keys, billing, and team management are scoped to the client level. See [Clients and Instances](/concepts/memory-scopes#clients-and-instances). *** ### Compaction The process of condensing a conversation's accumulated [context](#context-response) into a shorter summary while preserving the most important information. Compaction reduces token usage when injecting context into LLM prompts. Strategies include `adaptive`, `aggressive`, `balanced`, and `conservative`. See [Context Compaction](/concepts/context-end-to-end#context-compaction) and the [Context API](/sdk-reference/conversation-context/compact). *** ### Confidence Score A numerical value between `0.0` and `1.0` assigned to each extracted [memory](#memory) indicating how certain the extraction model is that the information is accurate and correctly categorized. The minimum confidence threshold is auto-tuned by Synap from your [use-case file](/concepts/memory-architecture#the-use-case-file). Memories below the threshold are discarded during ingestion. *** ### Context Budget The maximum number of tokens allowed in a [context response](#context-response). The context budget prevents the retrieval system from returning more data than the downstream LLM can process. Auto-set per instance by Synap from your [use-case file](/concepts/memory-architecture#the-use-case-file). *** ### Context Response The structured output returned by the [Context Fetch](/sdk-reference/conversation-context/fetch) method. Contains ranked memories organized by type ([facts](#fact), [preferences](#preference), [episodes](#episode), [emotions](#emotion)), along with metadata about query performance and token usage. *** ### Correlation ID A unique request identifier returned by the SDK on every response and surfaced on every error. The correlation ID traces a request through the entire Synap processing pipeline, including asynchronous jobs. Always include the correlation ID when [contacting support](/resources/support). Format: `syn___` (e.g. `syn_abc123_1706123456789_x7k9m2`). *** ### Customer A scoping entity within a [client](#client) that represents an end-customer or account. Memories scoped to a customer are accessible by all [users](#scope) within that customer but isolated from other customers. Part of the [scope chain](#scope-chain). See [Scopes](/concepts/memory-scopes). *** ### Entity Resolution The process of identifying and linking mentions of the same real-world entity across different conversations and documents. For example, "John", "John Smith", and "my manager" may all resolve to the same person entity. Synap uses semantic matching to resolve entities. Unresolved entities are auto-registered at the CUSTOMER scope. See [Entities and Resolution](/concepts/entity-resolution). *** ### Episode A [memory](#memory) type representing a narrative account of an event or interaction. Episodes capture "what happened" with temporal context. Example: "User described a frustrating experience trying to set up their printer last weekend." See [Memory Lifecycle](/concepts/memories-and-context). *** ### Emotion A [memory](#memory) type representing an emotional state, sentiment, or feeling expressed in a conversation. Example: "User expressed excitement about their upcoming vacation." See [Memory Lifecycle](/concepts/memories-and-context). *** ### Fact A [memory](#memory) type representing objective, factual information extracted from conversations. Facts are statements about the world, the user, or their circumstances. Example: "User works at Acme Corp as a senior engineer." See [Memory Lifecycle](/concepts/memories-and-context). *** ### Graph Store One of two storage backends used by Synap. The graph store maintains entity relationships and traversal paths, enabling queries like "find all memories related to this person's workplace." Graph queries complement [vector store](#vector-store) similarity search for deeper contextual retrieval. See [Storage Engines](/concepts/how-ingestion-works). *** ### Ingestion The asynchronous pipeline that processes raw documents into structured [memories](#memory). The pipeline has four stages: **extraction** (identifying memory-worthy content), **categorization** (classifying into memory types), **entity resolution** (linking entity mentions), and **storage** (writing to vector and graph stores). See [Memory Lifecycle](/concepts/memories-and-context) and the [Ingestion SDK](/sdk/ingestion). *** ### Instance An isolated memory store for a single AI agent, belonging to a [client](#client). Each instance has its own [Memory Architecture](#maca-memory-architecture-configuration) (auto-generated by Synap from your use-case file), authentication credentials, and usage metrics. Instances are identified by `inst_`. See [Clients and Instances](/concepts/memory-scopes#clients-and-instances) and [Managing Instances](/dashboard/overview#managing-instances). *** ### MACA (Memory Architecture Configuration) The per-instance configuration that controls how Synap processes, stores, and retrieves memories. Synap generates it automatically from the [use-case file](/concepts/memory-architecture#the-use-case-file) you upload at Instance creation; you do not author it by hand. To change behavior, re-upload an updated use-case file. See [Customized Memory Architectures](/concepts/memory-architecture). *** ### Memory A unit of structured knowledge extracted from a document during [ingestion](#ingestion). Memories are typed as [facts](#fact), [preferences](#preference), [episodes](#episode), [emotions](#emotion), or [temporal events](#temporal-event). Each memory has a [confidence score](#confidence-score), [scope](#scope), and linked [entities](#entity-resolution). See [Memory Lifecycle](/concepts/memories-and-context). *** ### Memory Category The classification of a [memory](#memory) into one of the supported types: `fact`, `preference`, `episode`, `emotion`, or `temporal_event`. Categories are determined during the categorization stage of [ingestion](#ingestion). The set of active categories is configurable in [MACA](/concepts/memory-architecture). *** ### Memory Type See [Memory Category](#memory-category). *** ### Namespace An isolated storage partition within the [vector store](#vector-store) or [graph store](#graph-store). Each [instance](#instance) gets its own namespace to ensure memory isolation. Namespace identifiers are auto-generated based on the instance ID. *** ### Preference A [memory](#memory) type representing a user's opinions, likes, dislikes, or behavioral preferences. Example: "Prefers light roast coffee, especially pour-over method." See [Memory Lifecycle](/concepts/memories-and-context). *** ### Primary Scope The most specific [scope](#scope) in the [scope chain](#scope-chain) that a memory belongs to. Determines the default visibility of the memory. A memory with `user_id` set has USER as its primary scope; with only `customer_id`, it has CUSTOMER scope. *** ### Ranking Signals The factors used to order retrieved memories by relevance during [context fetch](/sdk-reference/conversation-context/fetch). Signals include recency (newer memories ranked higher), relevance (semantic similarity to the query), and confidence (higher confidence memories ranked higher). *** ### Retention Policy Rules governing how long memories are stored and when they are evicted. Retention behavior (time-to-live and per-scope capacity) is set per [instance](#instance) by Synap from your [use-case file](/concepts/memory-architecture#the-use-case-file), with some use-cases retaining memory indefinitely. *** ### Scope A visibility boundary that determines who can access a [memory](#memory). Synap supports four scope levels: **USER** (single user), **CUSTOMER** (all users in a customer account), **CLIENT** (all users across the organization), and **WORLD** (global). See [Scopes](/concepts/memory-scopes). *** ### Scope Chain The hierarchical ordering of [scopes](#scope) from narrowest to broadest: **USER** > **CUSTOMER** > **CLIENT** > **WORLD**. During [context fetch](/sdk-reference/conversation-context/fetch), Synap searches from the narrowest applicable scope outward, prioritizing more specific memories. See [Scopes](/concepts/memory-scopes). *** ### Temporal Event A [memory](#memory) type representing a time-bound event with specific dates, deadlines, or scheduled occurrences. Example: "User has a dentist appointment on January 20th at 2pm." See [Memory Lifecycle](/concepts/memories-and-context). *** ### Vector Store One of two storage backends used by Synap. The vector store indexes memory embeddings for fast similarity search. See [Storage Engines](/concepts/how-ingestion-works). *** ### Webhook An HTTP callback that delivers real-time event notifications to your application. Webhooks are not yet shipped. See [Webhooks](/dashboard/webhooks) for status. # Performance & Limits Source: https://docs.maximem.ai/resources/performance-limits How to think about latency, throughput, and limits in Synap. Specific numbers come from your own measurements in Dashboard → Usage. Synap publishes very few hard numbers. The two below are the ones the SDK actually enforces. Everything else (latency, throughput, rate-limit ceilings) depends on your Instance, your use-case, and your workload, and is best read off the Dashboard as you run. ## SDK timeouts The SDK ships with two default timeouts, one per retrieval mode: | Mode | Default timeout | | ---------- | --------------- | | `fast` | `8000` ms | | `accurate` | `30000` ms | Both are configurable per call via `SDKConfig.timeouts`. ## Retrieval modes `sdk.conversation.context.fetch()` accepts `mode="fast"` (default) or `mode="accurate"`. * **`fast`**: lower-latency path. Queries the vector store and the graph store. * **`accurate`**: higher-quality path. Queries the vector store and the graph store, and additionally runs LLM subquery decomposition and reranking on the candidate set. Both modes pull from the same underlying memory; `accurate` simply spends more compute to widen and re-order the result set. On either mode, passing `precision_level="medium"` further reduces response time by skipping an additional relevance-refinement pass, trading some precision. Recall isn't impacted, since the same candidate memories are searched, but outputs are less precisely filtered. ## Where to see live numbers Per-Instance latency, request volume, and rate-limit headroom are visible in **Dashboard → Usage**. Use that view for capacity planning rather than any number quoted in docs. Your workload is the source of truth. Document/message size and rate ceilings are likewise per-Instance rather than fixed platform constants, and are read off the same **Dashboard → Usage** view. If you need a ceiling raised, contact **[support@maximem.ai](mailto:support@maximem.ai)**. ## Behavior under load * The SDK auto-retries transient errors (rate limits, service-unavailable) using `RetryPolicy` with exponential backoff. You generally don't need to wrap calls in your own retry loop. * When retries are exhausted on a rate-limited call, the SDK raises `RateLimitError`. Catch it to fall back gracefully or surface a user-visible error. * If you consistently hit `RateLimitError` for an Instance, your per-Instance ceiling is tuneable. Contact **[support@maximem.ai](mailto:support@maximem.ai)**. ## Status and monitoring * **Status page**: [synap.maximem.ai/status](https://synap.maximem.ai/status) * **Per-Instance metrics**: Dashboard → Usage # Sensitive Data FAQ Source: https://docs.maximem.ai/resources/pii-faq The questions engineering teams and security reviewers actually ask about Synap's sensitive data protection, sometimes called PII protection. Two halves: what it does to your application, and what it means for a security review. For the concepts behind these answers, read [Sensitive Data Protection](/guides/pii-protection). For the claims a security review needs in one place, read [Security and Trust](/resources/security-trust). ## From your engineering team No, unless you choose it. For any field type you protect, your own API keys receive the real value by default, so the text your application reads back is identical to what it reads today. Request and response shapes do not change. Only two settings change what you get back, and both are yours to pick: marking a field type **Do not store it**, and restricting one of your own API keys. A third, **Protect from everyone**, exists behind the advanced link and does exactly what its name says, including to you. No. There is no new call to make, no parameter to pass, and no version to pin for this. Everything is configured in the Dashboard. If you deliberately restrict a key so it receives placeholders, your code has to be ready to see a placeholder in the text. See [Aliases](/concepts/aliases#what-your-application-receives). Placeholders are stable: the same value always becomes the same placeholder, in the same form, however it was spelled. So deduplication still merges duplicates, corrections still supersede what they correct, and entity identity in the graph still resolves to one thing rather than two. Every change to this feature is held to the same benchmark gate Synap uses for retrieval quality, with a hard limit on how far any individual benchmark may move. Ingestion picks up a detection pass, and ingestion is asynchronous, so it does not sit in your request path. Reads pick up a small step to put real values back before the response is assembled, which is short next to a fetch that is already dominated by retrieval. If a fetch returns no placeholders at all, which is the case for every account that has not configured anything, that step is skipped entirely. Yes, and this is a hard requirement in the design rather than an optimisation. Your query goes through the same detection step your content did. A search for `9876543210` is turned into the same placeholder that was stored, so it matches the memory holding it. A value Synap has never seen leaves the query untouched, which is correct: no memory holds that value either. They stay exactly as they are, in plain text. Policy applies forward only, from the moment you approve it. Every record carries the policy version that was in force when it was created, so you can always tell which memories were written under which rules. Re-processing older memories under a new policy is possible and is a job we run for you on request; it costs real time and money, so it is not automatic. Yes. Set the categories back and approve, and new memories stop being protected. Memories already protected stay that way, and stay readable: your application keeps receiving the real values for them, because the placeholders still resolve. We do not promise perfect detection, and [Security and Trust](/resources/security-trust#what-we-promise-and-what-we-do-not) states exactly what we do and do not promise. What you get instead of a promise is visibility. Watch mode runs for every account from day one and shows you what is being found in your own traffic, by field type, with counts. A gap is a number you can see rather than something you find out about later. The publication of measured precision and recall per field type is part of the same idea. For the settings most people pick, it costs you nothing visible. A false positive is protected on the way in and revealed straight back to you on the way out, so the text your application receives is unchanged. It shows up as a count on your dashboard. If a field type is producing noise, move it down a level or set it to **Keep it**. Describe it on the **Your own field types** tab with a name, a category, and a few real examples. No deploy and no ticket. One caveat that matters: a field type you describe is currently detected in the **Try it** test box only, and is not yet applied to live ingestion. See [Your own field types](/guides/custom-field-types) for the full picture, and contact us if you need a custom format protected in production now. Yes. The **Try it** tab takes sample text and shows exactly what would be detected and what your current settings would do with it at each destination. Nothing you paste there is stored: not the text, not the values, not a counter, not a log line. The result carries positions and field type names rather than the matched text, so it is safe to paste into a ticket. Yes. Issue that tool a restricted API key. A key's grant can only ever narrow what your policy already allows, never widen it, so there is no way to accidentally hand a key more access than you intended. See [restricting an API key](/dashboard/pii-and-data-controls#restricting-an-api-key). It depends on whether you have a policy in force. In watch mode, a detection failure is counted and the document proceeds. Watch mode changes nothing about your data, so a failure there must not be able to break your ingestion. Under an approved policy, a document that cannot be protected is refused rather than stored. Continuing anyway would send a value to a model you told us to keep it from, which is the one outcome this feature exists to prevent. *** ## From a security or legal reviewer Protected values are held in one place, encrypted under a key that belongs to your account alone. The plain value is not written to the memory store, the vector store, the graph, or our logs. This applies to field types you have protected. A field type you set to **Keep it**, and anything ingested before you approved a policy, is ordinary content. The application can, using a master secret held in the deployment environment. Synap staff cannot read a protected value without a deliberate reveal action, which requires a named person, a written reason, and writes an audit entry that you can read on your own dashboard the same day. There is no path that shows a value to our staff without leaving that record, and an impersonated session cannot reveal at all. The reason field is itself screened, and a reveal whose reason contains what looks like a sensitive value is refused along with the reason. We state the limits of this honestly: see [what we do not protect against](/resources/security-trust#what-we-do-not-protect-against). Yes. Erasure destroys the encrypted form of every protected value held for that person, which makes every reference to them permanently unreadable everywhere at once, including in database backups we cannot go back and rewrite. Removing an entire account is a single operation with the same effect across all of its data. One thing is deliberately kept and is named in the preview before anything runs: values shared with other people, because deleting those would break other people's memories. See [Erasing a person](/guides/erasure). Yes. During ingestion, content is sent to the model provider that performs extraction. Under a policy, protected values reach that provider as placeholders rather than real values. Floor field types never reach it at all. Whether a given provider retains or trains on data sent to it is a contractual question and is answered in our Data Processing Agreement, not here. Request it from **[legal@maximem.ai](mailto:legal@maximem.ai)**. Protected values are encrypted individually, under a key held per account, on top of the volume-level encryption described in [Security and Trust](/resources/security-trust#encryption). Memory text itself is not field-encrypted, and that is a deliberate choice rather than an omission: encrypting it would make semantic search impossible, and semantic search is the product. Protection for memory content comes from the sensitive values not being in it. We publish it per field type rather than claiming a single number, and we do not sell two different mechanisms at the same confidence. Field types with a check digit, such as Aadhaar, GSTIN, and card numbers, are close to certain: a value that fails its checksum is rejected rather than guessed at. Field types found by shape are strong but weaker than that. Names and street addresses are found statistically, with both misses and false alarms, and **no detector for either is switched on today**. One exists, we measured it, and it is off because the measurement says it is not good enough: it finds two thirds of names in written text, under half in a voice transcript, and none of the 8 street lines in the corpus at all. The full table is published rather than summarised. The current measured figures for both, and the size of the corpora they were measured on, are in [Security and Trust](/resources/security-trust#sensitive-data-detection-measured). Yes, append only, kept for one year. Each entry records the field type, the action taken, the placeholder involved, the scope, the time, who acted and whether they were your person or ours, the reason they gave, and the policy version in force. It never records the value, and never records the text around it. You read it on your own **Activity** tab and export it as a CSV file. Your policy history shows every version, who approved it, and when. Stated plainly, because a reviewer will find these anyway: * **Names and street addresses are not detected.** A detector exists and is switched off in every environment, because its measured precision is under the bar we set for it. Do not plan around names being protected. * **Health, origin and belief data are not detected.** Those categories accept a setting; no field type ships in them. * **Biometric detection is text only**, and only for an encoded template that appears next to a word labelling it. An unlabelled binary attachment is not covered. * **Images, audio, and scanned documents are not covered.** * **Data stored before this shipped is plain text and stays that way.** * **We do not protect against an attacker who holds our application environment and our database at the same time.** The full list is on [Security and Trust](/resources/security-trust#what-we-promise-and-what-we-do-not). ## Next steps The categories, the settings, and the floor. Everything an enterprise security review asks about, in one place. Set a policy, test it, and approve it. Deleting someone, and what becomes unreadable. # Pricing & Credits Source: https://docs.maximem.ai/resources/pricing How Synap bills: every plan includes a monthly allotment of credits, and memory operations consume credits. Start free, scale as your usage grows, and bring your own LLM key on paid plans. Synap bills on **credits**. Every plan includes a monthly credit allotment, and the memory operations your app performs draw down that balance. You can start **free**, and current plans and prices are always on the [Dashboard → Manage billing](https://synap.maximem.ai). Prices below reflect current launch/promotional pricing and can change. The [Dashboard](https://synap.maximem.ai) is the source of truth for what your plan costs today. This page explains the **model** so the numbers make sense. ## What's a credit? A credit is Synap's unit of usage. Memory operations consume credits as they run: * **Ingestion**: `memories.create`, `memories.batch_create`, and `conversation.record_message` * **Retrieval**: `*.context.fetch` (and `accurate` mode costs more than `fast`, since it runs LLM subquery decomposition + reranking) * **Compaction**: `conversation.context.compact` * **Real-time listening**: each concurrent streaming session (`instance.listen`) Two SDK calls let you stay ahead of your balance: ```python theme={null} # Estimate what an operation will cost before you run it estimate = await sdk.credits.estimate(...) # Check how many credits you have left balance = await sdk.credits.get_balance() ``` See [`credits.estimate`](/sdk-reference/credits/estimate), [`credits.get_balance`](/sdk-reference/credits/get-balance), [`credits.get_ledger`](/sdk-reference/credits/get-ledger), and [`credits.redeem`](/sdk-reference/credits/redeem) for the full reference. When you run out of credits, the behavior depends on your plan (see [Overage](#overage--hard-caps) below): paid plans bill overage automatically, while the free Trial is hard-capped and raises [`InsufficientCreditsError`](/sdk/error-handling). Catch it to surface a top-up prompt. ## Plans | Plan | Price | Credits / month | Projects | Concurrent listening | Support | Overage | Audit retention | | --------------------------------- | ---------------- | --------------- | --------- | -------------------- | -------------- | ------------------- | --------------- | | **Trial** (indie devs) | **Free** | 5,000 | 1 | 2 | Docs | Hard cap (none) | 7-day | | **Starter** (going to production) | from **\$19/mo** | 25,000 | 3 | 10 | Email (48h) | \$2.00 / 1K credits | 30-day | | **Pro** (growing startups) | **\$249/mo** | 150,000 | 10 | 50 | Email (24h) | \$1.75 / 1K credits | 90-day | | **Scale** (scaled apps) | **\$999/mo** | 750,000 | 50 | 500 | Slack (4h SLA) | \$1.50 / 1K credits | 180-day | | **Enterprise** | Custom | Volume-based | Unlimited | Unlimited | Dedicated CSM | Negotiated | Custom | **Annual billing saves \~17%** versus paying monthly. Project and listening-session counts are indicative. For current, exact pricing and to change plans, use the [Dashboard](https://synap.maximem.ai); for volume pricing, [contact sales](https://maximem.ai). ## Overage & hard caps * **Trial** is **hard-capped**: once the monthly credits are spent, operations stop (and the SDK raises `InsufficientCreditsError`) until the next cycle. No surprise charges. * **Paid plans** allow **overage**: usage beyond your monthly allotment is billed per 1,000 credits at the rate shown above (lower per-credit as you move up tiers). ## BYOK: bring your own LLM key On **Pro and above**, you can connect your own LLM provider key (**BYOK**). Synap then uses your provider for the model calls in its pipeline (extraction, accurate-mode reranking), so that usage bills to your provider account instead of consuming Synap credits, useful for controlling cost and data flow at scale. Scale supports **multi-provider** BYOK; Enterprise supports **any provider**. ## Managing your plan Plans, invoices, current pricing, and BYOK configuration all live in the **[Dashboard](https://synap.maximem.ai) → Manage billing**. Use [`credits.get_ledger`](/sdk-reference/credits/get-ledger) to audit consumption programmatically. Estimate an operation's cost before you run it. Timeouts and the limits that are visible in Dashboard → Usage. # Security & Trust Source: https://docs.maximem.ai/resources/security-trust How Synap handles encryption, isolation, data residency, deletion, and compliance. This page covers everything an enterprise security review will ask about. If your specific question isn't answered here, email **[security@maximem.ai](mailto:security@maximem.ai)**. ## Encryption | Layer | Mechanism | | ------------------------- | ------------------------------------------------------------------------------------------------------- | | In transit | TLS 1.3 on every SDK connection. | | At rest: application data | AES-256 at rest for vector store and graph store. | | At rest: credentials | API keys are hashed with SHA-256 before storage. Plaintext keys are never persisted on the server side. | | Backups | Encrypted with separate keys; backup-restore is audited. | All traffic between SDK and Synap Cloud is verified against pinned certificates. The SDK never falls back to plaintext if TLS negotiation fails. ## Sensitive data protection Synap detects sensitive values in the content you send and applies a policy you set, per category, deciding what reaches the model, what we store, what your application receives, and what our staff can see. The full explanation is in [Sensitive Data Protection](/guides/pii-protection); this section is the part a security review needs. Protected values are held in one place, encrypted individually under a key that belongs to your account alone. That key is itself held encrypted, and removing it makes every value it protected unreadable at once. Memory text is not field-encrypted, deliberately: encrypting it would make semantic search impossible, and semantic search is the product. Protection for memory content comes from the sensitive values not being in it. ### Sensitive data detection: measured We publish accuracy per field type rather than claiming one number, because the detection mechanisms are not equally strong and selling them at the same confidence would be dishonest. Measured with detector version `1.2.0` against a labelled corpus of **64 cases** that we maintain ourselves: | Field type | How it is found | Cases | Recall | Precision | | ---------------------- | ------------------------- | ----- | ------ | --------- | | Aadhaar number | check digit | 10 | 1.00 | 1.00 | | Card number | check digit | 4 | 1.00 | 1.00 | | GSTIN | check digit | 1 | 1.00 | 1.00 | | PAN | fixed format | 2 | 1.00 | 1.00 | | Passport number | fixed format | 1 | 1.00 | 1.00 | | Voter ID | fixed format | 1 | 1.00 | 1.00 | | Driving licence | fixed format | 1 | 1.00 | 1.00 | | Vehicle registration | fixed format | 1 | 1.00 | 1.00 | | IFSC code | fixed format | 1 | 1.00 | 1.00 | | UPI ID | fixed format | 1 | 1.00 | 1.00 | | Email address | fixed format | 2 | 1.00 | 1.00 | | Phone number | fixed format | 4 | 1.00 | 1.00 | | IP address | fixed format | 1 | 1.00 | 1.00 | | MAC address | fixed format | 1 | 1.00 | 1.00 | | API key or secret code | fixed format | 2 | 1.00 | 1.00 | | Private key | fixed format | 1 | 1.00 | 1.00 | | Bank account number | needs context | 1 | 1.00 | 1.00 | | Possible card number | needs context | 2 | 1.00 | 1.00 | | Card security code | needs context | 1 | 1.00 | 1.00 | | Card PIN | needs context | 1 | 1.00 | 1.00 | | Password | needs context | 1 | 1.00 | 1.00 | | PIN code | needs context | 2 | 1.00 | 1.00 | | Raw biometric data | labelled template in text | 3 | 1.00 | 1.00 | Zero false alarms across the corpus. Every recorded minimum was met. Read these numbers for what they are. They are measured against a corpus we wrote, not against your production traffic, and a corpus of this size proves that each detector works on the cases we know about rather than that no case exists which it misses. They are the bar we hold ourselves to and re-run against, and they are published rather than asserted. Field types found by check digit carry a genuinely stronger promise than the rest: a value that fails its checksum is rejected outright rather than guessed at. ### Names and addresses: measured, and switched off Every field type in the table above is found by a pattern or a check digit. Names and street addresses cannot be, so they need a statistical model, and a statistical model has to be judged on numbers rather than on the fact that it exists. We built one and measured it against a second corpus of **103 passages** carrying 95 labelled names and addresses, plus 37 passages carrying none. **It is switched off in every environment**, and this table is the reason. We are publishing it because a reviewer deciding whether to trust us is better served by a measured failure than by silence. | Field type | As written | Lowercased | Transcript | | ------------------------- | ---------- | ---------- | ---------- | | Person name, recall | 65.5% | 47.3% | 45.5% | | Person name, precision | 75.0% | 81.2% | 83.3% | | Street address, recall | 45.0% | 27.5% | 22.5% | | Street address, precision | 81.8% | 78.6% | 81.8% | The three columns are one body of text under three mechanical transforms, so the only variable between them is capitalisation and punctuation. The transcript column is what speech recognition actually emits, so a voice product would get the right-hand column: under half the names and under a quarter of the addresses. Three specific findings sit behind those averages, and each matters more than the average does. * **Street address does not find street lines.** Recall on an actual house number and street is **0 of 8, in all three columns**. What the detector finds is cities and localities. The part of an address most people mean when they ask for addresses to be protected is the part it is worst at. * **Place names are mistyped along regional lines.** Indian place names come back typed as people while Western ones come back typed as places. The text would still be covered, so this is not a leak, but a policy that sets Identity and Location differently would apply the wrong one, and you would have no way to see why. * **Precision is under our own floor.** We require 0.80 precision from a statistical detector before it may be enabled. Person name as written measures 0.75. That is the specific line this detector fails. If names or addresses are load-bearing for your compliance position, this feature does not currently meet it, and we would rather you know that from this page than discover it in an audit. ### What we promise, and what we do not Taken without softening. **We promise** that the field types on the floor list are never stored, for every account, whatever the settings say. That list is: full card number, card security code, card PIN, passwords, API keys and other secret codes, private keys, and raw biometric data. No account setting, no preset, and no support request changes it. **We promise** that a field type you mark **Do not store it** does not come to rest in our memory store, our vector store, our graph, or our logs, subject to the detection limits below. What is kept is the fact that a value was given, not the value. **We promise** that deleting a person or an entire account makes their protected values permanently unreadable everywhere, including in database backups we cannot go back and rewrite. See [Erasing a person](/guides/erasure). **We promise** that our staff cannot read a protected value without leaving a record. A reveal requires a named person and a written reason, writes one audit entry per value, and appears on your own **Activity** tab the same day. An impersonated session cannot reveal at all. **We do not promise perfect detection.** Pattern-based fields with a check digit are close to certain. Names and addresses are found statistically, with both misses and false alarms, and we publish measured numbers per field type rather than a claim. Specifically today: * **`person_name` and `street_address` have no detector running.** Names and street addresses are not detected for any account. A statistical detector for them exists and is switched off in every environment, because we measured it and it is not good enough to turn on. The numbers are published below rather than kept internal. Do not plan around names being protected. * **Health, and origin and belief data are not detected.** Those two categories accept a setting and it is stored and applied, but no field type ships in either, so nothing in them is currently found. * **Biometric detection is text only.** Synap detects an encoded biometric template when it appears in text alongside a word that labels it, such as "fingerprint template", "iris minutiae", or an ISO 19794 header. **An unlabelled binary attachment is not covered**, and neither is an unlabelled encoded blob, which is indistinguishable from any other encoded blob. **We do not promise that data already stored is protected.** Everything held before this shipped is plain text and stays that way until a re-processing job runs, which costs real time and money. **We do not cover images, audio, or scanned documents** in this version. Detection runs over text. ### What we do not protect against We do not protect against an attacker who holds both our application environment and our database at the same time. The key that opens account keys is held in the deployment environment, so anyone who can read that environment and also read the database can read protected values. A managed key service would close that gap by moving the key into hardware the application can never read, and we are not using one. We state this rather than leaving a reviewer to work it out. ## Isolation model Synap enforces isolation at three boundaries: 1. **Instance isolation**: each Instance has its own logical storage namespace across vector and graph stores. Memories from one Instance are never queryable from another, even by accident, because every query is scoped to an Instance ID resolved server-side from your API key. 2. **Scope isolation**: within an Instance, every memory is tagged with USER → CUSTOMER → CLIENT → WORLD scope. A user-scoped retrieval never returns memories from a different user, regardless of similarity. See [Memory Scopes](/concepts/memory-scopes). 3. **Network isolation**: Synap Cloud is network-isolated per region. Customer data never crosses regions. ## Data residency | Region | Location | Status | | ------------- | --------- | --------------------------------------------------------------- | | US East | Virginia | Available | | EU Central | Frankfurt | Available | | Other regions | n/a | On request, contact [sales@maximem.ai](mailto:sales@maximem.ai) | You pick the region at Client creation. Memories stay in that region for their entire lifecycle, including backups and replicas. Cross-region replication is not done automatically. ## Deployment options | Option | Availability | | --------------------------- | ---------------------------------- | | Synap Cloud (fully managed) | All plans | | Private / VPC deployment | Enterprise | | Self-hosted / on-premise | Enterprise (self-hosted licensing) | Synap is offered as a fully managed cloud service, and, on Enterprise, as a private/VPC deployment or a self-hosted/on-premise install for teams with strict data-residency or air-gap requirements. The open-source SDK ([github.com/maximem-ai/maximem\_synap\_sdk](https://github.com/maximem-ai/maximem_synap_sdk)) is available independently of the managed platform. ## Enterprise access controls | Control | Availability | | ---------------------------------------------- | ------------------------------------------------------------------------------------ | | SSO / SAML | Enterprise | | Role-based access control (RBAC) | Enterprise (configurable) | | Bring-your-own-key (BYOK) for the LLM pipeline | Pro and above (see [Pricing → BYOK](/resources/pricing#byok-bring-your-own-llm-key)) | | Custom SLAs & dedicated customer success | Enterprise | Contact **[sales@maximem.ai](mailto:sales@maximem.ai)** to enable SSO/SAML, RBAC, VPC/private deployment, or self-hosting. ## Deletion guarantees Synap supports per-memory, per-conversation, per-user, per-customer, and per-instance deletion. * **Soft delete** (default): the memory is removed from retrieval results immediately and purged from active stores within 24 hours. The deletion is logged to the audit trail. * **Hard delete** (on request): removes the memory from backups as well, within 30 days. Use this for GDPR Right-to-be-Forgotten and CCPA Right-to-Delete requests. Contact **[privacy@maximem.ai](mailto:privacy@maximem.ai)** to initiate. Deletion cascades through the entity graph: when a user is deleted, any entities exclusively referenced by their memories are also removed. Entities co-referenced by other users (e.g., a shared product entity) are retained. **Erasure** is a third and stronger option for accounts using [sensitive data protection](/guides/pii-protection). Protected values are encrypted under a key held per account; erasing a person destroys the encrypted form of every value held for them, which makes those values unreadable everywhere at the same moment, including in backups already taken. There is no waiting period and nothing to scan for, because there is no longer anything that could be read. Removing an entire account has the same effect across all of its data in one operation. Erasure covers protected values. It does not reach data ingested before you approved a policy, which was never protected and is ordinary content. Values shared with other people are deliberately kept and are named in the preview before anything runs. See [Erasing a person](/guides/erasure) for the full behaviour and how to request it. ## Compliance posture | Framework | Status | | ------------- | ----------------------------------------------------------------------------------------- | | SOC 2 Type II | In progress, target audit completion Q3 2026 | | GDPR | Compliant. DPA available on request from **[legal@maximem.ai](mailto:legal@maximem.ai)**. | | CCPA | Compliant. | | HIPAA | Not currently certified. Do not send PHI to Synap. | | ISO 27001 | On roadmap (2026). | Synap maintains a vendor security questionnaire (CAIQ + Lite SIG). Request via **[security@maximem.ai](mailto:security@maximem.ai)**. ## Sub-processors Synap maintains a public sub-processor list. See the Synap [sub-processor disclosure](https://maximem.ai/subprocessors) for the current list. ## Audit logs Every dashboard and SDK action is logged with `correlation_id`, principal (user / API key), timestamp, action, and resource. Audit logs are retained for 90 days by default and can be exported from the Dashboard (on Enterprise plans). ## Reporting a vulnerability Email **[security@maximem.ai](mailto:security@maximem.ai)** with reproduction steps. We acknowledge within 24 hours and aim to issue a patch within 7 days for critical vulnerabilities. We do not currently run a public bug bounty but will reward responsibly-disclosed issues. ## What we do NOT do Stated explicitly so there are no surprises in your security review: * **We do not train models on customer data.** * **We do not share customer data with sub-processors beyond what is listed above.** * **We do not allow Synap engineers to query customer data without a documented support ticket from the customer.** Where that access happens through our own tools it leaves a record: revealing a protected value requires a named person and a written reason, writes one audit entry per value, and appears on your Activity tab. Direct database access is restricted to a small number of engineers by credential, and is not separately recorded query by query. We would rather tell you which of those two things you are relying on than describe both as the same control. * **We do not retain deleted memories.** Once a hard delete completes, the data is gone, including from backups within the 30-day window. # Support Source: https://docs.maximem.ai/resources/support Whether you are stuck on an integration, have a question about the API, or need to report an issue, we are here to help. ## Support Channels Report bugs, request features, and track known issues. Search existing issues before opening a new one. Your question may already have an answer. Join our community for real-time help, discussions, and announcements. The `#help` channel is monitored by the Synap team and experienced community members. For private issues, account questions, or enterprise support, email us at **[support@maximem.ai](mailto:support@maximem.ai)**. We respond within one business day. Check real-time system status, view incident history, and subscribe to outage notifications. Bookmark this page for quick access during incidents. ### Follow us Release notes, product updates, and engineering posts. Reply on threads or DM us. Company news, hiring, and longer-form posts about agent memory and architecture. Live chat with the team and community in `#help`, `#announcements`, and `#feedback`. ## When Contacting Support To help us resolve your issue as quickly as possible, please include the following information: Every SDK response includes a correlation ID. Capture it from the response (or from the error you caught) and include it in support tickets. This ID traces the request through the entire Synap processing pipeline and is the single most useful piece of information for debugging. ``` syn_abc123_1706123456789_x7k9m2 ``` Include the instance ID where the issue occurred. Format: `inst_`. ``` inst_f1e2d3c4b5a69078 ``` Provide the approximate time when the issue occurred, including your timezone. ISO 8601 format is preferred. ``` 2025-01-15T14:32:00Z ``` Include the exception class name and message printed by the SDK. ``` InvalidInputError: instance 'inst_a1b2c3d4e5f67890' not found ``` If possible, describe the sequence of SDK calls that trigger the issue. Include arguments (with API keys redacted). Never share your API key or signing secret in support requests, GitHub issues, or Discord messages. Redact them before posting. If you believe a credential has been compromised, revoke it immediately from the Dashboard. ## SLA Tiers Response times vary by support tier: | Tier | First Response | Resolution Target | Channels | | ------------------------- | -------------- | ----------------- | ------------------------------------- | | **Community** (Free) | Best effort | Best effort | GitHub Issues, Discord | | **Standard** (Pro) | 1 business day | 3 business days | Email, GitHub Issues, Discord | | **Priority** (Enterprise) | 4 hours | 1 business day | Dedicated Slack channel, email, phone | | **Critical** (Enterprise) | 1 hour | 4 hours | 24/7 on-call, dedicated Slack channel | SLA timers run during business hours (9am-6pm PT, Monday through Friday) for Standard and Priority tiers. Critical tier SLAs apply 24/7 including holidays. ## Reporting Security Issues If you discover a security vulnerability, please do **not** report it via public channels (GitHub Issues, Discord). Instead: 1. Email **[security@maximem.ai](mailto:security@maximem.ai)** with a detailed description of the vulnerability 2. Include steps to reproduce the issue 3. Allow us a reasonable timeframe to address the issue before public disclosure We take security reports seriously and will acknowledge receipt within 24 hours. ## Useful Resources Before contacting support, these resources may help resolve your issue: Complete list of error codes with descriptions, common causes, and resolution steps. Answers to the most common questions about Synap, the SDK, memory management, and billing. Authentication, configuration, and common SDK patterns. Recent changes, bug fixes, and new features. Check here first if something stopped working after an update. ## Feedback We actively use feedback to prioritize features and improve the platform. Share your thoughts through any of these channels: * **Feature requests**: Open a GitHub Issue with the `enhancement` label * **Documentation feedback**: Use the "Was this page helpful?" widget at the bottom of any docs page * **General feedback**: Post in the `#feedback` channel on Discord or email [product@maximem.ai](mailto:product@maximem.ai) * **Integrations**: Let us know which frameworks and languages you would like Synap to support natively The best feature requests include a description of the problem you are trying to solve, not just the solution you want. Understanding the "why" helps us design better features for everyone. # cache.clear Source: https://docs.maximem.ai/sdk-reference/cache/clear Clear all locally-cached SDK data, including fetched context bundles and anticipation entries. ```python Python theme={null} sdk.cache.clear() ``` ```typescript TypeScript theme={null} sdk.cache.clear() ``` `clear()` wipes the SDK's in-process cache: every scope, every user, every customer. This affects only the local cache layer; durable memory stored on the Synap platform is untouched. The cache repopulates lazily on subsequent `fetch()` calls. Useful in tests, in long-running processes that need to drop stale entries, or when you want to force a fresh round-trip to the platform. This is a synchronous method, no `await`. ### Parameters This method takes no parameters. ### Returns Returns `None`. ### Example ```python Python theme={null} from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK(api_key="synap_your_key_here") # Drop every locally-cached entry; the next fetch() will hit the platform. sdk.cache.clear() ``` ```javascript JavaScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); // Drop every locally-cached entry; the next fetch() will hit the platform. sdk.cache.clear(); ``` ```typescript TypeScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); // Drop every locally-cached entry; the next fetch() will hit the platform. sdk.cache.clear(); ``` ### Raises This method does not raise SDK errors. See [Error Codes](/sdk-reference/errors) for the full SDK exception hierarchy. ### See also * [cache.clear\_user](/sdk-reference/cache/clear-user): drop one user's cached data (GDPR). * [cache.clear\_customer](/sdk-reference/cache/clear-customer): drop one customer's cached data. * [cache.stats](/sdk-reference/cache/stats): inspect hit rate and entry counts. # cache.clear_customer Source: https://docs.maximem.ai/sdk-reference/cache/clear-customer Clear all locally-cached SDK data for a single customer (B2B tenant). **B2B only.** Customer scope exists only on a B2B instance, that is one whose `user_context_isolation` is `strict`. A B2C instance (`user_context_isolation: "equals_customer"`) does not accept `customer_id` on any call and rejects [`customer.context.fetch`](/sdk-reference/context/customer-fetch) with HTTP 400, so it has no customer-scoped entries to clear. On B2C, use [`cache.clear_user`](/sdk-reference/cache/clear-user) instead. Call `GET /api/v1/auth/whoami` to see which mode your instance is in. ```python Python theme={null} sdk.cache.clear_customer(customer_id) ``` ```typescript TypeScript theme={null} sdk.cache.clear_customer(customerId: string) ``` `clear_customer()` removes every cache entry whose customer scope matches `customer_id`, including any nested user-scoped entries that belong to that customer. Use it when offboarding a B2B tenant, when test fixtures need a clean slate, or when a customer's data has drifted and you want to force a refresh from the platform. This is a synchronous method, no `await`. ### Parameters The external customer identifier whose cached data should be dropped. Must match the `customer_id` you originally passed when populating the cache. Customer scoping is a B2B concern only. See [B2C vs B2B](/concepts/memory-scopes#b2c-vs-b2b-which-scopes-apply-to-you). ### Returns Returns `None`. ### Example ```python Python theme={null} from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK(api_key="synap_your_key_here") # Customer is being offboarded; drop their cached context. sdk.cache.clear_customer("cust_456") ``` ```javascript JavaScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); // Customer is being offboarded; drop their cached context. sdk.cache.clear_customer('cust_456'); ``` ```typescript TypeScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); // Customer is being offboarded; drop their cached context. sdk.cache.clear_customer('cust_456'); ``` ### Raises This method does not raise SDK errors. See [Error Codes](/sdk-reference/errors) for the full SDK exception hierarchy. ### See also * [cache.clear](/sdk-reference/cache/clear): drop the entire local cache. * [cache.clear\_user](/sdk-reference/cache/clear-user): drop one user's cached data (GDPR). * [cache.stats](/sdk-reference/cache/stats): inspect hit rate and entry counts. # cache.clear_user Source: https://docs.maximem.ai/sdk-reference/cache/clear-user Clear all locally-cached SDK data for a single user. Useful for GDPR right-to-be-forgotten flows. ```python Python theme={null} sdk.cache.clear_user(user_id) ``` ```typescript TypeScript theme={null} sdk.cache.clear_user(userId: string) ``` `clear_user()` removes every cache entry whose user scope matches `user_id`, leaving other users' cached data untouched. Pair this with the durable memory deletion APIs to honor right-to-be-forgotten requests end-to-end: this call handles the SDK's local cache, while platform-side deletion ensures the user's stored memories are removed from Synap itself. This is a synchronous method, no `await`. ### Parameters The external user identifier whose cached data should be dropped. Must match the `user_id` you originally passed to `fetch()`, `memories.create()`, or other scope-aware calls. ### Returns Returns `None`. ### Example ```python Python theme={null} from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK(api_key="synap_your_key_here") # Part of a GDPR right-to-be-forgotten workflow. sdk.cache.clear_user("user_789") ``` ```javascript JavaScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); // Part of a GDPR right-to-be-forgotten workflow. sdk.cache.clear_user('user_789'); ``` ```typescript TypeScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); // Part of a GDPR right-to-be-forgotten workflow. sdk.cache.clear_user('user_789'); ``` ### Raises This method does not raise SDK errors. See [Error Codes](/sdk-reference/errors) for the full SDK exception hierarchy. ### See also * [cache.clear](/sdk-reference/cache/clear): drop the entire local cache. * [cache.clear\_customer](/sdk-reference/cache/clear-customer): drop one customer's cached data. * [cache.stats](/sdk-reference/cache/stats): inspect hit rate and entry counts. # cache.stats Source: https://docs.maximem.ai/sdk-reference/cache/stats Return a snapshot of local cache state: entry counts, storage size, and per-backend breakdown. ```python Python theme={null} sdk.cache.stats() ``` ```typescript TypeScript theme={null} sdk.cache.stats() ``` `stats()` returns a dict describing the current state of the SDK's local cache: how many entries it holds across all backends, total storage footprint, and a per-backend breakdown. Use it to verify your cache configuration is taking effect, to monitor cache size in production, or to debug stale-data issues during development. This is a synchronous method, no `await`. If the cache manager is not initialized (e.g., caching disabled in `SDKConfig`), the returned dict is `{"enabled": False}`. ### Parameters This method takes no parameters. **The two SDKs report different shapes.** Python returns the backend's own counters (`enabled`, `total_entries`, `total_bytes`, `backends`). The JavaScript cache is in memory and reports `{ bundles, items }`. Read the fields for the SDK you are on rather than assuming they match. ### Returns A `dict[str, Any]` describing cache state. Whether the local cache is active. `False` indicates no other fields will be present. The client identifier the cache is scoped to. Filesystem path where cache backends store their data. Total number of entries summed across all backends. Total storage footprint in bytes, summed across all backends. Per-backend stats. Each entry has a `key` (backend identifier) plus that backend's own metrics (`entry_count`, `total_bytes`, and any backend-specific fields). ### Example ```python theme={null} from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK(api_key="synap_your_key_here") await sdk.initialize() # ... after some fetch() activity ... stats = sdk.cache.stats() if stats["enabled"]: print(f"Entries: {stats['total_entries']}") print(f"Size: {stats['total_bytes'] / 1024:.1f} KB") for backend in stats["backends"]: print(f" {backend['key']}: {backend.get('entry_count', 0)} entries") else: print("Cache disabled") ``` ### Raises This method does not raise SDK errors. See [Error Codes](/sdk-reference/errors) for the full SDK exception hierarchy. ### See also * [cache.clear](/sdk-reference/cache/clear): drop the entire local cache. * [cache.clear\_user](/sdk-reference/cache/clear-user): drop one user's cached data (GDPR). * [cache.clear\_customer](/sdk-reference/cache/clear-customer): drop one customer's cached data. # client.context.fetch Source: https://docs.maximem.ai/sdk-reference/context/client-fetch Retrieve organizational (client-scoped) context: product knowledge, documentation, and announcements visible to every user across every customer. ```python Python theme={null} await sdk.client.context.fetch( conversation_id=None, search_query=None, max_results=10, types=None, mode="fast", precision_level="high", ) ``` ```typescript TypeScript theme={null} await sdk.client.context.fetch(options?: FetchOptions) ``` Retrieve organizational context scoped to your application (client). Client-scoped memories are visible to all users across all customers. This is typically used for product knowledge, documentation, and announcements that were ingested via [bootstrap ingestion](/concepts/how-ingestion-works#bootstrap-ingestion). ### Parameters Optional conversation identifier. When provided, results are biased toward memories relevant to the active conversation. When supplied, it must be a valid UUID [registered via `record_message`](/concepts/context-end-to-end#short-term-context). One or more search queries to find relevant organizational memories. If omitted, returns the most recent and highest-confidence client-scoped memories. Maximum number of memory items to return. Defaults to `10`. Maximum `50`. Filter results to specific memory types. If omitted, all types are included. | Value | Description | | ---------------- | ------------------------------------------------------ | | `fact` | Factual information about your organization or product | | `preference` | Organizational preferences and standards | | `episode` | Significant organizational events | | `temporal_event` | Time-bound organizational events (launches, deadlines) | Retrieval mode that controls the speed-quality tradeoff: the retrieval axis (`fast` vs `accurate`) of [Retrieval Modes](/concepts/retrieval-modes). | Value | Description | | ---------- | ----------------------------------------------------------------- | | `fast` | Vector search only. Lower latency. **Default.** | | `accurate` | Full vector + graph + re-ranking. Higher quality, higher latency. | For real per-mode latency on your instance, see **Dashboard → Usage**. Controls how precisely results are filtered before they're returned. | Value | Description | | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | `high` | Results go through an additional relevance-refinement pass before being returned. **Default.** | | `medium` | Skips the refinement pass for faster responses. Recall isn't impacted (the same candidate memories are searched), but outputs are less precisely filtered. | Independent of `mode`; combine with either `fast` or `accurate`. For real latency on your instance, see **Dashboard → Usage**. ### Returns A `ContextResponse` with the following fields: Array of fact memories relevant to the query. Each includes `content`, `confidence`, `entities`, `source`, and `relevance_score`. Array of preference memories. Array of episode memories. Response metadata including `total_results`, `query_time_ms`, `tokens_used`, `scope`, and `mode`. ### Example ```python Python theme={null} from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK(api_key="synap_your_key_here") await sdk.initialize() context = await sdk.client.context.fetch( search_query=["product features", "API rate limits"], max_results=5, types=["fact"], mode="accurate", ) for fact in context.facts: print(f"[{fact.confidence:.2f}] {fact.content}") ``` ```javascript JavaScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); await sdk.initialize(); const context = await sdk.client.context.fetch({ search_query: ['product features', 'API rate limits'], max_results: 5, types: ['fact'], mode: 'accurate', }); for (const fact of context.facts ?? []) { console.log(`[${(fact.confidence ?? 0).toFixed(2)}] ${fact.content}`); } ``` ```typescript TypeScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); await sdk.initialize(); const context = await sdk.client.context.fetch({ search_query: ['product features', 'API rate limits'], max_results: 5, types: ['fact'], mode: 'accurate', }); for (const fact of context.facts ?? []) { console.log(`[${(fact.confidence ?? 0).toFixed(2)}] ${fact.content}`); } ``` ```json Response theme={null} { "facts": [ { "id": "mem_org_a1b2c3d4", "content": "The API rate limit is 1000 requests per minute for Enterprise tier", "confidence": 0.97, "entities": [], "source": { "ingestion_id": "ing_bootstrap_001", "document_type": "knowledge-article", "document_created_at": "2025-01-10T00:00:00Z" }, "relevance_score": 0.94 }, { "id": "mem_org_e5f67890", "content": "Platform supports SSO with SAML and OIDC protocols", "confidence": 0.95, "entities": [], "source": { "ingestion_id": "ing_bootstrap_002", "document_type": "document", "document_created_at": "2025-01-10T00:00:00Z" }, "relevance_score": 0.87 } ], "preferences": [], "episodes": [], "metadata": { "total_results": 2, "query_time_ms": 124, "tokens_used": 289, "scope": { "client_id": "cli_a1b2c3d4e5f67890" }, "mode": "accurate" } } ``` Client context is cached with a **30-minute TTL**. Repeated queries within the TTL window are served from cache without re-querying the storage engines. New bootstrap ingestions automatically invalidate the cache. ### Raises * `InvalidInputError`: when `mode` is not `"fast"` or `"accurate"`. * `InvalidInputError`: when `precision_level` is not `"high"` or `"medium"`. * `SDKNotInitializedError`: when called before `await sdk.initialize()`. * `AuthenticationError`: when the API key is invalid or revoked. ### See also * [`sdk.user.context.fetch`](/sdk-reference/context/user-fetch): user-scoped context * [`sdk.customer.context.fetch`](/sdk-reference/context/customer-fetch): customer-scoped context * [`sdk.context.fetch`](/sdk-reference/context/fetch): unified scope-chain fetch # customer.context.fetch Source: https://docs.maximem.ai/sdk-reference/context/customer-fetch Retrieve customer-scoped context: shared organizational knowledge, policies, and team-wide memories visible to every user within a customer. **B2B only.** `customer.context.fetch` (`POST /v1/context/customer/fetch`) is available only on a B2B instance, that is one whose `user_context_isolation` is `strict`. On a B2C instance (`user_context_isolation: "equals_customer"`) there is no customer scope, the call is not available, and it is rejected with HTTP 400. Call `GET /api/v1/auth/whoami` to see which mode your instance is in: it returns `user_context_isolation`. On a B2C instance, fetch at user scope with [`user.context.fetch`](/sdk-reference/context/user-fetch) instead. ```python Python theme={null} await sdk.customer.context.fetch( customer_id, conversation_id=None, search_query=None, max_results=10, types=None, mode="fast", precision_level="high", ) ``` ```typescript TypeScript theme={null} await sdk.customer.context.fetch(options?: FetchOptions) ``` Retrieve context scoped to a specific customer organization. Customer-scoped memories are visible to all users within that customer. This is useful for fetching shared organizational knowledge, company policies, and team-wide context. ### Parameters The customer identifier to fetch context for. Always required, because this call is B2B-only: a B2C instance does not accept `customer_id` anywhere. See [B2C vs B2B](/concepts/memory-scopes#b2c-vs-b2b-which-scopes-apply-to-you). Optional conversation identifier. When provided, results are biased toward memories relevant to the active conversation. When supplied, it must be a valid UUID [registered via `record_message`](/concepts/context-end-to-end#short-term-context). One or more search queries to find relevant customer memories. If omitted, returns the most recent and highest-confidence customer-scoped memories. Maximum number of memory items to return. Defaults to `10`. Maximum `50`. Filter results to specific memory types. If omitted, all types are included. | Value | Description | | ---------------- | -------------------------------------------------------- | | `fact` | Factual information about the customer organization | | `preference` | Customer organizational preferences | | `episode` | Significant events within the customer organization | | `emotion` | Sentiment and emotional context within the organization | | `temporal_event` | Time-bound events (project deadlines, fiscal year, etc.) | Retrieval mode that controls the speed-quality tradeoff: the retrieval axis (`fast` vs `accurate`) of [Retrieval Modes](/concepts/retrieval-modes). | Value | Description | | ---------- | ----------------------------------------------------------------- | | `fast` | Vector search only. Lower latency. **Default.** | | `accurate` | Full vector + graph + re-ranking. Higher quality, higher latency. | For real per-mode latency on your instance, see **Dashboard → Usage**. Controls how precisely results are filtered before they're returned. | Value | Description | | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | `high` | Results go through an additional relevance-refinement pass before being returned. **Default.** | | `medium` | Skips the refinement pass for faster responses. Recall isn't impacted (the same candidate memories are searched), but outputs are less precisely filtered. | Independent of `mode`; combine with either `fast` or `accurate`. For real latency on your instance, see **Dashboard → Usage**. ### Returns A `ContextResponse` with the following fields: Array of fact memories relevant to the query. Array of preference memories. Array of episode memories. Array of emotion memories. Response metadata including `total_results`, `query_time_ms`, `tokens_used`, `scope`, and `mode`. ### Example ```python Python theme={null} from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK(api_key="synap_your_key_here") await sdk.initialize() context = await sdk.customer.context.fetch( customer_id="cust_acme_corp", search_query=["project management tools", "engineering stack"], max_results=5, types=["fact", "preference"], mode="accurate", ) for fact in context.facts: print(f"[{fact.confidence:.2f}] {fact.content}") ``` ```javascript JavaScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); await sdk.initialize(); const context = await sdk.customer.context.fetch({ customer_id: 'cust_acme_corp', search_query: ['project management tools', 'engineering stack'], max_results: 5, types: ['fact', 'preference'], mode: 'accurate', }); for (const fact of context.facts ?? []) { console.log(`[${(fact.confidence ?? 0).toFixed(2)}] ${fact.content}`); } ``` ```typescript TypeScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); await sdk.initialize(); const context = await sdk.customer.context.fetch({ customer_id: 'cust_acme_corp', search_query: ['project management tools', 'engineering stack'], max_results: 5, types: ['fact', 'preference'], mode: 'accurate', }); for (const fact of context.facts ?? []) { console.log(`[${(fact.confidence ?? 0).toFixed(2)}] ${fact.content}`); } ``` ```json Response theme={null} { "facts": [ { "id": "mem_cust_f1a2b3c4", "content": "Acme Corp uses Jira for project management and Slack for communication", "confidence": 0.94, "entities": [ { "canonical_name": "Acme Corp", "type": "organization", "entity_id": "ent_acme001" } ], "source": { "ingestion_id": "ing_cust_7a8b9c0d", "document_type": "ai-chat-conversation", "document_created_at": "2025-01-12T14:30:00Z" }, "relevance_score": 0.92, "scope": "customer" }, { "id": "mem_cust_d5e6f7a8", "content": "Engineering team uses Python 3.11 and PostgreSQL for all services", "confidence": 0.91, "entities": [ { "canonical_name": "Acme Corp Engineering", "type": "team", "entity_id": "ent_acme_eng" } ], "source": { "ingestion_id": "ing_cust_0a9b8c7d", "document_type": "document", "document_created_at": "2025-01-08T09:00:00Z" }, "relevance_score": 0.88, "scope": "customer" } ], "preferences": [ { "id": "mem_cust_b9c0d1e2", "content": "Team prefers detailed code reviews over quick approvals", "confidence": 0.87, "entities": [], "source": { "ingestion_id": "ing_cust_7a8b9c0d", "document_type": "ai-chat-conversation", "document_created_at": "2025-01-12T14:30:00Z" }, "relevance_score": 0.79, "scope": "customer" } ], "episodes": [], "emotions": [], "metadata": { "total_results": 3, "query_time_ms": 187, "tokens_used": 456, "scope": { "customer_id": "cust_acme_corp", "client_id": "cli_a1b2c3d4e5f67890" }, "mode": "accurate" } } ``` Customer-scoped memories take priority over client-scoped memories when they cover the same topic, following the [scope chain](/concepts/memory-scopes) priority rules. To merge customer and client context in a single call, use [`sdk.context.fetch`](/sdk-reference/context/fetch). ### Raises * `InvalidInputError`: when `mode` is not `"fast"` or `"accurate"`. * `InvalidInputError`: when `precision_level` is not `"high"` or `"medium"`. * `SDKNotInitializedError`: when called before `await sdk.initialize()`. * `AuthenticationError`: when the API key is invalid or revoked. * `ContextNotFoundError`: when `customer_id` does not exist for this instance. ### See also * [`sdk.user.context.fetch`](/sdk-reference/context/user-fetch): user-scoped context * [`sdk.client.context.fetch`](/sdk-reference/context/client-fetch): organizational (client) context * [`sdk.context.fetch`](/sdk-reference/context/fetch): unified scope-chain fetch # fetch Source: https://docs.maximem.ai/sdk-reference/context/fetch Fetch and merge context across all relevant scopes in a single call. ```python Python theme={null} await sdk.fetch(conversation_id=None, user_id=None, customer_id=None, search_query=None, max_results=20, types=None, mode="fast", precision_level="high", include_conversation_context=True, scopes=None, include_scope_labels=False, context_mode="in-conversation", include_profile=True, last_n_conversations=1) ``` ```typescript TypeScript theme={null} await sdk.fetch(options?: UnifiedFetchOptions) ``` The recommended entry point for framework integrations. `fetch()` queries every scope you provide an identifier for in parallel, deduplicates the merged items (first scope wins), attributes each item to its source scope, optionally folds in the conversation's compacted history plus recent messages, and returns a ready-to-inject `formatted_context` string. Pass it the identifiers you have on hand for the current turn (conversation, user, customer) and use the resulting `formatted_context` directly in your LLM prompt. ### Parameters Conversation scope identifier. When provided, the conversation scope is queried and (unless disabled) the conversation's compacted history is included. Must be a valid UUID (e.g. `str(uuid.uuid4())`) [registered via `record_message`](/concepts/context-end-to-end#short-term-context). User scope identifier. Threaded into the conversation-scope sub-fetch so per-user privacy filtering applies. Customer scope identifier. Required on B2B. **Not accepted on B2C**: passing it is rejected with HTTP 400. See [B2C vs B2B](/concepts/memory-scopes#b2c-vs-b2b-which-scopes-apply-to-you). Required for B2B deployments where customer-scoped context is in use. On a B2C deployment, leave it out entirely. Search queries applied to all queried scopes. Defaults to `None` (no query-side filtering). Maximum results per scope. The merged total may be higher across scopes. Defaults to `20`. Memory types to include. Defaults to `None`, which means all types. Retrieval mode: the retrieval axis (`fast` vs `accurate`) of [Retrieval Modes](/concepts/retrieval-modes). `"fast"` (default) or `"accurate"`. Result precision: `"high"` (default) or `"medium"`. With `"high"`, results go through an additional relevance-refinement pass before being returned. `"medium"` skips the refinement pass for faster responses; recall isn't impacted (the same candidate memories are searched), but outputs are less precisely filtered. Independent of `mode`; combine with either `fast` or `accurate`. For real latency on your instance, see **Dashboard → Usage**. Include the conversation's compacted history plus recent messages in the result. Defaults to `True`. Only effective when `conversation_id` is also provided. Explicitly limit which scopes to query (e.g. `["user", "customer"]`). Defaults to `None`, which queries every scope for which an identifier was provided. Annotate each item with its source scope in the `formatted_context` output. Defaults to `False`. `"in-conversation"` (default) returns the usual merged item lists. `"conversation-summary"` instead returns a caller `profile` plus summaries of the last `last_n_conversations` conversations: the call-start read for async integrations. Requires `user_id`. These three summary-mode params are forwarded **only** to the user-scope sub-fetch (the other scopes reject summary mode). In summary mode `search_query`, `mode` and `precision_level` are ignored (it is an assembly, not a retrieval), and `customer_id` is required on B2B. Summary mode only: include the caller profile. Defaults to `True`. Summary mode only: how many previous conversations to summarize. Defaults to `1`. Range 0–20. ### Returns A `UnifiedContextResponse` with merged items, scope attribution, and a `formatted_context` string ready for LLM injection. Summary mode only: the caller profile (`attributes`, `overview`, `extras`, `meta`). `None` outside summary mode. Summary mode only: previous-conversation summaries. `None` outside summary mode. Pre-formatted context block you can drop straight into an LLM prompt. Honors `include_scope_labels` and `include_conversation_context`. The scopes that were actually queried for this call, in order. Mapping from item ID to the scope that produced it after deduplication. When `include_conversation_context=True` and a `conversation_id` was supplied, the conversation's compacted history and recent messages. `None` otherwise. ### Example ```python Python theme={null} import uuid from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK(api_key="synap_your_key_here") await sdk.initialize() # conversation_id must be a valid UUID; reuse the conversation's id conversation_id = str(uuid.uuid4()) # Fetch everything for a conversation turn. ctx = await sdk.fetch( conversation_id=conversation_id, user_id="user-456", customer_id="cust-789", search_query=["user preferences"], ) prompt = ctx.formatted_context # Drop into your LLM system prompt. # Fetch only user + customer context (skip conversation scope). ctx = await sdk.fetch( user_id="user-456", customer_id="cust-789", scopes=["user", "customer"], ) ``` ```javascript JavaScript theme={null} import { randomUUID } from 'node:crypto'; import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); await sdk.initialize(); // conversation_id must be a valid UUID; reuse the conversation's id let conversation_id = randomUUID(); // Fetch everything for a conversation turn. let ctx = await sdk.fetch({ conversation_id: conversation_id, user_id: 'user-456', customer_id: 'cust-789', search_query: ['user preferences'], }); const prompt = ctx.formatted_context; // Drop into your LLM system prompt. // Fetch only user + customer context (skip conversation scope). ctx = await sdk.fetch({ user_id: 'user-456', customer_id: 'cust-789', scopes: ['user', 'customer'], }); ``` ```typescript TypeScript theme={null} import { randomUUID } from 'node:crypto'; import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); await sdk.initialize(); // conversation_id must be a valid UUID; reuse the conversation's id let conversation_id = randomUUID(); // Fetch everything for a conversation turn. let ctx = await sdk.fetch({ conversation_id: conversation_id, user_id: 'user-456', customer_id: 'cust-789', search_query: ['user preferences'], }); const prompt = ctx.formatted_context; // Drop into your LLM system prompt. // Fetch only user + customer context (skip conversation scope). ctx = await sdk.fetch({ user_id: 'user-456', customer_id: 'cust-789', scopes: ['user', 'customer'], }); ``` ### Conversation-summary mode (call start) For async integrations, fire one summary-mode fetch at call connect, keyed by the caller's identity. It resolves a caller profile plus the last call's summary (no retrieval, pure assembly), and folds them into `formatted_context` as `## Caller Profile` and `## Previous Conversations` sections. ```python theme={null} ctx = await sdk.fetch( user_id="+919812345678", context_mode="conversation-summary", include_profile=True, last_n_conversations=1, ) prompt_block = ctx.formatted_context # "## Caller Profile ..." + "## Previous Conversations ..." ``` The call-end counterpart is [`conversation.ingest_transcript`](/sdk-reference/conversation/ingest-transcript). ### Raises * `InvalidInputError`: when `precision_level` is not `"high"` or `"medium"`. * `AuthenticationError`: when the SDK has not been initialized (call `await sdk.initialize()` first). Per-scope fetch failures are logged and skipped rather than raised, so a partial-scope outage still returns the scopes that succeeded. See [Error Codes](/sdk-reference/errors) for the full SDK exception hierarchy. ### See also * [initialize](/sdk-reference/lifecycle/initialize): required before calling `fetch`. * [as\_tool](/sdk-reference/lifecycle/as-tool): expose `fetch` to an LLM as a tool definition. # user.context.fetch Source: https://docs.maximem.ai/sdk-reference/context/user-fetch Retrieve user-scoped context: facts, preferences, episodes, and temporal events about a specific end user. ```python Python theme={null} await sdk.user.context.fetch( user_id, conversation_id=None, search_query=None, max_results=10, types=None, mode="fast", precision_level="high", customer_id=None, context_mode="in-conversation", include_profile=True, last_n_conversations=1, ) ``` ```typescript TypeScript theme={null} await sdk.user.context.fetch(options?: FetchOptions) ``` Fetch context scoped to a single end user. User-scoped memories capture personal facts, preferences, and history that the SDK has accumulated for this user across their conversations. Use this on the server before composing a prompt so the model can ground its response in what you already know about the person. ### Parameters The user identifier to fetch context for. Must match a `user_id` you've previously ingested or initialized a conversation for. Optional conversation identifier. When provided, results are biased toward memories relevant to the active conversation and the SDK can inject periodic user summaries into the response. When supplied, it must be a valid UUID (e.g. `str(uuid.uuid4())`) [registered via `record_message`](/concepts/context-end-to-end#short-term-context). One or more search queries to find relevant user memories. If omitted, returns the most recent and highest-confidence user-scoped memories. Maximum number of memory items to return. Defaults to `10`. Maximum `50`. Filter results to specific memory types. If omitted, all types are included. | Value | Description | | ---------------- | ------------------------------------------------------------ | | `fact` | Factual information about the user | | `preference` | Stated or inferred user preferences | | `episode` | Notable events from the user's history | | `emotion` | Sentiment and emotional context | | `temporal_event` | Time-bound events (deadlines, appointments, recurring dates) | Retrieval mode that controls the speed-quality tradeoff: the retrieval axis (`fast` vs `accurate`) of [Retrieval Modes](/concepts/retrieval-modes). | Value | Description | | ---------- | ----------------------------------------------------------------- | | `fast` | Vector search only. Lower latency. **Default.** | | `accurate` | Full vector + graph + re-ranking. Higher quality, higher latency. | For real per-mode latency on your instance, see **Dashboard → Usage**. Controls how precisely results are filtered before they're returned. | Value | Description | | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | `high` | Results go through an additional relevance-refinement pass before being returned. **Default.** | | `medium` | Skips the refinement pass for faster responses. Recall isn't impacted (the same candidate memories are searched), but outputs are less precisely filtered. | Independent of `mode`; combine with either `fast` or `accurate`. For real latency on your instance, see **Dashboard → Usage**. Customer identifier the user belongs to. Required on B2B. **Not accepted on B2C**: passing it is rejected with HTTP 400. See [B2C vs B2B](/concepts/memory-scopes#b2c-vs-b2b-which-scopes-apply-to-you). **Required for B2B instances.** On a B2C instance (`user_context_isolation: "equals_customer"`) it must be omitted: sending it returns HTTP 400. `"in-conversation"` (default) returns the usual item lists. `"conversation-summary"` instead returns a caller `profile` and summaries of the last `last_n_conversations` conversations: the call-start read for async integrations. In summary mode `search_query`, `mode` and `precision_level` are ignored, and `customer_id` is required on B2B. Summary mode only: include the caller profile. Defaults to `True`. Summary mode only: number of previous conversations to summarize. Defaults to `1`. Range 0–20. ### Returns A `ContextResponse` with the following fields: Array of fact memories relevant to the query. Each includes `content`, `confidence`, `entities`, `source`, and `relevance_score`. Array of preference memories. Array of episode memories. Array of emotion memories. Array of time-bound event memories. Response metadata including `correlation_id`, `source` (`cache` | `cloud` | `anticipation`), `ttl_seconds`, and `retrieved_at`. Summary mode only: the caller profile (`attributes`, `overview`, `extras`, `meta`, plus `.raw`). `None` outside summary mode. Summary mode only: a list of `ConversationSummaryModel` (per-call `summary`, `classification`, `analysis`, `summary_status`, timestamps). `None` outside summary mode. ### Example ```python Python theme={null} import uuid from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK(api_key="synap_your_key_here") await sdk.initialize() context = await sdk.user.context.fetch( user_id="user_jane_doe", conversation_id=str(uuid.uuid4()), # must be a valid UUID search_query=["dietary restrictions", "favorite restaurants"], max_results=5, types=["fact", "preference"], mode="accurate", ) for fact in context.facts: print(f"[{fact.confidence:.2f}] {fact.content}") for pref in context.preferences: print(f"prefers: {pref.content}") ``` ```javascript JavaScript theme={null} import { randomUUID } from 'node:crypto'; import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); await sdk.initialize(); const context = await sdk.user.context.fetch({ user_id: 'user_jane_doe', conversation_id: randomUUID(), // must be a valid UUID search_query: ['dietary restrictions', 'favorite restaurants'], max_results: 5, types: ['fact', 'preference'], mode: 'accurate', }); for (const fact of context.facts ?? []) { console.log(`[${(fact.confidence ?? 0).toFixed(2)}] ${fact.content}`); } for (const pref of context.preferences ?? []) { console.log(`prefers: ${pref.content}`); } ``` ```typescript TypeScript theme={null} import { randomUUID } from 'node:crypto'; import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); await sdk.initialize(); const context = await sdk.user.context.fetch({ user_id: 'user_jane_doe', conversation_id: randomUUID(), // must be a valid UUID search_query: ['dietary restrictions', 'favorite restaurants'], max_results: 5, types: ['fact', 'preference'], mode: 'accurate', }); for (const fact of context.facts ?? []) { console.log(`[${(fact.confidence ?? 0).toFixed(2)}] ${fact.content}`); } for (const pref of context.preferences ?? []) { console.log(`prefers: ${pref.content}`); } ``` ### Raises * `InvalidInputError`: when `mode` is not `"fast"` or `"accurate"`. * `InvalidInputError`: when `precision_level` is not `"high"` or `"medium"`. * `SDKNotInitializedError`: when called before `await sdk.initialize()`. * `AuthenticationError`: when the API key is invalid or revoked. * `ContextNotFoundError`: when `user_id` does not exist for this instance. ### See also * [`sdk.customer.context.fetch`](/sdk-reference/context/customer-fetch): customer-scoped context * [`sdk.client.context.fetch`](/sdk-reference/context/client-fetch): organizational (client) context * [`sdk.context.fetch`](/sdk-reference/context/fetch): unified scope-chain fetch # conversation.context.compact Source: https://docs.maximem.ai/sdk-reference/conversation-context/compact Trigger asynchronous compaction of a conversation into a compressed summary. ```python theme={null} await sdk.conversation.context.compact( conversation_id: str, strategy: Optional[str] = None, compaction_level: Optional[str] = None, target_tokens: Optional[int] = None, force: bool = False, ) -> CompactionTriggerResponse ``` Kicks off compaction for a conversation. Compaction runs asynchronously. This call returns a handle with a `compaction_id` and an `in_progress` status. Use [`get_compaction_status`](/sdk-reference/conversation-context/get-compaction-status) to poll for completion, then [`get_compacted`](/sdk-reference/conversation-context/get-compacted) to retrieve the resulting summary. If a previous compacted summary already exists, it is returned on the trigger response as `previous_context` so you have something usable while the new compaction runs. ### Parameters The conversation to compact. Must be a valid UUID [registered via `record_message`](/concepts/context-end-to-end#short-term-context). Override the compaction strategy. One of: * `"aggressive"`: maximum compression, shortest output * `"balanced"`: middle-ground compression * `"conservative"`: preserve more detail * `"adaptive"`: let Synap pick based on conversation shape Backward-compatible alias for `strategy`. Prefer `strategy` in new code. Override the target token budget for the compacted output. Compact even if the conversation has not crossed the automatic threshold. ### Returns A `CompactionTriggerResponse` describing the in-flight job. Identifier for this compaction run. Echo of the supplied conversation id. Initial status, typically `"in_progress"`. How compaction was triggered (e.g., `"manual_api"`). When the run started. Rough ETA in seconds. Previously compacted context, if any. Usable while the new run finishes. Age of `previous_context` in seconds. Identifier of the previous compaction run. ### Example ```python Python theme={null} import uuid from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK(api_key="synap_your_key_here") await sdk.initialize() # conversation_id must be a valid UUID; reuse the conversation's id conversation_id = str(uuid.uuid4()) job = await sdk.conversation.context.compact( conversation_id=conversation_id, strategy="balanced", target_tokens=800, ) print(f"Compaction {job.compaction_id} started (status: {job.status})") if job.previous_context: print("Previous compacted context still usable while new run completes.") ``` ```javascript JavaScript theme={null} import { randomUUID } from 'node:crypto'; import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); await sdk.initialize(); // conversation_id must be a valid UUID; reuse the conversation's id let conversation_id = randomUUID(); const job = await sdk.conversation.context.compact({ conversation_id: conversation_id, strategy: 'balanced', target_tokens: 800, }); console.log(`Compaction ${job.compaction_id} started (status: ${job.status})`); if (job.previous_context) { console.log('Previous compacted context still usable while new run completes.'); } ``` ```typescript TypeScript theme={null} import { randomUUID } from 'node:crypto'; import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); await sdk.initialize(); // conversation_id must be a valid UUID; reuse the conversation's id let conversation_id = randomUUID(); const job = await sdk.conversation.context.compact({ conversation_id: conversation_id, strategy: 'balanced', target_tokens: 800, }); console.log(`Compaction ${job.compaction_id} started (status: ${job.status})`); if (job.previous_context) { console.log('Previous compacted context still usable while new run completes.'); } ``` ### Raises * `AuthenticationError`: when the API key is missing or invalid. * `NetworkError`: when the SDK cannot reach Synap. ### See also * [conversation.context.get\_compaction\_status](/sdk-reference/conversation-context/get-compaction-status) * [conversation.context.get\_compacted](/sdk-reference/conversation-context/get-compacted) * [conversation.context.get\_context\_for\_prompt](/sdk-reference/conversation-context/get-context-for-prompt) # conversation.context.fetch Source: https://docs.maximem.ai/sdk-reference/conversation-context/fetch Fetch conversation-scoped context (facts, preferences, episodes) for a specific conversation. ```python theme={null} await sdk.conversation.context.fetch( conversation_id: str, search_query: Optional[List[str]] = None, max_results: int = 10, types: Optional[List[str]] = None, mode: str = "fast", precision_level: str = "high", user_id: Optional[str] = None, customer_id: Optional[str] = None, ) -> ContextResponse ``` Retrieves the context Synap has built up for a single conversation: facts surfaced from earlier turns, user preferences, episodic memories, emotional cues, and temporal events. Use this to assemble grounding material before generating the next assistant turn. Passing `user_id` (and `customer_id`) is strongly recommended: it scopes the in-process anticipation cache to that user so bundles prefetched for one user can never be served on another user's lookup. These will become required in a future release. ### Parameters The conversation to fetch context for. Must be a valid UUID (e.g. `str(uuid.uuid4())`), the same id you used when [recording the conversation's messages](/concepts/context-end-to-end#short-term-context). Optional list of query strings to bias retrieval. When omitted, the most relevant recent context is returned. Maximum number of items to return per context type. Context types to include. Defaults to all available types (facts, preferences, episodes, emotions, temporal events). Retrieval mode: the retrieval axis (`fast` vs `accurate`) of [Retrieval Modes](/concepts/retrieval-modes): * `"fast"`: direct query, lower latency. Best for in-the-loop prompt assembly. * `"accurate"`: LLM-enhanced queries, higher quality at higher latency. Best when you can afford the extra latency. For real per-mode latency on your instance, see **Dashboard → Usage**. Controls how precisely results are filtered before they're returned: * `"high"`: results go through an additional relevance-refinement pass before being returned. **Default.** * `"medium"`: skips the refinement pass for faster responses. Recall isn't impacted (the same candidate memories are searched), but outputs are less precisely filtered. Independent of `mode`; combine with either `fast` or `accurate`. For real latency on your instance, see **Dashboard → Usage**. External user id. Strongly recommended: scopes anticipation cache lookups to the right user and avoids deriving scope from a not-yet-written conversation row. External customer id, forwarded alongside `user_id` for the same scoping reason. Required on B2B. **Not accepted on B2C**: passing it is rejected with HTTP 400. See [B2C vs B2B](/concepts/memory-scopes#b2c-vs-b2b-which-scopes-apply-to-you). ### Returns A `ContextResponse` containing the retrieved context. Facts learned about the user during this conversation. Stated or inferred preferences relevant to this conversation. Episodic memories tied to this conversation. Detected emotional signals. Time-anchored events relevant to the conversation. Compacted/summary context for this conversation, when available. Correlation id, source (`cache` or `cloud`), TTL, and retrieval timestamp. ### Example ```python Python theme={null} import uuid from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK(api_key="synap_your_key_here") await sdk.initialize() # reuse the same conversation_id you recorded messages under conversation_id = str(uuid.uuid4()) ctx = await sdk.conversation.context.fetch( conversation_id=conversation_id, search_query=["dinner plans", "dietary restrictions"], max_results=5, mode="fast", user_id="user_alice", customer_id="customer_acme", ) for fact in ctx.facts: print(fact.content) ``` ```javascript JavaScript theme={null} import { randomUUID } from 'node:crypto'; import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); await sdk.initialize(); // reuse the same conversation_id you recorded messages under let conversation_id = randomUUID(); const ctx = await sdk.conversation.context.fetch({ conversation_id: conversation_id, search_query: ['dinner plans', 'dietary restrictions'], max_results: 5, mode: 'fast', user_id: 'user_alice', customer_id: 'customer_acme', }); for (const fact of ctx.facts ?? []) { console.log(fact.content); } ``` ```typescript TypeScript theme={null} import { randomUUID } from 'node:crypto'; import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); await sdk.initialize(); // reuse the same conversation_id you recorded messages under let conversation_id = randomUUID(); const ctx = await sdk.conversation.context.fetch({ conversation_id: conversation_id, search_query: ['dinner plans', 'dietary restrictions'], max_results: 5, mode: 'fast', user_id: 'user_alice', customer_id: 'customer_acme', }); for (const fact of ctx.facts ?? []) { console.log(fact.content); } ``` ### Raises * `InvalidInputError`: when `mode` is not `"fast"` or `"accurate"`. * `InvalidInputError`: when `precision_level` is not `"high"` or `"medium"`. * `AuthenticationError`: when the API key is missing or invalid. * `NetworkError`: when the SDK cannot reach Synap. ### See also * [conversation.record\_message](/sdk-reference/conversation/record-message) * [conversation.context.compact](/sdk-reference/conversation-context/compact) * [conversation.context.get\_context\_for\_prompt](/sdk-reference/conversation-context/get-context-for-prompt) # conversation.context.get_compacted Source: https://docs.maximem.ai/sdk-reference/conversation-context/get-compacted Retrieve an existing compacted summary for a conversation without triggering a new compaction. ```python theme={null} await sdk.conversation.context.get_compacted( conversation_id: str, version: Optional[int] = None, format: str = "structured", ) -> Optional[CompactionResponse] ``` Fetches the most recent compacted summary for a conversation. Local SDK cache is checked first (5 minute TTL); on miss the SDK fetches from Synap and caches the result. Returns `None` if no compacted summary exists yet. Call [`compact`](/sdk-reference/conversation-context/compact) first in that case. When a specific `version` is requested the local cache is bypassed and the value is fetched fresh from Synap. ### Parameters The conversation whose compacted summary you want to read. Must be a valid UUID [registered via `record_message`](/concepts/context-end-to-end#short-term-context). Specific compaction version. Defaults to the latest. When supplied, the SDK always fetches from the cloud and skips the local cache. Output format: * `"structured"`: structured fields (`facts`, `decisions`, `preferences`, `current_state`) * `"narrative"`: prose summary * `"injection"`: preformatted for direct prompt injection ### Returns A `CompactionResponse` if a compacted summary exists, otherwise `None`. The formatted compacted context. Token count of the original conversation. Token count after compaction. Compaction ratio (compacted / original). Strategy actually applied (e.g., `adaptive`, `balanced`). Identifier of the compaction run that produced this summary. Strategy used for this compaction. Quality validation score, when available. Whether the validation check passed. Extracted facts (structured format). Extracted decisions (structured format). Extracted preferences (structured format). Snapshot of current conversation state (structured format). Optional warning when validation flagged a concern. Correlation id, source (`cache` or `cloud`), TTL, and retrieval timestamp. ### Example ```python Python theme={null} import uuid from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK(api_key="synap_your_key_here") await sdk.initialize() # conversation_id must be a valid UUID; reuse the conversation's id conversation_id = str(uuid.uuid4()) compacted = await sdk.conversation.context.get_compacted( conversation_id=conversation_id, format="structured", ) if compacted is None: print("No compaction yet. Trigger one with sdk.conversation.context.compact()") else: print(f"Compressed {compacted.original_token_count} -> {compacted.compacted_token_count} tokens") for fact in compacted.facts: print("-", fact) ``` ```javascript JavaScript theme={null} import { randomUUID } from 'node:crypto'; import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); await sdk.initialize(); // conversation_id must be a valid UUID; reuse the conversation's id let conversation_id = randomUUID(); const compacted = await sdk.conversation.context.get_compacted({ conversation_id: conversation_id, format: 'structured', }); if (compacted == null) { console.log('No compaction yet. Trigger one with sdk.conversation.context.compact()'); } else { console.log(`Compressed ${compacted.original_token_count} -> ${compacted.compacted_token_count} tokens`); for (const fact of (compacted.facts as string[] | undefined) ?? []) { console.log('-', fact); } } ``` ```typescript TypeScript theme={null} import { randomUUID } from 'node:crypto'; import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); await sdk.initialize(); // conversation_id must be a valid UUID; reuse the conversation's id let conversation_id = randomUUID(); const compacted = await sdk.conversation.context.get_compacted({ conversation_id: conversation_id, format: 'structured', }); if (compacted == null) { console.log('No compaction yet. Trigger one with sdk.conversation.context.compact()'); } else { console.log(`Compressed ${compacted.original_token_count} -> ${compacted.compacted_token_count} tokens`); for (const fact of (compacted.facts as string[] | undefined) ?? []) { console.log('-', fact); } } ``` ### Raises * `AuthenticationError`: when the API key is missing or invalid. * `NetworkError`: when the SDK cannot reach Synap. ### See also * [conversation.context.compact](/sdk-reference/conversation-context/compact) * [conversation.context.get\_compaction\_status](/sdk-reference/conversation-context/get-compaction-status) * [conversation.context.get\_context\_for\_prompt](/sdk-reference/conversation-context/get-context-for-prompt) # conversation.context.get_compaction_status Source: https://docs.maximem.ai/sdk-reference/conversation-context/get-compaction-status Check whether a conversation has compacted context, whether it's stale, and whether a compaction run is in progress. ```python theme={null} await sdk.conversation.context.get_compaction_status( conversation_id: str, ) -> CompactionStatusResponse ``` Returns the current compaction state for a conversation. Use this after [`compact`](/sdk-reference/conversation-context/compact) to poll for completion before calling [`get_compacted`](/sdk-reference/conversation-context/get-compacted). The SDK checks its in-process anticipation cache first. If a `compaction_update` bundle has been pushed for this conversation, the call returns `status="completed"` immediately without a network round-trip. ### Parameters The conversation to inspect. Must be a valid UUID [registered via `record_message`](/concepts/context-end-to-end#short-term-context). ### Returns A `CompactionStatusResponse` describing the current state. Echo of the supplied conversation id. One of `"none"`, `"in_progress"`, `"completed"`, or `"failed"`. Identifier of the latest compaction run, when one exists. When the latest compaction completed. Ratio of compacted-to-raw token count for the latest run. Lower values indicate more aggressive compression. Quality score for the latest compaction (0.0-1.0). Reflects how well the compacted summary preserves the original conversation's information. For `status="in_progress"`, an estimated number of seconds until completion. Failure reason when `status="failed"`. Version number of the latest compaction. Increments each time a new run completes. When the latest compaction run was started. ### Example ```python theme={null} import asyncio import uuid from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK(api_key="synap_your_key_here") await sdk.initialize() # conversation_id must be a valid UUID; reuse the conversation's id conversation_id = str(uuid.uuid4()) job = await sdk.conversation.context.compact( conversation_id=conversation_id, ) # Poll until the run completes while True: status = await sdk.conversation.context.get_compaction_status( conversation_id=conversation_id, ) if status.status in ("completed", "failed"): break await asyncio.sleep(2) print(f"Final status: {status.status}") ``` ### Raises * `AuthenticationError`: when the API key is missing or invalid. * `NetworkError`: when the SDK cannot reach Synap. ### See also * [conversation.context.compact](/sdk-reference/conversation-context/compact) * [conversation.context.get\_compacted](/sdk-reference/conversation-context/get-compacted) * [conversation.context.get\_context\_for\_prompt](/sdk-reference/conversation-context/get-context-for-prompt) # conversation.context.get_context_for_prompt Source: https://docs.maximem.ai/sdk-reference/conversation-context/get-context-for-prompt Get compacted context combined with recent un-compacted messages, pre-formatted for LLM prompt injection. ```python theme={null} await sdk.conversation.context.get_context_for_prompt( conversation_id: str, style: str = "structured", ) -> ContextForPromptResponse ``` Returns a single `formatted_context` string that stitches together the compacted history with any messages that arrived after the last compaction cutoff. Drop the result straight into your prompt. No further assembly required. If no compaction has run yet, every recorded message comes back as a recent message, so this method is useful from the very first turn. ### Parameters The conversation to assemble context for. Must be a valid UUID [registered via `record_message`](/concepts/context-end-to-end#short-term-context). Formatting style for the compacted portion: * `"structured"`: labelled sections (facts, decisions, preferences, current state) * `"narrative"`: prose summary * `"bullet_points"`: bulleted list ### Returns A `ContextForPromptResponse` ready for prompt injection. The combined compacted + recent context, formatted per `style`. Inject directly into your LLM prompt. Whether usable context (compacted or recent) was found. Whether the compacted portion is stale relative to newer messages. Compression ratio of the compacted portion. Quality validation score of the compacted portion. How long ago the compaction completed. True when the compacted portion was flagged for quality concerns. Raw un-compacted messages since the last compaction. Use if you want to format them yourself. Number of un-compacted messages included. Number of messages covered by the compacted portion. Total messages in the conversation. ### Example ```python theme={null} import uuid from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK(api_key="synap_your_key_here") await sdk.initialize() # conversation_id must be a valid UUID; reuse the conversation's id conversation_id = str(uuid.uuid4()) ctx = await sdk.conversation.context.get_context_for_prompt( conversation_id=conversation_id, style="structured", ) system_prompt = f"""You are a helpful assistant. Conversation context so far: {ctx.formatted_context or "(no prior context)"} """ print(system_prompt) print(f"Covered {ctx.compacted_message_count} compacted + {ctx.recent_message_count} recent messages.") ``` ### Raises * `AuthenticationError`: when the API key is missing or invalid. * `NetworkError`: when the SDK cannot reach Synap. ### See also * [conversation.context.compact](/sdk-reference/conversation-context/compact) * [conversation.context.get\_compacted](/sdk-reference/conversation-context/get-compacted) * [conversation.context.get\_compaction\_status](/sdk-reference/conversation-context/get-compaction-status) * [conversation.context.fetch](/sdk-reference/conversation-context/fetch) # conversation.ingest_transcript Source: https://docs.maximem.ai/sdk-reference/conversation/ingest-transcript One-shot async push of a full conversation transcript (plus optional analysis) for background extraction and summarization. ```python theme={null} await sdk.conversation.ingest_transcript( conversation_id: str, user_id: str, transcript: Union[str, List[TranscriptTurn]], customer_id: Optional[str] = None, conversation_type: Optional[str] = None, analysis: Optional[Dict[str, Any]] = None, metadata: Optional[Dict[str, Any]] = None, started_at: Optional[datetime] = None, ended_at: Optional[datetime] = None, ) -> TranscriptIngestResponse ``` Push a whole conversation transcript in a single call at the **end** of a session. Synap records the transcript, enqueues background extraction, and fires a summary compaction, then returns immediately; nothing here sits on a hot path. This is the conversation-end half of the async integration pattern; the conversation-start half is [`sdk.fetch(context_mode="conversation-summary")`](/sdk-reference/context/fetch). Poll completion with [`sdk.memories.status(ingestion_id)`](/sdk-reference/memories/status) or `sdk.memories.wait_for_completion(ingestion_id)`. ### Parameters The client's own call/conversation id. **Any string is accepted**: it is *not* validated as a UUID (unlike `record_message`). The server coerces it and echoes the original back as `external_conversation_id`. Reuse a stable id per call (e.g. your telephony provider's call id, or `"{phone}:{call_start_iso}"`). Caller identity, e.g. an E.164 phone number for a voice call. The conversation content. A plain string is split by the server on the turn grammar; a `List[TranscriptTurn]` is **preferred** because it preserves per-turn timestamps and speaker labels. ```python theme={null} from maximem_synap import TranscriptTurn TranscriptTurn( role: Literal["user", "assistant"], content: str, timestamp: Optional[datetime] = None, # turn time; server time if absent speaker: Optional[str] = None, # display label, e.g. "Agent Priya" metadata: Optional[Dict[str, Any]] = None, ) ``` **Required on B2B (strict-isolation) instances**; omit on B2C (`equals_customer`), where the server collapses it from `user_id`. Free-form label, ≤ 64 chars (e.g. `"voice"`, `"text"`, `"video"`). Your own per-call analysis JSON (≤ 64 KB). Stored verbatim (returned by conversation-summary fetches and shown in the dashboard) **and** fed to extraction as high-confidence hints. Open-ended metadata (≤ 64 KB). Conversation start time. Conversation end time. ### Returns A `TranscriptIngestResponse`. The server-coerced (UUID-form) conversation id. The original id you supplied, echoed verbatim. Handle for `sdk.memories.status()` / `wait_for_completion()`. Always set on success, never null, even on the `duplicate` branch. `"queued"` (recorded + enqueued) or `"duplicate"` (an identical transcript was already pushed). Number of turns persisted. `"in_progress"` (compaction enqueued), `"already_compacted"` (duplicate where the summary already exists), or `"skipped"` (no turns to summarize). When the push was accepted. Every response also exposes `.raw`: the untyped response dict, for forward compatibility with fields a newer server may add. ### Idempotency The push is idempotent on `(conversation_id, transcript)`: * **Same transcript, same id** → `status="duplicate"` with the original `ingestion_id`. Retries are free. * **Different transcript, same id** → `TranscriptConflictError` (HTTP 409). A call's transcript is immutable; mint a **new** `conversation_id` for a new call. ### Example ```python theme={null} from datetime import datetime, timezone from maximem_synap import MaximemSynapSDK, TranscriptTurn sdk = MaximemSynapSDK(api_key="synap_your_key_here") await sdk.initialize() resp = await sdk.conversation.ingest_transcript( conversation_id="call_01H8XZ", # any client string user_id="+919812345678", # E.164 caller id conversation_type="voice", transcript=[ TranscriptTurn(role="assistant", content="Hi, is this a good time?", speaker="Agent Priya"), TranscriptTurn(role="user", content="Yes — I'm looking for a 3 BHK in Baner.", speaker="Caller"), ], analysis={"disposition": "interested", "sentiment": "positive"}, started_at=datetime(2026, 7, 15, 10, 0, tzinfo=timezone.utc), ended_at=datetime(2026, 7, 15, 10, 6, tzinfo=timezone.utc), ) # Optionally wait for extraction to finish before the next call. await sdk.memories.wait_for_completion(resp.ingestion_id) ``` ### Raises * `InvalidInputError`: empty transcript, oversized `analysis`/`metadata`, or a B2B push missing `customer_id` (HTTP 400/422). * `TranscriptConflictError`: a *different* transcript was already ingested under this `conversation_id` (HTTP 409). Subclass of `ConflictError`. * `RateLimitError` / `InsufficientCreditsError`: as applicable. * `AuthenticationError`: when the API key is missing or invalid. ### See also * [context.fetch (conversation-summary mode)](/sdk-reference/context/fetch): the conversation-start read. * [user.get\_profile](/sdk-reference/user/get-profile) * [memories.status](/sdk-reference/memories/status) / [memories.wait\_for\_completion](/sdk-reference/memories/wait-for-completion) # conversation.record_message Source: https://docs.maximem.ai/sdk-reference/conversation/record-message Record a single conversation message (user or assistant turn) into Synap. ```python theme={null} await sdk.conversation.record_message( conversation_id: str, role: str, content: str, user_id: str, customer_id: Optional[str] = None, session_id: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None, ) -> Dict[str, Any] ``` Records a single message turn against a conversation. Synap uses recorded messages to build conversation-scoped context (facts, preferences, episodes) and as input for compaction. Both `user` and `assistant` turns should be recorded so the system has the full transcript. ### Parameters Stable identifier for the conversation. All messages sharing the same `conversation_id` form one transcript. Must be a valid UUID: generate one with `str(uuid.uuid4())` and reuse it for every turn in the conversation. This call is [what registers a conversation](/concepts/context-end-to-end#short-term-context). Message role. Must be `"user"` or `"assistant"`. The message text. External user identifier (your application's user ID). Required so the message is scoped to the right end user. External customer/tenant identifier. Required on B2B. **Not accepted on B2C**: passing it is rejected with HTTP 400. See [B2C vs B2B](/concepts/memory-scopes#b2c-vs-b2b-which-scopes-apply-to-you). **Required for B2B instances** (multi-tenant), where each message must be scoped to a tenant. **Must be omitted on B2C instances** (`user_context_isolation: "equals_customer"`), where the field is not accepted at all and a message carrying it is rejected with HTTP 400. Optional session identifier. Auto-generated if not provided. Free-form metadata to attach to the message (e.g., model name, latency, custom tags). ### Returns A dict describing the recorded message. Server-assigned message identifier. Echo of the supplied conversation id. Session id (the one you passed, or the auto-generated one). ISO-8601 timestamp of when the message was accepted. ### Example ```python Python theme={null} import uuid from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK(api_key="synap_your_key_here") await sdk.initialize() # conversation_id must be a valid UUID; generate once and reuse for the whole transcript conversation_id = str(uuid.uuid4()) await sdk.conversation.record_message( conversation_id=conversation_id, role="user", content="I prefer dark mode and concise answers.", user_id="user_alice", customer_id="customer_acme", # B2B only: required there, rejected on B2C metadata={"channel": "web"}, ) await sdk.conversation.record_message( conversation_id=conversation_id, role="assistant", content="Got it. I'll keep answers short and assume dark mode.", user_id="user_alice", customer_id="customer_acme", ) ``` ```javascript JavaScript theme={null} import { randomUUID } from 'node:crypto'; import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); await sdk.initialize(); // conversation_id must be a valid UUID; generate once and reuse for the whole transcript let conversation_id = randomUUID(); await sdk.conversation.record_message({ conversation_id: conversation_id, role: 'user', content: 'I prefer dark mode and concise answers.', user_id: 'user_alice', customer_id: 'customer_acme', // B2B only: required there, rejected on B2C metadata: {'channel': 'web'}, }); await sdk.conversation.record_message({ conversation_id: conversation_id, role: 'assistant', content: "Got it. I'll keep answers short and assume dark mode.", user_id: 'user_alice', customer_id: 'customer_acme', }); ``` ```typescript TypeScript theme={null} import { randomUUID } from 'node:crypto'; import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); await sdk.initialize(); // conversation_id must be a valid UUID; generate once and reuse for the whole transcript let conversation_id = randomUUID(); await sdk.conversation.record_message({ conversation_id: conversation_id, role: 'user', content: 'I prefer dark mode and concise answers.', user_id: 'user_alice', customer_id: 'customer_acme', // B2B only: required there, rejected on B2C metadata: {'channel': 'web'}, }); await sdk.conversation.record_message({ conversation_id: conversation_id, role: 'assistant', content: "Got it. I'll keep answers short and assume dark mode.", user_id: 'user_alice', customer_id: 'customer_acme', }); ``` ### Raises * `InvalidInputError`: when `role` is not `"user"` or `"assistant"`, or when required identifiers are missing. * `AuthenticationError`: when the API key is missing or invalid. * `NetworkError`: when the SDK cannot reach Synap. ### See also * [conversation.record\_messages\_batch](/sdk-reference/conversation/record-messages-batch) * [conversation.context.fetch](/sdk-reference/conversation-context/fetch) * [conversation.context.compact](/sdk-reference/conversation-context/compact) # conversation.record_messages_batch Source: https://docs.maximem.ai/sdk-reference/conversation/record-messages-batch Record multiple conversation messages in a single batched call. ```python theme={null} await sdk.conversation.record_messages_batch( messages: List[Dict[str, Any]], ) -> Dict[str, Any] ``` Records many messages in one call. Use this when backfilling an existing transcript, syncing a buffered queue, or recording a multi-turn exchange after it completes. Each entry in `messages` follows the same shape as the arguments to [`record_message`](/sdk-reference/conversation/record-message). ### Parameters List of message dicts. Each dict supports the following keys: * `conversation_id` (str, **required**): conversation identifier; must be a valid UUID registered via [`record_message`](/concepts/context-end-to-end#short-term-context) * `role` (str, **required**): must be `"user"` or `"assistant"` * `content` (str, **required**): message text * `user_id` (str, optional): external user id * `customer_id` (str, optional): external customer id; required on B2B, not accepted on B2C, where a message carrying it is rejected with HTTP 400 ([B2C vs B2B](/concepts/memory-scopes#b2c-vs-b2b-which-scopes-apply-to-you)) * `session_id` (str, optional): session identifier * `metadata` (dict, optional): free-form metadata Unlike [`record_message`](/sdk-reference/conversation/record-message), `user_id` and `customer_id` are optional in batch mode. If omitted, the entry is recorded without explicit user/customer attribution. ### Returns A dict summarising the batch outcome. Number of messages submitted. Number of messages accepted. Number of messages rejected. Per-message results in submission order. Successful entries include `message_id` and `recorded_at`; failed entries include an `error` field. ### Example ```python Python theme={null} import uuid from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK(api_key="synap_your_key_here") await sdk.initialize() # conversation_id must be a valid UUID; reuse one id across the whole transcript conversation_id = str(uuid.uuid4()) result = await sdk.conversation.record_messages_batch( messages=[ { "conversation_id": conversation_id, "role": "user", "content": "What's on my calendar today?", "user_id": "user_alice", "customer_id": "customer_acme", }, { "conversation_id": conversation_id, "role": "assistant", "content": "You have two meetings and a dentist appointment.", "user_id": "user_alice", "customer_id": "customer_acme", }, ] ) print(f"Recorded {result['succeeded']} of {result['total']} messages") ``` ```javascript JavaScript theme={null} import { randomUUID } from 'node:crypto'; import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); await sdk.initialize(); // conversation_id must be a valid UUID; reuse one id across the whole transcript const conversation_id = randomUUID(); const result = await sdk.conversation.record_messages_batch([ { 'conversation_id': conversation_id, 'role': 'user', 'content': "What's on my calendar today?", 'user_id': 'user_alice', 'customer_id': 'customer_acme', }, { 'conversation_id': conversation_id, 'role': 'assistant', 'content': 'You have two meetings and a dentist appointment.', 'user_id': 'user_alice', 'customer_id': 'customer_acme', }, ]); console.log(`Recorded ${result['succeeded']} of ${result['total']} messages`); ``` ```typescript TypeScript theme={null} import { randomUUID } from 'node:crypto'; import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); await sdk.initialize(); // conversation_id must be a valid UUID; reuse one id across the whole transcript const conversation_id = randomUUID(); const result = await sdk.conversation.record_messages_batch([ { 'conversation_id': conversation_id, 'role': 'user', 'content': "What's on my calendar today?", 'user_id': 'user_alice', 'customer_id': 'customer_acme', }, { 'conversation_id': conversation_id, 'role': 'assistant', 'content': 'You have two meetings and a dentist appointment.', 'user_id': 'user_alice', 'customer_id': 'customer_acme', }, ]); console.log(`Recorded ${result['succeeded']} of ${result['total']} messages`); ``` ### Raises * `InvalidInputError`: when an entry is missing required keys or `role` is invalid. * `AuthenticationError`: when the API key is missing or invalid. * `NetworkError`: when the SDK cannot reach Synap. ### See also * [conversation.record\_message](/sdk-reference/conversation/record-message) * [conversation.context.fetch](/sdk-reference/conversation-context/fetch) # credits.estimate Source: https://docs.maximem.ai/sdk-reference/credits/estimate Get a dry-run credit cost quote for a planned operation, without spending any credits. ```python Python theme={null} await sdk.credits.estimate( metric_type, units, item_count=None, endpoint=None, mode=None, ) ``` ```typescript TypeScript theme={null} await sdk.credits.estimate(options: EstimateOptions) ``` Returns the credit cost that would be charged for a hypothetical operation, based on the current pricing for your client. The call is a pure quote (it never debits the wallet) so it's safe to invoke before a large ingestion batch, a long retrieval, or whenever you want to surface an "estimated cost" preview to your users. ### Parameters The metered metric you want to price. Examples include `"llm_input_tokens"`, `"llm_output_tokens"`, `"memories_ingested"`, and `"retrievals"`. Match the metric you'd expect to see in the ledger. How many units of `metric_type` the planned operation would consume (e.g. number of tokens, number of memories). Optional count of discrete items the operation would touch. Some metrics price differently for batched vs. single-item operations. Operation identifier the quote is scoped to. Lets the server apply the right rate when a metric has different pricing across operations. Leave unset to use the default rate for the metric. Processing mode for the planned operation (for example `"fast"` or `"long-range"` for ingestion: the ingestion axis of [Retrieval Modes](/concepts/retrieval-modes)). Some metrics price differently per mode. ### Returns `CreditEstimate` with the quoted cost. The credit amount that would be debited if the operation ran with the supplied parameters at the current rate. No credits are spent by this call. ### Example ```python Python theme={null} from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK(api_key="synap_your_key_here") await sdk.initialize() # Quote the cost of ingesting ~1,000 LLM input tokens quote = await sdk.credits.estimate( metric_type="llm_input_tokens", units=1000, mode="long-range", ) print(f"Would cost {quote.credits_estimate} credits") # Compare against the current balance before proceeding balance = await sdk.credits.get_balance() if quote.credits_estimate > balance.balance_credits: print("Not enough credits. Top up or redeem a code first.") ``` ```javascript JavaScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); await sdk.initialize(); // Quote the cost of ingesting ~1,000 LLM input tokens const quote = await sdk.credits.estimate({ metric_type: 'llm_input_tokens', units: 1000, mode: 'long-range', }); console.log(`Would cost ${quote.credits_estimate} credits`); // Compare against the current balance before proceeding const balance = await sdk.credits.get_balance(); if (quote.credits_estimate > balance.balance_credits) { console.log('Not enough credits. Top up or redeem a code first.'); } ``` ```typescript TypeScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); await sdk.initialize(); // Quote the cost of ingesting ~1,000 LLM input tokens const quote = await sdk.credits.estimate({ metric_type: 'llm_input_tokens', units: 1000, mode: 'long-range', }); console.log(`Would cost ${quote.credits_estimate} credits`); // Compare against the current balance before proceeding const balance = await sdk.credits.get_balance(); if (quote.credits_estimate > balance.balance_credits) { console.log('Not enough credits. Top up or redeem a code first.'); } ``` ### Raises * `SynapAuthError`: when the API key is missing or invalid. * `SynapValidationError`: when `metric_type` is not recognized or `units` is negative. ### See also * [credits.get\_balance](/sdk-reference/credits/get-balance) * [credits.get\_ledger](/sdk-reference/credits/get-ledger) * [credits.redeem](/sdk-reference/credits/redeem) # credits.get_balance Source: https://docs.maximem.ai/sdk-reference/credits/get-balance Return the current credit balance and per-bucket breakdown for your wallet. ```python Python theme={null} await sdk.credits.get_balance() ``` ```typescript TypeScript theme={null} await sdk.credits.get_balance() ``` Fetches a snapshot of your client's credit wallet, including the total balance, a low-balance warning flag, and the individual buckets that make up the balance. Buckets are grouped by their funding source (e.g. paid top-up, promotional grant, redeem code) and may carry their own expiry dates. Use this before kicking off a large ingestion or query workload, or to drive an in-product "credits remaining" indicator. ### Parameters This method takes no parameters. ### Returns `CreditBalance` dataclass with the wallet snapshot. The client (organization) the wallet belongs to. Total spendable credits across all buckets. `True` when the balance has fallen below the configured low-balance threshold. A good signal to surface a "top up" prompt to your operators. Per-source breakdown of the balance. Each `CreditBucket` has: * `source_type` (`string`): origin of the credits (e.g. `"paid"`, `"promo"`, `"redeem"`). * `balance` (`float`): credits remaining in this bucket. * `expires_at` (`datetime | None`): when this bucket expires, if applicable. `None` means the bucket does not expire. ### Example ```python Python theme={null} from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK(api_key="synap_your_key_here") await sdk.initialize() balance = await sdk.credits.get_balance() print(f"You have {balance.balance_credits} credits") if balance.warning_low: print("Balance is low. Consider topping up or redeeming a code.") for bucket in balance.buckets: expiry = bucket.expires_at.isoformat() if bucket.expires_at else "never" print(f" {bucket.source_type}: {bucket.balance} (expires: {expiry})") ``` ```javascript JavaScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); await sdk.initialize(); const balance = await sdk.credits.get_balance(); console.log(`You have ${balance.balance_credits} credits`); if (balance.warning_low) { console.log('Balance is low. Consider topping up or redeeming a code.'); } for (const bucket of balance.buckets ?? []) { const expiry = bucket.expires_at ? bucket.expires_at : 'never'; console.log(` ${bucket.source_type}: ${bucket.balance} (expires: ${expiry})`); } ``` ```typescript TypeScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); await sdk.initialize(); const balance = await sdk.credits.get_balance(); console.log(`You have ${balance.balance_credits} credits`); if (balance.warning_low) { console.log('Balance is low. Consider topping up or redeeming a code.'); } for (const bucket of balance.buckets ?? []) { const expiry = bucket.expires_at ? bucket.expires_at : 'never'; console.log(` ${bucket.source_type}: ${bucket.balance} (expires: ${expiry})`); } ``` ### Raises * `SynapAuthError`: when the API key is missing or invalid. ### See also * [credits.get\_ledger](/sdk-reference/credits/get-ledger) * [credits.estimate](/sdk-reference/credits/estimate) * [credits.redeem](/sdk-reference/credits/redeem) # credits.get_ledger Source: https://docs.maximem.ai/sdk-reference/credits/get-ledger Paginate through the credit ledger for your wallet, with optional filtering by entry type and time range. ```python Python theme={null} await sdk.credits.get_ledger( entry_type=None, from_time=None, to_time=None, limit=100, offset=0, ) ``` ```typescript TypeScript theme={null} await sdk.credits.get_ledger(options?: LedgerOptions) ``` Returns a paginated view of the credit ledger for the caller's wallet. Every credit movement (top-ups, redemptions, debits from usage, expirations) produces a ledger entry. The client-facing ledger does not expose USD amounts or internal rate detail. Useful for building an in-product usage history view, reconciling charges, or auditing recent debits after a spike in usage. ### Parameters Filter to a single entry type. Common values include `"credit"` (additions to the wallet) and `"debit"` (deductions for usage). Omit to return all entry types. Inclusive lower bound on `created_at`. Omit to start from the beginning of history. Inclusive upper bound on `created_at`. Omit to read up to the present. Maximum number of entries to return in this page. Number of entries to skip. Combine with `limit` to walk the full ledger. ### Returns `CreditLedgerPage` with one page of entries plus pagination metadata. The ledger rows for this page. Each `CreditLedgerEntry` has: * `ledger_id` (`string`): stable identifier for the row. * `entry_type` (`string`): `"credit"`, `"debit"`, or similar. * `delta` (`float`): signed credit change (positive for credits added, negative for usage). * `metric_type` (`string | None`): the metered metric that produced the entry (e.g. `"llm_input_tokens"`), when applicable. * `category` (`string | None`): coarse grouping for the entry (e.g. `"ingestion"`, `"retrieval"`), when applicable. * `created_at` (`datetime`): when the entry was recorded. Total number of entries matching the filters (across all pages). Echo of the `limit` used for this page. Echo of the `offset` used for this page. ### Example ```python theme={null} from datetime import datetime, timedelta, timezone from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK(api_key="synap_your_key_here") await sdk.initialize() # Show debits from the last 7 days since = datetime.now(timezone.utc) - timedelta(days=7) page = await sdk.credits.get_ledger( entry_type="debit", from_time=since, limit=50, ) print(f"{page.total} debit entries in the last 7 days") for entry in page.entries: print( f" {entry.created_at.isoformat()} " f"{entry.delta:+.2f} " f"{entry.category or '-'} ({entry.metric_type or '-'})" ) ``` ### Raises * `SynapAuthError`: when the API key is missing or invalid. * `SynapValidationError`: when `limit` or `offset` are out of range. ### See also * [credits.get\_balance](/sdk-reference/credits/get-balance) * [credits.estimate](/sdk-reference/credits/estimate) * [credits.redeem](/sdk-reference/credits/redeem) # credits.redeem Source: https://docs.maximem.ai/sdk-reference/credits/redeem Apply a redeem code to the current wallet and return the granted credits and new balance. ```python Python theme={null} await sdk.credits.redeem(code) ``` ```typescript TypeScript theme={null} await sdk.credits.redeem(code: string) ``` Applies a redeem code (for example, a promotional code or a top-up voucher) to your client's wallet. On success, returns the credits granted by the code, the wallet's new total balance, and the expiry date of the newly funded bucket (if any). Pair with [`credits.get_balance`](/sdk-reference/credits/get-balance). Checking `warning_low` is a good place to prompt the operator for a code. ### Parameters The redeem code to apply, in its canonical format (for example `"SYN-XXXX-XXXX-XXXX-X"`). Codes are case-sensitive. ### Returns `RedeemResult` with the outcome of the redemption. Stable identifier for this redemption. Use it to correlate with the matching ledger entry from [`credits.get_ledger`](/sdk-reference/credits/get-ledger). Number of credits added to the wallet by this code. Total wallet balance after the redemption, in credits. When the newly funded bucket expires, if the code carries an expiry. `None` means the granted credits do not expire. ### Example ```python theme={null} from maximem_synap import MaximemSynapSDK from maximem_synap.errors import InvalidInputError, InsufficientCreditsError sdk = MaximemSynapSDK(api_key="synap_your_key_here") await sdk.initialize() try: result = await sdk.credits.redeem("SYN-XXXX-XXXX-XXXX-X") except InvalidInputError as exc: print(f"Code rejected: {exc}") except InsufficientCreditsError as exc: # Already redeemed, expired, or exhausted. print(f"Code not usable: {exc}") else: print( f"Added {result.credits_granted} credits " f"(new balance: {result.new_balance_credits})" ) if result.expires_at: print(f"These credits expire at {result.expires_at.isoformat()}") ``` ### Raises * `InvalidInputError`: when the code is malformed or unknown. * `InsufficientCreditsError`: when the code has already been redeemed, has expired, or has been exhausted. The exception's `balance_credits` field is `None` for these cases, so you can surface the error message directly. * `SynapAuthError`: when the API key is missing or invalid. ### See also * [credits.get\_balance](/sdk-reference/credits/get-balance) * [credits.get\_ledger](/sdk-reference/credits/get-ledger) * [credits.estimate](/sdk-reference/credits/estimate) # Error Codes Source: https://docs.maximem.ai/sdk-reference/errors All Synap API errors follow a consistent format with a human-readable `error` message, a machine-readable `code`, and an optional `details` object with additional context. ```json theme={null} { "error": "Human-readable error message", "code": "MACHINE_READABLE_CODE", "details": { "field": "additional context" } } ``` Use the `code` field for programmatic error handling. The `error` message may change between API versions, but `code` values are stable. *** ## Authentication Errors Errors related to API keys and credentials. **HTTP Status:** `401` **Description:** The request is missing authentication credentials or the provided credentials are not recognized. **Common Causes:** * Missing `Authorization` header * Malformed bearer token (e.g., missing `Bearer ` prefix) * API key does not exist in the system **Resolution:** * Verify the `Authorization: Bearer ` header is present * Check that the API key is correctly copied without leading or trailing whitespace * Generate a new API key from the Dashboard if the key may have been deleted ```json theme={null} { "error": "Missing or invalid authentication credentials", "code": "UNAUTHORIZED", "details": {} } ``` **HTTP Status:** `403` **Description:** The credentials are valid but do not have permission to perform the requested operation. **Common Causes:** * API key does not have access to the requested instance * Attempting to access another client's resources * Attempting an admin operation with a read-only key **Resolution:** * Verify the API key has the correct permissions in the Dashboard * Check that the instance belongs to the same client as the API key * Use an admin-level API key for management operations ```json theme={null} { "error": "Insufficient permissions for this operation", "code": "FORBIDDEN", "details": { "required_permission": "instances:write", "current_permissions": ["instances:read", "memories:read"] } } ``` **HTTP Status:** `401` **Description:** The provided API key is structurally valid but does not match any known credential in the system. **Common Causes:** * API key was rotated and the old key is being used * API key was revoked from the dashboard **Resolution:** * Check for recent credential rotation events in the audit log * Verify the SDK is using the correct API key for the target instance * Contact support with the correlation ID if the issue persists ```json theme={null} { "error": "Credential does not match any known credential", "code": "CREDENTIAL_INVALID", "details": {} } ``` *** ## Input Validation Errors Errors caused by invalid request parameters, missing required fields, or malformed data. **HTTP Status:** `400` **Description:** One or more request parameters are invalid, missing, or malformed. **Common Causes:** * Missing required field in the request body * Field value is the wrong type (e.g., string instead of integer) * Field value is out of the allowed range * Invalid enum value **Resolution:** * Check the `details` field for specific information about which fields are invalid * Refer to the endpoint documentation for required fields and valid values ```json theme={null} { "error": "Invalid input parameters", "code": "INVALID_INPUT", "details": { "fields": { "document_type": "Must be one of: ai-chat-conversation, human-chat-conversation, support-ticket, knowledge-article, document, email, note", "mode": "Required field is missing" } } } ``` **HTTP Status:** `429` **Description:** The API key has exceeded its rate limit for the current window. **Common Causes:** * High-volume ingestion without batching * Polling loops without backoff * Multiple processes sharing the same API key **Resolution:** * Implement exponential backoff with jitter * Use the `Retry-After` header to determine when to retry * Use batch endpoints for high-volume operations * Contact support to increase your rate limit ```json theme={null} { "error": "Rate limit exceeded. Retry after 30 seconds.", "code": "RATE_LIMITED", "details": { "limit": 600, "remaining": 0, "reset_at": "2025-01-15T10:01:00Z", "retry_after_seconds": 30 } } ``` *** ## Conflict Errors Errors returned when a request conflicts with the current state of a resource. These are **permanent**: the SDK does **not** retry them. **HTTP Status:** `409` **Class:** `ConflictError` (subclass of `SynapPermanentError`) **Description:** The request conflicts with the resource's current state and retrying the identical request will keep conflicting. **Common Causes:** * `conversation.compact()` when a compaction is already in progress for that conversation. **Behavioral change (SDK 0.4.0):** `compact()`'s "already in progress" 409 now raises `ConflictError` immediately, instead of being retried as a transient error and eventually surfacing as one. Catch `ConflictError` (or its base `SynapPermanentError`) where you previously caught the retry-exhausted transient error. **HTTP Status:** `409` **Class:** `TranscriptConflictError` (subclass of `ConflictError`) **Description:** `conversation.ingest_transcript()` was called with a `conversation_id` that was previously ingested with a **different** transcript. A call's transcript is immutable. **Resolution:** Mint a new `conversation_id` for a new call. Re-pushing the *identical* transcript is fine; it returns `status="duplicate"`, not an error. ```json theme={null} { "detail": { "code": "transcript_conflict", "message": "conversation_id '...' was previously ingested with a different transcript" } } ``` ## Resource Not Found Errors Errors returned when a requested resource does not exist. **HTTP Status:** `404` **Description:** The specified instance does not exist or is not accessible with the current credentials. **Common Causes:** * Typographical error in the instance ID * Instance was archived or deleted * Instance belongs to a different client **Resolution:** * Verify the instance ID format: `inst_` * List instances via `GET /dashboard/instances` to find valid IDs * Check the Dashboard for archived instances ```json theme={null} { "error": "Instance not found", "code": "INSTANCE_NOT_FOUND", "details": { "instance_id": "inst_nonexistent123456" } } ``` **HTTP Status:** `404` **Description:** The specified conversation could not be resolved for this instance. Fetching context for a brand-new `conversation_id` that simply has no messages yet does **not** raise this error. Context retrieval returns an empty result in that case. This error is reserved for conversations that cannot be resolved at all (wrong instance, malformed id, or purged data). **Common Causes:** * Conversation ID is incorrect or malformed * Conversation was started on a different instance * Conversation data has been purged due to retention policies **Resolution:** * Verify the conversation ID * Check that the conversation belongs to the correct instance * Review retention policies if the conversation may have expired ```json theme={null} { "error": "Conversation not found", "code": "CONVERSATION_NOT_FOUND", "details": { "conversation_id": "conv_nonexistent" } } ``` **HTTP Status:** `404` **Description:** The specified configuration version does not exist. **Common Causes:** * Typographical error in the config ID * Configuration was from a different instance * Configuration version was superseded and purged **Resolution:** * List configuration versions via `GET /instances/{id}/memory-architecture/versions` * Verify the config ID matches the target instance ```json theme={null} { "error": "Configuration version not found", "code": "CONFIG_NOT_FOUND", "details": { "config_id": "maca_cfg_nonexistent", "instance_id": "inst_f1e2d3c4b5a69078" } } ``` *** ## System Errors Errors caused by internal system issues or temporary unavailability. **HTTP Status:** `500` **Description:** An unexpected error occurred on Synap's side. This is not caused by the request and should be reported if it persists. **Common Causes:** * Unhandled exception in the processing pipeline * Database connectivity issue * Upstream service failure **Resolution:** * Retry the request with exponential backoff * If the error persists, contact support with the `X-Correlation-Id` header value * Check the [status page](https://synap.maximem.ai/status) for known incidents ```json theme={null} { "error": "An internal error occurred. Please try again.", "code": "INTERNAL_ERROR", "details": { "correlation_id": "req_7f3a2b1c-9d4e-4f5a-8b6c-1d2e3f4a5b6c" } } ``` **HTTP Status:** `503` **Description:** Synap is temporarily unable to process the request. The service will recover automatically. **Common Causes:** * Planned maintenance window * Auto-scaling event in progress * Dependent service temporarily unavailable **Resolution:** * Retry with exponential backoff using the `Retry-After` header * Check the [status page](https://synap.maximem.ai/status) for planned maintenance * Contact support if the issue persists beyond the maintenance window ```json theme={null} { "error": "Service is temporarily unavailable. Please retry.", "code": "SERVICE_UNAVAILABLE", "details": { "retry_after_seconds": 60, "reason": "maintenance" } } ``` *** ## Error Handling Best Practices Always use the `code` field for programmatic error handling, not the `error` message. Error messages may be localized or reworded. Retry `429`, `500`, and `503` errors with exponential backoff and jitter. Never retry `400`, `401`, `403`, or `404` errors without changing the request. Log the `X-Correlation-Id` from every error response. Include it when contacting support to expedite troubleshooting. Validate inputs on the client side before sending requests. This reduces unnecessary API calls and provides faster feedback to users. ### SDK Error Handling The Python SDK wraps API errors in typed exceptions: ```python theme={null} import uuid from maximem_synap import ( AuthenticationError, # 401, 403 ContextNotFoundError, # 404 RateLimitError, # 429 ServiceUnavailableError,# 503 InvalidInputError, # 400 SynapError, # base class for all SDK errors ) # conversation_id must be a valid UUID try: context = await sdk.conversation.context.fetch( conversation_id=str(uuid.uuid4()), search_query=["user preferences"], ) except RateLimitError as e: print(f"Rate limited. Retry after {e.retry_after} seconds.") except ContextNotFoundError as e: print(f"Conversation not found: {e.details}") except ServiceUnavailableError as e: print(f"Server unavailable. Correlation ID: {e.correlation_id}") ``` For the full list of SDK exception classes and which to catch when, see [SDK Error Handling](/sdk/error-handling). # instance.listen Source: https://docs.maximem.ai/sdk-reference/instance/listen Start a bidirectional gRPC stream that delivers real-time anticipated context bundles to your agent. ```python Python theme={null} await sdk.instance.listen( on_reconnect=None, on_disconnect=None, on_context=None, ) ``` ```typescript TypeScript theme={null} await sdk.instance.listen(options?: ListenOptions) ``` Advanced: for real-time integrations. Most applications only need [`fetch`](/sdk-reference/context/fetch) and [`memories.create`](/sdk-reference/memories/create). Use `listen` when you want Synap to proactively push anticipated context to your agent over a long-lived stream, instead of waiting for each `fetch()` round-trip. `listen()` opens a bidirectional gRPC stream between the SDK and the Synap platform. Once the stream is active, you can call [`instance.send_message`](/sdk-reference/instance/send-message) to broadcast agent activity (user messages, tool calls, context requests), and the platform streams back anticipated context bundles. Each incoming bundle is automatically stored in the SDK's anticipation cache so subsequent `fetch()` calls return instantly, and your `on_context` callback fires for any custom handling you need. The stream is a latency optimization layered on top of the normal request-response API: it never changes what `fetch()` returns, only how fast it returns. It is not memory-neutral, though: conversation turns sent over it are persisted and are promoted into long-term memory when the conversation compacts. See [Real-Time Anticipation](/concepts/real-time-anticipation) for the full model. Connection targets come from [`SDKConfig`](/sdk/configuration): `grpc_host`, `grpc_port`, and `grpc_use_tls`. ### Events the SDK sends for you Once the stream is live, the SDK instruments `fetch()` automatically. You do not write this code, but these events flow on your stream: | Event | Emitted when | | ------------------- | -------------------------------------------------- | | `context_fetch` | A retrieval is requested | | `context_used` | A retrieval was served from the anticipation cache | | `context_assembled` | The SDK finalizes what it composed for the model | They drive the platform's learning loop (prefetch scoring, per-pattern hit rates, and the Requests page audit trail) and carry ids and counts only, never raw prompt content. ### One stream per instance In a long-lived or multi-tenant server, open **one** stream for the process and distinguish users via the `user_id` / `customer_id` on each `send_message` and `fetch`. Do not open a stream per user session: the platform enforces per-instance and per-client stream quotas (currently 100 concurrent streams per instance, 200 per client), and a stream-per-session design saturates them under real concurrency, sending surplus connections into a `RESOURCE_EXHAUSTED` reconnect loop. Streams have a maximum lifetime of one hour, after which the server closes them and the SDK reconnects with exponential backoff (10 attempts; the counter resets on each successful connect). Periodic reconnects are expected; they are not an error condition. ### Parameters Callback invoked when the underlying stream reconnects after a transient failure. Receives the attempt count as its only argument. Useful for logging or surfacing connection health in your UI. Callback invoked when the stream disconnects. Receives the disconnect reason as a string. Callback invoked each time the platform pushes an anticipated context bundle. The bundle dict contains keys like `items_by_type`, `retrieval_mode`, and `bundle_id`. Bundles are also written to the SDK's anticipation cache automatically. You only need this callback if you want to react to bundles directly (e.g., to prefetch UI state). Sync and async callables are both supported. All three callbacks are optional, and `on_context` is optional even if you rely on anticipation: bundles reach the cache whether or not you supply it. Note the arity: `on_reconnect` takes the attempt count and `on_disconnect` takes the reason. A zero-argument callback raises `TypeError` at the moment the stream drops. ### Returns Returns `None`. The coroutine resolves once the stream is established; the stream itself stays open until you call [`instance.stop_listening`](/sdk-reference/instance/stop-listening). ### Example ```python theme={null} import asyncio import uuid from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK(api_key="synap_your_key_here") await sdk.initialize() # conversation_id must be a valid UUID conversation_id = str(uuid.uuid4()) def on_context(bundle): print(f"Got anticipated bundle {bundle.get('bundle_id')}") # Run listen() in a background task so the main loop can send messages. listen_task = asyncio.create_task( sdk.instance.listen(on_context=on_context) ) try: await sdk.instance.send_message( content="What's my account balance?", user_id="user_789", customer_id="cust_456", conversation_id=conversation_id, ) # ... your agent loop continues here, calling send_message # for each user turn, tool call, and context request. # # Streamed turns reach long-term memory only when this conversation # compacts. Call memories.create() for anything that must be # retrievable before then. finally: await sdk.instance.stop_listening() listen_task.cancel() ``` ### Raises * `SDKNotInitializedError`: when `initialize()` has not been called. * `AuthenticationError`: when credentials are rejected by the platform. See [Error Codes](/sdk-reference/errors) for the full SDK exception hierarchy. ### See also * [Real-Time Anticipation](/concepts/real-time-anticipation): what the stream does and what it deliberately does not do. * [Real-Time Anticipation in a Server](/patterns/real-time-anticipation-server): running one shared stream in a multi-tenant process. * [instance.send\_message](/sdk-reference/instance/send-message): push agent activity over the active stream. * [instance.stop\_listening](/sdk-reference/instance/stop-listening): close the stream and release resources. * [fetch](/sdk-reference/context/fetch): request-response context retrieval (no streaming required). ## JavaScript: opening the stream The stream lets the server push context bundles ahead of time, which the SDK then serves locally instead of making a billed retrieval. It is opt-in. ```bash theme={null} npm install @grpc/grpc-js @grpc/proto-loader ``` ```ts theme={null} await synap.instance.listen(); // Report a turn as it happens, so the server can anticipate the next one. await synap.instance.send_message({ content: "What seat do I usually pick?", role: "user", conversation_id, user_id, customer_id, }); // When you are done. await synap.instance.stop_listening(); ``` Without the stream, every retrieval is a cloud fetch. With it, turns the server anticipated correctly are answered from a local bundle. Whether that is worth a persistent connection depends on your traffic shape. # instance.send_message Source: https://docs.maximem.ai/sdk-reference/instance/send-message Send a conversation event over the active gRPC stream so Synap can anticipate context for the agent's next turn. ```python Python theme={null} await sdk.instance.send_message( content, role="user", conversation_id=None, user_id=None, customer_id=None, session_id=None, event_type="user_message", metadata=None, tool_name=None, tool_args=None, search_queries=None, context_types=None, ) ``` ```typescript TypeScript theme={null} await sdk.instance.send_message(options: SendMessageOptions) ``` Advanced: for real-time integrations. Requires an active [`instance.listen`](/sdk-reference/instance/listen) stream. If the stream is not active, this call raises `ListeningNotActiveError`. `send_message()` publishes a single agent activity event onto the bidirectional gRPC stream that `listen()` established. Each event tells the Synap platform what just happened in your agent (a user turn, an assistant reply, a tool call, or an explicit context request) so the platform can anticipate what context the agent will need next and push it back over the stream. **`send_message()` does not ingest the turn when you call it, but the turn is not discarded either.** Conversation events are persisted to conversation history, and when the conversation compacts (by default at 3,000 tokens or 10 messages), those raw turns are promoted into the same ingestion pipeline `memories.create()` uses. For an agent reporting conversation turns this is sufficient on its own; see [Agent Integration](/setup/agent-integration). It is deferred, not conditional: compaction fires at 3,000 tokens, 10 messages, **or 5 minutes of inactivity**, so every conversation gets there. Call [`memories.create`](/sdk-reference/memories/create) only for content that is not a conversation turn, or that must be retrievable sooner. Never for the same text you streamed, which would extract it twice. ### What the platform does with the event | `event_type` | Effect | | ------------------- | ------------------------------------------------------------------------------ | | `user_message` | Persisted to conversation history; signals a new turn | | `assistant_message` | Persisted to conversation history; **triggers anticipation for the next turn** | | `tool_call` | Observed for situational awareness (informative, not a trigger) | | `context_request` | `search_queries` / `context_types` used as direct anticipation hints | Persisted turns feed both layers of context. They advance the conversation toward automatic [context compaction](/sdk-reference/conversation-context/compact) once it crosses the configured token or message threshold, and that same compaction promotes the raw turns into long-term memory. A `user_message` or `assistant_message` is persisted only when **both** `user_id` and `customer_id` are present. If either is missing, the platform skips persistence and logs server-side, and your application receives no error and no exception. On a B2C instance send `user_id` only: `customer_id` is not accepted there, and an event without one is persisted normally. Emit `assistant_message` **after** your agent produces its reply, not before. Anticipation runs between turns, so this event is what warms the cache for the *next* turn. ### Parameters The message content. For `user_message` and `assistant_message` events this is the natural-language turn; for `tool_call` events it can be a short description of the tool invocation. Either `"user"` or `"assistant"`. External identifier for the conversation this event belongs to. Required to associate the event with the right conversation scope. Must be a valid UUID [registered via `record_message`](/concepts/context-end-to-end#short-term-context). External user identifier. Omit for customer- or client-scope events. External customer identifier. Required on B2B. **Not accepted on B2C**: passing it is rejected with HTTP 400. See [B2C vs B2B](/concepts/memory-scopes#b2c-vs-b2b-which-scopes-apply-to-you). External session identifier. The kind of event being reported. Common values: `user_message`, `assistant_message`, `tool_call`, `context_request`. Additional string key-value metadata attached to the event. For `tool_call` events: the name of the tool the agent is invoking. The platform uses this to classify the tool call and anticipate the agent's next data needs. For `tool_call` events: a JSON-encodable arguments dict for the tool invocation. For `tool_call` or `context_request` events: the retrieval queries the agent plans to run. Used as direct anticipation hints. For `tool_call` or `context_request` events: the memory categories the agent plans to fetch. ### Returns Returns `None`. The coroutine resolves once the event has been written to the stream. ### Example ```python Python theme={null} import uuid # conversation_id must be a valid UUID; reuse the one for the active conversation conversation_id = str(uuid.uuid4()) await sdk.instance.send_message( content="search_orders", role="assistant", event_type="tool_call", conversation_id=conversation_id, user_id="user_789", customer_id="cust_456", tool_name="search_orders", tool_args={"status": "shipped", "limit": 5}, search_queries=["recent shipped orders"], context_types=["order_history"], ) ``` ```javascript JavaScript theme={null} import { randomUUID } from 'node:crypto'; // conversation_id must be a valid UUID; reuse the one for the active conversation let conversation_id = randomUUID(); await sdk.instance.send_message({ content: 'search_orders', role: 'assistant', event_type: 'tool_call', conversation_id: conversation_id, user_id: 'user_789', customer_id: 'cust_456', tool_name: 'search_orders', tool_args: {'status': 'shipped', 'limit': 5}, search_queries: ['recent shipped orders'], context_types: ['order_history'], }); ``` ```typescript TypeScript theme={null} import { randomUUID } from 'node:crypto'; // conversation_id must be a valid UUID; reuse the one for the active conversation let conversation_id = randomUUID(); await sdk.instance.send_message({ content: 'search_orders', role: 'assistant', event_type: 'tool_call', conversation_id: conversation_id, user_id: 'user_789', customer_id: 'cust_456', tool_name: 'search_orders', tool_args: {'status': 'shipped', 'limit': 5}, search_queries: ['recent shipped orders'], context_types: ['order_history'], }); ``` ### Raises * `ListeningNotActiveError`: when [`instance.listen`](/sdk-reference/instance/listen) has not been called or the stream has been closed. See [Error Codes](/sdk-reference/errors) for the full SDK exception hierarchy. ### See also * [Real-Time Anticipation](/concepts/real-time-anticipation): how streaming and ingestion fit together. * [instance.listen](/sdk-reference/instance/listen): open the stream this method writes to. * [instance.stop\_listening](/sdk-reference/instance/stop-listening): close the stream. * [memories.create](/sdk-reference/memories/create): the call that actually creates memories. # instance.stop_listening Source: https://docs.maximem.ai/sdk-reference/instance/stop-listening Close the active gRPC stream opened by instance.listen and release the transport. ```python Python theme={null} await sdk.instance.stop_listening() ``` ```typescript TypeScript theme={null} await sdk.instance.stop_listening() ``` Advanced: for real-time integrations. Pair this with [`instance.listen`](/sdk-reference/instance/listen) and call it during shutdown (or in a `finally` block) so the stream is closed cleanly. `stop_listening()` ends the bidirectional gRPC stream that `listen()` established, cancels the underlying transport, and clears the SDK's reference to it. After this call returns, `instance.is_listening` is `False` and any subsequent `instance.send_message` calls raise `ListeningNotActiveError`. Calling `stop_listening()` when no stream is active is a safe no-op. Your application owns the stream's lifecycle. Nothing closes it for you except [`shutdown()`](/sdk-reference/lifecycle/shutdown), so a long-lived server should call this from its shutdown path. See [Real-Time Anticipation in a Server](/patterns/real-time-anticipation-server). Closing the stream does not lose data. Turns already sent with [`send_message`](/sdk-reference/instance/send-message) are persisted server-side and still reach long-term memory when the conversation compacts, whether or not the stream is open. Closing ends anticipation only; `fetch()` continues to work over the normal request-response path. ### Parameters This method takes no parameters. ### Returns Returns `None`. ### Example ```python theme={null} import asyncio import uuid from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK(api_key="synap_your_key_here") await sdk.initialize() # conversation_id must be a valid UUID conversation_id = str(uuid.uuid4()) listen_task = asyncio.create_task(sdk.instance.listen()) try: await sdk.instance.send_message( content="Hello", user_id="user_789", customer_id="cust_456", # required: without both ids the turn is dropped conversation_id=conversation_id, ) finally: await sdk.instance.stop_listening() listen_task.cancel() ``` ### Raises This method does not raise SDK errors under normal use. See [Error Codes](/sdk-reference/errors) for the full SDK exception hierarchy. ### See also * [Real-Time Anticipation](/concepts/real-time-anticipation): what the stream does and does not do. * [instance.listen](/sdk-reference/instance/listen): start the stream. * [instance.send\_message](/sdk-reference/instance/send-message): push events while the stream is open. * [shutdown](/sdk-reference/lifecycle/shutdown): full SDK teardown, which also tears down any active stream. # as_tool Source: https://docs.maximem.ai/sdk-reference/lifecycle/as-tool Return an LLM-ready tool definition for fetching Synap context. ```python Python theme={null} sdk.as_tool(*, scope="user", user_id=None, customer_id=None, conversation_id=None, name=None, description=None, style="openai") ``` ```typescript TypeScript theme={null} sdk.as_tool(options?: AsToolOptions) ``` Produces a tool definition dict you can pass straight into an OpenAI or Anthropic tool-calling loop. The returned dict carries the JSON schema, a scope-appropriate description, and a bound async `handler` coroutine the host runtime invokes when the LLM calls the tool. Scope identifiers (`user_id`, `customer_id`, `conversation_id`) are closed over inside the handler so the LLM cannot accidentally drop them. The per-user privacy filter holds regardless of what the model passes at call time. Prefer `sdk.fetch(...)` as the default integration; reach for `as_tool` only when the LLM genuinely needs agency over when context is fetched mid-reasoning. ### Parameters Which scope the tool fetches from. One of `"conversation"`, `"user"`, `"customer"`, `"client"`, or `"unified"` (cross-scope). Defaults to `"user"`. Closed-over user identifier. Required when `scope="user"`, and strongly recommended for `"conversation"` and `"unified"` so the per-user privacy filter applies. Closed-over customer identifier. Required when `scope="customer"`. Required on B2B. **Not accepted on B2C**: passing it is rejected with HTTP 400. See [B2C vs B2B](/concepts/memory-scopes#b2c-vs-b2b-which-scopes-apply-to-you). Optional closed-over conversation identifier. When provided, the tool always fetches for this conversation; when omitted, the LLM supplies it per call. Must be a valid UUID [registered via `record_message`](/concepts/context-end-to-end#short-term-context). Override the tool name exposed to the LLM. Defaults to `synap_fetch_{scope}_context`. Override the tool description. Defaults to a scope-specific blurb that primes the LLM to call it for context retrieval. Output dict shape. `"openai"` returns `{"type": "function", "function": {...}}`; `"anthropic"` returns `{"name", "description", "input_schema"}`. Defaults to `"openai"`. ### Returns A tool definition dict shaped for the requested `style`. The dict always carries an async `handler` key with the bound coroutine; host runtimes that don't use it can ignore it. Present in `"openai"` style only. Always `"function"`. Present in `"openai"` style only. Contains `name`, `description`, and `parameters` (the JSON schema). Present in `"anthropic"` style. The tool name. Present in `"anthropic"` style. The tool description. Present in `"anthropic"` style. JSON schema describing the tool's call-time arguments. Async coroutine the host runtime should `await` with the LLM's tool-call arguments. Returns the same shape `sdk.fetch(...)` produces. ### Example ```python theme={null} from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK(api_key="synap_your_key_here") await sdk.initialize() # OpenAI-style tool, bound to a specific user. tool = sdk.as_tool(scope="user", user_id="user-456") # Hand the schema to the LLM, then route tool calls through the handler. tool_call_args = {"search_query": ["dietary preferences"]} result = await tool["handler"](**tool_call_args) print(result["formatted_context"]) # Anthropic-style, cross-scope. anthropic_tool = sdk.as_tool( scope="unified", user_id="user-456", customer_id="cust-789", style="anthropic", ) ``` ### Raises * `InvalidInputError`: when `scope` is not one of the accepted values, when `scope="user"` is requested without `user_id`, when `scope="customer"` is requested without `customer_id`, or when `style` is not `"openai"` or `"anthropic"`. See [Error Codes](/sdk-reference/errors) for the full SDK exception hierarchy. ### See also * [fetch](/sdk-reference/context/fetch): the recommended pre-fetch integration path. * [initialize](/sdk-reference/lifecycle/initialize): required before calling `as_tool`'s handler. ## JavaScript: as a tool definition `as_tool()` returns a tool definition with the scope identifiers closed over, so the model chooses the query but never whose memory to read. ```ts theme={null} const tool = synap.as_tool({ scope: "user", user_id, customer_id }); // OpenAI shape by default; pass style: "anthropic" for the other. const result = await tool.handler({ search_query: ["loyalty status"] }); ``` # configure Source: https://docs.maximem.ai/sdk-reference/lifecycle/configure Update SDK configuration. Must be called before initialize(). ```python Python theme={null} sdk.configure(**kwargs) ``` ```typescript TypeScript theme={null} sdk.configure(options?: ConfigureOptions) ``` Adjusts SDK-wide settings such as cache backend, session timeout, retry policy, and logging. `configure()` is synchronous and **must be called before `initialize()`**. Once the SDK is initialized, re-configuration is rejected to prevent inconsistent runtime state. Any options not supplied keep their existing values. ### Parameters Override the default cache storage path on disk. Cache backend selection. Pass `"sqlite"` to enable on-disk caching, or `None` to disable it. Session timeout in minutes. Accepted range is `5`-`1440`. Per-operation timeout overrides. Pass either a `TimeoutConfig` instance or a plain dict with the fields you want to override. Retry policy for transient failures. Pass a `RetryPolicy`, a dict, or `None` to disable retries entirely. Logging verbosity. One of `"DEBUG"`, `"INFO"`, `"WARNING"`, `"ERROR"`. Custom logger instance to replace the SDK's internal logger. ### Returns Returns `None`. ### Example ```python Python theme={null} from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK(api_key="synap_your_key_here") # Configure BEFORE initialize(). sdk.configure( cache_backend="sqlite", session_timeout_minutes=60, log_level="INFO", ) await sdk.initialize() ``` ```javascript JavaScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); // Configure BEFORE initialize(). sdk.configure({ cache_backend: 'sqlite', session_timeout_minutes: 60, log_level: 'INFO', }); await sdk.initialize(); ``` ```typescript TypeScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); // Configure BEFORE initialize(). sdk.configure({ cache_backend: 'sqlite', session_timeout_minutes: 60, log_level: 'INFO', }); await sdk.initialize(); ``` ### Raises * `InvalidInputError`: when `configure()` is called after `initialize()`, or when an option value falls outside its accepted range. See [Error Codes](/sdk-reference/errors) for the full SDK exception hierarchy. ### See also * [initialize](/sdk-reference/lifecycle/initialize): the next step after `configure()`. * [shutdown](/sdk-reference/lifecycle/shutdown): graceful teardown on exit. # initialize Source: https://docs.maximem.ai/sdk-reference/lifecycle/initialize Initialize the SDK. Must be called before any context operations. ```python Python theme={null} await sdk.initialize() ``` ```typescript TypeScript theme={null} await sdk.initialize() ``` Performs the one-time setup that must run before any other SDK call. `initialize()` resolves the Synap API key (from the `api_key=` constructor kwarg first, then the `SYNAP_API_KEY` environment variable), establishes the authenticated client identity, sets up the optional local cache, and starts the telemetry collector. Calling it a second time on the same SDK instance is a safe no-op. ### Parameters This method takes no parameters. ### Returns Returns `None`. ### Example ```python Python theme={null} import os from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK(api_key=os.environ["SYNAP_API_KEY"]) await sdk.initialize() # SDK is now ready for context operations. ctx = await sdk.fetch(user_id="user-456") ``` ```javascript JavaScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: process.env.SYNAP_API_KEY }); await sdk.initialize(); // SDK is now ready for context operations. const ctx = await sdk.fetch({ user_id: 'user-456', }); ``` ```typescript TypeScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: process.env.SYNAP_API_KEY }); await sdk.initialize(); // SDK is now ready for context operations. const ctx = await sdk.fetch({ user_id: 'user-456', }); ``` ### Raises * `AuthenticationError`: when no API key is supplied (neither `api_key=` nor `SYNAP_API_KEY`), the key is rejected by the platform, or credential loading otherwise fails. See [Error Codes](/sdk-reference/errors) for the full SDK exception hierarchy. ### See also * [configure](/sdk-reference/lifecycle/configure): adjust SDK-wide options before `initialize()`. * [shutdown](/sdk-reference/lifecycle/shutdown): flush telemetry and release resources on exit. * [fetch](/sdk-reference/context/fetch): the recommended cross-scope context entry point. # shutdown Source: https://docs.maximem.ai/sdk-reference/lifecycle/shutdown Gracefully shutdown the SDK. Flushes telemetry, closes connections, and releases resources. ```python Python theme={null} await sdk.shutdown() ``` ```typescript TypeScript theme={null} await sdk.shutdown() ``` Performs an orderly teardown: emits a final telemetry event, stops the telemetry collector, closes the underlying transports, and releases the cache handle. Call `shutdown()` during process exit (or on hot-reload teardown in long-running services) to ensure pending telemetry is flushed and resources are not leaked. After `shutdown()`, the same SDK instance is no longer usable. Create a new instance if you need to reconnect. `shutdown()` also releases the identities this SDK was reachable by (the API key it was built with, and the `instance_id` that `initialize()` resolved), so the next construction builds a genuinely fresh SDK rather than handing back this closed one. That is what makes reconnecting, and picking up a rotated API key without restarting the process, work. It only releases an identity that still points at this SDK, so shutting one down never disconnects a replacement that has already taken its place. In `maximem-synap` 0.4.0 and earlier this was not the case: the entry outlived teardown and the next caller was handed the shut-down SDK, with its transports and stream already closed. Fixed in 0.4.1. ### Parameters This method takes no parameters. ### Returns Returns `None`. ### Example ```python theme={null} from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK(api_key="synap_your_key_here") await sdk.initialize() try: ctx = await sdk.fetch(user_id="user-456") # ... use ctx ... finally: await sdk.shutdown() ``` ### Raises No method-specific exceptions; see [Error Codes](/sdk-reference/errors) for the general SDK exception hierarchy. ### See also * [initialize](/sdk-reference/lifecycle/initialize): the matching startup call. * [configure](/sdk-reference/lifecycle/configure): adjust SDK options before startup. # memories.batch_create Source: https://docs.maximem.ai/sdk-reference/memories/batch-create Ingest multiple documents in a single call for bootstrap or bulk ingestion. ```python Python theme={null} await sdk.memories.batch_create(documents, fail_fast=False) ``` ```typescript TypeScript theme={null} await sdk.memories.batch_create(options: BatchCreateOptions) ``` Ingest multiple documents in a single call. This is the primary method for **bootstrap ingestion**: loading historical conversations, backfilling knowledge bases, and migrating data from other systems. Each document is processed independently through the ingestion pipeline. ### Parameters List of `CreateMemoryRequest` objects. Each entry has the same fields as [`memories.create`](/sdk-reference/memories/create) (document, document\_type, user\_id, customer\_id, mode, metadata, etc.). `customer_id` is required on B2B and not accepted on B2C, where an entry carrying it is rejected with HTTP 400 ([B2C vs B2B](/concepts/memory-scopes#b2c-vs-b2b-which-scopes-apply-to-you)); `mode` is the ingestion axis (`fast` vs `long-range`) of [Retrieval Modes](/concepts/retrieval-modes). If `True`, the entire batch is rejected as soon as any single document fails validation. If `False` (default), valid documents are accepted and invalid ones are returned with `error_message` populated in the per-item result. ### Returns `BatchCreateResponse` with aggregate counts and per-document results. Unique identifier for the batch submission. Total number of documents submitted. Number of documents successfully queued. Number of documents that failed validation. Per-document results. Each contains `ingestion_id`, `document_id`, `status`, `queued_at`, and an optional `error_message` if that document was rejected. ### Example ```python Python theme={null} from maximem_synap import MaximemSynapSDK from maximem_synap.memories.models import CreateMemoryRequest sdk = MaximemSynapSDK(api_key="synap_your_key_here") await sdk.initialize() batch = await sdk.memories.batch_create( documents=[ CreateMemoryRequest( document="User: My favorite color is blue.\nAssistant: Noted!", document_type="ai-chat-conversation", user_id="user_789", customer_id="cust_456", mode="fast", ), CreateMemoryRequest( document="User: I work at Acme Corp as a senior engineer.", document_type="ai-chat-conversation", user_id="user_789", customer_id="cust_456", mode="fast", ), ], fail_fast=False, ) print(f"{batch.succeeded}/{batch.total} queued") ``` ```javascript JavaScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); await sdk.initialize(); const batch = await sdk.memories.batch_create({ documents: [ { document: "User: My favorite color is blue.\nAssistant: Noted!", document_type: 'ai-chat-conversation', user_id: 'user_789', customer_id: 'cust_456', mode: 'fast' }, { document: 'User: I work at Acme Corp as a senior engineer.', document_type: 'ai-chat-conversation', user_id: 'user_789', customer_id: 'cust_456', mode: 'fast' }, ], fail_fast: false, }); console.log(`${batch.succeeded}/${batch.total} queued`); ``` ```typescript TypeScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); await sdk.initialize(); const batch = await sdk.memories.batch_create({ documents: [ { document: "User: My favorite color is blue.\nAssistant: Noted!", document_type: 'ai-chat-conversation', user_id: 'user_789', customer_id: 'cust_456', mode: 'fast' }, { document: 'User: I work at Acme Corp as a senior engineer.', document_type: 'ai-chat-conversation', user_id: 'user_789', customer_id: 'cust_456', mode: 'fast' }, ], fail_fast: false, }); console.log(`${batch.succeeded}/${batch.total} queued`); ``` Batch ingestion is more efficient than individual calls when you have multiple documents to process. The maximum batch size is 100 documents per call. For large-scale bootstrap operations, see the [Bootstrap Ingestion](/concepts/how-ingestion-works#bootstrap-ingestion) guide. ### Raises * `SynapAuthError`: when the API key is missing or invalid. * `SynapValidationError`: when the batch payload is malformed or exceeds the maximum size. ### See also * [memories.create](/sdk-reference/memories/create) * [memories.create\_from\_file](/sdk-reference/memories/create-from-file) * [memories.status](/sdk-reference/memories/status) # memories.create Source: https://docs.maximem.ai/sdk-reference/memories/create Ingest a document into Synap's memory pipeline asynchronously. ```python Python theme={null} await sdk.memories.create(document, ...) ``` ```typescript TypeScript theme={null} await sdk.memories.create(options: CreateMemoryOptions) ``` Ingest a document into Synap's memory pipeline. The document is processed asynchronously through the extraction, categorization, entity resolution, and storage stages. The call returns immediately with an `ingestion_id` you can poll via `memories.status()`. ### Parameters The raw document content to ingest. This can be a conversation transcript, a knowledge base article, a support ticket, or any text content. The type of document being ingested. This determines how the ingestion pipeline processes the content. | Value | Description | | ------------------------- | --------------------------------------------------------- | | `ai-chat-conversation` | A conversation between a user and an AI assistant | | `document` | A generic document or report | | `email` | An email message or thread | | `pdf` | PDF document content (text extracted) | | `image` | Image descriptions or OCR text | | `audio` | Audio transcriptions | | `meeting-transcript` | Meeting transcription content | | `human-chat-conversation` | A conversation between two or more humans *(coming soon)* | | `support-ticket` | A customer support ticket or thread *(coming soon)* | | `knowledge-article` | A structured knowledge base article *(coming soon)* | | `note` | A freeform note or annotation *(coming soon)* | An optional external identifier for deduplication. If a document with this ID has already been ingested, the request is rejected as a conflict. Timestamp of when the document was originally created. Used for temporal ordering of memories. Defaults to the current time if not provided. Always provide `document_created_at` when backfilling historical data. Accurate timestamps improve temporal reasoning during retrieval (e.g., "What did the user say last month?"). The external user ID this memory is about. Omit for customer- or client-scope ingestion. The external customer ID this memory belongs to. Required on B2B. **Not accepted on B2C**: passing it is rejected with HTTP 400. See [B2C vs B2B](/concepts/memory-scopes#b2c-vs-b2b-which-scopes-apply-to-you). Omit for client-scope ingestion, and omit it always on a B2C instance. The effective scope is derived by the server from the IDs you pass and the instance's user-context isolation (B2C/B2B). The ingestion processing mode: the ingestion axis (`fast` vs `long-range`) of [Retrieval Modes](/concepts/retrieval-modes). | Value | Description | | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `fast` | Optimized for speed. Uses lighter extraction models. Best for high-volume, real-time ingestion. | | `long-range` | Optimized for quality. Runs the full extraction pipeline including deep entity resolution, relationship mapping, and graph storage. Best for conversations and important documents. | Arbitrary key-value pairs to attach to the memory. Useful for filtering and organizing memories. ### Returns `CreateMemoryResponse` with the ingestion job identifiers and initial status. Unique identifier for tracking this ingestion job. Pass this to [`memories.status`](/sdk-reference/memories/status) or [`memories.wait_for_completion`](/sdk-reference/memories/wait-for-completion). The document identifier (either the one you supplied or one generated by the server). Initial status of the ingestion job. Always `queued` for new submissions. Server timestamp at which the job was accepted into the queue. ### Example ```python Python theme={null} import uuid from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK(api_key="synap_your_key_here") await sdk.initialize() # conversation_id must be a valid UUID when used to identify a conversation conversation_id = str(uuid.uuid4()) result = await sdk.memories.create( document=( "User: I just moved to San Francisco and I'm looking for a good coffee shop.\n" "Assistant: Welcome to SF! There are some great options. Do you prefer light or dark roasts?\n" "User: Definitely light roasts. I'm also a big fan of pour-over." ), document_type="ai-chat-conversation", user_id="user_789", customer_id="cust_456", mode="long-range", metadata={"conversation_id": conversation_id, "source": "web-chat"}, ) print(result.ingestion_id) ``` ```javascript JavaScript theme={null} import { randomUUID } from 'node:crypto'; import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); await sdk.initialize(); // conversation_id must be a valid UUID when used to identify a conversation const conversation_id = randomUUID(); const result = await sdk.memories.create({ document: ( "User: I just moved to San Francisco and I'm looking for a good coffee shop.\nAssistant: Welcome to SF! There are some great options. Do you prefer light or dark roasts?\nUser: Definitely light roasts. I'm also a big fan of pour-over." ), document_type: 'ai-chat-conversation', user_id: 'user_789', customer_id: 'cust_456', mode: 'long-range', metadata: {'conversation_id': conversation_id, 'source': 'web-chat'}, }); console.log(result.ingestion_id); ``` ```typescript TypeScript theme={null} import { randomUUID } from 'node:crypto'; import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); await sdk.initialize(); // conversation_id must be a valid UUID when used to identify a conversation const conversation_id = randomUUID(); const result = await sdk.memories.create({ document: ( "User: I just moved to San Francisco and I'm looking for a good coffee shop.\nAssistant: Welcome to SF! There are some great options. Do you prefer light or dark roasts?\nUser: Definitely light roasts. I'm also a big fan of pour-over." ), document_type: 'ai-chat-conversation', user_id: 'user_789', customer_id: 'cust_456', mode: 'long-range', metadata: {'conversation_id': conversation_id, 'source': 'web-chat'}, }); console.log(result.ingestion_id); ``` Ingestion is asynchronous. A successful return means the document has been accepted for processing, not that extraction is complete. Use [`memories.status`](/sdk-reference/memories/status), [`memories.wait_for_completion`](/sdk-reference/memories/wait-for-completion), or [webhooks](/dashboard/webhooks) to track progress. ### Raises * `SynapAuthError`: when the API key is missing or invalid. * `SynapValidationError`: when required fields are missing or `document_type` / `mode` are not recognized. * `SynapConflictError`: when a document with the same `document_id` has already been ingested. ### See also * [memories.batch\_create](/sdk-reference/memories/batch-create) * [memories.create\_from\_file](/sdk-reference/memories/create-from-file) * [memories.status](/sdk-reference/memories/status) * [memories.wait\_for\_completion](/sdk-reference/memories/wait-for-completion) # memories.create_from_file Source: https://docs.maximem.ai/sdk-reference/memories/create-from-file Ingest a file from disk, an open file-like object, or raw text into the memory pipeline. ```python Python theme={null} await sdk.memories.create_from_file(user_id, customer_id, ...) ``` ```typescript TypeScript theme={null} await sdk.memories.create_from_file(options: CreateFromFileOptions) ``` Ingest a file or raw text into the memory pipeline. Use this when you want Synap to upload and parse a document (PDF, text file, transcript, etc.) rather than passing a string directly. Exactly one of `file_path`, `file`, or `text` must be provided. ### Parameters The user this memory is about. The customer this memory belongs to. Required on B2B. **Not accepted on B2C**: passing it is rejected with HTTP 400. See [B2C vs B2B](/concepts/memory-scopes#b2c-vs-b2b-which-scopes-apply-to-you). Scope hint for the upload. One of `"b2c"` (default) or `"b2b"`. Path on disk to read and upload. Mutually exclusive with `file` and `text`. An open binary file-like object. When using this, you must also pass `filename`. Mutually exclusive with `file_path` and `text`. Name to use for the upload. Required when passing `file=`. Raw text content to ingest instead of a file. Mutually exclusive with `file_path` and `file`. Override the auto-detected document type (for example, `"pdf"`, `"email"`, `"meeting-transcript"`). Ingestion mode: `"fast"` or `"long-range"` (default), the ingestion axis of [Retrieval Modes](/concepts/retrieval-modes). Additional metadata to attach to the memory. Serialized to JSON before upload. ### Returns `CreateMemoryResponse` with the ingestion job identifiers and initial status. Unique identifier for tracking this ingestion job. The document identifier assigned to the upload. Initial status of the ingestion job. Typically `queued`. Timestamp at which the job was accepted into the queue. ### Example ```python Python theme={null} from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK(api_key="synap_your_key_here") await sdk.initialize() # Upload from disk result = await sdk.memories.create_from_file( user_id="user_789", customer_id="cust_456", file_path="/data/transcripts/2026-05-17-call.pdf", document_type="pdf", mode="long-range", metadata={"source": "support-call"}, ) print(result.ingestion_id) # Upload raw text result = await sdk.memories.create_from_file( user_id="user_789", customer_id="cust_456", text="Customer prefers email follow-ups over phone.", document_type="note", ) ``` ```javascript JavaScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); await sdk.initialize(); // Upload from disk let result = await sdk.memories.create_from_file({ user_id: 'user_789', customer_id: 'cust_456', file_path: '/data/transcripts/2026-05-17-call.pdf', document_type: 'pdf', mode: 'long-range', metadata: {'source': 'support-call'}, }); console.log(result.ingestion_id); // Upload raw text result = await sdk.memories.create_from_file({ user_id: 'user_789', customer_id: 'cust_456', text: 'Customer prefers email follow-ups over phone.', document_type: 'note', }); ``` ```typescript TypeScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); await sdk.initialize(); // Upload from disk let result = await sdk.memories.create_from_file({ user_id: 'user_789', customer_id: 'cust_456', file_path: '/data/transcripts/2026-05-17-call.pdf', document_type: 'pdf', mode: 'long-range', metadata: {'source': 'support-call'}, }); console.log(result.ingestion_id); // Upload raw text result = await sdk.memories.create_from_file({ user_id: 'user_789', customer_id: 'cust_456', text: 'Customer prefers email follow-ups over phone.', document_type: 'note', }); ``` ### Raises * `ValueError`: when none of `file_path`, `file`, or `text` is provided. * `SynapAuthError`: when the API key is missing or invalid. * `SynapValidationError`: when the upload payload is malformed. ### See also * [memories.create](/sdk-reference/memories/create) * [memories.batch\_create](/sdk-reference/memories/batch-create) * [memories.wait\_for\_completion](/sdk-reference/memories/wait-for-completion) # memories.delete Source: https://docs.maximem.ai/sdk-reference/memories/delete Permanently delete a memory by its ID. ```python Python theme={null} await sdk.memories.delete(memory_id) ``` ```typescript TypeScript theme={null} await sdk.memories.delete(memoryId: string) ``` Permanently delete a specific memory. The memory is removed from both the vector store and the graph store. Associated entity references are updated but the entities themselves are not deleted. ### Parameters The memory ID to delete. ### Returns A confirmation dict. The ID of the memory that was deleted. Server timestamp at which the deletion was applied. ### Example ```python theme={null} from maximem_synap import MaximemSynapSDK from uuid import UUID sdk = MaximemSynapSDK(api_key="synap_your_key_here") await sdk.initialize() result = await sdk.memories.delete(UUID("a1b2c3d4-e5f6-7890-abcd-ef0123456789")) print(result["memory_id"]) print(result["deleted_at"]) ``` Memory deletion is permanent and cannot be undone. ### Raises * `SynapAuthError`: when the API key is missing or invalid. * `SynapNotFoundError`: when the `memory_id` does not exist or is outside the caller's scope. ### See also * [memories.get](/sdk-reference/memories/get) * [memories.update](/sdk-reference/memories/update) * [memories.create](/sdk-reference/memories/create) # memories.get Source: https://docs.maximem.ai/sdk-reference/memories/get Retrieve a specific memory by its ID. ```python Python theme={null} await sdk.memories.get(memory_id) ``` ```typescript TypeScript theme={null} await sdk.memories.get(memoryId: string) ``` Retrieve a single memory by its identifier. Returns the structured `Memory` object including content, confidence, and source-document metadata. ### Parameters The memory ID. Typically obtained from [`memories.status`](/sdk-reference/memories/status) (`memory_ids`) or from a recall result. ### Returns A `Memory` object. Unique memory identifier. Memory type: `fact`, `preference`, `episode`, `emotion`, or `temporal_event`. The extracted memory content. Confidence score of the extraction, between `0.0` and `1.0`. High-level category assigned by the extraction pipeline. More specific subcategory under `category`. Timestamp of when this memory was created. Timestamp of the last update. `None` if never updated. Identifier of the document this memory was extracted from. ### Example ```python theme={null} from maximem_synap import MaximemSynapSDK from uuid import UUID sdk = MaximemSynapSDK(api_key="synap_your_key_here") await sdk.initialize() memory = await sdk.memories.get(UUID("a1b2c3d4-e5f6-7890-abcd-ef0123456789")) print(memory.memory_type) # "preference" print(memory.content) # "Prefers light roast coffee, especially pour-over" print(memory.confidence) # 0.92 ``` ### Raises * `SynapAuthError`: when the API key is missing or invalid. * `SynapNotFoundError`: when the `memory_id` does not exist or is outside the caller's scope. ### See also * [memories.update](/sdk-reference/memories/update) * [memories.delete](/sdk-reference/memories/delete) * [memories.status](/sdk-reference/memories/status) # memories.status Source: https://docs.maximem.ai/sdk-reference/memories/status Check the progress of an asynchronous ingestion job. ```python Python theme={null} await sdk.memories.status(ingestion_id) ``` ```typescript TypeScript theme={null} await sdk.memories.status(ingestionId: string) ``` Check the progress of an asynchronous ingestion job. Use the `ingestion_id` returned by [`memories.create`](/sdk-reference/memories/create), [`memories.batch_create`](/sdk-reference/memories/batch-create), or [`memories.create_from_file`](/sdk-reference/memories/create-from-file). ### Parameters The ingestion job ID returned from a create call. ### Returns `MemoryStatusResponse` with the job's current state and the IDs of any memories it has produced so far. The ingestion job identifier. The document identifier associated with this ingestion. Current status of the ingestion job. | Value | Description | | ----------------- | -------------------------------------------------------------------------------- | | `queued` | Document is waiting to be processed | | `processing` | Document is currently being processed through the pipeline | | `completed` | All extraction and storage stages completed successfully | | `failed` | Processing failed. Check `error_message` for details. | | `partial_success` | Some extractions succeeded but others failed. Check `error_message` for details. | Timestamp when the job was queued. Timestamp when processing started, or `None` if still queued. Timestamp when processing finished, or `None` if still in progress. Number of memories produced from this ingestion so far. IDs of memories produced from this ingestion. You can pass these to [`memories.get`](/sdk-reference/memories/get). Error description if `status` is `failed` or `partial_success`; otherwise `None`. ### Example ```python Python theme={null} from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK(api_key="synap_your_key_here") await sdk.initialize() status = await sdk.memories.status(result.ingestion_id) print(status.status) # e.g. "completed" print(status.memories_created) # e.g. 3 print(status.memory_ids) # list of UUIDs ``` ```javascript JavaScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); await sdk.initialize(); const status = await sdk.memories.status(result.ingestion_id); console.log(status.status); // e.g. "completed" console.log(status.memories_created); // e.g. 3 console.log(status.memory_ids); // list of UUIDs ``` ```typescript TypeScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); await sdk.initialize(); const status = await sdk.memories.status(result.ingestion_id); console.log(status.status); // e.g. "completed" console.log(status.memories_created); // e.g. 3 console.log(status.memory_ids); // list of UUIDs ``` ### Raises * `SynapAuthError`: when the API key is missing or invalid. * `SynapNotFoundError`: when the `ingestion_id` is unknown. ### See also * [memories.wait\_for\_completion](/sdk-reference/memories/wait-for-completion) * [memories.create](/sdk-reference/memories/create) * [memories.get](/sdk-reference/memories/get) # memories.update Source: https://docs.maximem.ai/sdk-reference/memories/update Update the content of an existing memory using a merge strategy. ```python Python theme={null} await sdk.memories.update(memory_id, document, ...) ``` ```typescript TypeScript theme={null} await sdk.memories.update(options: UpdateMemoryOptions) ``` Update an existing memory. The update behavior depends on the merge strategy: you can fully replace, append to, or smart-merge the existing content. ### Parameters The memory ID to update. Updated memory content. Behavior depends on `merge_strategy`. How the update should be applied. Defaults to `smart-merge`. | Value | Description | | ------------- | --------------------------------------------------------------------------------------------------------------------------------- | | `replace` | Fully replace the existing memory content with the new content. | | `append` | Append the new content to the existing memory, preserving the original. | | `smart-merge` | Intelligently merge new information with existing content, deduplicating and resolving conflicts by preferring the newer version. | Optional new document type to associate with the memory. Optional updated metadata. Merged with existing metadata. ### Returns The updated `Memory` object, same shape as the response from [`memories.get`](/sdk-reference/memories/get). ### Example ```python theme={null} from maximem_synap import MaximemSynapSDK from uuid import UUID sdk = MaximemSynapSDK(api_key="synap_your_key_here") await sdk.initialize() updated = await sdk.memories.update( memory_id=UUID("a1b2c3d4-e5f6-7890-abcd-ef0123456789"), document="Prefers light roast coffee, especially pour-over. Also enjoys cold brew in summer.", merge_strategy="smart-merge", metadata={"source": "web-chat"}, ) print(updated.content) print(updated.updated_at) ``` ### Raises * `SynapAuthError`: when the API key is missing or invalid. * `SynapNotFoundError`: when the `memory_id` does not exist or is outside the caller's scope. * `SynapValidationError`: when `merge_strategy` or `document_type` is not a recognized value. ### See also * [memories.get](/sdk-reference/memories/get) * [memories.delete](/sdk-reference/memories/delete) * [memories.create](/sdk-reference/memories/create) # memories.wait_for_completion Source: https://docs.maximem.ai/sdk-reference/memories/wait-for-completion Block until an ingestion job reaches a terminal status, or raise on timeout. ```python Python theme={null} await sdk.memories.wait_for_completion(ingestion_id, timeout_seconds=300, poll_interval_seconds=2) ``` ```typescript TypeScript theme={null} await sdk.memories.wait_for_completion(ingestionId: string, options?: WaitOptions) ``` Wait for an ingestion job to reach a terminal status (`completed`, `failed`, or `partial_success`). Internally polls [`memories.status`](/sdk-reference/memories/status) at the requested interval and returns the final status response. Useful for scripts and tests where you need a synchronous result before moving on. ### Parameters The ingestion job ID to wait on. Maximum time to wait in seconds. Defaults to `300`. How often to poll for status in seconds. Defaults to `2`. ### Returns `MemoryStatusResponse`: the final status response once the job reaches `completed`, `failed`, or `partial_success`. See [`memories.status`](/sdk-reference/memories/status) for the full field list. ### Example ```python theme={null} from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK(api_key="synap_your_key_here") await sdk.initialize() result = await sdk.memories.create( document="User prefers email communication.", document_type="ai-chat-conversation", user_id="user_789", customer_id="cust_456", mode="long-range", ) final = await sdk.memories.wait_for_completion( result.ingestion_id, timeout_seconds=120, poll_interval_seconds=3, ) print(final.status) # "completed" print(final.memory_ids) # IDs ready to fetch ``` Prefer this helper over your own polling loop in scripts and integration tests. In production applications, use [webhooks](/dashboard/webhooks) instead of long-polling. ### Raises * `TimeoutError`: when the job does not reach a terminal status within `timeout_seconds`. * `SynapAuthError`: when the API key is missing or invalid. * `SynapNotFoundError`: when the `ingestion_id` is unknown. ### See also * [memories.status](/sdk-reference/memories/status) * [memories.create](/sdk-reference/memories/create) * [memories.create\_from\_file](/sdk-reference/memories/create-from-file) # Migration Source: https://docs.maximem.ai/sdk-reference/migration SDK methods for migrating data to Synap from other memory systems. ## Overview Synap supports bulk data migration from other memory systems, knowledge management platforms, or custom datastores. Whether you are replacing an existing memory layer or consolidating data from multiple sources, the SDK's batch ingestion handles large-scale transfers with idempotency and per-document error reporting. Dedicated migration tooling (pre-flight validation, rollback, schema mapping) is on the roadmap. Today, `sdk.memories.batch_create()` with the `BOOTSTRAP` priority is the recommended approach for bulk data migration. This page covers the SDK migration **methods**. For source-specific positioning and concept mapping (Mem0, Zep, Letta, SuperMemory → Synap, including data export and call-mapping tables), see the [Migrate from Competitors](/migrations/overview) guide. ## Current Migration Path ### Using the SDK batch ingestion `sdk.memories.batch_create()` with `BOOTSTRAP` priority is the recommended approach for migrating data today. Bootstrap priority ensures high-throughput processing without impacting real-time ingestion. ```python theme={null} from maximem_synap import MaximemSynapSDK from maximem_synap.memories.models import CreateMemoryRequest import asyncio async def migrate_data(sdk: MaximemSynapSDK, documents: list[dict]): """ Migrate documents to Synap using the SDK batch ingestion. """ batch_size = 100 total = len(documents) for i in range(0, total, batch_size): batch = documents[i:i + batch_size] requests = [ CreateMemoryRequest( document=doc["content"], document_type=doc.get("document_type", "document"), document_id=doc.get("id"), document_created_at=doc.get("created_at"), user_id=doc.get("user_id"), customer_id=doc.get("customer_id"), mode="long-range", metadata={ "source": "migration", **doc.get("metadata", {}) } ) for doc in batch ] result = await sdk.memories.batch_create( documents=requests, fail_fast=False ) progress = min(i + batch_size, total) print(f"Migrated {progress}/{total}, accepted: {len(result.results)}, rejected: {len(result.errors)}") print("Migration complete") ``` ### Migration Best Practices Always include a `document_id` in the metadata for each migrated document. Use the original system's identifier to ensure idempotency. If you need to re-run the migration (e.g., after fixing a transformation bug), documents with the same `document_id` will be updated rather than duplicated. ```python theme={null} "metadata": { "document_id": f"legacy_{original_system}_{original_id}", "source": "migration" } ``` Include the original `created_at` timestamp from the source system in the metadata. This helps maintain chronological context even though the memory's Synap `created_at` will reflect the migration time. ```python theme={null} "metadata": { "original_created_at": "2024-06-15T10:30:00Z", "source": "migration" } ``` Before starting the migration, build a mapping from source system user/customer identifiers to Synap's `user_id` and `customer_id` values. Consistent identity mapping ensures that migrated memories are correctly scoped and retrievable. Run a small test batch (10-50 documents) first. Verify that: * Documents are processed successfully (check ingestion status) * Entity resolution produces expected results * Retrieval returns the migrated memories for relevant queries * Scoping is correct (user, customer, client levels) For large migrations, implement progress tracking in your migration script. Log batch completion, track failures, and maintain a checkpoint so you can resume from the last successful batch if interrupted. ## Future Migration Features Dedicated migration tooling will include the following capabilities when released: Pre-migration validation that checks your data against Synap's schema requirements, identifies potential issues (missing fields, encoding problems, oversized documents), and provides a detailed report before you commit to the migration. Real-time progress tracking for long-running migrations. Query the migration status, see per-batch results, identify failed documents, and get estimated time to completion. The ability to roll back a migration if results are not as expected. Rollback removes all documents ingested as part of a specific migration job, including their vector embeddings and graph connections. Declarative schema mapping that transforms source data formats into Synap's document model without writing custom code. Define field mappings, transformations, and default values in a configuration file. Want early access to the dedicated migration tooling? Reach out via the [Synap Dashboard](https://synap.maximem.ai) with details about your migration use case and data volume. ## Next Steps SDK methods for memory creation and batch operations. Mapping your existing memory system (Mem0, Zep, Letta, SuperMemory) to Synap. Patterns for bulk-loading data into Synap. # API Reference Source: https://docs.maximem.ai/sdk-reference/overview The Synap SDK provides programmatic access for **bootstrap ingestion**, **organizational and customer context retrieval**, and **data migration**. This page covers cross-cutting concerns you will encounter through the SDK: authentication, rate limits, error handling, correlation IDs, and pagination. ## Authentication The SDK authenticates with an API key generated when you create an Instance in the [Synap Dashboard](https://synap.maximem.ai). One key per Instance; rotate or revoke at any time. Never expose your API key in client-side code, public repositories, or browser requests. API keys should only be used in server-side environments. See [Authentication](/setup/authentication) for a detailed walkthrough of API key generation and the credential lifecycle. ## SDK versioning The SDK and the underlying wire protocol are versioned. Non-breaking additions (new fields, new optional parameters) may be added within a major version without notice. Breaking changes ship in new major versions of the SDK; the upgrade path is documented in the [Changelog](/resources/changelog). ## Pagination List methods on the SDK return paginated results. Pass `page` and `page_size` to control the window: Page number to retrieve. Defaults to `1`. Number of items per page. Defaults to `20`. Maximum `100`. Returned objects include both the data and the pagination envelope: ```python theme={null} result = await sdk.memories.list(page=1, page_size=20) result.data # list of memory records result.page # 1 result.page_size # 20 result.total # 42 result.total_pages # 3 ``` ## Rate Limiting Requests are rate-limited per API key. When the limit is exceeded, the SDK raises `RateLimitError`. Tier limits: | Tier | Requests per minute | Burst | | ---------- | ------------------- | ------ | | Free | 60 | 10 | | Pro | 600 | 50 | | Enterprise | Custom | Custom | The SDK automatically retries with exponential backoff on rate-limit errors. The exception carries the server's `Retry-After`: ```python theme={null} try: await sdk.memories.create(...) except RateLimitError as e: print(e.retry_after_seconds) # seconds to wait, or None ``` ## Correlation IDs Every SDK call records a correlation ID: a unique identifier propagated through the full request lifecycle, including background jobs like ingestion. Read it from a context response's metadata, or from any Synap error: ```python Python theme={null} context = await sdk.user.context.fetch(user_id="user_123") print(context.metadata.correlation_id) # "req_7f3a2b1c-9d4e-4f5a-8b6c-1d2e3f4a5b6c" ``` ```javascript JavaScript theme={null} const context = await sdk.user.context.fetch({ user_id: 'user_123', }); console.log(context.metadata?.correlation_id); // "req_7f3a2b1c-9d4e-4f5a-8b6c-1d2e3f4a5b6c" ``` ```typescript TypeScript theme={null} const context = await sdk.user.context.fetch({ user_id: 'user_123', }); console.log(context.metadata?.correlation_id); // "req_7f3a2b1c-9d4e-4f5a-8b6c-1d2e3f4a5b6c" ``` Always include the `correlation_id` when contacting support. It allows the team to trace the exact request path and identify issues quickly. ## Error handling The SDK raises typed exceptions for different failure classes. Each exception exposes a `code` (machine-readable), a `message` (human-readable), and a `details` object with context: ```python Python theme={null} from maximem_synap import ( SynapError, AuthenticationError, InvalidInputError, ContextNotFoundError, RateLimitError, ServiceUnavailableError, ) try: await sdk.memories.get("mem_nonexistent") except ContextNotFoundError as e: print(e) # human-readable description print(e.correlation_id) # server-side request id, for support ``` ```javascript JavaScript theme={null} import { AuthenticationError, ContextNotFoundError, InvalidInputError, RateLimitError, ServiceUnavailableError, SynapError, isSynapError } from '@maximem/synap-js-sdk'; try { await sdk.memories.get('mem_nonexistent'); } catch (e) { if (!(e instanceof ContextNotFoundError)) throw e; console.log(e.code); // stable machine-readable code console.log(e.message); // human-readable description console.log(e.correlationId); // server-side request id, for support } ``` ```typescript TypeScript theme={null} import { AuthenticationError, ContextNotFoundError, InvalidInputError, RateLimitError, ServiceUnavailableError, SynapError, isSynapError } from '@maximem/synap-js-sdk'; try { await sdk.memories.get('mem_nonexistent'); } catch (e) { if (!(e instanceof ContextNotFoundError)) throw e; console.log(e.code); // stable machine-readable code console.log(e.message); // human-readable description console.log(e.correlationId); // server-side request id, for support } ``` All SDK exceptions inherit from `SynapError` and split into two branches: `SynapTransientError` (the SDK auto-retries) and `SynapPermanentError` (your code must handle). Common exceptions: | Exception | Branch | When raised | | ----------------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `AuthenticationError` | Permanent | API key missing, invalid, or revoked | | `InvalidInputError` | Permanent | Request parameters invalid or missing required fields | | `InvalidInstanceIdError` | Permanent | Subtype of `InvalidInputError`. A **malformed** `instance_id` (not `inst_` + 16 hex) is raised by the constructor, client-side; an **unknown** one comes back as `InvalidInputError` from the server. Catching the parent handles both | | `InvalidConversationIdError` | Permanent | Subtype of `InvalidInputError` for a malformed `conversation_id`: currently surfaced as `InvalidInputError`; catch the parent | | `ContextNotFoundError` | Permanent | Requested instance, conversation, or memory does not exist | | `SessionExpiredError` | Permanent | Session expired; re-authenticate | | `InsufficientCreditsError` | Permanent | Wallet has insufficient credits for this operation | | `ListeningAlreadyActiveError` | Permanent | `instance.listen()` called while another stream is open | | `ListeningNotActiveError` | Permanent | `instance.send_message()` called without an active stream | | `RateLimitError` | Transient | Rate limit exceeded: SDK auto-retries with backoff | | `NetworkTimeoutError` | Transient | Request timed out: SDK auto-retries with backoff | | `ServiceUnavailableError` | Transient | Synap temporarily unavailable: SDK auto-retries with backoff | | `AgentUnavailableError` | Transient | Internal agent unavailable: SDK auto-retries with backoff | For the full hierarchy and per-exception handling guidance, see [Error Handling](/sdk/error-handling). For wire-level error codes (when interpreting the server's `code` field), see [Error Codes](/sdk-reference/errors). ## SDK installation The official Python SDK is the primary customer interface. ```bash pip theme={null} pip install maximem-synap ``` ```bash poetry theme={null} poetry add maximem-synap ``` ```bash uv theme={null} uv add maximem-synap # pip-compatible (existing venv): uv pip install maximem-synap ``` ```python Python theme={null} import uuid from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK( api_key="synap_your_key_here" ) await sdk.initialize() # All SDK methods are async # conversation_id must be a valid UUID context = await sdk.conversation.context.fetch( conversation_id=str(uuid.uuid4()), search_query=["user preferences"] ) ``` ```javascript JavaScript theme={null} import { randomUUID } from 'node:crypto'; import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); await sdk.initialize(); // All SDK methods are async // conversation_id must be a valid UUID const context = await sdk.conversation.context.fetch({ conversation_id: randomUUID(), search_query: ['user preferences'], }); ``` ```typescript TypeScript theme={null} import { randomUUID } from 'node:crypto'; import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); await sdk.initialize(); // All SDK methods are async // conversation_id must be a valid UUID const context = await sdk.conversation.context.fetch({ conversation_id: randomUUID(), search_query: ['user preferences'], }); ``` A JavaScript/TypeScript SDK is also available for Node.js environments: ```bash theme={null} npm install @maximem/synap-js-sdk ``` The JavaScript client mirrors this namespaced API one to one, and keeps a flat JavaScript-idiomatic surface alongside it. See [JavaScript and TypeScript SDK](/setup/installation#javascript-and-typescript-sdk). ### Reading these examples in JavaScript or TypeScript The examples throughout this reference are written in Python. They translate mechanically, because the two SDKs share namespaces, method names and field names exactly. Two differences, and that is all: 1. Python passes keyword arguments; JavaScript passes one options object. 2. Python's `MaximemSynapSDK()` is JavaScript's `new SynapClient()`, and its `initialize()` / `shutdown()` are spelled the same. ```python Python theme={null} sdk = MaximemSynapSDK(api_key=API_KEY) await sdk.initialize() await sdk.memories.create( document="The customer prefers window seats.", user_id="user-123", customer_id="customer-456", ) context = await sdk.user.context.fetch( user_id="user-123", search_query=["seat preference"], max_results=10, ) ``` ```ts JavaScript / TypeScript theme={null} const synap = new SynapClient({ apiKey: API_KEY }); await synap.initialize(); await synap.memories.create({ document: "The customer prefers window seats.", user_id: "user-123", customer_id: "customer-456", }); const context = await synap.user.context.fetch({ user_id: "user-123", search_query: ["seat preference"], max_results: 10, }); ``` Field names stay **snake\_case** in JavaScript on the namespaced surface, so a Python example's argument names can be copied across unchanged. The flat surface (`fetchUserContext`, `searchMemory`) is the exception: it returns the normalised camelCase shape. Pick one style per codebase. The JavaScript SDK is a native Node.js client requiring Node.js 20+. Context and memory operations run on Node, Vercel Edge, Cloudflare Workers and the browser. The optional anticipation stream needs raw TCP and so is Node only. See [Installation → JavaScript and TypeScript SDK](/setup/installation#javascript-and-typescript-sdk). See the [Installation](/setup/installation) guide for full setup instructions for both SDKs. # user.get_profile Source: https://docs.maximem.ai/sdk-reference/user/get-profile Fetch a user's profile document: client-defined critical attributes plus a free-text overview. ```python theme={null} await sdk.user.get_profile( user_id: str, customer_id: Optional[str] = None, ) -> UserProfileModel ``` Fetch the accumulated **profile** for a user: the client-defined critical attributes (e.g. intent, budget, preferences) plus a short free-text overview that Synap maintains across the user's conversations. This is a convenience getter. The same document is returned inline by [`sdk.fetch(context_mode="conversation-summary", include_profile=True)`](/sdk-reference/context/fetch); reach for `get_profile` when you want the profile on its own: dashboardless debugging, your own tooling, or a quick lookup. Profiles are **on by default**. See the [User Profile guide](/sdk/user-profile) for defining your attribute schema (or opting out per instance). For a user whose profile has not been built yet, there is nothing to return and the call raises `ContextNotFoundError`. ### Parameters Caller identity (e.g. an E.164 phone number). **Required on B2B (strict-isolation) instances** so the profile resolves to the right tenant; omit on B2C. ### Returns A `UserProfileModel`. Critical attributes keyed by name. Each `ProfileAttributeModel` carries `value`, `confidence`, `updated_at`, `source_conversation_id` (and `.raw`). A short free-text summary of the caller. Stable facts worth keeping that match no configured attribute. Document metadata (`_meta`): schema, version, `updated_at`, etc. `.raw` exposes the full untyped profile document. ### Example ```python theme={null} from maximem_synap import MaximemSynapSDK, ContextNotFoundError sdk = MaximemSynapSDK(api_key="synap_your_key_here") await sdk.initialize() try: profile = await sdk.user.get_profile("+919812345678") print(profile.overview) for name, attr in profile.attributes.items(): print(f"{name}: {attr.value} (confidence {attr.confidence})") except ContextNotFoundError: print("No profile yet for this caller.") ``` ### Raises * `ContextNotFoundError`: no profile exists for this user (HTTP 404). * `AuthenticationError`: when the API key is missing or invalid. ### See also * [context.fetch (conversation-summary mode)](/sdk-reference/context/fetch) * [conversation.ingest\_transcript](/sdk-reference/conversation/ingest-transcript) and [memories.create](/sdk-reference/memories/create): any ingestion that creates memories for a user refreshes their profile; an end-of-call transcript push is one example. # SDK Configuration Source: https://docs.maximem.ai/sdk/configuration Customize SDK behavior for your environment. **This page is the Python shape.** The JavaScript SDK takes the same settings as plain options on the constructor rather than an `SDKConfig` object, so the TypeScript tabs differ in shape, not just in syntax. Three differences worth knowing before you read them: * **Three keys do nothing here.** `storage_path`, `cache_backend` and `session_timeout_minutes` are accepted and ignored, so one config object can be shared between the two SDKs. The cache is in memory, so there is no path to point at and no backend to swap. `log_level` is ignored too, but `logger` is real: see below. * **The stream endpoint moves.** `grpc_host`, `grpc_port` and `grpc_use_tls` are options on [`instance.listen()`](/sdk-reference/instance/listen), not on the client. * **Timeouts and the retry policy are real,** and their defaults match Python exactly: connect 5s, read 30s, write 10s, 3 attempts, backoff base 1s capped at 10s. * **`logger` works.** Pass `(level, message) => void` and every diagnostic the SDK emits routes there instead of the console. JavaScript has no global logging framework whose level there would be to set, so the SDK takes the sink directly rather than a level plus a framework. Full surface: [JavaScript: client options](#javascript-client-options) below. ## Overview The Synap SDK is configured via the `SDKConfig` object, which controls storage, credentials, caching, timeouts, retries, and logging. Sensible defaults are provided for all fields, so you only need to override what matters for your environment. ## SDKConfig Reference ```python Python theme={null} from maximem_synap import SDKConfig config = SDKConfig( storage_path=None, # Default: SDK-managed local directory (cache only) cache_backend="sqlite", # "sqlite" or None session_timeout_minutes=30, # Range: 5-1440 timeouts=TimeoutConfig(), # Connection and read timeouts retry_policy=RetryPolicy(), # Retry behavior for transient errors log_level="WARNING", # DEBUG, INFO, WARNING, ERROR grpc_host=None, # Override the real-time stream host grpc_port=None, # Override the real-time stream port grpc_use_tls=None # None = TLS on; False = plaintext ) ``` ```typescript TypeScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: undefined, // Default: SYNAP_API_KEY instanceId: undefined, // Default: resolved from the key baseUrl: undefined, // Default: Synap Cloud timeouts: { connect: 5.0, read: 30.0, write: 10.0 }, // Seconds retryPolicy: { maxAttempts: 3 }, // null disables retries keepAlive: {}, // Connection reuse heartbeat: false, // Periodic /health ping logger: (level, message) => {}, // Default: console.warn sdk_st_authoritative: false, // Render prompts locally st_verbatim_overlay: true, // See a turn you just wrote }); ``` ### Field Reference At a glance, `SDKConfig` accepts: * `storage_path`: directory for the local SQLite cache and transient state. Defaults to an SDK-managed directory. The API key is never written here; it is read from `SYNAP_API_KEY` (or the `api_key=` constructor argument) on each start. See [Storage Path](#storage-path). * `cache_backend`: `"sqlite"` (default) for on-disk caching, or `None` to disable it. See [Cache Backend](#cache-backend). * `session_timeout_minutes`: how long a session stays active before re-authentication. Default `30`, valid range `5` to `1440`. See [Session Timeout](#session-timeout). * `timeouts`: a [TimeoutConfig](#timeoutconfig) for per-operation network timeouts. * `retry_policy`: a [RetryPolicy](#retrypolicy) for transient-error retries, or `None` to disable retries. * `log_level`: logging verbosity, one of `"DEBUG"`, `"INFO"`, `"WARNING"` (default), `"ERROR"`. See [Log Level](#log-level). * `grpc_host` / `grpc_port` / `grpc_use_tls`: endpoint overrides for the real-time anticipation stream. Only needed for self-hosted or local deployments. See [gRPC Connection](#grpc-connection). The complete field list, types, accepted ranges, and the `configure()` rules (including the `logger` override). ## TimeoutConfig Controls how long the SDK waits for individual network operations. ```python Python theme={null} from maximem_synap import TimeoutConfig timeouts = TimeoutConfig( connect=5.0, # Seconds to establish TCP connection read=30.0, # Seconds to wait for response data write=10.0, # Seconds to wait for request upload stream_idle=60.0 # Seconds of inactivity before reconnecting the event stream ) ``` ```javascript JavaScript theme={null} const timeouts = TimeoutConfig({ connect: 5.0, // Seconds to establish TCP connection read: 30.0, // Seconds to wait for response data write: 10.0, // Seconds to wait for request upload stream_idle: 60.0, // Seconds of inactivity before reconnecting the event stream }); ``` ```typescript TypeScript theme={null} const timeouts = TimeoutConfig({ connect: 5.0, // Seconds to establish TCP connection read: 30.0, // Seconds to wait for response data write: 10.0, // Seconds to wait for request upload stream_idle: 60.0, // Seconds of inactivity before reconnecting the event stream }); ``` Maximum time in seconds to establish a TCP connection to Synap Cloud. Increase this if your network has high latency or unreliable DNS resolution. Maximum time in seconds to wait for a complete response after sending a request. This should be higher than your expected query latency. For compaction of very large conversations, you may need to increase this. Maximum time in seconds to upload request data. Relevant for large batch ingestion payloads. Increase if you are sending very large documents. Maximum idle time in seconds for the SDK's real-time event stream (used by `sdk.instance.listen()`). If no data is received within this window, the stream is considered stale and reconnected. Increase for low-traffic instances where events are infrequent. ## gRPC Connection These three fields point the real-time anticipation stream ([`instance.listen()`](/sdk-reference/instance/listen)) at a specific endpoint. Leave them unset for Synap Cloud; the defaults are correct and TLS is on. ```python Python theme={null} config = SDKConfig( grpc_host="localhost", # Default: Synap Cloud grpc_port=50051, # Default: Synap Cloud grpc_use_tls=False, # Default: None (TLS on) ) ``` ```typescript TypeScript theme={null} // listen() options here, not client configuration. await sdk.instance.listen({ host: 'localhost', // Default: Synap Cloud port: 50051, // Default: Synap Cloud use_tls: false, // Default: TLS on }); ``` Hostname for the real-time stream. `None` uses the Synap Cloud endpoint. Set this when running against a self-hosted or local Synap deployment. Port for the real-time stream, commonly `50051` for local deployments. `None` uses the Synap Cloud default. `None` uses the transport default (TLS **on**). Set to `False` only for a plaintext endpoint, such as a local container. Never disable TLS against a remote host. These fields affect only the streaming transport. REST calls use the separate `api_base_url`. Point both at the same deployment or the SDK will authenticate against one environment and stream against another. ### Caching and async ingestion Ingestion is asynchronous: memories from a turn are not immediately retrievable. If you fetch right after writing, an empty result is a legitimate response, and with `cache_backend="sqlite"` the local read cache will hold onto that empty result for its TTL, so later turns keep seeing nothing. For live agents that ingest and retrieve in the same loop, disable the local read cache: ```python Python theme={null} config = SDKConfig(cache_backend=None) ``` ```typescript TypeScript theme={null} // Nothing to disable: the JavaScript read cache is in memory, and a write // invalidates the scope it touched. ``` The anticipation cache is unaffected and keeps working: the server pushes fresh bundles, so it invalidates correctly on its own. ## RetryPolicy Controls automatic retry behavior for transient errors. ```python Python theme={null} from maximem_synap import RetryPolicy retry_policy = RetryPolicy( max_attempts=3, # Total attempts (1 initial + 2 retries) backoff_base=1.0, # Base delay in seconds backoff_max=10.0, # Maximum delay cap backoff_jitter=True, # Add randomized jitter retryable_errors=[ # Which error types to retry "NetworkTimeoutError", "RateLimitError", "ServiceUnavailableError", "SynapTransientError" ] ) ``` ```typescript TypeScript theme={null} import type { RetryPolicy } from '@maximem/synap-js-sdk'; // camelCase, and no retryable_errors: see the note below. const retryPolicy: RetryPolicy = { maxAttempts: 3, // Total attempts (1 initial + 2 retries) backoffBase: 1.0, // Base delay in seconds backoffMax: 10.0, // Maximum delay cap backoffJitter: true, // Add randomized jitter }; ``` The total number of attempts including the initial request. Setting this to `1` means no retries (only the initial attempt). Setting to `5` means up to 4 retries after the initial failure. The base delay in seconds for exponential backoff. The delay for attempt N is `backoff_base * 2^(N-2)`, capped at `backoff_max`. A higher base means longer waits between retries, which is gentler on rate-limited endpoints. The maximum delay in seconds between retry attempts. Prevents exponential backoff from growing unbounded for high `max_attempts` values. When enabled, adds a random component to the backoff delay. This prevents the "thundering herd" problem where multiple SDK instances retry at the exact same time after a shared failure. Strongly recommended for production deployments. The list of error type names that should be automatically retried. Only errors in this list trigger the retry policy. All other errors are raised immediately. For `RateLimitError`, the SDK uses the server-provided `retry_after_seconds` value instead of the exponential backoff calculation. ## Storage Path The `storage_path` directory holds the local SQLite cache and transient state. The SDK sets restrictive filesystem permissions on this directory on creation. The API key is never stored here; it comes from `SYNAP_API_KEY` or the `api_key=` constructor argument. ### When to Override | Scenario | Recommended `storage_path` | | ------------------------------- | ---------------------------------------------------------------------- | | Standard deployment | `None` (use SDK default) | | Docker container | `/var/lib/synap/` mapped to a persistent volume (optional, cache only) | | Serverless function | Disable the cache (`cache_backend=None`) | | Multiple instances on same host | `/var/lib/synap//` | | Testing | `/tmp/synap-test/` | ## Credentials The SDK reads the API key on every start. There are exactly two sources: | Source | Description | | ------------------------------------ | ----------------------------------------------------------- | | `api_key="..."` constructor argument | Passed directly to `MaximemSynapSDK(...)` | | `SYNAP_API_KEY` environment variable | Read automatically when the constructor argument is omitted | The SDK uses your API key for every call. The instance ID is resolved automatically from the API key; you do not need to set it manually. ## Cache Backend ### SQLite (Default, Recommended) ```python theme={null} config = SDKConfig(cache_backend="sqlite") ``` The SQLite cache stores recent retrieval results locally, enabling sub-millisecond cache hits for repeated queries. The cache respects TTL values from the server (`ttl_seconds` in response metadata) and evicts stale entries automatically. Benefits: * Sub-millisecond cache hits for repeated context fetches * Persists across SDK restarts (within TTL) * Automatic size management and TTL-based eviction * Zero configuration (SQLite is bundled with Python) ### Disabled ```python Python theme={null} config = SDKConfig(cache_backend=None) ``` ```typescript TypeScript theme={null} // Python only: the JavaScript cache is in memory, so there is no // backend to disable. ``` Disables local caching entirely. Every `conversation.context.fetch()` call goes to Synap Cloud. Use this when: * You need guaranteed freshness on every call * You are running in a read-only filesystem (and cannot use a RAM-backed path) * You are debugging cache-related issues ## Session Timeout ```python theme={null} config = SDKConfig(session_timeout_minutes=60) ``` The session timeout controls how long the SDK's authenticated session remains valid before re-authentication is required. The valid range is 5 to 1440 minutes (24 hours). | Setting | Value | Use Case | | ------------- | -------------- | --------------------------------------------------- | | Short session | 5-15 min | High-security environments, compliance requirements | | Default | 30 min | General-purpose applications | | Long session | 60-120 min | Long-running batch processes | | Maximum | 1440 min (24h) | Background workers, data pipelines | ## Log Level The SDK uses Python's standard `logging` module. The log level controls verbosity of the `synap` logger. ```python Python theme={null} config = SDKConfig(log_level="DEBUG") ``` ```typescript TypeScript theme={null} // JavaScript has no global logging framework, so there is no level to set. // Pass the sink instead: every diagnostic the SDK emits goes to it. const sdk = new SynapClient({ logger: (level, message) => myLogger[level](`[synap] ${message}`), }); // Silence them entirely: const quiet = new SynapClient({ logger: () => {} }); ``` `log_level` is accepted and ignored in JavaScript, so a shared config object still works. Filter by the `level` argument in your own sink instead: it is one of `debug`, `info`, `warn`, `error`. | Level | What is Logged | | --------- | ------------------------------------------------------------------------------------------------------------ | | `DEBUG` | All internal operations: request/response bodies, cache lookups, retry decisions, credential rotation timing | | `INFO` | Initialization, connection events, credential rotation, compaction triggers | | `WARNING` | Deprecation notices, approaching rate limits, cache size warnings | | `ERROR` | Failed operations that could not be retried, authentication failures, data corruption | The `DEBUG` level may log sensitive information including request payloads. Do not use `DEBUG` in production environments where logs may be exposed to unauthorized parties. ## Using `configure()` The `configure()` method allows you to update individual configuration fields after constructing the SDK but before calling `initialize()`. ```python Python theme={null} sdk = MaximemSynapSDK( api_key="synap_your_key_here" ) # Override specific fields without constructing a full SDKConfig sdk.configure(log_level="DEBUG") sdk.configure(session_timeout_minutes=120) sdk.configure(cache_backend=None) await sdk.initialize() ``` ```typescript TypeScript theme={null} const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); // Accepted and ignored: sdk.configure({ log_level: 'DEBUG' }); sdk.configure({ session_timeout_minutes: 120 }); sdk.configure({ cache_backend: null }); // Real: sdk.configure({ logger: (level, message) => myLogger[level](message) }); // These two are real: sdk.configure({ timeouts: { read: 45.0 } }); sdk.configure({ retryPolicy: { maxAttempts: 5 } }); await sdk.initialize(); ``` Calling `configure()` after `initialize()` raises `InvalidInputError('Cannot reconfigure after initialization')`. ## Environment Variables A small set of connection settings can come from the environment. Everything else must be set in code via `SDKConfig`; pass it to the constructor (or `configure()`) before `initialize()`. | Variable | Sets | | ------------------- | ---------------------------------------------- | | `SYNAP_API_KEY` | API key, when no `api_key=` argument is passed | | `SYNAP_BASE_URL` | `api_base_url`, the REST endpoint | | `SYNAP_INSTANCE_ID` | `instance_id`, when no argument is passed | Explicit configuration always wins. Each variable is consulted only when the corresponding `SDKConfig` field is left as `None`, so setting a value in code makes the environment variable inert. ```python Python theme={null} import os from maximem_synap import MaximemSynapSDK, SDKConfig sdk = MaximemSynapSDK( api_key=os.environ["SYNAP_API_KEY"], config=SDKConfig(log_level=os.environ.get("LOG_LEVEL", "WARNING")), ) ``` ```typescript TypeScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; // SYNAP_API_KEY is read automatically. Route diagnostics with `logger`. const sdk = new SynapClient({ logger: (level, message) => myLogger[level](`[synap] ${message}`), }); ``` ## Common Configurations Verbose logging, short timeouts, and aggressive retries for fast feedback during development. ```python Python theme={null} from maximem_synap import SDKConfig, TimeoutConfig, RetryPolicy dev_config = SDKConfig( storage_path="/tmp/synap-dev", # Temporary path for dev cache_backend="sqlite", session_timeout_minutes=120, timeouts=TimeoutConfig( connect=3.0, read=15.0, write=5.0, stream_idle=30.0 ), retry_policy=RetryPolicy( max_attempts=2, backoff_base=0.5, backoff_max=3.0 ), log_level="DEBUG" ) ``` ```typescript TypeScript theme={null} // Development const sdk = new SynapClient({ timeouts: { connect: 3.0, read: 15.0, write: 5.0 }, retryPolicy: { maxAttempts: 2, backoffBase: 0.5, backoffMax: 3.0 }, }); ``` Conservative timeouts, standard retries with jitter, minimal logging, and persistent storage. ```python Python theme={null} from maximem_synap import SDKConfig, TimeoutConfig, RetryPolicy prod_config = SDKConfig( storage_path="/var/lib/synap", cache_backend="sqlite", session_timeout_minutes=30, timeouts=TimeoutConfig( connect=10.0, read=30.0, write=10.0, stream_idle=120.0 ), retry_policy=RetryPolicy( max_attempts=3, backoff_base=1.0, backoff_max=10.0, backoff_jitter=True ), log_level="WARNING" ) ``` ```typescript TypeScript theme={null} // Production const sdk = new SynapClient({ timeouts: { connect: 10.0, read: 30.0, write: 10.0 }, retryPolicy: { maxAttempts: 3, backoffBase: 1.0, backoffMax: 10.0, backoffJitter: true }, heartbeat: true, // JS-only: keeps a long-lived connection warm }); ``` Isolated storage, disabled caching for deterministic tests, no retries for immediate failure feedback. ```python Python theme={null} from maximem_synap import SDKConfig, TimeoutConfig test_config = SDKConfig( storage_path="/tmp/synap-test", cache_backend=None, # No caching for deterministic tests session_timeout_minutes=5, timeouts=TimeoutConfig( connect=2.0, read=5.0, write=3.0, stream_idle=10.0 ), retry_policy=None, # No retries: fail fast in tests log_level="DEBUG" ) # Use _force_new to bypass singleton caching in tests sdk = MaximemSynapSDK( api_key="synap_test_key", config=test_config, _force_new=True ) ``` ```typescript TypeScript theme={null} // Testing. _force_new gives this test its own client; call shutdown() in // teardown, because nothing else will. const sdk = new SynapClient({ timeouts: { connect: 2.0, read: 5.0, write: 3.0 }, retryPolicy: null, // no retries: fail fast in tests _force_new: true, }); ``` Optimized for batch ingestion workloads with generous timeouts, higher retry limits, and long sessions. ```python Python theme={null} from maximem_synap import SDKConfig, TimeoutConfig, RetryPolicy throughput_config = SDKConfig( storage_path="/var/lib/synap", cache_backend="sqlite", session_timeout_minutes=1440, # 24 hours for long batch jobs timeouts=TimeoutConfig( connect=15.0, read=60.0, # Long reads for large batch responses write=30.0, # Long writes for large batch payloads stream_idle=300.0 # 5 min idle for streaming ), retry_policy=RetryPolicy( max_attempts=5, backoff_base=2.0, backoff_max=30.0, backoff_jitter=True ), log_level="INFO" ) ``` ```typescript TypeScript theme={null} // High throughput const sdk = new SynapClient({ timeouts: { connect: 5.0, read: 60.0, write: 15.0 }, retryPolicy: { maxAttempts: 5, backoffBase: 0.5, backoffMax: 20.0, backoffJitter: true }, keepAlive: {}, // JS-only: reuse connections across requests }); ``` ## Full Configuration Example Putting it all together with explicit values for every field: ```python Python theme={null} from maximem_synap import MaximemSynapSDK, SDKConfig, TimeoutConfig, RetryPolicy sdk = MaximemSynapSDK( api_key="synap_your_key_here", config=SDKConfig( storage_path="/var/lib/myapp/synap", cache_backend="sqlite", session_timeout_minutes=60, timeouts=TimeoutConfig( connect=10.0, read=30.0, write=10.0, stream_idle=120.0 ), retry_policy=RetryPolicy( max_attempts=3, backoff_base=1.0, backoff_max=10.0, backoff_jitter=True, retryable_errors=[ "NetworkTimeoutError", "RateLimitError", "ServiceUnavailableError", "SynapTransientError" ] ), log_level="WARNING" ) ) await sdk.initialize() ``` ```typescript TypeScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here', timeouts: { connect: 10.0, read: 30.0, write: 10.0 }, retryPolicy: { maxAttempts: 3, backoffBase: 1.0, backoffMax: 10.0, backoffJitter: true, // No retryable_errors; see the note below }, }); await sdk.initialize(); ``` ## Next Steps Learn the full initialization lifecycle with configuration. Understand how retry policies interact with the error hierarchy. Review configuration best practices before going live. Start ingesting data with your configured SDK. ## JavaScript: client options Pass these to `new SynapClient({ ... })`: Your Synap API key. Falls back to `SYNAP_API_KEY` when omitted. API base URL. Falls back to `SYNAP_BASE_URL`, then the Synap Cloud default. Set it when you run a self-hosted deployment. Optional. Resolved from the API key during `initialize()` when omitted. Falls back to `SYNAP_INSTANCE_ID`. `{ connect, read, write }` in seconds. Defaults to 5s connect and 30s read. `{ maxAttempts, backoffBase, backoffMax, backoffJitter }`. Defaults to 3 attempts with jittered exponential backoff. Keep the connection warm with a periodic health ping. Worth it for a long-lived process, pointless in serverless where the container is frozen between requests. Supply your own `fetch`. Useful for testing or for routing through a proxy.
# Context Compaction Source: https://docs.maximem.ai/sdk/context-compaction Compress conversations while preserving key information. ## Overview As conversations grow, the amount of context you need to inject into your LLM's prompt grows with them. Context compaction solves this by intelligently compressing conversation history while preserving the most important information: facts, decisions, preferences, and key narrative elements. Compaction is particularly valuable when: * Conversations exceed your LLM's context window * You want to reduce token costs without losing critical context * You need to summarize long conversation histories into a concise prompt segment Synap provides two ways to work with compacted context: | Approach | Method | Best for | | ----------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------- | | **Get context for prompt** | `get_context_for_prompt()` | Most integrations: one call, prompt-ready context | | **Manual compaction control** | `compact()` + `get_compacted()` + `get_compaction_status()` | Fine-grained control over strategy, token budgets, and timing | ## Prerequisites: Recording Messages Before calling any compaction method, the conversation must have messages recorded via `sdk.conversation.record_message()`. Calling `compact()` or `get_context_for_prompt()` on a conversation with no recorded messages raises a `SynapError` because no messages have been recorded yet. ```python Python theme={null} import uuid conversation_id = str(uuid.uuid4()) # Record messages before any compaction call. # Each message must carry the user_id and customer_id that scope the conversation. await sdk.conversation.record_message( conversation_id=conversation_id, role="user", content="I'd like to plan a trip to Japan in April.", user_id="user_alice", customer_id="acme_corp", ) await sdk.conversation.record_message( conversation_id=conversation_id, role="assistant", content="Great choice! April is cherry blossom season. Would you prefer Tokyo or Kyoto?", user_id="user_alice", customer_id="acme_corp", ) ``` ```javascript JavaScript theme={null} import { randomUUID } from 'node:crypto'; let conversation_id = randomUUID(); // Record messages before any compaction call. // Each message must carry the user_id and customer_id that scope the conversation. await sdk.conversation.record_message({ conversation_id: conversation_id, role: 'user', content: "I'd like to plan a trip to Japan in April.", user_id: 'user_alice', customer_id: 'acme_corp', }); await sdk.conversation.record_message({ conversation_id: conversation_id, role: 'assistant', content: 'Great choice! April is cherry blossom season. Would you prefer Tokyo or Kyoto?', user_id: 'user_alice', customer_id: 'acme_corp', }); ``` ```typescript TypeScript theme={null} import { randomUUID } from 'node:crypto'; let conversation_id = randomUUID(); // Record messages before any compaction call. // Each message must carry the user_id and customer_id that scope the conversation. await sdk.conversation.record_message({ conversation_id: conversation_id, role: 'user', content: "I'd like to plan a trip to Japan in April.", user_id: 'user_alice', customer_id: 'acme_corp', }); await sdk.conversation.record_message({ conversation_id: conversation_id, role: 'assistant', content: 'Great choice! April is cherry blossom season. Would you prefer Tokyo or Kyoto?', user_id: 'user_alice', customer_id: 'acme_corp', }); ``` `conversation_id` must be a valid UUID string. Non-UUID strings (e.g. `"conv_abc123"`) are rejected by the server. Use `str(uuid.uuid4())` to generate one, or pass a UUID you already manage in your system. *** ## Quick Start: Get Context for Your Prompt For most use cases, `get_context_for_prompt()` is all you need. It returns the best available compacted context in a single call, pre-formatted for injection into your LLM's system prompt. ```python Python theme={null} result = await sdk.conversation.context.get_context_for_prompt( conversation_id="3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c" ) if result.available: # Inject into your LLM system prompt system_prompt = f"""You are a helpful assistant. ## Conversation History {result.formatted_context}""" else: # No compacted context yet: use context.fetch() or trigger compact() system_prompt = "You are a helpful assistant." ``` ```javascript JavaScript theme={null} const result = await sdk.conversation.context.get_context_for_prompt({ conversation_id: '3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c', }); if (result.available) { // Inject into your LLM system prompt const systemPrompt = `You are a helpful assistant. ## Conversation History ${result.formatted_context}`; } else { // No compacted context yet: use context.fetch() or trigger compact() const systemPrompt = 'You are a helpful assistant.'; } ``` ```typescript TypeScript theme={null} const result = await sdk.conversation.context.get_context_for_prompt({ conversation_id: '3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c', }); if (result.available) { // Inject into your LLM system prompt const systemPrompt = `You are a helpful assistant. ## Conversation History ${result.formatted_context}`; } else { // No compacted context yet: use context.fetch() or trigger compact() const systemPrompt = 'You are a helpful assistant.'; } ``` The SDK caches the result locally (5-minute TTL), so calling this method on every LLM turn is safe and fast. The response exposes the prompt-ready `formatted_context` string plus metadata you act on: `available` (whether any compacted context exists), `is_stale` (newer messages recorded since the last compaction), and `quality_warning` (validation score below threshold). `validation_score`, `compression_ratio`, and `compaction_age_seconds` are also returned for monitoring and custom staleness thresholds. Every field on `ContextForPromptResponse`, with types and defaults. ### Formatting Styles The `style` parameter is accepted by the SDK (`"structured"`, `"narrative"`, `"bullet_points"`) and will control output formatting in a future backend release. At this stage, all three values return the same raw message content: there is no formatting difference between them yet. ```python Python theme={null} result = await sdk.conversation.context.get_context_for_prompt( conversation_id="3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c", style="structured" # "narrative" and "bullet_points" return identical output for now ) ``` ```javascript JavaScript theme={null} const result = await sdk.conversation.context.get_context_for_prompt({ conversation_id: '3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c', style: 'structured', // "narrative" and "bullet_points" return identical output for now }); ``` ```typescript TypeScript theme={null} const result = await sdk.conversation.context.get_context_for_prompt({ conversation_id: '3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c', style: 'structured', // "narrative" and "bullet_points" return identical output for now }); ``` Style-based formatting differentiation is not yet active on the backend. You can safely pass any supported style value: it will take effect automatically once the backend support ships, with no code changes required on your side. ### Handling Missing Context When `available` is `False`, no compacted context exists yet. This happens for new conversations or conversations that haven't been compacted. You have two options: ```python Python theme={null} result = await sdk.conversation.context.get_context_for_prompt( conversation_id="3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c" ) if not result.available: # Option 1: Fall back to retrieval-based context context = await sdk.conversation.context.fetch( conversation_id="3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c", search_query=[user_message], mode="fast" ) # Option 2: Trigger compaction, then retry await sdk.conversation.context.compact( conversation_id="3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c", strategy="adaptive" ) ``` ```javascript JavaScript theme={null} const result = await sdk.conversation.context.get_context_for_prompt({ conversation_id: '3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c', }); if (!result.available) { // Option 1: Fall back to retrieval-based context const context = await sdk.conversation.context.fetch({ conversation_id: '3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c', search_query: [user_message], mode: 'fast', }); // Option 2: Trigger compaction, then retry await sdk.conversation.context.compact({ conversation_id: '3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c', strategy: 'adaptive', }); } ``` ```typescript TypeScript theme={null} const result = await sdk.conversation.context.get_context_for_prompt({ conversation_id: '3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c', }); if (!result.available) { // Option 1: Fall back to retrieval-based context const context = await sdk.conversation.context.fetch({ conversation_id: '3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c', search_query: [user_message], mode: 'fast', }); // Option 2: Trigger compaction, then retry await sdk.conversation.context.compact({ conversation_id: '3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c', strategy: 'adaptive', }); } ``` *** ## Manual Compaction Control Use these methods when you need fine-grained control over compaction: choosing a strategy, setting token budgets, polling for completion, or retrieving specific versions. ### Triggering Compaction Use `sdk.conversation.context.compact()` to explicitly trigger compaction with specific parameters. ```python Python theme={null} compaction = await sdk.conversation.context.compact( conversation_id="3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c", strategy="adaptive", target_tokens=2000, force=False ) print(f"Compaction ID: {compaction.compaction_id}") print(f"Status: {compaction.status}") ``` ```javascript JavaScript theme={null} const compaction = await sdk.conversation.context.compact({ conversation_id: '3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c', strategy: 'adaptive', target_tokens: 2000, force: false, }); console.log(`Compaction ID: ${compaction.compaction_id}`); console.log(`Status: ${compaction.status}`); ``` ```typescript TypeScript theme={null} const compaction = await sdk.conversation.context.compact({ conversation_id: '3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c', strategy: 'adaptive', target_tokens: 2000, force: false, }); console.log(`Compaction ID: ${compaction.compaction_id}`); console.log(`Status: ${compaction.status}`); ``` **Key parameters.** `strategy` controls how aggressively the context is compressed (see [Strategies](#compaction-strategies) below; defaults to `"adaptive"`). `target_tokens` sets a desired output size and takes priority over the strategy's default compression level. `force=True` skips staleness checks so the conversation re-compacts even if no new memories have been ingested since the last run. Every `compact()` parameter, including the `compaction_level` alias, with types and defaults. ### Compaction Strategies The `CompactionLevel` enum exposes seven values: `low`, `medium`, `high`, `conservative`, `balanced`, `aggressive`, and `adaptive`. The four customer-facing strategies below cover the recommended use cases. **Recommended.** Dynamically adjusts compression based on the content. Dense, fact-heavy conversations are compressed less aggressively; repetitive or low-information conversations are compressed more aggressively. * Typical compression: 30-60% of original tokens * Preserves: all high-confidence facts, decisions, preferences, key narrative flow * Drops: repetitive information, low-value conversational filler, redundant context ```python Python theme={null} compaction = await sdk.conversation.context.compact( conversation_id="3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c", strategy="adaptive" ) ``` ```javascript JavaScript theme={null} const compaction = await sdk.conversation.context.compact({ conversation_id: '3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c', strategy: 'adaptive', }); ``` ```typescript TypeScript theme={null} const compaction = await sdk.conversation.context.compact({ conversation_id: '3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c', strategy: 'adaptive', }); ``` Maximum compression. Retains only the most critical facts and decisions. Best for very long conversations where you need to fit context into a tight token budget. * Typical compression: \~15% of original tokens * Preserves: highest-confidence facts, explicit decisions, critical preferences * Drops: narrative context, lower-confidence facts, episode details, emotional context ```python Python theme={null} compaction = await sdk.conversation.context.compact( conversation_id="3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c", strategy="aggressive", target_tokens=500 ) ``` ```javascript JavaScript theme={null} const compaction = await sdk.conversation.context.compact({ conversation_id: '3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c', strategy: 'aggressive', target_tokens: 500, }); ``` ```typescript TypeScript theme={null} const compaction = await sdk.conversation.context.compact({ conversation_id: '3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c', strategy: 'aggressive', target_tokens: 500, }); ``` Moderate compression that preserves a broader range of context while still achieving meaningful reduction. * Typical compression: \~40% of original tokens * Preserves: facts, preferences, decisions, episode summaries * Drops: redundant information, low-confidence extractions, verbose narrative ```python Python theme={null} compaction = await sdk.conversation.context.compact( conversation_id="3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c", strategy="balanced" ) ``` ```javascript JavaScript theme={null} const compaction = await sdk.conversation.context.compact({ conversation_id: '3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c', strategy: 'balanced', }); ``` ```typescript TypeScript theme={null} const compaction = await sdk.conversation.context.compact({ conversation_id: '3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c', strategy: 'balanced', }); ``` Minimal compression. Preserves nearly all extracted information with light deduplication and reformatting. * Typical compression: \~70% of original tokens * Preserves: nearly everything, including facts, preferences, episodes, emotions, narrative * Drops: exact duplicates, obvious filler ```python Python theme={null} compaction = await sdk.conversation.context.compact( conversation_id="3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c", strategy="conservative" ) ``` ```javascript JavaScript theme={null} const compaction = await sdk.conversation.context.compact({ conversation_id: '3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c', strategy: 'conservative', }); ``` ```typescript TypeScript theme={null} const compaction = await sdk.conversation.context.compact({ conversation_id: '3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c', strategy: 'conservative', }); ``` #### Strategy Selection Guide | Scenario | Recommended Strategy | | -------------------------------------------------- | --------------------------------- | | General-purpose, unsure what to use | `adaptive` | | Very long conversations (100+ turns) | `aggressive` with `target_tokens` | | Important conversations, high-value context | `conservative` | | Moderate conversations, cost optimization | `balanced` | | Dynamic workload with varying conversation lengths | `adaptive` | ### CompactionTriggerResponse The `compact()` method **kicks off** a compaction job asynchronously. It returns a `CompactionTriggerResponse` confirming the job was accepted, not the compacted content itself. The handle carries a `compaction_id` (use it with `get_compaction_status()` to poll progress) and an initial `status`. To retrieve the compacted output, call `get_compacted()` once the job completes (see [Retrieving Compacted Context](#retrieving-compacted-context) below) or poll status via `get_compaction_status()`. Every field on `CompactionTriggerResponse`, including `trigger_type`, `initiated_at`, `estimated_completion_seconds`, and the `previous_context` returned while a new run completes. ### Retrieving Compacted Context Use `sdk.conversation.context.get_compacted()` to retrieve a previously compacted version of a conversation's context without triggering a new compaction. This returns the rich `CompactionResponse` model with the actual compacted text and typed extractions. ```python Python theme={null} compacted = await sdk.conversation.context.get_compacted( conversation_id="3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c", version=None, # None = latest version format="structured", ) if compacted: print(f"Compacted context: {compacted.compacted_context[:200]}...") print(f"Compression ratio: {compacted.compression_ratio:.0%}") for fact in compacted.facts: print(f"- {fact}") else: print("No compaction exists for this conversation yet.") ``` ```javascript JavaScript theme={null} const compacted = await sdk.conversation.context.get_compacted({ conversation_id: '3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c', // omit `version` for the latest format: 'structured', }); if (compacted) { console.log(`Compacted context: ${String(compacted.compacted_context).slice(0, 200)}...`); console.log(`Compression ratio: ${((compacted.compression_ratio as number) * 100).toFixed(0)}%`); for (const fact of (compacted.facts as string[] | undefined) ?? []) { console.log('-', fact); } } else { console.log('No compaction exists for this conversation yet'); } ``` ```typescript TypeScript theme={null} const compacted = await sdk.conversation.context.get_compacted({ conversation_id: '3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c', // omit `version` for the latest format: 'structured', }); if (compacted) { console.log(`Compacted context: ${String(compacted.compacted_context).slice(0, 200)}...`); console.log(`Compression ratio: ${((compacted.compression_ratio as number) * 100).toFixed(0)}%`); for (const fact of (compacted.facts as string[] | undefined) ?? []) { console.log('-', fact); } } else { console.log('No compaction exists for this conversation yet'); } ``` **Key parameters.** Pass `version=None` (the default) for the latest compaction, or a specific version number to fetch that run from the cloud (skipping the local cache). `format` controls the shape of the result: `"structured"` returns typed `facts`, `decisions`, `preferences`, and `current_state` lists; `"narrative"` returns a prose summary in `compacted_context`; `"injection"` returns a pre-formatted string for direct prompt injection. `get_compacted()` returns a `CompactionResponse` with the compacted text (`compacted_context`), token counts and `compression_ratio`, the typed `facts` / `decisions` / `preferences` extractions, and quality signals (`validation_score`, `validation_passed`, `quality_warning`). Pay attention to the `quality_warning` field. When present, it indicates that the compaction may have lost important information. Consider using a less aggressive strategy or increasing `target_tokens` if quality warnings appear consistently. Every `get_compacted()` parameter and `CompactionResponse` field, with types and defaults. ### Checking Compaction Status Use `sdk.conversation.context.get_compaction_status()` to check the current compaction state of a conversation without retrieving the full compacted content. ```python Python theme={null} status = await sdk.conversation.context.get_compaction_status( conversation_id="3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c" ) print(f"Status: {status.status}") # e.g. "completed", "in_progress", "none" print(f"Compaction ID: {status.compaction_id}") print(f"Latest version: {status.latest_version}") print(f"Compression ratio: {status.compression_ratio}") print(f"Validation score: {status.validation_score}") print(f"Completed at: {status.completed_at}") print(f"Latest created at: {status.latest_created_at}") ``` ```javascript JavaScript theme={null} const status = await sdk.conversation.context.get_compaction_status({ conversation_id: '3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c', }); console.log(`Status: ${status.status}`); // e.g. "completed", "in_progress", "none" console.log(`Compaction ID: ${status.compaction_id}`); console.log(`Latest version: ${status.latest_version}`); console.log(`Compression ratio: ${status.compression_ratio}`); console.log(`Validation score: ${status.validation_score}`); console.log(`Completed at: ${status.completed_at}`); console.log(`Latest created at: ${status.latest_created_at}`); ``` ```typescript TypeScript theme={null} const status = await sdk.conversation.context.get_compaction_status({ conversation_id: '3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c', }); console.log(`Status: ${status.status}`); // e.g. "completed", "in_progress", "none" console.log(`Compaction ID: ${status.compaction_id}`); console.log(`Latest version: ${status.latest_version}`); console.log(`Compression ratio: ${status.compression_ratio}`); console.log(`Validation score: ${status.validation_score}`); console.log(`Completed at: ${status.completed_at}`); console.log(`Latest created at: ${status.latest_created_at}`); ``` The status response is a `CompactionStatusResponse` Pydantic model with the following fields: | Field | Type | Description | | ------------------------------ | -------------------- | ---------------------------------------------------------------------------------- | | `conversation_id` | `str` | The conversation this status refers to | | `status` | `str` | One of `"completed"`, `"in_progress"`, `"failed"`, or `"none"` (no compaction yet) | | `compaction_id` | `Optional[str]` | ID of the current compaction job, if one exists | | `completed_at` | `Optional[datetime]` | When the last successful compaction finished | | `compression_ratio` | `Optional[float]` | Compression ratio of the current compaction | | `validation_score` | `Optional[float]` | Quality score of the current compaction | | `estimated_completion_seconds` | `Optional[int]` | If `status == "in_progress"`, approximate seconds remaining | | `error_message` | `Optional[str]` | Populated when `status == "failed"` | | `latest_version` | `Optional[int]` | Highest version number on record | | `latest_created_at` | `Optional[datetime]` | When `latest_version` was created | To check whether *any* compaction exists, use `status.status != "none"`. To check whether the compaction is current with the latest messages, fetch `get_context_for_prompt` and inspect its `is_stale` field: staleness is a property of the prompt-ready view, not the compaction job itself. Every field on `CompactionStatusResponse`, with types and defaults. *** ## Full Examples ### Simple: Get Context for Prompt The recommended approach for most integrations. One call per LLM turn. ```python Python theme={null} async def chat_with_memory(conversation_id: str, user_message: str): """Chat using compacted memory context.""" # Get compacted context (cached locally, safe to call every turn) compacted = await sdk.conversation.context.get_context_for_prompt( conversation_id=conversation_id, style="structured" ) # Also fetch query-specific context for this message recent = await sdk.conversation.context.fetch( conversation_id=conversation_id, search_query=[user_message], max_results=5, mode="fast" ) recent_facts = "\n".join( f"- {fact.content}" for fact in recent.facts if fact.confidence >= 0.8 ) # Build system prompt history_section = "" if compacted.available: history_section = f""" ## Conversation History (Compacted) {compacted.formatted_context} """ if compacted.is_stale: # Optionally trigger re-compaction in the background await sdk.conversation.context.compact( conversation_id=conversation_id, strategy="adaptive" ) system_prompt = f"""You are a helpful assistant with memory of past conversations. {history_section} ## Recently Relevant Facts {recent_facts if recent_facts else "None specifically relevant to this query."} Use this context to personalize your responses.""" # Pass to your LLM # response = await llm.generate(system_prompt, user_message) return system_prompt ``` ```javascript JavaScript theme={null} async function chatWithMemory(conversationId, userMessage) { // Chat using compacted memory context. // Get compacted context (cached locally, safe to call every turn) const compacted = await sdk.conversation.context.get_context_for_prompt({ conversation_id: conversationId, style: 'structured', }); // Also fetch query-specific context for this message const recent = await sdk.conversation.context.fetch({ conversation_id: conversationId, search_query: [userMessage], max_results: 5, mode: 'fast', }); const recentFacts = (recent.facts ?? []) .filter((fact) => (fact.confidence ?? 0) >= 0.8) .map((fact) => `- ${fact.content}`) .join('\n'); // Build system prompt let historySection = ''; if (compacted.available) { historySection = ` ## Conversation History (Compacted) ${compacted.formatted_context} `; if (compacted.is_stale) { // Optionally trigger re-compaction in the background await sdk.conversation.context.compact({ conversation_id: conversationId, strategy: 'adaptive', }); } } const systemPrompt = `You are a helpful assistant with memory of past conversations. ${historySection} ## Recently Relevant Facts ${recentFacts || 'None specifically relevant to this query.'} Use this context to personalize your responses.`; // Pass to your LLM // const response = await llm.generate(systemPrompt, userMessage); return systemPrompt; } ``` ```typescript TypeScript theme={null} async function chatWithMemory( conversationId: string, userMessage: string, ): Promise { // Chat using compacted memory context. // Get compacted context (cached locally, safe to call every turn) const compacted = await sdk.conversation.context.get_context_for_prompt({ conversation_id: conversationId, style: 'structured', }); // Also fetch query-specific context for this message const recent = await sdk.conversation.context.fetch({ conversation_id: conversationId, search_query: [userMessage], max_results: 5, mode: 'fast', }); const recentFacts = (recent.facts ?? []) .filter((fact) => (fact.confidence ?? 0) >= 0.8) .map((fact) => `- ${fact.content}`) .join('\n'); // Build system prompt let historySection = ''; if (compacted.available) { historySection = ` ## Conversation History (Compacted) ${compacted.formatted_context} `; if (compacted.is_stale) { // Optionally trigger re-compaction in the background await sdk.conversation.context.compact({ conversation_id: conversationId, strategy: 'adaptive', }); } } const systemPrompt = `You are a helpful assistant with memory of past conversations. ${historySection} ## Recently Relevant Facts ${recentFacts || 'None specifically relevant to this query.'} Use this context to personalize your responses.`; // Pass to your LLM // const response = await llm.generate(systemPrompt, userMessage); return systemPrompt; } ``` ### Advanced: Manual Compaction Control Use this approach when you need explicit control over compaction strategy and timing. ```python Python theme={null} async def get_optimized_context(conversation_id: str, token_budget: int = 2000) -> str: """Get compacted context for a conversation, re-compacting if needed.""" # Check current compaction status (CompactionStatusResponse: use dot access) status = await sdk.conversation.context.get_compaction_status( conversation_id=conversation_id ) if status.status == "completed": # Existing compaction is available: retrieve it compacted = await sdk.conversation.context.get_compacted( conversation_id=conversation_id, format="structured" ) if compacted: return compacted.compacted_context # No completed compaction yet: trigger one trigger = await sdk.conversation.context.compact( conversation_id=conversation_id, strategy="adaptive", target_tokens=token_budget ) print(f"Compaction {trigger.compaction_id} triggered, " f"status={trigger.status}, " f"~{trigger.estimated_completion_seconds}s to complete") # Poll until ready, then fetch the result while True: status = await sdk.conversation.context.get_compaction_status( conversation_id=conversation_id ) if status.status in ("completed", "failed"): break await asyncio.sleep(1) if status.status == "failed": raise RuntimeError(f"Compaction failed: {status.error_message}") compacted = await sdk.conversation.context.get_compacted( conversation_id=conversation_id, format="structured" ) if compacted.quality_warning: print(f"Compaction quality warning set") print(f"Compacted {compacted.original_token_count} tokens → " f"{compacted.compacted_token_count} tokens " f"({compacted.compression_ratio:.0%} ratio, " f"validation: {compacted.validation_score})") return compacted.compacted_context async def chat_with_compacted_memory(conversation_id: str, user_message: str): """Chat using compacted memory context.""" # Get optimized context within token budget memory_context = await get_optimized_context( conversation_id=conversation_id, token_budget=2000 ) # Also fetch recent high-relevance context for this specific query recent = await sdk.conversation.context.fetch( conversation_id=conversation_id, search_query=[user_message], max_results=5, mode="fast" ) # Build system prompt with both compacted history and recent context recent_facts = "\n".join( f"- {fact.content}" for fact in recent.facts if fact.confidence >= 0.8 ) system_prompt = f"""You are a helpful assistant with memory of past conversations. ## Conversation History (Compacted) {memory_context} ## Recently Relevant Facts {recent_facts if recent_facts else "None specifically relevant to this query."} Use this context to personalize your responses.""" # Pass to your LLM # response = await llm.generate(system_prompt, user_message) return system_prompt ``` ```javascript JavaScript theme={null} const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); async function getOptimizedContext(conversationId, tokenBudget = 2000) { // Get compacted context for a conversation, re-compacting if needed. // Check current compaction status let status = await sdk.conversation.context.get_compaction_status({ conversation_id: conversationId, }); if (status.status === 'completed') { // Existing compaction is available: retrieve it const existing = await sdk.conversation.context.get_compacted({ conversation_id: conversationId, format: 'structured', }); if (existing) return existing.compacted_context; } // No completed compaction yet: trigger one const trigger = await sdk.conversation.context.compact({ conversation_id: conversationId, strategy: 'adaptive', target_tokens: tokenBudget, }); console.log( `Compaction ${trigger.compaction_id} triggered, status=${trigger.status}, ` + `~${trigger.estimated_completion_seconds}s to complete`, ); // Poll until ready, then fetch the result for (;;) { status = await sdk.conversation.context.get_compaction_status({ conversation_id: conversationId, }); if (status.status === 'completed' || status.status === 'failed') break; await sleep(1000); } if (status.status === 'failed') { throw new Error(`Compaction failed: ${status.error_message}`); } const compacted = await sdk.conversation.context.get_compacted({ conversation_id: conversationId, format: 'structured', }); if (compacted === null) throw new Error('Compaction reported complete but returned nothing'); if (compacted.quality_warning) console.log('Compaction quality warning set'); console.log( `Compacted ${compacted.original_token_count} tokens -> ` + `${compacted.compacted_token_count} tokens ` + `(${(compacted.compression_ratio * 100).toFixed(0)}% ratio, ` + `validation: ${compacted.validation_score})`, ); return compacted.compacted_context; } async function chatWithCompactedMemory(conversationId, userMessage) { // Get optimized context within token budget const memoryContext = await getOptimizedContext(conversationId, 2000); // Also fetch recent high-relevance context for this specific query const recent = await sdk.conversation.context.fetch({ conversation_id: conversationId, search_query: [userMessage], max_results: 5, mode: 'fast', }); const recentFacts = (recent.facts ?? []) .filter((fact) => (fact.confidence ?? 0) >= 0.8) .map((fact) => `- ${fact.content}`) .join('\n'); const systemPrompt = `You are a helpful assistant with memory of past conversations. ## Conversation History (Compacted) ${memoryContext} ## Recently Relevant Facts ${recentFacts || 'None specifically relevant to this query.'} Use this context to personalize your responses.`; // Pass to your LLM // const response = await llm.generate(systemPrompt, userMessage); return systemPrompt; } ``` ```typescript TypeScript theme={null} const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); async function getOptimizedContext( conversationId: string, tokenBudget = 2000, ): Promise { // Get compacted context for a conversation, re-compacting if needed. // Check current compaction status let status = await sdk.conversation.context.get_compaction_status({ conversation_id: conversationId, }); if (status.status === 'completed') { // Existing compaction is available: retrieve it const existing = await sdk.conversation.context.get_compacted({ conversation_id: conversationId, format: 'structured', }); if (existing) return existing.compacted_context; } // No completed compaction yet: trigger one const trigger = await sdk.conversation.context.compact({ conversation_id: conversationId, strategy: 'adaptive', target_tokens: tokenBudget, }); console.log( `Compaction ${trigger.compaction_id} triggered, status=${trigger.status}, ` + `~${trigger.estimated_completion_seconds}s to complete`, ); // Poll until ready, then fetch the result for (;;) { status = await sdk.conversation.context.get_compaction_status({ conversation_id: conversationId, }); if (status.status === 'completed' || status.status === 'failed') break; await sleep(1000); } if (status.status === 'failed') { throw new Error(`Compaction failed: ${status.error_message}`); } const compacted = await sdk.conversation.context.get_compacted({ conversation_id: conversationId, format: 'structured', }); if (compacted === null) throw new Error('Compaction reported complete but returned nothing'); if (compacted.quality_warning) console.log('Compaction quality warning set'); console.log( `Compacted ${compacted.original_token_count} tokens -> ` + `${compacted.compacted_token_count} tokens ` + `(${((compacted.compression_ratio as number) * 100).toFixed(0)}% ratio, ` + `validation: ${compacted.validation_score})`, ); return compacted.compacted_context; } async function chatWithCompactedMemory( conversationId: string, userMessage: string, ): Promise { // Get optimized context within token budget const memoryContext = await getOptimizedContext(conversationId, 2000); // Also fetch recent high-relevance context for this specific query const recent = await sdk.conversation.context.fetch({ conversation_id: conversationId, search_query: [userMessage], max_results: 5, mode: 'fast', }); const recentFacts = (recent.facts ?? []) .filter((fact) => (fact.confidence ?? 0) >= 0.8) .map((fact) => `- ${fact.content}`) .join('\n'); const systemPrompt = `You are a helpful assistant with memory of past conversations. ## Conversation History (Compacted) ${memoryContext} ## Recently Relevant Facts ${recentFacts || 'None specifically relevant to this query.'} Use this context to personalize your responses.`; // Pass to your LLM // const response = await llm.generate(systemPrompt, userMessage); return systemPrompt; } ``` ## Answering from the SDK, without a round trip `get_context_for_prompt()` and `get_compacted()` normally call Synap Cloud. When the SDK already holds the conversation's compaction and its recent turns, it can answer from that instead, which saves the round trip **and** the metered call. This is off by default, because it changes where the answer comes from and you should opt into that knowingly. ```python Python theme={null} from maximem_synap import MaximemSynapSDK, SDKConfig sdk = MaximemSynapSDK(config=SDKConfig(sdk_st_authoritative=True)) ``` ```typescript TypeScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ sdk_st_authoritative: true }); ``` `SYNAP_SDK_ST_AUTHORITATIVE=1` enables it without a code change. **When the local path is taken.** Only when the conversation is *warm*: a compaction has arrived for it, or turns are buffered locally. A cold conversation still goes to the server, because there is nothing to render. A `get_compacted()` call that pins an explicit `version` also always goes to the server, since that is a request for one specific stored artefact. **What you give up.** A locally rendered response reports `is_stale: false` and leaves `compression_ratio` and `validation_score` unset, because those are server-side measurements the SDK cannot compute. If your code branches on them, keep this off. Both SDKs render the three styles (`structured`, `narrative`, `bullet_points`) to byte-identical output, so switching languages does not change what your model sees. ## Best Practices For most integrations, `get_context_for_prompt()` is the right choice. It returns prompt-ready context in a single call, handles local caching automatically, and includes staleness and quality metadata. Only reach for the manual methods (`compact()`, `get_compacted()`, `get_compaction_status()`) when you need explicit control over strategy or token budgets. Use compacted context for broad historical context and `conversation.context.fetch()` for query-specific recent context. This gives your LLM both a comprehensive history and targeted relevant details. Both examples above demonstrate this pattern. The `adaptive` strategy automatically selects the right compression level based on content density. It is the safest default for most applications and handles a wide range of conversation lengths and content types. If your LLM has a 128k token context window and your system prompt uses \~2k tokens plus the user message, you might allocate 4-8k tokens for compacted memory context. Use `target_tokens` to enforce this budget. Use `get_compaction_status()` to avoid unnecessary re-compaction. Only compact when `is_stale` is `True`, meaning new memories have been added since the last compaction. This saves processing time and API calls. If you are using `get_context_for_prompt()`, the `is_stale` field on the response serves the same purpose. Track `validation_score` over time. Consistently low scores (below 0.7) may indicate that your conversations contain highly diverse topics that do not compress well. Consider switching to `conservative` strategy or increasing `target_tokens`. When `quality_warning` is present, log it and consider falling back to a less aggressive strategy. You can implement an automatic fallback pattern: ```python Python theme={null} compaction = await sdk.conversation.context.compact( conversation_id=conv_id, strategy="balanced" ) if compaction.quality_warning: # Retry with less compression compaction = await sdk.conversation.context.compact( conversation_id=conv_id, strategy="conservative", force=True ) ``` ```javascript JavaScript theme={null} let compaction = await sdk.conversation.context.compact({ conversation_id: conv_id, strategy: 'balanced', }); if (compaction.quality_warning) { // Retry with less compression compaction = await sdk.conversation.context.compact({ conversation_id: conv_id, strategy: 'conservative', force: true, }); } ``` ```typescript TypeScript theme={null} let compaction = await sdk.conversation.context.compact({ conversation_id: conv_id, strategy: 'balanced', }); if (compaction.quality_warning) { // Retry with less compression compaction = await sdk.conversation.context.compact({ conversation_id: conv_id, strategy: 'conservative', force: true, }); } ``` ## Next Steps Retrieve context to combine with compacted history. Ingest new data that triggers compaction staleness. Deep dive into compaction algorithms and architecture. Configure timeouts and retries for compaction operations. # Context Fetch Source: https://docs.maximem.ai/sdk/context-fetch Fetch contextual memories for your AI agent. Also known as memory retrieval. ## Overview Context fetch is how your AI agent gets relevant context from stored memories. When your agent needs to know a user's preferences, recall past conversations, or understand organizational context, it calls the retrieval API. Synap returns a structured `ContextResponse` containing facts, preferences, episodes, and emotions ranked by relevance to the query. Context fetch is designed to sit in your agent's **hot path**: the `fast` mode is lower-latency than `accurate`, making it suitable for real-time conversation flows. **A brand-new conversation (or a user/scope with nothing ingested yet) returns an empty `ContextResponse`, not an error.** The typed lists (`facts`, `preferences`, `episodes`, `emotions`, `temporal_events`) come back empty. Check for emptiness rather than catching an exception for the cold-start case. `ContextNotFoundError` is reserved for a context resource that is genuinely missing/removed, and a malformed (non-UUID) `conversation_id` raises `InvalidInputError`. See [Error Handling](/sdk/error-handling#contextnotfounderror) for the per-method breakdown. ## Conversation Context The primary retrieval interface is `sdk.conversation.context.fetch()`. It returns context relevant to a specific conversation, enriched with memories from the user's broader history. ```python Python theme={null} context = await sdk.conversation.context.fetch( conversation_id="3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c", search_query=["project deadlines", "Q2 planning"], max_results=10, types=["facts", "preferences"], mode="fast" ) print(f"Found {len(context.facts)} facts, {len(context.preferences)} preferences") ``` ```javascript JavaScript theme={null} const context = await sdk.conversation.context.fetch({ conversation_id: '3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c', search_query: ['project deadlines', 'Q2 planning'], max_results: 10, types: ['facts', 'preferences'], mode: 'fast', }); console.log(`Found ${(context.facts ?? []).length} facts, ${(context.preferences ?? []).length} preferences`); ``` ```typescript TypeScript theme={null} const context = await sdk.conversation.context.fetch({ conversation_id: '3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c', search_query: ['project deadlines', 'Q2 planning'], max_results: 10, types: ['facts', 'preferences'], mode: 'fast', }); console.log(`Found ${(context.facts ?? []).length} facts, ${(context.preferences ?? []).length} preferences`); ``` ### Key parameters `conversation_id` is the only required argument; it must be a valid UUID string (generate one with `str(uuid.uuid4())` or reuse a UUID you already manage). The remaining arguments shape retrieval: `search_query` (one or more semantic queries, merged and re-ranked when you pass several), `max_results` (default `10`), `mode` (`fast` or `accurate`; see [Retrieval Modes](#retrieval-modes) below), and `types`. There is also an optional `precision_level` (`"high"`, the default, or `"medium"`): `"medium"` gives you faster responses with less precisely filtered results; recall isn't impacted. See [Precision level](#precision-level) below. For the `types` filter on `conversation.context.fetch()`, the accepted string values are the plural forms plus `"all"`: `"facts"`, `"preferences"`, `"episodes"`, `"emotions"`, `"temporal"`, and `"all"`. Omitting `types` returns every type. **Full parameter reference →** Every argument, including the recommended `user_id` / `customer_id` scoping hints, is documented field-by-field in [`conversation.context.fetch`](/sdk-reference/conversation-context/fetch). ## Retrieval Modes Synap offers two retrieval modes that trade off latency against comprehensiveness. | Aspect | `fast` | `accurate` | | ----------------- | ---------------------------------------------- | --------------------------------------------------------------------------- | | **Search method** | Vector + graph (no LLM subquery decomposition) | Vector + graph + LLM subquery decomposition + reranking | | **Best for** | Real-time chat, low-latency requirements | Complex queries, relationship-aware context | | **Ranking** | Similarity + graph signals | Multi-signal ranking (similarity + recency + graph centrality + LLM rerank) | Start with `fast` mode. Switch to `accurate` when you need relationship-aware context, such as queries that span multiple entities ("What did Alice say about the project Bob is leading?"). Retrieval `mode` values (`fast` / `accurate`) are distinct from ingestion `mode` values (`fast` / `long-range`). They control different stages of the pipeline and are not interchangeable: passing `"long-range"` to `context.fetch()` or `"accurate"` to `memories.create()` will be rejected. ### When to Use Each Mode * You are in a real-time conversation flow * The query is about a single topic or entity * You need the lowest-latency retrieval path * You are retrieving frequently (e.g., every turn) * The query involves relationships between entities * You need context spanning multiple conversations * You are building a comprehensive summary * You can afford additional latency for LLM-driven query decomposition and reranking ### Precision level Every context fetch also accepts an optional `precision_level` parameter that controls how tightly results are filtered before they are returned. | `precision_level` | Behavior | | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | `high` | Results go through an additional relevance-refinement pass before being returned. **Default.** | | `medium` | Skips the refinement pass for faster responses. Recall isn't impacted (the same candidate memories are searched), but outputs are less precisely filtered. | `precision_level` is independent of both `mode` axes: combine it with either `fast` or `accurate` retrieval (it does not apply to ingestion). Passing any value other than `"high"` or `"medium"` raises `InvalidInputError`. For real latency on your instance, see **Dashboard → Usage**. Keep `high` for most integrations; drop to `medium` on latency-critical hot paths where a few extra loosely-related items are acceptable. ## Response Structure The `ContextResponse` object contains structured memory types and metadata. ```python Python theme={null} context = await sdk.conversation.context.fetch( conversation_id="3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c", search_query=["dietary preferences"], mode="fast" ) ``` ```javascript JavaScript theme={null} const context = await sdk.conversation.context.fetch({ conversation_id: '3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c', search_query: ['dietary preferences'], mode: 'fast', }); ``` ```typescript TypeScript theme={null} const context = await sdk.conversation.context.fetch({ conversation_id: '3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c', search_query: ['dietary preferences'], mode: 'fast', }); ``` A `ContextResponse` is a bag of typed memory items: `facts`, `preferences`, `episodes`, `emotions`, and `temporal_events`, plus a `metadata` object. Each list is empty when nothing relevant was found, so iterate defensively. The examples below show the fields you reach for most often; the complete Pydantic signatures for every type live in the reference. **Full response reference →** [Response Shapes](/sdk/response-shapes) is the single source of truth for every field on `Fact`, `Preference`, `Episode`, `Emotion`, `TemporalEvent`, `ContextResponse`, and `ResponseMetadata`. ### Facts Facts are discrete, verified pieces of information extracted from memories. ```python Python theme={null} for fact in context.facts: print(f"[{fact.confidence:.0%}] {fact.content}") print(f" Source: {fact.source}") print(f" Extracted: {fact.extracted_at}") ``` ```javascript JavaScript theme={null} for (const fact of context.facts ?? []) { console.log(`[${((fact.confidence ?? 0) * 100).toFixed(0) + "%"}] ${fact.content}`); console.log(` Source: ${fact.source}`); console.log(` Extracted: ${fact.extracted_at}`); } ``` ```typescript TypeScript theme={null} for (const fact of context.facts ?? []) { console.log(`[${((fact.confidence ?? 0) * 100).toFixed(0) + "%"}] ${fact.content}`); console.log(` Source: ${fact.source}`); console.log(` Extracted: ${fact.extracted_at}`); } ``` ### Preferences Preferences capture user likes, dislikes, and stated preferences. Note that the certainty signal on a `Preference` is `strength` (not `confidence` as on `Fact`), and the text is in `content` grouped by `category`. ```python Python theme={null} for pref in context.preferences: print(f"[{pref.category}] {pref.content} (strength: {pref.strength:.0%})") ``` ```javascript JavaScript theme={null} for (const pref of context.preferences ?? []) { console.log(`[${pref.category}] ${pref.content} (strength: ${((pref.strength ?? 0) * 100).toFixed(0) + "%"})`); } ``` ```typescript TypeScript theme={null} for (const pref of context.preferences ?? []) { console.log(`[${pref.category}] ${pref.content} (strength: ${((pref.strength ?? 0) * 100).toFixed(0) + "%"})`); } ``` ### Episodes Episodes represent summarized narrative segments from past interactions. The narrative text is in `summary` (not `content`), ranked by `significance`. ```python Python theme={null} for episode in context.episodes: print(f"[{episode.significance:.0%}] {episode.summary}") print(f" Occurred: {episode.occurred_at}") print(f" Participants: {', '.join(episode.participants)}") ``` ```javascript JavaScript theme={null} for (const episode of context.episodes ?? []) { console.log(`[${((episode.significance ?? 0) * 100).toFixed(0) + "%"}] ${episode.summary}`); console.log(` Occurred: ${episode.occurred_at}`); console.log(` Participants: ${', '.join(episode.participants)}`); } ``` ```typescript TypeScript theme={null} for (const episode of context.episodes ?? []) { console.log(`[${((episode.significance ?? 0) * 100).toFixed(0) + "%"}] ${episode.summary}`); console.log(` Occurred: ${episode.occurred_at}`); console.log(` Participants: ${', '.join(episode.participants)}`); } ``` ### Emotions Emotions capture detected emotional states and sentiment from interactions, scored by `intensity`. ```python Python theme={null} for emotion in context.emotions: print(f"{emotion.emotion_type} (intensity: {emotion.intensity:.0%})") print(f" Triggered by: {emotion.context}") print(f" Detected: {emotion.detected_at}") ``` ```javascript JavaScript theme={null} for (const emotion of context.emotions ?? []) { console.log(`${emotion.emotion_type} (intensity: ${((emotion.intensity ?? 0) * 100).toFixed(0) + "%"})`); console.log(` Triggered by: ${emotion.context}`); console.log(` Detected: ${emotion.detected_at}`); } ``` ```typescript TypeScript theme={null} for (const emotion of context.emotions ?? []) { console.log(`${emotion.emotion_type} (intensity: ${((emotion.intensity ?? 0) * 100).toFixed(0) + "%"})`); console.log(` Triggered by: ${emotion.context}`); console.log(` Detected: ${emotion.detected_at}`); } ``` ### Temporal Events Time-bound events with explicit start and (optionally) end markers, e.g., "user's subscription renews on 2026-08-12". ```python Python theme={null} for event in context.temporal_events: print(f"{event.content} ({event.temporal_category})") print(f" Valid: {event.event_date} → {event.valid_until or 'open-ended'}") ``` ```javascript JavaScript theme={null} for (const event of context.temporal_events ?? []) { console.log(`${event.content} (${event.temporal_category})`); console.log(` Valid: ${event.event_date} → ${event.valid_until || 'open-ended'}`); } ``` ```typescript TypeScript theme={null} for (const event of context.temporal_events ?? []) { console.log(`${event.content} (${event.temporal_category})`); console.log(` Valid: ${event.event_date} → ${event.valid_until || 'open-ended'}`); } ``` ### Response Metadata Every `ContextResponse` includes metadata about the retrieval operation. ```python Python theme={null} meta = context.metadata print(f"Correlation ID: {meta.correlation_id}") print(f"Source: {meta.source}") # "cache", "cloud", or "anticipation" print(f"TTL: {meta.ttl_seconds}s") if meta.compaction_applied is not None: print(f"Compaction applied: {meta.compaction_applied.value}") # e.g. "adaptive" ``` ```javascript JavaScript theme={null} const meta = context.metadata; console.log(`Correlation ID: ${meta.correlationId}`); console.log(`Source: ${meta.source}`); // "cache", "cloud", or "anticipation" console.log(`TTL: ${meta.ttl_seconds}s`); if (meta.compaction_applied != null) { console.log(`Compaction applied: ${meta.compaction_applied.value}`); // e.g. "adaptive" } ``` ```typescript TypeScript theme={null} const meta = context.metadata; console.log(`Correlation ID: ${meta.correlationId}`); console.log(`Source: ${meta.source}`); // "cache", "cloud", or "anticipation" console.log(`TTL: ${meta.ttl_seconds}s`); if (meta.compaction_applied != null) { console.log(`Compaction applied: ${meta.compaction_applied.value}`); // e.g. "adaptive" } ``` Log `metadata.correlation_id` for debugging and support inquiries. `metadata.source` tells you where the response came from (`"cache"`, `"cloud"`, or `"anticipation"`), and `metadata.ttl_seconds` is how long the local cache treats it as fresh. `metadata.compaction_applied` is **not** a boolean. It is `None` when no compaction ran, or a `CompactionLevel` enum value when one did, so always test with `if meta.compaction_applied is not None` rather than a truthiness check. The strategy-named members (`adaptive`, `aggressive`, `balanced`, `conservative`) are the ones you'll typically see on retrieval. See [`CompactionLevel`](/sdk/response-shapes#compactionlevel-enum) for the full enum. ## Scoped Retrieval In addition to conversation-level retrieval, Synap provides scope-specific interfaces for retrieving memories at the user, customer, and client levels. ### User Context Retrieve all memories scoped to a specific user, across all their conversations. ```python Python theme={null} # `user_id` is required. `conversation_id` is optional: pass it if you want # the relevance ranking to bias toward memories from that conversation. user_context = await sdk.user.context.fetch( user_id="user_alice", customer_id="acme_corp", # required on B2B instances search_query=["travel preferences"], max_results=20, mode="accurate", ) # Returns facts, preferences, episodes, emotions, temporal_events # scoped to user_alice. ``` ```javascript JavaScript theme={null} // `user_id` is required. `conversation_id` is optional: pass it if you want // the relevance ranking to bias toward memories from that conversation. const user_context = await sdk.user.context.fetch({ user_id: 'user_alice', customer_id: 'acme_corp', // required on B2B instances search_query: ['travel preferences'], max_results: 20, mode: 'accurate', }); // Returns facts, preferences, episodes, emotions, temporal_events // scoped to user_alice. ``` ```typescript TypeScript theme={null} // `user_id` is required. `conversation_id` is optional: pass it if you want // the relevance ranking to bias toward memories from that conversation. const user_context = await sdk.user.context.fetch({ user_id: 'user_alice', customer_id: 'acme_corp', // required on B2B instances search_query: ['travel preferences'], max_results: 20, mode: 'accurate', }); // Returns facts, preferences, episodes, emotions, temporal_events // scoped to user_alice. ``` ### Customer Context Retrieve memories shared across all users within a customer (organization/tenant). ```python Python theme={null} # `customer_id` is required for customer-scoped retrieval. customer_context = await sdk.customer.context.fetch( customer_id="acme_corp", search_query=["engineering team OKRs"], max_results=15, mode="accurate", ) ``` ```javascript JavaScript theme={null} // `customer_id` is required for customer-scoped retrieval. const customer_context = await sdk.customer.context.fetch({ customer_id: 'acme_corp', search_query: ['engineering team OKRs'], max_results: 15, mode: 'accurate', }); ``` ```typescript TypeScript theme={null} // `customer_id` is required for customer-scoped retrieval. const customer_context = await sdk.customer.context.fetch({ customer_id: 'acme_corp', search_query: ['engineering team OKRs'], max_results: 15, mode: 'accurate', }); ``` ### Client Context Retrieve memories at the broadest scope, across all customers and users within your Synap client. ```python Python theme={null} client_context = await sdk.client.context.fetch( search_query=["product roadmap"], max_results=10, mode="fast", ) ``` ```javascript JavaScript theme={null} const client_context = await sdk.client.context.fetch({ search_query: ['product roadmap'], max_results: 10, mode: 'fast', }); ``` ```typescript TypeScript theme={null} const client_context = await sdk.client.context.fetch({ search_query: ['product roadmap'], max_results: 10, mode: 'fast', }); ``` Scoped retrieval respects Synap's scope hierarchy. User context includes user-scoped memories. Customer context includes customer-scoped memories visible to all users in that customer. Client context includes client-wide memories. Higher scopes never leak memories from narrower scopes unless those memories were explicitly created at the broader scope. See [Memory Scopes](/concepts/memory-scopes) for details. **Full parameter reference →** Each scoped method has its own reference page with the complete argument list and per-scope `types` values: [`user.context.fetch`](/sdk-reference/context/user-fetch), [`customer.context.fetch`](/sdk-reference/context/customer-fetch), and [`client.context.fetch`](/sdk-reference/context/client-fetch). ## Conversation-Summary Mode (Conversation Start) `fetch()` and `user.context.fetch()` accept `context_mode="conversation-summary"`: the **conversation-start read** for async integrations. Instead of retrieving item lists, it assembles the user's **profile** plus summaries of their last *n* conversations (what each was about and how it progressed). It runs no search and no LLM work (it returns the stored profile and precomputed summaries), so it is fast enough to fire the moment a conversation begins, before your agent's first reply. The profile half of this mode is **on by default**. See the [User Profile guide](/sdk/user-profile) for defining your critical-attribute schema (or opting out per instance). ```python Python theme={null} ctx = await sdk.fetch( user_id="+919812345678", # user identity (e.g. phone, external id) context_mode="conversation-summary", include_profile=True, last_n_conversations=1, ) # formatted_context now carries "## Caller Profile" and "## Previous Conversations" prompt_block = ctx.formatted_context # Or read the typed fields directly: if ctx.profile: print(ctx.profile.overview) for call in (ctx.conversations or []): print(call.summary_status, call.summary) ``` ```javascript JavaScript theme={null} const ctx = await sdk.fetch({ user_id: '+919812345678', // user identity (e.g. phone, external id) context_mode: 'conversation-summary', include_profile: true, last_n_conversations: 1, }); // formatted_context now carries "## Caller Profile" and "## Previous Conversations" const prompt_block = ctx.formatted_context; // Or read the typed fields directly: if (ctx.profile) { console.log(ctx.profile.overview); } for (const call of (ctx.conversations || [])) { console.log(call.summary_status, call.summary); } ``` ```typescript TypeScript theme={null} const ctx = await sdk.fetch({ user_id: '+919812345678', // user identity (e.g. phone, external id) context_mode: 'conversation-summary', include_profile: true, last_n_conversations: 1, }); // formatted_context now carries "## Caller Profile" and "## Previous Conversations" const prompt_block = ctx.formatted_context; // Or read the typed fields directly: if (ctx.profile) { console.log(ctx.profile.overview); } for (const call of (ctx.conversations || [])) { console.log(call.summary_status, call.summary); } ``` Behavior notes: * Requires `user_id`. On B2B instances `customer_id` is also required (the same user identifier under two different customers refers to two different people). * `search_query`, `mode` and `precision_level` are **ignored**: this is an assembly, not a retrieval. * In the unified `sdk.fetch`, the three summary params are forwarded **only** to the user-scope sub-fetch. * `last_n_conversations=0` with `include_profile=False` is valid: you get an empty result, not an error. * A conversation whose summary hasn't landed yet reports `summary_status="pending"` (or `"failed"`); the conversation still appears in the list. The call-end counterpart is [`conversation.ingest_transcript`](/sdk/ingestion#one-shot-transcript-ingest). Full reference: [`fetch`](/sdk-reference/context/fetch) and [`user.context.fetch`](/sdk-reference/context/user-fetch). ## The `types` Filter Use the `types` parameter to retrieve only specific memory types. This reduces response size and processing time when you only need certain kinds of context. ```python Python theme={null} # Only retrieve facts and preferences context = await sdk.conversation.context.fetch( conversation_id="3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c", types=["facts", "preferences"], mode="fast" ) # Only retrieve temporal/episode context context = await sdk.conversation.context.fetch( conversation_id="3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c", types=["episodes", "temporal"], mode="accurate" ) # Explicitly request all types (same as omitting the parameter) context = await sdk.conversation.context.fetch( conversation_id="3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c", types=["all"], mode="fast" ) ``` ```javascript JavaScript theme={null} // Only retrieve facts and preferences let context = await sdk.conversation.context.fetch({ conversation_id: '3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c', types: ['facts', 'preferences'], mode: 'fast', }); // Only retrieve temporal/episode context context = await sdk.conversation.context.fetch({ conversation_id: '3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c', types: ['episodes', 'temporal'], mode: 'accurate', }); // Explicitly request all types (same as omitting the parameter) context = await sdk.conversation.context.fetch({ conversation_id: '3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c', types: ['all'], mode: 'fast', }); ``` ```typescript TypeScript theme={null} // Only retrieve facts and preferences let context = await sdk.conversation.context.fetch({ conversation_id: '3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c', types: ['facts', 'preferences'], mode: 'fast', }); // Only retrieve temporal/episode context context = await sdk.conversation.context.fetch({ conversation_id: '3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c', types: ['episodes', 'temporal'], mode: 'accurate', }); // Explicitly request all types (same as omitting the parameter) context = await sdk.conversation.context.fetch({ conversation_id: '3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c', types: ['all'], mode: 'fast', }); ``` ## Search Queries The `search_query` parameter drives semantic search. Synap matches your queries against stored memories using both semantic similarity and graph relationships. ### Single Query ```python Python theme={null} context = await sdk.conversation.context.fetch( conversation_id="3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c", search_query=["What are the user's dietary restrictions?"] ) ``` ```javascript JavaScript theme={null} const context = await sdk.conversation.context.fetch({ conversation_id: '3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c', search_query: ["What are the user's dietary restrictions?"], }); ``` ```typescript TypeScript theme={null} const context = await sdk.conversation.context.fetch({ conversation_id: '3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c', search_query: ["What are the user's dietary restrictions?"], }); ``` ### Multiple Queries When you provide multiple queries, Synap runs each independently and merges the results with deduplication and re-ranking. ```python Python theme={null} context = await sdk.conversation.context.fetch( conversation_id="3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c", search_query=[ "dietary preferences and restrictions", "favorite restaurants and cuisines", "food allergies" ], max_results=15 ) ``` ```javascript JavaScript theme={null} const context = await sdk.conversation.context.fetch({ conversation_id: '3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c', search_query: [ 'dietary preferences and restrictions', 'favorite restaurants and cuisines', 'food allergies' ], max_results: 15, }); ``` ```typescript TypeScript theme={null} const context = await sdk.conversation.context.fetch({ conversation_id: '3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c', search_query: [ 'dietary preferences and restrictions', 'favorite restaurants and cuisines', 'food allergies' ], max_results: 15, }); ``` Use multiple queries to broaden recall when a single query might miss relevant memories. For example, a user asking "What should I eat?" might benefit from queries about dietary preferences, allergies, and favorite cuisines simultaneously. ### No Query (Recency-Based) When `search_query` is omitted, retrieval returns the most recent and contextually relevant memories for the conversation without semantic filtering. ```python Python theme={null} context = await sdk.conversation.context.fetch( conversation_id="3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c", max_results=5 ) ``` ```javascript JavaScript theme={null} const context = await sdk.conversation.context.fetch({ conversation_id: '3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c', max_results: 5, }); ``` ```typescript TypeScript theme={null} const context = await sdk.conversation.context.fetch({ conversation_id: '3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c', max_results: 5, }); ``` ## The `.raw` Property For forward compatibility with future Synap API changes, every response object exposes a `.raw` property containing the unprocessed API response as a dictionary. ```python Python theme={null} context = await sdk.conversation.context.fetch( conversation_id="3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c", mode="fast" ) # Access raw response for forward compatibility raw = context.raw print(raw.keys()) ``` ```javascript JavaScript theme={null} const context = await sdk.conversation.context.fetch({ conversation_id: '3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c', mode: 'fast', }); // The namespaced surface already returns the raw response, so there is no // separate `.raw`: read any field the server sent straight off the object. console.log(Object.keys(context)); ``` ```typescript TypeScript theme={null} const context = await sdk.conversation.context.fetch({ conversation_id: '3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c', mode: 'fast', }); // The namespaced surface already returns the raw response, so there is no // separate `.raw`: read any field the server sent straight off the object. console.log(Object.keys(context)); ``` The `.raw` property is useful when Synap adds new fields to the API response that have not yet been mapped to typed SDK properties. You can access new fields immediately without waiting for an SDK update. ## Full Example: System Prompt Injection The most common use case for context fetch is injecting contextual memories into your LLM's system prompt. ```python Python theme={null} import json from openai import AsyncOpenAI openai = AsyncOpenAI() async def chat_with_memory(conversation_id: str, user_message: str): # Fetch relevant context context = await sdk.conversation.context.fetch( conversation_id=conversation_id, search_query=[user_message], max_results=10, mode="fast", precision_level="medium", # hot path: skip the refinement pass for a faster response ) # Build memory context string memory_lines = [] if context.facts: memory_lines.append("## Known Facts") for fact in context.facts: if fact.confidence >= 0.7: memory_lines.append(f"- {fact.content}") if context.preferences: memory_lines.append("\n## User Preferences") for pref in context.preferences: memory_lines.append(f"- {pref.content}") if context.episodes: memory_lines.append("\n## Relevant Past Interactions") for episode in context.episodes: memory_lines.append(f"- {episode.summary}") memory_context = "\n".join(memory_lines) if memory_lines else "No prior context available." # Inject into system prompt system_prompt = f"""You are a helpful assistant with access to the user's memory. Use the following context to personalize your responses. Do not mention that you are reading from a memory system. {memory_context}""" # Call LLM response = await openai.chat.completions.create( model="gpt-4o", messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_message} ] ) assistant_message = response.choices[0].message.content # Ingest the new conversation turn for future memory await sdk.memories.create( document=f"User: {user_message}\nAssistant: {assistant_message}", document_type="ai-chat-conversation", user_id="user_12345", customer_id="acme_corp", mode="long-range", ) return assistant_message ``` ```javascript JavaScript theme={null} import OpenAI from 'openai'; const openai = new OpenAI(); async function chatWithMemory(conversationId, userMessage) { // Fetch relevant context const context = await sdk.conversation.context.fetch({ conversation_id: conversationId, search_query: [userMessage], max_results: 10, mode: 'fast', precision_level: 'medium', // hot path: skip the refinement pass for a faster response }); // Build memory context string. Each collection is optional on the raw // response, so default it before testing or iterating. const facts = context.facts ?? []; const preferences = context.preferences ?? []; const episodes = context.episodes ?? []; const memoryLines = []; if (facts.length) { memoryLines.push('## Known Facts'); for (const fact of facts) { if ((fact.confidence ?? 0) >= 0.7) memoryLines.push(`- ${fact.content}`); } } if (preferences.length) { memoryLines.push('\n## User Preferences'); for (const pref of preferences) memoryLines.push(`- ${pref.content}`); } if (episodes.length) { memoryLines.push('\n## Relevant Past Interactions'); // Episodes carry `summary`, not `content`. for (const episode of episodes) memoryLines.push(`- ${episode.summary}`); } const memoryContext = memoryLines.length ? memoryLines.join('\n') : 'No prior context available.'; // Inject into system prompt const systemPrompt = `You are a helpful assistant with access to the user's memory. Use the following context to personalize your responses. Do not mention that you are reading from a memory system. ${memoryContext}`; // Call LLM const response = await openai.chat.completions.create({ model: 'gpt-4o', messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: userMessage }, ], }); const assistantMessage = response.choices[0]?.message.content ?? ''; // Ingest the new conversation turn for future memory await sdk.memories.create({ document: `User: ${userMessage}\nAssistant: ${assistantMessage}`, document_type: 'ai-chat-conversation', user_id: 'user_12345', customer_id: 'acme_corp', mode: 'long-range', }); return assistantMessage; } ``` ```typescript TypeScript theme={null} import OpenAI from 'openai'; const openai = new OpenAI(); async function chatWithMemory( conversationId: string, userMessage: string, ): Promise { // Fetch relevant context const context = await sdk.conversation.context.fetch({ conversation_id: conversationId, search_query: [userMessage], max_results: 10, mode: 'fast', precision_level: 'medium', // hot path: skip the refinement pass for a faster response }); // Build memory context string. Each collection is optional on the raw // response, so default it before testing or iterating. const facts = context.facts ?? []; const preferences = context.preferences ?? []; const episodes = context.episodes ?? []; const memoryLines: string[] = []; if (facts.length) { memoryLines.push('## Known Facts'); for (const fact of facts) { if ((fact.confidence ?? 0) >= 0.7) memoryLines.push(`- ${fact.content}`); } } if (preferences.length) { memoryLines.push('\n## User Preferences'); for (const pref of preferences) memoryLines.push(`- ${pref.content}`); } if (episodes.length) { memoryLines.push('\n## Relevant Past Interactions'); // Episodes carry `summary`, not `content`. for (const episode of episodes) memoryLines.push(`- ${episode.summary}`); } const memoryContext = memoryLines.length ? memoryLines.join('\n') : 'No prior context available.'; // Inject into system prompt const systemPrompt = `You are a helpful assistant with access to the user's memory. Use the following context to personalize your responses. Do not mention that you are reading from a memory system. ${memoryContext}`; // Call LLM const response = await openai.chat.completions.create({ model: 'gpt-4o', messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: userMessage }, ], }); const assistantMessage = response.choices[0]?.message.content ?? ''; // Ingest the new conversation turn for future memory await sdk.memories.create({ document: `User: ${userMessage}\nAssistant: ${assistantMessage}`, document_type: 'ai-chat-conversation', user_id: 'user_12345', customer_id: 'acme_corp', mode: 'long-range', }); return assistantMessage; } ``` For long-running conversations, combine `context.fetch()` with `get_context_for_prompt()` for a hybrid approach: compacted history provides broad context while retrieval provides query-specific details. See the [Context Compaction](/sdk/context-compaction) guide for examples of this pattern. ## Next Steps Learn how entities are resolved to improve retrieval quality. Compress context to reduce LLM token costs. Feed more data into Synap to enrich retrieval results. Understand how scope filtering affects retrieval boundaries. ## JavaScript: cross-scope fetch `fetch()` queries every scope you supply an identifier for, in parallel, deduplicates by item id, and returns a `formatted_context` string ready for prompt injection. ```ts theme={null} const unified = await synap.fetch({ conversation_id, user_id, customer_id, search_query: ["seat preference"], }); console.log(unified.formatted_context); console.log(unified.scopes_queried); // ["conversation", "user", "customer"] ``` A scope that fails is dropped with a warning rather than failing the whole call, so partial context is still returned. An `InvalidInputError` is the exception: a malformed request surfaces instead of degrading to an empty result. # Error Handling Source: https://docs.maximem.ai/sdk/error-handling Handle errors gracefully in your Synap integration. ## Overview The Synap SDK uses a structured error hierarchy to distinguish between transient errors (which are automatically retried) and permanent errors (which require your intervention). Understanding this hierarchy is essential for building robust integrations. This page is the handling guide: the hierarchy, when each error fires, and the try/except patterns to catch them. For the exhaustive catalog of server-side error codes (the wire-level `code` field, HTTP status, and `details` shape) see [Error Codes](/sdk-reference/errors). For the complete SDK exception table at a glance, see [SDK Reference: Error handling](/sdk-reference/overview#error-handling). ## Error Hierarchy All Synap errors inherit from `SynapError`. The two main branches determine retry behavior: ``` SynapError (base) ├── SynapTransientError (retryable: SDK auto-retries) │ ├── NetworkTimeoutError │ ├── RateLimitError │ ├── ServiceUnavailableError │ └── AgentUnavailableError └── SynapPermanentError (non-retryable: requires code/config fix) ├── InvalidInputError │ ├── InvalidInstanceIdError │ └── InvalidConversationIdError ├── AuthenticationError ├── ContextNotFoundError ├── SessionExpiredError ├── InsufficientCreditsError ├── ListeningAlreadyActiveError └── ListeningNotActiveError ``` All errors include an optional `correlation_id` field that uniquely identifies the request. This ID is invaluable for debugging and when contacting Synap support. The hierarchy below reflects the full public error surface. `AgentUnavailableError` is transient (retryable); all others under `SynapPermanentError` are non-retryable. ## Transient Errors Transient errors represent temporary conditions that typically resolve on their own. The SDK **automatically retries** these errors according to your configured [retry policy](#retry-policy-configuration). You only need to handle them if all retry attempts are exhausted. ### NetworkTimeoutError Raised when a network request to Synap Cloud times out before completing. **When it occurs:** * Network connectivity issues between your application and Synap Cloud * DNS resolution failures * The request exceeded the configured `connect` or `read` timeout **How to handle:** `conversation_id` must be a valid UUID string; non-UUID values are rejected by the server. Generate one with `str(uuid.uuid4())`, or reuse the UUID you already manage per conversation. The examples below use `str(uuid.uuid4())` to make this explicit. ```python Python theme={null} import uuid from maximem_synap import NetworkTimeoutError try: context = await sdk.conversation.context.fetch( conversation_id=str(uuid.uuid4()), mode="fast" ) except NetworkTimeoutError as e: logger.warning( "Network timeout after all retries (correlation_id=%s)", e.correlation_id ) # Fall back to cached context or proceed without memory context = None ``` ```javascript JavaScript theme={null} import { randomUUID } from 'node:crypto'; import { NetworkTimeoutError, isSynapError } from '@maximem/synap-js-sdk'; try { let context = await sdk.conversation.context.fetch({ conversation_id: randomUUID(), mode: 'fast', }); } catch (e) { if (!(e instanceof NetworkTimeoutError)) throw e; console.warn('Network timeout after all retries (correlation_id=%s)', e.correlationId); // Fall back to cached context or proceed without memory context = null; } ``` ```typescript TypeScript theme={null} import { randomUUID } from 'node:crypto'; import { NetworkTimeoutError, isSynapError } from '@maximem/synap-js-sdk'; try { let context = await sdk.conversation.context.fetch({ conversation_id: randomUUID(), mode: 'fast', }); } catch (e) { if (!(e instanceof NetworkTimeoutError)) throw e; console.warn('Network timeout after all retries (correlation_id=%s)', e.correlationId); // Fall back to cached context or proceed without memory context = null; } ``` ### RateLimitError Raised when your application exceeds the rate limit for the Synap API. Includes a `retry_after_seconds` field indicating how long to wait before retrying. **When it occurs:** * Too many requests in a short time window * Burst traffic exceeding your plan's rate limit **How to handle:** ```python Python theme={null} from maximem_synap import RateLimitError try: response = await sdk.memories.create( document=doc, document_type="ai-chat-conversation", user_id=uid, customer_id=cid, ) except RateLimitError as e: logger.warning( "Rate limited. Retry after %d seconds (correlation_id=%s)", e.retry_after_seconds, e.correlation_id ) # The SDK retries automatically, but if all retries are exhausted: # - Queue the operation for later # - Reduce request frequency # - Consider upgrading your plan ``` ```javascript JavaScript theme={null} import { RateLimitError, isSynapError } from '@maximem/synap-js-sdk'; try { const response = await sdk.memories.create({ document: doc, document_type: 'ai-chat-conversation', user_id: uid, customer_id: cid, }); } catch (e) { if (!(e instanceof RateLimitError)) throw e; console.warn('Rate limited. Retry after %d seconds (correlation_id=%s)', e.retryAfterSeconds, e.correlationId); // The SDK retries automatically, but if all retries are exhausted: // - Queue the operation for later // - Reduce request frequency // - Consider upgrading your plan } ``` ```typescript TypeScript theme={null} import { RateLimitError, isSynapError } from '@maximem/synap-js-sdk'; try { const response = await sdk.memories.create({ document: doc, document_type: 'ai-chat-conversation', user_id: uid, customer_id: cid, }); } catch (e) { if (!(e instanceof RateLimitError)) throw e; console.warn('Rate limited. Retry after %d seconds (correlation_id=%s)', e.retryAfterSeconds, e.correlationId); // The SDK retries automatically, but if all retries are exhausted: // - Queue the operation for later // - Reduce request frequency // - Consider upgrading your plan } ``` The SDK's built-in retry policy respects `retry_after_seconds` automatically. If the rate limit is short (a few seconds), the SDK waits and retries without raising the error to your code. The error only surfaces when all retry attempts are exhausted. ### ServiceUnavailableError Raised when Synap Cloud is temporarily unavailable due to maintenance, deployment, or an outage. **When it occurs:** * Synap Cloud is undergoing maintenance * A rolling deployment is in progress * Temporary backend issues **How to handle:** ```python Python theme={null} import uuid from maximem_synap import ServiceUnavailableError try: context = await sdk.conversation.context.fetch( conversation_id=str(uuid.uuid4()), mode="fast" ) except ServiceUnavailableError as e: logger.error( "Synap Cloud unavailable (correlation_id=%s)", e.correlation_id ) # Serve from cache if available, or degrade gracefully ``` ```javascript JavaScript theme={null} import { randomUUID } from 'node:crypto'; import { ServiceUnavailableError, isSynapError } from '@maximem/synap-js-sdk'; try { const context = await sdk.conversation.context.fetch({ conversation_id: randomUUID(), mode: 'fast', }); } catch (e) { if (!(e instanceof ServiceUnavailableError)) throw e; console.error('Synap Cloud unavailable (correlation_id=%s)', e.correlationId); // Serve from cache if available, or degrade gracefully } ``` ```typescript TypeScript theme={null} import { randomUUID } from 'node:crypto'; import { ServiceUnavailableError, isSynapError } from '@maximem/synap-js-sdk'; try { const context = await sdk.conversation.context.fetch({ conversation_id: randomUUID(), mode: 'fast', }); } catch (e) { if (!(e instanceof ServiceUnavailableError)) throw e; console.error('Synap Cloud unavailable (correlation_id=%s)', e.correlationId); // Serve from cache if available, or degrade gracefully } ``` ## Permanent Errors Permanent errors indicate problems that will not resolve by retrying. They require changes to your code, configuration, or data. ### InvalidInputError Raised when the request contains invalid parameters or data that fails validation. **When it occurs:** * Invalid `document_type` value * Missing required fields * Parameter values outside valid ranges * Malformed data in the request body **How to handle:** ```python Python theme={null} from maximem_synap import InvalidInputError try: response = await sdk.memories.create( document="", # Empty document document_type="invalid-type", # Invalid document_type value user_id="user_123", customer_id="acme_corp", ) except InvalidInputError as e: logger.error("Invalid input: %s", e) # Fix the input data and retry ``` ```javascript JavaScript theme={null} import { InvalidInputError, isSynapError } from '@maximem/synap-js-sdk'; try { const response = await sdk.memories.create({ document: '', // Empty document document_type: 'invalid-type', // Invalid document_type value user_id: 'user_123', customer_id: 'acme_corp', }); } catch (e) { if (!(e instanceof InvalidInputError)) throw e; console.error('Invalid input: %s', e); // Fix the input data and retry } ``` ```typescript TypeScript theme={null} import { InvalidInputError, isSynapError } from '@maximem/synap-js-sdk'; try { const response = await sdk.memories.create({ document: '', // Empty document document_type: 'invalid-type', // Invalid document_type value user_id: 'user_123', customer_id: 'acme_corp', }); } catch (e) { if (!(e instanceof InvalidInputError)) throw e; console.error('Invalid input: %s', e); // Fix the input data and retry } ``` ### InvalidInstanceIdError A defined subtype of `InvalidInputError` for an unknown or malformed `instance_id`. A **malformed** `instance_id` is rejected by the SDK itself, and raised from the constructor before any request is made: you do not reach `initialize()`. An id that is well-formed but **unknown** to the server is a different case: it comes back as an HTTP 400 and surfaces as the base `InvalidInputError`. Catching `InvalidInputError` handles both, since this is a subtype of it. **When it occurs:** * The `instance_id` does not match the expected format: `inst_` followed by 16 hex characters, e.g. `inst_a1b2c3d4e5f67890`. Raised by `MaximemSynapSDK(...)` itself; an empty value is left alone, since the instance is normally resolved from the API key during `initialize()`. * The instance has been deleted or deactivated (server-side, surfaces as `InvalidInputError`) * A typo in the instance ID **How to handle:** ```python Python theme={null} from maximem_synap import InvalidInputError try: sdk = MaximemSynapSDK(instance_id=configured_id, api_key=key) await sdk.initialize() except InvalidInputError as e: # also catches InvalidInstanceIdError logger.error( "Invalid instance ID. Verify in the Synap Dashboard. " "(correlation_id=%s)", getattr(e, "correlation_id", None) ) raise ``` ```javascript JavaScript theme={null} import { SynapClient, InvalidInputError } from '@maximem/synap-js-sdk'; try { const sdk = new SynapClient({ instanceId: configuredId, apiKey: key }); await sdk.initialize(); } catch (e) { // Also catches InvalidInstanceIdError, which extends InvalidInputError. if (!(e instanceof InvalidInputError)) throw e; console.error( 'Invalid instance ID. Verify in the Synap Dashboard. (correlation_id=%s)', e.correlationId, ); throw e; } ``` ```typescript TypeScript theme={null} import { SynapClient, InvalidInputError } from '@maximem/synap-js-sdk'; try { const sdk = new SynapClient({ instanceId: configuredId, apiKey: key }); await sdk.initialize(); } catch (e) { // Also catches InvalidInstanceIdError, which extends InvalidInputError. if (!(e instanceof InvalidInputError)) throw e; console.error( 'Invalid instance ID. Verify in the Synap Dashboard. (correlation_id=%s)', e.correlationId, ); throw e; } ``` Passing `instance_id` is optional, and usually unnecessary: the API key already identifies its instance and the SDK resolves it during `initialize()`. Omitting it avoids this error class entirely. ### InvalidConversationIdError A defined subtype of `InvalidInputError` for a malformed `conversation_id` (for example, a non-UUID string). The SDK currently surfaces a malformed `conversation_id` as the base **`InvalidInputError`** (HTTP 400), not as this specific subtype. Catch `InvalidInputError`. Note that a *well-formed* `conversation_id` with no messages yet does **not** raise; it returns an empty `ContextResponse` (see [cold-start behavior](#contextnotfounderror)). **When it occurs:** * The `conversation_id` is not a valid UUID * A typo or wrong identifier format **How to handle:** ```python Python theme={null} import uuid from maximem_synap import InvalidInputError try: context = await sdk.conversation.context.fetch( conversation_id=str(uuid.uuid4()) # must be a valid UUID ) except InvalidInputError as e: logger.warning("Malformed conversation_id: %s", e) # Generate a valid UUID and retry ``` ```javascript JavaScript theme={null} import { randomUUID } from 'node:crypto'; import { InvalidInputError, isSynapError } from '@maximem/synap-js-sdk'; try { const context = await sdk.conversation.context.fetch({ conversation_id: randomUUID(), // must be a valid UUID }); } catch (e) { if (!(e instanceof InvalidInputError)) throw e; console.warn('Malformed conversation_id: %s', e); // Generate a valid UUID and retry } ``` ```typescript TypeScript theme={null} import { randomUUID } from 'node:crypto'; import { InvalidInputError, isSynapError } from '@maximem/synap-js-sdk'; try { const context = await sdk.conversation.context.fetch({ conversation_id: randomUUID(), // must be a valid UUID }); } catch (e) { if (!(e instanceof InvalidInputError)) throw e; console.warn('Malformed conversation_id: %s', e); // Generate a valid UUID and retry } ``` ### AuthenticationError Raised when the SDK cannot authenticate with Synap Cloud. This is a general authentication failure. **When it occurs:** * API key is invalid or revoked * No API key was provided (neither `SYNAP_API_KEY` env var nor the `api_key=` constructor argument) * The instance's credentials have been rotated without updating the API key your application uses **How to handle:** ```python Python theme={null} from maximem_synap import AuthenticationError try: await sdk.initialize() except AuthenticationError as e: logger.error( "Authentication failed: %s (correlation_id=%s)", e, e.correlation_id ) # Re-bootstrap with a new token, or check SYNAP_API_KEY in the environment ``` ```javascript JavaScript theme={null} import { AuthenticationError, isSynapError } from '@maximem/synap-js-sdk'; try { await sdk.initialize(); } catch (e) { if (!(e instanceof AuthenticationError)) throw e; console.error('Authentication failed: %s (correlation_id=%s)', e, e.correlationId); // Re-bootstrap with a new token, or check SYNAP_API_KEY in the environment } ``` ```typescript TypeScript theme={null} import { AuthenticationError, isSynapError } from '@maximem/synap-js-sdk'; try { await sdk.initialize(); } catch (e) { if (!(e instanceof AuthenticationError)) throw e; console.error('Authentication failed: %s (correlation_id=%s)', e, e.correlationId); // Re-bootstrap with a new token, or check SYNAP_API_KEY in the environment } ``` ### ContextNotFoundError Raised when a requested context resource genuinely cannot be located, distinct from a valid resource that simply has no memories yet, which returns an **empty** `ContextResponse` rather than raising. **Empty result vs. raised error, which happens per method:** * `sdk.conversation.context.fetch()`: a brand-new or never-ingested `conversation_id` returns an **empty** `ContextResponse` (`facts == []`, `preferences == []`, etc.), **not** an error. This is the normal cold-start path. A malformed (non-UUID) `conversation_id` raises `InvalidInputError` instead. * `sdk.user.context.fetch()` / `sdk.customer.context.fetch()` / `sdk.client.context.fetch()`: a scope that has never had memories ingested also returns an **empty** `ContextResponse`. Treat empty lists as "no context yet," not as an error. `ContextNotFoundError` is reserved for the case where the underlying context resource itself is missing or was removed, not for the everyday "new conversation / new user" case. **When it occurs:** * A previously available context resource was deleted before retrieval * The backend cannot locate the addressed context resource (as opposed to locating it and finding it empty) **How to handle:** ```python Python theme={null} from maximem_synap import ContextNotFoundError try: context = await sdk.user.context.fetch(user_id="user_12345") except ContextNotFoundError: logger.info("Context resource missing for user, starting with empty state") context = None # Note: a user who simply has no memories yet does NOT raise here; the fetch # returns an empty ContextResponse. Check `not context.facts and not # context.preferences and ...` for the cold-start case rather than relying on # this exception. ``` ```javascript JavaScript theme={null} import { ContextNotFoundError } from '@maximem/synap-js-sdk'; let context = null; try { context = await sdk.user.context.fetch({ user_id: 'user_12345' }); } catch (e) { if (!(e instanceof ContextNotFoundError)) throw e; console.info('Context resource missing for user, starting with empty state'); context = null; } // Note: a user who simply has no memories yet does NOT raise here; the fetch // returns an empty response, and each collection may be absent entirely. // Check `!(context?.facts?.length) && !(context?.preferences?.length)` for the // cold-start case rather than relying on this exception. ``` ```typescript TypeScript theme={null} import { ContextNotFoundError, type RawContext } from '@maximem/synap-js-sdk'; let context: RawContext | null = null; try { context = await sdk.user.context.fetch({ user_id: 'user_12345' }); } catch (e) { if (!(e instanceof ContextNotFoundError)) throw e; console.info('Context resource missing for user, starting with empty state'); context = null; } // Note: a user who simply has no memories yet does NOT raise here; the fetch // returns an empty response, and each collection may be absent entirely. // Check `!(context?.facts?.length) && !(context?.preferences?.length)` for the // cold-start case rather than relying on this exception. ``` ### SessionExpiredError Raised when the current session has expired and cannot be resumed. Sessions are time-bounded and must be re-established after expiry. **When it occurs:** * The session has been idle beyond its expiry window * The session was invalidated server-side (e.g., credential rotation) **How to handle:** ```python Python theme={null} import uuid from maximem_synap import SessionExpiredError try: context = await sdk.conversation.context.fetch( conversation_id=str(uuid.uuid4()) ) except SessionExpiredError as e: logger.warning( "Session expired (correlation_id=%s). Re-initializing...", e.correlation_id ) await sdk.initialize() # Retry the operation ``` ```javascript JavaScript theme={null} import { randomUUID } from 'node:crypto'; import { SessionExpiredError, isSynapError } from '@maximem/synap-js-sdk'; try { const context = await sdk.conversation.context.fetch({ conversation_id: randomUUID(), }); } catch (e) { if (!(e instanceof SessionExpiredError)) throw e; console.warn('Session expired (correlation_id=%s). Re-initializing...', e.correlationId); await sdk.initialize(); // Retry the operation } ``` ```typescript TypeScript theme={null} import { randomUUID } from 'node:crypto'; import { SessionExpiredError, isSynapError } from '@maximem/synap-js-sdk'; try { const context = await sdk.conversation.context.fetch({ conversation_id: randomUUID(), }); } catch (e) { if (!(e instanceof SessionExpiredError)) throw e; console.warn('Session expired (correlation_id=%s). Re-initializing...', e.correlationId); await sdk.initialize(); // Retry the operation } ``` ### InsufficientCreditsError Raised when the client's credit balance is too low to satisfy the request. Carries `balance_credits`, `minimum_required_credits`, `recovery_url`, and `redeem_url` so you can surface the right next-step to the user. See [Pricing & Credits](/resources/pricing) for how credits and overage work. **When it occurs:** * The client has run out of credits * The current credit balance is below the minimum required for the requested operation **How to handle:** ```python Python theme={null} from maximem_synap import InsufficientCreditsError try: response = await sdk.memories.create( document=conversation_text, document_type="ai-chat-conversation", user_id=user_id, customer_id=customer_id, ) except InsufficientCreditsError as e: logger.error( "Insufficient credits: balance=%s, required=%s, recover=%s", e.balance_credits, e.minimum_required_credits, e.recovery_url ) # Surface the recovery / redeem URL to the operator or user ``` ```javascript JavaScript theme={null} import { InsufficientCreditsError, isSynapError } from '@maximem/synap-js-sdk'; try { const response = await sdk.memories.create({ document: conversation_text, document_type: 'ai-chat-conversation', user_id: user_id, customer_id: customer_id, }); } catch (e) { if (!(e instanceof InsufficientCreditsError)) throw e; console.error('Insufficient credits: balance=%s, required=%s, recover=%s', e.balanceCredits, e.requiredCredits, e.recoveryUrl); // Surface the recovery / redeem URL to the operator or user } ``` ```typescript TypeScript theme={null} import { InsufficientCreditsError, isSynapError } from '@maximem/synap-js-sdk'; try { const response = await sdk.memories.create({ document: conversation_text, document_type: 'ai-chat-conversation', user_id: user_id, customer_id: customer_id, }); } catch (e) { if (!(e instanceof InsufficientCreditsError)) throw e; console.error('Insufficient credits: balance=%s, required=%s, recover=%s', e.balanceCredits, e.requiredCredits, e.recoveryUrl); // Surface the recovery / redeem URL to the operator or user } ``` ### AgentUnavailableError Raised when the Synap agent backing the instance is temporarily unavailable. This is a transient error; the SDK will automatically retry. **When it occurs:** * The agent process is restarting or being redeployed * Temporary resource contention on the backend **How to handle:** ```python Python theme={null} from maximem_synap import AgentUnavailableError try: response = await sdk.memories.create( document=conversation_text, user_id=user_id, customer_id=customer_id ) except AgentUnavailableError as e: logger.warning( "Agent unavailable after all retries (correlation_id=%s)", e.correlation_id ) # Queue for retry later ``` ```javascript JavaScript theme={null} import { AgentUnavailableError, isSynapError } from '@maximem/synap-js-sdk'; try { const response = await sdk.memories.create({ document: conversation_text, user_id: user_id, customer_id: customer_id, }); } catch (e) { if (!(e instanceof AgentUnavailableError)) throw e; console.warn('Agent unavailable after all retries (correlation_id=%s)', e.correlationId); // Queue for retry later } ``` ```typescript TypeScript theme={null} import { AgentUnavailableError, isSynapError } from '@maximem/synap-js-sdk'; try { const response = await sdk.memories.create({ document: conversation_text, user_id: user_id, customer_id: customer_id, }); } catch (e) { if (!(e instanceof AgentUnavailableError)) throw e; console.warn('Agent unavailable after all retries (correlation_id=%s)', e.correlationId); // Queue for retry later } ``` The SDK automatically retries `AgentUnavailableError` according to your configured retry policy. This error only surfaces to your code when all retry attempts are exhausted. ### ListeningAlreadyActiveError Raised when you call `listen()` on an instance that already has an active listening stream. Only one stream can be active per SDK instance at a time. **When it occurs:** * Calling `listen()` a second time without first calling `stop_listening()` * Duplicate initialization paths in your application **How to handle:** ```python Python theme={null} from maximem_synap import ListeningAlreadyActiveError try: await sdk.instance.listen(on_context=on_event) except ListeningAlreadyActiveError: logger.warning("Listening stream already active; skipping duplicate call") # No action needed; the existing stream is still running ``` ```javascript JavaScript theme={null} import { ListeningAlreadyActiveError, isSynapError } from '@maximem/synap-js-sdk'; try { await sdk.instance.listen({ on_context: on_event, }); } catch (e) { if (!(e instanceof ListeningAlreadyActiveError)) throw e; console.warn('Listening stream already active; skipping duplicate call'); // No action needed; the existing stream is still running } ``` ```typescript TypeScript theme={null} import { ListeningAlreadyActiveError, isSynapError } from '@maximem/synap-js-sdk'; try { await sdk.instance.listen({ on_context: on_event, }); } catch (e) { if (!(e instanceof ListeningAlreadyActiveError)) throw e; console.warn('Listening stream already active; skipping duplicate call'); // No action needed; the existing stream is still running } ``` ### ListeningNotActiveError Raised by `send_message()` when no listening stream is currently active. **When it occurs:** * Calling `send_message()` before `listen()` has been called * Calling `send_message()` after `stop_listening()`, or while the stream is down and has not yet reconnected `stop_listening()` does **not** raise this. It is idempotent and a safe no-op when no stream is active, so it needs no guard. **How to handle:** ```python Python theme={null} from maximem_synap import ListeningNotActiveError try: await sdk.instance.send_message( content=text, conversation_id=conversation_id, user_id=user_id, customer_id=customer_id, ) except ListeningNotActiveError: # Anticipation is an optimization: losing a signal event is not fatal. logger.debug("synap_stream_down; skipping anticipation event") ``` ```javascript JavaScript theme={null} import { ListeningNotActiveError, isSynapError } from '@maximem/synap-js-sdk'; try { await sdk.instance.send_message({ content: text, conversation_id: conversation_id, user_id: user_id, customer_id: customer_id, }); } catch (e) { if (!(e instanceof ListeningNotActiveError)) throw e; // Anticipation is an optimization: losing a signal event is not fatal. console.debug('synap_stream_down; skipping anticipation event'); } ``` ```typescript TypeScript theme={null} import { ListeningNotActiveError, isSynapError } from '@maximem/synap-js-sdk'; try { await sdk.instance.send_message({ content: text, conversation_id: conversation_id, user_id: user_id, customer_id: customer_id, }); } catch (e) { if (!(e instanceof ListeningNotActiveError)) throw e; // Anticipation is an optimization: losing a signal event is not fatal. console.debug('synap_stream_down; skipping anticipation event'); } ``` In a long-lived server, guard with `if sdk.instance.is_listening:` rather than catching per call. That is what the framework integrations do: stream sends log and never raise. ## Stream health The Listen stream fails in a way that is deliberately invisible to your application, so it needs different handling from the errors above. | Condition | Behaviour | | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `listen()` fails at startup | **Python raises** (`AuthenticationError`, `SDKNotInitializedError`). The **TypeScript** SDK logs a console warning and falls back to HTTP. | | Stream drops mid-run | `on_disconnect(reason)` fires; the SDK reconnects with exponential backoff (10 attempts, counter resets on each success). | | Server closes at max lifetime (1 hour) | Routine. `on_reconnect(attempt)` fires. Not an error. | | Per-instance/client quota exceeded | `RESOURCE_EXHAUSTED`, then a reconnect loop. Usually means one stream per session instead of one per process. | | Reconnects exhausted | Stream stays down. **`fetch()` keeps working over REST**, and no exception ever reaches your code. | A permanently dead stream is silent: retrieval still returns correct results, just slower. Do not infer stream health from your agent working. Export `sdk.instance.is_listening` to your health endpoint and alert when it stays false. See [Real-Time Anticipation in a Server](/patterns/real-time-anticipation-server). ## Using correlation\_id Every Synap error includes an optional `correlation_id` that uniquely identifies the failed request within Synap's distributed tracing system. ```python Python theme={null} import uuid from maximem_synap import SynapError try: context = await sdk.conversation.context.fetch( conversation_id=str(uuid.uuid4()) ) except SynapError as e: logger.error( "Synap error: %s | correlation_id: %s | type: %s", str(e), e.correlation_id, type(e).__name__ ) ``` ```javascript JavaScript theme={null} import { randomUUID } from 'node:crypto'; import { SynapError, isSynapError } from '@maximem/synap-js-sdk'; try { const context = await sdk.conversation.context.fetch({ conversation_id: randomUUID(), }); } catch (e) { if (!(e instanceof SynapError)) throw e; console.error( 'Synap error: %s | correlation_id: %s | type: %s', String(e), e.correlationId, e.constructor.name, ); } ``` ```typescript TypeScript theme={null} import { randomUUID } from 'node:crypto'; import { SynapError, isSynapError } from '@maximem/synap-js-sdk'; try { const context = await sdk.conversation.context.fetch({ conversation_id: randomUUID(), }); } catch (e) { if (!(e instanceof SynapError)) throw e; console.error( 'Synap error: %s | correlation_id: %s | type: %s', String(e), e.correlationId, e.constructor.name, ); } ``` Always log the `correlation_id` from errors. Synap support can use it to trace the exact request path through the backend, including which pipeline stages were involved, what data was processed, and where the failure occurred. ## Retry Policy Configuration The SDK's built-in retry policy handles transient errors automatically. You can customize the retry behavior through `RetryPolicy` in your `SDKConfig`. ```python Python theme={null} from maximem_synap import SDKConfig, RetryPolicy config = SDKConfig( retry_policy=RetryPolicy( max_attempts=5, # Total attempts (1 initial + 4 retries) backoff_base=1.0, # Base delay in seconds backoff_max=10.0, # Maximum delay between retries backoff_jitter=True, # Add random jitter to prevent thundering herd retryable_errors=[ # Error types to retry "NetworkTimeoutError", "RateLimitError", "ServiceUnavailableError", "SynapTransientError" ] ) ) ``` ```javascript JavaScript theme={null} // No SDKConfig wrapper: the policy is an option on the client. const sdk = new SynapClient({ retryPolicy: { maxAttempts: 5, // Total attempts (1 initial + 4 retries) backoffBase: 1.0, // Base delay in seconds backoffMax: 10.0, // Maximum delay between retries backoffJitter: true, // Add random jitter to prevent thundering herd }, }); ``` ```typescript TypeScript theme={null} import { SynapClient, type RetryPolicy } from '@maximem/synap-js-sdk'; const retryPolicy: RetryPolicy = { maxAttempts: 5, // Total attempts (1 initial + 4 retries) backoffBase: 1.0, // Base delay in seconds backoffMax: 10.0, // Maximum delay between retries backoffJitter: true, // Add random jitter to prevent thundering herd }; const sdk = new SynapClient({ retryPolicy }); ``` **JavaScript decides this differently, on purpose.** There is no `retryable_errors` list: every transient error is retried, and a non-idempotent call whose outcome is unknown is not, whatever its type. That second rule is what stops a lost response on `memories.create` from ingesting and billing the same content twice, and a per-error list could switch it off. To handle rate limits yourself, catch `RateLimitError` at the call site. ### Retry Behavior The SDK uses exponential backoff with optional jitter: ``` Attempt 1: Immediate Attempt 2: backoff_base * 2^0 = 1.0s (± jitter) Attempt 3: backoff_base * 2^1 = 2.0s (± jitter) Attempt 4: backoff_base * 2^2 = 4.0s (± jitter) Attempt 5: backoff_base * 2^3 = 8.0s (± jitter, capped at backoff_max) ``` For `RateLimitError`, the SDK respects the `retry_after_seconds` value instead of the exponential backoff, waiting the exact duration specified by the server. ### Disabling Retries To disable automatic retries entirely (useful for testing or when you implement your own retry logic): ```python Python theme={null} config = SDKConfig(retry_policy=None) ``` ```javascript JavaScript theme={null} const sdk = new SynapClient({ retryPolicy: null }); ``` ```typescript TypeScript theme={null} const sdk = new SynapClient({ retryPolicy: null }); ``` ### Customizing Retryable Errors By default, all four transient error types are retried (`NetworkTimeoutError`, `RateLimitError`, `ServiceUnavailableError`, and `AgentUnavailableError`). Listing the base `SynapTransientError` in `retryable_errors` covers every transient subtype, including any added in future SDK releases. You can customize this list, though adding permanent errors is generally not recommended. ```python theme={null} config = SDKConfig( retry_policy=RetryPolicy( retryable_errors=[ "NetworkTimeoutError", "ServiceUnavailableError" # Removed RateLimitError, handle rate limits manually ] ) ) ``` **Python only, deliberately.** JavaScript retries every transient error and never a non-idempotent call whose outcome is unknown. Narrowing that by error type could re-enable a retry that ingests and bills the same content twice, so the list is not offered. Catch `RateLimitError` at the call site instead. ## Common Error Handling Patterns ### Catch-All with Transient/Permanent Distinction ```python Python theme={null} from maximem_synap import ( SynapError, SynapTransientError, SynapPermanentError, ) try: response = await sdk.memories.create( document=conversation_text, document_type="ai-chat-conversation", user_id=user_id, customer_id=customer_id, mode="long-range", ) except SynapTransientError as e: # All retries exhausted for a transient error logger.warning( "Transient error after retries: %s (correlation_id=%s)", e, e.correlation_id ) # Queue for retry later, or degrade gracefully await retry_queue.enqueue(conversation_text, user_id) except SynapPermanentError as e: # Something is fundamentally wrong logger.error( "Permanent error: %s (correlation_id=%s)", e, e.correlation_id ) # Alert, fix the issue, do not retry as-is raise except SynapError as e: # Catch-all for any unexpected Synap error logger.error("Unexpected Synap error: %s", e) raise ``` ```javascript JavaScript theme={null} import { SynapError, TransientError, PermanentError, } from '@maximem/synap-js-sdk'; try { const response = await sdk.memories.create({ document: conversationText, document_type: 'ai-chat-conversation', user_id: userId, customer_id: customerId, mode: 'long-range', }); } catch (e) { // Python has one `except` per class; JavaScript has one `catch`, so the // branches move inside it. Order matters the same way: most specific first. if (e instanceof TransientError) { // All retries exhausted for a transient error console.warn( 'Transient error after retries: %s (correlation_id=%s)', e, e.correlationId, ); // Queue for retry later, or degrade gracefully await retryQueue.enqueue(conversationText, userId); } else if (e instanceof PermanentError) { // Something is fundamentally wrong console.error('Permanent error: %s (correlation_id=%s)', e, e.correlationId); // Alert, fix the issue, do not retry as-is throw e; } else if (e instanceof SynapError) { // Catch-all for any unexpected Synap error console.error('Unexpected Synap error: %s', e); throw e; } else { // Not ours: never swallow a bug in your own code as "Synap was down". throw e; } } ``` ```typescript TypeScript theme={null} import { SynapError, TransientError, PermanentError, } from '@maximem/synap-js-sdk'; try { const response = await sdk.memories.create({ document: conversationText, document_type: 'ai-chat-conversation', user_id: userId, customer_id: customerId, mode: 'long-range', }); } catch (e) { // Python has one `except` per class; JavaScript has one `catch`, so the // branches move inside it. Order matters the same way: most specific first. if (e instanceof TransientError) { // All retries exhausted for a transient error console.warn( 'Transient error after retries: %s (correlation_id=%s)', e, e.correlationId, ); // Queue for retry later, or degrade gracefully await retryQueue.enqueue(conversationText, userId); } else if (e instanceof PermanentError) { // Something is fundamentally wrong console.error('Permanent error: %s (correlation_id=%s)', e, e.correlationId); // Alert, fix the issue, do not retry as-is throw e; } else if (e instanceof SynapError) { // Catch-all for any unexpected Synap error console.error('Unexpected Synap error: %s', e); throw e; } else { // Not ours: never swallow a bug in your own code as "Synap was down". throw e; } } ``` ### Per-Operation Error Handling ```python Python theme={null} from maximem_synap import ( InvalidInputError, NetworkTimeoutError, RateLimitError, SynapError, ) async def safe_fetch_context(conversation_id: str, query: str): """Fetch context with graceful degradation.""" try: return await sdk.conversation.context.fetch( conversation_id=conversation_id, search_query=[query], mode="fast" ) except InvalidInputError: logger.info("Malformed conversation_id %s, skipping memory", conversation_id) return None except NetworkTimeoutError: logger.warning("Timeout fetching context, proceeding without memory") return None except RateLimitError as e: logger.warning( "Rate limited, retry after %ds", e.retry_after_seconds ) return None except SynapError as e: logger.error( "Unexpected error fetching context: %s (correlation_id=%s)", e, e.correlation_id ) return None ``` ```javascript JavaScript theme={null} import { InvalidInputError, NetworkTimeoutError, RateLimitError, SynapError, } from '@maximem/synap-js-sdk'; async function safeFetchContext(conversationId, query) { // Fetch context with graceful degradation. try { return await sdk.conversation.context.fetch({ conversation_id: conversationId, search_query: [query], mode: 'fast', }); } catch (e) { if (e instanceof InvalidInputError) { console.info('Malformed conversation_id %s, skipping memory', conversationId); return null; } if (e instanceof NetworkTimeoutError) { console.warn('Timeout fetching context, proceeding without memory'); return null; } if (e instanceof RateLimitError) { console.warn('Rate limited, retry after %ds', e.retryAfterSeconds); return null; } if (e instanceof SynapError) { console.error( 'Unexpected error fetching context: %s (correlation_id=%s)', e, e.correlationId, ); return null; } throw e; // not ours } } ``` ```typescript TypeScript theme={null} import { InvalidInputError, NetworkTimeoutError, RateLimitError, SynapError, type RawContext, } from '@maximem/synap-js-sdk'; async function safeFetchContext( conversationId: string, query: string, ): Promise { // Fetch context with graceful degradation. try { return await sdk.conversation.context.fetch({ conversation_id: conversationId, search_query: [query], mode: 'fast', }); } catch (e) { if (e instanceof InvalidInputError) { console.info('Malformed conversation_id %s, skipping memory', conversationId); return null; } if (e instanceof NetworkTimeoutError) { console.warn('Timeout fetching context, proceeding without memory'); return null; } if (e instanceof RateLimitError) { console.warn('Rate limited, retry after %ds', e.retryAfterSeconds); return null; } if (e instanceof SynapError) { console.error( 'Unexpected error fetching context: %s (correlation_id=%s)', e, e.correlationId, ); return null; } throw e; // not ours } } ``` ### Initialization Error Handling ```python Python theme={null} from maximem_synap import ( AuthenticationError, InvalidInputError, NetworkTimeoutError, SynapError, ) async def initialize_with_fallback(): """Initialize SDK with comprehensive error handling.""" try: await sdk.initialize() return True except InvalidInputError: logger.error("Invalid instance ID. Check your configuration.") return False except AuthenticationError as e: logger.error( "Auth failed (no API key or invalid credentials): %s (correlation_id=%s)", e, e.correlation_id ) return False except NetworkTimeoutError: logger.error("Cannot reach Synap Cloud. Check network connectivity.") return False except SynapError as e: logger.error( "Unexpected initialization error: %s (correlation_id=%s)", e, e.correlation_id ) return False ``` ```javascript JavaScript theme={null} import { AuthenticationError, InvalidInputError, NetworkTimeoutError, SynapError, } from '@maximem/synap-js-sdk'; async function initializeWithFallback() { // Initialize SDK with comprehensive error handling. try { await sdk.initialize(); return true; } catch (e) { if (e instanceof InvalidInputError) { console.error('Invalid instance ID. Check your configuration.'); return false; } if (e instanceof AuthenticationError) { console.error( 'Auth failed (no API key or invalid credentials): %s (correlation_id=%s)', e, e.correlationId, ); return false; } if (e instanceof NetworkTimeoutError) { console.error('Cannot reach Synap Cloud. Check network connectivity.'); return false; } if (e instanceof SynapError) { console.error( 'Unexpected initialization error: %s (correlation_id=%s)', e, e.correlationId, ); return false; } throw e; // not ours } } ``` ```typescript TypeScript theme={null} import { AuthenticationError, InvalidInputError, NetworkTimeoutError, SynapError, } from '@maximem/synap-js-sdk'; async function initializeWithFallback(): Promise { // Initialize SDK with comprehensive error handling. try { await sdk.initialize(); return true; } catch (e) { if (e instanceof InvalidInputError) { console.error('Invalid instance ID. Check your configuration.'); return false; } if (e instanceof AuthenticationError) { console.error( 'Auth failed (no API key or invalid credentials): %s (correlation_id=%s)', e, e.correlationId, ); return false; } if (e instanceof NetworkTimeoutError) { console.error('Cannot reach Synap Cloud. Check network connectivity.'); return false; } if (e instanceof SynapError) { console.error( 'Unexpected initialization error: %s (correlation_id=%s)', e, e.correlationId, ); return false; } throw e; // not ours } } ``` ## Full error reference The hierarchy and handling patterns above cover how to catch and respond to each error. For the at-a-glance catalog (every SDK exception class with its transient/permanent type and common cause) see [SDK Reference: Error handling](/sdk-reference/overview#error-handling). For the server-side wire codes those exceptions wrap (HTTP status, machine-readable `code`, and `details` shape) see [Error Codes](/sdk-reference/errors). ## Next Steps Customize retry policies, timeouts, and other SDK settings. Set up the SDK with proper error handling from the start. Contact Synap support with correlation IDs for issue resolution. Common questions about errors and troubleshooting. ## JavaScript: typed error handling Error classes are real classes, so `instanceof` narrowing works, and each carries a stable `.code` and a `.transient` flag: ```ts theme={null} import { RateLimitError, AuthenticationError, InsufficientCreditsError, isSynapError, } from "@maximem/synap-js-sdk"; try { await synap.user.context.fetch({ user_id, customer_id }); } catch (error) { if (!isSynapError(error)) throw error; if (error instanceof RateLimitError) { console.warn(`Retry after ${error.retryAfterSeconds}s`); } else if (error instanceof AuthenticationError) { console.error("Check SYNAP_API_KEY"); } else if (error instanceof InsufficientCreditsError) { console.error( `Short ${error.requiredCredits} credits (balance ${error.balanceCredits}). ` + `Top up: ${error.recoveryUrl} Redeem: ${error.redeemUrl}`, ); } } ``` Transient errors are retried automatically. Ingestion is deliberately **not** retried when a failure leaves the outcome unknown, because a retry would store and bill twice.
# Ingestion Source: https://docs.maximem.ai/sdk/ingestion Send conversations, documents, and other data into Synap. ## Overview Ingestion is how you feed data into Synap's memory system. Every conversation, document, email, or transcript you send through `sdk.memories.create()` enters the ingestion pipeline where it is categorized, chunked, entities are extracted and resolved, and the result is persisted across Synap's vector and graph storage engines. Ingestion is **asynchronous**. When you call `create()`, Synap immediately returns an `ingestion_id` that you can use to poll the processing status. This design allows high-throughput workloads without blocking your application. ## Creating a Memory Use `sdk.memories.create()` to send a single document into the ingestion pipeline. ```python Python theme={null} from datetime import datetime response = await sdk.memories.create( document="User: What's the status of Project Atlas?\nAssistant: Project Atlas is on track for Q2 launch...", document_type="ai-chat-conversation", user_id="user_12345", customer_id="cust_67890", mode="long-range", metadata={"session_id": "sess_abc", "agent_version": "2.1.0"} ) print(f"Ingestion ID: {response.ingestion_id}") print(f"Status: {response.status}") ``` ```javascript JavaScript theme={null} const response = await sdk.memories.create({ document: "User: What's the status of Project Atlas?\nAssistant: Project Atlas is on track for Q2 launch...", document_type: 'ai-chat-conversation', user_id: 'user_12345', customer_id: 'cust_67890', mode: 'long-range', metadata: {'session_id': 'sess_abc', 'agent_version': '2.1.0'}, }); console.log(`Ingestion ID: ${response.ingestion_id}`); console.log(`Status: ${response.status}`); ``` ```typescript TypeScript theme={null} const response = await sdk.memories.create({ document: "User: What's the status of Project Atlas?\nAssistant: Project Atlas is on track for Q2 launch...", document_type: 'ai-chat-conversation', user_id: 'user_12345', customer_id: 'cust_67890', mode: 'long-range', metadata: {'session_id': 'sess_abc', 'agent_version': '2.1.0'}, }); console.log(`Ingestion ID: ${response.ingestion_id}`); console.log(`Status: ${response.status}`); ``` **Key parameters for the example above:** * `document`: the raw content to ingest (full transcript with speaker labels for conversations). * `document_type`: tells the pipeline which extraction and chunking strategy to apply. See [Document Types](#document-types) below. * `mode`: the depth of extraction. See [Ingest Modes](#ingest-modes) below. * `user_id` / `customer_id`: control the memory scope. The effective scope is derived from the combination you pass plus the instance's B2C vs B2B configuration; see [Memory Scopes](/concepts/memory-scopes). * `document_id`: an optional idempotency key. Resubmitting the same `document_id` updates the existing memory instead of creating a duplicate. The call returns immediately with an `ingestion_id` (status `"queued"`) that you poll via `sdk.memories.status()`. Every parameter, the complete response shape, and the errors raised by `memories.create()`. ## Document Types The `document_type` parameter tells the ingestion pipeline which extraction and chunking strategies to apply. | Document Type | Description | Optimized For | | ---------------------- | ------------------------------------- | --------------------------------------------------------- | | `ai-chat-conversation` | Multi-turn AI assistant conversations | Speaker turns, intent extraction, preference detection | | `document` | General text documents | Paragraph chunking, topic extraction | | `email` | Email messages and threads | Sender/recipient extraction, action items, thread context | | `pdf` | PDF document content (text extracted) | Section-aware chunking, header/footer handling | | `image` | Image descriptions or OCR text | Entity extraction from visual content descriptions | | `audio` | Audio transcriptions | Speaker diarization awareness, temporal markers | | `meeting-transcript` | Meeting transcription content | Multi-speaker extraction, action items, decisions | For `image` and `audio` types, you provide the **text content** (description, transcript, or OCR output), not the raw binary file. Media processing and transcription should be handled upstream of Synap. ## Ingest Modes Synap offers two ingestion modes that trade off processing depth against throughput. **Optimized for speed.** Performs basic chunking, lightweight entity extraction, and vector embedding. Skips deep relationship mapping and advanced categorization. * Lower processing latency than `long-range` * Best for: high-throughput pipelines, real-time chat logging, non-critical data **Optimized for quality.** Runs the full extraction pipeline including deep entity resolution, relationship mapping, preference detection, emotional analysis, and graph storage. * Higher processing latency than `fast`, in exchange for deeper extraction * Best for: conversations, documents where deep extraction matters, building long-term user profiles Use `long-range` mode for conversations and documents where deep extraction matters. Use `fast` for high-throughput scenarios where speed is critical. You can always re-ingest a document in `long-range` mode later by resubmitting with the same `document_id`. Ingestion `mode` values (`fast` / `long-range`) are distinct from retrieval `mode` values (`fast` / `accurate`). They control different stages of the pipeline and are not interchangeable; passing `"accurate"` to `memories.create()` or `"long-range"` to `context.fetch()` will be rejected. ## Code Examples ### Ingesting a Conversation ```python Python theme={null} response = await sdk.memories.create( document="""User: I'm planning a trip to Japan in April. Assistant: Great choice! April is cherry blossom season in Japan. Would you like recommendations for Tokyo or Kyoto? User: Both! I prefer boutique hotels over large chains, and I'm vegetarian. Assistant: I'll keep your preference for boutique hotels and vegetarian dining in mind...""", document_type="ai-chat-conversation", user_id="user_12345", customer_id="cust_67890", mode="long-range" ) ``` ```javascript JavaScript theme={null} const response = await sdk.memories.create({ document: `User: I'm planning a trip to Japan in April. Assistant: Great choice! April is cherry blossom season in Japan. Would you like recommendations for Tokyo or Kyoto? User: Both! I prefer boutique hotels over large chains, and I'm vegetarian. Assistant: I'll keep your preference for boutique hotels and vegetarian dining in mind...`, document_type: 'ai-chat-conversation', user_id: 'user_12345', customer_id: 'cust_67890', mode: 'long-range', }); ``` ```typescript TypeScript theme={null} const response = await sdk.memories.create({ document: `User: I'm planning a trip to Japan in April. Assistant: Great choice! April is cherry blossom season in Japan. Would you like recommendations for Tokyo or Kyoto? User: Both! I prefer boutique hotels over large chains, and I'm vegetarian. Assistant: I'll keep your preference for boutique hotels and vegetarian dining in mind...`, document_type: 'ai-chat-conversation', user_id: 'user_12345', customer_id: 'cust_67890', mode: 'long-range', }); ``` ### Ingesting a Document ```python Python theme={null} response = await sdk.memories.create( document="Q3 2025 Engineering OKRs\n\n1. Ship Synap SDK v2.0...", document_type="document", user_id="user_12345", customer_id="cust_67890", document_created_at=datetime(2025, 7, 1), metadata={"source": "confluence", "page_id": "12345"} ) ``` ```javascript JavaScript theme={null} const response = await sdk.memories.create({ document: "Q3 2025 Engineering OKRs\n\n1. Ship Synap SDK v2.0...", document_type: 'document', user_id: 'user_12345', customer_id: 'cust_67890', document_created_at: datetime(2025, 7, 1), metadata: {'source': 'confluence', 'page_id': '12345'}, }); ``` ```typescript TypeScript theme={null} const response = await sdk.memories.create({ document: "Q3 2025 Engineering OKRs\n\n1. Ship Synap SDK v2.0...", document_type: 'document', user_id: 'user_12345', customer_id: 'cust_67890', document_created_at: datetime(2025, 7, 1), metadata: {'source': 'confluence', 'page_id': '12345'}, }); ``` ### Ingesting with User and Customer Scoping When both `user_id` and `customer_id` are provided, the memory is accessible at both scopes. This is useful when a user's conversation may contain information relevant to the broader organization. ```python Python theme={null} response = await sdk.memories.create( document="User: Our team decided to switch from Jira to Linear...", document_type="ai-chat-conversation", user_id="user_alice", customer_id="cust_acme_corp", mode="long-range" ) # This memory is now retrievable via: # - sdk.user.context.fetch() for user_alice # - sdk.customer.context.fetch() for cust_acme_corp # - sdk.conversation.context.fetch() for the conversation ``` ```javascript JavaScript theme={null} const response = await sdk.memories.create({ document: 'User: Our team decided to switch from Jira to Linear...', document_type: 'ai-chat-conversation', user_id: 'user_alice', customer_id: 'cust_acme_corp', mode: 'long-range', }); // This memory is now retrievable via: // - sdk.user.context.fetch() for user_alice // - sdk.customer.context.fetch() for cust_acme_corp // - sdk.conversation.context.fetch() for the conversation ``` ```typescript TypeScript theme={null} const response = await sdk.memories.create({ document: 'User: Our team decided to switch from Jira to Linear...', document_type: 'ai-chat-conversation', user_id: 'user_alice', customer_id: 'cust_acme_corp', mode: 'long-range', }); // This memory is now retrievable via: // - sdk.user.context.fetch() for user_alice // - sdk.customer.context.fetch() for cust_acme_corp // - sdk.conversation.context.fetch() for the conversation ``` ## Batch Ingestion For bulk workloads, use `sdk.memories.batch_create()` to submit multiple documents in a single request. ```python Python theme={null} from maximem_synap import CreateMemoryRequest documents = [ CreateMemoryRequest( document="User: Book me a flight to NYC next Tuesday...", document_type="ai-chat-conversation", user_id="user_12345", mode="long-range" ), CreateMemoryRequest( document="Meeting notes from sprint planning...", document_type="meeting-transcript", customer_id="cust_67890", mode="fast" ), CreateMemoryRequest( document="Support ticket: Login issues after password reset...", document_type="document", user_id="user_67890", customer_id="cust_67890", mode="long-range" ), ] batch_response = await sdk.memories.batch_create( documents=documents, fail_fast=False ) print(f"Submitted: {batch_response.total}") print(f"Succeeded: {batch_response.succeeded}") print(f"Failed: {batch_response.failed}") for result in batch_response.results: print(f" {result.ingestion_id}: {result.status}") ``` ```javascript JavaScript theme={null} let documents = [ { document: 'User: Book me a flight to NYC next Tuesday...', document_type: 'ai-chat-conversation', user_id: 'user_12345', mode: 'long-range' }, { document: 'Meeting notes from sprint planning...', document_type: 'meeting-transcript', customer_id: 'cust_67890', mode: 'fast' }, { document: 'Support ticket: Login issues after password reset...', document_type: 'document', user_id: 'user_67890', customer_id: 'cust_67890', mode: 'long-range' }, ]; const batch_response = await sdk.memories.batch_create({ documents: documents, fail_fast: false, }); console.log(`Submitted: ${batch_response.total}`); console.log(`Succeeded: ${batch_response.succeeded}`); console.log(`Failed: ${batch_response.failed}`); for (const result of batch_response.results ?? []) { console.log(` ${result.ingestion_id}: ${result.status}`); } ``` ```typescript TypeScript theme={null} let documents = [ { document: 'User: Book me a flight to NYC next Tuesday...', document_type: 'ai-chat-conversation', user_id: 'user_12345', mode: 'long-range' }, { document: 'Meeting notes from sprint planning...', document_type: 'meeting-transcript', customer_id: 'cust_67890', mode: 'fast' }, { document: 'Support ticket: Login issues after password reset...', document_type: 'document', user_id: 'user_67890', customer_id: 'cust_67890', mode: 'long-range' }, ]; const batch_response = await sdk.memories.batch_create({ documents: documents, fail_fast: false, }); console.log(`Submitted: ${batch_response.total}`); console.log(`Succeeded: ${batch_response.succeeded}`); console.log(`Failed: ${batch_response.failed}`); for (const result of batch_response.results ?? []) { console.log(` ${result.ingestion_id}: ${result.status}`); } ``` ### The `fail_fast` Option `batch_create()` takes a `fail_fast` flag (default `False`). With `False`, all documents are processed and invalid ones are rejected individually while valid ones proceed; with `True`, the entire batch aborts if any document fails validation and no documents are ingested. The `CreateMemoryRequest` fields, `fail_fast` semantics, and the `BatchCreateResponse` shape. ## Recording Conversation Messages `sdk.memories.create()` ingests a fully-formed document. For live agents that need to stream messages turn-by-turn (so compaction and `get_context_for_prompt()` always see the latest history), use `sdk.conversation.record_message()` instead. ```python Python theme={null} await sdk.conversation.record_message( conversation_id="b85f1c2a-9d3e-4f0a-8b6c-1a2b3c4d5e6f", # must be a valid UUID role="user", # "user" or "assistant" content="I'd like to upgrade my plan.", user_id="user_12345", customer_id="cust_67890", ) ``` ```typescript TypeScript theme={null} await sdk.conversation.record_message(options: RecordMessageOptions) ``` A few things to get right for the example above: * `conversation_id` must be a valid UUID string (non-UUID values are rejected) and the same value should be reused across all turns of a single conversation. * `role` must be either `"user"` or `"assistant"`. * `content` is subject to the per-message size limit listed in [Performance & Limits](/resources/performance-limits). The call returns a dict with `message_id`, `conversation_id`, `session_id`, and `recorded_at`. Every parameter, scope rules for `customer_id`, the response shape, and the errors raised. ### Recording Messages in Batch For backfills or buffered writes, `record_messages_batch` accepts a list of message dicts: ```python Python theme={null} import uuid # conversation_id must be a valid UUID string; reuse it across all turns # of the same conversation. conv_id = str(uuid.uuid4()) await sdk.conversation.record_messages_batch( messages=[ { "conversation_id": conv_id, "role": "user", "content": "Hello!", "user_id": "user_12345", "customer_id": "cust_67890", }, { "conversation_id": conv_id, "role": "assistant", "content": "Hi! How can I help?", "user_id": "user_12345", "customer_id": "cust_67890", }, ] ) ``` ```javascript JavaScript theme={null} import { randomUUID } from 'node:crypto'; // conversation_id must be a valid UUID string; reuse it across all turns // of the same conversation. const conv_id = randomUUID(); await sdk.conversation.record_messages_batch([ { 'conversation_id': conv_id, 'role': 'user', 'content': 'Hello!', 'user_id': 'user_12345', 'customer_id': 'cust_67890', }, { 'conversation_id': conv_id, 'role': 'assistant', 'content': 'Hi! How can I help?', 'user_id': 'user_12345', 'customer_id': 'cust_67890', }, ]); ``` ```typescript TypeScript theme={null} import { randomUUID } from 'node:crypto'; // conversation_id must be a valid UUID string; reuse it across all turns // of the same conversation. const conv_id = randomUUID(); await sdk.conversation.record_messages_batch([ { 'conversation_id': conv_id, 'role': 'user', 'content': 'Hello!', 'user_id': 'user_12345', 'customer_id': 'cust_67890', }, { 'conversation_id': conv_id, 'role': 'assistant', 'content': 'Hi! How can I help?', 'user_id': 'user_12345', 'customer_id': 'cust_67890', }, ]); ``` Returns a dict with `total`, `succeeded`, `failed`, and a per-message `results[]` list. Recording messages is **separate from** ingesting a memory via `memories.create()`. Recorded messages are the input to compaction (`compact()` and `get_context_for_prompt()` need them). Memories created via `memories.create()` are the long-term, extracted, scope-aware knowledge layer. Most production agents do both: stream every turn via `record_message()` for live compaction, and periodically ingest the full conversation via `memories.create()` for long-term recall. ### When one call is enough (cost & dedup) "Do both" is the common pattern, not a requirement: the two calls serve different layers and carry different cost. Both consume credits; see [Pricing & Credits](/resources/pricing) for the model and `sdk.credits.estimate()` to size an operation before you run it. * **`record_message()` is lightweight.** It appends a turn to the conversation buffer that feeds compaction; it does **not** run the full extraction pipeline per call. Streaming every turn is the intended usage. * **`memories.create()` is the heavier call.** It runs chunking, entity extraction/resolution, and vector + graph persistence. Calling it on **every** turn, rather than periodically on a fuller transcript, is the main source of avoidable double-write cost. Pick based on what each conversation actually needs: * **Live compaction only** (you only need a prompt-ready rolling summary, no long-term cross-conversation recall): `record_message()` alone is enough. Skip `memories.create()`. * **Long-term recall only** (batch transcripts, documents, backfills where you never call compaction): `memories.create()` alone is enough. Skip `record_message()`. * **Both** (live agents that also need durable, scope-aware memory): stream turns with `record_message()`, then ingest the **assembled** conversation with `memories.create()` periodically (e.g. at session end or every N turns) rather than per turn. To keep the long-term layer from accumulating duplicates when you re-ingest a growing transcript, pass a stable `document_id` so repeat submissions **update** the existing memory instead of creating a new one. Extraction-level dedup (deduplicating overlapping facts/preferences across submissions) is handled by the ingestion pipeline and by `smart-merge` on `update()`; the `document_id` idempotency key is what prevents whole-document duplicates. ## One-Shot Transcript Ingest For **async integrations** (where Synap does nothing during a session and everything happens at the end) `conversation.ingest_transcript()` collapses "stream turns + assemble + ingest + summarize" into a single call. It records the whole transcript, enqueues extraction, and fires a summary compaction, then returns immediately. Nothing sits on a hot path. ```python Python theme={null} from maximem_synap import TranscriptTurn resp = await sdk.conversation.ingest_transcript( conversation_id="call_01H8XZ", # any client string — NOT validated as a UUID user_id="+919812345678", conversation_type="voice", transcript=[ TranscriptTurn(role="assistant", content="Hi, is this a good time?"), TranscriptTurn(role="user", content="Yes — looking for a 3 BHK in Baner."), ], analysis={"disposition": "interested"}, # stored + used as extraction hints ) await sdk.memories.wait_for_completion(resp.ingestion_id) ``` ```javascript JavaScript theme={null} import { TranscriptTurn } from '@maximem/synap-js-sdk'; const resp = await sdk.conversation.ingest_transcript({ conversation_id: 'call_01H8XZ', // any client string — NOT validated as a UUID user_id: '+919812345678', conversation_type: 'voice', transcript: [ { role: 'assistant', content: 'Hi, is this a good time?' }, { role: 'user', content: 'Yes — looking for a 3 BHK in Baner.' }, ], analysis: {'disposition': 'interested'}, // stored + used as extraction hints }); await sdk.memories.wait_for_completion(resp.ingestion_id); ``` ```typescript TypeScript theme={null} import { TranscriptTurn } from '@maximem/synap-js-sdk'; const resp = await sdk.conversation.ingest_transcript({ conversation_id: 'call_01H8XZ', // any client string — NOT validated as a UUID user_id: '+919812345678', conversation_type: 'voice', transcript: [ { role: 'assistant', content: 'Hi, is this a good time?' }, { role: 'user', content: 'Yes — looking for a 3 BHK in Baner.' }, ], analysis: {'disposition': 'interested'}, // stored + used as extraction hints }); await sdk.memories.wait_for_completion(resp.ingestion_id); ``` Key differences from `record_message` + `memories.create`: * **`conversation_id` is an arbitrary string**: no UUID validation. The server coerces it and echoes the original as `external_conversation_id`. * **Idempotent.** Re-pushing an identical transcript returns `status="duplicate"` with the original `ingestion_id`; a *different* transcript under the same id raises `TranscriptConflictError` (a call's transcript is immutable). * **`analysis` does double duty**: stored verbatim (surfaced at call-start fetch and in the dashboard) and injected into extraction as high-confidence hints. Pair it with a call-start [`sdk.fetch(context_mode="conversation-summary")`](/sdk/context-fetch#conversation-summary-mode-conversation-start). Full parameter reference: [conversation.ingest\_transcript](/sdk-reference/conversation/ingest-transcript). ## Checking Ingestion Status Ingestion is asynchronous. Use `sdk.memories.status()` to poll the processing state of a submitted document. ```python Python theme={null} status = await sdk.memories.status(ingestion_id=response.ingestion_id) print(f"Status: {status.status}") print(f"Queued at: {status.queued_at}") print(f"Started at: {status.started_at}") print(f"Completed at: {status.completed_at}") if status.status == "completed": print(f"Memory IDs: {status.memory_ids}") print(f"Memories created: {status.memories_created}") elif status.status == "failed": print(f"Error: {status.error_message}") ``` ```javascript JavaScript theme={null} const status = await sdk.memories.status(response.ingestion_id); console.log(`Status: ${status.status}`); console.log(`Queued at: ${status.queued_at}`); console.log(`Started at: ${status.started_at}`); console.log(`Completed at: ${status.completed_at}`); if (status.status == 'completed') { console.log(`Memory IDs: ${status.memory_ids}`); console.log(`Memories created: ${status.memories_created}`); } else if (status.status == 'failed') { console.log(`Error: ${status.error_message}`); } ``` ```typescript TypeScript theme={null} const status = await sdk.memories.status(response.ingestion_id); console.log(`Status: ${status.status}`); console.log(`Queued at: ${status.queued_at}`); console.log(`Started at: ${status.started_at}`); console.log(`Completed at: ${status.completed_at}`); if (status.status == 'completed') { console.log(`Memory IDs: ${status.memory_ids}`); console.log(`Memories created: ${status.memories_created}`); } else if (status.status == 'failed') { console.log(`Error: ${status.error_message}`); } ``` ### Ingestion Statuses | Status | Description | | ----------------- | --------------------------------------------------------------------------- | | `queued` | Document accepted and waiting for processing | | `processing` | Actively being processed through the ingestion pipeline | | `completed` | Successfully ingested. Memories are available for retrieval | | `failed` | Processing failed. Check `error_message` for details | | `partial_success` | Some extractions succeeded but others failed. Partial results are available | The `partial_success` status typically occurs with large documents where some chunks process successfully while others encounter extraction errors. The successfully processed portions are still available for retrieval. ## Updating Memories Update an existing memory using `sdk.memories.update()`. This is useful when the source document has been edited or when you want to append new information. ```python Python theme={null} updated = await sdk.memories.update( memory_id=memory_id, document="Updated conversation transcript with additional turns...", merge_strategy="smart-merge", metadata={"updated_at": "2025-03-15", "reason": "new turns added"} ) ``` ```javascript JavaScript theme={null} const updated = await sdk.memories.update({ memory_id: memory_id, document: 'Updated conversation transcript with additional turns...', merge_strategy: 'smart-merge', metadata: {'updated_at': '2025-03-15', 'reason': 'new turns added'}, }); ``` ```typescript TypeScript theme={null} const updated = await sdk.memories.update({ memory_id: memory_id, document: 'Updated conversation transcript with additional turns...', merge_strategy: 'smart-merge', metadata: {'updated_at': '2025-03-15', 'reason': 'new turns added'}, }); ``` ### Merge Strategies Completely replaces the existing memory content with the new document. Previous extractions are discarded and re-extracted from the new content. Use when the document has been fully rewritten. Adds the new content to the end of the existing memory. Previous extractions are preserved, and new extractions are generated only from the appended content. Use when adding new turns to a conversation. Intelligently merges the new content with the existing memory. The pipeline detects overlapping sections, deduplicates extractions, and reconciles conflicting information by preferring the newer version. Use when the document has been partially edited. ## Deleting Memories Remove a memory and all its associated extractions permanently. ```python Python theme={null} await sdk.memories.delete(memory_id=memory_id) ``` ```typescript TypeScript theme={null} await sdk.memories.delete(memoryId: string) ``` Deletion is **permanent and irreversible**. All extractions, entity associations, and graph relationships derived from this memory are removed. This operation cannot be undone. ## Best Practices Always prefix conversation turns with speaker labels (`User:`, `Assistant:`, or actual names). The ingestion pipeline uses these labels to correctly attribute preferences, facts, and intents to the right participant. Use stable, deterministic identifiers for `user_id` and `customer_id`. These IDs form the basis of scoped retrieval. Inconsistent IDs fragment the user's memory across multiple scopes. When re-ingesting content that may have been submitted before (e.g., webhook retries), always provide a `document_id`. This prevents duplicate memories and ensures updates are applied cleanly. When backfilling historical conversations or documents, set `document_created_at` to the original timestamp. This enables accurate temporal reasoning ("What did the user say last month?"). Reserve `long-range` mode for content where deep understanding matters (user conversations, strategic documents). Use `fast` mode for high-volume, lower-priority data (automated logs, bulk imports). For bulk ingestion (backfills, migrations), use `batch_create()` with `fail_fast=False`. This reduces HTTP overhead and allows the pipeline to optimize scheduling. ## Next Steps Query the memories you have ingested for contextual retrieval. Understand how entities are automatically resolved during ingestion. Compress long conversations to reduce token costs. Configure SDK behavior, timeouts, and retry policies. # Initializing the SDK Source: https://docs.maximem.ai/sdk/initialization Set up the Synap SDK in your application. ## Overview Before your application can ingest memories or retrieve context, you must initialize the Synap SDK. Initialization validates your API key, establishes a secure connection, and prepares local caching. The SDK follows a strict **initialize, use, shutdown** lifecycle. ## Basic Initialization The simplest way to get started requires only your `SYNAP_API_KEY` environment variable. Generate an API key from the [Synap Dashboard](https://synap.maximem.ai) and set it in your environment. ```python Python theme={null} from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK( api_key="synap_your_key_here" ) await sdk.initialize() ``` ```javascript JavaScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); await sdk.initialize(); ``` ```typescript TypeScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); await sdk.initialize(); ``` You **must** call `await sdk.initialize()` before invoking any SDK operations. Calling methods like `sdk.memories.create()` before initialization raises an `AuthenticationError`. ### What `initialize()` Does When you call `initialize()`, the SDK performs the following steps in order: The SDK picks up the API key from the `api_key=` argument (if passed) or the `SYNAP_API_KEY` environment variable. The SDK asks Synap who the key belongs to. Your API key is the authoritative identity: this is why you never have to plumb an instance or client ID through your application yourself. The SDK opens an authenticated connection to Synap and, if real-time streaming is enabled, an additional streaming channel. If a `cache_backend` is configured (default: `sqlite`), the SDK sets up the local cache database at the storage path, namespaced to the client the key resolved to. Identity is resolved here, at `initialize()`, not when you construct the SDK. Construction only records which API key this SDK will use, and that key is what decides whether you get a new SDK or the existing one for that credential (see [Singleton Pattern](#singleton-pattern)). ## Initialization with Custom Configuration Pass an `SDKConfig` object to customize SDK behavior at construction time. ```python Python theme={null} from maximem_synap import MaximemSynapSDK, SDKConfig, TimeoutConfig, RetryPolicy config = SDKConfig( storage_path="/var/lib/myapp/synap", cache_backend="sqlite", session_timeout_minutes=60, timeouts=TimeoutConfig( connect=10.0, read=45.0, write=15.0, stream_idle=120.0 ), retry_policy=RetryPolicy( max_attempts=5, backoff_base=1.5, backoff_max=30.0, backoff_jitter=True ), log_level="INFO" ) sdk = MaximemSynapSDK( api_key="synap_your_key_here", config=config ) await sdk.initialize() ``` ```javascript JavaScript theme={null} // Settings are constructor options; there is no SDKConfig wrapper. const sdk = new SynapClient({ timeouts: { connect: 10.0, read: 45.0, write: 15.0, }, retryPolicy: { maxAttempts: 5, backoffBase: 1.5, backoffMax: 30.0, backoffJitter: true, }, }); await sdk.initialize(); ``` ```typescript TypeScript theme={null} // Settings are constructor options; there is no SDKConfig wrapper. const sdk = new SynapClient({ timeouts: { connect: 10.0, read: 45.0, write: 15.0, }, retryPolicy: { maxAttempts: 5, backoffBase: 1.5, backoffMax: 30.0, backoffJitter: true, }, }); await sdk.initialize(); ``` See [SDK Configuration](/sdk/configuration) for a complete reference of all configuration options. For the full `initialize()` signature and parameters, see the [API Reference](/sdk-reference/lifecycle/initialize). ## Singleton Pattern The SDK keeps **one live instance per API key**. Construct it twice with the same key (from two modules, or on every request) and the second construction hands you the SDK that already exists instead of opening a second set of connections and caches. ```python Python theme={null} sdk_a = MaximemSynapSDK(api_key="synap_key_1") sdk_b = MaximemSynapSDK(api_key="synap_key_1") # Same key, one live SDK: sdk_b shares sdk_a's connections, caches and credentials. ``` ```javascript JavaScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk_a = new SynapClient({ apiKey: 'synap_key_1' }); const sdk_b = new SynapClient({ apiKey: 'synap_key_1' }); // Same key, one live SDK: sdk_b shares sdk_a's connections, caches and credentials. ``` ```typescript TypeScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk_a = new SynapClient({ apiKey: 'synap_key_1' }); const sdk_b = new SynapClient({ apiKey: 'synap_key_1' }); // Same key, one live SDK: sdk_b shares sdk_a's connections, caches and credentials. ``` This prevents accidental duplication of connections and caches in applications that construct the SDK from multiple modules. `sdk_a is sdk_b` evaluates to `False`. The two objects share their internal state; they are not literally the same object. Use the SDK, don't identity-check it. **`SYNAP_INSTANCE_ID` in the environment is fine. `instance_id=` in the constructor is not.** They behave differently on purpose. The environment variable records which instance you are on and leaves the SDK keyed on your credential, so two keys stay independent. The constructor argument makes the id the identity: a second key used under it is silently discarded, and rotating a key that way has no effect. The dashboard gives you both variables to paste, and your API key already determines which instance you reach either way, so there is nothing to gain from the constructor form. ### Running multiple API keys in one process Construct the SDK with **different** API keys and you get independent SDKs, each with its own credentials, connections, caches and short-term stores. This is the supported pattern for a backend serving several tenants, and for pointing one SDK at staging and another at production inside a single process. ```python Python theme={null} tenant_a = MaximemSynapSDK(api_key="synap_key_tenant_a") tenant_b = MaximemSynapSDK(api_key="synap_key_tenant_b") await tenant_a.initialize() await tenant_b.initialize() # Each SDK authenticates as its own tenant. Local caches are namespaced per # instance under ~/.synap///, so they don't collide # either. ``` ```javascript JavaScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const tenant_a = new SynapClient({ apiKey: 'synap_key_tenant_a' }); const tenant_b = new SynapClient({ apiKey: 'synap_key_tenant_b' }); await tenant_a.initialize(); await tenant_b.initialize(); // Each SDK authenticates as its own tenant. Local caches are namespaced per // instance under ~/.synap///, so they don't collide // either. ``` ```typescript TypeScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const tenant_a = new SynapClient({ apiKey: 'synap_key_tenant_a' }); const tenant_b = new SynapClient({ apiKey: 'synap_key_tenant_b' }); await tenant_a.initialize(); await tenant_b.initialize(); // Each SDK authenticates as its own tenant. Local caches are namespaced per // instance under ~/.synap///, so they don't collide // either. ``` The per-instance segment of that path arrived in **0.4.3**. Up to 0.4.2 the cache was namespaced by `client_id` alone: the Synap *account*, not the memory store, so two instances belonging to one account shared the same cache files. Where the same `customer_id` or `user_id` appeared under both, one instance could be served the other's cached context. Server-side scoping was never affected; this was local disk only. Upgrading moves the cache to the new path, so the first request per entity after the upgrade is a miss. **Requires `maximem-synap` 0.4.1 or newer.** In earlier versions the singleton was keyed on the instance ID, which is empty at construction time and only resolved from your API key during `initialize()`. Every SDK built without an explicit `instance_id` therefore landed in the same slot, and the second construction silently adopted the first one's credentials, so the second tenant's reads and writes were executed against the first tenant's instance, with no error raised. If your process constructs SDKs for more than one API key, upgrade before relying on this section: ```bash theme={null} pip install --upgrade "maximem-synap>=0.4.1" ``` From 0.4.2 this also holds once an SDK knows its own instance. `initialize()` resolves the instance from your API key, and the SDK answers to that instance ID from then on, so constructing with it returns the same live SDK rather than standing up a second set of connections, caches and streams for one instance. #### Two keys, one instance The one case that gives you duplication rather than reuse is **two different API keys issued against the same instance**. Each key is its own identity, so each gets its own SDK: two Listen streams, two anticipation caches and two short-term stores, all for one instance. That is deliberate. Merging them would mean one caller transacting on the other's credential, which would make key rotation, revocation and per-key attribution all silently wrong. The cost is that short-term context recorded through one SDK is not visible to the other until the server round-trips it, and that the pair consumes two of the instance's concurrent streams. `initialize()` logs a warning when it detects this, naming the instance, so it does not stay invisible. Unless you specifically want two separately-authenticated SDKs, use one API key per instance per process. ### Rotating a key in a long-running process An SDK keeps the credential it was constructed with for its whole life, so rolling a new key into your secrets manager does not by itself change what a running process authenticates as. What happens next depends on how you construct: | How you construct | Passing a new key | | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | `MaximemSynapSDK(api_key=...)` | A different key is a different identity, so you get a new SDK on the new key. The rotation takes effect immediately. | | `MaximemSynapSDK(instance_id=...)` | The instance ID is the identity, so you get the **existing** SDK back: still on the old key. | If you construct by `instance_id`, call `await sdk.shutdown()` before reconstructing (that releases the slot, so the next construction builds a fresh SDK on the new key), or restart the worker. Do it **before** revoking the old key, or in-flight requests start failing authentication. Two keys issued against the same instance are one identity only on the `instance_id` path. Constructed the usual way, with `api_key=`, each key gets its own SDK authenticating as itself, which is why rotation on that path needs nothing special. ### Overriding the Singleton for Testing In test environments, use `_force_new=True` to bypass the singleton entirely and build a fresh SDK on every construction, even for a key that already has one. ```python Python theme={null} sdk = MaximemSynapSDK( api_key="synap_test_key", _force_new=True ) ``` ```javascript JavaScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_test_key', _force_new: true }); ``` ```typescript TypeScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_test_key', _force_new: true }); ``` An SDK built this way is never registered as the singleton for its key. It is therefore invisible to other constructions, and shutting it down leaves any live SDK for the same key untouched, which is what makes it safe to create and discard one per test. `_force_new` is intended for tests and for framework adapters that manage SDK lifetime themselves. You do not need it to run several tenants in one process, because different API keys already give you separate SDKs. Reaching for it in application code means opting out of connection and cache reuse, so each extra SDK pays for its own connections, its own streaming channel, and its own cache handles. ## Environment Variable Initialization For CI/CD, containers, serverless, and production, just set the environment variable: ```bash Linux / macOS theme={null} export SYNAP_API_KEY="synap_your_key_here" export SYNAP_INSTANCE_ID="inst_your_instance_id" ``` ```powershell Windows (PowerShell, session) theme={null} $env:SYNAP_API_KEY = "synap_your_key_here" $env:SYNAP_INSTANCE_ID = "inst_your_instance_id" ``` ```powershell Windows (PowerShell, persistent) theme={null} [System.Environment]::SetEnvironmentVariable("SYNAP_API_KEY", "synap_your_key_here", "User") [System.Environment]::SetEnvironmentVariable("SYNAP_INSTANCE_ID", "inst_your_instance_id", "User") ``` ```ini .env file (with python-dotenv) theme={null} SYNAP_API_KEY=synap_your_key_here SYNAP_INSTANCE_ID=inst_your_instance_id ``` ```python Python theme={null} from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK() await sdk.initialize() ``` ```javascript JavaScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient(); await sdk.initialize(); ``` ```typescript TypeScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient(); await sdk.initialize(); ``` The SDK reads `SYNAP_API_KEY` automatically and resolves the instance ID from the server. ## The `configure()` Method If you need to adjust configuration after constructing the SDK but before calling `initialize()`, use the `configure()` method. ```python Python theme={null} sdk = MaximemSynapSDK( api_key="synap_your_key_here" ) # Adjust config before initialization sdk.configure( log_level="DEBUG", session_timeout_minutes=120 ) await sdk.initialize() ``` ```javascript JavaScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); // Adjust config before initialization sdk.configure({ log_level: 'DEBUG', session_timeout_minutes: 120, }); await sdk.initialize(); ``` ```typescript TypeScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); // Adjust config before initialization sdk.configure({ log_level: 'DEBUG', session_timeout_minutes: 120, }); await sdk.initialize(); ``` Calling `configure()` after `initialize()` raises `InvalidInputError('Cannot reconfigure after initialization')`. All configuration must be finalized before the SDK is initialized. ## SDK Lifecycle The SDK follows a strict three-phase lifecycle: SDK lifecycle: initialize, use, shutdown ### 1. Initialize Call `await sdk.initialize()` to validate the API key and establish connections. ### 2. Use Invoke SDK operations: `sdk.memories.*`, `sdk.conversation.context.*`, `sdk.cache.*`, etc. All operations are async and must be awaited. ### 3. Shutdown Call `await sdk.shutdown()` to gracefully tear down the SDK. ```python Python theme={null} await sdk.shutdown() ``` ```typescript TypeScript theme={null} await sdk.shutdown() ``` Always call `shutdown()` before your application exits to ensure pending telemetry is flushed and active streaming connections are closed cleanly. Failing to call `shutdown()` may result in lost telemetry data and lingering connections. ## Initialize and Shut Down Cleanly For cleaner lifecycle management, wrap your application logic in a `try/finally` so `shutdown()` always runs, even if an error is raised mid-flight. ```python Python theme={null} from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK( api_key="synap_your_key_here" ) try: await sdk.initialize() # Your application logic response = await sdk.memories.create( document="User asked about project deadlines...", document_type="ai-chat-conversation", user_id="user_12345", customer_id="acme_corp", ) print(f"Ingestion ID: {response.ingestion_id}") finally: await sdk.shutdown() ``` ```javascript JavaScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); try { await sdk.initialize(); // Your application logic const response = await sdk.memories.create({ document: 'User asked about project deadlines...', document_type: 'ai-chat-conversation', user_id: 'user_12345', customer_id: 'acme_corp', }); console.log(`Ingestion ID: ${response.ingestion_id}`); } finally { await sdk.shutdown(); } ``` ```typescript TypeScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient({ apiKey: 'synap_your_key_here' }); try { await sdk.initialize(); // Your application logic const response = await sdk.memories.create({ document: 'User asked about project deadlines...', document_type: 'ai-chat-conversation', user_id: 'user_12345', customer_id: 'acme_corp', }); console.log(`Ingestion ID: ${response.ingestion_id}`); } finally { await sdk.shutdown(); } ``` ## Full Example with Error Handling The following example demonstrates a production-ready initialization pattern with comprehensive error handling. ```python Python theme={null} import logging import uuid from maximem_synap import MaximemSynapSDK, SDKConfig, TimeoutConfig, RetryPolicy from maximem_synap import ( AuthenticationError, NetworkTimeoutError, SynapError, ) logger = logging.getLogger(__name__) async def create_synap_sdk() -> MaximemSynapSDK: """Initialize the Synap SDK with production-ready configuration.""" config = SDKConfig( storage_path="/var/lib/myapp/synap", cache_backend="sqlite", session_timeout_minutes=60, timeouts=TimeoutConfig(connect=10.0, read=30.0), retry_policy=RetryPolicy(max_attempts=3), log_level="WARNING", ) sdk = MaximemSynapSDK( api_key="synap_your_key_here", config=config, ) try: await sdk.initialize() logger.info("Synap SDK initialized successfully") return sdk except AuthenticationError as e: logger.error( "Authentication failed: %s (correlation_id=%s)", e, e.correlation_id ) raise except NetworkTimeoutError: logger.error( "Could not reach Synap Cloud. Check network connectivity." ) raise except SynapError as e: logger.error( "Unexpected Synap error during init: %s (correlation_id=%s)", e, e.correlation_id ) raise async def main(): sdk = await create_synap_sdk() try: # Application logic here. # conversation_id must be a valid UUID string; generate one with # str(uuid.uuid4()) or reuse a UUID you already manage per conversation. context = await sdk.conversation.context.fetch( conversation_id=str(uuid.uuid4()), mode="fast" ) print(f"Retrieved {len(context.facts)} facts") finally: await sdk.shutdown() logger.info("Synap SDK shut down cleanly") ``` ```javascript JavaScript theme={null} import { randomUUID } from 'node:crypto'; import { SynapClient, AuthenticationError, NetworkTimeoutError, SynapError, } from '@maximem/synap-js-sdk'; async function createSynapSdk() { const sdk = new SynapClient({ apiKey: 'synap_your_key_here', timeouts: { connect: 10.0, read: 30.0 }, retryPolicy: { maxAttempts: 3 }, }); try { await sdk.initialize(); console.info('Synap SDK initialized successfully'); return sdk; } catch (e) { // JS allows one catch, so discriminate inside it. Branch on `.code` // rather than instanceof at a package boundary: a dual ESM/CJS graph // can hand you two copies of the same class. if (e instanceof AuthenticationError) { console.error('Authentication failed: %s (correlation_id=%s)', e, e.correlationId); } else if (e instanceof NetworkTimeoutError) { console.error('Could not reach Synap Cloud. Check network connectivity.'); } else if (e instanceof SynapError) { console.error( 'Unexpected Synap error during init: %s (correlation_id=%s)', e, e.correlationId, ); } throw e; } } async function main() { const sdk = await createSynapSdk(); try { // Application logic here. // conversation_id must be a valid UUID string; generate one with // randomUUID() or reuse a UUID you already manage per conversation. const context = await sdk.conversation.context.fetch({ conversation_id: randomUUID(), mode: 'fast', }); console.log(`Retrieved ${(context.facts ?? []).length} facts`); } finally { await sdk.shutdown(); console.info('Synap SDK shut down cleanly'); } } ``` ```typescript TypeScript theme={null} import { randomUUID } from 'node:crypto'; import { SynapClient, AuthenticationError, NetworkTimeoutError, SynapError, } from '@maximem/synap-js-sdk'; async function createSynapSdk(): Promise { const sdk = new SynapClient({ apiKey: 'synap_your_key_here', timeouts: { connect: 10.0, read: 30.0 }, retryPolicy: { maxAttempts: 3 }, }); try { await sdk.initialize(); console.info('Synap SDK initialized successfully'); return sdk; } catch (e) { // JS allows one catch, so discriminate inside it. Branch on `.code` // rather than instanceof at a package boundary: a dual ESM/CJS graph // can hand you two copies of the same class. if (e instanceof AuthenticationError) { console.error('Authentication failed: %s (correlation_id=%s)', e, e.correlationId); } else if (e instanceof NetworkTimeoutError) { console.error('Could not reach Synap Cloud. Check network connectivity.'); } else if (e instanceof SynapError) { console.error( 'Unexpected Synap error during init: %s (correlation_id=%s)', e, e.correlationId, ); } throw e; } } async function main(): Promise { const sdk = await createSynapSdk(); try { // Application logic here. // conversation_id must be a valid UUID string; generate one with // randomUUID() or reuse a UUID you already manage per conversation. const context = await sdk.conversation.context.fetch({ conversation_id: randomUUID(), mode: 'fast', }); console.log(`Retrieved ${(context.facts ?? []).length} facts`); } finally { await sdk.shutdown(); console.info('Synap SDK shut down cleanly'); } } ``` The API key is read fresh on every start. There is no one-time setup step: the same `SYNAP_API_KEY` works forever (until you revoke it in the Dashboard). ## Next Steps Send conversations and documents into Synap's memory system. Query contextual memories for your AI agent. Explore all configuration options in detail. ## JavaScript: namespaced and flat surfaces The client exposes two call styles against the same instance. **Namespaced methods** mirror the Python SDK one to one, so the same call shapes work in both languages. They return the raw snake\_case response. ```ts theme={null} await synap.memories.create({ document, user_id, customer_id }); await synap.user.context.fetch({ user_id, customer_id, search_query: ["seat preference"] }); await synap.customer.context.fetch({ customer_id }); await synap.client.context.fetch({}); await synap.conversation.record_message({ conversation_id, // a UUID role: "user", content: "I prefer window seats", user_id, customer_id, // B2B only: required there, NOT accepted on B2C }); await synap.conversation.context.get_context_for_prompt({ conversation_id }); await synap.credits.get_balance(); ``` **Flat methods** are the JavaScript-idiomatic surface. They return the normalised camelCase shape. ```ts theme={null} await synap.addMemory({ document, user_id, customer_id }); await synap.searchMemory({ user_id, customer_id, query: "seat preference" }); await synap.getMemories({ user_id, customer_id }); await synap.fetchUserContext({ user_id, customer_id }); await synap.fetchCustomerContext({ customer_id }); await synap.fetchClientContext({}); await synap.getContextForPrompt({ conversation_id }); ``` **`customer_id` depends on your instance.** On a B2B instance (`user_context_isolation = strict`) it is required. On a **B2C** instance (`equals_customer`) it is **not accepted**: the `user_id` is the whole identity, and a call carrying a `customer_id` is rejected. The SDK checks this at the call site once `initialize()` has learned which mode you are on, so the mistake surfaces immediately rather than as a silently empty fetch. See [B2C vs B2B](/concepts/memory-scopes#b2c-vs-b2b-which-scopes-apply-to-you). The two surfaces return **different shapes on purpose**. `user.context.fetch()` gives you the raw snake\_case response; `fetchUserContext()` gives you the normalised camelCase one. Pick one style per codebase rather than mixing them. ## How it differs from the Python SDK The two SDKs share one behaviour contract and the same method set, so a Python example translates call for call. Seven things still differ, and every one of them has bitten someone. `wait_for_completion`, `record_message`, `stop_listening`, `batch_create`, `get_context_for_prompt`, `create_from_file`. All of them keep Python's spelling so a snippet ports without renaming. `memories.waitForCompletion` is `undefined`, not an alias, so the mistake surfaces as `is not a function` at the call site rather than at import. ```javascript theme={null} await sdk.memories.wait_for_completion(result.ingestion_id); // correct await sdk.memories.waitForCompletion(result.ingestion_id); // TypeError ``` *Arguments* are the opposite: both spellings work everywhere, so `{ user_id }` and `{ userId }` are equally valid. Python returns a Pydantic model whose `facts`, `preferences`, `episodes`, `emotions` and `temporal_events` always exist, empty at worst. The namespaced JavaScript surface returns the raw JSON, where a collection the server omitted is `undefined`. ```javascript theme={null} for (const fact of context.facts ?? []) { /* ... */ } console.log((context.facts ?? []).length); ``` Reading `context.facts.length` on a response with no facts throws. Python caches to SQLite (`cache_backend` defaults to `"sqlite"`), so a restarted process keeps its cache. JavaScript caches in memory. This is a **billing** difference, not only a latency one: a cache miss is a metered retrieval. Long-lived servers are barely affected. Short-lived processes and serverless functions will see more cloud fetches than the equivalent Python deployment. Every error carries a stable `.code`. With a dual ESM/CJS dependency graph a consumer can end up holding two copies of the same error class, and `instanceof` then fails against the copy it was not built from. ```javascript theme={null} if (isSynapError(e) && e.code === 'insufficient_credits') { /* top up */ } ``` `instanceof` is made to work across copies as well, but `.code` is the documented contract and the one to rely on at a package boundary. `storage_path`, `cache_backend` and `session_timeout_minutes` exist in `ConfigureOptions` so a config object can be shared between the two SDKs without a type error. They do nothing here: the cache is in memory, so there is no path to point at and no backend to swap. (`session_timeout_minutes` does nothing in the Python SDK either.) `log_level` is likewise ignored, but `logger` is not: pass a `(level, message) => void` and every diagnostic the SDK emits goes to it instead of the console. There is no global logging framework in JavaScript whose level there would be to set, so the SDK takes the sink directly. Timeouts and the retry policy are real, and their defaults match Python exactly: connect 5s, read 30s, write 10s, stream idle 60s, 3 attempts, backoff base 1s capped at 10s. Each memory type names its text differently: facts, preferences and temporal events use `content`, episodes use `summary`, emotions use `context`. Concatenating the collections and reading `.content` across them is wrong in both languages. JavaScript ships a helper that flattens every collection and normalises the text into `.memory`: ```javascript theme={null} import { flattenContextItems } from '@maximem/synap-js-sdk'; const block = flattenContextItems(context).map((m) => `- ${m.memory}`).join('\n'); ``` Python has no free-function equivalent. Its `format_for_prompt` is a method on the cross-scope `sdk.fetch()` result, not on a scoped fetch. Every HTTP path (ingestion, retrieval, profiles, credits) runs on Edge, Workers and in the browser. `listen()` is gRPC, so it needs Node and the two optional peers. Importing the SDK in an Edge route stays safe; only the stream is unavailable there. See [Where it runs](/setup/installation#where-it-runs). # Response Shapes Source: https://docs.maximem.ai/sdk/response-shapes Pydantic type definitions for every response object the SDK returns. Use as a single source of truth when wiring response data through your application. **JavaScript ships these as TypeScript types, and there are two shapes.** The namespaced surface (`sdk.user.context.fetch`) returns the raw response: `RawContext`, whose items are `RawContextItem` with the **snake\_case** field names shown below, and every collection optional. The legacy flat surface (`sdk.fetchUserContext`) returns `NormalisedContext`, whose items are the exported `Fact`, `Preference`, `Episode`, `Emotion` and `TemporalEvent` types with **camelCase** fields (`extractedAt`, not `extracted_at`) and no optionality. Both are exported. Pick the one matching the surface you call. See [namespaced and flat surfaces](/sdk/initialization#javascript-namespaced-and-flat-surfaces). Every SDK method returns a Pydantic model. This page lists them all in one place so you can grep for field names without hunting through individual method docs. **Type-specific field names.** The Pydantic models below intentionally use type-specific field names (`Preference.strength`, not `confidence`, and `Episode.summary`, not `content`) because each memory type has different semantics. The SDK handles transport-level field mapping internally; you only work with these typed models in application code. All types live in `maximem_synap` and are importable from the top-level package: ```python Python theme={null} from maximem_synap import ( Fact, Preference, Episode, Emotion, TemporalEvent, ContextResponse, ConversationContextModel, ResponseMetadata, CreateMemoryResponse, MemoryStatusResponse, IngestStatus, IngestMode, CompactionResponse, CompactionTriggerResponse, CompactionStatusResponse, ContextForPromptResponse, RecentMessage, CompactionLevel, ) ``` ```typescript TypeScript theme={null} import type { Fact, Preference, Episode, Emotion, TemporalEvent, RawContext, RawContextItem, RawConversationContext, ContextMetadata, NormalisedContext, ConversationContext, FlatMemory, CreateMemoryResult, BatchCreateResult, TranscriptIngestResult, CompactOptions, CompactionLevel, IngestMode, DocumentType, } from '@maximem/synap-js-sdk'; ``` *** ## Memory item types These are the atomic units of structured memory. A `ContextResponse` is a bag of these. ### Fact ```python Python theme={null} class Fact(BaseModel): id: str # opaque identifier content: str # natural-language fact confidence: float # 0.0 - 1.0 source: str # memory ID this fact was extracted from extracted_at: datetime metadata: Dict[str, Any] = {} event_date: Optional[datetime] = None # when the fact became true, if known valid_until: Optional[datetime] = None # when the fact stopped being true, if known temporal_category: Optional[str] = None # "perpetual" | "temporal_fact" | "episode" temporal_confidence: float = 0.0 source_evidence: Optional[List[str]] = None ``` ```typescript TypeScript theme={null} import type { Json } from '@maximem/synap-js-sdk'; // Namespaced surface: raw, snake_case, every field optional. interface RawContextItem { id?: string; content?: string; confidence?: number; source?: string; extracted_at?: string | null; metadata?: Json; event_date?: string | null; valid_until?: string | null; temporal_category?: string | null; temporal_confidence?: number; [key: string]: unknown; } // Flat surface: normalised, camelCase, required. interface Fact { id: string; content: string; confidence: number; source: string; extractedAt: string | null; metadata: Json; } ``` ### Preference ```python Python theme={null} class Preference(BaseModel): id: str category: str # e.g., "communication", "dietary", "ui" content: str strength: float # 0.0 - 1.0: NOT named `confidence` source: str = "" extracted_at: datetime metadata: Dict[str, Any] = {} event_date: Optional[datetime] = None valid_until: Optional[datetime] = None temporal_category: Optional[str] = None temporal_confidence: float = 0.0 source_evidence: Optional[List[str]] = None ``` ```typescript TypeScript theme={null} import type { Json } from '@maximem/synap-js-sdk'; interface Preference { id: string; category: string; content: string; /** Python names this `strength`, not `confidence`. So does this. */ strength: number; source: string; extractedAt: string | null; metadata: Json; } ``` ### Episode ```python Python theme={null} class Episode(BaseModel): id: str summary: str # narrative description: NOT `content` occurred_at: datetime significance: float # 0.0 - 1.0 participants: List[str] = [] # entity IDs involved metadata: Dict[str, Any] = {} event_date: Optional[datetime] = None valid_until: Optional[datetime] = None temporal_category: Optional[str] = None temporal_confidence: float = 0.0 source_evidence: Optional[List[str]] = None ``` ```typescript TypeScript theme={null} import type { Json } from '@maximem/synap-js-sdk'; interface Episode { id: string; /** Episodes carry `summary`, not `content`. */ summary: string; occurredAt: string | null; significance: number; participants: unknown[]; metadata: Json; } ``` ### Emotion ```python Python theme={null} class Emotion(BaseModel): id: str emotion_type: str # "frustrated" | "satisfied" | "confused" | … intensity: float # 0.0 - 1.0 detected_at: datetime context: str # what triggered the emotion metadata: Dict[str, Any] = {} event_date: Optional[datetime] = None valid_until: Optional[datetime] = None temporal_category: Optional[str] = None temporal_confidence: float = 0.0 source_evidence: Optional[List[str]] = None ``` ```typescript TypeScript theme={null} import type { Json } from '@maximem/synap-js-sdk'; interface Emotion { id: string; emotionType: string; intensity: number; detectedAt: string | null; /** The human-readable text lives here, not in a `content` field. */ context: string; metadata: Json; } ``` ### TemporalEvent ```python Python theme={null} class TemporalEvent(BaseModel): id: str content: str event_date: datetime # REQUIRED: when the event occurred valid_until: Optional[datetime] = None temporal_category: str # REQUIRED: "perpetual" | "temporal_fact" | "episode" temporal_confidence: float # REQUIRED: 0.0 - 1.0 confidence: float = 0.0 source: str = "" extracted_at: Optional[datetime] = None metadata: Dict[str, Any] = {} source_evidence: Optional[List[str]] = None ``` ```typescript TypeScript theme={null} import type { Json } from '@maximem/synap-js-sdk'; interface TemporalEvent { id: string; content: string; eventDate: string | null; validUntil: string | null; temporalCategory: string | null; temporalConfidence: number; confidence: number; source: string; extractedAt: string | null; metadata: Json; } ``` *** ## Context responses ### ContextResponse Returned by `conversation.context.fetch`, `user.context.fetch`, `customer.context.fetch`, `client.context.fetch`. ```python theme={null} class ContextResponse(BaseModel): facts: List[Fact] = [] preferences: List[Preference] = [] episodes: List[Episode] = [] emotions: List[Emotion] = [] temporal_events: List[TemporalEvent] = [] metadata: ResponseMetadata # Optional rolling conversation context window (recent messages, summary) conversation_context: Optional[ConversationContextModel] = None # Populated only in conversation-summary fetches (context_mode="conversation-summary") profile: Optional[UserProfileModel] = None conversations: Optional[List[ConversationSummaryModel]] = None ``` Iterate over all items in priority order: ```python Python theme={null} for item in ctx.facts + ctx.preferences + ctx.episodes + ctx.emotions + ctx.temporal_events: print(item) ``` ```javascript JavaScript theme={null} for (const item of ctx.facts + ctx.preferences + ctx.episodes + ctx.emotions + ctx.temporal_events) { console.log(item); } ``` ```typescript TypeScript theme={null} for (const item of ctx.facts + ctx.preferences + ctx.episodes + ctx.emotions + ctx.temporal_events) { console.log(item); } ``` The atomized lists above are the primary surface. The optional `conversation_context` carries the rolling session view (compacted summary plus recent turns) as a single coherent block rather than atomized items. It is `None` unless the conversation has a compaction available. ### ConversationContextModel The current-session context attached to a `ContextResponse` as `conversation_context`. It bundles the compacted narrative summary, current state, key extractions, and the most recent raw turns together, instead of splitting them into the typed item lists. It is `None` when no compacted/session context exists for the conversation (e.g. a fresh conversation). ```python theme={null} class ConversationContextModel(BaseModel): summary: Optional[str] = None # compacted narrative summary current_state: Dict[str, Any] = {} # rolling "where things stand" state key_extractions: Dict[str, List[Dict[str, Any]]] = {} # grouped facts/decisions/preferences recent_turns: List[Dict[str, Any]] = [] # most recent raw conversation turns compaction_id: Optional[str] = None # source compaction, if any compacted_at: Optional[str] = None # when that compaction ran conversation_id: Optional[str] = None ``` `conversation_context` is the coherent-block view; `facts` / `preferences` / `episodes` / `emotions` / `temporal_events` are the atomized view of retrieval. Most integrations read the atomized lists; reach for `conversation_context` when you want the pre-assembled session narrative. For prompt-ready compacted text specifically, prefer `get_context_for_prompt()` (see [Context Compaction](/sdk/context-compaction)). ### UserProfileModel The caller profile returned inline by conversation-summary fetches (as `ContextResponse.profile` / `UnifiedContextResponse.profile`) and directly by `user.get_profile`. It bundles client-defined critical attributes with a short free-text overview. ```python theme={null} class ProfileAttributeModel(BaseModel): # extra="allow", .raw value: Any = None confidence: Optional[float] = None updated_at: Optional[str] = None source_conversation_id: Optional[str] = None class UserProfileModel(BaseModel): # extra="allow", .raw attributes: Dict[str, ProfileAttributeModel] = {} overview: Optional[str] = None extras: Dict[str, Any] = {} meta: Dict[str, Any] = {} # the profile document's _meta ``` ### ConversationSummaryModel One previous-conversation summary, returned in the `conversations` list of a conversation-summary fetch: what a prior call was about and how it progressed. ```python theme={null} class ConversationSummaryModel(BaseModel): # extra="allow", .raw conversation_id: str # server-coerced UUID external_conversation_id: Optional[str] # the id you minted conversation_type: Optional[str] started_at: Optional[datetime] ended_at: Optional[datetime] last_message_at: Optional[datetime] message_count: int = 0 summary_status: str # "available" | "pending" | "failed" summary: Optional[Dict[str, Any]] # narrative overview + current_state classification: Optional[Dict[str, Any]] # {primary_category, subcategory, objective} analysis: Optional[Dict[str, Any]] # your own analysis JSON, echoed back compaction_version: Optional[int] compacted_at: Optional[datetime] ``` A conversation whose summary hasn't been produced yet returns `summary=None, summary_status="pending"`; one whose compaction failed returns `"failed"`. Callers still see that the call happened either way. `UnifiedContextResponse.format_for_prompt()` renders these under a `## Previous Conversations` section (and the profile under `## Caller Profile`). ### ResponseMetadata ```python theme={null} class ResponseMetadata(BaseModel): correlation_id: str # log this on errors ttl_seconds: int # local-cache validity source: str # "cache", "cloud", or "anticipation" compaction_applied: Optional[CompactionLevel] = None # enum if compaction ran, else None retrieved_at: datetime ``` `compaction_applied` is **not** a bool. It's `None` when no compaction ran, or a `CompactionLevel` enum value when one did. Test with `if meta.compaction_applied is not None`. *** ## Ingestion responses ### CreateMemoryResponse Returned by `memories.create`. Ingestion is async: this comes back immediately with an `ingestion_id` you can poll. ```python theme={null} class CreateMemoryResponse(BaseModel): ingestion_id: UUID # poll status with sdk.memories.status(ingestion_id) document_id: str status: IngestStatus # see enum below queued_at: datetime error_message: Optional[str] = None # populated when status == FAILED ``` ### TranscriptIngestResponse Returned by `conversation.ingest_transcript`. Like `CreateMemoryResponse`, ingestion is async: poll `ingestion_id` with `memories.status()`. ```python theme={null} class TranscriptIngestResponse(BaseModel): # extra="allow", .raw conversation_id: str # server-coerced (UUID form) external_conversation_id: str # the id you supplied, echoed back ingestion_id: UUID # always set (never null, even on "duplicate") status: Literal["queued", "duplicate"] turns_recorded: int summary_status: Literal["in_progress", "already_compacted", "skipped"] queued_at: datetime ``` ### IngestStatus enum ```python theme={null} class IngestStatus(str, Enum): QUEUED = "queued" PROCESSING = "processing" COMPLETED = "completed" FAILED = "failed" PARTIAL_SUCCESS = "partial_success" ``` ### MemoryStatusResponse Returned by `memories.status(ingestion_id)`. ```python theme={null} class MemoryStatusResponse(BaseModel): ingestion_id: UUID document_id: str status: IngestStatus queued_at: datetime started_at: Optional[datetime] = None completed_at: Optional[datetime] = None memories_created: int = 0 memory_ids: List[str] = [] # IDs of memories produced by this ingestion error_message: Optional[str] = None ``` *** ## Compaction responses ### CompactionTriggerResponse Returned by `conversation.context.compact`. This call **kicks off** a compaction job asynchronously and returns this trigger confirmation, **not** the compacted content. To get the actual compacted text, call `get_compacted()` once the job completes (or poll `get_compaction_status()`). ```python theme={null} class CompactionTriggerResponse(BaseModel): compaction_id: str # poll status with get_compaction_status conversation_id: str status: str # e.g. "queued" | "in_progress" trigger_type: str # "manual" | "scheduled" | "threshold" | … initiated_at: datetime estimated_completion_seconds: Optional[int] = None # Populated when a prior compaction already existed for this conversation previous_context: Optional[str] = None previous_context_age_seconds: Optional[int] = None previous_compaction_id: Optional[str] = None ``` ### CompactionResponse Returned by `conversation.context.get_compacted`. Carries the actual compacted text and typed extractions. ```python theme={null} class CompactionResponse(BaseModel): compacted_context: str # the actual compacted text original_token_count: int compacted_token_count: int compression_ratio: float level_applied: CompactionLevel metadata: ResponseMetadata compaction_id: Optional[str] = None strategy_used: Optional[str] = None validation_score: Optional[float] = None validation_passed: Optional[bool] = None quality_warning: Optional[bool] = None # True if quality below threshold # Typed extractions surfaced from the underlying conversation facts: List[Dict[str, Any]] = [] decisions: List[Dict[str, Any]] = [] preferences: List[Dict[str, Any]] = [] current_state: Optional[Dict[str, Any]] = None ``` ### CompactionStatusResponse Returned by `get_compaction_status`. **This is a Pydantic model: access fields as attributes, not dict keys.** ```python theme={null} class CompactionStatusResponse(BaseModel): conversation_id: str status: str # "completed" | "in_progress" | "failed" | "none" compaction_id: Optional[str] = None completed_at: Optional[datetime] = None compression_ratio: Optional[float] = None validation_score: Optional[float] = None estimated_completion_seconds: Optional[int] = None error_message: Optional[str] = None latest_version: Optional[int] = None latest_created_at: Optional[datetime] = None ``` ### ContextForPromptResponse Returned by `get_context_for_prompt`. Optimized for direct injection into an LLM system prompt. ```python theme={null} class ContextForPromptResponse(BaseModel): formatted_context: Optional[str] = None # ready to splice into a system prompt available: bool = False # is there compacted context yet? is_stale: bool = False # new messages since the last compaction? compression_ratio: Optional[float] = None validation_score: Optional[float] = None compaction_age_seconds: Optional[int] = None quality_warning: bool = False # default False, never None recent_messages: List[RecentMessage] = [] recent_message_count: int = 0 compacted_message_count: int = 0 total_message_count: int = 0 ``` ### CompactionLevel enum ```python theme={null} class CompactionLevel(str, Enum): LOW = "low" MEDIUM = "medium" HIGH = "high" CONSERVATIVE = "conservative" BALANCED = "balanced" AGGRESSIVE = "aggressive" ADAPTIVE = "adaptive" ``` All seven values are equally canonical members of the enum. *** ## Type-checking tips * Only `ContextResponse` and `CompactionResponse` have `model_config = {"extra": "allow"}` and expose the raw cloud payload via a `.raw` property; other models silently drop unknown fields. When the cloud adds a new field on those two models, you can read it from `response.raw` until a typed attribute ships. * Datetime fields are timezone-aware (UTC). When comparing, use `datetime.now(timezone.utc)`, not `datetime.utcnow()`. * For runtime validation (e.g., in your application boundary), call `.model_validate(...)` rather than constructing manually: Pydantic enforces all constraints. ## JavaScript: exported types Option and response types are exported for annotating your own functions: ```ts theme={null} import type { SynapClientOptions, CreateMemoryOptions, UnifiedFetchOptions, UnifiedContext, RawContext, NormalisedContext, FetchOptions, ToolDefinition, DocumentType, IngestMode, } from "@maximem/synap-js-sdk"; ```
# Testing Source: https://docs.maximem.ai/sdk/testing Patterns for testing application code that depends on Synap: unit tests with mocks, fixtures, and integration tests against a real Instance. Testing code that calls the Synap SDK falls into three patterns. Pick by how realistic you need the test to be. ## 1. Unit tests with mocks Use this for testing **your business logic** that happens around Synap calls: prompt assembly, decision logic, response handling. Mock the SDK so the test is fast and deterministic and doesn't need network. ```python Python theme={null} # tests/test_chat.py import pytest from unittest.mock import AsyncMock, MagicMock from maximem_synap import ContextResponse, Fact, ResponseMetadata from datetime import datetime, timezone from myapp.chat import handle_turn # your code under test def make_fake_context(facts: list[str]) -> ContextResponse: """Build a realistic ContextResponse without hitting the network.""" return ContextResponse( facts=[ Fact( id=f"fact_{i}", content=content, confidence=0.9, source="test", extracted_at=datetime.now(timezone.utc), ) for i, content in enumerate(facts) ], metadata=ResponseMetadata( correlation_id="test-corr-id", ttl_seconds=300, source="cloud", retrieved_at=datetime.now(timezone.utc), ), ) @pytest.fixture def fake_sdk(): sdk = MagicMock() sdk.conversation.context.fetch = AsyncMock(return_value=make_fake_context([])) sdk.memories.create = AsyncMock(return_value=MagicMock(ingestion_id="ing_test")) return sdk async def test_handle_turn_uses_facts_in_prompt(fake_sdk): fake_sdk.conversation.context.fetch.return_value = make_fake_context( ["User prefers dark mode", "User is on the Pro plan"] ) reply = await handle_turn( sdk=fake_sdk, user_id="user_test", customer_id="cust_test", conversation_id="3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c", message="What plan am I on?", ) # Assert the system prompt mentioned both retrieved facts call_args = fake_sdk.openai_client.chat.completions.create.call_args # if you injected it system = call_args.kwargs["messages"][0]["content"] assert "dark mode" in system assert "Pro plan" in system async def test_handle_turn_ingests_the_turn(fake_sdk): await handle_turn(sdk=fake_sdk, user_id="u", customer_id="c", conversation_id="3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c", message="Hi") fake_sdk.memories.create.assert_called_once() kwargs = fake_sdk.memories.create.call_args.kwargs assert kwargs["user_id"] == "u" assert kwargs["customer_id"] == "c" assert kwargs["document_type"] == "ai-chat-conversation" ``` ```typescript TypeScript theme={null} // tests/chat.test.ts import { describe, it, expect, vi } from 'vitest'; import type { RawContext, SynapClient } from '@maximem/synap-js-sdk'; import { handleTurn } from '../src/chat.js'; // your code under test // Build a realistic response without hitting the network. The namespaced // surface returns the raw snake_case shape, so the fixture uses those names. export function makeFakeContext(facts: string[]): RawContext { return { facts: facts.map((content, i) => ({ id: `fact_${i}`, content, confidence: 0.9, source: 'test', extracted_at: '2026-01-01T00:00:00Z', })), metadata: { correlation_id: 'test-corr-id', ttl_seconds: 300, source: 'cloud', retrieved_at: '2026-01-01T00:00:00Z', }, } as RawContext; } // Mock the SDK's public methods, not its transport: those are your seams. export function makeFakeSdk(context: RawContext = makeFakeContext([])) { return { conversation: { context: { fetch: vi.fn().mockResolvedValue(context) } }, memories: { create: vi.fn().mockResolvedValue({ ingestion_id: 'ing_test' }) }, } as unknown as SynapClient; } describe('handleTurn', () => { it('injects retrieved facts into the prompt', async () => { const sdk = makeFakeSdk(makeFakeContext(['User prefers dark mode'])); const reply = await handleTurn(sdk, { user_id: 'u', conversation_id: '3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c', message: 'hello', }); expect(reply).toBeTruthy(); }); }); ``` **Why use the real Pydantic models when mocking** Constructing a real `ContextResponse` instead of `MagicMock` catches field-name typos at test-write time. If you later upgrade the SDK and a field is removed, the test fails loudly instead of silently passing on a mock that "accepts everything." ## 2. FastAPI integration tests with dependency overrides If you wired the SDK via `Depends(get_sdk)` (the recommended pattern in [Setup & Integration](/setup/detailed-integration)), FastAPI's `app.dependency_overrides` swaps it out per test. In Express there is no injector, so export the client from one module and let the test replace it with `vi.mock`. ```python Python theme={null} from fastapi.testclient import TestClient from myapp.main import app, get_sdk def test_chat_endpoint_with_mock_sdk(): fake_sdk = MagicMock() fake_sdk.conversation.context.fetch = AsyncMock(return_value=make_fake_context([])) fake_sdk.memories.create = AsyncMock() app.dependency_overrides[get_sdk] = lambda: fake_sdk try: client = TestClient(app) response = client.post("/chat", json={ "message": "hello", "user_id": "u", "customer_id": "c", "conversation_id": "3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c", }) assert response.status_code == 200 finally: app.dependency_overrides.clear() ``` ```typescript TypeScript theme={null} import { describe, it, expect, vi, beforeEach } from 'vitest'; import request from 'supertest'; // Replace the module the route imports its client from. Do this before // importing the app, so the route closes over the fake. vi.mock('../src/synap.js', () => ({ sdk: { conversation: { context: { fetch: vi.fn().mockResolvedValue(makeFakeContext([])) } }, memories: { create: vi.fn().mockResolvedValue({ ingestion_id: 'ing_test' }) }, }, init: vi.fn(), cleanup: vi.fn(), })); describe('POST /chat', () => { beforeEach(() => { vi.resetModules(); }); it('answers with a mocked SDK', async () => { const { app } = await import('../src/server.js'); const response = await request(app).post('/chat').send({ message: 'hello', user_id: 'u', customer_id: 'c', conversation_id: '3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c', }); expect(response.status).toBe(200); }); }); ``` ## 3. End-to-end against a real test Instance For the highest-fidelity tests (pre-release smoke tests, contract tests against new SDK versions) run against a dedicated test Instance in Synap Cloud. ```python Python theme={null} import asyncio import os import uuid import pytest from maximem_synap import MaximemSynapSDK pytestmark = pytest.mark.integration # opt-in marker so unit suite stays fast @pytest.fixture(scope="session") async def real_sdk(): """One real SDK per test session, using a dedicated test instance.""" sdk = MaximemSynapSDK(api_key=os.environ["SYNAP_TEST_API_KEY"]) await sdk.initialize() yield sdk await sdk.shutdown() # Note: the async session fixture above requires `pytest-asyncio` configured with # `asyncio_mode = "auto"`, or switch the decorator to `@pytest_asyncio.fixture(scope="session")`. # Under strict `pytest-asyncio` mode, plain `@pytest.fixture` async fixtures don't execute. @pytest.fixture def test_ids(): """Per-test isolated user/customer/conversation IDs so tests don't interfere.""" return { "user_id": f"test_user_{uuid.uuid4()}", "customer_id": f"test_cust_{uuid.uuid4()}", "conversation_id": str(uuid.uuid4()), } async def test_ingest_then_retrieve_roundtrip(real_sdk, test_ids): """Smoke test: ingestion → retrieval works end-to-end.""" await real_sdk.memories.create( document="User: I prefer dark mode.\nAssistant: Noted!", document_type="ai-chat-conversation", **test_ids, ) # Wait for async processing await asyncio.sleep(5) ctx = await real_sdk.conversation.context.fetch( conversation_id=test_ids["conversation_id"], search_query=["dark mode"], user_id=test_ids["user_id"], customer_id=test_ids["customer_id"], ) assert any("dark mode" in p.content.lower() for p in ctx.preferences) ``` ```typescript TypeScript theme={null} import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { randomUUID } from 'node:crypto'; import { SynapClient } from '@maximem/synap-js-sdk'; // Opt-in: keep the unit suite fast. Run with `vitest --project integration` // or guard on an env var, whichever your setup uses. const RUN = process.env.SYNAP_INTEGRATION === '1'; describe.skipIf(!RUN)('end-to-end against a real test Instance', () => { let sdk: SynapClient; beforeAll(async () => { // _force_new bypasses the per-identity client registry so this suite // cannot disturb a client the rest of the process already holds. sdk = new SynapClient({ apiKey: process.env.SYNAP_TEST_API_KEY, _force_new: true }); await sdk.initialize(); }); // Nothing else will close a _force_new client for you. afterAll(async () => { await sdk.shutdown(); }); it('ingests then retrieves, end to end', async () => { // Unique ids per test: Synap memories persist, and shared ids make // assertions flaky. const ids = { user_id: `test_user_${randomUUID()}`, customer_id: `test_cust_${randomUUID()}`, conversation_id: randomUUID(), }; const result = await sdk.memories.create({ document: 'User: I prefer dark mode.\nAssistant: Noted!', document_type: 'ai-chat-conversation', ...ids, }); // Wait on the pipeline rather than guessing with a fixed sleep. await sdk.memories.wait_for_completion(result.ingestion_id); const ctx = await sdk.conversation.context.fetch({ conversation_id: ids.conversation_id, search_query: ['dark mode'], user_id: ids.user_id, customer_id: ids.customer_id, }); expect( (ctx.preferences ?? []).some((p) => String(p.content).toLowerCase().includes('dark mode')), ).toBe(true); }); }); ``` **Use `_force_new=True` in tests to bypass the SDK singleton** The SDK keeps one live instance per API key. In tests where you want a fresh instance per test, opt out of that: ```python Python theme={null} sdk = MaximemSynapSDK(api_key="...", _force_new=True) ``` ```javascript JavaScript theme={null} const sdk = new SynapClient({ apiKey: '...', _force_new: true }); ``` ```typescript TypeScript theme={null} const sdk = new SynapClient({ apiKey: '...', _force_new: true }); ``` An SDK built this way is never registered as the singleton for its key, so tests can create and discard them freely: shutting one down won't disturb an SDK your application already holds for the same key. For the same reason, nothing else will ever close it for you: `await sdk.shutdown()` in your fixture teardown, or each test leaks its own transports, streaming channel and cache handles for the rest of the run. Don't reach for it in application code. You don't need it to run several tenants in one process, because different API keys already give you separate SDKs (see [Initialization](/sdk/initialization#running-multiple-api-keys-in-one-process)). Every extra SDK pays for its own connections, streaming channel and cache handles. **Use unique IDs per test** Synap memories persist. Two tests that both ingest `user_id="alice"` will see each other's data, and your assertions will be flaky. Always derive `user_id`, `customer_id`, `conversation_id` from a fresh UUID inside each test (`uuid.uuid4()` in Python, `crypto.randomUUID()` in JavaScript). ## 4. Snapshot testing of prompts If your application generates LLM system prompts that incorporate Synap context, snapshot-test the rendered prompt to catch unintended drift. ```python Python theme={null} from syrupy import snapshot async def test_prompt_renders_consistently(fake_sdk, snapshot): fake_sdk.conversation.context.fetch.return_value = make_fake_context([ "User prefers dark mode", "User is on Pro plan", ]) prompt = await build_prompt(fake_sdk, user_id="u", customer_id="c", conversation_id="3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c", message="What plan am I on?") assert prompt == snapshot ``` ```typescript TypeScript theme={null} import { expect, it } from 'vitest'; it('renders the prompt consistently', async () => { const fakeSdk = makeFakeSdk( makeFakeContext(['User prefers dark mode', 'User is on Pro plan']), ); const prompt = await buildPrompt(fakeSdk, { user_id: 'u', customer_id: 'c', conversation_id: '3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c', message: 'What plan am I on?', }); // Vitest has snapshots built in; no extra package. expect(prompt).toMatchSnapshot(); }); ``` Re-snapshot intentionally when you change prompt format; fail loudly when you change it by accident. ## What to skip in tests * **Don't mock the SDK's internal transport; mock its public methods.** Mock the SDK methods (`memories.create`, `context.fetch`); those are your seams. Mocking the transport couples your tests to internal SDK structure that will change. * **Don't snapshot `ContextResponse` objects directly.** They include timestamps and correlation IDs that change every run. Snapshot the *prompt string* you assemble from them. * **Don't share `user_id`s across tests.** Synap memories are real and persist; cross-test pollution will bite. # User Profile Source: https://docs.maximem.ai/sdk/user-profile Configure a structured, per-user profile that Synap builds and maintains from conversations: defined by your own attribute schema. ## Overview The **User Profile** is a structured document Synap maintains for every end user of your instance: a set of **critical attributes you define** (typed fields like a customer's budget, preferred language, or purchase timeline), a short narrative **overview**, and an **extras** bucket for stable facts that fall outside your schema. Profiles are built exclusively from what users actually said in ingested conversations, **extracted, never inferred**. If a conversation never touched an attribute, that attribute stays blank. Every populated attribute carries its own confidence, timestamp, and the ID of the conversation it came from. The profile is designed for **conversation-start injection**: one low-latency fetch returns the profile (plus recent conversation summaries) ready to paste into your agent's system prompt; see [Context Fetch](/sdk/context-fetch) and [`user.get_profile`](/sdk-reference/user/get-profile). ## Availability and enabling The feature ships platform-wide and is **on by default** for every instance. Without any configuration, Synap maintains the narrative overview and the `extras` bucket for each user. To get the full value, define your **critical attributes** in the instance's configuration: contact your Maximem team or use your dashboard's instance configuration. An instance can opt out entirely by setting `enabled: false`; that choice is preserved across configuration regenerations. ```json theme={null} { "user_profile": { "enabled": true, "overview_enabled": true, "max_profile_tokens": 1000, "critical_attributes": [ ... ] } } ``` Your attribute definitions are treated as client-owned state: they survive memory-architecture regeneration and instance promotion unchanged. ## Defining critical attributes Each attribute is a typed field: | Field | Type | Notes | | ---------------- | ----------------------------------------------------- | -------------------------------------------------------------------------------------------- | | `name` | string | snake\_case identifier, e.g. `budget_range` | | `type` | `string` \| `number` \| `boolean` \| `enum` \| `list` | `enum` requires `allowed_values` | | `description` | string | Guides extraction: state exactly what qualifies | | `allowed_values` | string\[] | Closed set for `enum` types | | `required` | boolean | Flags the attribute as expected (used by dashboards; extraction still never invents a value) | | `examples` | string\[] | Optional hints for open-ended fields | A realistic schema for a sales assistant: ```json theme={null} "critical_attributes": [ {"name": "customer_name", "type": "string", "description": "Only if explicitly stated by the customer — never inferred. Preferred name."}, {"name": "preferred_language", "type": "string", "description": "Preferred conversation language.", "examples": ["English", "Hindi"]}, {"name": "product_interest", "type": "enum", "description": "Product line the customer asked about.", "allowed_values": ["Starter", "Professional", "Enterprise"]}, {"name": "budget_range", "type": "string", "description": "Budget exactly as stated by the customer."}, {"name": "decision_timeline", "type": "enum", "description": "Stated purchase timeline.", "allowed_values": ["Immediate", "3 months", "6+ months"]}, {"name": "main_objection", "type": "string", "description": "Biggest objection raised.", "examples": ["Price", "Integration effort"]} ] ``` Write descriptions as extraction instructions. A phrase like *"only if explicitly stated by the customer"* in the description is honored by the extraction pass: fields stay blank until a conversation genuinely provides the value. ## How the profile is built The profile updates as a by-product of normal [ingestion](/sdk/ingestion); there is no separate API call to maintain it: 1. Your application pushes a conversation (for example with [`conversation.ingest_transcript`](/sdk-reference/conversation/ingest-transcript)) and moves on; everything below is asynchronous and off your hot path. 2. Synap's ingestion pipeline extracts long-term memories from the transcript. 3. At the end of the pipeline, the profile step loads your instance's attribute schema and merges newly extracted, conversation-grounded values into the user's profile document, updating only attributes the conversation actually evidenced, respecting `allowed_values`, and rewriting the narrative overview. 4. Each write bumps the profile's version and records, per attribute, the source conversation ID and timestamp. Unrelated attributes are never touched, and concurrent ingestions for the same user merge safely. Stable facts that don't match any configured attribute are kept in the profile's `extras` object rather than discarded. Client-supplied analysis passed with the transcript is used as extraction *hints* only; the transcript remains the source of truth. Attribute-schema edits take effect on new ingestions within a few minutes (configuration is cached briefly). Existing profile values are preserved; new and changed attributes populate as subsequent conversations provide evidence. ## Reading the profile At conversation start, fetch the profile together with recent conversation summaries in one call: ```python Python theme={null} ctx = await sdk.fetch( user_id="user-123", context_mode="conversation-summary", include_profile=True, last_n_conversations=1, ) prompt_block = ctx.formatted_context # "## Caller Profile" + "## Previous Conversations" profile = ctx.profile # typed document, or None if not yet built ``` ```javascript JavaScript theme={null} const ctx = await sdk.fetch({ user_id: 'user-123', context_mode: 'conversation-summary', include_profile: true, last_n_conversations: 1, }); const prompt_block = ctx.formatted_context; // "## Caller Profile" + "## Previous Conversations" const profile = ctx.profile; // typed document, or None if not yet built ``` ```typescript TypeScript theme={null} const ctx = await sdk.fetch({ user_id: 'user-123', context_mode: 'conversation-summary', include_profile: true, last_n_conversations: 1, }); const prompt_block = ctx.formatted_context; // "## Caller Profile" + "## Previous Conversations" const profile = ctx.profile; // typed document, or None if not yet built ``` Or read it directly: ```python Python theme={null} profile = await sdk.user.get_profile(user_id="user-123") ``` ```javascript JavaScript theme={null} const profile = await sdk.user.get_profile({ user_id: 'user-123', }); ``` ```typescript TypeScript theme={null} const profile = await sdk.user.get_profile({ user_id: 'user-123', }); ``` The document shape: ```json theme={null} { "attributes": { "budget_range": { "value": "about 20k per year", "confidence": 1.0, "updated_at": "2026-08-01T10:14:03Z", "source_conversation_id": "call-0042" } }, "overview": "Short narrative summary of who this user is and what they want.", "extras": {"team_size": 40}, "_meta": {"schema": 1, "version": 3, "updated_at": "2026-08-01T10:14:03Z"} } ``` See [`user.get_profile`](/sdk-reference/user/get-profile) for response details and [Response Shapes](/sdk/response-shapes) for the typed models. # Agent Integration Source: https://docs.maximem.ai/setup/agent-integration The integration built for agents: open one real-time stream, report each turn as it happens, and let Synap form long-term memory on its own. No per-turn ingestion calls. If you are wiring Synap into a live agent (a chat assistant, a copilot, a voice bot, anything with a conversation loop), this is the integration to build. Your agent reports what happened; Synap handles retrieval and memory formation. Agent integration flow: sdk.initialize() initializes the SDK, then sdk.instance.listen() starts listening once at startup, then a repeating per-turn block of send_message(user_message), fetch() served from cache, your LLM, and send_message(assistant_message), and finally sdk.instance.stop_listening() on shutdown That is the whole loop. **There is no `memories.create()` in it.** ## Why there is no ingestion call Conversation turns you report with `send_message()` are persisted to conversation history. When the conversation compacts, Synap promotes those raw turns into the long-term ingestion pipeline: the same extraction stages `memories.create()` runs, producing memories of the same quality. Compaction happens on any of three triggers: | Trigger | Default | Applies to | | ------------------- | ----------------------- | ------------------ | | **Token threshold** | 3,000 tokens | Long conversations | | **Message count** | 10 messages | Busy conversations | | **Idle period** | 5 minutes of inactivity | Everything else | The token and message thresholds are configurable per Instance. The idle trigger is what makes this complete: a two-turn exchange that never approaches the other thresholds still compacts a few minutes after the user stops, and its turns still become memory. Because the idle trigger catches whatever the thresholds miss, every conversation reaches long-term memory eventually. Reporting turns with `send_message()` is sufficient on its own. ## The integration ```python Python theme={null} from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK(api_key=os.environ["SYNAP_API_KEY"]) await sdk.initialize() ``` ```javascript JavaScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient(); // reads SYNAP_API_KEY from the environment await sdk.initialize(); ``` ```typescript TypeScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient(); // reads SYNAP_API_KEY from the environment await sdk.initialize(); ``` Not one per user, not one per session. Scope travels on each call, not on the stream. ```python Python theme={null} await sdk.instance.listen( on_reconnect=lambda attempt: log.info("stream reconnected (%d)", attempt), on_disconnect=lambda reason: log.warning("stream lost: %s", reason), ) ``` ```javascript JavaScript theme={null} // Node only, and needs the optional peers: // npm install @grpc/grpc-js @grpc/proto-loader await sdk.instance.listen({ on_reconnect: (attempt) => console.info('stream reconnected (%d)', attempt), on_disconnect: (reason) => console.warn('stream lost: %s', reason), }); ``` ```typescript TypeScript theme={null} // Node only, and needs the optional peers: // npm install @grpc/grpc-js @grpc/proto-loader await sdk.instance.listen({ on_reconnect: (attempt: number) => console.info('stream reconnected (%d)', attempt), on_disconnect: (reason: string) => console.warn('stream lost: %s', reason), }); ``` **This step is the one JavaScript restriction.** Every other call in this loop runs on Edge and Workers; `listen()` is gRPC, so it needs Node plus `@grpc/grpc-js` and `@grpc/proto-loader`. Importing the SDK in an Edge route stays safe, only the stream is unavailable there. See [Real-Time Anticipation in a Server](/patterns/real-time-anticipation-server) for quotas, reconnects, and the health check worth alerting on. ```python Python theme={null} import uuid conversation_id = str(uuid.uuid4()) # one per conversation, reused every turn async def handle_turn(user_text: str, user_id: str, customer_id: str) -> str: await sdk.instance.send_message( content=user_text, role="user", event_type="user_message", conversation_id=conversation_id, user_id=user_id, customer_id=customer_id, ) context = await sdk.user.context.fetch( user_id=user_id, customer_id=customer_id, conversation_id=conversation_id, search_query=[user_text], ) reply = await your_llm(context, user_text) await sdk.instance.send_message( content=reply, role="assistant", event_type="assistant_message", conversation_id=conversation_id, user_id=user_id, customer_id=customer_id, ) return reply ``` ```javascript JavaScript theme={null} import { randomUUID } from 'node:crypto'; const conversationId = randomUUID(); // one per conversation, reused every turn async function handleTurn(userText, userId, customerId) { await sdk.instance.send_message({ content: userText, role: 'user', event_type: 'user_message', conversation_id: conversationId, user_id: userId, customer_id: customerId, }); const context = await sdk.user.context.fetch({ user_id: userId, customer_id: customerId, conversation_id: conversationId, search_query: [userText], }); const reply = await yourLlm(context, userText); await sdk.instance.send_message({ content: reply, role: 'assistant', event_type: 'assistant_message', conversation_id: conversationId, user_id: userId, customer_id: customerId, }); return reply; } ``` ```typescript TypeScript theme={null} import { randomUUID } from 'node:crypto'; import type { RawContext } from '@maximem/synap-js-sdk'; declare function yourLlm(context: RawContext, userText: string): Promise; const conversationId = randomUUID(); // one per conversation, reused every turn async function handleTurn( userText: string, userId: string, customerId: string, ): Promise { await sdk.instance.send_message({ content: userText, role: 'user', event_type: 'user_message', conversation_id: conversationId, user_id: userId, customer_id: customerId, }); const context = await sdk.user.context.fetch({ user_id: userId, customer_id: customerId, conversation_id: conversationId, search_query: [userText], }); const reply = await yourLlm(context, userText); await sdk.instance.send_message({ content: reply, role: 'assistant', event_type: 'assistant_message', conversation_id: conversationId, user_id: userId, customer_id: customerId, }); return reply; } ``` Emit `assistant_message` **after** the reply. Anticipation runs between turns, so this event is what pre-warms the next one. ```python Python theme={null} await sdk.instance.stop_listening() await sdk.shutdown() ``` ```javascript JavaScript theme={null} await sdk.instance.stop_listening(); await sdk.shutdown(); ``` ```typescript TypeScript theme={null} await sdk.instance.stop_listening(); await sdk.shutdown(); ``` ## Requirements **Every event needs both `user_id` and `customer_id`.** A conversation event missing either is discarded server-side with no client-visible error. The turn is never persisted, so it never becomes memory. On a B2C instance send `user_id` only: `customer_id` is not accepted there, and an event without one is persisted normally. The stream must stay open across turns. This integration suits servers, workers, and voice sessions, not per-request serverless functions, which cannot hold a connection. Stream quotas are per Instance and per client. Opening one per user session exhausts them under real concurrency. Reused across every turn of a conversation. `send_message()` does not validate it, but `fetch()` and every other call do. ## What still uses REST The stream is not the entire transport, and it is not meant to be. Two things go over HTTP: * **`initialize()`**: resolves your client and instance from the API key * **`fetch()` on an anticipation miss**: pushed bundles cover what Synap predicted; anything it did not predict is fetched normally You do not code either differently. `fetch()` is one call whether it resolves from the local anticipation cache in about a millisecond or falls through to the network. ## When to still call `memories.create()` Promotion is automatic but not instant. A turn becomes retrievable when its conversation compacts, which for a quiet conversation means about five minutes. Reach for explicit ingestion in three cases: A fact the very next turn depends on, or a live demo showing memory forming. Waiting for compaction is not an option. Promotion ingests conversation turns as conversation content. To set `document_type`, `mode`, custom metadata, or to write at customer or client scope, ingest explicitly. Product docs, support tickets, CRM records, and backfills belong in [ingestion](/concepts/how-ingestion-works), not on the stream. Do not do both on the same text. If you report a turn with `send_message()` **and** ingest that same turn with `memories.create()`, the content is extracted twice, once immediately and once at compaction, costing double and producing overlapping memories the deduplication stage then reconciles. Pick one owner per piece of content: stream conversation turns, ingest everything else. ## Framework integrations Two packages drive the stream for you: [Strands Agents](/integrations/strands-agents) via `SynapStreamHook`, and the [Vercel AI SDK](/integrations/vercel-ai-sdk) via its model middleware. With any other framework, or none, use the loop above directly. It is plain SDK calls and composes with anything. The stream in depth: event types, cache behavior, and failure modes. Quotas, reconnects, and the silent failure to alert on. # Authentication Source: https://docs.maximem.ai/setup/authentication How to authenticate your SDK with Synap Cloud using API keys. ## Overview Synap uses **API keys** for all SDK authentication. Set your key and your instance id as environment variables and you are done. ```bash Linux / macOS theme={null} export SYNAP_API_KEY="synap_your_key_here" export SYNAP_INSTANCE_ID="inst_your_instance_id" ``` ```powershell Windows (PowerShell, session) theme={null} $env:SYNAP_API_KEY = "synap_your_key_here" $env:SYNAP_INSTANCE_ID = "inst_your_instance_id" ``` ```powershell Windows (PowerShell, persistent) theme={null} [System.Environment]::SetEnvironmentVariable("SYNAP_API_KEY", "synap_your_key_here", "User") [System.Environment]::SetEnvironmentVariable("SYNAP_INSTANCE_ID", "inst_your_instance_id", "User") ``` ```ini .env file (with python-dotenv) theme={null} SYNAP_API_KEY=synap_your_key_here SYNAP_INSTANCE_ID=inst_your_instance_id ``` The SDK authenticates every request to Synap Cloud using your API key. `SYNAP_INSTANCE_ID` is optional, and the dashboard gives you both together so you can paste them in one go. Set the instance id as an **environment variable**, not as a constructor argument. `SYNAP_INSTANCE_ID` records which instance you are on and leaves the SDK keyed on your credential. Passing `instance_id=` to `MaximemSynapSDK(...)` is different: it makes the id the identity, so a second key used under it is silently discarded and key rotation stops taking effect. See [Singleton Pattern](/sdk/initialization#singleton-pattern). ## Getting your API key 1. Log in to the [Synap Dashboard](https://synap.maximem.ai) 2. Navigate to your instance 3. Click **API Keys** in the instance detail page 4. Click **Generate API Key**, give it a label, and copy the key The key is displayed **only once**. Copy it immediately. If you lose it, revoke it and generate a new one. API keys start with `synap_` and look like this: ``` synap_Bx7Kp2mN9vQ4rT6wY8zA1cE3fG5hJ7kLm0pR2sT4uV6wX8yZ0aB1cD3eF5g ``` ## Using the API key ### Option 1: Environment variable (recommended) Set `SYNAP_API_KEY` and `SYNAP_INSTANCE_ID`: ```bash Linux / macOS theme={null} export SYNAP_API_KEY="synap_your_key_here" export SYNAP_INSTANCE_ID="inst_your_instance_id" ``` ```powershell Windows (PowerShell, session) theme={null} $env:SYNAP_API_KEY = "synap_your_key_here" $env:SYNAP_INSTANCE_ID = "inst_your_instance_id" ``` ```powershell Windows (PowerShell, persistent) theme={null} [System.Environment]::SetEnvironmentVariable("SYNAP_API_KEY", "synap_your_key_here", "User") [System.Environment]::SetEnvironmentVariable("SYNAP_INSTANCE_ID", "inst_your_instance_id", "User") ``` ```ini .env file (with python-dotenv) theme={null} SYNAP_API_KEY=synap_your_key_here SYNAP_INSTANCE_ID=inst_your_instance_id ``` The SDK reads these automatically: ```python Python theme={null} from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK() await sdk.initialize() ``` ```javascript JavaScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient(); await sdk.initialize(); ``` ```typescript TypeScript theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; const sdk = new SynapClient(); await sdk.initialize(); ``` This is the recommended approach for all environments: local development, CI/CD, Docker, Kubernetes, Vercel, AWS Lambda. ### Option 2: Constructor parameter Pass the key directly: ```python Python theme={null} sdk = MaximemSynapSDK( api_key="synap_your_key_here" ) await sdk.initialize() ``` ```javascript JavaScript theme={null} const sdk = new SynapClient({ apiKey: 'synap_your_key_here', }); await sdk.initialize(); ``` ```typescript TypeScript theme={null} const sdk = new SynapClient({ apiKey: 'synap_your_key_here', }); await sdk.initialize(); ``` ## Priority order When `initialize()` is called, the SDK resolves credentials in this order: 1. The constructor parameter (`api_key=` in Python, `apiKey` in JavaScript) 2. `SYNAP_API_KEY` environment variable The first one that succeeds wins. If neither is available, `initialize()` raises an `AuthenticationError`. The instance id resolves the same way (`instance_id=` / `instanceId`, then `SYNAP_INSTANCE_ID`), but it is optional: if neither is set, `initialize()` resolves it from your API key. Setting it changes nothing about which instance you reach, since the key already determines that. ## Multiple keys per instance You can generate multiple API keys for the same instance. Each key has a label and can be revoked independently. Common patterns: | Label | Usage | | ------------ | --------------------------- | | `production` | Your production servers | | `staging` | Staging/preview environment | | `ci` | CI/CD pipeline | | `dev-alice` | Developer's local machine | | `dev-bob` | Another developer | Revoke any key without affecting the others. These patterns each put one key in one process, which is what you want. If a single process uses two keys that belong to the same instance, you get two independent SDKs rather than one shared one, and `initialize()` warns you about it. See [Two keys, one instance](/sdk/initialization#two-keys-one-instance). In JavaScript, credentials resolve identically and the SDK reads `SYNAP_API_KEY` from `process.env` on every runtime that exposes it, Edge and Workers included. See [Where it runs](/setup/installation#where-it-runs). ## Security best practices Use `.env` files (added to `.gitignore`) or your platform's secrets manager. GitHub's secret scanning will flag leaked `synap_` keys automatically. Generate a different key for development, staging, CI, and production. If one leaks, revoke only that key; the others continue working. Generate a new key, update your environment, verify it works, then revoke the old one. There's no expiry deadline; rotate on your own schedule. API keys work everywhere: Vercel, AWS Lambda, Cloudflare Workers, Docker, Kubernetes. Set `SYNAP_API_KEY` in your platform's environment configuration and you're done. No file I/O, no extra setup step. ## Troubleshooting | Error | Cause | Fix | | --------------------------------------------- | ----------------------------------- | ------------------------------------------------------------- | | `AuthenticationError: No Synap API key found` | SDK can't find credentials anywhere | Set `SYNAP_API_KEY` env var or pass `api_key=` to constructor | | `AuthenticationError: Invalid credentials` | Key is wrong, revoked, or malformed | Check the key in your dashboard; is it active? | # Integration Reference Source: https://docs.maximem.ai/setup/detailed-integration The deeper end-to-end integration walkthrough: framework matrix (FastAPI / Flask / Django / Next.js), LLM-provider variants, scope strategy, real-time streaming, and going to production. This page is the **framework-and-provider cookbook**: drop-in snippets for FastAPI, Flask, Next.js, Django, OpenAI, Anthropic, Google Gemini (free tier), and the Vercel AI SDK. It assumes you've already finished the [Quickstart](/getting-started/quickstart) and have an API key. If you want a fully-worked walkthrough that builds one application end-to-end with conversation routing, error handling, and graceful degradation, follow [First Integration](/setup/first-integration) instead, then come back here when you need to swap frameworks or LLM providers. ## Overview This guide walks you through integrating Synap into your application. You will learn how to initialize the SDK in popular Python web frameworks, bridge async/sync execution models, and wire Synap into your LLM provider's generation pipeline. By the end of this page, your application will have a working memory-augmented agent pattern: retrieve context from Synap, inject it into the LLM prompt, generate a response, and ingest the conversation back into Synap. ## Framework Integration FastAPI is the most common framework for Synap integrations. Use the `lifespan` event for initialization and shutdown, and access the SDK instance from your route handlers. ```python theme={null} from contextlib import asynccontextmanager from fastapi import FastAPI, Depends, Request from maximem_synap import MaximemSynapSDK @asynccontextmanager async def lifespan(app: FastAPI): sdk = MaximemSynapSDK() # reads SYNAP_API_KEY from env await sdk.initialize() app.state.sdk = sdk try: yield finally: await sdk.shutdown() app = FastAPI(lifespan=lifespan) def get_sdk(request: Request) -> MaximemSynapSDK: """Dependency that provides the initialized SDK from app.state.""" return request.app.state.sdk @app.post("/chat") async def chat( message: str, user_id: str, customer_id: str, conversation_id: str, synap: MaximemSynapSDK = Depends(get_sdk) ): # 1. Retrieve relevant context context = await synap.conversation.context.fetch( conversation_id=conversation_id, search_query=[message] ) # 2. Build prompt with retrieved memories memories = ( context.facts + context.preferences + context.episodes + context.emotions + context.temporal_events ) system_prompt = build_system_prompt(memories) # 3. Call your LLM (see LLM integration below) response = await generate_response(system_prompt, message) # 4. Ingest the turn for long-term memory await synap.memories.create( document=f"User: {message}\nAssistant: {response}", document_type="ai-chat-conversation", user_id=user_id, customer_id=customer_id, metadata={"conversation_id": conversation_id} ) return {"response": response} ``` Storing the SDK on `app.state` (instead of a module-level global) keeps it scoped to the FastAPI app instance, which makes integration tests trivial: `TestClient(app)` creates a fresh app with its own SDK, and you can replace `app.state.sdk` with a mock before each test. The `Depends(get_sdk)` indirection then lets you override the SDK per-test via `app.dependency_overrides[get_sdk] = lambda: mock_sdk`. Flask is synchronous by default, so you need to bridge to Synap's async SDK. Use the app factory pattern and initialize the SDK at startup. ```python theme={null} import asyncio from flask import Flask, request, g from maximem_synap import MaximemSynapSDK sdk = None loop = None def create_app(): global sdk, loop app = Flask(__name__) # Create a dedicated event loop for async operations loop = asyncio.new_event_loop() # Initialize SDK synchronously at startup sdk = MaximemSynapSDK() # reads SYNAP_API_KEY from env loop.run_until_complete(sdk.initialize()) @app.teardown_appcontext def shutdown_sdk(exception=None): pass # Handled by atexit import atexit @atexit.register def cleanup(): if sdk and loop: loop.run_until_complete(sdk.shutdown()) loop.close() @app.route("/chat", methods=["POST"]) def chat(): data = request.json message = data["message"] user_id = data["user_id"] customer_id = data["customer_id"] conversation_id = data["conversation_id"] # Bridge async SDK calls to sync Flask handlers context = loop.run_until_complete( sdk.conversation.context.fetch( conversation_id=conversation_id, search_query=[message] ) ) memories = ( context.facts + context.preferences + context.episodes + context.emotions + context.temporal_events ) system_prompt = build_system_prompt(memories) response = generate_response(system_prompt, message) loop.run_until_complete( sdk.memories.create( document=f"User: {message}\nAssistant: {response}", document_type="ai-chat-conversation", user_id=user_id, customer_id=customer_id, metadata={"conversation_id": conversation_id} ) ) return {"response": response} return app ``` Using `loop.run_until_complete()` in Flask blocks the worker thread during async operations. For production Flask deployments with high concurrency, consider migrating to FastAPI or running the async SDK calls in a thread pool. Initialize once at process startup and share the client across requests. The client is safe to reuse: constructing a second one on the same identity returns the first, so an accidental second `new SynapClient()` does not open a second cache. ```javascript theme={null} import express from 'express'; import { SynapClient, isSynapError } from '@maximem/synap-js-sdk'; const app = express(); app.use(express.json()); const sdk = new SynapClient(); // reads SYNAP_API_KEY from the environment await sdk.initialize(); app.post('/chat', async (req, res) => { const { message, userId, customerId, conversationId } = req.body; let memories = []; try { const context = await sdk.conversation.context.fetch({ conversation_id: conversationId, search_query: [message], }); memories = context.facts ?? []; } catch (e) { if (!isSynapError(e)) throw e; // only degrade on Synap's own errors req.log?.warn(`Synap unavailable: ${e.code}`); } const reply = await yourLlm(memories, message); await sdk.memories.create({ document: `User: ${message}\nAssistant: ${reply}`, document_type: 'ai-chat-conversation', user_id: userId, customer_id: customerId, metadata: { conversation_id: conversationId }, }); res.json({ reply }); }); // Flush in-flight work before the process exits. process.on('SIGTERM', async () => { await sdk.shutdown(); process.exit(0); }); app.listen(3000); ``` The SDK does not register its own `process.on('exit')` handler: it would leak a listener per client, never fire in Lambda, and `process` does not exist in Workers. Call `shutdown()` yourself, as above. The Synap SDK's HTTP paths run anywhere, Edge and Workers included. The real-time anticipation stream is gRPC and needs Node, so pin any route that calls `listen()` to the Node.js runtime. See [Where it runs](/setup/installation#where-it-runs). Use `instrumentation.ts` to initialize the provider once at server startup. The `synap.wrap()` call in route handlers is lightweight; the provider instance is shared across requests. ```typescript theme={null} // instrumentation.ts import { createSynap } from "@maximem/synap-vercel-adk"; let _synap: Awaited> | null = null; export async function register() { if (process.env.NEXT_RUNTIME === "nodejs") { _synap = await createSynap({ apiKey: process.env.SYNAP_API_KEY }); await _synap.listen(); // optional: open real-time anticipation stream } } export function getSynap() { if (!_synap) throw new Error("Synap not initialized"); return _synap; } ``` `listen()` warms the anticipation cache so retrieval resolves locally. Your memory writes come from `synap.wrap()`, which ingests each turn automatically; `listen()` is not a substitute for it. Note that the middleware also streams each turn as a conversation event, so the same content is extracted again when the conversation compacts. See [Real-Time Anticipation](/concepts/real-time-anticipation#what-the-stream-does-to-memory). ```typescript theme={null} // app/api/chat/route.ts import { streamText } from "ai"; import { anthropic } from "@ai-sdk/anthropic"; import { getSynap } from "@/instrumentation"; export const runtime = "nodejs"; // required only because instrumentation.ts calls listen() export async function POST(req: Request) { const { messages, userId, conversationId } = await req.json(); const model = getSynap().wrap(anthropic("claude-sonnet-4-6"), { userId, conversationId, }); const result = streamText({ model, messages }); return result.toDataStreamResponse(); } ``` In Django, initialize the SDK in your `AppConfig.ready()` method. Use `asgiref.sync_to_async` for bridging in async views, or `asyncio.run()` for synchronous views. ```python theme={null} # myapp/apps.py import asyncio from django.apps import AppConfig class MyAppConfig(AppConfig): name = "myapp" def ready(self): from maximem_synap import MaximemSynapSDK # Store SDK instance on the app config self.sdk = MaximemSynapSDK() # reads SYNAP_API_KEY from env loop = asyncio.new_event_loop() loop.run_until_complete(self.sdk.initialize()) self._loop = loop # myapp/views.py from django.apps import apps from django.http import JsonResponse from django.views.decorators.http import require_POST import json def get_sdk(): return apps.get_app_config("myapp").sdk def get_loop(): return apps.get_app_config("myapp")._loop @require_POST def chat(request): data = json.loads(request.body) sdk = get_sdk() loop = get_loop() context = loop.run_until_complete( sdk.conversation.context.fetch( conversation_id=data["conversation_id"], search_query=[data["message"]] ) ) memories = ( context.facts + context.preferences + context.episodes + context.emotions + context.temporal_events ) system_prompt = build_system_prompt(memories) response = generate_response(system_prompt, data["message"]) loop.run_until_complete( sdk.memories.create( document=f"User: {data['message']}\nAssistant: {response}", document_type="ai-chat-conversation", user_id=data["user_id"], customer_id=data["customer_id"], metadata={"conversation_id": data["conversation_id"]} ) ) return JsonResponse({"response": response}) ``` For Django with ASGI (async views): ```python theme={null} # myapp/views.py (async version) from django.http import JsonResponse import json async def chat(request): data = json.loads(request.body) sdk = get_sdk() # No bridging needed: SDK is natively async context = await sdk.conversation.context.fetch( conversation_id=data["conversation_id"], search_query=[data["message"]] ) # ... rest of the handler ``` Using `loop.run_until_complete()` in Django blocks the worker thread during async operations. For production Django deployments with high concurrency, prefer the ASGI/async views pattern shown above, or run the async SDK calls in a thread pool. Be aware that long-lived event loops can interact poorly with dev-server auto-reload and ASGI worker lifecycles. ## Async/Sync Bridging Python only. The JavaScript SDK returns promises throughout and there is no synchronous surface to bridge. The Synap SDK is async-native. If your application uses a synchronous framework, you need to bridge the async calls. Here are the recommended patterns: Use for scripts, CLI tools, and simple synchronous applications. Creates a new event loop for each call. ```python theme={null} import asyncio from maximem_synap import MaximemSynapSDK sdk = MaximemSynapSDK() # reads SYNAP_API_KEY from env asyncio.run(sdk.initialize()) # Later, in synchronous code: context = asyncio.run(sdk.conversation.context.fetch(...)) ``` `asyncio.run()` creates a new event loop each time. This is fine for low-frequency calls but adds overhead for high-throughput applications. Create a single event loop at startup and reuse it for all SDK calls. This is the pattern shown in the Flask and Django examples above. ```python theme={null} import asyncio # At startup loop = asyncio.new_event_loop() # For each call result = loop.run_until_complete(sdk.some_async_method()) ``` If you are using Django's async views alongside sync code, `asgiref` provides utilities for bridging: ```python theme={null} from asgiref.sync import async_to_sync # Wrap an async SDK method for use in sync code fetch_context = async_to_sync(sdk.conversation.context.fetch) context = fetch_context(conversation_id="3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c", ...) # conversation_id must be a valid UUID ``` ## LLM Provider Integration Full example of a memory-augmented agent using OpenAI's `gpt-4o`: ```python Python theme={null} from openai import AsyncOpenAI from maximem_synap import MaximemSynapSDK openai_client = AsyncOpenAI(api_key="sk-...") synap_sdk = MaximemSynapSDK() # reads SYNAP_API_KEY from env async def memory_augmented_chat( user_message: str, user_id: str, customer_id: str, conversation_id: str ) -> str: # Step 1: Retrieve relevant context from Synap context = await synap_sdk.conversation.context.fetch( conversation_id=conversation_id, search_query=[user_message] ) # Step 2: Build the system prompt with retrieved memories # Each memory type names its text field differently: facts, preferences # and temporal events use `content`, episodes use `summary`, emotions # use `context`. Reading `.content` across all five raises # AttributeError on the first episode. def memory_text(item) -> str: return getattr(item, "content", None) or getattr(item, "summary", None) \ or getattr(item, "context", "") memories = ( context.facts + context.preferences + context.episodes + context.emotions + context.temporal_events ) memory_block = "\n".join([ f"- {memory_text(memory)}" for memory in memories ]) system_prompt = f"""You are a helpful assistant with access to the following information about the user and their organization: {memory_block} Use this information to provide personalized, contextual responses. If the user's question relates to something in the memories above, reference it naturally in your response.""" # Step 3: Call OpenAI with the enriched prompt response = await openai_client.chat.completions.create( model="gpt-4o", messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_message} ], temperature=0.7 ) assistant_message = response.choices[0].message.content # Step 4: Ingest the conversation turn for long-term memory await synap_sdk.memories.create( document=f"User: {user_message}\nAssistant: {assistant_message}", document_type="ai-chat-conversation", user_id=user_id, customer_id=customer_id, metadata={ "conversation_id": conversation_id, "model": "gpt-4o", "source": "chat" } ) return assistant_message ``` ```javascript JavaScript theme={null} import OpenAI from 'openai'; import { SynapClient, flattenContextItems } from '@maximem/synap-js-sdk'; const openaiClient = new OpenAI({ apiKey: 'sk-...' }); const synapSdk = new SynapClient(); // reads SYNAP_API_KEY from env async function memoryAugmentedChat(userMessage, userId, customerId, conversationId) { // Step 1: Retrieve relevant context from Synap const context = await synapSdk.conversation.context.fetch({ conversation_id: conversationId, search_query: [userMessage], }); // Step 2: Build the system prompt with retrieved memories const memories = flattenContextItems(context); // flattenContextItems normalises each type's text field into `.memory`, // so episodes and emotions are not silently blank. const memoryBlock = memories.map((m) => `- ${m.memory}`).join('\n'); const systemPrompt = `You are a helpful assistant with access to the following information about the user and their organization: ${memoryBlock} Use this information to provide personalized, contextual responses. If the user's question relates to something in the memories above, reference it naturally in your response.`; // Step 3: Call OpenAI with the enriched prompt const response = await openaiClient.chat.completions.create({ model: 'gpt-4o', messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: userMessage }, ], temperature: 0.7, }); const assistantMessage = response.choices[0]?.message.content ?? ''; // Step 4: Ingest the conversation turn for long-term memory await synapSdk.memories.create({ document: `User: ${userMessage}\nAssistant: ${assistantMessage}`, document_type: 'ai-chat-conversation', user_id: userId, customer_id: customerId, metadata: { conversation_id: conversationId, model: 'gpt-4o', source: 'chat', }, }); return assistantMessage; } ``` ```typescript TypeScript theme={null} import OpenAI from 'openai'; import { SynapClient, flattenContextItems } from '@maximem/synap-js-sdk'; const openaiClient = new OpenAI({ apiKey: 'sk-...' }); const synapSdk = new SynapClient(); // reads SYNAP_API_KEY from env async function memoryAugmentedChat( userMessage: string, userId: string, customerId: string, conversationId: string, ): Promise { // Step 1: Retrieve relevant context from Synap const context = await synapSdk.conversation.context.fetch({ conversation_id: conversationId, search_query: [userMessage], }); // Step 2: Build the system prompt with retrieved memories const memories = flattenContextItems(context); // flattenContextItems normalises each type's text field into `.memory`, // so episodes and emotions are not silently blank. const memoryBlock = memories.map((m) => `- ${m.memory}`).join('\n'); const systemPrompt = `You are a helpful assistant with access to the following information about the user and their organization: ${memoryBlock} Use this information to provide personalized, contextual responses. If the user's question relates to something in the memories above, reference it naturally in your response.`; // Step 3: Call OpenAI with the enriched prompt const response = await openaiClient.chat.completions.create({ model: 'gpt-4o', messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: userMessage }, ], temperature: 0.7, }); const assistantMessage = response.choices[0]?.message.content ?? ''; // Step 4: Ingest the conversation turn for long-term memory await synapSdk.memories.create({ document: `User: ${userMessage}\nAssistant: ${assistantMessage}`, document_type: 'ai-chat-conversation', user_id: userId, customer_id: customerId, metadata: { conversation_id: conversationId, model: 'gpt-4o', source: 'chat', }, }); return assistantMessage; } ``` Full example of a memory-augmented agent using Anthropic's Claude: ```python Python theme={null} from anthropic import AsyncAnthropic from maximem_synap import MaximemSynapSDK anthropic_client = AsyncAnthropic(api_key="sk-ant-...") synap_sdk = MaximemSynapSDK() # reads SYNAP_API_KEY from env async def memory_augmented_chat( user_message: str, user_id: str, customer_id: str, conversation_id: str ) -> str: # Step 1: Retrieve relevant context from Synap context = await synap_sdk.conversation.context.fetch( conversation_id=conversation_id, search_query=[user_message] ) # Step 2: Build the system prompt with retrieved memories # Each memory type names its text field differently: facts, preferences # and temporal events use `content`, episodes use `summary`, emotions # use `context`. Reading `.content` across all five raises # AttributeError on the first episode. def memory_text(item) -> str: return getattr(item, "content", None) or getattr(item, "summary", None) \ or getattr(item, "context", "") memories = ( context.facts + context.preferences + context.episodes + context.emotions + context.temporal_events ) memory_block = "\n".join([ f"- {memory_text(memory)}" for memory in memories ]) system_prompt = f"""You are a helpful assistant with access to the following information about the user and their organization: {memory_block} Use this information to provide personalized, contextual responses. Reference relevant memories naturally when they apply to the user's question.""" # Step 3: Call Claude with the enriched prompt response = await anthropic_client.messages.create( model="claude-sonnet-4-6", max_tokens=1024, system=system_prompt, messages=[ {"role": "user", "content": user_message} ] ) assistant_message = response.content[0].text # Step 4: Ingest the conversation turn for long-term memory await synap_sdk.memories.create( document=f"User: {user_message}\nAssistant: {assistant_message}", document_type="ai-chat-conversation", user_id=user_id, customer_id=customer_id, metadata={ "conversation_id": conversation_id, "model": "claude-sonnet-4-6", "source": "chat" } ) return assistant_message ``` ```javascript JavaScript theme={null} import Anthropic from '@anthropic-ai/sdk'; import { SynapClient, flattenContextItems } from '@maximem/synap-js-sdk'; const anthropicClient = new Anthropic({ apiKey: 'sk-ant-...' }); const synapSdk = new SynapClient(); // reads SYNAP_API_KEY from env async function memoryAugmentedChat(userMessage, userId, customerId, conversationId) { // Step 1: Retrieve relevant context from Synap const context = await synapSdk.conversation.context.fetch({ conversation_id: conversationId, search_query: [userMessage], }); // Step 2: Build the system prompt with retrieved memories. // flattenContextItems normalises each type's text field into `.memory`, // so episodes and emotions are not silently blank. const memoryBlock = flattenContextItems(context) .map((m) => `- ${m.memory}`) .join('\n'); const systemPrompt = `You are a helpful assistant with access to the following information about the user and their organization: ${memoryBlock} Use this information to provide personalized, contextual responses.`; // Step 3: Call the model with the enriched prompt const response = await anthropicClient.messages.create({ model: 'claude-sonnet-4-6', max_tokens: 1024, system: systemPrompt, messages: [{ role: 'user', content: userMessage }], }); const assistantMessage = response.content[0]?.type === 'text' ? response.content[0].text : ''; // Step 4: Ingest the conversation turn for long-term memory await synapSdk.memories.create({ document: `User: ${userMessage}\nAssistant: ${assistantMessage}`, document_type: 'ai-chat-conversation', user_id: userId, customer_id: customerId, metadata: { conversation_id: conversationId, model: 'claude-sonnet-4-6', source: 'chat', }, }); return assistantMessage; } ``` ```typescript TypeScript theme={null} import Anthropic from '@anthropic-ai/sdk'; import { SynapClient, flattenContextItems } from '@maximem/synap-js-sdk'; const anthropicClient = new Anthropic({ apiKey: 'sk-ant-...' }); const synapSdk = new SynapClient(); // reads SYNAP_API_KEY from env async function memoryAugmentedChat( userMessage: string, userId: string, customerId: string, conversationId: string, ): Promise { // Step 1: Retrieve relevant context from Synap const context = await synapSdk.conversation.context.fetch({ conversation_id: conversationId, search_query: [userMessage], }); // Step 2: Build the system prompt with retrieved memories. // flattenContextItems normalises each type's text field into `.memory`, // so episodes and emotions are not silently blank. const memoryBlock = flattenContextItems(context) .map((m) => `- ${m.memory}`) .join('\n'); const systemPrompt = `You are a helpful assistant with access to the following information about the user and their organization: ${memoryBlock} Use this information to provide personalized, contextual responses.`; // Step 3: Call the model with the enriched prompt const response = await anthropicClient.messages.create({ model: 'claude-sonnet-4-6', max_tokens: 1024, system: systemPrompt, messages: [{ role: 'user', content: userMessage }], }); const assistantMessage = response.content[0]?.type === 'text' ? response.content[0].text : ''; // Step 4: Ingest the conversation turn for long-term memory await synapSdk.memories.create({ document: `User: ${userMessage}\nAssistant: ${assistantMessage}`, document_type: 'ai-chat-conversation', user_id: userId, customer_id: customerId, metadata: { conversation_id: conversationId, model: 'claude-sonnet-4-6', source: 'chat', }, }); return assistantMessage; } ``` Gemini has a **free API tier**: generate a key at [Google AI Studio](https://aistudio.google.com/apikey) with no billing setup. This lets you complete the Quickstart and test memory-augmented chat without an OpenAI or Anthropic account. Full example of a memory-augmented agent using Google's `gemini-2.0-flash`: ```python Python theme={null} from google import genai from google.genai import types from maximem_synap import MaximemSynapSDK gemini_client = genai.Client(api_key="...") # from Google AI Studio synap_sdk = MaximemSynapSDK() # reads SYNAP_API_KEY from env async def memory_augmented_chat( user_message: str, user_id: str, customer_id: str, conversation_id: str ) -> str: # Step 1: Retrieve relevant context from Synap context = await synap_sdk.conversation.context.fetch( conversation_id=conversation_id, search_query=[user_message] ) # Step 2: Build the system prompt with retrieved memories # Each memory type names its text field differently: facts, preferences # and temporal events use `content`, episodes use `summary`, emotions # use `context`. Reading `.content` across all five raises # AttributeError on the first episode. def memory_text(item) -> str: return getattr(item, "content", None) or getattr(item, "summary", None) \ or getattr(item, "context", "") memories = ( context.facts + context.preferences + context.episodes + context.emotions + context.temporal_events ) memory_block = "\n".join([ f"- {memory_text(memory)}" for memory in memories ]) system_prompt = f"""You are a helpful assistant with access to the following information about the user and their organization: {memory_block} Use this information to provide personalized, contextual responses. Reference relevant memories naturally when they apply to the user's question.""" # Step 3: Call Gemini with the enriched prompt response = await gemini_client.aio.models.generate_content( model="gemini-2.0-flash", contents=user_message, config=types.GenerateContentConfig( system_instruction=system_prompt, temperature=0.7 ) ) assistant_message = response.text # Step 4: Ingest the conversation turn for long-term memory await synap_sdk.memories.create( document=f"User: {user_message}\nAssistant: {assistant_message}", document_type="ai-chat-conversation", user_id=user_id, customer_id=customer_id, metadata={ "conversation_id": conversation_id, "model": "gemini-2.0-flash", "source": "chat" } ) return assistant_message ``` ```javascript JavaScript theme={null} import { GoogleGenerativeAI } from '@google/generative-ai'; import { SynapClient, flattenContextItems } from '@maximem/synap-js-sdk'; const genai = new GoogleGenerativeAI(process.env.GOOGLE_API_KEY ?? ''); const synapSdk = new SynapClient(); // reads SYNAP_API_KEY from env async function memoryAugmentedChat(userMessage, userId, customerId, conversationId) { // Step 1: Retrieve relevant context from Synap const context = await synapSdk.conversation.context.fetch({ conversation_id: conversationId, search_query: [userMessage], }); // Step 2: Build the system prompt with retrieved memories. // flattenContextItems normalises each type's text field into `.memory`, // so episodes and emotions are not silently blank. const memoryBlock = flattenContextItems(context) .map((m) => `- ${m.memory}`) .join('\n'); const systemPrompt = `You are a helpful assistant with access to the following information about the user and their organization: ${memoryBlock} Use this information to provide personalized, contextual responses.`; // Step 3: Call the model with the enriched prompt const model = genai.getGenerativeModel({ model: 'gemini-2.0-flash', systemInstruction: systemPrompt, }); const response = await model.generateContent(userMessage); const assistantMessage = response.response.text(); // Step 4: Ingest the conversation turn for long-term memory await synapSdk.memories.create({ document: `User: ${userMessage}\nAssistant: ${assistantMessage}`, document_type: 'ai-chat-conversation', user_id: userId, customer_id: customerId, metadata: { conversation_id: conversationId, model: 'gemini-2.0-flash', source: 'chat', }, }); return assistantMessage; } ``` ```typescript TypeScript theme={null} import { GoogleGenerativeAI } from '@google/generative-ai'; import { SynapClient, flattenContextItems } from '@maximem/synap-js-sdk'; const genai = new GoogleGenerativeAI(process.env.GOOGLE_API_KEY ?? ''); const synapSdk = new SynapClient(); // reads SYNAP_API_KEY from env async function memoryAugmentedChat( userMessage: string, userId: string, customerId: string, conversationId: string, ): Promise { // Step 1: Retrieve relevant context from Synap const context = await synapSdk.conversation.context.fetch({ conversation_id: conversationId, search_query: [userMessage], }); // Step 2: Build the system prompt with retrieved memories. // flattenContextItems normalises each type's text field into `.memory`, // so episodes and emotions are not silently blank. const memoryBlock = flattenContextItems(context) .map((m) => `- ${m.memory}`) .join('\n'); const systemPrompt = `You are a helpful assistant with access to the following information about the user and their organization: ${memoryBlock} Use this information to provide personalized, contextual responses.`; // Step 3: Call the model with the enriched prompt const model = genai.getGenerativeModel({ model: 'gemini-2.0-flash', systemInstruction: systemPrompt, }); const response = await model.generateContent(userMessage); const assistantMessage = response.response.text(); // Step 4: Ingest the conversation turn for long-term memory await synapSdk.memories.create({ document: `User: ${userMessage}\nAssistant: ${assistantMessage}`, document_type: 'ai-chat-conversation', user_id: userId, customer_id: customerId, metadata: { conversation_id: conversationId, model: 'gemini-2.0-flash', source: 'chat', }, }); return assistantMessage; } ``` If your application uses the [Vercel AI SDK](https://sdk.vercel.ai), `@maximem/synap-vercel-adk` wraps any `LanguageModelV1` model as a middleware. Context retrieval and memory writes happen automatically, with no manual fetch/inject/ingest loop needed. Retrieval and ingestion run anywhere, Edge and Workers included. Only `listen()` is Node-only, because the anticipation stream is gRPC. See [Where it runs](/setup/installation#where-it-runs). **Setup (run once at startup)** ```typescript theme={null} // instrumentation.ts (Next.js) or top-level module import { createSynap } from "@maximem/synap-vercel-adk"; export const synap = await createSynap({ apiKey: process.env.SYNAP_API_KEY, }); // Optional: open real-time anticipation cache updates await synap.listen(); ``` `listen()` warms the anticipation cache; memories are written by `synap.wrap()`, which ingests each turn automatically. The middleware also streams each turn as a conversation event, and those turns are extracted again when the conversation compacts. See [Real-Time Anticipation](/concepts/real-time-anticipation#what-the-stream-does-to-memory). **Usage in a route handler or server action** ```typescript theme={null} import { generateText, streamText } from "ai"; import { anthropic } from "@ai-sdk/anthropic"; import { synap } from "@/lib/synap"; // conversationId must be a valid UUID; generate one with crypto.randomUUID() const model = synap.wrap(anthropic("claude-sonnet-4-6"), { userId: "user-123", conversationId: crypto.randomUUID(), }); // generateText: context injected, memory written automatically const { text } = await generateText({ model, messages: [{ role: "user", content: userMessage }], }); // streamText works identically const result = streamText({ model, messages }); ``` **Next.js App Router (streaming)** ```typescript theme={null} // app/api/chat/route.ts import { streamText } from "ai"; import { anthropic } from "@ai-sdk/anthropic"; import { synap } from "@/lib/synap"; export async function POST(req: Request) { const { messages, userId, conversationId } = await req.json(); const model = synap.wrap(anthropic("claude-sonnet-4-6"), { userId, conversationId, }); const result = streamText({ model, messages }); return result.toDataStreamResponse(); } ``` `synap.wrap()` returns a standard `LanguageModelV1`; it is compatible with all Vercel AI SDK functions (`generateText`, `streamText`, `generateObject`, etc.) and any framework built on top of the AI SDK. For the `writeMemory` option (default `true`), the middleware fires a background memory write after each generation so it never adds latency to the response. Set `writeMemory: false` to disable. ## The Memory-Augmented Agent Pattern Every Synap integration follows the same fundamental pattern: Fetch relevant memories from Synap based on the user's message, identity, and conversation history. ```python Python theme={null} context = await sdk.conversation.context.fetch(...) ``` ```javascript JavaScript theme={null} const context = await sdk.conversation.context.fetch({ /* ... */ }); ``` ```typescript TypeScript theme={null} const context = await sdk.conversation.context.fetch({ /* ... */ }); ``` Inject retrieved memories into the system prompt or message history. This gives the LLM access to personalized, contextual information. ```python Python theme={null} memories = ( context.facts + context.preferences + context.episodes + context.emotions + context.temporal_events ) system_prompt = build_prompt_with_memories(memories) ``` ```javascript JavaScript theme={null} import { flattenContextItems } from '@maximem/synap-js-sdk'; // Concatenates every collection in the same order as the Python example. const memories = flattenContextItems(context); const systemPrompt = buildPromptWithMemories(memories); ``` ```typescript TypeScript theme={null} import { flattenContextItems } from '@maximem/synap-js-sdk'; // Concatenates every collection in the same order as the Python example. const memories = flattenContextItems(context); const systemPrompt = buildPromptWithMemories(memories); ``` Call your LLM provider with the enriched prompt. The model generates a response informed by the user's history, preferences, and organizational context. ```python Python theme={null} response = await llm_client.generate(system_prompt, user_message) ``` ```javascript JavaScript theme={null} const response = await llmClient.generate(systemPrompt, userMessage); ``` ```typescript TypeScript theme={null} const response = await llmClient.generate(systemPrompt, userMessage); ``` Send the conversation turn back to Synap for processing and long-term storage. This closes the loop: today's conversation becomes tomorrow's context. ```python Python theme={null} await sdk.memories.create(document=turn_content, document_type="ai-chat-conversation", user_id=..., customer_id=...) ``` ```javascript JavaScript theme={null} await sdk.memories.create({ document: turnContent, document_type: 'ai-chat-conversation', user_id: userId, customer_id: customerId, }); ``` ```typescript TypeScript theme={null} await sdk.memories.create({ document: turnContent, document_type: 'ai-chat-conversation', user_id: userId, customer_id: customerId, }); ``` ## Error Handling and Graceful Degradation Synap should enhance your application, not be a single point of failure. Design your integration to degrade gracefully when Synap is unavailable. **In JavaScript, branch on `error.code`, not `instanceof`.** With a dual ESM/CJS dependency graph a consumer can hold two copies of the same error class, and `instanceof` then fails against the copy it was not built from. `isSynapError(e)` narrows to Synap's own taxonomy, so a bug in your code rethrows instead of being swallowed as "Synap was down". See [How it differs from the Python SDK](/sdk/initialization#how-it-differs-from-the-python-sdk). ```python Python theme={null} async def chat_with_graceful_degradation(user_message: str, **kwargs) -> str: memories = [] # Attempt to retrieve context, but don't fail if Synap is down try: context = await synap_sdk.conversation.context.fetch( conversation_id=kwargs["conversation_id"], search_query=[user_message] ) memories = ( context.facts + context.preferences + context.episodes + context.emotions + context.temporal_events ) except Exception as e: logger.warning(f"Synap retrieval failed, proceeding without context: {e}") # Generate response (with or without memories) system_prompt = build_system_prompt(memories) # handles empty list gracefully response = await generate_response(system_prompt, user_message) # Attempt to ingest, but don't fail if Synap is down try: await synap_sdk.memories.create( document=f"User: {user_message}\nAssistant: {response}", document_type="ai-chat-conversation", user_id=kwargs["user_id"], customer_id=kwargs["customer_id"], metadata={"conversation_id": kwargs["conversation_id"]} ) except Exception as e: logger.warning(f"Synap ingestion failed: {e}") return response ``` ```javascript JavaScript theme={null} import { isSynapError, flattenContextItems } from '@maximem/synap-js-sdk'; async function chatWithGracefulDegradation(userMessage, opts) { let memories = []; // Attempt to retrieve context, but don't fail if Synap is down try { const context = await synapSdk.conversation.context.fetch({ conversation_id: opts.conversationId, search_query: [userMessage], }); memories = flattenContextItems(context); } catch (e) { // isSynapError narrows to the SDK's own taxonomy; anything else rethrows, // so a bug in your own code is not swallowed as "Synap was down". if (!isSynapError(e)) throw e; logger.warn(`Synap retrieval failed, proceeding without context: ${e.code}`); } // Generate response (with or without memories) const systemPrompt = buildSystemPrompt(memories); // handles empty list gracefully const response = await generateResponse(systemPrompt, userMessage); // Attempt to ingest, but don't fail if Synap is down try { await synapSdk.memories.create({ document: `User: ${userMessage}\nAssistant: ${response}`, document_type: 'ai-chat-conversation', user_id: opts.userId, customer_id: opts.customerId, metadata: { conversation_id: opts.conversationId }, }); } catch (e) { if (!isSynapError(e)) throw e; logger.warn(`Synap ingestion failed: ${e.code}`); } return response; } ``` ```typescript TypeScript theme={null} import { isSynapError, flattenContextItems } from '@maximem/synap-js-sdk'; import type { FlatMemory } from '@maximem/synap-js-sdk'; interface ChatOptions { conversationId: string; userId: string; customerId: string; } async function chatWithGracefulDegradation( userMessage: string, opts: ChatOptions, ): Promise { let memories: FlatMemory[] = []; // Attempt to retrieve context, but don't fail if Synap is down try { const context = await synapSdk.conversation.context.fetch({ conversation_id: opts.conversationId, search_query: [userMessage], }); memories = flattenContextItems(context); } catch (e) { // isSynapError narrows to the SDK's own taxonomy; anything else rethrows, // so a bug in your own code is not swallowed as "Synap was down". if (!isSynapError(e)) throw e; logger.warn(`Synap retrieval failed, proceeding without context: ${e.code}`); } // Generate response (with or without memories) const systemPrompt = buildSystemPrompt(memories); // handles empty list gracefully const response = await generateResponse(systemPrompt, userMessage); // Attempt to ingest, but don't fail if Synap is down try { await synapSdk.memories.create({ document: `User: ${userMessage}\nAssistant: ${response}`, document_type: 'ai-chat-conversation', user_id: opts.userId, customer_id: opts.customerId, metadata: { conversation_id: opts.conversationId }, }); } catch (e) { if (!isSynapError(e)) throw e; logger.warn(`Synap ingestion failed: ${e.code}`); } return response; } ``` Always wrap Synap SDK calls in `try` blocks in production (`try/except` in Python, `try/catch` in JavaScript). Network issues, rate limits, or service disruptions should not prevent your application from responding to users. The LLM can still generate useful responses without memory context; it just won't be personalized. ## Next Steps Configure API keys for secure communication. Detailed SDK configuration options. Step-by-step tutorial for your first Synap integration. # First Integration Source: https://docs.maximem.ai/setup/first-integration The opinionated, recommended integration path: an end-to-end FastAPI + OpenAI walkthrough that builds a memory-enabled chatbot from scratch. This is the **canonical end-to-end tutorial**: read it top-to-bottom. It assumes you've finished the [Quickstart](/getting-started/quickstart) and want to wire Synap into a real application. If you only need a snippet for a specific framework (Flask, Next.js, Django) or LLM provider (Anthropic, Vercel AI SDK), open [Setup → Integration](/setup/detailed-integration) and copy the relevant tab instead. Prefer to explore the SDK in a browser before writing code? Use the [live playground](https://synap.maximem.ai/playground). **Working in JavaScript or TypeScript?** This walkthrough is Python and FastAPI end to end. For the same loop in Node, use the [Express tab](/setup/detailed-integration#framework-integration) in Setup → Integration, which is complete and runnable, and read this page for the reasoning behind each step. ## Prerequisites Before you begin, make sure you have: * **Python 3.11+** installed on your machine (this tutorial is Python; see the note above for the Node equivalent) * A **Synap account** with access to the [Dashboard](https://synap.maximem.ai) * An **instance** created in the Dashboard (see [Quickstart](/getting-started/quickstart) if you haven't done this yet). For best results, upload a Use-Case Markdown file when creating your instance; see [Use-Case Markdown](/concepts/memory-architecture#the-use-case-file) for the template and authoring guide. * An **OpenAI API key** (or any LLM provider; we use OpenAI in this tutorial for simplicity). No paid key yet? Use [Google Gemini's free tier](https://aistudio.google.com/apikey); see the Gemini snippet in [Setup → Integration](/setup/detailed-integration#llm-provider-integration). This tutorial assumes basic familiarity with Python async/await. If you are new to async Python, check out the [asyncio documentation](https://docs.python.org/3/library/asyncio.html) first. *** Create a new directory for your project and install the required dependencies: ```bash theme={null} mkdir synap-chatbot && cd synap-chatbot python -m venv venv source venv/bin/activate ``` Install the SDK and supporting libraries: ```bash pip theme={null} pip install maximem-synap openai fastapi uvicorn ``` ```bash poetry theme={null} poetry add maximem-synap openai fastapi uvicorn ``` ```bash uv theme={null} uv add maximem-synap openai fastapi uvicorn # pip-compatible (existing venv): uv pip install maximem-synap openai fastapi uvicorn ``` Your project will have the following structure: ``` synap-chatbot/ startup.py # SDK initialization and lifecycle main.py # FastAPI application with chat endpoint .env # Environment variables (not committed) ``` Create a `.env` file with your credentials. You will need two values: * **SYNAP\_API\_KEY**: The API key generated for your instance (format: `synap_`). Generate one from the Dashboard: open your instance, click **Generate API Key**, and copy the key; it is shown only once. * **SYNAP\_INSTANCE\_ID**: The instance id shown alongside the key in the Dashboard (format: `inst_` plus 16 hex characters). Optional, since `initialize()` resolves it from the API key, but the Dashboard gives you both together so you may as well paste both. * **OPENAI\_API\_KEY**: Your OpenAI API key ```bash .env theme={null} SYNAP_API_KEY=synap_your_key_here SYNAP_INSTANCE_ID=inst_your_instance_id OPENAI_API_KEY=sk-your-openai-key-here ``` Set the instance id as an **environment variable**, never as a constructor argument. `SYNAP_INSTANCE_ID` records which instance you are on and leaves the SDK keyed on your credential. Passing `instance_id=` to `MaximemSynapSDK(...)` makes the id the identity instead, so a second key used under it is silently discarded and key rotation stops taking effect. See [Singleton Pattern](/sdk/initialization#singleton-pattern). Never commit `.env` files to version control. Add `.env` to your `.gitignore` immediately. In production, use a secrets manager (AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault) instead of environment files. Create a module that manages the SDK lifecycle, imported by your application. The Python tab uses FastAPI; the JavaScript tabs use Express. ```python startup.py theme={null} from maximem_synap import MaximemSynapSDK, SDKConfig import os sdk = MaximemSynapSDK( api_key=os.environ["SYNAP_API_KEY"], config=SDKConfig( cache_backend="sqlite", log_level="INFO" ) ) async def init(): """Validate the API key, then open the real-time stream.""" await sdk.initialize() await sdk.instance.listen( on_reconnect=lambda attempt: print(f"Synap stream reconnected ({attempt})"), on_disconnect=lambda reason: print(f"Synap stream lost: {reason}"), ) async def cleanup(): """Close the stream, then flush pending operations.""" await sdk.instance.stop_listening() await sdk.shutdown() ``` ```javascript synap.mjs theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; // No SDKConfig wrapper, and no cache_backend: the JS cache is in memory. export const sdk = new SynapClient(); // reads SYNAP_API_KEY from the environment export async function init() { // Validate the API key, then open the real-time stream. await sdk.initialize(); await sdk.instance.listen({ on_reconnect: (attempt) => console.log(`Synap stream reconnected (${attempt})`), on_disconnect: (reason) => console.log(`Synap stream lost: ${reason}`), }); } export async function cleanup() { // Close the stream, then flush pending operations. await sdk.instance.stop_listening(); await sdk.shutdown(); } ``` ```typescript synap.ts theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; // No SDKConfig wrapper, and no cache_backend: the JS cache is in memory. export const sdk = new SynapClient(); // reads SYNAP_API_KEY from the environment export async function init(): Promise { // Validate the API key, then open the real-time stream. await sdk.initialize(); await sdk.instance.listen({ on_reconnect: (attempt: number) => console.log(`Synap stream reconnected (${attempt})`), on_disconnect: (reason: string) => console.log(`Synap stream lost: ${reason}`), }); } export async function cleanup(): Promise { // Close the stream, then flush pending operations. await sdk.instance.stop_listening(); await sdk.shutdown(); } ``` Key points about this setup: * **`sdk.instance.listen()`** opens one long-lived stream for the whole process. Your agent reports each turn on it, and Synap pushes anticipated context back so retrieval resolves locally. This is the [Agent Integration](/setup/agent-integration). * **`cache_backend="sqlite"`** enables local caching for faster repeated retrievals. * **`log_level="INFO"`** is appropriate for development. Switch to `"WARNING"` or `"ERROR"` in production. * The `sdk` object is a module-level singleton. Import it from any module and it will reference the same initialized instance. Open **one** stream per process, not one per user or request. Scope travels on each call instead. The callbacks each take one argument: `on_reconnect` receives the attempt count, `on_disconnect` the reason. The API key is read fresh every time the SDK starts. Leave `SYNAP_API_KEY` in your `.env` (or secrets manager); the same key keeps working until you revoke it in the Dashboard. Now create the `main.py` file. Start with the application lifespan manager, which ensures the SDK initializes on startup and shuts down cleanly when the server stops. ```python main.py theme={null} import os from contextlib import asynccontextmanager from fastapi import FastAPI from pydantic import BaseModel from openai import AsyncOpenAI from startup import sdk, init, cleanup # --- Lifespan Management --- @asynccontextmanager async def lifespan(app): """Initialize Synap and open the stream on startup; close both on exit.""" await init() print("Synap SDK initialized and listening. Ready to serve requests.") yield await cleanup() print("Synap SDK shut down cleanly.") app = FastAPI( title="Synap Chatbot", description="A memory-enabled chatbot powered by Synap", lifespan=lifespan ) openai_client = AsyncOpenAI(api_key=os.environ.get("OPENAI_API_KEY")) # --- Request/Response Models --- class ChatRequest(BaseModel): message: str conversation_id: str user_id: str customer_id: str | None = None # B2B instances only; leave unset on B2C class ChatResponse(BaseModel): response: str memories_used: int ``` ```javascript server.mjs theme={null} import express from 'express'; import OpenAI from 'openai'; import { sdk, init, cleanup } from './synap.mjs'; const app = express(); app.use(express.json()); const openaiClient = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); // --- Lifecycle --- // Express has no lifespan hook: initialize first, close on SIGTERM. await init(); console.log('Synap SDK initialized and listening. Ready to serve requests.'); const server = app.listen(8000); process.on('SIGTERM', async () => { server.close(); await cleanup(); console.log('Synap SDK shut down cleanly.'); process.exit(0); }); // Request shape: { message, conversation_id, user_id, customer_id? } // customer_id is B2B only; leave it unset on B2C. ``` ```typescript server.ts theme={null} import express from 'express'; import OpenAI from 'openai'; import { sdk, init, cleanup } from './synap.js'; const app = express(); app.use(express.json()); const openaiClient = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); interface ChatRequest { message: string; conversation_id: string; user_id: string; /** B2B instances only; leave unset on B2C. */ customer_id?: string; } interface ChatResponse { response: string; memories_used: number; } // --- Lifecycle --- // Express has no lifespan hook: initialize first, close on SIGTERM. await init(); console.log('Synap SDK initialized and listening. Ready to serve requests.'); const server = app.listen(8000); process.on('SIGTERM', async () => { server.close(); await cleanup(); console.log('Synap SDK shut down cleanly.'); process.exit(0); }); ``` The `lifespan` context manager is the recommended way to manage startup/shutdown in modern FastAPI applications (v0.95+). It replaces the older `@app.on_event("startup")` and `@app.on_event("shutdown")` hooks. Add the chat endpoint to `main.py`. This endpoint performs five operations in sequence: 1. **Report** the incoming user message on the stream 2. **Retrieve** relevant memories from Synap 3. **Build** a system prompt enriched with memory context 4. **Call** the LLM with the enriched prompt 5. **Report** the assistant's reply on the stream Reporting each turn with `send_message` is what makes the conversation retrievable: it registers the conversation and appends the turn to its history, exactly as `record_message` does over REST. Context fetched by `conversation_id` only returns turns that were reported, so an unregistered conversation returns empty results by design. ```python main.py (continued) theme={null} @app.post("/chat", response_model=ChatResponse) async def chat(req: ChatRequest): # customer_id is required on a B2B instance and not accepted on a # B2C one, so forward it only when the caller supplied it. scope = {"customer_id": req.customer_id} if req.customer_id else {} # ------------------------------------------------------- # Step 1: Report the user's message (registers the conversation) # ------------------------------------------------------- await sdk.instance.send_message( content=req.message, role="user", event_type="user_message", conversation_id=req.conversation_id, user_id=req.user_id, **scope, ) # ------------------------------------------------------- # Step 2: Retrieve relevant memories for this conversation # ------------------------------------------------------- context = await sdk.conversation.context.fetch( conversation_id=req.conversation_id, search_query=[req.message], max_results=5, types=["facts", "preferences"], mode="fast" ) # ------------------------------------------------------- # Step 3: Build system prompt with memory context # ------------------------------------------------------- memory_lines = [] for fact in context.facts: memory_lines.append( f"- {fact.content} (confidence: {fact.confidence:.0%})" ) for pref in context.preferences: memory_lines.append(f"- User preference: {pref.content}") memory_block = "\n".join(memory_lines) if memory_lines else ( "No prior context available." ) system_prompt = f"""You are a helpful assistant with memory. Known information about this user: {memory_block} Use this context naturally in your responses. Do not explicitly mention that you are reading from a memory system, just be naturally informed.""" # ------------------------------------------------------- # Step 4: Call the LLM # ------------------------------------------------------- response = await openai_client.chat.completions.create( model="gpt-4o", messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": req.message} ], temperature=0.7, max_tokens=1024 ) assistant_message = response.choices[0].message.content # ------------------------------------------------------- # Step 5: Report the assistant's reply # ------------------------------------------------------- # This completes the turn in the conversation's history and is what # pre-warms anticipation for the NEXT turn. There is no ingestion # call: both reported turns become long-term memory when this # conversation compacts. await sdk.instance.send_message( content=assistant_message, role="assistant", event_type="assistant_message", conversation_id=req.conversation_id, user_id=req.user_id, **scope, ) return ChatResponse( response=assistant_message, memories_used=len(memory_lines) ) ``` ```javascript server.mjs (continued) theme={null} app.post('/chat', async (req, res) => { const { message, conversation_id, user_id, customer_id } = req.body; // customer_id is required on a B2B instance and not accepted on a // B2C one, so forward it only when the caller supplied it. const scope = customer_id ? { customer_id } : {}; // ------------------------------------------------------- // Step 1: Report the user's message (registers the conversation) // ------------------------------------------------------- await sdk.instance.send_message({ content: message, role: 'user', event_type: 'user_message', conversation_id, user_id, ...scope, }); // ------------------------------------------------------- // Step 2: Retrieve relevant memories for this conversation // ------------------------------------------------------- const context = await sdk.conversation.context.fetch({ conversation_id, search_query: [message], max_results: 5, types: ['facts', 'preferences'], mode: 'fast', }); // ------------------------------------------------------- // Step 3: Build system prompt with memory context // ------------------------------------------------------- // Each collection is optional on the raw response, so default it. const memoryLines = []; for (const fact of context.facts ?? []) { memoryLines.push( `- ${fact.content} (confidence: ${((fact.confidence ?? 0) * 100).toFixed(0)}%)`, ); } for (const pref of context.preferences ?? []) { memoryLines.push(`- User preference: ${pref.content}`); } const memoryBlock = memoryLines.length ? memoryLines.join('\n') : 'No prior context available.'; const systemPrompt = `You are a helpful assistant with memory. Known information about this user: ${memoryBlock} Use this context naturally in your responses. Do not explicitly mention that you are reading from a memory system, just be naturally informed.`; // ------------------------------------------------------- // Step 4: Call the LLM // ------------------------------------------------------- const response = await openaiClient.chat.completions.create({ model: 'gpt-4o', messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: message }, ], temperature: 0.7, max_tokens: 1024, }); const assistantMessage = response.choices[0]?.message.content ?? ''; // ------------------------------------------------------- // Step 5: Report the assistant's reply // ------------------------------------------------------- // This completes the turn in the conversation's history and is what // pre-warms anticipation for the NEXT turn. There is no ingestion // call: both reported turns become long-term memory when this // conversation compacts. await sdk.instance.send_message({ content: assistantMessage, role: 'assistant', event_type: 'assistant_message', conversation_id, user_id, ...scope, }); res.json({ response: assistantMessage, memories_used: memoryLines.length }); }); ``` ```typescript server.ts (continued) theme={null} app.post('/chat', async (req, res) => { const { message, conversation_id, user_id, customer_id } = req.body as ChatRequest; // customer_id is required on a B2B instance and not accepted on a // B2C one, so forward it only when the caller supplied it. const scope = customer_id ? { customer_id } : {}; // ------------------------------------------------------- // Step 1: Report the user's message (registers the conversation) // ------------------------------------------------------- await sdk.instance.send_message({ content: message, role: 'user', event_type: 'user_message', conversation_id, user_id, ...scope, }); // ------------------------------------------------------- // Step 2: Retrieve relevant memories for this conversation // ------------------------------------------------------- const context = await sdk.conversation.context.fetch({ conversation_id, search_query: [message], max_results: 5, types: ['facts', 'preferences'], mode: 'fast', }); // ------------------------------------------------------- // Step 3: Build system prompt with memory context // ------------------------------------------------------- // Each collection is optional on the raw response, so default it. const memoryLines: string[] = []; for (const fact of context.facts ?? []) { memoryLines.push( `- ${fact.content} (confidence: ${((fact.confidence ?? 0) * 100).toFixed(0)}%)`, ); } for (const pref of context.preferences ?? []) { memoryLines.push(`- User preference: ${pref.content}`); } const memoryBlock = memoryLines.length ? memoryLines.join('\n') : 'No prior context available.'; const systemPrompt = `You are a helpful assistant with memory. Known information about this user: ${memoryBlock} Use this context naturally in your responses. Do not explicitly mention that you are reading from a memory system, just be naturally informed.`; // ------------------------------------------------------- // Step 4: Call the LLM // ------------------------------------------------------- const response = await openaiClient.chat.completions.create({ model: 'gpt-4o', messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: message }, ], temperature: 0.7, max_tokens: 1024, }); const assistantMessage = response.choices[0]?.message.content ?? ''; // ------------------------------------------------------- // Step 5: Report the assistant's reply // ------------------------------------------------------- // This completes the turn in the conversation's history and is what // pre-warms anticipation for the NEXT turn. There is no ingestion // call: both reported turns become long-term memory when this // conversation compacts. await sdk.instance.send_message({ content: assistantMessage, role: 'assistant', event_type: 'assistant_message', conversation_id, user_id, ...scope, }); res.json({ response: assistantMessage, memories_used: memoryLines.length }); }); ``` Let's break down each step: `sdk.instance.send_message()` publishes the turn on the open stream. Synap appends it to the conversation's rolling history **and registers the conversation** under this `conversation_id`, the same effect `conversation.record_message()` has over REST. That registration is what later lets `conversation.context.fetch(conversation_id=...)` resolve scope and return the conversation's turns. Skip it and the first `fetch` for a brand-new `conversation_id` comes back empty, by design. Reporting it also tells Synap what the agent is doing, so it can anticipate what context to push next. `user_id` is always required. On a **B2B** instance `customer_id` is required as well, and if either is missing the turn is dropped server-side with no error. On a **B2C** instance `customer_id` is **not accepted**: sending it fails with HTTP 400, so pass `user_id` alone. Never reuse the user identifier as the customer identifier. This example forwards `customer_id` only when the caller supplied one, so it stays correct on both shapes. `GET /api/v1/auth/whoami` returns your instance's `user_context_isolation` if you are unsure which shape you are on. The `sdk.conversation.context.fetch()` call searches Synap's vector and graph stores for memories relevant to the user's message. Key parameters: * **`search_query`**: A list of strings used for semantic search. Passing the user's message ensures we find contextually relevant memories. * **`max_results=5`**: Limits context to the top 5 most relevant memories, keeping the prompt concise. * **`types=["facts", "preferences"]`**: Retrieves only facts and preferences. Other types include `episodes`, `emotions`, and `temporal`. Use `all` to retrieve every type. * **`mode="fast"`**: Uses the `fast` retrieval path (lower latency). Use `accurate` when precision matters more; accurate adds LLM subquery decomposition + reranking on top of the same vector + graph search. The retrieved memories are formatted as bullet points and injected into the system prompt. This gives the LLM access to user-specific context without modifying the conversation history. The confidence score (e.g., `92%`) is included to help the LLM weigh how certain each piece of information is. You can omit confidence scores if you prefer a cleaner prompt. A standard OpenAI chat completion call. The system prompt now contains personalized context, so the LLM can respond as if it "remembers" the user. This works with any LLM provider: replace the OpenAI call with your preferred provider. One write, two jobs. `send_message(role="assistant", ...)` completes the turn in the conversation's rolling history, so the next turn's `context.fetch` sees the full exchange, and it is the event that pre-warms anticipation for the next turn, which is why it belongs *after* the LLM call, not before. Notice what is **not** here: `memories.create()`. Both reported turns become long-term memory on their own, when this conversation compacts: at 3,000 tokens, 10 messages, or 5 minutes of inactivity. Synap promotes the raw turns into the same ingestion pipeline `memories.create()` would have used. Add explicit ingestion only for content that is not a conversation turn (documents, tickets, backfills) or that must be retrievable sooner than compaction. Never for text you already reported; that extracts it twice. See [Agent Integration](/setup/agent-integration). Good practice for production deployments: add a health check that verifies the SDK is connected: ```python main.py (continued) theme={null} @app.get("/health") async def health(): try: stats = sdk.cache.stats() return { "status": "healthy", "synap_connected": True, "cache_entries": stats["total_entries"] } except Exception as e: return { "status": "degraded", "synap_connected": False, "error": str(e) } ``` ```javascript server.mjs (continued) theme={null} app.get('/health', (req, res) => { try { // The JS cache reports { bundles, items }; Python's SQLite backend // reports { entry_count, total_bytes, ... }. Different shapes. const stats = sdk.cache.stats(); res.json({ status: 'healthy', synap_connected: true, cache_bundles: stats.bundles, cache_items: stats.items, }); } catch (e) { res.json({ status: 'degraded', synap_connected: false, error: String(e), }); } }); ``` ```typescript server.ts (continued) theme={null} app.get('/health', (req, res) => { try { // The JS cache reports { bundles, items }; Python's SQLite backend // reports { entry_count, total_bytes, ... }. Different shapes. const stats = sdk.cache.stats(); res.json({ status: 'healthy', synap_connected: true, cache_bundles: stats.bundles, cache_items: stats.items, }); } catch (e) { res.json({ status: 'degraded', synap_connected: false, error: String(e), }); } }); ``` Load your environment variables and start the server: ```bash Python theme={null} # Load environment variables export $(cat .env | xargs) # Start the FastAPI server uvicorn main:app --reload --port 8000 ``` ```bash Node theme={null} # Node 20.6+ reads the file directly; no dotenv needed node --env-file=.env server.mjs ``` ```bash TypeScript theme={null} npx tsx --env-file=.env server.ts ``` You should see output confirming the SDK has initialized: ``` INFO: Started server process Synap SDK initialized. Ready to serve requests. INFO: Application startup complete. INFO: Uvicorn running on http://127.0.0.1:8000 ``` Now test with a few conversation turns: ```bash theme={null} # First message, no memories exist yet # conversation_id must be a UUID; generate one with `python -c "import uuid; print(uuid.uuid4())"` curl -X POST http://localhost:8000/chat \ -H "Content-Type: application/json" \ -d '{ "message": "Hi! I am planning a trip to Japan next month. Any tips?", "conversation_id": "3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c", "user_id": "user_alice" }' ``` ```json theme={null} { "response": "Japan is wonderful! What kind of experience are you looking for...", "memories_used": 0 } ``` ```bash theme={null} # Second message, Synap now has context from the first turn # Reuse the same UUID to keep both turns in the same conversation. curl -X POST http://localhost:8000/chat \ -H "Content-Type: application/json" \ -d '{ "message": "What should I pack?", "conversation_id": "3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c", "user_id": "user_alice" }' ``` ```json theme={null} { "response": "Since you're heading to Japan next month, here's what I'd recommend packing...", "memories_used": 2 } ``` The second turn works because the first was reported with `send_message`: the exchange is already in the conversation's history, so `context.fetch` returns it and the assistant naturally references the Japan trip. Had the turn never been reported, that fetch would come back empty by design. **Long-term memories take a few minutes to appear.** Conversation continuity works immediately, as you just saw, but the durable, cross-conversation memories that make `memories_used` climb are created when the conversation compacts, which for a quiet conversation means about five minutes. To see it without waiting, force compaction once you have sent a few turns: ```python Python theme={null} await sdk.conversation.context.compact( conversation_id="", force=True, # compact even though it is under the threshold ) ``` ```javascript JavaScript theme={null} await sdk.conversation.context.compact({ conversation_id: '', force: true, // compact even though it is under the threshold }); ``` ```typescript TypeScript theme={null} await sdk.conversation.context.compact({ conversation_id: '', force: true, // compact even though it is under the threshold }); ``` Give it a moment to process, then fetch again and `memories_used` will now be non-zero. In production you never call this; the thresholds and the idle timer handle it. Open the [Synap Dashboard](https://synap.maximem.ai) and navigate to your instance. You should see: * **API call counts** reflecting your test requests * **Memory counts** showing extracted facts, preferences, and entities * **Ingestion history** with the conversation turns you sent Dashboard showing API calls and memory counts *** ## Complete Code Here is the final version of each file for reference. The Python tabs are FastAPI; the JavaScript tabs are Express. ```python startup.py theme={null} from maximem_synap import MaximemSynapSDK, SDKConfig import os sdk = MaximemSynapSDK( api_key=os.environ["SYNAP_API_KEY"], config=SDKConfig( cache_backend="sqlite", log_level="INFO" ) ) async def init(): """Validate the API key, then open the real-time stream.""" await sdk.initialize() await sdk.instance.listen( on_reconnect=lambda attempt: print(f"Synap stream reconnected ({attempt})"), on_disconnect=lambda reason: print(f"Synap stream lost: {reason}"), ) async def cleanup(): """Close the stream, then flush pending operations.""" await sdk.instance.stop_listening() await sdk.shutdown() ``` ```python main.py theme={null} import os from contextlib import asynccontextmanager from fastapi import FastAPI from pydantic import BaseModel from openai import AsyncOpenAI from startup import sdk, init, cleanup @asynccontextmanager async def lifespan(app): await init() print("Synap SDK initialized. Ready to serve requests.") yield await cleanup() print("Synap SDK shut down cleanly.") app = FastAPI( title="Synap Chatbot", description="A memory-enabled chatbot powered by Synap", lifespan=lifespan ) openai_client = AsyncOpenAI(api_key=os.environ.get("OPENAI_API_KEY")) class ChatRequest(BaseModel): message: str conversation_id: str user_id: str customer_id: str | None = None # B2B instances only; leave unset on B2C class ChatResponse(BaseModel): response: str memories_used: int @app.post("/chat", response_model=ChatResponse) async def chat(req: ChatRequest): # customer_id is required on a B2B instance and not accepted on a # B2C one, so forward it only when the caller supplied it. scope = {"customer_id": req.customer_id} if req.customer_id else {} # Report the user's message (registers the conversation) await sdk.instance.send_message( content=req.message, role="user", event_type="user_message", conversation_id=req.conversation_id, user_id=req.user_id, **scope, ) # Retrieve relevant memories context = await sdk.conversation.context.fetch( conversation_id=req.conversation_id, search_query=[req.message], max_results=5, types=["facts", "preferences"], mode="fast" ) # Build system prompt with memory context memory_lines = [] for fact in context.facts: memory_lines.append( f"- {fact.content} (confidence: {fact.confidence:.0%})" ) for pref in context.preferences: memory_lines.append(f"- User preference: {pref.content}") memory_block = "\n".join(memory_lines) if memory_lines else ( "No prior context available." ) system_prompt = f"""You are a helpful assistant with memory. Known information about this user: {memory_block} Use this context naturally in your responses. Do not explicitly mention that you are reading from a memory system, just be naturally informed.""" # Call the LLM response = await openai_client.chat.completions.create( model="gpt-4o", messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": req.message} ], temperature=0.7, max_tokens=1024 ) assistant_message = response.choices[0].message.content # Report the assistant reply: completes the turn in conversation history # and pre-warms anticipation for the next turn. No ingestion call needed; # both turns become long-term memory when this conversation compacts. await sdk.instance.send_message( content=assistant_message, role="assistant", event_type="assistant_message", conversation_id=req.conversation_id, user_id=req.user_id, **scope, ) return ChatResponse( response=assistant_message, memories_used=len(memory_lines) ) @app.get("/health") async def health(): try: stats = sdk.cache.stats() return { "status": "healthy", "synap_connected": True, "cache_entries": stats["total_entries"] } except Exception as e: return { "status": "degraded", "synap_connected": False, "error": str(e) } ``` ```javascript synap.mjs theme={null} import { SynapClient } from '@maximem/synap-js-sdk'; // No SDKConfig wrapper, and no cache_backend: the JS cache is in memory. export const sdk = new SynapClient(); // reads SYNAP_API_KEY from the environment export async function init() { // Validate the API key, then open the real-time stream. await sdk.initialize(); await sdk.instance.listen({ on_reconnect: (attempt) => console.log(`Synap stream reconnected (${attempt})`), on_disconnect: (reason) => console.log(`Synap stream lost: ${reason}`), }); } export async function cleanup() { // Close the stream, then flush pending operations. await sdk.instance.stop_listening(); await sdk.shutdown(); } ``` ```javascript server.mjs theme={null} import express from 'express'; import OpenAI from 'openai'; import { sdk, init, cleanup } from './synap.mjs'; const app = express(); app.use(express.json()); const openaiClient = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); // --- Lifecycle --- await init(); console.log('Synap SDK initialized and listening. Ready to serve requests.'); const server = app.listen(8000); process.on('SIGTERM', async () => { server.close(); await cleanup(); console.log('Synap SDK shut down cleanly.'); process.exit(0); }); // --- Chat --- app.post('/chat', async (req, res) => { const { message, conversation_id, user_id, customer_id } = req.body; // customer_id is required on a B2B instance and not accepted on a // B2C one, so forward it only when the caller supplied it. const scope = customer_id ? { customer_id } : {}; // Step 1: report the user's message (registers the conversation) await sdk.instance.send_message({ content: message, role: 'user', event_type: 'user_message', conversation_id, user_id, ...scope, }); // Step 2: retrieve relevant memories for this conversation const context = await sdk.conversation.context.fetch({ conversation_id, search_query: [message], max_results: 5, types: ['facts', 'preferences'], mode: 'fast', }); // Step 3: build the system prompt. Each collection is optional on the // raw response, so default it before iterating. const memoryLines = []; for (const fact of context.facts ?? []) { memoryLines.push( `- ${fact.content} (confidence: ${((fact.confidence ?? 0) * 100).toFixed(0)}%)`, ); } for (const pref of context.preferences ?? []) { memoryLines.push(`- User preference: ${pref.content}`); } const memoryBlock = memoryLines.length ? memoryLines.join('\n') : 'No prior context available.'; const systemPrompt = `You are a helpful assistant with memory. Known information about this user: ${memoryBlock} Use this context naturally in your responses. Do not explicitly mention that you are reading from a memory system, just be naturally informed.`; // Step 4: call the LLM const response = await openaiClient.chat.completions.create({ model: 'gpt-4o', messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: message }, ], temperature: 0.7, max_tokens: 1024, }); const assistantMessage = response.choices[0]?.message.content ?? ''; // Step 5: report the assistant's reply. There is no ingestion call: // both reported turns become long-term memory when this conversation // compacts. await sdk.instance.send_message({ content: assistantMessage, role: 'assistant', event_type: 'assistant_message', conversation_id, user_id, ...scope, }); res.json({ response: assistantMessage, memories_used: memoryLines.length }); }); // --- Health --- app.get('/health', (req, res) => { try { // The JS cache reports { bundles, items }; Python's SQLite backend // reports { entry_count, total_bytes, ... }. Different shapes. const stats = sdk.cache.stats(); res.json({ status: 'healthy', synap_connected: true, cache_bundles: stats.bundles, cache_items: stats.items, }); } catch (e) { res.json({ status: 'degraded', synap_connected: false, error: String(e) }); } }); ``` *** ## What's Next? You have a working memory-enabled chatbot. Here are the natural next steps to make it production-ready: The use-case file is how Synap tunes what gets extracted, how it is stored, and how retrieval ranking works for your Instance. Set up memory isolation for multi-tenant applications with user, customer, and client scopes. Manage long conversations by compacting context to fit within your LLM's token budget. Security, performance, and monitoring best practices before going live. # Installation Source: https://docs.maximem.ai/setup/installation Install the Synap SDK for Python, JavaScript, or TypeScript, configure environment variables, and verify your setup. Synap ships two native SDKs: one for Python, one for JavaScript and TypeScript. Pick your language below. Each has a complete setup path of its own. | | Python | JavaScript / TypeScript | | --------------- | ------------------------- | --------------------------------------------------------------- | | **Package** | `maximem-synap` (PyPI) | `@maximem/synap-js-sdk` (npm) | | **Runtime** | Python 3.11+ | Node.js 20+ | | **Setup steps** | 1 (install) | 1 (install) | | **Types** | Built-in type hints | Bundled `.d.ts`, no extra step | | **Guide** | [Python SDK](#python-sdk) | [JavaScript and TypeScript SDK](#javascript-and-typescript-sdk) | JavaScript and TypeScript are served by the **same npm package**. TypeScript is not a separate SDK and needs no separate installation: types ship with the package. *** ## Python SDK ### Requirements * **Python 3.11+**: the SDK uses modern Python features including `asyncio`, type hints, and structural pattern matching * **pip 21.0+**, **Poetry 1.2+**, or **uv 0.4+** for package management * **An active Synap account**: [Sign up at synap.maximem.ai](https://synap.maximem.ai) ### Install ```bash pip theme={null} pip install maximem-synap ``` ```bash poetry theme={null} poetry add maximem-synap ``` ```bash uv theme={null} uv add maximem-synap # pip-compatible (existing venv): uv pip install maximem-synap ``` ```bash requirements.txt theme={null} # Add to your requirements.txt maximem-synap>=0.4.2 ``` The package name uses a hyphen (`maximem-synap`) but the import name uses an underscore (`maximem_synap`). Install with `pip install maximem-synap`, then `from maximem_synap import MaximemSynapSDK` in your code. **Pin at least 0.4.1 if one process ever uses more than one API key**: a per-tenant backend, a worker that switches keys between jobs, or staging and production side by side. In 0.4.0 and earlier, the second and later SDKs in a process silently adopted the first one's credentials, so their reads returned the first key's memory and their writes were committed against it. See the [0.4.1 release notes](/resources/changelog). A single-API-key process is unaffected. This installs the SDK with the following dependencies: * `httpx`: async HTTP client used by the SDK * `pydantic`: data validation and settings management * `cryptography`: credential handling * Additional transport dependencies pulled in automatically; no extra install needed. ### Configure Set your API key. See [environment variables](#environment-variables) for all options. ```bash theme={null} export SYNAP_API_KEY="synap_your_key_here" ``` ### Verify Run this script to verify your installation and connectivity: ```python verify_synap.py theme={null} import asyncio from maximem_synap import MaximemSynapSDK async def verify(): try: sdk = MaximemSynapSDK( api_key="synap_your_key_here" ) await sdk.initialize() print("[OK] SDK initialized successfully") print("[OK] Connected to Synap") await sdk.shutdown() print("[OK] SDK shut down cleanly") except Exception as e: print(f"[ERROR] {e}") if __name__ == "__main__": asyncio.run(verify()) ``` ```bash theme={null} python verify_synap.py ``` Expected output: ``` [OK] SDK initialized successfully [OK] Connected to instance: inst_a1b2c3d4e5f67890 [OK] SDK shut down cleanly ``` ### Async-first design The Python SDK is async-first. All SDK methods that interact with Synap Cloud are `async` and must be called with `await` inside an `async` function. If you're integrating with a synchronous codebase, use `asyncio.run()` to bridge the gap: ```python theme={null} import asyncio from maximem_synap import MaximemSynapSDK def ingest_sync(document: str, user_id: str, customer_id: str): """Synchronous wrapper for async ingestion.""" async def _ingest(): sdk = MaximemSynapSDK( api_key="synap_your_key_here" ) await sdk.initialize() result = await sdk.memories.create( document=document, document_type="ai-chat-conversation", user_id=user_id, customer_id=customer_id, ) await sdk.shutdown() return result return asyncio.run(_ingest()) ``` For frameworks that already run an event loop (FastAPI, Sanic, aiohttp), use the SDK directly without wrapping. ***
## JavaScript and TypeScript SDK One package serves both: `@maximem/synap-js-sdk`. Type definitions ship with it, so TypeScript is typed the moment you install and needs no extra step. ### Requirements * **Node.js 20+** * **An active Synap account**: [Sign up at synap.maximem.ai](https://synap.maximem.ai) ### Install ```bash theme={null} npm install @maximem/synap-js-sdk ``` That is the whole installation, in both languages. The [anticipation stream](/sdk-reference/instance/listen#javascript-opening-the-stream) is optional and needs two more packages. Skip this unless you want it: ```bash theme={null} npm install @grpc/grpc-js @grpc/proto-loader ``` Nothing changes. There is no separate JavaScript build, no `typescript` dependency to add, no `tsconfig.json` to create, and no compile step. `npm install` and you are done. The bundled type definitions are inert at runtime, so they cost you nothing. Most editors read them anyway, which means you get autocomplete and inline parameter help in a plain `.js` file without opting into TypeScript. Use `require()` or `import` depending on how your project is written; see [module format](#module-format). The examples below are labelled `ts`, but they use no type annotations, so they are valid JavaScript exactly as written. Swap the import line for a `require()` if your project is CommonJS. The one section that is genuinely TypeScript-only is [exported types](/sdk/response-shapes#javascript-exported-types), which you can skip. ### Configure Set your API key. See [environment variables](#environment-variables) for all options. ```bash theme={null} export SYNAP_API_KEY="synap_your_key_here" ``` Your `client_id` and `instance_id` are resolved from the key when you call `initialize()`, so you do not need to plumb them separately. ### Verify Check the install and your credentials in one go: ```js verify-synap.mjs theme={null} import { SynapClient } from "@maximem/synap-js-sdk"; const synap = new SynapClient({ apiKey: process.env.SYNAP_API_KEY }); try { await synap.initialize(); console.log("[OK] client initialized"); console.log(`[OK] connected to instance: ${synap.instance_id || "(resolved from key)"}`); await synap.shutdown(); console.log("[OK] shut down cleanly"); } catch (error) { console.error(`[ERROR] ${error.message}`); } ``` ```bash theme={null} node verify-synap.mjs ``` Expected output: ``` [OK] client initialized [OK] connected to instance: inst_a1b2c3d4e5f67890 [OK] shut down cleanly ``` ### Module format The package ships both ES modules and CommonJS, with types for each. Import it whichever way your project is written: ```ts ES modules / TypeScript theme={null} import { SynapClient } from "@maximem/synap-js-sdk"; ``` ```js CommonJS theme={null} const { SynapClient } = require("@maximem/synap-js-sdk"); ``` Because a dual-format dependency graph can hand you two copies of the same class, every error also carries a stable `.code` string. Branch on `error.code` rather than `instanceof` when you cannot guarantee a single copy. See [typed error handling](/sdk/error-handling#javascript-typed-error-handling). ### Where it runs | Runtime | Context and memories | Anticipation stream | | ------------------ | ----------------------------------------- | ------------------- | | Node.js 20+ | Yes | Yes (opt-in) | | Bun | Yes | Unverified | | Deno 2 | Yes | Unverified | | Vercel Edge | Yes | No | | Cloudflare Workers | Yes | No | | Browser | Yes (do not ship an API key to a browser) | No | The anticipation stream needs raw TCP and `node:http2`, which Edge runtimes and Workers do not provide. Importing the SDK there is safe: the stream lives behind the `@maximem/synap-js-sdk/grpc` subpath and is only ever loaded lazily, so it never enters an Edge bundle. Bun and Deno have both, so the stream is plausible on each, but bidirectional streaming has not been verified there.
### tsconfig requirements None specific to this package. It is verified against TypeScript 5.7 and 7.0 with `moduleResolution` set to `node`, `node16`, `nodenext` and `bundler`, and it typechecks under `strict`, `exactOptionalPropertyTypes` and `noUncheckedIndexedAccess`. A minimal configuration: ```json tsconfig.json theme={null} { "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "strict": true, "skipLibCheck": true, "outDir": "dist" }, "include": ["src/**/*.ts"] } ``` ### Next: using it Installation is done. For the client surface, the cross-scope fetch, LLM tool definitions, typed errors and the anticipation stream, see [Using the JavaScript SDK](/sdk/initialization#javascript-namespaced-and-flat-surfaces). ### Vercel AI SDK Middleware If your application uses the [Vercel AI SDK](https://sdk.vercel.ai), the `@maximem/synap-vercel-adk` middleware wraps any compatible model and injects Synap context automatically, with no changes to your existing `generateText` / `streamText` calls. ```bash theme={null} npm install @maximem/synap-vercel-adk ``` Requires Node.js 18+ and `ai >=3.0.0` as a peer dependency. TypeScript types are included. It runs on the **Next.js Edge Runtime** for context fetching and memory writes, which use `fetch` only. You do not need to pin `export const runtime = "nodejs"` for those. The one exception is the optional gRPC anticipation stream, which needs `@grpc/grpc-js` and therefore a Node.js runtime. It is loaded lazily, so importing the package on Edge is safe; only enabling the stream requires Node.js. *** ## Environment variables The SDK reads configuration from environment variables. This is the recommended approach for production deployments. ### All languages Your API key for SDK authentication. Generated in the Dashboard: navigate to your instance and click **Generate API Key**. Starts with `synap_`. Records which instance you are on. Optional; the dashboard gives you it alongside the API key so you can paste both in one go. Starts with `inst_`. Logging verbosity for the SDK. Accepts standard logging levels: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`. Defaults to `INFO`. Set the instance id as an **environment variable**, not as a constructor argument. `SYNAP_INSTANCE_ID` records which instance you are on and leaves the SDK keyed on your credential. Passing `instance_id=` to `MaximemSynapSDK(...)` is different: it makes the id the identity, so a second key used under it is silently discarded and key rotation stops taking effect. See [Singleton Pattern](/sdk/initialization#singleton-pattern). API base URL. Set it when you run a self-hosted deployment; otherwise leave it unset and the SDK uses Synap Cloud. Optional. Skips one identity round trip during `initialize()`. Resolved from the API key when omitted. ### Anticipation stream Only needed when you use the [anticipation stream](#anticipation-stream), and only when your deployment is not the default. Stream host. Defaults to the same deployment your API calls go to. Stream port. Defaults to `443`. Set to `0` for a plaintext connection. Only sensible against a local or tunnelled deployment. ### Setting them ```bash Linux / macOS theme={null} export SYNAP_API_KEY="synap_your_key_here" export SYNAP_INSTANCE_ID="inst_your_instance_id" export SYNAP_LOG_LEVEL="INFO" ``` ```powershell Windows (PowerShell) theme={null} $env:SYNAP_API_KEY = "synap_your_key_here" $env:SYNAP_INSTANCE_ID = "inst_your_instance_id" $env:SYNAP_LOG_LEVEL = "INFO" ``` ```ini .env file theme={null} SYNAP_API_KEY=synap_your_key_here SYNAP_INSTANCE_ID=inst_your_instance_id SYNAP_LOG_LEVEL=INFO ``` Never commit API keys to version control. Use a secrets manager (AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault) or environment variables in production. ## Credential storage The SDK reads the API key from `SYNAP_API_KEY` (or the `api_key` / `apiKey` constructor argument) on every startup. There is no on-disk credential cache; the key lives wherever your secrets manager or environment configuration puts it. ```python Python theme={null} # Reads SYNAP_API_KEY from the environment sdk = MaximemSynapSDK() # Or pass the key explicitly sdk = MaximemSynapSDK( api_key="synap_your_key_here" ) ``` ```js JavaScript / TypeScript theme={null} // Reads SYNAP_API_KEY from the environment const synap = new SynapClient(); // Or pass the key explicitly const synap = new SynapClient({ apiKey: "synap_your_key_here", }); ``` In Kubernetes, mount the API key as a secret and reference it via `SYNAP_API_KEY` in your pod spec. The same pattern works for Docker, Vercel, and AWS Lambda. ## Troubleshooting Verify the package is installed in your active Python environment: ```bash theme={null} pip show maximem-synap ``` If using a virtual environment, make sure it's activated. If using Poetry, prefix commands with `poetry run`. The client could not resolve a credential. Set `SYNAP_API_KEY` in the environment, or pass `apiKey` to the constructor: ```ts theme={null} const synap = new SynapClient({ apiKey: process.env.SYNAP_API_KEY }); ``` The anticipation stream needs two optional peers that are not installed by default, because most applications never open a stream: ```bash theme={null} npm install @grpc/grpc-js @grpc/proto-loader ``` Everything except `instance.listen()` works without them. It cannot. The stream needs raw TCP and `node:http2`, which those runtimes do not provide. Context fetching and memory writes work there normally; only the stream is unavailable. See [where it runs](#where-it-runs). Check `SYNAP_BASE_URL`. When it is unset the SDK uses Synap Cloud, and an explicit `baseUrl` in the constructor beats the environment. If you point the HTTP base at one deployment, point `SYNAP_GRPC_HOST` at the same one. Check that: 1. Your outbound network connectivity to Synap Cloud is permitted. 2. If behind a corporate proxy, configure `HTTPS_PROXY` in your environment. 3. Your `SYNAP_API_KEY` is correct and the key is active in the dashboard. If the SDK reports an authentication failure: 1. Confirm `SYNAP_API_KEY` starts with `synap_` and is not wrapped in quotes in your shell 2. Check the key is still active in the Dashboard (Instance → API Keys) 3. If the key was revoked, generate a new one and update your `.env` or secrets manager ## Next steps Configure API key authentication, multiple keys per instance, and key rotation. Connect Synap to your application framework and infrastructure. Explore all SDK initialization options, including custom credential providers. # Hermes Agent Source: https://docs.maximem.ai/vity/hermes Persistent, cross-session semantic memory for the Hermes Agent, distributed as a standalone plugin. Hermes Agent is stateless between sessions. This plugin gives it a long-term memory. Vity adds a persistent memory graph (facts, preferences, episodes, knowledge, and profile) to [Hermes Agent](https://github.com/NousResearch/hermes-agent). It automatically recalls relevant context before each turn and captures the conversation after each turn, so the agent remembers users and projects across separate sessions. Built on the [`maximem-vity-sdk`](https://pypi.org/project/maximem-vity-sdk/) Python client. The plugin and its dependencies install into Hermes' own Python environment. ## Features * **Long-term memory**: Stores facts, preferences, episodes, knowledge, and user profiles in a private cloud-vault. Recalls them semantically across sessions. * **Auto-recall**: Fetches relevant memories and injects them as context before each turn. * **Auto-capture**: Saves the user/assistant exchange to long-term memory after each turn. * **Memory mirroring**: When Hermes' built-in memory tool records a fact, it is also stored in Vity so it participates in semantic recall. * **Bounded, non-blocking**: Recall is time-boxed and degrades to no-memory-this-turn rather than hanging; captures and mirroring run on background threads, so the reply is never blocked. * **Agent tools**: `vity_recall`, `vity_profile`, `vity_store`, `vity_forget` for agent-driven memory operations. * **CLI commands**: Terminal memory management with `hermes maximem_vity`. ## API key You'll need a Maximem API key to use this plugin. 1. Sign up or log in at [app.maximem.ai](https://app.maximem.ai). 2. Open **API Keys** in the sidebar. 3. Click **Generate New Key**. 4. Copy your key (starts with `mx_...`). Keep this key secure. It owns the memory space. Use a separate key per account that needs isolated memories. ## Installation ```bash theme={null} pip install hermes-maximem-vity hermes-maximem-vity install ``` `hermes-maximem-vity install` does everything in one step: 1. Copies the plugin into `~/.hermes/plugins/maximem_vity/`, where Hermes discovers it. 2. **Prompts for your API key** and saves it to `~/.hermes/.env` (no duplicates). 3. **Activates** the provider (`memory.provider: maximem_vity`). It prints `✅ All set!` when finished. Start the agent with `hermes`. Already had Hermes (or the gateway) running during install? Restart it to load the newly-activated provider; gateway users run `hermes gateway restart`. ### Non-interactive installs Pass the key as a flag to skip the prompt, useful for scripted or CI setups: ```bash theme={null} hermes-maximem-vity install --api-key mx_your_key ``` System Python (e.g. Homebrew) blocks global `pip install`. Use **pipx** (recommended) or a virtual environment: ```bash theme={null} pipx install hermes-maximem-vity # install pipx first if needed: brew install pipx hermes-maximem-vity install ``` You don't need to match Python versions; the `maximem-vity-sdk` dependency is installed into Hermes' own environment automatically. ### Verify installation ```bash theme={null} hermes-maximem-vity status # plugin installed ✓, SDK available to Hermes ✓ hermes memory status # shows: maximem_vity ← active hermes maximem_vity status # API key set ✓, SDK installed ✓, connection ok ✓ ``` ### Update The installer always overwrites, so re-running it is how you upgrade: ```bash theme={null} pip install -U hermes-maximem-vity && hermes-maximem-vity install ``` ### Remove ```bash theme={null} hermes-maximem-vity uninstall ``` ## Configuration ### API key (required) The API key is a secret and is stored in `~/.hermes/.env`. | Env var | Required | Description | | ----------------- | -------- | ----------------------------------------------------------------- | | `MAXIMEM_API_KEY` | Yes | Your Maximem API key (`mx_...`). `VITY_API_KEY` is also accepted. | The API key owns the memory space. Use a separate key per account that needs isolated memories. **Changing your key**: a plain re-install keeps the existing key (you'll see `already configured`). To replace it, pass the new key explicitly; it's written de-duplicated, so no stale copies are left behind: ```bash theme={null} hermes-maximem-vity install --api-key mx_your_new_key ``` Or edit `~/.hermes/.env` directly, then restart Hermes (gateway users: `hermes gateway restart`). ### Tunables (optional) Non-secret options live in `$HERMES_HOME/vity.json`, which is created on install. | Key | Type | Default | Description | | ------------------- | ------- | ------- | ---------------------------------------------------------------------------- | | `auto_recall` | boolean | `true` | Inject relevant memories before each turn. *Recommended: keep enabled.* | | `auto_capture` | boolean | `true` | Capture the conversation after each turn. *Recommended: keep enabled.* | | `max_recall_tokens` | number | `1000` | Token budget for recalled context. *Higher = more context, more API usage.* | | `min_prompt_length` | number | `5` | Skip recall for very short prompts. *Prevents recall on messages like "hi".* | | `recall_timeout` | number | `6.0` | Max seconds to wait for pre-turn recall before proceeding with no memory. | ### Self-hosted backend (optional) To point the plugin at a non-default Maximem API URL, set `MAXIMEM_ENDPOINT` (or `endpoint` in `vity.json`). ## How it works Once installed and configured, the plugin works automatically: 1. **Recall before each turn**: a semantic search runs against your current message. It is bounded by `recall_timeout` (default 6s): if matches return in time they are injected as context; otherwise the turn proceeds with no memory rather than waiting. 2. **Capture after each turn**: the user/assistant exchange is saved to long-term memory. 3. **Memory mirroring**: when Hermes' built-in memory tool records a fact, it is also stored in Vity so it participates in semantic recall. Recall runs synchronously on the pre-turn path but is time-boxed, so a slow or unhealthy backend degrades to no-memory-this-turn instead of hanging. Capture and memory-mirroring writes run on background threads, so they never block the reply. No manual action needed. Use the tools and CLI below when you want direct control. ## Agent tools The plugin exposes four tools to the agent: | Tool | Parameters | Purpose | | -------------- | ------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | | `vity_recall` | `query` (required), `top_k` (default 10, max 50) | Semantic search of stored memories. | | `vity_profile` | none | Retrieve the user's full stored memory profile. | | `vity_store` | `content` (required), `memory_type` (`fact` / `preference` / `emotion` / `episode` / `knowledge` / `profile`) | Save a new memory. | | `vity_forget` | `query`, `dry_run` (default `true`) | Delete matching memories (previews first). | In chat, this is transparent: ask the agent to "remember that…" and it calls `vity_store`; ask "what do you know about…" and it calls `vity_recall`. No special commands are required. **Example interaction:** ``` You: "Remember that I prefer concise answers and I'm working on the Hermes migration." Agent: Got it, I've saved your preference for concise answers and noted the Hermes migration project. ``` ## CLI commands Manage memory directly from your terminal. ```bash theme={null} hermes maximem_vity status # config + live connection check hermes maximem_vity search "favorite color" # semantic search hermes maximem_vity search "deadlines" --limit 20 --json hermes maximem_vity store "I prefer dark mode" --type preference hermes maximem_vity forget "old project" # dry-run (preview) hermes maximem_vity forget "old project" --yes # confirm deletion ``` To (re)activate the provider, use `hermes config set memory.provider maximem_vity`. Avoid the interactive `hermes memory setup` wizard; buffered or pasted terminal input can drop the selection and leave the provider unset. ## Troubleshooting ### "API key not found" error Confirm the key and connection: ```bash theme={null} hermes maximem_vity status ``` If the connection check fails, re-run the installer with your key: ```bash theme={null} hermes-maximem-vity install --api-key mx_your_key ``` ### `maximem-vity-sdk not installed` even after `pip install` Hermes runs inside its own isolated environment, which is usually **not** the Python that ran `pip install` (e.g. Anaconda or system Python). The SDK must live in Hermes' environment, and `hermes-maximem-vity install` puts it there for you. Just re-run it: ```bash theme={null} hermes-maximem-vity install hermes-maximem-vity status # confirm: SDK available to Hermes ✓ ``` ### Memories not being recalled 1. **Check config**: ensure `auto_recall` is `true` in `$HERMES_HOME/vity.json`. 2. **Check prompt length**: prompts shorter than `min_prompt_length` (default: 5) won't trigger recall. 3. **Verify the provider is active:** ```bash theme={null} hermes memory status ``` Confirm `maximem_vity` shows as active. If not, run `hermes config set memory.provider maximem_vity`. ### Provider unset after setup The interactive `hermes memory setup` wizard can drop pasted input. Set the provider directly instead: ```bash theme={null} hermes config set memory.provider maximem_vity ``` ### Windows: `hermes-maximem-vity` is "not recognized as a command" `pip install --user` on Windows drops the console script in a `Scripts` folder that isn't on `PATH`. Add it for the current PowerShell session, then run install: ```powershell theme={null} $env:Path += ";$(python -m site --user-base)\Python$($(python -c 'import sys;print(f"{sys.version_info.major}{sys.version_info.minor}")'))\Scripts" hermes-maximem-vity install ``` Or call it by full path once: `& "$(python -m site --user-base)\Python313\Scripts\hermes-maximem-vity.exe" install`. To make it permanent, add that `Scripts` folder to your user `PATH` in **Environment Variables**. ## Support * **Documentation**: [docs.maximem.ai/vity](https://docs.maximem.ai/vity) * **API keys**: [app.maximem.ai/api-keys](https://app.maximem.ai/api-keys) * **Email**: [support@maximem.ai](mailto:support@maximem.ai) * **Twitter / X**: [@MaximemAI](https://twitter.com/maximem_ai) # Vity MCP Server Source: https://docs.maximem.ai/vity/mcp/overview Connect any MCP-capable agent (Claude Code, Cursor, and more) to your Vity memory with a URL and an API key. No code. Your AI tools each start from zero. This connects them all to the same memory. The Vity MCP server exposes your memory over the [Model Context Protocol](https://modelcontextprotocol.io), so any MCP-capable agent can recall what you have told it before, look up what you already know, save new facts, and write in your voice. You paste a URL and an API key; the agent discovers the tools and decides when to call them on its own. Building your own product? You want the [Synap MCP server](/integrations/mcp) instead: that one gives *your agent's users* memory, and takes a `synap_` token. This page is Vity: memory for **you**, in the AI tools you personally use, with an `mx_` key. ## Connection details | | | | ------------- | ------------------------------------- | | **URL** | `https://vity-mcp.maximem.ai/mcp` | | **Transport** | Streamable HTTP | | **Auth** | `Authorization: Bearer mx_...` header | ## What your agent can do Nine tools, in four groups. See [Tools & prompts](/vity/mcp/tools) for arguments and per-tool guidance. `recall_context` for a ready-to-use context block, `get_user_context` for a structured profile on a topic, `search_memories` for specific remembered facts. `query_knowledge` returns what you know about a subject and how deeply, so the agent can pitch its answer at the right level. `get_voice_card` returns how you write; `rewrite_as_me` restyles a draft in your voice; `summarize` gives you a summary that skips what you already know. `remember` saves one durable fact; `capture_conversation` saves a whole session so it survives into later ones. Also available: three prompts (`write_as_me`, `brief_me_on`, `catch_me_up`) and two resources (`vity://voice-card`, `vity://knowledge/topics`). ## Get your API key Generate it in the **Vity** dashboard at [app.maximem.ai](https://app.maximem.ai), not the Synap dashboard at synap.maximem.ai. They are separate products with separate keys: Vity keys start with `mx_`, Synap keys start with `synap_`. A `synap_` key will be rejected here. 1. Sign up or log in at [app.maximem.ai](https://app.maximem.ai). 2. Open **Settings → API Keys**. 3. Click **Generate New Key**. 4. Copy your key (starts with `mx_...`). Keep this key secure. It grants full read and write access to your memory vault; treat it like a password, and use a separate key per tool so you can revoke one without breaking the others. ## Connect your client ```bash theme={null} claude mcp add --transport http vity https://vity-mcp.maximem.ai/mcp \ --header "Authorization: Bearer mx_..." ``` Confirm it connected: ```bash theme={null} claude mcp list ``` `vity` should appear as connected. Inside a session, `/mcp` shows the same thing along with the available tools. Add this to `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` in a project: ```json theme={null} { "mcpServers": { "vity": { "url": "https://vity-mcp.maximem.ai/mcp", "headers": { "Authorization": "Bearer mx_..." } } } } ``` Reload Cursor, then check **Settings → MCP** for a green indicator next to `vity`. Most MCP clients read the same JSON shape. Point them at the URL and attach the key as a header: ```json theme={null} { "mcpServers": { "vity": { "url": "https://vity-mcp.maximem.ai/mcp", "headers": { "Authorization": "Bearer mx_..." } } } } ``` If your client asks for a transport, choose **Streamable HTTP** (sometimes listed as "HTTP" or "remote server"). Do not choose SSE or stdio. ### Check it works Ask your agent something that can only come from memory: ``` What do you know about me? ``` It should call `recall_context` or `get_user_context` and answer from your vault. If it answers "I don't have any information about you", the connection is fine but the vault is empty; see [Troubleshooting](#troubleshooting). ## Client compatibility This server authenticates with an API key in a request header, so it works with any client that lets you set custom headers: Claude Code, Cursor, and most CLI and desktop MCP clients. Clients that only support OAuth 2.1 connectors and give you nowhere to put a header (currently the **Claude.ai** and **ChatGPT** connector UIs) cannot connect to this server yet. OAuth support is planned. In the meantime, use a client that accepts headers, or the [OpenClaw](/vity/openclaw) and [Hermes](/vity/hermes) plugins. ## Your data stays yours Your API key *is* your identity here. The tools resolve whose memory to read from the key alone; none of them takes a user, account, or vault argument, so there is no request an agent could construct, accidentally or otherwise, that reaches anyone else's memory. What that means in practice: * **A key only ever reaches its own vault.** Sharing a key shares that vault; revoke it in the dashboard to cut access immediately. * **Nothing is written unless a write tool is called.** `remember` and `capture_conversation` are the only tools that store anything. * **Deletion is not exposed over MCP.** Deleting memories is a dashboard action, so no agent can remove your memory on its own initiative. See [Deleting memories](/vity/mcp/tools#deleting-memories). ## Rate limits and credits Limits apply per API key, per minute. Most retrieval tools allow 60 calls per minute; the LLM-backed ones are lower. `search_memories` and `query_knowledge` cost 1 credit per call, `summarize` costs 20, and `rewrite_as_me` is metered with a ceiling of 1,000 rewrites per day. Everything else is free. See the [per-tool table](/vity/mcp/tools#tools-at-a-glance) for exact figures, and [Pricing](/resources/pricing) for credit packs. When a limit is hit, the tool returns a plain-language message the agent can act on (wait and retry for a rate limit, top up for credits), rather than a protocol error. ## Troubleshooting The header did not reach the server. Check that your config uses the exact header name `Authorization` and the value `Bearer mx_...`, including the word `Bearer` and the space after it. In Claude Code, re-add the server with `--header` quoted as a single argument. The key is wrong, was revoked, or belongs to the wrong product. Confirm it starts with `mx_` (a `synap_` key is for [Synap](/integrations/mcp) and will not work here), then generate a fresh one at [app.maximem.ai](https://app.maximem.ai) and update your client config. Confirm the URL includes the `/mcp` path and that the transport is set to Streamable HTTP rather than SSE. Restart the client after editing its config; most read MCP config only at startup. Your vault may genuinely have nothing on that subject yet. Memory builds up as you use Vity, so a new account starts empty; save something with `remember` and search again. If you expect a match, the relevance floor may be too high. Ask the agent to retry `search_memories` with `min_score` around `0.15`. Just captured a conversation? Extraction runs in the background; give it a few seconds before searching. `get_voice_card` and `rewrite_as_me` need a voice profile built from your writing. Build one in the dashboard, then retry. `search_memories`, `query_knowledge` and `summarize` consume credits. Top up at [app.maximem.ai](https://app.maximem.ai); retrying without topping up will keep failing. Still stuck? Email [support@maximem.ai](mailto:support@maximem.ai). # Tools & prompts Source: https://docs.maximem.ai/vity/mcp/tools Every tool, prompt and resource the Vity MCP server exposes, with arguments, costs and guidance on which to reach for. Your agent discovers these automatically once the [server is connected](/vity/mcp/overview) and decides when to call them from their descriptions. This page is for when you want to know exactly what it has to work with, or want to ask for a specific tool by name. Every tool acts only on the memory belonging to the API key on the request. None of them takes a user or account argument. ## Tools at a glance | Tool | What it gives you | Rate limit | Credits | | ----------------------------------------------- | -------------------------------------------------- | ---------------- | ------- | | [`recall_context`](#recall_context) | A ready-to-use block of your relevant memories | 60/min | — | | [`search_memories`](#search_memories) | Ranked individual memories with their text and IDs | 60/min | 1 | | [`get_user_context`](#get_user_context) | A structured, grounded profile of you on a topic | 20/min | — | | [`query_knowledge`](#query_knowledge) | What you know about a subject, and how deeply | 60/min | 1 | | [`get_voice_card`](#get_voice_card) | How you write, as a spec the agent can follow | 30/min | — | | [`summarize`](#summarize) | A summary of a page or text, personalised to you | 20/min | 20 | | [`remember`](#remember) | Saves one durable fact about you | 30/min | — | | [`rewrite_as_me`](#rewrite_as_me) | Your draft, restyled in your own voice | 30/min, 1000/day | Metered | | [`capture_conversation`](#capture_conversation) | Saves a whole conversation for later recall | 120/min | — | ## Choosing between similar tools Four pairs look alike from the outside. The difference matters for both speed and quality of the answer: | If you want... | Use | Not | | -------------------------------- | ----------------- | ------------------------------------------------------------- | | Background to answer with, fast | `recall_context` | `get_user_context`: richer, but several seconds slower | | A specific remembered fact | `search_memories` | `recall_context`: returns prose, not individual entries | | Your depth on a subject | `query_knowledge` | `search_memories`: returns what you said, not what you know | | The agent to write in your voice | `get_voice_card` | `rewrite_as_me`: unless you want the engine to do the styling | | To keep one fact | `remember` | `capture_conversation`: that is for a whole exchange | *** ## Recall ### `recall_context` Returns your relevant memories as prose the agent can read straight into its answer. This is the default tool for personalising a response: it is the fastest of the recall tools and needs no parsing. What you just asked or said. Retrieval matches against this, so the agent should pass your actual words rather than an extracted keyword. Budget for the returned context, between 100 and 10000. Raise it for broad questions. `hybrid` (recommended), `semantic` (meaning only), or `recency` (newest first, best for "what was I working on recently"). Where the conversation is happening: `web`, `slack`, `telegram`, `whatsapp`, or `unknown`. ### `search_memories` Returns matching memories individually, with their text and IDs. Use it for specific recall ("what did I decide about the vendor", "what's my sister's name"), rather than general background. Costs 1 credit per call. A natural-language description of what you are looking for. How many results to return, between 1 and 20. Narrow to one of `preference`, `fact`, `task`, `relationship`, or `context`. Relevance floor between 0.0 and 1.0. Raise it to cut noise; lower it to around `0.15` if a query you expect to match returns nothing. ### `get_user_context` A structured profile of you in relation to some text or topic: what you know, where you stand, what you have already been through, and explicit guidance on what to emphasise and avoid. Richer and slower than `recall_context`: it takes several seconds. Worth it when the stakes justify the wait: drafting something under your name, advising on a decision, or prepping for a conversation. Every claim in the profile is checked against a real memory before you see it. Anything unsupported is dropped rather than guessed at. The material to profile you against: the message you received, the document you are reacting to, or the topic at hand. What the agent is about to produce, e.g. `"reply to this email"` or `"prep for a negotiation"`. Steers what the profile emphasises. Budget for the memory context behind the profile, between 100 and 4000. *** ## Knowledge ### `query_knowledge` Returns what you actually know about a subject, graded by expertise level and confidence, drawn from everything you have read and discussed. Distinct from `search_memories`: that returns text you said, this returns knowledge extracted from it. Use it to calibrate an explanation: an agent that knows you are already expert on a topic can skip the basics. Costs 1 credit per call. Semantic query. Omit it to browse by filters alone (relevance scores are then absent). Restrict to categories, e.g. `["technology", "finance"]`. Up to 20. Restrict to any of `high`, `medium`, `low`. `user_possesses_knowledge` (you know it) or `user_accessed_knowledge` (you merely encountered it, weaker evidence, and not to be treated as expertise). Only return items at or above this extraction confidence, 0.0 to 1.0. Also return how your knowledge is distributed across categories, useful for "what do I know about in general". Also return your aggregate knowledge profile. Results per page, between 1 and 50. Page with `offset` rather than maxing this out. Page offset. Relevance floor. *** ## Voice ### `get_voice_card` Returns your writing-voice profile (register, sentence rhythm, punctuation and casing habits, emoji use), so the agent can write to that spec itself. Takes no arguments. Best called before composing anything that goes out under your name. Needs a voice card built from your writing. If you do not have one yet, build it in the dashboard at [app.maximem.ai](https://app.maximem.ai). ### `rewrite_as_me` Hands your draft to Vity's voice engine and returns the same message as you would have written it. Use this when you want the engine to do the styling; use `get_voice_card` when you would rather have the spec and let the agent write. Takes a few seconds, and is limited to 1,000 rewrites per day per key. The message to rewrite. Write the full content first: the engine restyles what it is given, it does not invent what you leave out. Where it will be sent: e.g. `slack`, `email`, `linkedin`, `x`. Changes the register. Who it is going to, e.g. `"my manager"`, `"a close friend"`. Changes formality. ### `summarize` Summarises a page or a block of text, personalised against what you already know, so it skips the familiar and flags what is new to you. Costs 20 credits and takes several seconds. For a plain summary of text the agent already has in front of it, this is the wrong tool; use it when the personalisation or the URL fetch is the point. The page to fetch and summarize. Either this or `text` is required. Raw text to summarize instead of fetching a URL. Title of the source, if known. Output shape. For sources too large for one pass, request the next chunk by resending the same call with `segment` incremented. The result says when there is more. *** ## Capture ### `remember` Saves one durable fact about you to long-term memory: a preference, a decision, a relationship, an ongoing project. Stored statements are written to stand alone, because they will be read months later with none of the surrounding conversation. "Prefers async standups over daily calls", not "yes I do". The statement to remember, up to 10,000 characters. One of `preference`, `fact`, `task`, `relationship`, `context`. Improves later recall. `low`, `medium`, or `high`. ### `capture_conversation` Saves a whole conversation so the important parts survive into later sessions. Vity decides what is worth keeping, so nothing needs filtering first. Processing is asynchronous: the tool returns immediately and the memories become searchable a few seconds later. A search run instantly after a capture may not see them yet. Up to 50 messages, each `{"role": "user" | "assistant", "content": "...", "timestamp": }`. `timestamp` is optional. Where the conversation happened: `web`, `slack`, `telegram`, `whatsapp`, `unknown`. Optional identifier for which agent held the conversation. ## Deleting memories There is no delete tool over MCP, by design: an agent cannot remove your memory on its own initiative, or as cleanup it decided was helpful. Delete memories in the dashboard at [app.maximem.ai](https://app.maximem.ai), where you can see exactly what is going before it goes. ## Prompts Prompts are reusable instructions your client can offer as a slash command or template. All three chain the tools above in the right order, so you get a better result than asking for the same thing freehand. | Prompt | Argument | What it does | | ------------- | -------- | ------------------------------------------------------------------------------------------------------------ | | `write_as_me` | `what` | Drafts something in your voice, grounded in what you actually think and have done. | | `brief_me_on` | `topic` | Briefs you on a topic, calibrated to what you already know, skipping the familiar, leading with what is new. | | `catch_me_up` | `topic` | Summarises where you left off: what you had decided, and what is still open. | In Claude Code, prompts appear as `/vity:write_as_me`, `/vity:brief_me_on` and `/vity:catch_me_up`. Other clients surface them under their own prompt or template menu. ## Resources Two resources can be attached as context rather than called as tools: | Resource | Contents | | ------------------------- | --------------------------------------------- | | `vity://voice-card` | Your writing voice, same as `get_voice_card`. | | `vity://knowledge/topics` | A map of what you know about, by category. | Not every MCP client sends your API key when reading a resource. Where that happens, the resource returns a pointer to the equivalent tool instead of failing silently. **Tools are the reliable path**: prefer `get_voice_card` and `query_knowledge` if you have the choice. # OpenClaw Source: https://docs.maximem.ai/vity/openclaw Memory plugin for OpenClaw: syncs AI context across OpenClaw, ChatGPT, Claude, Gemini, Manus, and more. OpenClaw's memory is local. This plugin makes it universal. Sync your AI context across OpenClaw, ChatGPT, Claude, Gemini, Manus, and many more, so you never re-explain yourself again. ## Features * **Long-term memory**: Saves what is relevant for personalizing your OpenClaw and other AI app experiences as memories from your OpenClaw usage; in a private cloud-vault with encryption-at-rest and secure interfaces. Auto-consolidates and forgets stale information. Securely stores facts, preferences, episodes, goals, constraints, and more. * **Short-term memory**: Determines short-term memory needs from OpenClaw usage such as conversation summaries, tasks, facts, preferences, procedures, and more. Auto-converts them to long-term memory periodically. * **Data privacy & security**: You own all your data and only you can read your information. All LLM calls are made in secure mode. You have granular control to forget or delete what you need to. * **Cross-platform sync**: Save context from ChatGPT, Claude, Gemini, Manus, Perplexity, and other flows into your cloud-vault and use in OpenClaw automatically. * **Cross-channel memory**: Memories persist across Telegram, Slack, WhatsApp, Discord, and all supported channels. * **Auto-recall**: Automatically injects relevant memories before each LLM turn. * **Auto-capture**: Automatically stores conversation context after each turn. * **Slash commands**: `/remember` and `/recall` for direct memory interaction. * **Agent tools**: `maximem_store`, `maximem_search`, `maximem_forget` for agent-driven memory operations. * **CLI commands**: Terminal-based memory management with `openclaw maximem`. ## API key You'll need a Maximem API key to use this plugin. 1. Sign up or log in at [app.maximem.ai](https://app.maximem.ai). 2. Navigate to **Settings → API Keys**. 3. Click **Generate New Key**. 4. Copy your key (starts with `mx_...`). Keep this key secure. It grants access to your memory vault. ## Installation ```bash theme={null} openclaw plugins install @maximem/memory-plugin ``` That's it. The plugin is now installed. ### Verify installation ```bash theme={null} openclaw plugins list ``` You should see `memory-plugin` in the list with status `enabled`. If something isn't working: ```bash theme={null} openclaw plugins info memory-plugin openclaw plugins doctor ``` ## Configuration ### Option 1: Environment variable (recommended) Set your API key as an environment variable. This keeps it out of config files. ```bash theme={null} echo 'export MAXIMEM_API_KEY="mx_..."' >> ~/.zshrc source ~/.zshrc ``` ```bash theme={null} echo 'export MAXIMEM_API_KEY="mx_..."' >> ~/.bashrc source ~/.bashrc ``` ```powershell theme={null} [System.Environment]::SetEnvironmentVariable("MAXIMEM_API_KEY", "mx_...", "User") ``` Then restart your terminal. ### Option 2: Config file Edit `~/.openclaw/openclaw.json`: ```json theme={null} { "plugins": { "entries": { "memory-plugin": { "enabled": true, "config": { "apiKey": "mx_...", "autoRecall": true, "autoCapture": true, "maxRecallTokens": 1000, "minPromptLength": 5, "captureDebounceMs": 2000 } } } }, "agents": { "list": [{ "id": "main", "tools": { "allow": ["maximem_store", "maximem_search", "maximem_forget"] } }] } } ``` ### Configuration options | Option | Type | Default | Description | | ------------------- | ------- | ------- | --------------------------------------------------------------------------------------------------- | | `apiKey` | string | none | Your Maximem API key. **Required** (or set `MAXIMEM_API_KEY` env var). | | `autoRecall` | boolean | `true` | Automatically inject relevant memories before each turn. *Recommended: keep enabled.* | | `autoCapture` | boolean | `true` | Automatically capture conversation context after each turn. *Recommended: keep enabled.* | | `maxRecallTokens` | number | `1000` | Maximum tokens for recalled memory context (100 to 10000). *Higher = more context, more API usage.* | | `minPromptLength` | number | `5` | Minimum prompt length to trigger recall. *Prevents recall on very short messages like "hi".* | | `captureDebounceMs` | number | `2000` | Debounce window for batching captures (milliseconds). *Lower = faster capture, more API calls.* | ## Usage ### Quick start Once installed and configured, the plugin works automatically: 1. **You chat** with OpenClaw on any channel (WhatsApp, Slack, Telegram, etc.). 2. **Vity recalls** relevant context from your AI history, including past ChatGPT, Claude, and Perplexity conversations. 3. **OpenClaw responds** with full awareness of your cross-platform context. 4. **Vity captures** new information for future sessions. No manual action needed. It just works. Use the commands below when you want direct control. ### Slash commands Use these commands directly in any supported chat channel. #### `/remember [text]` Manually save information to long-term memory. ``` /remember My favorite programming language is TypeScript /remember I prefer dark themes for all applications /remember Project deadline is March 15, 2026 ``` **Response:** ``` ✓ Saved to memory: "My favorite programming language is TypeScript" ``` #### `/recall [query]` Search your long-term memory. ``` /recall favorite programming language /recall project deadline /recall preferences ``` **Response:** ``` Found 2 relevant memories: 1. "My favorite programming language is TypeScript" (saved 2 days ago) 2. "I prefer dark themes for all applications" (saved 1 week ago) ``` ### CLI commands Manage memories from your terminal. #### Search memories ```bash theme={null} openclaw maximem search "favorite color" openclaw maximem search "deadlines" --limit 20 openclaw maximem search "preferences" --json ``` **Options:** * `-l, --limit `: Maximum results (default: 10) * `--json`: Output as JSON for scripting ### Agent tools Enable these tools in your agent configuration to let OpenClaw manage memory autonomously: ```json theme={null} { "agents": { "list": [{ "id": "main", "tools": { "allow": ["maximem_store", "maximem_search", "maximem_forget"] } }] } } ``` With these enabled, your agent can: | Tool | What it does | | ---------------- | ------------------------------------------------------------------ | | `maximem_store` | Save important facts when you share information. | | `maximem_search` | Recall memories when answering questions about past conversations. | | `maximem_forget` | Remove specific memories when you ask it to. | **Example interaction:** ``` You: "Forget that I said I like Python. I've switched to Rust." Agent: Done. I've removed the memory about Python and noted your preference for Rust. ``` ## Troubleshooting ### "API key not found" error Ensure your API key is set correctly: ```bash theme={null} echo $MAXIMEM_API_KEY ``` This should output your key (starting with `mx_...`). If it's empty: 1. Re-run the export command from the [Configuration](#configuration) section. 2. Restart your terminal. 3. Try again. ### Memories not being recalled 1. **Check config**: ensure `autoRecall` is `true`. 2. **Check prompt length**: messages shorter than `minPromptLength` (default: 5 characters) won't trigger recall. 3. **Verify plugin status:** ```bash theme={null} openclaw plugins list ``` Confirm `memory-plugin` shows as `enabled`. ### Plugin not loading Run diagnostics: ```bash theme={null} openclaw plugins doctor ``` This checks for common configuration issues and missing dependencies. ### Context not syncing from other platforms Make sure you have: 1. The Maximem Vity Chrome extension installed for web-based LLMs. 2. Logged into the same Maximem account in both the extension and this plugin. 3. Enabled sync for the platforms you want (ChatGPT, Claude, etc.) in your Vity settings. ## Coming soon * `openclaw maximem stats`: view memory statistics and usage. * `openclaw maximem wipe`: bulk delete memories with filters. * Category-based filtering in search. ## Support * **Plugin site**: [memoryplugin-for-openclaw.com](https://memoryplugin-for-openclaw.com) * **Issues & bugs**: [GitHub Issues](https://github.com/gauravmaximem/moltbot-memory-plugin-maximem/issues) * **Email**: [support@maximem.ai](mailto:support@maximem.ai) * **Twitter / X**: [@MaximemAI](https://twitter.com/maximem_ai) # Maximem Vity Source: https://docs.maximem.ai/vity/overview Maximem Vity gives the AI apps you already use a shared, persistent memory. Install a plugin or browser extension and your context follows you across ChatGPT, Claude, Gemini, OpenClaw, and more, with no code. Maximem Vity is memory for the AI apps you already use. Where [Maximem Synap](/getting-started/overview) is developer infrastructure you build into your own agent, Vity is for end users: install a plugin or browser extension into a host product and your AI context syncs across platforms automatically, so you never have to re-explain yourself. Building your own agent? You want [Maximem Synap](/getting-started/overview) and its [SDK integrations](/integrations/overview). Vity is for adding memory to the apps you use day to day. ## What Vity does * **Cross-platform memory**: your facts, preferences, and history follow you across ChatGPT, Claude, Gemini, Manus, Perplexity, OpenClaw, and more. * **Automatic recall and capture**: relevant context is injected before each turn and new context is saved after, with no manual steps. * **You own your data**: memory lives in a private cloud vault with encryption at rest. You can recall, forget, or delete anything at any time. ## Available for One connector for every MCP-capable agent, including Claude Code and Cursor. Recall, knowledge, voice, and capture as tools. Memory plugin for OpenClaw. Syncs context across OpenClaw, ChatGPT, Claude, Gemini, Manus, and more. Persistent, cross-session semantic memory for the Hermes Agent. Recalls and captures automatically each turn. A Maximem Vity browser extension brings the same memory to web-based LLMs such as ChatGPT, Claude, Gemini, and Perplexity. More host apps are rolling out. ## Vity vs Synap | | Vity | Synap | | ----------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------ | | **Who it's for** | People using AI apps | Developers building agents | | **Install** | An MCP connector, a host product's plugin manager, or a browser extension | `pip install` / `npm install` | | **Configuration** | API key plus host config | SDK initialization in code | | **Memory hooks** | Automatic (auto-recall, auto-capture) | Explicit SDK calls or [integrations](/integrations/overview) | If you are building your own agent, see [Synap integrations](/integrations/overview). If you want memory inside an app you already use, install the Vity plugin for it.