Task 3 · 8 tasks
AgentCore Memory
Long-term, per-user memory that learns facts across sessions and keeps users apart.
The “Remember Me, Not Jordan” crisis
Alice is frustrated: “I told the agent last week I prefer Python reports, and today it forgot. Worse, Jordan could see my preferences!” File sessions on one laptop won't cut it. You need memory that persists across sessions and is isolated per user.
What the platform provisions for you
uv run bootcamp.py up 3- An AgentCore Memory resource with three extraction strategies: UserFacts (semantic), UserPreferences (user preference) and SessionSummaries (summary)
- Its
MEMORY_IDand one id per strategy (MEMORY_STRATEGY_ID,MEMORY_PREFERENCE_STRATEGY_ID,MEMORY_SUMMARY_STRATEGY_ID), visible instatusand injected into the agent in Task 4
Adding this stage usually takes ~11 min; the CLI prints progress (and full terraform output with --verbose).
Memory creation is the slow part; read the code below while it runs.
What you do as a developer
Read the code: who is calling?
Memory is partitioned by actor, so the actor must be the person really calling.
invoke --actor alice-chensigns in as Alice (Task 0) and sends her access token; the runtime's JWT authorizer validates it (Task 4), and the agent reads the actor from itsusernameclaim.phase2/app/agent/agent.pyACTOR_CLAIM = "username" """Cognito puts the signed-in user's name in this claim of its access tokens (M2M client tokens have none).""" def actor_from(context: RequestContext) -> str: """The signed-in user, from the verified token: never trust caller-supplied identity (a header anyone can set). Raises: PermissionError: The token names no user (e.g. a machine-to-machine client token). """ actor = token_claims(context).get(ACTOR_CLAIM) if not actor: raise PermissionError(f"the bearer token has no '{ACTOR_CLAIM}' claim: sign in as a user") return str(actor) def token_claims(context: RequestContext) -> dict: """The claims of the request's bearer token ({} when there is none). Decoding without checking the signature is safe only because the runtime's JWT authorizer already verified the signature, issuer, expiry and client before the request reached this code. """ headers = {name.lower(): value for name, value in (context.request_headers or {}).items()} token = headers.get("authorization", "").split(" ")[-1] try: payload = token.split(".")[1] return json.loads(base64.urlsafe_b64decode(payload + "=" * (-len(payload) % 4))) except (IndexError, ValueError): return {}Question: The original guide took the actor from a header the caller sets (
X-Amzn-Bedrock-AgentCore-Runtime-Custom-Actor-Id). What was wrong with that?Answer
Anyone allowed to invoke the agent could type any name into that header and read that user's memories: Jordan sends
alice-chenand gets Alice's preferences, exactly Alice's “Jordan could see my preferences” bug. A token is different: Cognito signs it, so itsusernamecannot be changed without breaking the signature. Never trust caller-supplied identity; take it from something the platform verified.Question:
token_claimsdecodes the token without checking its signature. Isn't that the same mistake?Answer
No, because of where it runs. The runtime's authorizer has already verified the signature, issuer, expiry and client before the request reaches your code, and only the
Authorizationheader is forwarded. The same decode in a service with no authorizer in front would be a hole.Short-term vs long-term memory
AgentCore Memory has two layers, and your agent uses both:
- Short-term: every turn is stored as an event under an actor and a session (
CreateEvent, read back withListEvents). Continuing a session (--session last) reloads its turns: the conversation so far, exact and immediate. - Long-term: strategies read those events in the background and extract records into namespaces (about 1-2 minutes later). Before each model call the agent searches them (
RetrieveMemoryRecords, with your question as the query) and gets the hits as<user_context>, even in a brand-new session.
short-term events vs long-term recordsread onlyinvoke --session S1 --> agent --CreateEvent--> SHORT-TERM: events, per actor + session (raw turns, kept 30 days) | strategies extract in the background (~1-2 min) v LONG-TERM: records, per strategy namespace /strategies/<UserFacts>/actors/alice-chen "Alice owns the Q3 report" /strategies/<UserPreferences>/actors/alice-chen "prefers answers in French" /strategies/<SessionSummaries>/actors/alice-chen/sessions/S1 "<topic>Headcount ...</topic>" | invoke (new session S2) --RetrieveMemoryRecords(query = your question)--> <user_context> before the model call invoke --session last --ListEvents--> the conversation so far (short-term), no extraction neededStrategy Type Extracts Namespace In your stack Semantic SEMANTICFacts (“Alice owns the Q3 report”) per actor Deployed: UserFacts User preference USER_PREFERENCEChoices and style (“answers in French”, “numbers in thousands”) per actor Deployed: UserPreferences Summary SUMMARIZATIONA running summary of one session, by topic per actor and session Deployed: SessionSummaries Episodic EPISODICEpisodes (goal, steps, outcome) plus reflections across them per actor (and session) Not deployed: more extraction calls per turn; try it in Experiments Custom / self-managed CUSTOMA built-in strategy with your own extraction/consolidation prompt and model (override), or your own pipeline fed by events (self-managed) you choose Not deployed: needs a memory execution role (override) or your own SNS/S3 pipeline Each strategy writes to its own namespace, built from its template:
/strategies/{memoryStrategyId}/actors/{actorId}for facts and preferences, plus/sessions/{sessionId}for summaries. The actor id in the path is what keeps Alice's records away from Jordan's, as long as you search by path:namespacePath=.../actors/patmatchespatandpat/sessions/..., while the oldernamespace=parameter is a plain string prefix that also matchespat-x. The Strands session manager andbootcamp.py memoryusenamespacePath.- Short-term: every turn is stored as an event under an actor and a session (
Read the code: what the agent retrieves
phase2/app/agent/agent.pyMEMORY_RELEVANCE_SCORE = 0.3 """Minimum relevance a long-term fact or past-session summary needs to be retrieved into the conversation.""" ALWAYS = 0.0 """No relevance threshold: preferences (language, format, units) apply to every question, whatever it is.""" def long_term_retrieval() -> dict[str, RetrievalConfig]: """Which long-term records to search before each model call: one namespace prefix per strategy, per actor. The session manager fills in {actorId}; a namespace is a path prefix, so the summary entry covers every past session of this actor (`.../sessions/<id>`). """ return { f"/strategies/{MEMORY_STRATEGY_ID}/actors/{{actorId}}": RetrievalConfig( top_k=5, relevance_score=MEMORY_RELEVANCE_SCORE ), f"/strategies/{MEMORY_PREFERENCE_STRATEGY_ID}/actors/{{actorId}}": RetrievalConfig( top_k=5, relevance_score=ALWAYS ), f"/strategies/{MEMORY_SUMMARY_STRATEGY_ID}/actors/{{actorId}}": RetrievalConfig( top_k=2, relevance_score=MEMORY_RELEVANCE_SCORE ), } def memory_session_manager(session_id: str, actor_id: str) -> AgentCoreMemorySessionManager: """Short-term: this session's turns (events). Long-term: this actor's facts, preferences and session summaries.""" config = AgentCoreMemoryConfig( memory_id=MEMORY_ID, session_id=session_id, actor_id=actor_id, retrieval_config=long_term_retrieval() ) return AgentCoreMemorySessionManager(agentcore_memory_config=config, region_name=REGION)One entry per strategy, each a namespace prefix for this actor: the summary entry stops before
/sessions/, so it searches the summaries of all this actor's earlier sessions.Question: Why is
MEMORY_RELEVANCE_SCORE0.3, and why do preferences use no threshold at all?Answer
The original guide used 0.7. But the semantic-memory score for an exact stored fact is about 0.45, so a 0.7 threshold drops everything and nothing is ever recalled. 0.3 keeps relevant facts and summaries while still filtering noise. A preference is different: “answer in French” has nothing in common with “how many people work in Sales?”, so its relevance score is low, yet it applies to every answer. Preferences are few per user, so the agent always takes the top 5.
Show Alice what the assistant knows
Challenge
Alice wants to see what the assistant knows about her. The reply already says who the agent believes it is talking to (
actor_id, from the verified token). Make it also includememories: the long-term facts AgentCore Memory holds for that actor.The agent is deployed in Task 4: do this after
up 4. Until then the check SKIPs with “deploy the agent first”.Hint 1
The reply is the last thing
run_agent()yields, and it already hasactor_id. Facts live in the namespace/strategies/{MEMORY_STRATEGY_ID}/actors/{actor_id}.Hint 2
boto3.client("bedrock-agentcore").list_memory_records(memoryId=..., namespacePath=..., maxResults=20)returnsmemoryRecordSummaries[].content.text. The runtime role already allowsListMemoryRecords. Return[]when there are none.Solution
phase2/app/agent/agent.pyimport boto3 def remembered_facts(actor_id: str) -> list[str]: """Long-term facts AgentCore Memory holds for this actor (at most 20).""" client = boto3.client("bedrock-agentcore", region_name=REGION) namespace = f"/strategies/{MEMORY_STRATEGY_ID}/actors/{actor_id}" response = client.list_memory_records(memoryId=MEMORY_ID, namespacePath=namespace, maxResults=20) return [record["content"]["text"] for record in response.get("memoryRecordSummaries", [])] # last lines of run_agent(): answer = final_answer(str(result or ""), specialist_results) memories = remembered_facts(actor_id) yield {"response": answer, "actor_id": actor_id, "session_id": session_id, "memories": memories}The check signs in as a brand-new user, so
memoriesis normally[]. Try it by hand afterinvoke "Please remember: I prefer reports as Python code." --actor alice-chen; extraction takes about 1-2 minutes. Anactor_idother than the signed-in user is a FAIL: it would mean users share memories.terminaluv run bootcamp.py invoke "What do you know about me?" --actor alice-chen uv run bootcamp.py invoke "What do you know about me?" --actor alice-chen --jsoninvokeprints the answer, then one compactkey: valueline per extra reply field (actor_id: alice-chen,memories: [...], latertools_used: [...]), cut to one line each. Add--jsonfor the full raw reply.Remember how Alice likes her answers
Challenge
Alice wants every answer in French, without repeating it each session. Tell the agent once, then prove a new session follows it, and find the record that makes it happen.
Needs the agent: do this after
up 4. The check SKIPs until then.Hint 1
Which strategy should catch “I prefer ...”? Look at the table above, then at
long_term_retrieval: which namespace does the agent search for it?Hint 2
Extraction is asynchronous: wait 1-2 minutes, then list the records with
uv run bootcamp.py memory --actor <you>. Ask your next question without--session last, so only long-term memory can carry the preference.Solution
terminaluv run bootcamp.py invoke "Please remember for all our future chats: I prefer that you always answer me in French." --actor alice-chen # about 1-2 minutes later: the preference is a long-term record uv run bootcamp.py memory --actor alice-chen uv run bootcamp.py invoke "How many people work in Sales?" --actor alice-chenuv run bootcamp.py memory, for the check test user (shortened)read onlymemory awsworkshop_carol_memory-MuVOx4EstM, actor bootcamp-check-41056d20 short-term (events per session, newest 5): 2 shown bootcamp-check-146ce96c4d0f4a3d835b9b3e3e4ed9b7: 5 events bootcamp-check-6396d962f2284a5eae54a636886f4270: 5 events long-term (records per strategy namespace): SessionSummaries (SUMMARIZATION) /strategies/SessionSummaries-P1zQ6e8yAx/actors/bootcamp-check-41056d20/sessions: 2 records - <topic name="Language Preference"> On 2026-10-08, the user instructed the assistant to always respond in French... - <topic name="User Inquiry and Assistant Capabilities"> On 2026-10-08 at 16:43:58 UTC, the user asked in one... UserFacts (SEMANTIC) /strategies/UserFacts-YOYVt0BfZk/actors/bootcamp-check-41056d20: 1 records - The user prefers to always be answered in French in all future conversations. UserPreferences (USER_PREFERENCE) /strategies/UserPreferences-uTMs9l5HU9/actors/bootcamp-check-41056d20: 1 records - {"context":"Conversational interactions","preference":"Always answer in French","categories":["language",...]}The UserPreferences record is retrieved with no relevance threshold (
ALWAYS), lands in<user_context>, and the prompt tells the model to follow stated preferences. The check does the same with a brand-new user: it states the preference, pollsListMemoryRecords(no model tokens) until the record exists, then asks from a new session and expects French.expected [CHALLENGE] lineread only[CHALLENGE] SKIP stage 3 preference recalled: deploy the agent (up 4) with a USER_PREFERENCE strategy on your memory, then re-run # after up 4: [CHALLENGE] PASS stage 3 preference recalled: stored {"context":"Conversational interactions","preference":"Always... | new session: Je peux vous aider à répondre aux questions sur les employés, les services et les...Experiments
- Run
uv run bootcamp.py memory --actor alice-chenafter a few invokes: compare the short-term sessions and events with the long-term records in each namespace. Which records appear after one turn, and which only after a longer session? - Set
ALWAYStoMEMORY_RELEVANCE_SCORE,deploy, and ask a data question: is the French preference still followed? - What would you trade by raising
top_kto 20, or the relevance score to 0.7? - Add an
EPISODICstrategy interraform/participant/task3_memory.tf(template/strategies/{memoryStrategyId}/actors/{actorId}/sessions/{sessionId}),up 4, and look at its records. Extraction is shared quota in this account: keep it to a few turns.
- Run
Re-test
For now, check the memory resource. Re-run it after
up 4and your change to see the challenge lines pass (the preference one takes 2-5 minutes, mostly waiting for extraction):terminaluv run bootcamp.py test --only 3expected [CHALLENGE] lineread only[CHALLENGE] SKIP stage 3 memories in the reply: the reply has no memories field yet # after your change: [CHALLENGE] PASS stage 3 memories in the reply: signed in as bootcamp-check-8560e352: actor_id=bootcamp-check-8560e352 memories=[]
Check your work
uv run bootcamp.py test --only 3Passes when your memory is ACTIVE with the UserFacts, UserPreferences and SessionSummaries strategies, followed by two [CHALLENGE] lines (SKIP until the agent exists).
Under the hood
AgentCore Memory stores conversation events per session and actor (short-term), and runs extraction strategies asynchronously to build long-term records. Each strategy uses a model to pull out its kind of record (facts, preferences, session summaries), consolidates it with what is already stored (a changed preference replaces the old one), and indexes it for vector retrieval. Strands' AgentCoreMemorySessionManager writes each turn as an event and injects retrieved facts into the context before the model call. Extraction takes a minute or so, so a fact isn't searchable instantly.