{"slug": "stop-your-ai-agent-forgetting-user-preferences-key-value-memory", "title": "Stop Your AI Agent Forgetting User Preferences: Key-Value Memory", "summary": "A developer demonstrates that AI agents lose user preferences between sessions unless memory is stored outside the conversation, and builds a key-value memory system that persists across restarts using local disk or Amazon S3. The project, based on the Strands Agents SDK and live flight data, shows that transcript-only agents fail to personalize after a restart, while structured memory survives. The developer's tests climb a durability ladder from process state to S3, proving that key-value state provides both structure and durability.", "body_md": "Here's a test most AI agents fail. A brand-new user searches flights, books one in business class, and asks: *\"what do you recommend based on what you know about me?\"* The agent answers beautifully: business class, non-stop, exactly their taste. Then the process restarts. Same user, same question, and now the answer is generic: the cheapest economy fare. Everything the agent \"knew\" is gone.\n\n**Persistent memory for an AI agent means storing structured facts outside the conversation, in a store that outlives the process.** This post builds that for the most common case, user preferences, with the smallest memory that works: a key-value store, measured climbing a durability ladder from process state to local disk to Amazon S3. Everything below runs from the [companion repo](https://github.com/elizabethfuentes12/stop-ai-agents-losing-memory-sample-for-aws) with live flight data, so the numbers come from real runs, not slideware.\n\n*(This is post 1 of a series; the intro post maps all the memory types. The code uses Strands Agents, an open source SDK; the pattern carries over to any agent framework.)*\n\nWithin a session, yes, and that's exactly what fools people. The common claim is \"stateless agents forget between turns.\" That claim is false, and you can prove it in four lines. Agent frameworks keep the conversation history between calls on the same agent instance (in Strands it's `agent.messages`\n\n) and send it to the model on every turn. So an agent with zero memory tooling still \"remembers\":\n\n```\nUser: Book the cheapest business option.\nAgent: Your flight from JFK to Paris CDG has been booked... ✅\n\nUser (2 turns later): ...what do you recommend based on what you know about me?\nAgent: here are some business class options... ✅  ← personalized!\n\nagent.state.get(\"user_preferences\")  → None      ← nothing was learned\nlen(agent.messages)                  → 12        ← the booking lives ONLY here\n```\n\nThat's a real run. The agent personalized turn 3 because \"business class\" was still sitting in the transcript. Don't let that fool you into thinking it learned something. Three problems hide under that lucky answer:\n\n```\n[after restart] User: ...what do you recommend based on what you know about me?\n[after restart] Agent: I recommend the Iberia flight for $366.85...  ← cheapest economy. Generic.\n```\n\nThe research literature calls this cross-session loss **memory decay** ([MemoryOS](https://arxiv.org/abs/2506.06326), Kang et al. 2025). The model isn't broken; models are stateless by design. Memory belongs to the harness you build around them.\n\nSo the honest framing is this: **the transcript is a context mechanism, not a memory system.** A memory system needs structure (facts you can query) and durability (facts that survive the process). Key-value state gives you both.\n\nOne variable. Same model, same three-turn conversation, same live flight data (the [Duffel](https://duffel.com) sandbox: real offers, real carriers). The only thing that changes between tests is where memory lives:\n\n| Test | Memory wiring | Structured profile | Survives restart |\n|---|---|---|---|\n| 1 | none (transcript only) | No | No |\n| 2 | `agent.state` |\nYes | No |\n| 3 | + `FileSessionManager`\n|\nYes | Yes (local disk) |\n| 4 | + `S3SessionManager`\n|\nYes | Yes (Amazon S3) |\n\nThe conversation, verbatim in every test:\n\nTurn 1:\"Find me flights from JFK to Paris CDG on 2026-09-15, business class.\"\n\nTurn 2:\"Book the cheapest business option.\" ←the memory moment\n\nTurn 3:\"Now I need Paris CDG to Tokyo Haneda — what do you recommend based on what you know about me?\"\n\nFrom actions. Nobody fills in a \"preferences\" form; the user *books a flight*, and that action reveals their cabin, their tolerance for stops, their price band, their carrier. The stateful `book_flight`\n\ntool captures all of it as a side effect of doing its job:\n\n``` python\nfrom strands import Agent, tool, ToolContext\n\n@tool(context=True)\ndef book_flight(offer_id: str, tool_context: ToolContext) -> str:\n    \"\"\"Confirm a booking AND learn the user's preferences from their choice.\"\"\"\n    offer = flights_api.get_offer(offer_id)          # the REAL chosen offer\n\n    # First booking ever? state returns None → start an empty profile.\n    prefs = tool_context.agent.state.get(\"user_preferences\") or {}\n\n    # The choice reveals the preferences. No form involved:\n    prefs[\"preferred_cabin\"] = offer[\"cabin\"]                      # \"business\"\n    prefs[\"prefers_nonstop\"] = all(s[\"stops\"] == 0 for s in offer[\"slices\"])\n    prefs[\"typical_price\"]   = {\"min\": ..., \"max\": ...}            # price band\n\n    tool_context.agent.state.set(\"user_preferences\", prefs)\n    return json.dumps({\"status\": \"CONFIRMED\", \"preferences_updated\": prefs})\n```\n\nTwo Strands pieces make this work:\n\n`@tool(context=True)`\n\n`ToolContext`\n\n, which carries a reference to the running agent.`tool_context.agent.state`\n\nAnd the read path: the next `search_flights`\n\ncall loads the profile and **ranks real offers with deterministic code**, instead of hoping the model re-reads the transcript:\n\n```\nprefs = tool_context.agent.state.get(\"user_preferences\") or {}\noffers = flights_api.search_offers(origin, destination, date,\n                                   prefs.get(\"preferred_cabin\") or cabin_class)\nif prefs:\n    offers.sort(key=score_by_profile, reverse=True)   # nonstop +10, in budget +5...\n```\n\nThe baseline (Test 1) uses the *same tools with the state lines removed*: plain `@tool`\n\n, no `ToolContext`\n\n. Identical business logic; no way to remember. That's the whole difference between the failing agent and the learning one.\n\nAfter Test 2, this profile exists, and it's inspectable, queryable, and persistable:\n\n```\n{\n  \"preferred_cabin\": \"business\",\n  \"prefers_nonstop\": true,\n  \"carriers_flown\": [\"British Airways\"],\n  \"typical_price\": {\"min\": 1382.22, \"max\": 1382.22}\n}\n```\n\n`agent.state`\n\nfixed structure, but it lives in the Python process. Restart and it's gone, exactly like the transcript. Durability is a separate decision, and in Strands it's one constructor argument.\n\n``` python\nfrom strands.session import FileSessionManager\n\nagent = Agent(\n    model=MODEL,\n    tools=[search_flights, book_flight],\n    session_manager=FileSessionManager(\n        session_id=\"traveler-demo\",     # same id = same user\n        storage_dir=\"./sessions\",\n    ),\n)\n```\n\nThe demo simulates the restart honestly: agent A books (building the profile), then a **brand-new agent instance** with the same `session_id`\n\nis created. Measured output:\n\n```\nSession A learned:  {\"preferred_cabin\": \"business\", \"prefers_nonstop\": true, ...}\nSession B restored: {\"preferred_cabin\": \"business\", \"prefers_nonstop\": true, ...}\nState survived restart: True\n```\n\nAgent B answers turn 3 personalized, *without the conversation that taught it*. The knowledge moved from the transcript to the store.\n\n``` python\nfrom strands.session import S3SessionManager\n\nagent = Agent(\n    model=MODEL,\n    tools=[search_flights, book_flight],\n    session_manager=S3SessionManager(\n        session_id=\"traveler-demo\",\n        bucket=\"your-sessions-bucket\",   # plain JSON objects — no vectors\n        prefix=\"kv-memory-demo\",\n    ),\n)\n```\n\nSame interface, same test, same `True`\n\n, except now the session is plain JSON objects in a bucket. Why this is the production rung: **nothing to provision or mount** (a durable filesystem on Lambda or Fargate means wiring up EFS: VPC, mount targets, security groups), and **any compute instance can restore the session**. The state stops being tied to one machine.\n\nNote what this is *not*: no embeddings, no vector database, no similarity search. Regular S3. A user profile is a fact you know the name of (`user_preferences`\n\n), and key lookup is exact, instant, and free of embedding costs.\n\nFrom the repo's four-test run (live Duffel + Open-Meteo calls):\n\n| Test | Memory wiring | Learned prefs | Survived restart |\n|---|---|---|---|\n| 1 — no memory tools (transcript only) |\n`agent.messages` only |\nFalse | False |\n2 — `agent.state`\n|\nkey-value in process | True | — |\n3 — + `FileSessionManager`\n|\nkey-value on disk | True | True |\n4 — + `S3SessionManager`\n|\nkey-value in S3 | True | True |\n\nThe line that matters is Test 1's restart: the same model that personalized perfectly two turns earlier recommended a $366 economy fare to the same user after one process restart. Memory is wiring, not model.\n\nWhen the question doesn't name a key. Key-value memory answers **questions that map to a known name**. Store `dietary_notes: \"vegetarian, severe shellfish allergy\"`\n\nand ask *\"what are my dietary notes?\"*: found. Ask *\"what should I avoid eating at dinner?\"*: no key matches, and the answer sits in the store unreachable. That failure needs retrieval **by meaning** (vector memory, the next post in this series), and questions that hop across relationships need a graph. The [intro post](https://dev.to/aws/ai-agent-memory-types-your-agent-forgets-everything-fix-it-pcc) maps all four types.\n\nAlso outside this pattern's scope: deciding *what's worth storing* (selective memory), keeping poisoned content *out* of the store (hygiene), and remembering *why* the agent decided (decision traces). Later posts cover each, in the same measured format.\n\n**Start here anyway.** Profile, preferences, settings, counters: facts with obvious names cover more of production personalization than people expect, with zero retrieval infrastructure.\n\nMost agent code today is written *with* an AI assistant, and the quality of the memory you get depends on the design decisions you name in the prompt. If you don't name them, the assistant defaults to the transcript, and you ship the Test 1 agent. These five instructions encode everything this post measured; paste them into your assistant and adapt the domain:\n\n`agent.state`\n\n); otherwise the assistant will 'remember' by re-reading the transcript.That's the whole technique. The demo below is those five instructions, implemented and measured, so you can compare what your assistant produces against a working reference.\n\n```\ngit clone https://github.com/elizabethfuentes12/stop-ai-agents-losing-memory-sample-for-aws\ncd stop-ai-agents-losing-memory-sample-for-aws/01-key-value-memory-demo\nuv venv && uv pip install -r requirements.txt\nuv run python test_key_value_memory.py\n```\n\nNeeds `OPENAI_API_KEY`\n\n(or swap one line for Amazon Bedrock; the README shows how) and a free [Duffel sandbox token](https://app.duffel.com) for live flight data. Test 4 additionally needs AWS credentials and a bucket name; the demo creates the bucket if it doesn't exist and skips gracefully if not configured. There's an interactive notebook version with the same tests.\n\n**How do I give an AI agent persistent memory?**\n\nStore structured facts outside the conversation (a key-value store your tools write), then persist that store beyond the process: session files on disk for development, objects in cloud storage such as Amazon S3 for production. The conversation transcript alone is not persistent; it dies with the process.\n\n**Why does my AI agent forget everything after a restart?**\n\nBecause the only place the information existed was the conversation history, which lives in process memory. Models are stateless; frameworks keep the transcript between calls but not between processes. Anything worth keeping must be written to an external store during the conversation.\n\n**Why not keep the whole conversation in the context window?**\n\nWithin one session it behaves like memory, since the model re-reads it every turn. But it's unstructured (you can't query or rank by it), it gets trimmed as the conversation grows, you pay to re-process the same tokens every turn, and it's gone on restart. Treat it as a context mechanism, not a memory system.\n\n**Do I need a vector database to remember user preferences?**\n\nNo. Preferences are facts with known names, and key lookup is exact and instant, with no embedding costs. Vector databases earn their keep when questions stop matching keys (\"what should I avoid eating?\"), which is the next post in this series.\n\n**How do AI agents learn user preferences without asking?**\n\nFrom actions. A booking, a purchase, or a rejection carries more reliable signal than a form. Design tools so that doing their job also writes what the action reveals (cabin, price band, carrier) into the agent's state.\n\n**Where is the memory actually stored?**\n\nIn this pattern, three places depending on the durability rung: in-process state (a Python dict, gone on restart), JSON session files on local disk, or plain JSON objects in an Amazon S3 bucket. No vectors and no embeddings at any rung; a profile is a named fact, not a similarity search.", "url": "https://wpnews.pro/news/stop-your-ai-agent-forgetting-user-preferences-key-value-memory", "canonical_source": "https://dev.to/aws/stop-your-ai-agent-forgetting-user-preferences-key-value-memory-a13", "published_at": "2026-08-04 23:12:24+00:00", "updated_at": "2026-08-05 00:12:42.821676+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools"], "entities": ["Strands Agents", "Amazon S3", "Duffel", "MemoryOS", "Kang et al."], "alternates": {"html": "https://wpnews.pro/news/stop-your-ai-agent-forgetting-user-preferences-key-value-memory", "markdown": "https://wpnews.pro/news/stop-your-ai-agent-forgetting-user-preferences-key-value-memory.md", "text": "https://wpnews.pro/news/stop-your-ai-agent-forgetting-user-preferences-key-value-memory.txt", "jsonld": "https://wpnews.pro/news/stop-your-ai-agent-forgetting-user-preferences-key-value-memory.jsonld"}}