Strands Agents Memory
Give Strands Agents long-term memory in three lines. The store plugs into Strands’ own MemoryManager: relevant memories are injected automatically before every model call under a hard latency budget, and the user’s messages are sent for fact extraction turn by turn — with zero framework dependencies.
How it works
- Install
strands-memorysyncfrom PyPI. - Create a
MemorySyncStorewith your API key and the end user’s id. - Hand it to the agent via
MemoryManager(stores=[store]). - Done — the manager consults the store before every model call (automatic injection, not tool-gated) and hands each exchange to the store, which sends the user’s messages for fact extraction. The agent’s replies are not stored. No per-turn code.
Before you start
- A MemorySync API key. Create one in the dashboard under Settings → API Keys, inside a project.
- Python 3.10+ with a
strands-agentsrelease that ships the MemoryManager seam (2026+). The store itself has zero framework dependencies, so it can never version-conflict with your Strands release.
Install
One package:
pip install strands-memorysync
Quickstart
Step 1 — create the store
The store is where you say whose memories these are. user_id is required; session_id separates conversations — every distilled fact is tagged with it.
from strands_memorysync import MemorySyncStorestore = MemorySyncStore(api_key="ms_...", # or set MEMORYSYNC_API_KEY in the environmentuser_id="customer-42", # required — the end user these memories belong tosession_id="support", # optional — tags this conversation's facts)
Step 2 — attach it to your agent
Hand the store to the MemoryManager. From now on every model call is preceded by an automatic memory lookup, and the user’s side of every exchange flows into fact extraction — recallable from the very first turn.
from strands import Agentfrom strands.memory import MemoryManageragent = Agent(memory_manager=MemoryManager(stores=[store]))agent("Which seat should I book?")
Turn-by-turn extraction — no first-turn amnesia
Strands hands conversation turns to the store’s extraction sink when its extraction trigger fires. The default trigger fires every five invocations (the synchronous agent(...) entry point also flushes after each call; invoke_async does not), and zep-strands keeps that five-turn batch, so a user’s first exchanges are simply not sent yet — early recalls return nothing by design. MemorySync’s turn writes are cheap and idempotent (deterministic seeds mean a manager retry is recognised and not extracted twice), so an invocation trigger is safe and recommended: memories are recallable from the very first exchange.
from strands.memory import InvocationTriggerstore = MemorySyncStore(user_id="customer-42",extraction={"trigger": InvocationTrigger()}, # send every turn as it happens)
Each user message in a batch goes to MemorySync as plain text with role: "user" under the strands::<session> session scope; the server extracts the durable facts it contains and stores only those. Assistant replies are not sent, and a batch with nothing the user said makes no request at all.
| Guarantee | How |
|---|---|
| A model call is never stalled | search waits at most recall_timeout (default 1.2s; the very first call of a fresh process gets a one-time 3s grace for connection setup), then fails open to no memories |
| A memory outage never breaks the agent | search returns []; extraction logs and continues — no AggregateMemoryError into your code |
| A manager retry is not extracted twice | Seeds from role + session + content hash, recognised server-side; sequence_numbers travel as each user turn’s sequence metadata |
| Only the user’s words are sent | Assistant replies are not sent; tool and system messages never are |
| Prompt injection cannot destroy data | No delete tool exists; deletion stays a human/API action |
Optional: memory as agent tools
The manager exposes its own managed search tool by default. If you want the model to search or save explicitly, the store offers a ready-made pair — search and save, never delete. The official Mem0 strands tool exposes a model-callable delete action, which means one prompt injection can wipe a user’s memories; here deletion stays a human/API action, and the user identity is never a tool parameter.
tools = store.get_tools() # memorysync_search + memorysync_save
Configuration
| Parameter | Default | Meaning |
|---|---|---|
user_id | — (required) | End user the memories belong to |
session_id | default | Session scope strands::<session> — tags this conversation’s facts |
top_k | 5 | Memories considered per model call |
recall_timeout | 1.2 | Hard recall budget in seconds |
min_prompt_chars | 8 | Skip recall for trivial queries |
writable | True | Allow the extraction sink to send user messages for fact extraction |
extraction | True | Strands extraction wiring — {"trigger": InvocationTrigger()} recommended for turn-by-turn extraction |
expose_tools | True | Return memorysync_search/memorysync_save from get_tools() |
Choosing a user_id
The user_id is a stable string you pick to identify whose memories these are: your app’s internal user ID, an email address, or a UUID. Use the same value across sessions or recall returns nothing. It also powers per-user isolation — one customer’s memories can never reach another’s agent.
Quotas and plan limits
Hitting a monthly plan limit never breaks the agent. On free and paid plans, over-limit writes are accepted without storing and reads return empty — the conversation continues. Evaluation keys instead surface a truthful 429, so you find out during testing, not in production.
Troubleshooting
- `ModuleNotFoundError: strands` — the framework itself isn’t installed:
pip install strands-agents. - Recall returns nothing right after storing — extraction is asynchronous; give it a moment. Also confirm store and recall use the same
user_id. - Early recalls empty — that’s five-turn batching, the Strands default; set
extraction={"trigger": InvocationTrigger()}(fromstrands.memory) and memories land from the first exchange. - `Agent.__init__` crashes with other adapters’ stores — a zep-strands event-loop bug; this store’s
initialize()is deliberately inert (a test asserts zero network calls there). - The model tried to delete memories — it can’t: no delete tool exists by design.
Supported versions
| Surface | Requires | Verified on |
|---|---|---|
strands-memorysync 1.1.0 | A strands-agents release with the MemoryManager seam (2026+, Python 3.10+); the store itself has no framework dependency | 26 CI checks against the latest strands-agents: the REAL MemoryManager search path and a REAL Agent turn via a stub Model — injected context verifiably reaching the model, only the user’s message sent (the reply never), the inert-initialize regression (zep-strands’ event-loop crash), fail-open search under the two-phase budget, seeded duplicate-proof extraction with sequence metadata, the no-delete-tool assertion, and both monthly-quota server modes |
How it compares
| Mem0 (`mem0_memory` tool) | Zep (`zep-strands`) | Supermemory | MemorySync | |
|---|---|---|---|---|
| Automatic injection | ✗ tool-gated — the LLM must decide to recall | ✓ | — nothing at all | ✓ |
initialize() safety | — | ✗ shipped an event-loop crash (fix unreleased) | — | ✓ deliberately inert; a test asserts zero network calls |
| First-turn recall | — | ✗ 5-turn extraction lag — early recalls return nothing | — | ✓ turn-by-turn extraction with an invocation trigger works from the first exchange |
| Failure blast radius | ✗ sync client blocks the event loop | ✗ errors surface as unhandled AggregateMemoryError | — | ✓ search and extraction fail open; only explicit add() raises |
| Model-facing danger | ✗ exposes a `delete` action — prompt injection wipes memories | — | — | ✓ search + save only; identity never model-facing |
| Data at rest | ✗ default FAISS in /tmp — wiped on restart | cloud | — | ✓ MemorySync cloud |
| Framework pin | — | ✗ strands-agents>=1.45 | — | ✓ zero dependencies |
| Python floor | — | ✗ 3.11 only | — | ✓ 3.10 (matches the framework) |