
The Day My Agent Met the Same Customer Twice
She'd just told it her preferred warehouse. Five minutes later, it asked again. The fix wasn't a smarter prompt - it was a memory layer that finally did its job.
It happened on a Tuesday.
A customer was on the line with one of our copilots — a long-time buyer, the kind who knows exactly what she wants and gets impatient when she has to repeat herself. She'd told the agent at the start of the call: "Use the Bengaluru-3 warehouse. Always. Don't ask me again."
Five minutes later, the agent asked her again.
She didn't yell. She just sighed. And in that sigh I heard every customer I was about to lose if I didn't fix this.

The thing is, the agent wasn't broken. The model wasn't dumb. The prompt was fine. What was missing was the layer underneath all of those — the one that decides what an agent gets to know about the person on the other end of the line, and for how long.
That layer has a name now. It's called Amazon Bedrock AgentCore Memory. And once I started building on it properly, the "agent met the same customer twice" problem stopped being a problem I had to solve every week.
This is the walk-through I wish I'd had when I started.
The two memories your agent actually has
Before I touched the SDK, I had to get one mental model right.
AgentCore Memory gives your agent two distinct kinds of memory, and confusing them is the source of most of the heartbreak I had in week one.
The first is short-term memory. This is the running conversation — turn by turn, user message, assistant reply, tool call, repeat. It lives inside a session, persists for up to 8 hours of session life or 15 minutes of inactivity, and it's automatic. You don't ask for it. You get it.
The second is long-term memory. This is the part you opt into. It's where insights from conversations get extracted and persisted across sessions, across days, across the lifetime of the user. It's also the part that, if you set it up right, means the customer who told you about Bengaluru-3 on Tuesday doesn't have to tell you again on Friday.

The trick — and it took me a few wasted afternoons to internalize this — is that long-term memory isn't a database you write to. It's something you configure, and the configuration is called a strategy.
Strategies, or: deciding what's worth remembering
You can write your own, but for almost everything I've built, three strategies out of the box have been enough:
- Semantic memory pulls out facts. "The customer prefers Bengaluru-3." "This vendor doesn't accept POs under ₹50,000." Stored as embeddings, searchable later.
- Summarization writes a running summary of a single session, so a downstream agent can catch up without reading the entire transcript.
- User preferences quietly accumulates how a specific person likes to be talked to. Channel, tone, language, formality.

Each strategy needs a namespace — basically a path that says where the extractions live. Namespaces use placeholders like
{actorId} and {sessionId}, and getting the namespace taxonomy right on day one matters more than almost anything else, because changing it later means re-extracting your entire memory store.So here's the moment that fixed Tuesday's customer. I created a memory resource with all three strategies wired in:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import boto3
control = boto3.client("bedrock-agentcore-control", region_name="us-east-1")
memory = control.create_memory(
name="HeartKartCopilotMemory",
description="Memory for the seasonal-retail copilot agents",
eventExpiryDuration=90,
memoryStrategies=[
{
"semanticMemoryStrategy": {
"name": "FactExtractor",
"namespaceTemplates": ["/facts/{actorId}/"],
}
},
{
"summaryMemoryStrategy": {
"name": "SessionSummarizer",
"namespaceTemplates": ["/summaries/{actorId}/{sessionId}/"],
}
},
{
"userPreferenceMemoryStrategy": {
"name": "PreferenceLearner",
"namespaceTemplates": ["/preferences/{actorId}/"],
}
},
],
)
memory_id = memory["id"]The
eventExpiryDuration is the retention window for the raw short-term events. The extracted long-term records have their own lives — once a strategy has pulled out the fact that the customer prefers Bengaluru-3, that fact lives on long after the raw turn it came from is gone.Feeding the memory: every turn is an event
This is the part the quickstart guides skip, and it's where I tripped twice before getting it right.
Every conversational turn — the user said something, the agent replied, a tool was called and returned — is an event. The event is what gets ingested into short-term memory, and it's what the strategies above feed on to do their long-term extraction in the background.
Here's the moment my agent first remembered someone:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from bedrock_agentcore.memory import MemoryClient
client = MemoryClient(region_name="us-east-1")
session_id = "vendor-call-2026-06-04-acme-bolts-001" # must be 33+ chars
actor_id = "customer:priya_natarajan"
client.create_event(
memory_id=memory_id,
actor_id=actor_id,
session_id=session_id,
payload=[
{"role": "user", "content": "Use Bengaluru-3 warehouse. Always. Don't ask me again."},
{"role": "assistant", "content": "Understood — Bengaluru-3 it is, for all your orders going forward."},
],
)Two scars I'll save you.

The session ID has to be at least 33 characters. Short IDs will quietly fail validation on some SDK versions and loudly throw on others. UUIDs work. Long timestamped strings work. Don't be clever about it.
And actor_id is the key that long-term memory hangs everything off, not session_id. If you let your actor identity drift — sometimes a user ID, sometimes an email, sometimes a CRM record — your long-term extractions end up scattered across namespaces that don't unify. Pick the actor identity early and pin it.
Catching up after a cold start

The first place memory paid for itself wasn't even across sessions. It was inside a session, after a Lambda cold start.
Picture it: the user is mid-conversation, you redeploy, a new runtime spins up. Without memory, the next turn lands on a freshly amnesiac agent. With memory, you do this:
1
2
3
4
5
6
recent_turns = client.get_last_k_turns(
memory_id=memory_id,
actor_id=actor_id,
session_id=session_id,
k=10,
)You hydrate the conversation back into context, and the user never notices. The first time this worked in production I genuinely laughed. The agent had been "gone" for forty seconds and came back like nothing had happened.
The moment that actually mattered: across sessions

Three days after that first ingest, Priya called back. Different session, different runtime, possibly a different region for all I knew.
1
2
3
4
5
6
preferences = client.retrieve_memories(
memory_id=memory_id,
namespace=f"/preferences/{actor_id}/",
query="warehouse and shipping preferences",
top_k=5,
)And there it was. Bengaluru-3. The agent prepended the preference into its system prompt, opened the call with "Hi Priya — I've got Bengaluru-3 set as your default, want me to use that?", and the call took two minutes instead of seven.
That's the moment AgentCore Memory stopped being a feature I was learning and started being infrastructure I was building on.
The new thing that changed what's possible

If I'd stopped writing here, this would be a perfectly fine post. But something landed last month that quietly made long-term memory a lot more useful, and most tutorials haven't caught up to it yet.
You can now attach structured metadata to long-term memory records. Up to ten indexed keys per memory resource — strings, numbers, string lists — and retrieval can filter on those attributes before semantic search runs.
Why this matters: before metadata, a query like "show me the high-priority commitments I made to this vendor in the last 30 days" was a prayer. You'd write a clever semantic query and hope the embeddings cooperated. Now it's a structured pre-filter, and the model only does similarity search inside a narrowed candidate pool.
You declare the metadata schema when you create or update the memory resource:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
control.update_memory(
memoryId=memory_id,
metadataConfiguration={
"indexedKeys": [
{"key": "priority", "type": "STRING"},
{"key": "po_value", "type": "NUMBER"},
{"key": "regions", "type": "STRING_LIST"},
],
"inferenceInstructions": (
"Tag each extracted memory with 'priority' (low/medium/high) "
"based on urgency expressed by the speaker, and 'po_value' "
"as the numeric purchase order amount in INR if mentioned."
),
},
)The
inferenceInstructions are optional but powerful — they let the extraction LLM populate metadata for you, based on the event content. You can also attach metadata explicitly at ingestion time if you'd rather not trust inference.Then at retrieval, you filter:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
results = client.retrieve_memories(
memory_id=memory_id,
namespace=f"/facts/{actor_id}/",
query="dispatch commitments at risk",
top_k=10,
metadata_filters=[
{
"left": {"metadataKey": "priority"},
"operator": "EQUALS_TO",
"right": {"metadataValue": {"stringValue": "high"}},
},
{
"left": {"metadataKey": "x-amz-agentcore-memory-createdAt"},
"operator": "AFTER",
"right": {"metadataValue": {"dateTimeValue": "2026-05-01T00:00:00Z"}},
},
],
)The
createdAt key is system-generated, always there, always filterable — so even if you never define a single custom key, you get free time-window filtering. Which, for anything that goes stale (inventory, prices, availability), is huge.What I'd tell past-me
A few patterns I'd hand my December self if I could.

Hit
/preferences/{actorId}/ and /facts/{actorId}/ in parallel on every turn, and merge the results into the prompt. Preferences shape how the agent talks. Facts shape what it can claim. Keep them separate so you can tune retrieval independently.For domains where last week's truth isn't this week's truth, always include a
createdAt filter at retrieval. Stale facts shouldn't compete with fresh ones for top-k slots.If you're multi-tenant, put the tenant in the namespace, not in metadata. Namespace boundaries are hard isolation. Metadata filters are soft, and one missed filter clause is a cross-tenant leak. I learned this the way you learn everything that matters — by getting it wrong first.
And one more thing nobody warned me about: strategy extraction is asynchronous. When you write an event, the long-term extraction happens in the background. Don't write-then-immediately-retrieve in the same turn and expect the new record. Give it a beat.
The quiet thing memory does
The thing I didn't see coming, after all the code and configuration, was the shift in how the agent felt to use.
The customer who got asked twice about her warehouse was the same customer who, two weeks later, told me, "It's nice — it remembers me now."
She didn't know what AgentCore Memory was. She didn't know about namespaces or strategies or metadata filters. She just knew the agent stopped wasting her time.
That's the bar. That's what this layer is for. And once you've felt the difference in production — once you've watched a customer relax into an agent that knows them — you stop building stateless ones.

Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article