DEV Community

Cover image for AI Agent Memory Made Easy - Amazon Bedrock AgentCore Memory with Spring AI
Yuriy Bezsonov
Yuriy Bezsonov

Posted on

AI Agent Memory Made Easy - Amazon Bedrock AgentCore Memory with Spring AI

What if your AI agent could remember not just the current conversation, but facts about users across sessions? A few months ago, I built a custom three-tier memory system with PostgreSQL, Testcontainers, and AI-powered summarization. It worked, but required significant code and infrastructure.

Now there's a simpler way. The Spring AI Amazon Bedrock AgentCore starter I've been contributing to integrates with Amazon Bedrock AgentCore Memory - a fully managed service that handles both short-term conversation history and long-term knowledge extraction for you.

In this post, I'll show you how to build an AI agent with persistent memory in under 50 lines of Java using JBang - no Maven project setup required. Your agent will remember previous messages and automatically extract facts from conversations - all without managing a database.

Why Amazon Bedrock AgentCore Memory?

AI models are stateless - ask "What's my name?" after introducing yourself, and the model has no idea. AgentCore Memory solves this with:

  1. Fully Managed: AWS handles storage, extraction, and retention. No Redis or PostgreSQL to configure for memory.
  2. Two Memory Types: Short-term for conversation context, long-term for extracted facts and preferences.
  3. Auto-Extraction: Long-term memory runs asynchronously - it automatically extracts semantic facts, user preferences, and summaries from conversations.

Short-Term Memory (STM):

  • Stores raw conversation events within sessions
  • Maintains context for multi-turn conversations
  • Enables conversation continuation even after service restarts

Long-Term Memory (LTM) Strategies:

Strategy Purpose Example
Semantic Extracts factual information "User lives in Berlin"
UserPreference Identifies preferences and choices "Prefers eco-friendly travel"
Summary Generates conversation summaries "Discussed trip planning to Vegas"
Episodic Captures meaningful interaction slices "User booked flight on March 15"

Custom vs Managed: A Comparison

Here's what I had to build for custom memory vs what Amazon Bedrock AgentCore Memory provides out of the box:

Aspect Custom Implementation AgentCore Memory
Infrastructure PostgreSQL + Testcontainers setup None - fully managed
Session Memory MessageWindowChatMemory + JDBC repository Auto-configured from environment
Long-Term Extraction Custom ConversationSummaryService with AI prompts Automatic - runs asynchronously
Preferences Storage Custom tier with manual extraction logic Built-in UserPreference strategy
Summarization Custom prompts parsing ===PREFERENCES=== sections Built-in Summary strategy
Code Required ~300 lines across multiple services ~50 lines total
Database Management Schema migrations, connection pooling None
Scaling Manual PostgreSQL scaling Managed by AWS

The custom approach gives you full control and works without AWS dependencies. But if you're already on AWS and want to move fast, Amazon Bedrock AgentCore Memory handles the infrastructure so you can focus on what your agent actually does.

Prerequisites

You'll need:

AWS Credentials

Configure AWS CLI with credentials that have Amazon Bedrock AgentCore access:

aws configure
Enter fullscreen mode Exit fullscreen mode

Verify access:

aws bedrock-agentcore-control list-memories --no-cli-pager --query 'memories[0].id' --output text
Enter fullscreen mode Exit fullscreen mode

Create the Memory Resource

Let's create an Amazon Bedrock AgentCore Memory resource with LTM strategies. Copy and run in terminal:

# Create memory resource
MEMORY_ID=$(aws bedrock-agentcore-control create-memory \
  --name "memory_demo" --description "Memory for AI Agent demo" \
  --event-expiry-duration 90 --no-cli-pager \
  --query "memory.id" --output text) && echo "Memory ID: ${MEMORY_ID}"

# Wait for memory to become active
echo -n "Waiting for memory to become active..."
while [ "$(aws bedrock-agentcore-control get-memory --memory-id ${MEMORY_ID} \
  --no-cli-pager --query 'memory.status' --output text)" != "ACTIVE" ]
do echo -n "."; sleep 5; done && echo " ACTIVE"

# Add LTM strategies
aws bedrock-agentcore-control update-memory \
  --memory-id "${MEMORY_ID}" --no-cli-pager \
  --memory-strategies '{
    "addMemoryStrategies": [
      {"semanticMemoryStrategy": {"name": "SemanticFacts", "description": "Extracts factual information", "namespaces": ["/strategies/{memoryStrategyId}/actors/{actorId}"]}},
      {"userPreferenceMemoryStrategy": {"name": "UserPreferences", "description": "Extracts user preferences", "namespaces": ["/strategies/{memoryStrategyId}/actors/{actorId}"]}}
    ]
  }'

# Wait for strategies to become active
echo -n "Waiting for strategies to become active..."
while aws bedrock-agentcore-control get-memory --memory-id ${MEMORY_ID} \
  --no-cli-pager --query 'memory.strategies[].status' --output text | grep -q "CREATING"
do echo -n "."; sleep 5; done && echo " ACTIVE"

# Get strategy IDs
SEMANTIC_ID=$(aws bedrock-agentcore-control get-memory \
  --memory-id "${MEMORY_ID}" --no-cli-pager \
  --query "memory.strategies[?name=='SemanticFacts'].strategyId | [0]" --output text)

PREFS_ID=$(aws bedrock-agentcore-control get-memory \
  --memory-id "${MEMORY_ID}" --no-cli-pager \
  --query "memory.strategies[?name=='UserPreferences'].strategyId | [0]" --output text)

echo "Memory ID: ${MEMORY_ID}"
echo "Semantic Strategy ID: ${SEMANTIC_ID}"
echo "Preferences Strategy ID: ${PREFS_ID}"
Enter fullscreen mode Exit fullscreen mode

This creates:

  • Amazon Bedrock AgentCore Memory resource with 90-day event retention
  • Semantic strategy for extracting facts from conversations
  • UserPreference strategy for identifying user preferences

The memory resource takes 2-5 minutes to become active.

The Application

Here's the complete AI agent with memory - under 50 lines of Java:

cat <<'EOF' > MemoryAgent.java
///usr/bin/env jbang "$0" "$@" ; exit $?
//DEPS org.springframework.boot:spring-boot-starter-web:3.5.9
//DEPS org.springframework.ai:spring-ai-starter-model-bedrock-converse:1.1.2
//DEPS org.springaicommunity:spring-ai-memory-bedrock-agentcore:1.0.0-RC5

//JAVA_OPTIONS -Dspring.ai.bedrock.aws.region=us-east-1
//JAVA_OPTIONS -Dspring.ai.bedrock.converse.chat.options.model=global.anthropic.claude-sonnet-4-20250514-v1:0

package com.example;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.client.advisor.api.Advisor;
import org.springframework.ai.chat.memory.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.*;
import java.util.*;

@SpringBootApplication
@RestController
public class MemoryAgent {

    private final ChatClient chatClient;

    public MemoryAgent(ChatClient.Builder builder,
                       @Autowired(required = false) ChatMemoryRepository memoryRepository,
                       @Autowired(required = false) List<Advisor> ltmAdvisors) {
        List<Advisor> advisors = new ArrayList<>();
        if (memoryRepository != null) {
            advisors.add(MessageChatMemoryAdvisor.builder(
                MessageWindowChatMemory.builder().chatMemoryRepository(memoryRepository).build()
            ).build());
        }
        if (ltmAdvisors != null) advisors.addAll(ltmAdvisors);
        this.chatClient = builder.defaultAdvisors(advisors.toArray(new Advisor[0])).build();
    }

    @PostMapping("/chat")
    public String chat(@RequestBody String prompt,
                       @RequestHeader(value = "X-Session-Id", defaultValue = "default") String sessionId) {
        return chatClient.prompt().user(prompt)
            .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, sessionId))
            .call().content();
    }

    public static void main(String[] args) { SpringApplication.run(MemoryAgent.class, args); }
}
EOF
Enter fullscreen mode Exit fullscreen mode

The JBang header declares dependencies, Spring AI auto-configures memory beans from environment variables, and the advisors handle both STM and LTM automatically.

Run the Application

Export the environment variables from the setup step, then run:

export AGENTCORE_MEMORY_MEMORY_ID=${MEMORY_ID}
export AGENTCORE_MEMORY_LONG_TERM_SEMANTIC_STRATEGY_ID=${SEMANTIC_ID}
export AGENTCORE_MEMORY_LONG_TERM_USER_PREFERENCE_STRATEGY_ID=${PREFS_ID}

jbang MemoryAgent.java
Enter fullscreen mode Exit fullscreen mode

JBang downloads dependencies on first run, then starts the Spring Boot application.

Test the Application

In another terminal, test conversation memory:

# First message - introduce yourself
echo "User: Hi, my name is Alex and I live in Berlin"
echo -n "Assistant: "; curl -s -X POST http://localhost:8080/chat \
  -H "Content-Type: text/plain" \
  -H "X-Session-Id: user123" \
  -d "Hi, my name is Alex and I live in Berlin"; echo -e "\n"

# Second message - ask about yourself
echo "User: What's my name and where do I live?"
echo -n "Assistant: "; curl -s -X POST http://localhost:8080/chat \
  -H "Content-Type: text/plain" \
  -H "X-Session-Id: user123" \
  -d "What's my name and where do I live?"; echo
Enter fullscreen mode Exit fullscreen mode

Expected output:

User: Hi, my name is Alex and I live in Berlin
Assistant: Hello Alex! Nice to meet you. Berlin is a wonderful city...

User: What's my name and where do I live?
Assistant: Your name is Alex and you live in Berlin.
Enter fullscreen mode Exit fullscreen mode

The agent remembers your name and location from the previous message. Long-term memory will extract "User's name is Alex" and "User lives in Berlin" as semantic facts for future sessions.

Test Long-Term Memory

Wait a few minutes for LTM extraction, then start a new session:

# New session - agent should remember facts from LTM
echo "User: Do you know anything about me?"
echo -n "Assistant: "; curl -s -X POST http://localhost:8080/chat \
  -H "Content-Type: text/plain" \
  -H "X-Session-Id: user123" \
  -d "Do you know anything about me?"; echo
Enter fullscreen mode Exit fullscreen mode

The agent retrieves extracted facts from long-term memory.

Cleanup

MEMORY_ID=$(aws bedrock-agentcore-control list-memories \
  --query "memories[?starts_with(id, 'memory_demo')].id | [0]" \
  --output text --no-cli-pager) && echo "MEMORY_ID: ${MEMORY_ID}"

[ "${MEMORY_ID}" != "None" ] && aws bedrock-agentcore-control delete-memory \
  --memory-id ${MEMORY_ID} --no-cli-pager
Enter fullscreen mode Exit fullscreen mode

Going Further

This demo uses semantic and preference strategies. AgentCore Memory also supports Summary and Episodic strategies, custom namespaces, and retention policies.

Memory pairs well with RAG - memory handles personal context while RAG provides domain knowledge. Check out my post on Amazon Bedrock Knowledge Base to add RAG to the same agent.

Conclusion

One Java file, a few shell commands, and you have persistent memory with automatic fact extraction. No database to manage, no extraction pipelines to build. The same code works in any Maven or Gradle project - JBang just keeps the demo simple.

I'm excited to contribute to the Spring AI Amazon Bedrock AgentCore starter that makes this integration possible. The source is on GitHub if you want to see how it works.

Learn More

Top comments (1)

Collapse
 
capestart profile image
CapeStart

This is a great walkthrough. Memory is the difference between a fun demo and something people come back to. Quick question: Do you have a preferred pattern for scoping memory per user/session and keeping it auditable?