diff --git a/typescript/01-tutorials/01-fundamentals/07-agent-core-memory/.gitignore b/typescript/01-tutorials/01-fundamentals/07-agent-core-memory/.gitignore new file mode 100644 index 00000000..2e4181dc --- /dev/null +++ b/typescript/01-tutorials/01-fundamentals/07-agent-core-memory/.gitignore @@ -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 \ No newline at end of file diff --git a/typescript/01-tutorials/01-fundamentals/07-agent-core-memory/README.md b/typescript/01-tutorials/01-fundamentals/07-agent-core-memory/README.md new file mode 100644 index 00000000..abe6f4a8 --- /dev/null +++ b/typescript/01-tutorials/01-fundamentals/07-agent-core-memory/README.md @@ -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 diff --git a/typescript/01-tutorials/01-fundamentals/07-agent-core-memory/images/architecture.png b/typescript/01-tutorials/01-fundamentals/07-agent-core-memory/images/architecture.png new file mode 100644 index 00000000..e6d76250 Binary files /dev/null and b/typescript/01-tutorials/01-fundamentals/07-agent-core-memory/images/architecture.png differ diff --git a/typescript/01-tutorials/01-fundamentals/07-agent-core-memory/images/create-memory.png b/typescript/01-tutorials/01-fundamentals/07-agent-core-memory/images/create-memory.png new file mode 100644 index 00000000..318ebe82 Binary files /dev/null and b/typescript/01-tutorials/01-fundamentals/07-agent-core-memory/images/create-memory.png differ diff --git a/typescript/01-tutorials/01-fundamentals/07-agent-core-memory/package.json b/typescript/01-tutorials/01-fundamentals/07-agent-core-memory/package.json new file mode 100644 index 00000000..eea029df --- /dev/null +++ b/typescript/01-tutorials/01-fundamentals/07-agent-core-memory/package.json @@ -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" + } +} diff --git a/typescript/01-tutorials/01-fundamentals/07-agent-core-memory/src/agent.ts b/typescript/01-tutorials/01-fundamentals/07-agent-core-memory/src/agent.ts new file mode 100644 index 00000000..4e928a3f --- /dev/null +++ b/typescript/01-tutorials/01-fundamentals/07-agent-core-memory/src/agent.ts @@ -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; +} diff --git a/typescript/01-tutorials/01-fundamentals/07-agent-core-memory/src/database.ts b/typescript/01-tutorials/01-fundamentals/07-agent-core-memory/src/database.ts new file mode 100644 index 00000000..a94c48d7 --- /dev/null +++ b/typescript/01-tutorials/01-fundamentals/07-agent-core-memory/src/database.ts @@ -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; +} + +class Database { + products: Record = { + '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(); diff --git a/typescript/01-tutorials/01-fundamentals/07-agent-core-memory/src/index.ts b/typescript/01-tutorials/01-fundamentals/07-agent-core-memory/src/index.ts new file mode 100644 index 00000000..08dbe144 --- /dev/null +++ b/typescript/01-tutorials/01-fundamentals/07-agent-core-memory/src/index.ts @@ -0,0 +1,32 @@ +import * as readline from 'readline'; +import { createAgent } from './agent.js'; +import { database } from './database.js'; + +const userId = process.argv[2] || 'USER_1'; +const sessionId = process.argv[3] || `SESSION_${Date.now()}`; + +async function chat() { + const agent = await createAgent(userId, sessionId); + + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); + console.log(`Shopping Assistant Ready for ${userId}! (type "exit" to quit)\n`); + + const question = (prompt: string): Promise => + new Promise((resolve) => rl.question(prompt, resolve)); + + while (true) { + const input = await question('You: '); + if (input.toLowerCase() === 'exit') { + console.log('\n[Final Cart]', database.getCart(userId)); + rl.close(); + break; + } + + const result = await agent.invoke(input); + const textBlock = result.lastMessage.content.find(block => 'text' in block); + const text = textBlock && 'text' in textBlock ? textBlock.text : 'No response'; + console.log(`\nAssistant: ${text}\n`); + } +} + +chat(); diff --git a/typescript/01-tutorials/01-fundamentals/07-agent-core-memory/src/memory.ts b/typescript/01-tutorials/01-fundamentals/07-agent-core-memory/src/memory.ts new file mode 100644 index 00000000..1b6d8547 --- /dev/null +++ b/typescript/01-tutorials/01-fundamentals/07-agent-core-memory/src/memory.ts @@ -0,0 +1,74 @@ +import { BedrockAgentCoreClient, CreateEventCommand, ListEventsCommand, RetrieveMemoryRecordsCommand } from "@aws-sdk/client-bedrock-agentcore"; + +const client = new BedrockAgentCoreClient({ region: process.env.AWS_REGION || 'us-east-1' }); + +export const MEMORY_ID = process.env.MEMORY_ID || 'strands_js_memory'; +export const PREFERENCE_STRATEGY_ID = process.env.PREFERENCE_STRATEGY_ID || 'preference_builtin'; +export const SUMMARY_STRATEGY_ID = process.env.SUMMARY_STRATEGY_ID || 'summary_builtin'; +export const STM_TURNS = parseInt(process.env.STM_TURNS || '3'); + +export const createEvent = async (text: string, role: 'ASSISTANT' | 'USER', actorId: string, sessionId: string) => { + const command = new CreateEventCommand({ + memoryId: MEMORY_ID, + actorId, + sessionId, + eventTimestamp: new Date(), + payload: [{ + conversational: { + content: { text }, + role, + } + }] + }); + await client.send(command); +}; + +export const loadEvents = async (actorId: string, sessionId: string) => { + const command = new ListEventsCommand({ + memoryId: MEMORY_ID, + actorId, + sessionId, + maxResults: STM_TURNS * 2, + }); + const result = await client.send(command); + const messages = result.events?.sort((a, b) => + (a.eventTimestamp?.getTime() || 0) - (b.eventTimestamp?.getTime() || 0) + ).map(e => { + const message = e.payload![0].conversational!; + return { + type: 'message' as const, + role: (message.role === 'ASSISTANT' ? 'assistant' : 'user') as 'user' | 'assistant', + content: [{ type: "textBlock" as const, text: message.content!.text! }] + }; + }) || []; + + console.log(`Loaded ${messages.length} messages (${STM_TURNS} turns)`); + return messages; +}; + +export const retrieveConversationSummary = async (actorId: string, sessionId: string) => { + const command = new RetrieveMemoryRecordsCommand({ + memoryId: MEMORY_ID, + namespace: `/strategies/${SUMMARY_STRATEGY_ID}/actors/${actorId}/sessions/${sessionId}`, + searchCriteria: { + searchQuery: "conversation summary", + topK: 5 + }, + }); + const result = await client.send(command); + const summaries = result.memoryRecordSummaries?.map(sum => sum.content?.text).filter(Boolean) || []; + return summaries.join('\n\n'); +}; + +export const retrieveUserPreferences = async (q: string, actorId: string) => { + const command = new RetrieveMemoryRecordsCommand({ + memoryId: MEMORY_ID, + namespace: `/strategies/${PREFERENCE_STRATEGY_ID}/actors/${actorId}`, + searchCriteria: { + searchQuery: q, + topK: 5 + }, + }); + const result = await client.send(command); + return result.memoryRecordSummaries?.map(sum => sum.content?.text) || []; +}; diff --git a/typescript/01-tutorials/01-fundamentals/07-agent-core-memory/tsconfig.json b/typescript/01-tutorials/01-fundamentals/07-agent-core-memory/tsconfig.json new file mode 100644 index 00000000..acd2ff2c --- /dev/null +++ b/typescript/01-tutorials/01-fundamentals/07-agent-core-memory/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "node", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +}