Amazon Bedrock AgentCore Memory provides several ways to turn conversations into useful memory.

The four managed strategies cover common memory types:

  • session summaries;
  • semantic facts;
  • user preferences;
  • episodes and reflections.

AgentCore also provides a self-managed strategy for cases where you want to process raw conversation batches using your own application logic.

This article shows how both approaches can be configured with AWS CDK. The code can be found in this github repo https://github.com/BradWebb101/agentcore-memory-stratergies

Please note, self managed strategies add a lot of complexity to your memory strategy. I did this to explore the idea not advising that you do it for all agentic applications

Creating the managed memory strategies

AgentCore exposes the managed strategies through MemoryStrategy factory methods in aws-cdk-lib/aws-bedrockagentcore.

Each strategy has its own namespace and purpose.

StrategyPurposeNamespaceSummaryRecent conversational context/summaries/{actorId}/{sessionId}/SemanticStable facts and decisions/facts/{actorId}/User preferenceUser choices and working preferences/preferences/{actorId}/EpisodicPrevious tasks, outcomes, and lessons/episodes/{actorId}/{sessionId}/

Summary memory

The summarisation strategy creates a compressed representation of a conversation.

It is useful when an agent needs recent context without retrieving every original message.

const summary = agentcore.MemoryStrategy.usingSummarization({
  strategyName: 'SessionSummary',
  description: 'Compress recent conversations into session-level summaries.',
  namespaces: [
    '/summaries/{actorId}/{sessionId}/',
  ],
});

A summary strategy answers:

What happened recently?

The namespace includes both the actor and session because summaries normally belong to a specific conversation.

Semantic memory

The semantic strategy extracts stable facts, decisions, and reusable technical information.

const semantic = agentcore.MemoryStrategy.usingSemantic({
  strategyName: 'SemanticFacts',
  description: 'Extract stable project, domain, and user facts.',
  namespaces: [
    '/facts/{actorId}/',
  ],
  customExtraction: {
    model,
    appendToPrompt: SEMANTIC_EXTRACTION_PROMPT,
  },
  customConsolidation: {
    model,
    appendToPrompt: SEMANTIC_CONSOLIDATION_PROMPT,
  },
});

Semantic memory answers:

What became true?

The custom extraction prompt can be used to define which information should be treated as reusable.

export const SEMANTIC_EXTRACTION_PROMPT = `
Extract only stable and reusable technical facts.
Include:
- programming languages;
- framework versions;
- repository conventions;
- architecture decisions;
- named systems;
- confirmed constraints;
- decisions that affect future implementation.
Exclude:
- temporary debugging context;
- one-response instructions;
- speculative ideas;
- secrets and credentials;
- unsupported assumptions.
Do not store transient incident details as durable actor facts.
`;

This helps keep one-off information, such as a temporary error message or sandbox endpoint, out of the durable fact namespace.

User preference memory

The user-preference strategy captures recurring choices about communication, code style, tools, and ways of working.

const preferences = agentcore.MemoryStrategy.usingUserPreference({
  strategyName: 'ValidatedPreferences',
  description: 'Capture durable preferences while preserving scope.',
  namespaces: [
    '/preferences/{actorId}/',
  ],
  customExtraction: {
    model,
    appendToPrompt: PREFERENCE_EXTRACTION_PROMPT,
  },
  customConsolidation: {
    model,
    appendToPrompt: PREFERENCE_CONSOLIDATION_PROMPT,
  },
});

Preference memory answers:

What does the user consistently want?

Examples might include:

Use concise responses during technical debugging.
Use tabs for JavaScript and TypeScript.
Use four spaces for Python.
Present architecture comparisons as a small table followed by one recommendation.

Where possible, preferences should preserve their applicability.

For example, an indentation preference should normally be associated with a language rather than stored as a global rule.

{
  "key": "coding.indentation",
  "value": "tabs",
  "applies_when": {
    "languages": [
      "JavaScript",
      "TypeScript"
    ]
  }
}

Episodic memory

The episodic strategy captures completed tasks and their outcomes.

const episodes = agentcore.MemoryStrategy.usingEpisodic({
  strategyName: 'EngineeringEpisodes',
  description: 'Capture reusable tasks, outcomes, and lessons.',
  namespaces: [
    '/episodes/{actorId}/{sessionId}/',
  ],
  customExtraction: {
    model,
    appendToPrompt: EPISODIC_EXTRACTION_PROMPT,
  },
  customConsolidation: {
    model,
    appendToPrompt: EPISODIC_CONSOLIDATION_PROMPT,
  },
});

Episodic memory answers:

What happened before, and what did we learn?

A useful episode should normally include:

  • the problem;
  • the root cause;
  • the approach taken;
  • the outcome;
  • a reusable lesson.
export const EPISODIC_EXTRACTION_PROMPT = `
Create an episode only when there is:
- a meaningful goal;
- an approach or sequence of actions;
- an observable outcome;
- a lesson that may help a future task.

Keep the episode concise.
Capture:
- problem class;
- root cause;
- resolution steps;
- one reusable lesson.
Ignore greetings, unfinished tasks, simple facts, and repetitions.
`;

A debugging conversation might therefore become:

{
  "title": "ECS health check failure",
  "problem": "Tasks failed load balancer health checks.",
  "root_cause": "The target group used port 8080 while Uvicorn listened on port 8000.",
  "resolution": "Changed Uvicorn to listen on port 8080.",
  "lesson": "Align the application, container, and target-group ports."
}

Adding reflection to episodic memory

AgentCore episodic memory can also produce reflections across episodes.

A reflection describes a broader pattern rather than a single task.

For example:

The user prefers short diagnostic guidance during active incidents,
but structured explanations with trade-offs during architecture reviews.

In this implementation, the reflection configuration is applied through the underlying L1 construct:

const cfnMemory = memory.node.findChild(
  'Memory',
) as agentcore.CfnMemory;

cfnMemory.addPropertyOverride(
  'MemoryStrategies.3.CustomMemoryStrategy.Configuration.EpisodicOverride.Reflection',
  {
    Namespaces: [
      '/episodes/{actorId}/{sessionId}/',
    ],
    ModelId: model.modelId,
    AppendToPrompt: EPISODIC_REFLECTION_PROMPT,
  },
);

This property override can be used when the higher-level CDK construct does not pass the reflection configuration through to CloudFormation.

Attaching the managed strategies to a Memory resource

The four strategies can be attached to one AgentCore Memory resource.

const memory = new agentcore.Memory(this, 'Memory', {
  memoryName: 'AgentMemory',
  description: 'Managed and self-managed agent memory.',
  expirationDuration: cdk.Duration.days(30),
  memoryStrategies: [
    summary,
    semantic,
    preferences,
    episodes,
  ],
});

The Memory service role also needs permission to invoke the selected model.

const memoryRole = memory.node
  .findChild('ServiceRole')
  .node.findChild('Resource') as iam.CfnRole;

const memoryModelPolicy = new iam.Policy(
  this,
  'MemoryModelInvocationPolicy',
  {
    statements: [
      new iam.PolicyStatement({
        actions: [
          'bedrock:InvokeModel',
          'bedrock:InvokeModelWithResponseStream',
        ],
        resources: ['*'],
      }),
    ],
  },
);
memoryModelPolicy.attachToRole(
  iam.Role.fromRoleArn(
    this,
    'MemoryServiceRole',
    memoryRole.attrArn,
  ),
);

At this point, AgentCore can process conversation events using the four managed strategies.

Adding a self-managed strategy

A self-managed strategy is different from the managed strategies.

Instead of supplying a model and extraction prompt, you supply:

  • an S3 location;
  • an SNS topic;
  • one or more trigger conditions.

When a trigger fires, AgentCore writes the conversation payload to S3 and sends a notification to SNS.

Your own application then processes the payload.

Creating the payload bucket

The S3 bucket stores the raw conversation batches sent by AgentCore.

const payloadBucket = new s3.Bucket(
  this,
  'MemoryPayloadBucket',
  {
    encryption: s3.BucketEncryption.S3_MANAGED,
    blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
    enforceSSL: true,
    lifecycleRules: [
      {
        expiration: cdk.Duration.days(7),
      },
    ],
    removalPolicy: cdk.RemovalPolicy.DESTROY,
    autoDeleteObjects: true,
  },
);

The payloads in this implementation expire after seven days.

They are temporary processing inputs rather than the final memory records.

Creating the SNS topic

The SNS topic notifies the processor when a payload is ready.

const processingTopic = new sns.Topic(
  this,
  'MemoryProcessingTopic',
);

Configuring the self-managed strategy

The self-managed strategy connects the Memory resource to the S3 bucket and SNS topic.

const selfManaged = agentcore.MemoryStrategy.usingSelfManaged({
  strategyName: 'CustomMemoryProcessor',
  description: 'Process conversation batches using custom memory logic.',
  historicalContextWindowSize: 10,
  invocationConfiguration: {
    topic: processingTopic,
    s3Location: {
      bucketName: payloadBucket.bucketName,
      objectKey: 'incoming/',
    },
  },
  triggerConditions: {
    messageBasedTrigger: 10,
    tokenBasedTrigger: 8_000,
    timeBasedTrigger: cdk.Duration.minutes(50),
  },
});

The trigger conditions use OR behaviour.

A batch is sent when the first threshold is reached:

10 messages
OR
8,000 tokens
OR
50 minutes of inactivity

The values can be adjusted depending on how often the custom processor should run.

Adding all five strategies

The self-managed strategy can sit alongside the four managed strategies.

const memory = new agentcore.Memory(this, 'Memory', {
  memoryName: 'AgentMemory',
  description: 'Managed and self-managed agent memory.',
  expirationDuration: cdk.Duration.days(30),
  memoryStrategies: [
    summary,
    semantic,
    preferences,
    episodes,
    selfManaged,
  ],
});

The managed strategies continue to produce their own memory records.

The self-managed strategy independently sends conversation batches to your custom processor.

Creating the self-managed processor

The processor can be implemented as a Lambda function subscribed to the SNS topic.

const processor = new lambda.Function(
  this,
  'MemoryProcessor',
  {
    runtime: lambda.Runtime.PYTHON_3_12,
    handler: 'memory_pipeline.handler.handler',
    code: lambda.Code.fromAsset(
      path.join(__dirname, '../lambda'),
    ),
    timeout: cdk.Duration.minutes(5),
    memorySize: 512,
    environment: {
      MEMORY_ID: memory.memoryId,
    },
  },
);

The Lambda needs permission to read the payload bucket.

payloadBucket.grantRead(processor);

It can then be subscribed to the SNS topic.

processingTopic.addSubscription(
  new subscriptions.LambdaSubscription(
    processor,
  ),
);

Allowing the processor to manage records

The Lambda can create, update, delete, and retrieve AgentCore memory records.

processor.addToRolePolicy(
  new iam.PolicyStatement({
    actions: [
      'bedrock-agentcore:BatchCreateMemoryRecords',
      'bedrock-agentcore:BatchUpdateMemoryRecords',
      'bedrock-agentcore:BatchDeleteMemoryRecords',
      'bedrock-agentcore:RetrieveMemoryRecords',
    ],
    resources: ['*'],
  }),
);

The exact permissions required depend on what the processor does.

A processor that only creates records may not need update or deletion access. A processor that consolidates existing memory will normally need retrieval and update permissions as well.

Processing the conversation batch

The Lambda receives the SNS notification, loads the payload from S3, and extracts the conversation data.

A simplified handler might look like this:

def handler(
    event: dict[str, Any],
    _context: Any,
) -> dict[str, Any]:
    memory_id = os.environ["MEMORY_ID"]
    writer = AgentCoreMemoryWriter(memory_id)
    processed = 0
    for message in _sns_messages(event):
        payload = _load_payload(message)
        actor_id = str(
            payload.get("actorId")
            or payload.get("actor_id")
            or "unknown"
        )
        session_id = str(
            payload.get("sessionId")
            or payload.get("session_id")
            or "unknown"
        )
        conversation_text = _conversation_text(
          payload,
        )
        candidates = extract_memories(
            conversation_text,
            session_id,
        )
        writer.write_candidates(
            actor_id,
            candidates,
        )
        processed += len(candidates)
    return {
        "statusCode": 200,
        "recordsProcessed": processed,
    }

The implementation of extract_memories depends on the use case.

It could use:

  • deterministic rules;
  • regular expressions;
  • a Bedrock structured-output call;
  • a domain-specific classifier;
  • an existing internal extraction service.

The self-managed strategy does not prescribe the extraction method.

It only provides the conversation batch and processing trigger.

Writing custom memory records

A custom candidate might contain fields such as:

@dataclass
class CandidateMemory:
    key: str
    value: str
    category: str
    confidence: float
    source_sessions: tuple[str, ...]
    applies_when: dict[str, Any] | None = None

For example:

CandidateMemory(
    key="coding.indentation",
    value="tabs",
    category="preference",
    confidence=0.90,
    source_sessions=("session-001",),
    applies_when={
        "languages": [
            "JavaScript",
            "TypeScript",
        ],
    },
)

The processor can then convert that candidate into the AgentCore record format.

record = {
    "namespace": f"/custom/{actor_id}/",
    "content": {
        "text": json.dumps({
            "key": memory.key,
            "value": memory.value,
            "category": memory.category,
            "confidence": memory.confidence,
            "source_sessions": memory.source_sessions,
            "applies_when": memory.applies_when,
        }),
    },
    "metadata": {
        "category": {
            "stringValue": memory.category,
        },
        "confidence": {
            "numberValue": memory.confidence,
        },
    },
}

The final record schema can be adjusted to fit the application.

Encoding structured metadata

AgentCore applies restrictions to metadata.stringValue.

Structured JSON should normally remain in content.text.

If a structured field also needs to be searchable in metadata, it can be flattened.

@staticmethod
def encode_applies_when(
    applies_when: dict[str, Any],
) -> str:
    parts = []
    for key in sorted(applies_when):
            value = applies_when[key]
            if isinstance(value, (list, tuple)):
                value_text = "+".join(
                    str(item)
                    for item in value
                )
            else:
                value_text = str(value)
            parts.append(
                f"{key}={value_text}",
            )
        return " ".join(parts)

For example:

{
  "languages": [
    "JavaScript",
    "TypeScript"
  ]
}

becomes:

languages=JavaScript+TypeScript

The complete JSON can still remain in the record content.

Updating existing records

A self-managed processor may need to update an existing record rather than creating a new one.

One option is to derive a stable identity from the record fields.

def memory_identity(
    memory: CandidateMemory,
) -> str:
    identity = (
        f"{memory.key}:"
        f"{memory.category}"
    )

    if memory.applies_when:
            identity += (
                ":"
                + encode_applies_when(
                    memory.applies_when,
                )
            )
        return identity

The processor can then:

  1. retrieve existing records;
  2. index them by identity;
  3. update matching records;
  4. create records that do not already exist.
existing = list_records(namespace)
existing_by_identity = {
    get_record_identity(record): record["memoryRecordId"]
    for record in existing
}
to_create = []
to_update = []
for candidate in candidates:
    identity = memory_identity(candidate)
    if identity in existing_by_identity:
        record = build_record(candidate)
        record["memoryRecordId"] = (
            existing_by_identity[identity]
        )
        to_update.append(record)
    else:
        to_create.append(
            build_record(candidate),
        )

The actual update rules can remain application-specific.

Writing relationship data

A self-managed processor can also extract relationships.

For example:

service-a -> depends_on -> service-b
developer -> works_on -> payments-platform
sqs-fifo -> feeds -> lambda

A relationship object might look like this:

@dataclass
class GraphEdge:
    source: str
    relationship: str
    target: str
    confidence: float
    source_session: str

The processor can store these relationships as AgentCore records.

edge_record = {
    "namespace": (
        f"/graph/{actor_id}/relationships/"
    ),
    "content": {
        "text": json.dumps({
            "source": edge.source,
            "relationship": edge.relationship,
            "target": edge.target,
            "confidence": edge.confidence,
            "source_session": edge.source_session,
        }),
    },
}

The same relationships can optionally be projected into a graph database.

edges = extract_graph_relationships(
    conversation_text,
    session_id,
)
if edges:
    writer.write_edges(
        actor_id,
        edges,
    )
if graph_writer.enabled:
    graph_writer.write_edges(
        edges,
    )

The graph integration can remain optional so the main memory pipeline does not depend on a graph database being available.

Choosing between managed and self-managed strategies

Managed strategies are the quickest way to add standard memory types.

They are appropriate when the application needs:

  • summaries;
  • semantic facts;
  • preferences;
  • episodes;
  • reflections.

A self-managed strategy is appropriate when the application needs custom processing over raw conversation batches.

Examples include:

  • application-specific extraction;
  • custom record schemas;
  • confidence tracking;
  • cross-session aggregation;
  • relationship extraction;
  • external graph projection;
  • integration with another data store;
  • custom update and deletion rules.

The two approaches are complementary.

A Memory resource can use the managed strategies for standard abstractions and a self-managed processor for application-specific requirements.

Conclusion

Amazon Bedrock AgentCore Memory provides four managed strategies for common memory patterns:

Summary
Semantic
User preference
Episodic

Each can be configured with its own namespace and custom extraction or consolidation guidance.

For custom processing, a self-managed strategy sends raw conversation batches through S3 and SNS to an application-owned processor.

Conversation events
    -> AgentCore trigger
    -> S3 payload
    -> SNS notification
    -> Lambda processor
    -> custom memory records

This gives you two implementation paths within the same Memory resource.

Use managed strategies where the built-in abstraction matches the requirement.

Use a self-managed strategy where you need to control the processing and record format yourself.