BKStart a conversation ↗

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.

The write path: one turn, two layers during the request in the background a user or agent turn CreateEvent(actor, session) short-term session mon · raw turns semantic preference summary episodic extract extract extract extract consolidate consolidate consolidate consolidate new · update · nothing new /facts/aoife/ /preferences/aoife/ /summaries/aoife/mon/ /fixes/aoife/ /fixes/ reflections across many episodes, later
The request only writes the event. Everything to the right of the dashed line happens afterwards, without the user waiting for it. Each strategy runs its own extraction prompt over the same new turns and files the result under its own namespace. Consolidation is why a corrected fact replaces the old one instead of sitting next to it.

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 environment

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

The read path: before every model call Sync's broken again. MessageAddedEvent search /facts/aoife/ top_k 5 · score ≥ 0.4 search /summaries/aoife/ top_k 2 · score ≥ 0.4 search /preferences/aoife/ top_k 3 · no threshold in parallel, query = the message <user_context> prepended the request tools system prompt earlier turns context + question tools and system prompt untouched: the cached prefix survives
Every user message triggers one semantic search per configured namespace, with the message itself as the query. What clears each threshold is joined into one block and put in front of the user's text. The parts of the request you want cached never change.

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.

Thursday's question, two ways tools system forty Monday turns → replay re-sent on every model call, and longer after every conversation memory user_context + question cached prefix new this turn consolidated records stay small; transcripts only grow
The same question, the same tools and the same system prompt. Replaying history sends all of Monday on every call. Memory sends four lines, and they sit after the cache point rather than in front of it.

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.

Who can see what, on Ciarán's Thursday aoife /facts/aoife/ /preferences/aoife/ /summaries/aoife/mon/ /summaries/aoife/thu/ /fixes/aoife/ episodes never read for ciarán ciarán /facts/ciaran/ /preferences/ciaran/ /summaries/ciaran/ /fixes/ciaran/ episodes read by the session manager · empty episodes feed /fixes/ reflections, from everyone's episodes recall_similar_fixes the one node that crosses users: keep it impersonal
Everything inside a dashed boundary is one user's, and the namespace string is the only thing that enforces it. The red box is the deliberate exception: lessons from Aoife's Monday reach Ciarán, and nothing else about Aoife does.

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.