Skip to content

Commit d653e7a

Browse files
committed
merge: resolve conflict in backend-stack.ts, keep recursive ZIP packager
2 parents 72bed16 + d45cfdb commit d653e7a

13 files changed

Lines changed: 786 additions & 624 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)

frontend/package-lock.json

Lines changed: 13 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
"tailwindcss": "^4",
5757
"tw-animate-css": "^1.2.9",
5858
"typescript": "^5",
59-
"vite": "^7.3.1",
59+
"vite": "^7.3.2",
6060
"vitest": "^4.0.18"
6161
}
6262
}

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: 30 additions & 15 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

@@ -183,7 +186,8 @@ export class BackendStack extends cdk.NestedStack {
183186

184187
// Read requirements
185188
const requirementsPath = path.join(patternDir, "requirements.txt") // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
186-
const requirements = fs.readFileSync(requirementsPath, "utf-8")
189+
const requirements = fs
190+
.readFileSync(requirementsPath, "utf-8")
187191
.split("\n")
188192
.map(line => line.trim())
189193
.filter(line => line && !line.startsWith("#"))
@@ -270,7 +274,16 @@ export class BackendStack extends cdk.NestedStack {
270274
Name: cdk.Names.uniqueResourceName(this, { maxLength: 48 }),
271275
EventExpiryDuration: 30,
272276
Description: `Short-term memory for ${config.stack_name_base} agent`,
273-
MemoryStrategies: [], // Empty array = short-term only (conversation history)
277+
MemoryStrategies: [
278+
{
279+
// Extracts and stores factual information shared by the user across sessions.
280+
// Stored under /facts/{actorId} — retrieved on each turn to personalise responses.
281+
SemanticMemoryStrategy: {
282+
Name: "FactExtractor",
283+
Namespaces: ["/facts/{actorId}"],
284+
},
285+
},
286+
],
274287
MemoryExecutionRoleArn: agentRole.roleArn,
275288
Tags: {
276289
Name: `${config.stack_name_base}_Memory`,
@@ -368,6 +381,14 @@ export class BackendStack extends cdk.NestedStack {
368381
MEMORY_ID: memoryId,
369382
STACK_NAME: config.stack_name_base,
370383
GATEWAY_CREDENTIAL_PROVIDER_NAME: `${config.stack_name_base}-runtime-gateway-auth`, // Used by @requires_access_token decorator to look up the correct provider
384+
// Controls whether the agent activates long-term semantic memory retrieval.
385+
// The memory resource always includes the SemanticMemoryStrategy (no cost to define it),
386+
// but retrieval is only performed when this is "true". See config.yaml: use_long_term_memory.
387+
USE_LONG_TERM_MEMORY: config.backend.use_long_term_memory ? "true" : "false",
388+
// Retrieval tuning for long-term memory. Only used when USE_LONG_TERM_MEMORY is "true".
389+
// See config.yaml: ltm_top_k and ltm_relevance_score.
390+
LTM_TOP_K: String(config.backend.ltm_top_k),
391+
LTM_RELEVANCE_SCORE: String(config.backend.ltm_relevance_score),
371392
}
372393

373394
// Add claude-agent-sdk specific environment variable
@@ -810,8 +831,6 @@ export class BackendStack extends cdk.NestedStack {
810831
},
811832
})
812833

813-
814-
815834
// Store for use in createAgentCoreRuntime()
816835
this.runtimeCredentialProvider = runtimeCredentialProvider
817836

@@ -1110,8 +1129,6 @@ export class BackendStack extends cdk.NestedStack {
11101129
),
11111130
description: "Machine Client Secret for M2M authentication",
11121131
})
1113-
1114-
11151132
}
11161133

11171134
/**
@@ -1139,18 +1156,16 @@ export class BackendStack extends cdk.NestedStack {
11391156

11401157
// Import the user-specified subnets by their IDs.
11411158
// These subnets must exist within the VPC specified above.
1142-
const subnets: ec2.ISubnet[] = vpcConfig.subnet_ids.map(
1143-
(subnetId: string, index: number) =>
1144-
ec2.Subnet.fromSubnetId(this, `ImportedSubnet${index}`, subnetId)
1159+
const subnets: ec2.ISubnet[] = vpcConfig.subnet_ids.map((subnetId: string, index: number) =>
1160+
ec2.Subnet.fromSubnetId(this, `ImportedSubnet${index}`, subnetId)
11451161
)
11461162

11471163
// Build the VPC config props for the AgentCore L2 construct.
11481164
// Security groups are optional — if not provided, the construct creates a default one.
11491165
const securityGroups =
11501166
vpcConfig.security_group_ids && vpcConfig.security_group_ids.length > 0
1151-
? vpcConfig.security_group_ids.map(
1152-
(sgId: string, index: number) =>
1153-
ec2.SecurityGroup.fromSecurityGroupId(this, `ImportedSG${index}`, sgId)
1167+
? vpcConfig.security_group_ids.map((sgId: string, index: number) =>
1168+
ec2.SecurityGroup.fromSecurityGroupId(this, `ImportedSG${index}`, sgId)
11541169
)
11551170
: undefined
11561171

@@ -1203,4 +1218,4 @@ export class BackendStack extends cdk.NestedStack {
12031218
const crypto = require("crypto")
12041219
return crypto.createHash("sha256").update(content).digest("hex").slice(0, 16)
12051220
}
1206-
}
1221+
}

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)