Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Dependencies
node_modules/
npm-debug.log*

# Build outputs
dist/
build/

# Environment variables
.env
.env.local

# IDE files
.vscode/
.idea/
*.swp
*.swo

# OS files
.DS_Store
Thumbs.db

# Logs
*.log
package-lock.json

database.json
129 changes: 129 additions & 0 deletions typescript/01-tutorials/01-fundamentals/07-agent-core-memory/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# State Management in Strands Agents with AWS Bedrock AgentCore Memory

This sample demonstrates how to integrate AWS Bedrock AgentCore memory with Strands Agents to build stateful agents with short-term and long-term memory.

## Key Concepts

- **Short-Term Memory (STM)**: Last K conversation turns loaded from agentcore events
- **Long-Term Memory (LTM)**: Conversation summaries and user preferences retrieved from agentcore
- **Agent State**: Runtime state hidden from the model, accessible to tools
- **Hooks**: Track and respond to agent lifecycle events, sync messages to agentcore

## What This Demo Does

![Architecture](./images/architecture.png)

Creates a shopping assistant that:
- Loads last 3 conversation turns from agentcore (STM)
- Retrieves conversation summary from LTM and adds to system prompt
- Queries user preferences from LTM on-demand via tool
- Tracks items in a user's cart (local storage)
- Auto-syncs all messages to agentcore via hooks

## Project Structure

```
src/
├── index.ts # CLI entry point, conversation loop
├── agent.ts # Agent configuration, tools, hooks
├── memory.ts # AgentCore memory integration (STM/LTM)
└── database.ts # Local cart storage
```

## Getting Started

### Prerequisites

- Node.js 20+
- AWS Bedrock AgentCore access

### Create a memory resource on Amazon Bedrock AgentCore

Follow these steps to create your AgentCore Memory resource

1. Visit the [Amazon Bedrock AgentCore memory creation page](https://console.aws.amazon.com/bedrock-agentcore/memory/create)
2. Enable Built in Summarization Strategy
3. Enable Built-in User Preferences Strategy
4. Click on `Create memory`
5. Copy memory id, and ids of the 2 created strategies

### Installation

```bash
npm install
```

### Configuration

Set environment variables:

```bash
export AWS_REGION=eu-central-1
export AWS_ACCESS_KEY_ID=your_access_key
export AWS_SECRET_ACCESS_KEY=your_secret_key

# AgentCore Memory Configuration
export MEMORY_ID=strands_js_memory-XXXXXXXXXX
export PREFERENCE_STRATEGY_ID=preference_builtin_eayb2-XXXXXXXXXX
export SUMMARY_STRATEGY_ID=summary_builtin_eayb2-XXXXXXXXXX
export STM_TURNS=3 # Number of conversation turns to load (default: 3)
```

### Build

```bash
npm run build
```

### Run

Start a new session:
```bash
npm start USER_123
```

Continue an existing session:
```bash
npm start USER_123 SESSION_12345
```

## Memory Architecture

### Short-Term Memory (STM)
- **Source**: AgentCore events via `ListEventsCommand`
- **Scope**: Last K turns (configurable via `STM_TURNS`)
- **Purpose**: Recent conversation context for the model

### Long-Term Memory (LTM)

#### Conversation Summary
- **Strategy**: `summary_builtin`
- **Namespace**: `/strategies/{summaryStrategyId}/actors/{actorId}/sessions/{sessionId}`
- **Purpose**: Injected into system prompt for context
- **Retrieval**: Top 5 summary records joined together

#### User Preferences
- **Strategy**: `preference_builtin`
- **Namespace**: `/strategies/{preferenceStrategyId}/actors/{actorId}`
- **Purpose**: Retrieved on-demand via `userPreferenceTool`
- **Retrieval**: Top 5 relevant preferences

## Testing Journey

1. **Start conversation**: `npm start USER_123 SESSION_001`
2. **Introduce yourself**: "My name is Alex"
3. **Browse products**: "Show me laptop prices"
4. **Add items**: "Add a laptop to my cart"
5. **Exit**: Type "exit"
6. **Restart same session**: `npm start USER_123 SESSION_001`
7. **Verify memory**: Agent remembers last 3 turns + has conversation summary in context
8. **New session**: `npm start USER_123 SESSION_002`
9. **Check preferences**: "What are my preferences?" (agent uses `userPreferenceTool` to query LTM)

## Key Takeaways

- **STM**: Sliding window of recent messages from agentcore events
- **LTM Summary**: Retrieved and injected into system prompt automatically
- **LTM Preferences**: Queried on-demand via tool when needed
- **Auto-sync**: All messages (user + assistant) automatically synced to agentcore via `MessageAddedEvent` hook
- **Cart**: Persists locally across sessions for the same user
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "strands-agents-ts-agentcore-memory",
"version": "1.0.0",
"description": "Simple TypeScript app using StrandsAgentsSDKTypescript and agentcore memory",
"type": "module",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsx src/index.ts",
"prepare": "npm run build"
},
"dependencies": {
"@aws-sdk/client-bedrock-agentcore": "^3.940.0",
"@strands-agents/sdk": "^0.1.2",
"zod": "^4.1.13"
},
"devDependencies": {
"@types/node": "^20.0.0",
"tsx": "^4.20.6",
"typescript": "^5.0.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { Agent, tool, MessageAddedEvent, BeforeToolCallEvent, AfterToolCallEvent } from '@strands-agents/sdk';
import { z } from 'zod';
import { database } from './database.js';
import { createEvent, retrieveUserPreferences, loadEvents, retrieveConversationSummary } from './memory.js';

const userPreferenceTool = tool({
name: 'userPreferenceTool',
description: 'Lookup user preferences from long-term memory',
inputSchema: z.object({
q: z.string().describe('User preference question')
}),
callback: async ({ q }, context) => {
const userId = context?.agent.state.get('userId') as string;
const preferences = await retrieveUserPreferences(q, userId);
console.log('\n****PREFERENCES_FROM_LTM****\n', JSON.stringify(preferences));
return JSON.stringify(preferences);
}
});

const viewCatalog = tool({
name: 'view_catalog',
description: 'Shows all available products in the catalog',
inputSchema: z.object({ _: z.string().optional() }),
callback: () => {
const products = Object.values(database.products);
return { products, totalProducts: products.length };
}
});

const addToCart = tool({
name: 'add_to_cart',
description: 'Adds an item to the shopping cart',
inputSchema: z.object({
productName: z.string(),
quantity: z.number().default(1)
}),
callback: (input, context) => {
const userId = context?.agent.state.get('userId') as string;
if (!userId) return { success: false, message: 'User not found', cart: null };

const product = database.products[input.productName.toLowerCase()];
if (!product) return { success: false, message: 'Product not found', cart: null };

const cart = database.getCart(userId);
const existing = cart.find(item => item.id === product.id);

if (existing) {
existing.quantity += input.quantity;
} else {
cart.push({ ...product, quantity: input.quantity });
}

database.setCart(userId, cart);
return { success: true, message: `Added ${input.quantity}x ${product.name}`, cart: cart };
}
});

const viewCart = tool({
name: 'view_cart',
description: 'Shows items in cart',
inputSchema: z.object({ _: z.string().optional() }),
callback: (input, context) => {
const userId = context?.agent.state.get('userId') as string;
if (!userId) return { cart: [], total: 0, itemCount: 0 };

const cart = database.getCart(userId);
const total = cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);
return { cart, total, itemCount: cart.length };
}
});

const removeFromCart = tool({
name: 'remove_from_cart',
description: 'Removes an item from cart',
inputSchema: z.object({
productName: z.string()
}),
callback: (input, context) => {
const userId = context?.agent.state.get('userId') as string;
if (!userId) return { success: false, message: 'User not found', cart: null };

const cart = database.getCart(userId);
const filtered = cart.filter(item =>
item.name.toLowerCase() !== input.productName.toLowerCase()
);

database.setCart(userId, filtered);
return { success: true, message: `Removed ${input.productName}`, cart: filtered };
}
});

export async function createAgent(userId: string, sessionId: string) {
console.log('Loading conversation history from agentcore...');
const messages = await loadEvents(userId, sessionId);

console.log('Retrieving conversation summary from LTM...');
const summary = await retrieveConversationSummary(userId, sessionId);

const basePrompt = 'You are a shopping assistant. Help users manage their cart. Lookup for user buying related preferences (currency, payment method, prefered delivery times, ...) when relevant.';
const systemPrompt = summary
? `${basePrompt}\n\nConversation summary:\n${summary}`
: basePrompt;

const agent = new Agent({
tools: [viewCatalog, addToCart, viewCart, removeFromCart, userPreferenceTool],
systemPrompt,
messages,
printer: false,
state: {
userId,
sessionId,
sessionStarted: new Date().toISOString()
}
});

agent.hooks.addCallback(MessageAddedEvent, async (event) => {
const userId = event.agent.state.get('userId') as string;
const sessionId = event.agent.state.get('sessionId') as string;
const textBlock = event.message.content.find((block: any) => 'text' in block && block.text);

if (textBlock && 'text' in textBlock && textBlock.text) {
const role = event.message.role === 'assistant' ? 'ASSISTANT' : 'USER';
await createEvent(textBlock.text as string, role, userId, sessionId);
}
});

return agent;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { readFileSync, writeFileSync, existsSync } from 'fs';

const DB_FILE = './database.json';

export type Product = {
id: string;
name: string;
price: number;
stock: number;
}

export type CartItem = {
id: string;
name: string;
price: number;
quantity: number;
}

type DatabaseData = {
carts: Record<string, CartItem[]>;
}

class Database {
products: Record<string, Product> = {
'laptop': { id: 'prod-1', name: 'Laptop', price: 999, stock: 10 },
'mouse': { id: 'prod-2', name: 'Mouse', price: 29, stock: 50 },
'keyboard': { id: 'prod-3', name: 'Keyboard', price: 79, stock: 30 },
'monitor': { id: 'prod-4', name: 'Monitor', price: 299, stock: 15 },
'headphones': { id: 'prod-5', name: 'Headphones', price: 149, stock: 25 }
};

private data: DatabaseData;

constructor() {
this.data = this.load();
}

private load(): DatabaseData {
if (existsSync(DB_FILE)) {
try {
return JSON.parse(readFileSync(DB_FILE, 'utf-8'));
} catch {}
}
return { carts: {} };
}

private save() {
writeFileSync(DB_FILE, JSON.stringify(this.data, null, 2));
}

getCart(userId: string): CartItem[] {
return this.data.carts[userId] || [];
}

setCart(userId: string, cart: CartItem[]) {
this.data.carts[userId] = cart;
this.save();
}
}

export const database = new Database();
Loading
Loading