Skip to content

Commit beac707

Browse files
authored
feat(memory): add CUSTOM strategy type to agentcore-cli (#677) (#694)
* feat(memory): add CUSTOM strategy type to agentcore-cli (#677) Enable self-managed (CUSTOM) memory strategy in the CLI, aligning with the CDK constructs which already support customMemoryStrategy via CloudFormation. Changes: - Add CUSTOM to MemoryStrategyTypeSchema enum and validation - Add CUSTOM description to TUI wizard strategy picker - Update CLI help text, LLM-compacted types, and AGENTS.md - Add documentation for self-managed strategy in docs/ - Update tests: flip rejection tests to acceptance, add new cases - Regenerate asset snapshots Constraint: CDK already maps CUSTOM → customMemoryStrategy in CFN Rejected: New SELF_MANAGED type | cross-layer naming inconsistency with CDK Confidence: high Scope-risk: narrow * refactor(memory): derive VALID_STRATEGIES and help text from schema Eliminates source-of-truth drift by deriving the valid strategies array and CLI help text from MemoryStrategyTypeSchema.options instead of maintaining hand-written duplicates. Constraint: MemoryStrategyTypeSchema is the single source of truth Confidence: high Scope-risk: narrow * fix(memory): add CUSTOM block to session.py templates Without this, selecting CUSTOM as the only strategy would render an empty retrieval_config dict with no guidance. Now both HTTP and A2A Strands templates include a CUSTOM Handlebars block with a TODO comment guiding users to add their own namespace and retrieval config. Constraint: CUSTOM is self-managed — no default namespaces to vend Rejected: Auto-generating a placeholder namespace | would mislead users into thinking it works out of the box Confidence: high Scope-risk: narrow * feat(memory): add CUSTOM as wizard MemoryOption and wire existing memories to new agents Add 'custom' as a 4th MemoryOption in the create/add-agent wizards, completing CUSTOM strategy support across all CLI flows. When adding an agent to a project with existing memories, the template now references all existing memories instead of always creating new ones. - Add 'custom' to MemoryOption type, MEMORY_OPTIONS array, and all validation/help text across create and add-agent commands - Add 'custom' case to mapGenerateInputToMemories (CUSTOM strategy, no default namespaces) and getMemoryLabel in wizard UI - Add mapExistingMemoriesToProviders() to convert project Memory[] to MemoryProviderRenderConfig[] for template rendering - Update mapGenerateConfigToRenderConfig to accept existing memories and merge them with new memory providers (deduped by name) - Update writeAgentToProject to skip adding duplicate memories - Wire existing project.memories through both add-agent paths (AgentPrimitive CLI + useAddAgent TUI hook) Constraint: Strands Agent takes a single session_manager, so session.py references memoryProviders[0] as the primary memory Rejected: Multi-select wizard for memory | template only supports one session_manager, all memory env vars already available at runtime Confidence: high Scope-risk: moderate * test(memory): add CUSTOM strategy and existingMemories tests Add 18 new tests to schema-mapper.test.ts covering: - mapGenerateInputToMemories with 'custom' MemoryOption - mapGenerateConfigToResources with custom memory - mapGenerateConfigToRenderConfig with existingMemories parameter (dedup logic, hasMemory from existing, combining existing + new) - mapExistingMemoriesToProviders (CUSTOM, empty strategies, multiple) Also fix docs/configuration.md missing EPISODIC strategy row. Constraint: CUSTOM strategy has no default namespaces or reflectionNamespaces Constraint: Dedup operates on provider name, not strategy type Confidence: high Scope-risk: narrow * style: fix prettier formatting in docs and source files
1 parent f5e1579 commit beac707

26 files changed

Lines changed: 361 additions & 66 deletions

File tree

docs/commands.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -225,12 +225,12 @@ agentcore add memory \
225225
--expiry 30
226226
```
227227

228-
| Flag | Description |
229-
| ---------------------- | --------------------------------------------------------------------------- |
230-
| `--name <name>` | Memory name |
231-
| `--strategies <types>` | Comma-separated: `SEMANTIC`, `SUMMARIZATION`, `USER_PREFERENCE`, `EPISODIC` |
232-
| `--expiry <days>` | Event expiry duration in days (default: 30, min: 7, max: 365) |
233-
| `--json` | JSON output |
228+
| Flag | Description |
229+
| ---------------------- | ------------------------------------------------------------------------------------- |
230+
| `--name <name>` | Memory name |
231+
| `--strategies <types>` | Comma-separated: `SEMANTIC`, `SUMMARIZATION`, `USER_PREFERENCE`, `EPISODIC`, `CUSTOM` |
232+
| `--expiry <days>` | Event expiry duration in days (default: 30, min: 7, max: 365) |
233+
| `--json` | JSON output |
234234

235235
### add gateway
236236

docs/configuration.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -239,11 +239,13 @@ on the next deployment.
239239

240240
### Memory Strategies
241241

242-
| Strategy | Description |
243-
| ----------------- | --------------------------------------------------- |
244-
| `SEMANTIC` | Vector-based similarity search for relevant context |
245-
| `SUMMARIZATION` | Compressed conversation history |
246-
| `USER_PREFERENCE` | Store user-specific preferences and settings |
242+
| Strategy | Description |
243+
| ----------------- | ----------------------------------------------------------- |
244+
| `SEMANTIC` | Vector-based similarity search for relevant context |
245+
| `SUMMARIZATION` | Compressed conversation history |
246+
| `USER_PREFERENCE` | Store user-specific preferences and settings |
247+
| `EPISODIC` | Capture and reflect on meaningful interaction episodes |
248+
| `CUSTOM` | Self-managed strategy with user-controlled extraction logic |
247249

248250
Strategy configuration:
249251

docs/memory.md

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -168,12 +168,13 @@ conversations, enabling cross-session recall via semantic search.
168168

169169
## Memory Strategies
170170

171-
| Strategy | Description |
172-
| ----------------- | ------------------------------------------------------ |
173-
| `SEMANTIC` | Vector-based similarity search for relevant context |
174-
| `SUMMARIZATION` | Compressed conversation history |
175-
| `USER_PREFERENCE` | Store user-specific preferences and settings |
176-
| `EPISODIC` | Capture and reflect on meaningful interaction episodes |
171+
| Strategy | Description |
172+
| ----------------- | ----------------------------------------------------------- |
173+
| `SEMANTIC` | Vector-based similarity search for relevant context |
174+
| `SUMMARIZATION` | Compressed conversation history |
175+
| `USER_PREFERENCE` | Store user-specific preferences and settings |
176+
| `EPISODIC` | Capture and reflect on meaningful interaction episodes |
177+
| `CUSTOM` | Self-managed strategy with user-controlled extraction logic |
177178

178179
You can combine multiple strategies:
179180

@@ -188,6 +189,36 @@ You can combine multiple strategies:
188189
}
189190
```
190191

192+
### Self-Managed (Custom) Strategy
193+
194+
The `CUSTOM` strategy lets you control memory extraction logic externally rather than relying on built-in
195+
implementations. This is useful when you need specialized extraction pipelines or want to integrate with your own
196+
processing infrastructure.
197+
198+
**Prerequisites:** CUSTOM strategies require user-managed extraction logic and are not functional without it. You must
199+
implement your own extraction mechanism (e.g., via AWS Lambda).
200+
201+
**Key characteristics:**
202+
203+
- No default namespaces are assigned — you provide your own or omit them
204+
- Each memory supports at most one CUSTOM strategy
205+
- You are responsible for implementing the extraction logic that processes memory events
206+
207+
```json
208+
{
209+
"type": "AgentCoreMemory",
210+
"name": "MyMemory",
211+
"eventExpiryDuration": 30,
212+
"strategies": [
213+
{
214+
"type": "CUSTOM",
215+
"name": "my_custom_strategy",
216+
"description": "Custom extraction logic"
217+
}
218+
]
219+
}
220+
```
221+
191222
### Strategy Options
192223

193224
Each strategy can have optional configuration:

src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1702,6 +1702,11 @@ def get_memory_session_manager(session_id: str, actor_id: str) -> Optional[Agent
17021702
{{/if}}
17031703
{{#if (includes memoryProviders.[0].strategies "SUMMARIZATION")}}
17041704
f"/summaries/{actor_id}/{session_id}": RetrievalConfig(top_k=3, relevance_score=0.5),
1705+
{{/if}}
1706+
{{#if (includes memoryProviders.[0].strategies "CUSTOM")}}
1707+
# TODO: Add your custom namespace and retrieval config.
1708+
# Custom strategies use user-controlled extraction logic.
1709+
# Example: f"/custom/{actor_id}/data": RetrievalConfig(top_k=3, relevance_score=0.5),
17051710
{{/if}}
17061711
}
17071712
{{/if}}
@@ -3987,6 +3992,11 @@ def get_memory_session_manager(session_id: str, actor_id: str) -> Optional[Agent
39873992
{{/if}}
39883993
{{#if (includes memoryProviders.[0].strategies "SUMMARIZATION")}}
39893994
f"/summaries/{actor_id}/{session_id}": RetrievalConfig(top_k=3, relevance_score=0.5),
3995+
{{/if}}
3996+
{{#if (includes memoryProviders.[0].strategies "CUSTOM")}}
3997+
# TODO: Add your custom namespace and retrieval config.
3998+
# Custom strategies use user-controlled extraction logic.
3999+
# Example: f"/custom/{actor_id}/data": RetrievalConfig(top_k=3, relevance_score=0.5),
39904000
{{/if}}
39914001
}
39924002
{{/if}}
@@ -4331,7 +4341,7 @@ file maps to a JSON config file and includes validation constraints as comments.
43314341
- **BuildType**: \`'CodeZip'\` | \`'Container'\`
43324342
- **NetworkMode**: \`'PUBLIC'\`
43334343
- **RuntimeVersion**: \`'PYTHON_3_10'\` | \`'PYTHON_3_11'\` | \`'PYTHON_3_12'\` | \`'PYTHON_3_13'\`
4334-
- **MemoryStrategyType**: \`'SEMANTIC'\` | \`'SUMMARIZATION'\` | \`'USER_PREFERENCE'\` | \`'EPISODIC'\`
4344+
- **MemoryStrategyType**: \`'SEMANTIC'\` | \`'SUMMARIZATION'\` | \`'USER_PREFERENCE'\` | \`'EPISODIC'\` | \`'CUSTOM'\`
43354345
43364346
### Build Types
43374347

src/assets/agents/AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ file maps to a JSON config file and includes validation constraints as comments.
6363
- **BuildType**: `'CodeZip'` | `'Container'`
6464
- **NetworkMode**: `'PUBLIC'`
6565
- **RuntimeVersion**: `'PYTHON_3_10'` | `'PYTHON_3_11'` | `'PYTHON_3_12'` | `'PYTHON_3_13'`
66-
- **MemoryStrategyType**: `'SEMANTIC'` | `'SUMMARIZATION'` | `'USER_PREFERENCE'` | `'EPISODIC'`
66+
- **MemoryStrategyType**: `'SEMANTIC'` | `'SUMMARIZATION'` | `'USER_PREFERENCE'` | `'EPISODIC'` | `'CUSTOM'`
6767

6868
### Build Types
6969

src/assets/python/a2a/strands/capabilities/memory/session.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ def get_memory_session_manager(session_id: str, actor_id: str) -> Optional[Agent
2121
{{/if}}
2222
{{#if (includes memoryProviders.[0].strategies "SUMMARIZATION")}}
2323
f"/summaries/{actor_id}/{session_id}": RetrievalConfig(top_k=3, relevance_score=0.5),
24+
{{/if}}
25+
{{#if (includes memoryProviders.[0].strategies "CUSTOM")}}
26+
# TODO: Add your custom namespace and retrieval config.
27+
# Custom strategies use user-controlled extraction logic.
28+
# Example: f"/custom/{actor_id}/data": RetrievalConfig(top_k=3, relevance_score=0.5),
2429
{{/if}}
2530
}
2631
{{/if}}

src/assets/python/http/strands/capabilities/memory/session.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ def get_memory_session_manager(session_id: str, actor_id: str) -> Optional[Agent
2121
{{/if}}
2222
{{#if (includes memoryProviders.[0].strategies "SUMMARIZATION")}}
2323
f"/summaries/{actor_id}/{session_id}": RetrievalConfig(top_k=3, relevance_score=0.5),
24+
{{/if}}
25+
{{#if (includes memoryProviders.[0].strategies "CUSTOM")}}
26+
# TODO: Add your custom namespace and retrieval config.
27+
# Custom strategies use user-controlled extraction logic.
28+
# Example: f"/custom/{actor_id}/data": RetrievalConfig(top_k=3, relevance_score=0.5),
2429
{{/if}}
2530
}
2631
{{/if}}

src/cli/commands/add/__tests__/add-memory.test.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,17 @@ describe('add memory command', () => {
5151
expect(json.error.includes('INVALID'), `Error: ${json.error}`).toBeTruthy();
5252
});
5353

54-
// Issue #235: CUSTOM strategy has been removed
55-
it('rejects CUSTOM strategy', async () => {
54+
// Issue #677: CUSTOM strategy is now supported
55+
it('creates memory with CUSTOM strategy', async () => {
56+
const memoryName = `memCustom${Date.now()}`;
5657
const result = await runCLI(
57-
['add', 'memory', '--name', 'testCustom', '--strategies', 'CUSTOM', '--json'],
58+
['add', 'memory', '--name', memoryName, '--strategies', 'CUSTOM', '--json'],
5859
projectDir
5960
);
60-
expect(result.exitCode).toBe(1);
61+
expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0);
6162
const json = JSON.parse(result.stdout);
62-
expect(json.success).toBe(false);
63-
expect(json.error.includes('CUSTOM'), `Error: ${json.error}`).toBeTruthy();
63+
expect(json.success).toBe(true);
64+
expect(json.memoryName).toBe(memoryName);
6465
});
6566
});
6667

src/cli/commands/add/__tests__/validate.test.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -953,17 +953,15 @@ describe('validate', () => {
953953
).toEqual({ valid: true });
954954
});
955955

956-
// AC23: CUSTOM strategy is not supported (Issue #235)
957-
it('rejects CUSTOM strategy', () => {
956+
// Issue #677: CUSTOM strategy is now supported
957+
it('accepts CUSTOM strategy', () => {
958958
const result = validateAddMemoryOptions({ ...validMemoryOptions, strategies: 'CUSTOM' });
959-
expect(result.valid).toBe(false);
960-
expect(result.error).toContain('Invalid strategy: CUSTOM');
959+
expect(result.valid).toBe(true);
961960
});
962961

963-
it('rejects CUSTOM even when mixed with valid strategies', () => {
962+
it('accepts CUSTOM mixed with other strategies', () => {
964963
const result = validateAddMemoryOptions({ ...validMemoryOptions, strategies: 'SEMANTIC,CUSTOM' });
965-
expect(result.valid).toBe(false);
966-
expect(result.error).toContain('Invalid strategy: CUSTOM');
964+
expect(result.valid).toBe(true);
967965
});
968966

969967
// AC24: Each individual valid strategy should pass

src/cli/commands/add/validate.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
BuildTypeSchema,
55
GatewayExceptionLevelSchema,
66
GatewayNameSchema,
7+
MemoryStrategyTypeSchema,
78
ModelProviderSchema,
89
ProtocolModeSchema,
910
RuntimeAuthorizerTypeSchema,
@@ -33,8 +34,8 @@ export interface ValidationResult {
3334
}
3435

3536
// Constants
36-
const MEMORY_OPTIONS = ['none', 'shortTerm', 'longAndShortTerm'] as const;
37-
const VALID_STRATEGIES = ['SEMANTIC', 'SUMMARIZATION', 'USER_PREFERENCE', 'EPISODIC'];
37+
const MEMORY_OPTIONS = ['none', 'shortTerm', 'longAndShortTerm', 'custom'] as const;
38+
const VALID_STRATEGIES: readonly string[] = MemoryStrategyTypeSchema.options;
3839

3940
/**
4041
* Validate that a credential name exists in the project spec.
@@ -134,7 +135,7 @@ export function validateAddAgentOptions(options: AddAgentOptions): ValidationRes
134135
if (!MEMORY_OPTIONS.includes(options.memory as (typeof MEMORY_OPTIONS)[number])) {
135136
return {
136137
valid: false,
137-
error: `Invalid memory option: ${options.memory}. Use none, shortTerm, or longAndShortTerm`,
138+
error: `Invalid memory option: ${options.memory}. Use none, shortTerm, longAndShortTerm, or custom`,
138139
};
139140
}
140141
// Parse and validate lifecycle configuration for import path
@@ -242,7 +243,7 @@ export function validateAddAgentOptions(options: AddAgentOptions): ValidationRes
242243
if (!MEMORY_OPTIONS.includes(options.memory as (typeof MEMORY_OPTIONS)[number])) {
243244
return {
244245
valid: false,
245-
error: `Invalid memory option: ${options.memory}. Use none, shortTerm, or longAndShortTerm`,
246+
error: `Invalid memory option: ${options.memory}. Use none, shortTerm, longAndShortTerm, or custom`,
246247
};
247248
}
248249
}

0 commit comments

Comments
 (0)