An agent that forgets the moment the reply finishes is the correct default, and the wrong behaviour for a reorder assistant. This article adds memory to that agent and proves it three ways: a preference survives into a new session, a second manager cannot see the first manager’s memory, and a failed restock is recalled by what it meant.
Every number below was measured while writing this, in one AWS account, in us-east-1.
The use case#
Northwind Gear runs forty stores and restocks them every Monday. A store manager told the reorder assistant, once, that only the North China region matters to them. The next Monday, in a new session, the assistant asked which region again. Nothing was wrong with the model. The sentence never left the session it was said in, so there was nothing for a later session to find.
What AgentCore does here#
Three of the nine capabilities carry this use case.
- Memory: one memory resource holds both halves. Short term is an append-only event log keyed by actor and session; long term is a set of records the service extracts from those events into namespaces. This article goes deep on that split.
- Runtime: hosts
app/aia_uc02/as a managed endpoint. Each session gets its own microVM, which is why the session id matters. - Identity, lightly: the actor id arrives in the invoke payload or in a runtime header, and becomes the partition key of the memory namespace. There is no Cognito pool here.
The other six stay off: no Gateway, no Policy, no Evaluations, no Code Interpreter, no Browser, and no Observability work beyond what the runtime emits.
Requirements#
- Node.js 20 or newer (22.22.2 here), AgentCore CLI 0.31.1, AWS CLI 2.x, uv 0.11.9
- Python 3.10 to 3.14 on the host; the runtime target is PYTHON_3_13, set in the contract
- An AWS account with Bedrock model access, region
us-east-1 - Model
deepseek.v3.2, passed through theMODEL_IDenvironment variable - IAM permissions to create a runtime, its role, and a memory resource with an execution role; the CLI writes both roles for you
agentcore --version # 0.31.1
aws configure get region # us-east-1
aws sts get-caller-identity --query Account --output text
# 123456789012The code#
https://github.com/pinxiong/bedrock-agentcore-in-action/tree/main/usecases/02-memory
02-memory/
├── run.py # every operation; forwards to tests/ and agentcore
├── Makefile # forwards to run.py, holds no logic
├── agentcore/
│ ├── agentcore.json # the runtime plus one memory with three strategies
│ └── aws-targets.json # deployment target: region us-east-1
├── app/aia_uc02/
│ ├── main.py # the agent: prompt, tool, actor resolution, entrypoint
│ ├── memory.py # the only file that knows short term from long term
│ ├── model/load.py # MODEL_ID -> BedrockModel
│ └── pyproject.toml # dependencies packaged into the zip
└── tests/
├── smoke.py # the four assertions, run against the deployment
└── doctor.py # toolchain, credentials, memory contractmemory.py is the whole subject of the article, and agentcore.json is where the namespace layout is decided.
How it works#
The memory resource is declared once, and the strategies decide what long-term records exist at all:
"memories": [{
"name": "aia_uc02_mem",
"eventExpiryDuration": 30,
"strategies": [
{"type": "USER_PREFERENCE", "namespaceTemplates": ["/users/{actorId}/preferences"]},
{"type": "EPISODIC", "namespaceTemplates": ["/episodes/{actorId}/{sessionId}"],
"reflectionNamespaceTemplates": ["/episodes/{actorId}"]},
{"type": "SEMANTIC", "namespaceTemplates": ["/users/{actorId}/facts"]}
]
}]The actor id is inside the namespace, so isolation is a property of the key space rather than a filter some code path has to remember. The reflection namespace sits one level above the per-session one, so a prefix read of /episodes/{actorId}/ reaches both a single episode and the lessons written across all of them.
The agent itself never stores a preference. It appends the turn and asks a question:
def remember(self, actor, session_id, turns):
# ... the real one wraps this in try/except and returns None on failure
event = self.client.create_event(
memory_id=self.memory_id, actor_id=actor,
session_id=session_id, messages=turns)
return event.get("eventId")
def recall(self, actor, query):
# ... condensed: the real one also drops records whose text is empty,
# and continues past a namespace that failed instead of falling through
found = []
for label, ns, by_path in (("preference", f"/users/{actor}/preferences", False),
("fact", f"/users/{actor}/facts", False),
("episode", f"/episodes/{actor}/", True)):
try:
kwargs = {"memory_id": self.memory_id, "query": query, "top_k": TOP_K}
kwargs["namespace_path" if by_path else "namespace"] = ns
found += [{"kind": label, "text": _text(r)}
for r in self.client.retrieve_memories(**kwargs)]
except Exception as exc: # one bad namespace is not fatal
log.warning("uc02 recall failed for %s: %s", label, exc)
return foundWriting the event is also what starts extraction, which is why the second session can find a sentence the first session said. Extraction is not part of the invoke: the CLI returned in 19 seconds (12,117 ms of it the model call), and the records arrived later.
Retrieval happens before the model is called, because the records become part of the system prompt:
records = store.recall(actor, prompt) if store else []
agent = Agent(
model=load_model(),
system_prompt=f"{SYSTEM_PROMPT}\n\nWhat you remember about this manager:\n{render(records)}",
# ... tools=list(TOOLS) and conversation_manager=NullConversationManager() too
)A fresh Agent per invoke, not one per session. A cached agent would keep serving the first turn’s memories, and a retrieval failure is logged and swallowed: an agent that cannot remember is still useful.
Deploy it#
From the use case folder:
make doctortoolchain
... four checks: python 3.14.0, node v22.22.2, aws cli 2.15.6, uv 0.11.9 ...
agentcore cli
PASS agentcore 0.31.1
this folder
PASS codeLocation 'app/aia_uc02' exists
PASS app environment present at app/aia_uc02/.venv
PASS run.py present
... four more folder checks ...
memory contract
PASS memory 'aia_uc02_mem' declared, events expire in 30d
PASS strategy USER_PREFERENCE present (the regional preference the second session has to recall)
PASS strategy EPISODIC present (the failed restock a later session has to recall)
PASS every strategy namespaces on {actorId}, so actors are isolated by key
aws environment
... three checks: credentials work, region is us-east-1, model available ...
19 passed, 0 warned, 0 failed
next: make deployThe memory contract check exists because a deployment missing the EPISODIC strategy would succeed and then fail an assertion much later.
make deploy Load deployment target ............ 1 ms
Validate project .................. 682 ms
Sync CDK dependencies ............. 2 ms
Build CDK project ................. 2.4 s
Synthesize CloudFormation ......... 7.1 s
Check bootstrap status ............ 665 ms
Check stack status ................ 1.4 s
Deploy to AWS ..................... 4 m 7 s
Persist deployment state .......... 715 ms
Total Duration: 4m 23s
Deployed to 'default' (stack: AgentCore-aiauc02-default)
Outputs:
ApplicationMemoryAiaUc02MemIdOutput7F0802F1: aiauc02_aia_uc02_mem-y7Ql9N3D59
ApplicationAgentAiaUc02RuntimeArnOutput9F81CB35:
arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/aiauc02_aia_uc02-ACujAB8uYJ
Step timings are the first deploy’s, the Outputs a minutes-later re-deploy of the same stack.
Two sessions, two different session ids. The first only states a preference:
python3 run.py invoke --session b0b998ab-89ca-4387-99cb-fc01130e9108-ff3568e8 \
--prompt "From now on, only reorder for the North China region. Ignore every other region."Based on your explicit instruction, I'll only reorder for the North China region.
The North China region needs reorders at three stores: Beijing Haidian (42 thermal
flasks), Tianjin Heping (17 insulated mugs), and Shijiazhuang Qiaoxi (8 lunch boxes). ...The second one is a new session and says nothing about regions:
python3 run.py invoke --session 03fd411e-293b-4aa2-a219-bd996c7e3481-6b0df16e \
--prompt "Draft this week's reorder list."I'll create the reorder list for the North China region as specified in your preferences. ...
Here's this week's reorder list for the North China region:
Store: Beijing Haidian, SKU: SKU-1001, ... Reorder Quantity: 42 units
Store: Tianjin Heping, SKU: SKU-1002, ... Reorder Quantity: 17 units
Store: Shijiazhuang Qiaoxi, SKU: SKU-1003, ... Reorder Quantity: 8 units
Note: I've applied your documented preference to only reorder for North China region, ...Both captures are trimmed, and ... marks every cut; the store lines also lose the bold markers the model emitted, which this page would render as bold. The second session had never seen the sentence about North China. The only place it could have come from is the memory resource.

The assertions run against the deployment:
make testmemory
session 1 e9f9d728131040b1ad532866e69b4981-cd9243d4
extraction visible after 1.5s · 1 record(s)
session 2 34b26d624d2140d1b767098a09abdfaa-8d1f1d6b · tools ['reorder_candidates']
reply "Based on your preference for North China region, here is this
week's reorder list: ..."
PASS assert_memory_1: a second session retrieves a preference written in the first
actor store_manager_north sees '{"context":"Inventory reordering and restocking operations for North China region, including historical restoc ...
actor store_manager_south sees '{"context":"Managing inventory reorder operations","preference":"Only reorder for the South China region; igno ...
actor store_manager_south reply "Based on your preference for South China
region only, here's this week's reorder list: ..."
PASS assert_memory_2: actor A cannot retrieve actor B memories
episodic extraction visible after 959s
reply "... Remember: the Guangzhou warehouse must not be used for North
China restocks."
PASS assert_memory_3: episodic strategy recalls a previously failed approach
PASS independence: no other use case's resources present
4 passed, 0 failedThe 1.5 seconds needs explaining: that preference had already been extracted by an earlier run against the same resource. The episode was new, and took 959.

Teardown is two steps, and the second one deletes:
make down
aia.py status 02>> agentcore remove all -y
All schemas reset to empty state
>> agentcore deploy -y --target default
Deploy to AWS...
Tear down stack...
Total Duration: 4m 11s
$ aia.py status 02
AgentCore Status (target: default, us-east-1)
Agents
aia_uc02: Local only
Memories
aia_uc02_mem: Local only (USER_PREFERENCE, EPISODIC, SEMANTIC)The Total Duration is from the deploy log this step writes (evidence/18-down-deploy.txt); the rest is the console output (evidence/12-down-raw.txt).

Removal only empties the local contract; the reconciling deploy destroys the stack. One thing survives on purpose: the CDK bootstrap stack and its asset bucket. The memory resource does not survive, so a re-deploy starts empty and re-earns every assertion.
What it produced#

The measured loop, one actor, two sessions:

The two actors wrote to /users/store_manager_north/preferences and /users/store_manager_south/preferences; a semantic query for “which region does this store manager reorder for” returned North China to one and South China to the other, and asking the model “Draft this week’s reorder list.” produced South China for the southern manager. There is no filter to get wrong, because the actor id is part of the key.
The third assertion taught me something. Episodic extraction is slow: episodes were written about fifteen minutes after the events they came from. It is not a broken strategy, it is a slower queue, and a 300-second deadline can only fail against it.
A prefix read of /episodes/store_manager_north/ returns two kinds of record: a BASE episode filed under the session that produced it, and a REFLECTION written one level up, over all of them. Hence the {sessionId} in one template and not the other:
BASE /episodes/store_manager_north/195f957a...-78cd451f/
{"situation":"The user reported a failed restocking attempt ... 9-day lead time"}
REFLECTION /episodes/store_manager_north/
{"title":"Structured Output Format for Reorder and Inventory Lists"}Cost, measured on one traced invoke: 3,433 input and 210 output tokens across two model calls, which at 0.62 and 1.85 USD per million tokens is 0.0025 USD. Memory storage and the extraction calls bill separately; I did not measure that line.
Troubleshooting#
Invalid length for parameter runtimeSessionId, value: 22, valid min length: 33. Short readable session ids look fine and fail before they are sent. The smoke test now builds ids from two uuid4 halves, because one hex uuid is 32 characters, one short of the minimum.no episodic record ... within 1500s. Not a broken strategy, just a slow queue: raise the deadline past the observed fifteen minutes, and print how long the wait actually took so the number in the article is a measurement.
Takeaways#
- Memory is two mechanisms, not one: an event log you write, and records the service extracts. Only the second one survives a new session.
- Put the actor id inside the namespace template. Isolation then costs nothing and cannot be forgotten in a code path.
- Extraction is asynchronous and slow: fifteen minutes for an episode. Every assertion needs a deadline, and every deadline needs to be longer than you think.
- Retrieval belongs in the system prompt, before the model call, not in a tool the model may or may not choose.
- Teardown is real:
remove allempties the contract, the deploy that follows does the deleting.