Task 3 · 8 tasks

AgentCore Memory

Long-term, per-user memory that learns facts across sessions and keeps users apart.

15 minEasy
Alice’s ask

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.

Platform

What the platform provisions for you

terminal
uv run bootcamp.py up 3
  • An AgentCore Memory resource with three extraction strategies: UserFacts (semantic), UserPreferences (user preference) and SessionSummaries (summary)
  • Its MEMORY_ID and one id per strategy (MEMORY_STRATEGY_ID, MEMORY_PREFERENCE_STRATEGY_ID, MEMORY_SUMMARY_STRATEGY_ID), visible in status and 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.

You

What you do as a developer

  1. Read the code: who is calling?

    Memory is partitioned by actor, so the actor must be the person really calling. invoke --actor alice-chen signs 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 its username claim.

    phase2/app/agent/agent.py
    ACTOR_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-chen and gets Alice's preferences, exactly Alice's “Jordan could see my preferences” bug. A token is different: Cognito signs it, so its username cannot be changed without breaking the signature. Never trust caller-supplied identity; take it from something the platform verified.

    Question: token_claims decodes 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 Authorization header is forwarded. The same decode in a service with no authorizer in front would be a hole.

  2. 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 with ListEvents). 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 only
    invoke --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 needed
    StrategyTypeExtractsNamespaceIn your stack
    SemanticSEMANTICFacts (“Alice owns the Q3 report”)per actorDeployed: UserFacts
    User preferenceUSER_PREFERENCEChoices and style (“answers in French”, “numbers in thousands”)per actorDeployed: UserPreferences
    SummarySUMMARIZATIONA running summary of one session, by topicper actor and sessionDeployed: SessionSummaries
    EpisodicEPISODICEpisodes (goal, steps, outcome) plus reflections across themper actor (and session)Not deployed: more extraction calls per turn; try it in Experiments
    Custom / self-managedCUSTOMA built-in strategy with your own extraction/consolidation prompt and model (override), or your own pipeline fed by events (self-managed)you chooseNot 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/pat matches pat and pat/sessions/..., while the older namespace= parameter is a plain string prefix that also matches pat-x. The Strands session manager and bootcamp.py memory use namespacePath.

  3. Read the code: what the agent retrieves

    phase2/app/agent/agent.py
    MEMORY_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_SCORE 0.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.

  4. 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 include memories: 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 has actor_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) returns memoryRecordSummaries[].content.text. The runtime role already allows ListMemoryRecords. Return [] when there are none.

    Solution
    phase2/app/agent/agent.py
    import 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 memories is normally []. Try it by hand after invoke "Please remember: I prefer reports as Python code." --actor alice-chen; extraction takes about 1-2 minutes. An actor_id other than the signed-in user is a FAIL: it would mean users share memories.

    terminal
    uv 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 --json

    invoke prints the answer, then one compact key: value line per extra reply field (actor_id: alice-chen, memories: [...], later tools_used: [...]), cut to one line each. Add --json for the full raw reply.

  5. 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
    terminal
    uv 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-chen
    uv run bootcamp.py memory, for the check test user (shortened)read only
    memory 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, polls ListMemoryRecords (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...
  6. Experiments

    • Run uv run bootcamp.py memory --actor alice-chen after 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 ALWAYS to MEMORY_RELEVANCE_SCORE, deploy, and ask a data question: is the French preference still followed?
    • What would you trade by raising top_k to 20, or the relevance score to 0.7?
    • Add an EPISODIC strategy in terraform/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.
  7. Re-test

    For now, check the memory resource. Re-run it after up 4 and your change to see the challenge lines pass (the preference one takes 2-5 minutes, mostly waiting for extraction):

    terminal
    uv run bootcamp.py test --only 3
    expected [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

terminal
uv run bootcamp.py test --only 3

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