Using the Memory SDK
MemorySessionManager, events, retrieval, and the LLM turn helper
- Choose the right SDK layer — `MemorySessionManager`, `MemorySession`, or `MemoryClient` — for a given task
- Create a memory with strategies on the `bedrock-agentcore-control` plane and poll `get_memory` status until `ACTIVE`
- Write conversational events with `create_event` / `add_turns` and retrieve long-term records with `retrieve_memory_records` / `search_long_term_memories`
- Wire the full `MemorySessionManager` flow: `create_memory_session` → `add_turns` → `search_long_term_memories`
- Use `process_turn_with_llm` with a `retrieval_config` and `llm_callback` for one-call retrieve-generate-store
- Handle eventual consistency (async extraction lag) and the `ResourceNotFoundException` / `ValidationException` / `ThrottlingException` failure modes
The `bedrock_agentcore.memory` Python SDK gives you three layers for talking to AgentCore Memory: `MemorySessionManager` (recommended), `MemorySession` (session-scoped), and the lower-level `MemoryClient` (labeled 'legacy' but fully functional). This lesson is hands-on: create a memory with strategies on the control plane and poll it to `ACTIVE`, write conversational events, retrieve long-term records by namespace and search query, and use the one-call `process_turn_with_llm` retrieve-generate-store helper — while coding defensively around extraction lag and the common exceptions.
- 1Three SDK layers: pick the right altitude
- 2Create a memory with strategies, then poll to ACTIVE
- 3Writing events and retrieving long-term records
- 4The MemorySessionManager flow (recommended)
- 5process_turn_with_llm: retrieve, generate, store in one call
- 6Eventual consistency and the exceptions you'll hit
Three SDK layers: pick the right altitude
The Python SDK package bedrock_agentcore.memory exposes three layers, each at a different altitude. They all ultimately drive the same two API planes (bedrock-agentcore-control for create/update/get/delete memory, bedrock-agentcore for events and records) — they just differ in how much plumbing they hide.
| Layer | Import | Scope | Use it when |
|---|---|---|---|
MemorySessionManager (recommended) | bedrock_agentcore.memory.session | Multi-session / multi-actor | You manage many actors/sessions from one process — the default choice for production agents |
MemorySession | bedrock_agentcore.memory.session | A single actorId + sessionId | You're inside one conversation and want a tidy session-scoped handle (you get one from the manager) |
MemoryClient | bedrock_agentcore.memory | Lower-level wrapper over both planes | You need fine-grained control, or you're following older tutorials/samples — labeled legacy / not recommended but fully functional and widely shown |
The mental model: MemoryClient is the thin wrapper over the raw boto3 operations (it maps create_event, retrieve_memories, get_last_k_turns, strategy management, etc. onto the two planes). MemorySessionManager sits above it and gives you session objects so you stop passing memory_id / actor_id / session_id into every call. MemorySession is the per-conversation handle the manager hands back.
# Layer 3 (lower-level, 'legacy' but functional)
from bedrock_agentcore.memory import MemoryClient
client = MemoryClient(region_name="us-east-1")
# Layers 1 & 2 (recommended)
from bedrock_agentcore.memory.session import (
MemorySessionManager, MemorySession,
ConversationalMessage, BlobMessage, MessageRole, RetrievalConfig,
)
manager = MemorySessionManager(memory_id="mem-123", region_name="us-east-1")
session = manager.create_memory_session(actor_id="user-123", session_id="session-456")Key insight
'Legacy' does not mean 'broken'
MemoryClient is explicitly labeled legacy / not recommended in the docs, yet it is the layer most existing samples and blog posts use, and every method still works. Don't rewrite a working MemoryClient integration just to chase the label — but reach for MemorySessionManager on new code, because it removes the repetitive memory_id/actor_id/session_id threading that's a common source of bugs.
Create a memory with strategies, then poll to ACTIVE
A memory resource is created on the control plane (bedrock-agentcore-control). It does not become usable instantly — CreateMemory returns with status CREATING, and you must poll get_memory until status is ACTIVE before writing or reading. CreateMemory accepts name, description, eventExpiryDuration (days; min 7, max 365, SDK default 90), the strategy list, an optional memoryExecutionRoleArn (required for override/self-managed strategies), and an optional encryptionKeyArn for a customer-managed KMS key (otherwise AgentCore uses a service-managed key).
Raw control-plane (boto3) — note the boto3 key is memoryStrategies:
import boto3, time
control = boto3.client('bedrock-agentcore-control')
resp = control.create_memory(
name="ShoppingSupportAgentMemory",
eventExpiryDuration=90, # days; min 7, max 365
memoryStrategies=[
{'summaryMemoryStrategy': {
'name': 'SessionSummarizer',
'namespaceTemplates': ['/summaries/{actorId}/{sessionId}/']}},
{'userPreferenceMemoryStrategy': {
'name': 'UserPreferenceExtractor',
'namespaceTemplates': ['/users/{actorId}/preferences/']}},
])
memory_id = resp['memory']['id']
# Poll until ACTIVE before any data-plane call
while control.get_memory(memoryId=memory_id)['memory']['status'] == 'CREATING':
time.sleep(5)
status = control.get_memory(memoryId=memory_id)['memory']['status']
assert status == 'ACTIVE', f"memory ended in {status}" # CREATING -> ACTIVE | FAILEDSDK MemoryClient wraps the poll for you with create_memory_and_wait (and the SDK keyword is strategies, not memoryStrategies):
from bedrock_agentcore.memory import MemoryClient
client = MemoryClient(region_name="us-east-1")
memory = client.create_memory_and_wait( # blocks until ACTIVE
name="ShoppingSupportAgentMemory",
strategies=[
{'summaryMemoryStrategy': {'name': 'SessionSummarizer',
'namespaceTemplates': ['/summaries/{actorId}/{sessionId}/']}},
],
event_expiry_days=90,
memory_execution_role_arn=None, # required only for override / self-managed strategies
)
memory_id = memory['id']The MemoryClient also offers create_or_get_memory(...) for idempotent setup in app startup code, and per-strategy helpers like add_semantic_strategy / add_summary_strategy / add_user_preference_strategy / add_episodic_strategy (and _and_wait variants) for attaching strategies after creation.
Watch out
No strategies = no long-term records, ever
If you create a memory with an empty strategy list you get a perfectly valid short-term store — raw events you can write and read back — but nothing is extracted. A later retrieve_memory_records / search_long_term_memories will return empty not because of lag, but because there is nothing to extract into. Long-term retrieval only works when a strategy is attached. Up to 6 strategies per memory (not adjustable).
Note
Config-key naming traps
Watch the plural/singular and SDK/CLI splits: boto3 uses memoryStrategies while the SDK keyword is strategies; the namespace field is namespaceTemplates (plural) in the SDK but namespaceTemplate in boto3/console. And the strategy enum differs by surface — SDK summaryMemoryStrategy maps to CLI enum SUMMARIZATION (not SUMMARY).
Writing events and retrieving long-term records
Once the memory is ACTIVE, you work on the data plane (bedrock-agentcore). There are two distinct operations to keep separate in your head:
- Write a turn with
create_event— this stores a raw, immutable short-term event scoped byactorId+sessionId. Each event carries one or more messages, each with a role (USER/ASSISTANT/TOOL/OTHER). - Read derived insight with
retrieve_memory_records(boto3RetrieveMemoryRecords) — this queries the long-term records a strategy extracted, scoped by namespace, ranked by a semanticsearchQuery, capped attopK.
Raw boto3 — the conversational payload shape and searchCriteria:
data = boto3.client('bedrock-agentcore')
# Write a USER turn (payload uses the 'conversational' content + role shape)
data.create_event(
memoryId="mem-123", actorId="customer-456", sessionId="session-789",
payload=[{"conversational": {"content": {"text": "I'm vegetarian and allergic to peanuts"}, "role": "USER"}}])
# Retrieve long-term records by namespace + search query + topK
records = data.retrieve_memory_records(
memoryId="mem-123",
namespace="/users/customer-456/preferences/",
searchCriteria={"searchQuery": "dietary restrictions", "topK": 5})SDK MemoryClient equivalents — create_event takes (text, ROLE) tuples, and retrieve_memories exposes query / top_k / namespace:
client.create_event(
memory_id="mem-123", actor_id="customer-456", session_id="session-789",
messages=[("I'm vegetarian and allergic to peanuts", "USER")])
recs = client.retrieve_memories(
memory_id="mem-123",
namespace="/users/customer-456/preferences/",
query="dietary restrictions",
top_k=5)For short-term reads (the raw turns, not extracted records) use client.get_last_k_turns(...) or client.list_events(...) — those hit ListEvents and are synchronous and immediately consistent, unlike long-term retrieval.
Tip
Namespace must match the template you configured
retrieve_memory_records filters by an exact-or-prefix namespace. If your strategy's namespaceTemplates was /users/{actorId}/preferences/, then for actor customer-456 you query /users/customer-456/preferences/ — the template variables are substituted with the concrete actor/session/strategy IDs. Use the trailing slash to avoid prefix collisions. Query the wrong namespace and you'll get zero hits even though records exist.
The MemorySessionManager flow (recommended)
MemorySessionManager is the layer you should reach for on new agents. You construct it once with the memory_id and region, then mint a MemorySession per conversation. The session object remembers its actor_id / session_id, so writes and reads stop carrying repetitive identifiers.
The canonical flow is three calls: create the session → add turns → search long-term memories.
from bedrock_agentcore.memory.session import (
MemorySessionManager, ConversationalMessage, MessageRole,
)
manager = MemorySessionManager(memory_id="mem-123", region_name="us-east-1")
session = manager.create_memory_session(actor_id="user-123", session_id="session-456")
# Write a USER + ASSISTANT turn as typed ConversationalMessage objects
session.add_turns([
ConversationalMessage("I love apples", MessageRole.USER),
ConversationalMessage("Apples are nutritious!", MessageRole.ASSISTANT),
])
# Later (a different session for the same actor), retrieve what was extracted
memories = session.search_long_term_memories(
query="what food does the user like",
namespace_path="/food/user-123/",
top_k=5)Notice the type system: instead of (text, role) tuples or raw {"conversational": ...} dicts, you pass ConversationalMessage(text, MessageRole.USER) objects (there's also BlobMessage for non-conversational payloads, e.g. tool output blobs). search_long_term_memories is the manager-layer name for the long-term query — same semantics as retrieve_memory_records: a namespace_path, a semantic query, and a top_k cap.
The payoff is plain when you serve many users from one process: build the manager once at startup, and call create_memory_session(actor_id=..., session_id=...) per request. No identifier threading, no accidental cross-actor writes.
Example
Same memory, two sessions, one actor
Long-term records are extracted across sessions for an actor. So a user can tell you 'I love apples' in session-456 on Monday, and on Friday in session-999 your search_long_term_memories(query="what food does the user like", namespace_path="/food/user-123/") surfaces it — provided a semantic or user-preference strategy was attached and extraction has completed. That cross-session recall is the entire point of long-term memory.
process_turn_with_llm: retrieve, generate, store in one call
process_turn_with_llm collapses the most common agent loop — retrieve relevant memories → call the LLM with them → store the new turn — into a single call. You hand it the user's input, a retrieval_config (a map of namespace → RetrievalConfig), and an llm_callback that does the actual model invocation. It returns a tuple of the retrieved memories, the model's response, and the stored event.
from bedrock_agentcore.memory.session import (
MemorySessionManager, RetrievalConfig,
)
manager = MemorySessionManager(memory_id="mem-123", region_name="us-east-1")
session = manager.create_memory_session(actor_id="user-123", session_id="session-456")
def my_llm(user_input: str, retrieved_memories) -> str:
# Build a prompt from the retrieved long-term memories + the user's question,
# call your model (Bedrock, OpenAI, etc.), and return the assistant text.
context = "\n".join(str(m) for m in retrieved_memories)
return call_model(system=f"Known about the user:\n{context}", user=user_input)
memories, response, event = session.process_turn_with_llm(
user_input="What did we discuss?",
llm_callback=my_llm,
retrieval_config={
"support/facts/{sessionId}/": RetrievalConfig(top_k=5, relevance_score=0.3),
})The retrieval_config keys are namespaces (template variables like {sessionId} are substituted for you), and each RetrievalConfig tunes that namespace's retrieval: top_k caps the number of records, relevance_score sets a minimum similarity threshold so low-confidence matches are dropped before they reach your prompt. You can list multiple namespaces to pull, say, both per-session facts and per-actor preferences in one pass.
This helper is convenience, not magic: under the hood it's still search_long_term_memories (or retrieve_memory_records) → your callback → add_turns/create_event. If you need to interleave tool calls or branch the conversation, drop back to the explicit three-call flow.
Tip
relevance_score keeps junk out of the prompt
Setting relevance_score=0.3 on a RetrievalConfig tells AgentCore to discard records below that similarity before they're returned. On a noisy memory this is the difference between a focused system prompt and one stuffed with marginally related facts that confuse the model. Start permissive, then raise the threshold if you see irrelevant memories leaking into responses.
Eventual consistency and the exceptions you'll hit
The single biggest gotcha when coding against Memory: long-term extraction is asynchronous. When you create_event, the raw event is stored synchronously and is immediately readable via get_last_k_turns / list_events. But the extraction pipeline that turns that event into long-term records runs in the background — roughly 20–40 seconds for built-in strategies (retrieval itself is ~200 ms once records exist) (as of writing — verify the live limits page; episodic episodes take longer still). A search_long_term_memories / retrieve_memory_records fired immediately after create_event will often return nothing — not an error, just empty.
Design for this. In tutorials and tests you'll see an explicit wait; in production you retrieve memories that were extracted on earlier turns, so the lag is naturally hidden.
import time
session.add_turns([ConversationalMessage("I'm vegetarian", MessageRole.USER)])
# Extraction is async (~20-40s for built-in strategies). An immediate read may be empty.
time.sleep(40) # acceptable in a test/tutorial; in prod, read records from prior turns
memories = session.search_long_term_memories(
query="dietary restrictions", namespace_path="/users/user-123/preferences/", top_k=5)The exceptions to handle. The SDK surfaces botocore errors; the ones you'll actually see:
| Exception | Typical cause | What to do |
|---|---|---|
ResourceNotFoundException | Wrong memory_id, memory not ACTIVE yet, or wrong actor/session | Verify the ID and that you polled to ACTIVE before reading/writing |
ValidationException | Bad payload shape, namespace doesn't match a template, eventExpiryDuration out of the 7–365 range, too many strategies (>6) | Fix the request; don't retry as-is |
ThrottlingException | Exceeded a per-operation TPS quota (e.g. CreateEvent limits, RetrieveMemoryRecords cap) | Back off with jitter and retry |
AccessDeniedException | Missing IAM permission (e.g. namespace-scoped condition keys block the action) | Fix the role/policy |
NoCredentialsError | No AWS credentials resolved in the environment | Configure credentials/role |
from botocore.exceptions import ClientError, NoCredentialsError
try:
recs = client.retrieve_memories(memory_id="mem-123",
namespace="/users/customer-456/preferences/", query="diet", top_k=5)
except ClientError as e:
code = e.response["Error"]["Code"]
if code == "ResourceNotFoundException":
... # bad id / not ACTIVE
elif code == "ValidationException":
... # bad request shape — do not blindly retry
elif code == "ThrottlingException":
... # back off + retry with jitter
elif code == "AccessDeniedException":
... # IAM / namespace-scoped condition keys
else:
raise
except NoCredentialsError:
... # no AWS credentials in the environmentWatch out
Empty results are not always lag
Three different causes produce an empty long-term read, and they need different fixes: (1) no strategy attached — nothing is ever extracted; (2) extraction lag — records will appear in ~20–40s; (3) wrong namespace — records exist but you queried the wrong path. Before adding a time.sleep, confirm a strategy exists and the namespace matches the configured template. TPS quotas and exact lag are [VOLATILE] — verify the live limits page rather than hard-coding numbers.
Try it: Build a memory, write a turn, and prove eventual consistency
Goal: stand up an AgentCore Memory with a strategy, write a conversational turn, and observe the async extraction lag firsthand using both the recommended MemorySessionManager and the lower-level MemoryClient.
Prereqs: AWS credentials with AgentCore Memory permissions, pip install bedrock-agentcore boto3, and a region from the supported list (e.g. us-east-1).
- Create + poll. Using
bedrock-agentcore-control, callcreate_memorywith oneuserPreferenceMemoryStrategy(namespace template/users/{actorId}/preferences/). Pollcontrol.get_memory(memoryId=...)['memory']['status']in a loop until it isACTIVE. Print the elapsed time. (Or useMemoryClient.create_memory_and_wait(...)and confirm it blocks until ACTIVE.) - Write a turn (manager). Build a
MemorySessionManager, callcreate_memory_session(actor_id="user-1", session_id="s-001"), andadd_turns([ConversationalMessage("I'm vegetarian and allergic to peanuts", MessageRole.USER)]). - Read immediately — expect empty. Right away, call
session.search_long_term_memories(query="dietary restrictions", namespace_path="/users/user-1/preferences/", top_k=5). Record that it returns nothing (this is extraction lag, not an error). - Wait, then re-read.
time.sleep(40)and retrieve again. Confirm a preference record now appears. Note how long it actually took versus the ~20–40s guidance — and check the live limits page rather than trusting the number. - Repeat with
MemoryClient. Do the same write/read withclient.create_event(memory_id, actor_id, session_id, messages=[("...", "USER")])andclient.retrieve_memories(memory_id, namespace=..., query=..., top_k=5). Confirm both layers hit the same records. - Break it on purpose. Query a deliberately wrong namespace (e.g.
/users/user-99/preferences/) and observe an empty result; then pass a malformed payload and catch theValidationException. Write three sentences distinguishing the three causes of an empty long-term read: no strategy, extraction lag, and wrong namespace.
Key takeaways
- 1The SDK has three layers over the same two planes: `MemorySessionManager` (recommended, multi-session/actor), `MemorySession` (session-scoped), and `MemoryClient` (lower-level, labeled 'legacy' but fully functional and used by most samples).
- 2Memory is created on the control plane (`bedrock-agentcore-control` / `create_memory`) and is not usable until status is `ACTIVE` — poll `get_memory` (or use `create_memory_and_wait`). No strategy attached means no long-term records, ever.
- 3Write turns with `create_event` (conversational payload + role; SDK takes `(text, ROLE)` tuples); read derived insight with `retrieve_memory_records` / `search_long_term_memories` scoped by namespace, ranked by `searchQuery`/`query`, capped at `topK`/`top_k`.
- 4The recommended flow is `create_memory_session(actor_id, session_id)` → `add_turns([ConversationalMessage(...)])` → `search_long_term_memories(query, namespace_path, top_k)`; the session object removes repetitive identifier threading.
- 5`process_turn_with_llm(user_input, llm_callback, retrieval_config={ns: RetrievalConfig(top_k, relevance_score)})` does retrieve-generate-store in one call and returns `(memories, response, event)`.
- 6Long-term extraction is asynchronous (~20–40s for built-in strategies as of writing — verify live), so an immediate read after a write may return empty; in production you read records extracted on earlier turns.
- 7Handle `ResourceNotFoundException` (bad id / not ACTIVE), `ValidationException` (bad shape — don't retry), `ThrottlingException` (back off), `AccessDeniedException` (IAM), and `NoCredentialsError`.
Quiz
Lock in what you learned
Check your understanding
0 / 4 answered
1.You call `retrieve_memory_records` immediately after a successful `create_event` and get an empty result, with no exception raised. The memory has a `userPreferenceMemoryStrategy` attached and is `ACTIVE`. What is the most likely explanation?
2.Which sequence is the recommended `MemorySessionManager` flow for writing a turn and then retrieving long-term memories?
3.What does `process_turn_with_llm` do and what does it return?
4.Your code raises `ResourceNotFoundException` on the first `create_event` call right after `create_memory` returned successfully. What is the most likely fix?
Go deeper
Hand-picked sources to keep learning
Concept hub for short-term vs long-term, strategies, and the two API planes (control vs data).
The MemoryClient / MemorySessionManager / MemorySession API surface — create_event, retrieve_memories, process_turn_with_llm.
CreateMemory params (eventExpiryDuration, strategies, encryption) and polling to ACTIVE.
Verify the live values: 6 strategies/memory cap, retention 7–365 days, per-operation TPS (CreateEvent, RetrieveMemoryRecords), extraction token limits.
End-to-end walkthrough including the eventual-consistency wait pattern after writing events.
Source for bedrock_agentcore.memory; cross-check exact method signatures and the latest version on PyPI (VOLATILE).