Skip to content

Commit 247de18

Browse files
feat: add EPISODIC memory strategy support (#651)
* test: add integ tests for evaluator and online-eval resource lifecycle * test: remove redundant levels/rating-scales section * refactor: type readProjectConfig return as AgentCoreProjectSpec * feat: add EPISODIC memory strategy support Add EPISODIC as a new memory strategy type across the CLI: - Schema enum with default episode namespace (/strategy/{memoryStrategyId}/actor/{actorId}/) - Default reflection namespaces for episodic (required by the service) - reflectionNamespaces field on MemoryStrategy schema - CLI validation, help text, and TUI descriptions - longAndShortTerm preset now includes EPISODIC - LLM-compacted schema and AGENTS.md docs - Unit and integration tests * fix: enforce reflectionNamespaces for EPISODIC strategy and update docs Add Zod refinement to MemoryStrategySchema that requires reflectionNamespaces when strategy type is EPISODIC, matching the AWS service requirement. Update docs/commands.md and docs/memory.md to document the EPISODIC strategy and reflectionNamespaces field. * test: add E2E test for memory with EPISODIC strategy - Add strands-bedrock-memory E2E test that deploys with longAndShortTerm memory (includes all 4 strategies including EPISODIC) - Extend e2e-helper to support configurable memory option - Strengthen integ create-memory test to verify EPISODIC strategy is present with reflectionNamespaces in longAndShortTerm preset * test: add comprehensive EPISODIC coverage across all memory flows - create.test.ts: verify EPISODIC with namespaces and reflectionNamespaces in longAndShortTerm preset - add-remove-resources integ: verify `add memory --strategies EPISODIC` persists reflectionNamespaces in agentcore.json - TUI test: drive Add Memory wizard through strategy multi-select, verify EPISODIC is persisted with correct config in agentcore.json * docs: add TUI screenshots for EPISODIC memory strategy flow * fix: use /episodes/{actorId}/{sessionId} and /reflections/{actorId} for EPISODIC namespaces * style: fix prettier formatting * fix: remove unused OIDC_WELL_KNOWN_SUFFIX constant from validate.ts * fix: correct reflectionNamespaces doc — remove incorrect prefix claim * fix: remove screenshots from docs/ per review feedback * fix: restore advanced-gate screenshots that exist on main * fix: enforce reflectionNamespaces prefix validation per AWS docs - Default reflectionNamespaces changed from /reflections/{actorId} to /episodes/{actorId} so it's a proper prefix of the episode namespace - Added Zod refinement to validate that each reflectionNamespace is a prefix of at least one namespace - Restored doc claim that reflectionNamespaces must be a prefix of namespaces - Added tests for prefix validation (accept valid, reject invalid) --------- Co-authored-by: Harrison Weinstock <hkobew@amazon.com>
1 parent 2142c77 commit 247de18

21 files changed

Lines changed: 454 additions & 49 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` |
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` |
232+
| `--expiry <days>` | Event expiry duration in days (default: 30, min: 7, max: 365) |
233+
| `--json` | JSON output |
234234

235235
### add gateway
236236

docs/memory.md

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -151,17 +151,23 @@ async def invoke(payload, context):
151151

152152
## Memory Strategies
153153

154-
| Strategy | Description |
155-
| ----------------- | --------------------------------------------------- |
156-
| `SEMANTIC` | Vector-based similarity search for relevant context |
157-
| `SUMMARIZATION` | Compressed conversation history |
158-
| `USER_PREFERENCE` | Store user-specific preferences and settings |
154+
| Strategy | Description |
155+
| ----------------- | ------------------------------------------------------ |
156+
| `SEMANTIC` | Vector-based similarity search for relevant context |
157+
| `SUMMARIZATION` | Compressed conversation history |
158+
| `USER_PREFERENCE` | Store user-specific preferences and settings |
159+
| `EPISODIC` | Capture and reflect on meaningful interaction episodes |
159160

160161
You can combine multiple strategies:
161162

162163
```json
163164
{
164-
"strategies": [{ "type": "SEMANTIC" }, { "type": "SUMMARIZATION" }, { "type": "USER_PREFERENCE" }]
165+
"strategies": [
166+
{ "type": "SEMANTIC" },
167+
{ "type": "SUMMARIZATION" },
168+
{ "type": "USER_PREFERENCE" },
169+
{ "type": "EPISODIC" }
170+
]
165171
}
166172
```
167173

@@ -178,12 +184,13 @@ Each strategy can have optional configuration:
178184
}
179185
```
180186

181-
| Field | Required | Description |
182-
| ------------- | -------- | ----------------------------------------------- |
183-
| `type` | Yes | Strategy type |
184-
| `name` | No | Custom name (defaults to `<memoryName>-<type>`) |
185-
| `description` | No | Strategy description |
186-
| `namespaces` | No | Array of namespace paths for scoping |
187+
| Field | Required | Description |
188+
| ---------------------- | ------------- | --------------------------------------------------------------------------- |
189+
| `type` | Yes | Strategy type |
190+
| `name` | No | Custom name (defaults to `<memoryName>-<type>`) |
191+
| `description` | No | Strategy description |
192+
| `namespaces` | No | Array of namespace paths for scoping |
193+
| `reflectionNamespaces` | EPISODIC only | Namespaces for cross-episode reflections (must be a prefix of `namespaces`) |
187194

188195
## Event Expiry
189196

e2e-tests/e2e-helper.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ interface E2EConfig {
2525
modelProvider: string;
2626
requiredEnvVar?: string;
2727
build?: string;
28+
memory?: string;
2829
}
2930

3031
export function createE2ESuite(cfg: E2EConfig) {
@@ -54,7 +55,7 @@ export function createE2ESuite(cfg: E2EConfig) {
5455
'--model-provider',
5556
cfg.modelProvider,
5657
'--memory',
57-
'none',
58+
cfg.memory ?? 'none',
5859
'--json',
5960
];
6061

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { createE2ESuite } from './e2e-helper.js';
2+
3+
createE2ESuite({ framework: 'Strands', modelProvider: 'Bedrock', memory: 'longAndShortTerm' });

integ-tests/add-remove-resources.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,35 @@ describe('integration: add and remove resources', () => {
3636
expect(found, `Memory "${memoryName}" should be in config`).toBe(true);
3737
});
3838

39+
it('adds a memory with EPISODIC strategy and verifies reflectionNamespaces', async () => {
40+
const episodicMemName = `EpiMem${Date.now().toString().slice(-6)}`;
41+
const result = await runCLI(
42+
['add', 'memory', '--name', episodicMemName, '--strategies', 'EPISODIC', '--json'],
43+
project.projectPath
44+
);
45+
46+
expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0);
47+
const json = JSON.parse(result.stdout);
48+
expect(json.success).toBe(true);
49+
50+
// Verify EPISODIC in config with reflectionNamespaces
51+
const config = await readProjectConfig(project.projectPath);
52+
const memories = config.memories as {
53+
name: string;
54+
strategies: { type: string; reflectionNamespaces?: string[] }[];
55+
}[];
56+
const mem = memories.find(m => m.name === episodicMemName);
57+
expect(mem, 'Memory should exist').toBeTruthy();
58+
59+
const episodic = mem!.strategies.find(s => s.type === 'EPISODIC');
60+
expect(episodic, 'EPISODIC strategy should exist').toBeTruthy();
61+
expect(episodic!.reflectionNamespaces, 'Should have reflectionNamespaces').toBeDefined();
62+
expect(episodic!.reflectionNamespaces!.length).toBeGreaterThan(0);
63+
64+
// Clean up
65+
await runCLI(['remove', 'memory', '--name', episodicMemName, '--json'], project.projectPath);
66+
});
67+
3968
it('removes the memory resource', async () => {
4069
const result = await runCLI(['remove', 'memory', '--name', memoryName, '--json'], project.projectPath);
4170

integ-tests/create-memory.test.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,21 @@ describe.skipIf(!prereqs.npm || !prereqs.git)('integration: create with memory o
8181

8282
// longAndShortTerm should have strategies defined
8383
const memory = memories![0]!;
84-
const strategies = memory.strategies as Record<string, unknown>[] | undefined;
84+
const strategies = memory.strategies as { type: string; reflectionNamespaces?: string[] }[] | undefined;
8585
expect(strategies, 'memory should have strategies').toBeDefined();
86-
expect(strategies!.length).toBeGreaterThanOrEqual(2);
86+
expect(strategies!.length).toBe(4);
87+
88+
// Verify all four strategy types are present
89+
const types = strategies!.map(s => s.type);
90+
expect(types).toContain('SEMANTIC');
91+
expect(types).toContain('USER_PREFERENCE');
92+
expect(types).toContain('SUMMARIZATION');
93+
expect(types).toContain('EPISODIC');
94+
95+
// Verify EPISODIC has reflectionNamespaces
96+
const episodic = strategies!.find(s => s.type === 'EPISODIC');
97+
expect(episodic, 'EPISODIC strategy should exist').toBeTruthy();
98+
expect(episodic!.reflectionNamespaces, 'EPISODIC should have reflectionNamespaces').toBeDefined();
99+
expect(episodic!.reflectionNamespaces!.length).toBeGreaterThan(0);
87100
});
88101
});
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
/**
2+
* TUI Integration Test: Add Memory with EPISODIC Strategy
3+
*
4+
* Drives the "Add Memory" wizard through the TUI to verify that when a user
5+
* selects the EPISODIC strategy, it is correctly persisted in agentcore.json
6+
* with both namespaces and reflectionNamespaces.
7+
*
8+
* Exercises:
9+
* - Navigation from HelpScreen -> Add Resource -> Memory
10+
* - Memory name input
11+
* - Expiry selection (default 30 days)
12+
* - Strategy multi-select including EPISODIC
13+
* - Confirm review screen
14+
* - Verification that agentcore.json contains EPISODIC with reflectionNamespaces
15+
*/
16+
import { TuiSession, WaitForTimeoutError } from '../../src/tui-harness/index.js';
17+
import { createMinimalProjectDir } from './helpers.js';
18+
import type { MinimalProjectDirResult } from './helpers.js';
19+
import { readFile as readFileAsync } from 'node:fs/promises';
20+
import { dirname, join } from 'node:path';
21+
import { fileURLToPath } from 'node:url';
22+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
23+
24+
// ---------------------------------------------------------------------------
25+
// Paths & Constants
26+
// ---------------------------------------------------------------------------
27+
28+
const __filename = fileURLToPath(import.meta.url);
29+
const __dirname = dirname(__filename);
30+
const CLI_DIST = join(__dirname, '..', '..', 'dist', 'cli', 'index.mjs');
31+
32+
// ---------------------------------------------------------------------------
33+
// Helpers
34+
// ---------------------------------------------------------------------------
35+
36+
function getScreenText(session: TuiSession): string {
37+
return session.readScreen().lines.join('\n');
38+
}
39+
40+
async function safeWaitFor(session: TuiSession, pattern: string | RegExp, timeoutMs = 10_000): Promise<boolean> {
41+
try {
42+
await session.waitFor(pattern, timeoutMs);
43+
return true;
44+
} catch (err) {
45+
if (err instanceof WaitForTimeoutError) {
46+
return false;
47+
}
48+
throw err;
49+
}
50+
}
51+
52+
const settle = (ms = 400) => new Promise<void>(r => setTimeout(r, ms));
53+
54+
// ---------------------------------------------------------------------------
55+
// Test Suite
56+
// ---------------------------------------------------------------------------
57+
58+
describe('Add Memory with EPISODIC Strategy', () => {
59+
let session: TuiSession;
60+
let projectDir: MinimalProjectDirResult;
61+
62+
beforeAll(async () => {
63+
projectDir = await createMinimalProjectDir({ projectName: 'episodic-test' });
64+
65+
session = await TuiSession.launch({
66+
command: process.execPath,
67+
args: [CLI_DIST],
68+
cwd: projectDir.dir,
69+
cols: 120,
70+
rows: 35,
71+
});
72+
});
73+
74+
afterAll(async () => {
75+
if (session?.alive) {
76+
await session.close();
77+
}
78+
if (projectDir) {
79+
await projectDir.cleanup();
80+
}
81+
});
82+
83+
it('Step 1: reaches HelpScreen', async () => {
84+
const found = await safeWaitFor(session, 'Commands', 15_000);
85+
expect(found).toBe(true);
86+
});
87+
88+
it('Step 2: navigates to Add Resource screen', async () => {
89+
await session.sendKeys('add');
90+
await settle();
91+
await session.sendSpecialKey('enter');
92+
const found = await safeWaitFor(session, 'Add Resource', 5_000);
93+
expect(found).toBe(true);
94+
});
95+
96+
it('Step 3: selects Memory from the resource list', async () => {
97+
// Add Resource list: 0: Agent, 1: Memory
98+
await session.sendSpecialKey('down');
99+
await settle();
100+
101+
const text = getScreenText(session);
102+
expect(text).toContain('Memory');
103+
104+
await session.sendSpecialKey('enter');
105+
const found = await safeWaitFor(session, 'Name', 5_000);
106+
expect(found).toBe(true);
107+
});
108+
109+
it('Step 4: enters memory name', async () => {
110+
await session.sendKeys('EpisodicTestMemory');
111+
await settle();
112+
await session.sendSpecialKey('enter');
113+
114+
const found = await safeWaitFor(session, /[Ee]xpiry|days/, 5_000);
115+
expect(found).toBe(true);
116+
});
117+
118+
it('Step 5: selects default expiry (30 days)', async () => {
119+
// Default is 30 days, just press enter
120+
await session.sendSpecialKey('enter');
121+
await settle();
122+
123+
// Should reach strategies multi-select
124+
const found = await safeWaitFor(session, /[Ss]trateg/, 5_000);
125+
expect(found).toBe(true);
126+
});
127+
128+
it('Step 6: selects all strategies including EPISODIC', async () => {
129+
// Strategy list order matches enum: SEMANTIC, SUMMARIZATION, USER_PREFERENCE, EPISODIC
130+
// Toggle SEMANTIC (cursor starts at 0)
131+
await session.sendSpecialKey('space');
132+
await settle(200);
133+
134+
// Toggle SUMMARIZATION
135+
await session.sendSpecialKey('down');
136+
await session.sendSpecialKey('space');
137+
await settle(200);
138+
139+
// Toggle USER_PREFERENCE
140+
await session.sendSpecialKey('down');
141+
await session.sendSpecialKey('space');
142+
await settle(200);
143+
144+
// Toggle EPISODIC
145+
await session.sendSpecialKey('down');
146+
await session.sendSpecialKey('space');
147+
await settle(200);
148+
149+
// Verify EPISODIC is visible on screen
150+
const text = getScreenText(session);
151+
expect(text).toContain('Episodic');
152+
153+
// Confirm selection
154+
await session.sendSpecialKey('enter');
155+
156+
// Should reach confirm/review screen
157+
const found = await safeWaitFor(session, /[Rr]eview|[Cc]onfirm/, 5_000);
158+
expect(found).toBe(true);
159+
});
160+
161+
it('Step 7: confirm screen shows EPISODIC strategy', () => {
162+
const text = getScreenText(session);
163+
164+
// Verify all strategies appear in the review
165+
expect(text).toContain('SEMANTIC');
166+
expect(text).toContain('EPISODIC');
167+
expect(text).toContain('EpisodicTestMemory');
168+
});
169+
170+
it('Step 8: confirms and creates memory', async () => {
171+
await session.sendSpecialKey('enter');
172+
await settle(1000);
173+
174+
// Should return to HelpScreen or show success
175+
const found = await safeWaitFor(session, /Commands|[Ss]uccess|added/, 10_000);
176+
expect(found).toBe(true);
177+
});
178+
179+
it('Step 9: agentcore.json contains EPISODIC with reflectionNamespaces', async () => {
180+
const configPath = join(projectDir.dir, 'agentcore', 'agentcore.json');
181+
const raw = await readFileAsync(configPath, 'utf-8');
182+
const config = JSON.parse(raw);
183+
184+
const memories = config.memories as {
185+
name: string;
186+
strategies: { type: string; namespaces?: string[]; reflectionNamespaces?: string[] }[];
187+
}[];
188+
expect(memories.length).toBeGreaterThan(0);
189+
190+
const memory = memories.find(m => m.name === 'EpisodicTestMemory');
191+
expect(memory, 'EpisodicTestMemory should exist in agentcore.json').toBeTruthy();
192+
193+
// Verify all 4 strategies present
194+
const types = memory!.strategies.map(s => s.type);
195+
expect(types).toContain('SEMANTIC');
196+
expect(types).toContain('SUMMARIZATION');
197+
expect(types).toContain('USER_PREFERENCE');
198+
expect(types).toContain('EPISODIC');
199+
200+
// Verify EPISODIC has namespaces AND reflectionNamespaces
201+
const episodic = memory!.strategies.find(s => s.type === 'EPISODIC');
202+
expect(episodic, 'EPISODIC strategy should exist').toBeTruthy();
203+
expect(episodic!.namespaces, 'EPISODIC should have namespaces').toBeDefined();
204+
expect(episodic!.namespaces!.length).toBeGreaterThan(0);
205+
expect(episodic!.reflectionNamespaces, 'EPISODIC should have reflectionNamespaces').toBeDefined();
206+
expect(episodic!.reflectionNamespaces!.length).toBeGreaterThan(0);
207+
});
208+
});

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4330,7 +4330,7 @@ file maps to a JSON config file and includes validation constraints as comments.
43304330
- **BuildType**: \`'CodeZip'\` | \`'Container'\`
43314331
- **NetworkMode**: \`'PUBLIC'\`
43324332
- **RuntimeVersion**: \`'PYTHON_3_10'\` | \`'PYTHON_3_11'\` | \`'PYTHON_3_12'\` | \`'PYTHON_3_13'\`
4333-
- **MemoryStrategyType**: \`'SEMANTIC'\` | \`'SUMMARIZATION'\` | \`'USER_PREFERENCE'\`
4333+
- **MemoryStrategyType**: \`'SEMANTIC'\` | \`'SUMMARIZATION'\` | \`'USER_PREFERENCE'\` | \`'EPISODIC'\`
43344334
43354335
### Build Types
43364336

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'`
66+
- **MemoryStrategyType**: `'SEMANTIC'` | `'SUMMARIZATION'` | `'USER_PREFERENCE'` | `'EPISODIC'`
6767

6868
### Build Types
6969

0 commit comments

Comments
 (0)