Agentic AI AcademyAgentic AI Academy
AgentCore Memory/Lesson 3 of 5

Actors, Sessions, Events & Namespaces

The data model and namespace-scoped access

Intermediate 12 minBuilder
What you'll be able to do
  • 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
At a glance

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.

  1. 1The data model: from Memory resource down to a record
  2. 2Namespaces: how long-term records are organized
  3. 3The trailing-slash rule: avoiding prefix collisions
  4. 4The naming gotcha: namespaceTemplates vs namespaceTemplate
  5. 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:

text
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:

RoleMeaning
USERInput from the end user/actor
ASSISTANTThe agent's response
TOOLA tool/function result fed back into the conversation
OTHERAnything 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:

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

python
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:

python
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:

text
/users/{actorId}/preferences     <-- BAD: no trailing slash
/users/{actorId}/preferences-archive

Resolved 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:

text
/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.

SurfaceConfig keyCardinality
Python SDK (bedrock_agentcore.memory)namespaceTemplates (plural)List of templates
boto3 (bedrock-agentcore-control) / ConsolenamespaceTemplate (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
# 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 keyOperatorUse
bedrock-agentcore:namespaceStringEqualsMatch one exact namespace
bedrock-agentcore:namespacePathStringLikeMatch a path pattern (wildcards)

An exact-match policy that allows retrieval from a single namespace and nothing else:

json
{
  "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:

json
{
  "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.

  1. Create the Memory. Using boto3.client('bedrock-agentcore-control').create_memory(...), attach a userPreferenceMemoryStrategy on /users/{actorId}/preferences/ and a summaryMemoryStrategy on /summaries/{actorId}/{sessionId}/. Poll get_memory(...)['memory']['status'] until it is ACTIVE. Note which strategy requires {sessionId} in its template and why.
  2. Write events and read back. On the data plane (bedrock-agentcore), call create_event for an actor/session with a couple of USER/ASSISTANT messages. Wait briefly (extraction is asynchronous), then retrieve_memory_records against the resolved preferences namespace /users/<actorId>/preferences/. Confirm you get the per-actor preference, not the per-session summary.
  3. Reproduce a prefix collision. On paper (or in a scratch resource), define two sibling namespaces without trailing slashes — e.g. /users/{actorId}/notes and /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.
  4. Trip the config-key gotcha (optional). Copy an SDK-style strategy dict that uses namespaceTemplates (plural) directly into your boto3 create_memory call. Observe the ValidationException, then rename the key to the singular namespaceTemplate and watch it succeed.
  5. Write the IAM policy. Author an IAM policy that allows bedrock-agentcore:RetrieveMemoryRecords only under /tenants/acme/* using the bedrock-agentcore:namespacePath condition key with StringLike. Then write a second statement that allows exact retrieval from /users/u-42/preferences/ using bedrock-agentcore:namespace with StringEquals.
  6. 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

  1. 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).
  2. 2Short-term events are scoped by actorId + sessionId; long-term Memory records are derived by Strategies and organized by namespace. No strategy = no records.
  3. 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.
  4. 4Namespaces (long-term only) are hierarchical slash paths with template variables {actorId}/{sessionId}/{memoryStrategyId}; granularity runs per-session -> per-actor -> per-strategy -> global.
  5. 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.
  6. 6Config key gotcha: the Python SDK uses namespaceTemplates (plural); boto3/console use namespaceTemplate (singular) — a ValidationException on an unknown key is usually this.
  7. 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