Skip to content

Commit dba02e3

Browse files
committed
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
1 parent 3162dcd commit dba02e3

11 files changed

Lines changed: 57 additions & 19 deletions

File tree

src/cli/commands/add/validate.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export interface ValidationResult {
3434
}
3535

3636
// Constants
37-
const MEMORY_OPTIONS = ['none', 'shortTerm', 'longAndShortTerm'] as const;
37+
const MEMORY_OPTIONS = ['none', 'shortTerm', 'longAndShortTerm', 'custom'] as const;
3838
const VALID_STRATEGIES: readonly string[] = MemoryStrategyTypeSchema.options;
3939

4040
/**
@@ -135,7 +135,7 @@ export function validateAddAgentOptions(options: AddAgentOptions): ValidationRes
135135
if (!MEMORY_OPTIONS.includes(options.memory as (typeof MEMORY_OPTIONS)[number])) {
136136
return {
137137
valid: false,
138-
error: `Invalid memory option: ${options.memory}. Use none, shortTerm, or longAndShortTerm`,
138+
error: `Invalid memory option: ${options.memory}. Use none, shortTerm, longAndShortTerm, or custom`,
139139
};
140140
}
141141
// Parse and validate lifecycle configuration for import path
@@ -243,7 +243,7 @@ export function validateAddAgentOptions(options: AddAgentOptions): ValidationRes
243243
if (!MEMORY_OPTIONS.includes(options.memory as (typeof MEMORY_OPTIONS)[number])) {
244244
return {
245245
valid: false,
246-
error: `Invalid memory option: ${options.memory}. Use none, shortTerm, or longAndShortTerm`,
246+
error: `Invalid memory option: ${options.memory}. Use none, shortTerm, longAndShortTerm, or custom`,
247247
};
248248
}
249249
}

src/cli/commands/create/action.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ export async function createProject(options: CreateProjectOptions): Promise<Crea
120120
}
121121
}
122122

123-
type MemoryOption = 'none' | 'shortTerm' | 'longAndShortTerm';
123+
type MemoryOption = 'none' | 'shortTerm' | 'longAndShortTerm' | 'custom';
124124

125125
export interface CreateWithAgentOptions {
126126
name: string;

src/cli/commands/create/command.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ async function handleCreateCLI(options: CreateOptions): Promise<void> {
129129
framework: options.framework as SDKFramework | undefined,
130130
modelProvider: options.modelProvider as ModelProvider | undefined,
131131
apiKey: options.apiKey,
132-
memory: (options.memory as 'none' | 'shortTerm' | 'longAndShortTerm') ?? 'none',
132+
memory: (options.memory as 'none' | 'shortTerm' | 'longAndShortTerm' | 'custom') ?? 'none',
133133
protocol: options.protocol as ProtocolMode | undefined,
134134
agentId: options.agentId,
135135
agentAliasId: options.agentAliasId,
@@ -170,7 +170,7 @@ export const registerCreate = (program: Command) => {
170170
)
171171
.option('--model-provider <provider>', 'Model provider (Bedrock, Anthropic, OpenAI, Gemini) [non-interactive]')
172172
.option('--api-key <key>', 'API key for non-Bedrock providers [non-interactive]')
173-
.option('--memory <option>', 'Memory option (none, shortTerm, longAndShortTerm) [non-interactive]')
173+
.option('--memory <option>', 'Memory option (none, shortTerm, longAndShortTerm, custom) [non-interactive]')
174174
.option('--protocol <protocol>', 'Protocol: HTTP, MCP, A2A (default: HTTP) [non-interactive]')
175175
.option('--type <type>', 'Agent type: create or import (default: create) [non-interactive]')
176176
.option('--agent-id <id>', 'Bedrock Agent ID (required for --type import) [non-interactive]')

src/cli/commands/create/validate.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export interface ValidationResult {
2121
error?: string;
2222
}
2323

24-
const MEMORY_OPTIONS = ['none', 'shortTerm', 'longAndShortTerm'] as const;
24+
const MEMORY_OPTIONS = ['none', 'shortTerm', 'longAndShortTerm', 'custom'] as const;
2525

2626
/** Check if a folder with the given name already exists in the directory */
2727
export function validateFolderNotExists(name: string, cwd: string): true | string {
@@ -72,7 +72,7 @@ export function validateCreateOptions(options: CreateOptions, cwd?: string): Val
7272
if (!MEMORY_OPTIONS.includes(options.memory as (typeof MEMORY_OPTIONS)[number])) {
7373
return {
7474
valid: false,
75-
error: `Invalid memory option: ${options.memory}. Use none, shortTerm, or longAndShortTerm`,
75+
error: `Invalid memory option: ${options.memory}. Use none, shortTerm, longAndShortTerm, or custom`,
7676
};
7777
}
7878
return { valid: true };
@@ -190,7 +190,7 @@ export function validateCreateOptions(options: CreateOptions, cwd?: string): Val
190190
if (!MEMORY_OPTIONS.includes(options.memory as (typeof MEMORY_OPTIONS)[number])) {
191191
return {
192192
valid: false,
193-
error: `Invalid memory option: ${options.memory}. Use none, shortTerm, or longAndShortTerm`,
193+
error: `Invalid memory option: ${options.memory}. Use none, shortTerm, longAndShortTerm, or custom`,
194194
};
195195
}
196196
}

src/cli/operations/agent/generate/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export {
33
mapGenerateConfigToResources,
44
mapGenerateConfigToRenderConfig,
55
mapGenerateInputToMemories,
6+
mapExistingMemoriesToProviders,
67
mapModelProviderToCredentials,
78
mapModelProviderToIdentityProviders,
89
type GenerateConfigMappingResult,

src/cli/operations/agent/generate/schema-mapper.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ export function mapGenerateInputToMemories(memory: MemoryOption, projectName: st
7676
...(type === 'EPISODIC' && { reflectionNamespaces: DEFAULT_EPISODIC_REFLECTION_NAMESPACES }),
7777
});
7878
}
79+
} else if (memory === 'custom') {
80+
// Custom strategy has no default namespaces - user provides their own extraction logic
81+
strategies.push({ type: 'CUSTOM' });
7982
}
8083

8184
return [
@@ -252,29 +255,54 @@ async function mapGatewaysToGatewayProviders(): Promise<GatewayProviderRenderCon
252255
}
253256
}
254257

258+
/**
259+
* Maps existing Memory resources to memory providers for template rendering.
260+
* Used when adding an agent to a project that already has memories defined.
261+
*/
262+
export function mapExistingMemoriesToProviders(memories: Memory[]): MemoryProviderRenderConfig[] {
263+
return memories.map(m => ({
264+
name: m.name,
265+
envVarName: computeMemoryEnvVarName(m.name),
266+
strategies: m.strategies.map(s => s.type),
267+
}));
268+
}
269+
255270
/**
256271
* Maps GenerateConfig to AgentRenderConfig for template rendering.
257272
* @param config - Generate config (note: config.projectName is actually the agent name)
258273
* @param identityProviders - Identity providers to include (caller controls credential naming)
274+
* @param existingMemories - Existing project memories to wire to the agent template
259275
*/
260276
export async function mapGenerateConfigToRenderConfig(
261277
config: GenerateConfig,
262-
identityProviders: IdentityProviderRenderConfig[]
278+
identityProviders: IdentityProviderRenderConfig[],
279+
existingMemories?: Memory[]
263280
): Promise<AgentRenderConfig> {
264281
const isMcp = config.protocol === 'MCP';
265282
const gatewayProviders = isMcp ? [] : await mapGatewaysToGatewayProviders();
266283

284+
// Build memory providers: existing memories first, then any new memory from the option.
285+
// This ensures agents in existing projects reference all available memories.
286+
const existingProviders = existingMemories?.length ? mapExistingMemoriesToProviders(existingMemories) : [];
287+
const newProviders = mapMemoryOptionToMemoryProviders(config.memory, config.projectName);
288+
const allProviders = [...existingProviders];
289+
for (const np of newProviders) {
290+
if (!allProviders.some(ep => ep.name === np.name)) {
291+
allProviders.push(np);
292+
}
293+
}
294+
267295
return {
268296
name: config.projectName,
269297
sdkFramework: config.sdk,
270298
targetLanguage: config.language,
271299
modelProvider: config.modelProvider,
272-
hasMemory: isMcp ? false : config.memory !== 'none',
300+
hasMemory: isMcp ? false : allProviders.length > 0,
273301
hasIdentity: isMcp ? false : identityProviders.length > 0,
274302
hasGateway: gatewayProviders.length > 0,
275303
isVpc: config.networkMode === 'VPC',
276304
buildType: config.buildType,
277-
memoryProviders: isMcp ? [] : mapMemoryOptionToMemoryProviders(config.memory, config.projectName),
305+
memoryProviders: isMcp ? [] : allProviders,
278306
identityProviders: isMcp ? [] : identityProviders,
279307
gatewayProviders,
280308
gatewayAuthTypes: [...new Set(gatewayProviders.map(g => g.authType))],

src/cli/operations/agent/generate/write-agent-to-project.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,10 @@ export async function writeAgentToProject(config: GenerateConfig, options?: Writ
4040

4141
// Add resources to project
4242
project.agents.push(agent);
43-
project.memories.push(...memories);
43+
44+
// Only add memories that don't already exist in the project (avoid duplicates)
45+
const newMemories = memories.filter(m => !project.memories.some(em => em.name === m.name));
46+
project.memories.push(...newMemories);
4447

4548
// Handle credentials based on strategy
4649
if (strategy) {

src/cli/primitives/AgentPrimitive.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,10 @@ export class AgentPrimitive extends BasePrimitive<AddAgentOptions, RemovableReso
205205
)
206206
.option('--model-provider <provider>', 'Model provider: Bedrock, Anthropic, OpenAI, Gemini [non-interactive]')
207207
.option('--api-key <key>', 'API key for non-Bedrock providers [non-interactive]')
208-
.option('--memory <mem>', 'Memory: none, shortTerm, longAndShortTerm (create path only) [non-interactive]')
208+
.option(
209+
'--memory <mem>',
210+
'Memory: none, shortTerm, longAndShortTerm, custom (create path only) [non-interactive]'
211+
)
209212
.option('--protocol <protocol>', 'Protocol: HTTP, MCP, A2A (default: HTTP) [non-interactive]')
210213
.option('--code-location <path>', 'Path to existing code (BYO path only) [non-interactive]')
211214
.option('--entrypoint <file>', 'Entry file relative to code-location (BYO, default: main.py) [non-interactive]')
@@ -408,8 +411,8 @@ export class AgentPrimitive extends BasePrimitive<AddAgentOptions, RemovableReso
408411
];
409412
}
410413

411-
// Render templates with correct identity provider
412-
const renderConfig = await mapGenerateConfigToRenderConfig(generateConfig, identityProviders);
414+
// Render templates with correct identity provider and existing project memories
415+
const renderConfig = await mapGenerateConfigToRenderConfig(generateConfig, identityProviders, project.memories);
413416
const renderer = createRenderer(renderConfig);
414417
await renderer.render({ outputDir: projectRoot });
415418

src/cli/tui/screens/agent/useAddAgent.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,8 +208,8 @@ async function handleCreatePath(
208208
];
209209
}
210210

211-
// Generate agent files with correct identity provider
212-
const renderConfig = await mapGenerateConfigToRenderConfig(generateConfig, identityProviders);
211+
// Generate agent files with correct identity provider and existing project memories
212+
const renderConfig = await mapGenerateConfigToRenderConfig(generateConfig, identityProviders, project.memories);
213213
const renderer = createRenderer(renderConfig);
214214
await renderer.render({ outputDir: projectRoot });
215215

src/cli/tui/screens/generate/GenerateWizardUI.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,8 @@ function getMemoryLabel(memory: MemoryOption): string {
374374
return 'Short-term';
375375
case 'longAndShortTerm':
376376
return 'Long-term + short-term';
377+
case 'custom':
378+
return 'Custom';
377379
}
378380
}
379381

0 commit comments

Comments
 (0)