Skip to content

Commit f593ba4

Browse files
authored
Merge pull request #88 from awslabs/feature/add-long-term-memory
feature/add long term memory
2 parents 13f4fcd + a12cbb7 commit f593ba4

11 files changed

Lines changed: 773 additions & 611 deletions

File tree

File renamed without changes.

docs/AGENT_CONFIGURATION.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ A basic conversational agent using the Strands framework with AgentCore Memory i
1616

1717
- Multi-turn conversational chat
1818
- Maintains conversation history with short-term memory
19+
- **Optional long-term memory**: When `use_long_term_memory: true` is set in `config.yaml`, the agent uses a `SemanticMemoryStrategy` to extract and recall facts across sessions (keyed by Cognito user ID). See [Memory Integration Guide](MEMORY_INTEGRATION.md#enabling-long-term-memory) for details.
1920
- Streams responses for better UX
2021
- Authenticated via Cognito (user identity tracked in memory)
2122

docs/MEMORY_INTEGRATION.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,46 @@ AgentCore provides two types of memory: **short-term memory** stores raw convers
66

77
---
88

9+
## Enabling Long-Term Memory
10+
11+
Long-term memory (LTM) is supported on the **`strands-single-agent`** pattern. It uses a `SemanticMemoryStrategy` to automatically extract and store facts from conversations, enabling the agent to recall information across sessions. Facts are keyed by the Cognito `userId`, so each user gets their own persistent memory.
12+
13+
### How It Works
14+
15+
1. **Infrastructure**: The CDK stack always creates the `SemanticMemoryStrategy` on the memory resource (there is no cost to simply define the strategy). A `USE_LONG_TERM_MEMORY` environment variable is passed to the agent runtime.
16+
2. **Agent behavior**: When `USE_LONG_TERM_MEMORY` is `"true"`, the Strands agent's `AgentCoreMemorySessionManager` is configured with a `retrieval_config` that reads from the `/facts/{actorId}` namespace on each turn. When `"false"` (the default), only short-term conversation history is active.
17+
3. **Fact extraction**: AgentCore processes conversation events asynchronously and extracts factual information (e.g., "the user lives in Seattle", "the user prefers Python"). These facts are stored under `/facts/{actorId}` and retrieved on subsequent turns to personalize responses.
18+
19+
### Configuration
20+
21+
Toggle LTM in `infra-cdk/config.yaml`:
22+
23+
```yaml
24+
backend:
25+
use_long_term_memory: true # Enable long-term semantic memory retrieval
26+
```
27+
28+
Then redeploy:
29+
30+
```bash
31+
cd infra-cdk && cdk deploy --all
32+
```
33+
34+
### Cost Considerations
35+
36+
LTM incurs additional charges beyond short-term memory:
37+
38+
- **Storage**: $0.75 per 1,000 memory records stored
39+
- **Retrieval**: $0.50 per 1,000 retrieval calls
40+
41+
When `use_long_term_memory` is `false`, neither cost applies — short-term memory (conversation history) is the only active feature.
42+
43+
### Other Patterns
44+
45+
The **`langgraph-single-agent`** pattern does not currently use long-term memory. It uses `AgentCoreMemorySaver` as a LangGraph checkpointer for short-term conversation persistence only. See the LangGraph section below for details on adding long-term memory via `AgentCoreMemoryStore`.
46+
47+
---
48+
949
## Step 1: Configure Memory with CDK
1050

1151
Memory resources are created using [CloudFormation L1 constructs](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-bedrockagentcore-memory.html). **L2 constructs will be available in future releases.**
@@ -157,6 +197,32 @@ config = AgentCoreMemoryConfig(
157197
)
158198
```
159199

200+
**With long-term memory enabled** (see [Enabling Long-Term Memory](#enabling-long-term-memory) above):
201+
202+
The `strands-single-agent` pattern conditionally enables LTM retrieval based on the `USE_LONG_TERM_MEMORY` environment variable. When enabled, the agent retrieves facts from the `/facts/{actorId}` namespace on each turn:
203+
204+
```python
205+
use_ltm = os.environ.get("USE_LONG_TERM_MEMORY", "false").lower() == "true"
206+
207+
retrieval_config = (
208+
{
209+
"/facts/{actorId}": RetrievalConfig(
210+
top_k=10,
211+
relevance_score=0.3,
212+
)
213+
}
214+
if use_ltm
215+
else None
216+
)
217+
218+
config = AgentCoreMemoryConfig(
219+
memory_id=memory_id,
220+
session_id=session_id,
221+
actor_id=user_id,
222+
retrieval_config=retrieval_config,
223+
)
224+
```
225+
160226
**💡 Example:** See this approach implemented in `patterns/strands-single-agent/basic_agent.py`
161227

162228
**📚 Official AWS Guide:** [Strands SDK Memory Integration](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/strands-sdk-memory.html)

infra-cdk/.prettierrc

Lines changed: 0 additions & 11 deletions
This file was deleted.

infra-cdk/config.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ backend:
1515
deployment_type: docker # Available deployment types: docker (default), zip (not supported for claude-agent-sdk)
1616
network_mode: PUBLIC # Available network modes: PUBLIC (default), VPC
1717

18+
# Long-term memory uses a SemanticMemoryStrategy to extract and store facts across sessions.
19+
# This incurs additional costs: $0.75/1,000 records stored + $0.50/1,000 retrieval calls.
20+
# Set to true when you want to try persistent cross-session memory, which is keyed off of the
21+
# Cognito userId.
22+
use_long_term_memory: false
23+
ltm_top_k: 10 # Number of facts to retrieve per turn (default: 10)
24+
ltm_relevance_score: 0.3 # Minimum similarity threshold for retrieval (default: 0.3)
25+
1826
# VPC configuration - required when network_mode is VPC
1927
# Your VPC must have the necessary VPC endpoints for AWS services.
2028
# See docs/DEPLOYMENT.md for the full list of required VPC endpoints.

infra-cdk/lib/backend-stack.ts

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -113,10 +113,13 @@ export class BackendStack extends cdk.NestedStack {
113113
let agentRuntimeArtifact: agentcore.AgentRuntimeArtifact
114114
let zipPackagerResource: cdk.CustomResource | undefined
115115

116-
if (deploymentType === "zip" && (pattern === "claude-agent-sdk-single-agent" || pattern === "claude-agent-sdk-multi-agent")) {
116+
if (
117+
deploymentType === "zip" &&
118+
(pattern === "claude-agent-sdk-single-agent" || pattern === "claude-agent-sdk-multi-agent")
119+
) {
117120
throw new Error(
118121
"claude-agent-sdk patterns require Docker deployment (deployment_type: docker) " +
119-
"because they need Node.js and the claude-code CLI installed at build time."
122+
"because they need Node.js and the claude-code CLI installed at build time."
120123
)
121124
}
122125

@@ -147,7 +150,7 @@ export class BackendStack extends cdk.NestedStack {
147150

148151
// Read agent code files and encode as base64
149152
const agentCode: Record<string, string> = {}
150-
153+
151154
// Read pattern .py files
152155
for (const file of fs.readdirSync(patternDir)) {
153156
if (file.endsWith(".py")) {
@@ -166,7 +169,8 @@ export class BackendStack extends cdk.NestedStack {
166169

167170
// Read requirements
168171
const requirementsPath = path.join(patternDir, "requirements.txt") // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
169-
const requirements = fs.readFileSync(requirementsPath, "utf-8")
172+
const requirements = fs
173+
.readFileSync(requirementsPath, "utf-8")
170174
.split("\n")
171175
.map(line => line.trim())
172176
.filter(line => line && !line.startsWith("#"))
@@ -241,7 +245,16 @@ export class BackendStack extends cdk.NestedStack {
241245
Name: cdk.Names.uniqueResourceName(this, { maxLength: 48 }),
242246
EventExpiryDuration: 30,
243247
Description: `Short-term memory for ${config.stack_name_base} agent`,
244-
MemoryStrategies: [], // Empty array = short-term only (conversation history)
248+
MemoryStrategies: [
249+
{
250+
// Extracts and stores factual information shared by the user across sessions.
251+
// Stored under /facts/{actorId} — retrieved on each turn to personalise responses.
252+
SemanticMemoryStrategy: {
253+
Name: "FactExtractor",
254+
Namespaces: ["/facts/{actorId}"],
255+
},
256+
},
257+
],
245258
MemoryExecutionRoleArn: agentRole.roleArn,
246259
Tags: {
247260
Name: `${config.stack_name_base}_Memory`,
@@ -339,6 +352,14 @@ export class BackendStack extends cdk.NestedStack {
339352
MEMORY_ID: memoryId,
340353
STACK_NAME: config.stack_name_base,
341354
GATEWAY_CREDENTIAL_PROVIDER_NAME: `${config.stack_name_base}-runtime-gateway-auth`, // Used by @requires_access_token decorator to look up the correct provider
355+
// Controls whether the agent activates long-term semantic memory retrieval.
356+
// The memory resource always includes the SemanticMemoryStrategy (no cost to define it),
357+
// but retrieval is only performed when this is "true". See config.yaml: use_long_term_memory.
358+
USE_LONG_TERM_MEMORY: config.backend.use_long_term_memory ? "true" : "false",
359+
// Retrieval tuning for long-term memory. Only used when USE_LONG_TERM_MEMORY is "true".
360+
// See config.yaml: ltm_top_k and ltm_relevance_score.
361+
LTM_TOP_K: String(config.backend.ltm_top_k),
362+
LTM_RELEVANCE_SCORE: String(config.backend.ltm_relevance_score),
342363
}
343364

344365
// Add claude-agent-sdk specific environment variable
@@ -767,8 +788,6 @@ export class BackendStack extends cdk.NestedStack {
767788
},
768789
})
769790

770-
771-
772791
// Store for use in createAgentCoreRuntime()
773792
this.runtimeCredentialProvider = runtimeCredentialProvider
774793

@@ -935,8 +954,6 @@ export class BackendStack extends cdk.NestedStack {
935954
),
936955
description: "Machine Client Secret for M2M authentication",
937956
})
938-
939-
940957
}
941958

942959
/**
@@ -964,18 +981,16 @@ export class BackendStack extends cdk.NestedStack {
964981

965982
// Import the user-specified subnets by their IDs.
966983
// These subnets must exist within the VPC specified above.
967-
const subnets: ec2.ISubnet[] = vpcConfig.subnet_ids.map(
968-
(subnetId: string, index: number) =>
969-
ec2.Subnet.fromSubnetId(this, `ImportedSubnet${index}`, subnetId)
984+
const subnets: ec2.ISubnet[] = vpcConfig.subnet_ids.map((subnetId: string, index: number) =>
985+
ec2.Subnet.fromSubnetId(this, `ImportedSubnet${index}`, subnetId)
970986
)
971987

972988
// Build the VPC config props for the AgentCore L2 construct.
973989
// Security groups are optional — if not provided, the construct creates a default one.
974990
const securityGroups =
975991
vpcConfig.security_group_ids && vpcConfig.security_group_ids.length > 0
976-
? vpcConfig.security_group_ids.map(
977-
(sgId: string, index: number) =>
978-
ec2.SecurityGroup.fromSecurityGroupId(this, `ImportedSG${index}`, sgId)
992+
? vpcConfig.security_group_ids.map((sgId: string, index: number) =>
993+
ec2.SecurityGroup.fromSecurityGroupId(this, `ImportedSG${index}`, sgId)
979994
)
980995
: undefined
981996

@@ -1028,4 +1043,4 @@ export class BackendStack extends cdk.NestedStack {
10281043
const crypto = require("crypto")
10291044
return crypto.createHash("sha256").update(content).digest("hex").slice(0, 16)
10301045
}
1031-
}
1046+
}

infra-cdk/lib/utils/config-manager.ts

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,23 @@ export interface AppConfig {
3636
network_mode: NetworkMode
3737
/** VPC configuration. Required when network_mode is "VPC". */
3838
vpc?: VpcConfig
39+
/**
40+
* Enable long-term memory (SemanticMemoryStrategy) for the agent.
41+
* When true, the agent extracts and retrieves facts across sessions.
42+
* This incurs additional costs: $0.75/1,000 records stored + $0.50/1,000 retrievals.
43+
* Defaults to false.
44+
*/
45+
use_long_term_memory: boolean
46+
/**
47+
* Number of facts to retrieve per turn when long-term memory is enabled.
48+
* Maps to the top_k parameter of RetrievalConfig. Defaults to 10.
49+
*/
50+
ltm_top_k: number
51+
/**
52+
* Minimum similarity threshold for long-term memory retrieval.
53+
* Maps to the relevance_score parameter of RetrievalConfig. Defaults to 0.3.
54+
*/
55+
ltm_relevance_score: number
3956
}
4057
}
4158

@@ -63,7 +80,9 @@ export class ConfigManager {
6380
configPath = defaultConfigPath
6481
}
6582
if (!fs.existsSync(configPath)) {
66-
throw new Error(`Configuration file ${configPath} does not exist. Please create config.yaml file.`)
83+
throw new Error(
84+
`Configuration file ${configPath} does not exist. Please create config.yaml file.`
85+
)
6786
}
6887

6988
try {
@@ -72,7 +91,9 @@ export class ConfigManager {
7291

7392
const deploymentType = parsedConfig.backend?.deployment_type || "docker"
7493
if (deploymentType !== "docker" && deploymentType !== "zip") {
75-
throw new Error(`Invalid deployment_type '${deploymentType}' in ${configPath}. Must be 'docker' or 'zip'.`)
94+
throw new Error(
95+
`Invalid deployment_type '${deploymentType}' in ${configPath}. Must be 'docker' or 'zip'.`
96+
)
7697
}
7798

7899
const stackNameBase = parsedConfig.stack_name_base
@@ -89,20 +110,28 @@ export class ConfigManager {
89110
// Validate network_mode if provided
90111
const networkMode = parsedConfig.backend?.network_mode || "PUBLIC"
91112
if (networkMode !== "PUBLIC" && networkMode !== "VPC") {
92-
throw new Error(`Invalid network_mode '${networkMode}' in ${configPath}. Must be 'PUBLIC' or 'VPC'.`)
113+
throw new Error(
114+
`Invalid network_mode '${networkMode}' in ${configPath}. Must be 'PUBLIC' or 'VPC'.`
115+
)
93116
}
94117

95118
// Validate VPC configuration when network_mode is VPC
96119
const vpcConfig = parsedConfig.backend?.vpc
97120
if (networkMode === "VPC") {
98121
if (!vpcConfig) {
99-
throw new Error(`backend.vpc configuration is required in ${configPath} when network_mode is 'VPC'.`)
122+
throw new Error(
123+
`backend.vpc configuration is required in ${configPath} when network_mode is 'VPC'.`
124+
)
100125
}
101126
if (!vpcConfig.vpc_id) {
102-
throw new Error(`backend.vpc.vpc_id is required in ${configPath} when network_mode is 'VPC'.`)
127+
throw new Error(
128+
`backend.vpc.vpc_id is required in ${configPath} when network_mode is 'VPC'.`
129+
)
103130
}
104131
if (!vpcConfig.subnet_ids || vpcConfig.subnet_ids.length === 0) {
105-
throw new Error(`backend.vpc.subnet_ids must contain at least one subnet ID in ${configPath} when network_mode is 'VPC'.`)
132+
throw new Error(
133+
`backend.vpc.subnet_ids must contain at least one subnet ID in ${configPath} when network_mode is 'VPC'.`
134+
)
106135
}
107136
}
108137

@@ -114,6 +143,9 @@ export class ConfigManager {
114143
deployment_type: deploymentType,
115144
network_mode: networkMode,
116145
vpc: vpcConfig,
146+
use_long_term_memory: parsedConfig.backend?.use_long_term_memory === true,
147+
ltm_top_k: parsedConfig.backend?.ltm_top_k ?? 10,
148+
ltm_relevance_score: parsedConfig.backend?.ltm_relevance_score ?? 0.3,
117149
},
118150
}
119151
} catch (error) {

0 commit comments

Comments
 (0)