Agentic AI AcademyAgentic AI Academy
AgentCore Memory/Lesson 4 of 5

Using the Memory SDK

MemorySessionManager, events, retrieval, and the LLM turn helper

Intermediate 14 minBuilder
What you'll be able to do
  • 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
At a glance

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.

  1. 1Three SDK layers: pick the right altitude
  2. 2Create a memory with strategies, then poll to ACTIVE
  3. 3Writing events and retrieving long-term records
  4. 4The MemorySessionManager flow (recommended)
  5. 5process_turn_with_llm: retrieve, generate, store in one call
  6. 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.

LayerImportScopeUse it when
MemorySessionManager (recommended)bedrock_agentcore.memory.sessionMulti-session / multi-actorYou manage many actors/sessions from one process — the default choice for production agents
MemorySessionbedrock_agentcore.memory.sessionA single actorId + sessionIdYou're inside one conversation and want a tidy session-scoped handle (you get one from the manager)
MemoryClientbedrock_agentcore.memoryLower-level wrapper over both planesYou 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.

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

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

SDK MemoryClient wraps the poll for you with create_memory_and_wait (and the SDK keyword is strategies, not memoryStrategies):

python
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 by actorId + sessionId. Each event carries one or more messages, each with a role (USER / ASSISTANT / TOOL / OTHER).
  • Read derived insight with retrieve_memory_records (boto3 RetrieveMemoryRecords) — this queries the long-term records a strategy extracted, scoped by namespace, ranked by a semantic searchQuery, capped at topK.

Raw boto3 — the conversational payload shape and searchCriteria:

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

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

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

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

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

ExceptionTypical causeWhat to do
ResourceNotFoundExceptionWrong memory_id, memory not ACTIVE yet, or wrong actor/sessionVerify the ID and that you polled to ACTIVE before reading/writing
ValidationExceptionBad 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
ThrottlingExceptionExceeded a per-operation TPS quota (e.g. CreateEvent limits, RetrieveMemoryRecords cap)Back off with jitter and retry
AccessDeniedExceptionMissing IAM permission (e.g. namespace-scoped condition keys block the action)Fix the role/policy
NoCredentialsErrorNo AWS credentials resolved in the environmentConfigure credentials/role
python
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 environment

Watch 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).

  1. Create + poll. Using bedrock-agentcore-control, call create_memory with one userPreferenceMemoryStrategy (namespace template /users/{actorId}/preferences/). Poll control.get_memory(memoryId=...)['memory']['status'] in a loop until it is ACTIVE. Print the elapsed time. (Or use MemoryClient.create_memory_and_wait(...) and confirm it blocks until ACTIVE.)
  2. Write a turn (manager). Build a MemorySessionManager, call create_memory_session(actor_id="user-1", session_id="s-001"), and add_turns([ConversationalMessage("I'm vegetarian and allergic to peanuts", MessageRole.USER)]).
  3. 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).
  4. 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.
  5. Repeat with MemoryClient. Do the same write/read with client.create_event(memory_id, actor_id, session_id, messages=[("...", "USER")]) and client.retrieve_memories(memory_id, namespace=..., query=..., top_k=5). Confirm both layers hit the same records.
  6. 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 the ValidationException. Write three sentences distinguishing the three causes of an empty long-term read: no strategy, extraction lag, and wrong namespace.

Key takeaways

  1. 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).
  2. 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.
  3. 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`.
  4. 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. 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)`.
  6. 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.
  7. 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