Actors, Sessions, Events & Namespaces
The data model and namespace-scoped access
- Define every entity in the Memory data model — Memory resource, Actor, Session, Event, Branch, Memory record, Strategy — and the four message roles
- Explain why Events are immutable and how short-term scoping by actorId + sessionId differs from long-term namespace organization
- Design clean namespace templates using {actorId}/{sessionId}/{memoryStrategyId} and choose the right granularity
- Apply the trailing-slash rule to avoid prefix collisions between sibling namespaces
- Distinguish the SDK config key namespaceTemplates (plural) from the boto3/console namespaceTemplate (singular)
- Write namespace-scoped IAM using the bedrock-agentcore:namespace and bedrock-agentcore:namespacePath condition keys for least-privilege retrieval
AgentCore Memory has a precise data model — a Memory resource holds Events (immutable, role-tagged messages) keyed by Actor and Session, and the service derives long-term Memory records via Strategies. This lesson nails that vocabulary, then teaches how long-term records are organized into hierarchical, slash-delimited namespaces with template variables — including the trailing-slash rule that prevents prefix collisions and the IAM condition keys that let you scope access to a single namespace.
- 1The data model: from Memory resource down to a record
- 2Namespaces: how long-term records are organized
- 3The trailing-slash rule: avoiding prefix collisions
- 4The naming gotcha: namespaceTemplates vs namespaceTemplate
- 5Namespace-scoped IAM: least-privilege retrieval
The data model: from Memory resource down to a record
Before you can design a clean memory layout you need the exact vocabulary. AgentCore Memory is not a free-form key-value store — it has a fixed object model, and every concept below maps to a real field name you will pass to the API.
Memory (the resource). The top-level container you create with CreateMemory on the bedrock-agentcore-control plane. It carries an id, an encryption setting (a customer-managed KMS key, or service-managed by default), an event-expiry duration, and the list of strategies attached to it. Its lifecycle status moves through:
CREATING -> ACTIVE -> (FAILED)You must poll until the status is ACTIVE before writing events — control.get_memory(memoryId=...)['memory']['status']. A FAILED resource never becomes usable; delete and recreate it.
Actor (actorId). Who the memory is about — usually an end user, but it can be any principal you choose to model (a customer, a tenant, a device). You assign the value; AgentCore does not invent it.
Session (sessionId). A single bounded conversation for an actor. Short-term events are scoped by the pair actorId + sessionId — that pair is the addressing key for raw turn-by-turn history.
Event. The unit you write to the data plane via CreateEvent. An event contains one or more messages, and crucially events are immutable — you append, you never edit. That immutability is what makes the raw interaction log a trustworthy audit trail.
Message roles. Every message inside an event is tagged with exactly one role:
| Role | Meaning |
|---|---|
USER | Input from the end user/actor |
ASSISTANT | The agent's response |
TOOL | A tool/function result fed back into the conversation |
OTHER | Anything that doesn't fit the above (e.g. system notes) |
These roles matter downstream: several strategies only consume USER/ASSISTANT messages, while episodic extraction wants TOOL results included for best quality.
Branch. You can fork a conversation off an existing event by referencing its rootEventId, creating a parallel line of history (think "what-if" replays or A/B continuations) without mutating the original. List them with list_branches.
Memory record. A single long-term item — a derived insight (a fact, a preference, a summary, an episode) the service extracts asynchronously if you attached a strategy. Records are read with RetrieveMemoryRecords / retrieve_memories. No strategy means no records: you only ever get back the raw short-term events you wrote.
Strategy (memoryStrategyId). A configured extraction recipe attached to the Memory that tells AgentCore what to derive from raw events (semantic facts, summaries, user preferences, episodes). Each strategy owns one or more namespaces where its records land.
Key insight
Two scoping schemes, one resource
Short-term and long-term data are addressed differently. Short-term events are scoped by actorId + sessionId (write with CreateEvent, read with ListEvents/get_last_k_turns). Long-term records are organized by namespace (read with RetrieveMemoryRecords/retrieve_memories). Conflating the two is the most common modeling mistake — namespaces have nothing to do with raw events.
Watch out
Events are immutable
There is no UpdateEvent. You can DeleteEvent, but you cannot edit one. Design your write path to append correct events rather than expecting to patch them later. The same immutability is what lets consolidation keep an audit trail: outdated long-term records are marked INVALID rather than overwritten.
Namespaces: how long-term records are organized
Namespaces apply to long-term memory only — they are the addressing scheme for the records a strategy produces. A namespace is a hierarchical, slash-delimited path that can contain template variables the service fills in at extraction time. The supported variables are:
| Variable | Substituted with |
|---|---|
{actorId} | The actor the record is about |
{sessionId} | The session the record came from |
{memoryStrategyId} / {strategyId} | The strategy that produced the record |
You attach one or more namespace templates to each strategy when you create the Memory. At retrieval time you pass a concrete (substituted) namespace to RetrieveMemoryRecords to search within it.
control = boto3.client('bedrock-agentcore-control')
resp = control.create_memory(
name="ShoppingSupportAgentMemory",
memoryStrategies=[
{'summaryMemoryStrategy': {
'name': 'SessionSummarizer',
# Summary records are session-scoped, so the template MUST include {sessionId}
'namespaceTemplates': ['/summaries/{actorId}/{sessionId}/']}},
{'userPreferenceMemoryStrategy': {
'name': 'UserPreferenceExtractor',
# Preferences live per actor, across all sessions
'namespaceTemplates': ['/users/{actorId}/preferences/']}}])
# poll control.get_memory(memoryId=...)['memory']['status'] until 'ACTIVE'When you later read records, you supply the resolved path:
data = boto3.client('bedrock-agentcore')
data.retrieve_memory_records(
memoryId="mem-123",
namespace="/customer-support/user-1/preferences/",
searchCriteria={"searchQuery": "shipping address and contact preferences", "topK": 5})Granularity is a design choice. The hierarchy lets you tune how narrowly records are partitioned, trading isolation against cross-context recall:
- Per-session —
{actorId}/{sessionId}/— tightest scope; records never bleed between conversations. - Per-actor —
{actorId}/— durable knowledge about one user across all their sessions (the natural home for preferences). - Per-strategy —
/strategy/{memoryStrategyId}/...— partition by what kind of insight it is. - Global — a fixed prefix with no variables — shared across everyone (use sparingly; it is a cross-actor blast radius).
Different built-in strategies have natural defaults: semantic facts default to roughly /strategy/{memoryStrategyId}/actors/{actorId}/, while the Summary strategy is session-scoped and its template requires {sessionId}.
Example
Choosing granularity by intent
A travel-assistant might use /users/{actorId}/preferences/ for "prefers aisle seats" (durable, per-actor) but /trips/{actorId}/{sessionId}/ for facts about one booking conversation (ephemeral, per-session). Same actor, two namespaces, two different recall behaviors — that is the whole point of the hierarchy.
The trailing-slash rule: avoiding prefix collisions
This is the single most important syntactic detail in namespace design, and it bites people who skip it. Always end a namespace with a trailing slash.
Namespace matching for retrieval is prefix-based. Consider two sibling namespaces written without trailing slashes:
/users/{actorId}/preferences <-- BAD: no trailing slash
/users/{actorId}/preferences-archiveResolved for actorId = u-42, the first becomes /users/u-42/preferences. Because matching is by prefix, that string is also a prefix of /users/u-42/preferences-archive — so a search scoped to the first namespace can leak records from the second. The two namespaces collide.
Add the trailing slash and the collision disappears:
/users/{actorId}/preferences/ <-- GOOD
/users/{actorId}/preferences-archive/Now /users/u-42/preferences/ is no longer a prefix of /users/u-42/preferences-archive/ (the slash makes them disjoint at that level). Every namespace template in this lesson's examples ends in / for exactly this reason — /summaries/{actorId}/{sessionId}/, /users/{actorId}/preferences/, /customer-support/user-1/preferences/.
The rule generalizes: treat the trailing slash as a hard convention on every template you write, the same way you'd always close a bracket. It costs one character and removes a whole class of silent cross-namespace leaks.
Watch out
Prefix collisions fail open, not closed
A missing trailing slash doesn't throw an error — it silently widens what a retrieval can see. That makes it a privacy bug, not a crash: records you thought were isolated to one namespace become reachable from a sibling. There's no exception to catch, so the only defense is the convention itself.
The naming gotcha: namespaceTemplates vs namespaceTemplate
AgentCore exposes the same concept under two slightly different key names depending on which tool you use, and mixing them up produces validation errors that are easy to misread.
| Surface | Config key | Cardinality |
|---|---|---|
Python SDK (bedrock_agentcore.memory) | namespaceTemplates (plural) | List of templates |
boto3 (bedrock-agentcore-control) / Console | namespaceTemplate (singular) | The same field, named differently |
This mirrors a broader pattern in the Memory API where the SDK and the raw control plane disagree on field names — for example the SDK takes strategies= while boto3 takes memoryStrategies=. The fix is mechanical but you have to know it exists:
# Python SDK shape (note the PLURAL key, inside a strategy dict):
{'summaryMemoryStrategy': {
'name': 'SessionSummarizer',
'namespaceTemplates': ['/summaries/{actorId}/{sessionId}/']}}
# boto3 / console shape uses the SINGULAR key name for the same data.If a create_memory call rejects your strategy with a ValidationException complaining about an unknown member, check the plural-vs-singular spelling of the namespace key before anything else — it is almost always this, not a problem with your path.
Tip
Read the error, then check the spelling
A ValidationException mentioning an unexpected key on a strategy is the tell. Match the key to the surface you're calling: namespaceTemplates when you're in the Python SDK, namespaceTemplate when you're hand-rolling boto3 or working in the Console.
Namespace-scoped IAM: least-privilege retrieval
Because namespaces are paths, AgentCore lets you write IAM policies that restrict an agent's role to specific namespaces — the same way you'd scope an S3 policy to a key prefix. This is how you enforce, for example, that a per-tenant agent role can only retrieve records under that tenant's path.
Two condition keys make this work, used on actions such as bedrock-agentcore:RetrieveMemoryRecords:
| Condition key | Operator | Use |
|---|---|---|
bedrock-agentcore:namespace | StringEquals | Match one exact namespace |
bedrock-agentcore:namespacePath | StringLike | Match a path pattern (wildcards) |
An exact-match policy that allows retrieval from a single namespace and nothing else:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "bedrock-agentcore:RetrieveMemoryRecords",
"Resource": "*",
"Condition": {
"StringEquals": {
"bedrock-agentcore:namespace": "/users/u-42/preferences/"
}
}
}]
}A pattern-match policy that allows retrieval anywhere under a tenant prefix using the StringLike key:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "bedrock-agentcore:RetrieveMemoryRecords",
"Resource": "*",
"Condition": {
"StringLike": {
"bedrock-agentcore:namespacePath": "/tenants/acme/*"
}
}
}]
}The trailing-slash discipline from the previous section pays off directly here: because well-formed namespaces don't collide by prefix, a StringLike pattern like /tenants/acme/* won't accidentally grant access to /tenants/acme-corp/ records. Sloppy namespaces undermine your IAM as much as your retrieval.
Key insight
Namespaces double as a security boundary
Designing namespaces isn't only about retrieval quality — the path you choose becomes the unit you can lock down in IAM. A clean per-actor or per-tenant layout (/tenants/{tenantId}/...) lets you grant each agent role exactly the slice it needs via namespacePath StringLike, turning a recall decision into a least-privilege boundary.
Note
Verify exact action and key names against the live docs
Condition-key and action names are stable concepts, but always confirm the exact strings (bedrock-agentcore:namespace, bedrock-agentcore:namespacePath, bedrock-agentcore:RetrieveMemoryRecords) against the current AgentCore permissions and Memory organization pages before shipping a policy — IAM rejects typos silently by simply not matching.
Try it: Design a namespace layout and lock it down
Goal: create a Memory with two strategies on deliberately scoped namespaces, prove the trailing-slash rule matters, and write a least-privilege IAM policy for retrieval.
- Create the Memory. Using
boto3.client('bedrock-agentcore-control').create_memory(...), attach auserPreferenceMemoryStrategyon/users/{actorId}/preferences/and asummaryMemoryStrategyon/summaries/{actorId}/{sessionId}/. Pollget_memory(...)['memory']['status']until it isACTIVE. Note which strategy requires{sessionId}in its template and why. - Write events and read back. On the data plane (
bedrock-agentcore), callcreate_eventfor an actor/session with a couple ofUSER/ASSISTANTmessages. Wait briefly (extraction is asynchronous), thenretrieve_memory_recordsagainst the resolved preferences namespace/users/<actorId>/preferences/. Confirm you get the per-actor preference, not the per-session summary. - Reproduce a prefix collision. On paper (or in a scratch resource), define two sibling namespaces without trailing slashes — e.g.
/users/{actorId}/notesand/users/{actorId}/notes-private. Resolve them for one actorId and write down why a prefix match on the first reaches the second. Then add trailing slashes to both and explain why the collision is gone. - Trip the config-key gotcha (optional). Copy an SDK-style strategy dict that uses
namespaceTemplates(plural) directly into your boto3create_memorycall. Observe theValidationException, then rename the key to the singularnamespaceTemplateand watch it succeed. - Write the IAM policy. Author an IAM policy that allows
bedrock-agentcore:RetrieveMemoryRecordsonly under/tenants/acme/*using thebedrock-agentcore:namespacePathcondition key withStringLike. Then write a second statement that allows exact retrieval from/users/u-42/preferences/usingbedrock-agentcore:namespacewithStringEquals. - Reflect. In three sentences: what granularity did you choose for each strategy and why, and how did your namespace design make the IAM policy easier (or harder) to write? Cross-check every action and condition-key string against the live permissions page before you'd ship this.
Key takeaways
- 1The Memory data model is fixed: a Memory resource (status CREATING -> ACTIVE -> FAILED; poll to ACTIVE) contains Events, which hold one or more immutable, role-tagged messages (USER/ASSISTANT/TOOL/OTHER).
- 2Short-term events are scoped by actorId + sessionId; long-term Memory records are derived by Strategies and organized by namespace. No strategy = no records.
- 3A Branch forks events off a rootEventId for parallel history without mutating originals; a Strategy (memoryStrategyId) is the extraction recipe that owns the namespaces its records land in.
- 4Namespaces (long-term only) are hierarchical slash paths with template variables {actorId}/{sessionId}/{memoryStrategyId}; granularity runs per-session -> per-actor -> per-strategy -> global.
- 5Always end a namespace with a trailing slash: matching is prefix-based, so `/x/preferences` collides with `/x/preferences-archive` while `/x/preferences/` does not. Collisions fail open (silent leaks), not closed.
- 6Config key gotcha: the Python SDK uses namespaceTemplates (plural); boto3/console use namespaceTemplate (singular) — a ValidationException on an unknown key is usually this.
- 7Scope retrieval with IAM condition keys bedrock-agentcore:namespace (StringEquals) and bedrock-agentcore:namespacePath (StringLike) on actions like bedrock-agentcore:RetrieveMemoryRecords; clean namespaces make these least-privilege boundaries reliable.
Quiz
Lock in what you learned
Check your understanding
0 / 4 answered
1.Two strategies write to /users/{actorId}/notes and /users/{actorId}/notes-private (no trailing slashes). A retrieval scoped to the first namespace also returns records from the second. Why?
2.Your boto3 create_memory call returns a ValidationException about an unexpected member on the summaryMemoryStrategy. You copied the strategy dict from a Python SDK example. What's the most likely cause?
3.You need a per-tenant agent role that can only retrieve long-term records under that tenant's path. Which IAM construct fits?
4.Which statement about the Memory data model is correct?
Go deeper
Hand-picked sources to keep learning
The canonical reference for the Memory data model, short-term vs long-term, and the two API planes.
Namespace templates, the template variables, and granularity — the trailing-slash convention lives here. Verify exact key names against this page.
Confirm the exact action and condition-key strings (namespace, namespacePath, RetrieveMemoryRecords) before shipping a scoped policy.
Worked examples of namespace design and how records are extracted and retrieved.
Per-resource caps (e.g. up to 6 strategies per memory, retention 7-365 days) are VOLATILE — confirm current values here.
MemoryClient / MemorySessionManager source; check whether your call expects namespaceTemplates (SDK) or maps to namespaceTemplate (boto3).