What Should Your Agent Remember? AgentCore Memory, Worked Through
Monday, 09:40. Aoife opens the support chat for a desktop app with cloud sync. Sync has been failing since the morning. Over forty minutes the agent establishes the app version (3.2), the OS (macOS) and the network (the office). It suggests a reinstall, which does nothing. Then it reads the sync log, sees TLS handshake failures, works out that the company proxy is intercepting the traffic, and walks Aoife through pointing the app at the proxy's certificate. Sync comes back. On the way out, Aoife says: next time just give me the terminal commands, I don't need the tour.
Thursday, 14:15. New chat. "Sync's broken again."
A stateless agent asks which version they're on.
That's the whole problem. Every model call starts from nothing, and the conversation history is the only state there is. Close the chat and it's gone. Keep every transcript and replay it, and by the third conversation you're paying to send the model forty turns of Monday to answer one line on Thursday.
AgentCore Memory is AWS's managed answer to that. This post wires it into a Strands agent for exactly the scenario above and follows what happens to each sentence Aoife types: where it's stored, what it becomes, and what the agent sees on Thursday.
Two kinds of memory, two different keys
AgentCore Memory is one resource with two layers, and the layers are keyed differently. Most of what follows comes down to that.
Short-term memory is the event log, keyed by session. Every turn is written as an event with CreateEvent, tagged with an actorId (who) and a sessionId (which conversation). It's the raw transcript. If Aoife's browser crashes on Monday and the chat reconnects with the same session ID, the agent reloads the conversation and carries on. Open a new session and short-term memory has nothing for you.
Long-term memory is extracted records, keyed by namespace. You attach strategies to the memory resource. After events land, AgentCore runs those strategies in the background: an LLM reads the new turns, extracts anything worth keeping, then consolidates it against what's already stored, deciding whether this is a new record or an update to an existing one. The result is a small set of short text records, embedded for semantic search and filed under a namespace you choose, like /facts/aoife/.
The session dies, the records don't. Thursday's agent is in a fresh session with an empty short-term log, and it can still search everything long-term memory knows about Aoife.
There are four built-in strategies, and each one answers a different question:
| Strategy | Answers | From Monday, it would keep |
|---|---|---|
semanticMemoryStrategy |
What's true about this user? | On v3.2, on macOS; office network has a TLS-intercepting proxy |
userPreferenceMemoryStrategy |
How do they like to be dealt with? | Wants terminal commands, not step-by-step walkthroughs |
summaryMemoryStrategy |
What happened in that conversation? | Sync failed with TLS errors; reinstall didn't help; fixed by trusting the proxy's certificate |
episodicMemoryStrategy |
What did we try, and what worked? | The episode: goal, tools used, the failed reinstall, the fix. Then, across many episodes, reflections |
That right-hand column is illustrative. The real records are worded by the extraction model, not by me, and they'll be a little different every time.
Step 1: the memory resource
This is a one-off setup, not something your agent does on every request. Run it from a script, the console, or agentcore add memory in the AgentCore CLI.
from bedrock_agentcore.memory import MemoryClient
client = MemoryClient(region_name="us-west-2")
memory = client.create_memory_and_wait(
name="SupportAgentMemory",
description="What the support agent remembers about users and fixes",
strategies=[
{"semanticMemoryStrategy": {
"name": "Facts",
"namespaces": ["/facts/{actorId}/"],
}},
{"userPreferenceMemoryStrategy": {
"name": "Preferences",
"namespaces": ["/preferences/{actorId}/"],
}},
{"summaryMemoryStrategy": {
"name": "SessionSummaries",
"namespaces": ["/summaries/{actorId}/{sessionId}/"],
}},
{"episodicMemoryStrategy": {
"name": "Fixes",
"namespaces": ["/fixes/{actorId}/"],
"reflectionConfiguration": {"namespaces": ["/fixes/"]},
}},
],
)
print(memory["id"]) # goes into the agent's environmentThe namespaces are the design. {actorId} and {sessionId} are filled in by the service at extraction time, so facts and preferences live per user, summaries live per user per conversation, and episodes live per user. Reflections are the one deliberate exception. /fixes/ sits above every user's episodes, because the point of a reflection is to learn from all of them.
Namespaces start and end with /. The trailing slash isn't cosmetic. Without it, /facts/ann is a prefix of /facts/anne.
Step 2: the agent
The bedrock-agentcore package ships a Strands session manager that does both halves for you. It writes each message as a short-term event, and before every model call it searches long-term memory and hands the results to the model.
import os
from strands import Agent, tool
from bedrock_agentcore.memory import MemoryClient
from bedrock_agentcore.memory.integrations.strands.config import (
AgentCoreMemoryConfig,
RetrievalConfig,
)
from bedrock_agentcore.memory.integrations.strands.session_manager import (
AgentCoreMemorySessionManager,
)
MEMORY_ID = os.environ["SUPPORT_MEMORY_ID"]
REGION = "us-west-2"
memory_client = MemoryClient(region_name=REGION)
@tool
def recall_similar_fixes(problem: str) -> str:
"""Look up lessons learned from past support cases that resemble this problem."""
records = memory_client.retrieve_memories(
memory_id=MEMORY_ID,
namespace="/fixes/", # exact match: reflections only, not every user's episodes
query=problem,
top_k=3,
)
return "\n".join(r["content"]["text"] for r in records) or "No similar cases yet."
def handle_message(user_id: str, conversation_id: str, text: str) -> str:
config = AgentCoreMemoryConfig(
memory_id=MEMORY_ID,
actor_id=user_id, # who: from your auth layer, never from the request body
session_id=conversation_id, # which conversation
retrieval_config={
"/facts/{actorId}/": RetrievalConfig(top_k=5, relevance_score=0.4),
"/summaries/{actorId}/": RetrievalConfig(top_k=2, relevance_score=0.4),
"/preferences/{actorId}/": RetrievalConfig(top_k=3, relevance_score=0.0),
},
)
with AgentCoreMemorySessionManager(agentcore_memory_config=config, region_name=REGION) as memory:
agent = Agent(
system_prompt=SUPPORT_PROMPT,
tools=[read_sync_log, recall_similar_fixes],
session_manager=memory,
)
return str(agent(text))Three choices in there are worth slowing down for, because each is the difference between memory that helps and memory that sits there.
The summaries key is /summaries/{actorId}/, not /summaries/{actorId}/{sessionId}/. The strategy writes one summary per session. The session manager reads with namespacePath, which is hierarchical: it returns every record at or below that path. Use the full session-scoped namespace as the retrieval key, which is what the setup examples show, and on Thursday you're searching for a summary of Thursday's conversation, which doesn't exist yet. Drop the last segment and you search across all of Aoife's past conversations, which is the whole reason you wanted summaries.
Preferences get relevance_score=0.0. Retrieval is a semantic search using the user's latest message as the query. "Sync's broken again" is a good match for Monday's summary and for "office network has a TLS-intercepting proxy". It is not a good match for "prefers terminal commands", so a threshold would quietly filter out exactly the preference you need on Thursday. A user's preferences are a handful of records and are relevant to every reply, so take the top few unconditionally. (The session manager skips the score filter when the threshold is falsy, so 0.0 means "no filter".)
Reflections are a tool, not automatic context. The session manager only does hierarchical retrieval, and a path query on /fixes/ would also return every user's raw episodes, Monday's chat included. Reflections live at /fixes/, so an exact namespace= lookup returns only them. Making that a tool also lets the model decide when a past fix is worth looking up, instead of paying for a search on every "thanks!".
What the model actually sees on Thursday
Aoife types "Sync's broken again." Strands appends the message, which fires a MessageAddedEvent. The session manager's hook catches it, takes that text as the search query, and fires one RetrieveMemoryRecords call per namespace in parallel. It drops anything under each namespace's threshold and joins what's left into a single block. That block is inserted at the front of the message:
<user_context>
Sync failed with TLS handshake errors on Monday; reinstalling did not help; fixed by trusting the office proxy's certificate.
User runs version 3.2 on macOS.
User's office network uses a proxy that intercepts TLS.
User prefers terse answers with terminal commands rather than step-by-step walkthroughs.
</user_context>
Sync's broken again.So the reply can skip the interview:
Same TLS errors as Monday? If you're on the office network, the proxy's certificate may have rotated. Check with:
openssl s_client -connect sync.example.com:443 -proxy proxy.corp:8080 | head -20
Version, OS and network never had to be asked for, there's no walkthrough, and nobody suggests the reinstall again.
Why it goes in the user message
The memory block goes into the user turn, not the system prompt, and that matters more than it looks. If you've read the prompt caching post, you know a cache hit needs a byte-identical prefix: tools, then system prompt, then history. Put per-turn memories in the system prompt and the prefix changes on every call, so nothing after the tools is ever a hit. Put them in the newest user message and the tools, system prompt and earlier turns stay cacheable. The memory is the new part of the request, and the new part of the request is the only part you should be paying full price for.
The other cost is plain size. Thursday's request carries four lines of context, not Monday's forty turns. That gap grows with every conversation Aoife has, because long-term memory consolidates (a changed fact updates the record) while a transcript only ever gets longer.
Ciarán's Thursday
Same Thursday, different user. Ciarán has never contacted support. The sync log shows TLS handshake failures.
Nothing in /facts/ciaran/ or /summaries/ciaran/ helps, because there's nothing there. Aoife's facts are in Aoife's own namespace and Ciarán's retrieval never touches them. But by now the episodic strategy has processed Monday's chat as a completed episode: the goal, the reinstall that failed, the log read, the certificate fix. Across that episode and others like it, it has written a reflection along the lines of:
For sync failures that show TLS handshake errors, check for a TLS-intercepting proxy before suggesting a reinstall. Reinstalling has not resolved this class of failure.
The agent sees TLS errors, calls recall_similar_fixes("sync failing, TLS handshake errors"), and goes straight to the proxy. Ciarán never sits through a reinstall.
This is the part I find most useful, and it's the part that's easiest to undervalue. Facts and preferences make an agent polite. Episodes and reflections make it better at the job. The agent learns which approaches work from outcomes, without anyone retraining anything or editing the system prompt.
It's also where you need to think hardest. Reflections deliberately cross user boundaries, and AWS's docs call out the privacy implications of cross-actor analysis. If a reflection could carry something personal ("the user at Acme Corp…"), either reflect at the actor level or put a guardrail on what gets stored. Lessons should travel between users. Details about a user shouldn't.
Memory is not your knowledge base
The fix for "the proxy cert rotated" lives in your product docs, not in memory. Long-term memory answers who is this user and what happened before. Retrieval over your documentation, a Bedrock Knowledge Base or anything else, answers what is true right now. AWS draws the same line in its comparison. Put manuals in memory and they get "consolidated" by an LLM. Put user history in your RAG index and every user's chat ends up in one corpus.
Things that will bite
Extraction is asynchronous. Long-term records appear some time after the events that produced them. AWS doesn't publish a number. One measurement of a direct write saw 15–30 seconds before it became searchable, and episodes wait until AgentCore decides the episode is finished. Within a session that's fine, because short-term memory already has the turn. It's not fine in an integration test that writes a turn and immediately asserts on retrieval. Poll with a timeout.
Retrieval failures are silent. If a namespace is wrong, IAM denies the call, or the service has a bad minute, the SDK logs it and returns an empty list. The session manager catches anything else that goes wrong. Your agent carries on with no <user_context> and no error, and it just seems a bit dim. Alarm on the logs or on the memory metrics, because no user will ever report "it forgot me".
The namespace string is your tenant boundary. Nothing checks that actor_id belongs to the caller. Take it from your authenticated identity, never from the request body, and back it up with IAM: the bedrock-agentcore:namespace and bedrock-agentcore:namespacePath condition keys can pin a role to its own subtree.
Batching needs the with block. batch_size above 1 buffers events locally. If the session manager isn't closed, by the context manager or close() in a finally, the buffered turns never reach memory, and nothing they contained is ever extracted.
One agent per session. Two agents writing to the same session_id get a warning and a confused transcript. If you have a supervisor and sub-agents, give each its own session and share knowledge through long-term namespaces.
Verdict
Turning on memory is the easy part, and nearly free. The design work is deciding what your agent should remember and who it should share it with, and in AgentCore that design is the namespace layout. Write it down before you write the agent: per-user facts and preferences, per-conversation summaries read across conversations, per-user episodes, and one carefully considered shared node for lessons.
Get that right and the payoff isn't that Thursday's agent remembers Aoife's version number. It's that Ciarán's agent already knows not to suggest the reinstall.