diff --git a/docs/configuration.md b/docs/configuration.md index 6e42c101a..05f580107 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -244,7 +244,7 @@ Strategy configuration: "type": "SEMANTIC", "name": "custom_semantic", "description": "Custom semantic memory", - "namespaces": ["/users/facts", "/users/preferences"] + "namespaceTemplates": ["/users/facts", "/users/preferences"] } ``` diff --git a/docs/memory.md b/docs/memory.md index ccfeb8c61..cd429ce57 100644 --- a/docs/memory.md +++ b/docs/memory.md @@ -196,17 +196,19 @@ Each strategy can have optional configuration: "type": "SEMANTIC", "name": "custom_semantic", "description": "Custom semantic memory", - "namespaces": ["/users/facts", "/users/preferences"] + "namespaceTemplates": ["/users/facts", "/users/preferences"] } ``` -| Field | Required | Description | -| ---------------------- | ------------- | --------------------------------------------------------------------------- | -| `type` | Yes | Strategy type | -| `name` | No | Custom name (defaults to `-`) | -| `description` | No | Strategy description | -| `namespaces` | No | Array of namespace paths for scoping | -| `reflectionNamespaces` | EPISODIC only | Namespaces for cross-episode reflections (must be a prefix of `namespaces`) | +| Field | Required | Description | +| ------------------------------ | ------------- | --------------------------------------------------------------------------------------------- | +| `type` | Yes | Strategy type | +| `name` | No | Custom name (defaults to `-`) | +| `description` | No | Strategy description | +| `namespaceTemplates` | No | Array of namespace templates for scoping | +| `reflectionNamespaceTemplates` | EPISODIC only | Templates for cross-episode reflections (must be a prefix of `namespaceTemplates`) | +| `namespaces` | No | **Deprecated alias for `namespaceTemplates`.** Accepted for backward compatibility. | +| `reflectionNamespaces` | EPISODIC only | **Deprecated alias for `reflectionNamespaceTemplates`.** Accepted for backward compatibility. | ## Event Expiry diff --git a/e2e-tests/fixtures/import/setup_memory_full.py b/e2e-tests/fixtures/import/setup_memory_full.py index 277179cfb..1f4a0253c 100644 --- a/e2e-tests/fixtures/import/setup_memory_full.py +++ b/e2e-tests/fixtures/import/setup_memory_full.py @@ -31,7 +31,7 @@ def main(): "semanticMemoryStrategy": { "name": "bugbash_semantic", "description": "Semantic strategy for bugbash testing", - "namespaces": ["default"], + "namespaceTemplates": ["default"], } }, { @@ -68,7 +68,7 @@ def main(): print(f" eventExpiryDuration: 30") print(f" executionRoleArn: {role_arn}") print(" strategies:") - print(" - type: SEMANTIC, name: bugbash_semantic, namespaces: [default]") + print(" - type: SEMANTIC, name: bugbash_semantic, namespaceTemplates: [default]") print(" - type: SUMMARIZATION, name: bugbash_summary") print(" - type: USER_PREFERENCE, name: bugbash_userpref") print(" tags: {env: bugbash, team: agentcore-cli}") diff --git a/integ-tests/add-remove-resources.test.ts b/integ-tests/add-remove-resources.test.ts index 19d607481..11d763bbd 100644 --- a/integ-tests/add-remove-resources.test.ts +++ b/integ-tests/add-remove-resources.test.ts @@ -44,7 +44,7 @@ describe('integration: add and remove resources', () => { telemetry.assertMetricEmitted({ command: 'add.memory', exit_reason: 'success' }); }); - it('adds a memory with EPISODIC strategy and verifies reflectionNamespaces', async () => { + it('adds a memory with EPISODIC strategy and verifies reflectionNamespaceTemplates', async () => { const episodicMemName = `EpiMem${Date.now().toString().slice(-6)}`; const result = await runCLI( ['add', 'memory', '--name', episodicMemName, '--strategies', 'EPISODIC', '--json'], @@ -56,19 +56,19 @@ describe('integration: add and remove resources', () => { const json = JSON.parse(result.stdout); expect(json.success).toBe(true); - // Verify EPISODIC in config with reflectionNamespaces + // Verify EPISODIC in config with reflectionNamespaceTemplates const config = await readProjectConfig(project.projectPath); const memories = config.memories as { name: string; - strategies: { type: string; reflectionNamespaces?: string[] }[]; + strategies: { type: string; reflectionNamespaceTemplates?: string[] }[]; }[]; const mem = memories.find(m => m.name === episodicMemName); expect(mem, 'Memory should exist').toBeTruthy(); const episodic = mem!.strategies.find(s => s.type === 'EPISODIC'); expect(episodic, 'EPISODIC strategy should exist').toBeTruthy(); - expect(episodic!.reflectionNamespaces, 'Should have reflectionNamespaces').toBeDefined(); - expect(episodic!.reflectionNamespaces!.length).toBeGreaterThan(0); + expect(episodic!.reflectionNamespaceTemplates, 'Should have reflectionNamespaceTemplates').toBeDefined(); + expect(episodic!.reflectionNamespaceTemplates!.length).toBeGreaterThan(0); telemetry.assertMetricEmitted({ command: 'add.memory', diff --git a/integ-tests/create-memory.test.ts b/integ-tests/create-memory.test.ts index ac80f1ba4..6079441f0 100644 --- a/integ-tests/create-memory.test.ts +++ b/integ-tests/create-memory.test.ts @@ -80,7 +80,7 @@ describe.skipIf(!prereqs.npm || !prereqs.git)('integration: create with memory o // longAndShortTerm should have strategies defined const memory = memories![0]!; - const strategies = memory.strategies as { type: string; reflectionNamespaces?: string[] }[] | undefined; + const strategies = memory.strategies as { type: string; reflectionNamespaceTemplates?: string[] }[] | undefined; expect(strategies, 'memory should have strategies').toBeDefined(); expect(strategies!.length).toBe(4); @@ -91,10 +91,10 @@ describe.skipIf(!prereqs.npm || !prereqs.git)('integration: create with memory o expect(types).toContain('SUMMARIZATION'); expect(types).toContain('EPISODIC'); - // Verify EPISODIC has reflectionNamespaces + // Verify EPISODIC has reflectionNamespaceTemplates const episodic = strategies!.find(s => s.type === 'EPISODIC'); expect(episodic, 'EPISODIC strategy should exist').toBeTruthy(); - expect(episodic!.reflectionNamespaces, 'EPISODIC should have reflectionNamespaces').toBeDefined(); - expect(episodic!.reflectionNamespaces!.length).toBeGreaterThan(0); + expect(episodic!.reflectionNamespaceTemplates, 'EPISODIC should have reflectionNamespaceTemplates').toBeDefined(); + expect(episodic!.reflectionNamespaceTemplates!.length).toBeGreaterThan(0); }); }); diff --git a/integ-tests/tui/add-memory-episodic.test.ts b/integ-tests/tui/add-memory-episodic.test.ts index c4dd65d46..372ee286d 100644 --- a/integ-tests/tui/add-memory-episodic.test.ts +++ b/integ-tests/tui/add-memory-episodic.test.ts @@ -3,7 +3,7 @@ * * Drives the "Add Memory" wizard through the TUI to verify that when a user * selects the EPISODIC strategy, it is correctly persisted in agentcore.json - * with both namespaces and reflectionNamespaces. + * with both namespaceTemplates and reflectionNamespaceTemplates. * * Exercises: * - Navigation from HelpScreen -> Add Resource -> Memory @@ -11,7 +11,7 @@ * - Expiry selection (default 30 days) * - Strategy multi-select including EPISODIC * - Confirm review screen - * - Verification that agentcore.json contains EPISODIC with reflectionNamespaces + * - Verification that agentcore.json contains EPISODIC with reflectionNamespaceTemplates */ import { TuiSession, WaitForTimeoutError } from '../../src/tui-harness/index.js'; import { createMinimalProjectDir } from './helpers.js'; @@ -176,14 +176,14 @@ describe('Add Memory with EPISODIC Strategy', () => { expect(found).toBe(true); }); - it('Step 9: agentcore.json contains EPISODIC with reflectionNamespaces', async () => { + it('Step 9: agentcore.json contains EPISODIC with reflectionNamespaceTemplates', async () => { const configPath = join(projectDir.dir, 'agentcore', 'agentcore.json'); const raw = await readFileAsync(configPath, 'utf-8'); const config = JSON.parse(raw); const memories = config.memories as { name: string; - strategies: { type: string; namespaces?: string[]; reflectionNamespaces?: string[] }[]; + strategies: { type: string; namespaceTemplates?: string[]; reflectionNamespaceTemplates?: string[] }[]; }[]; expect(memories.length).toBeGreaterThan(0); @@ -197,12 +197,12 @@ describe('Add Memory with EPISODIC Strategy', () => { expect(types).toContain('USER_PREFERENCE'); expect(types).toContain('EPISODIC'); - // Verify EPISODIC has namespaces AND reflectionNamespaces + // Verify EPISODIC has namespaceTemplates AND reflectionNamespaceTemplates const episodic = memory!.strategies.find(s => s.type === 'EPISODIC'); expect(episodic, 'EPISODIC strategy should exist').toBeTruthy(); - expect(episodic!.namespaces, 'EPISODIC should have namespaces').toBeDefined(); - expect(episodic!.namespaces!.length).toBeGreaterThan(0); - expect(episodic!.reflectionNamespaces, 'EPISODIC should have reflectionNamespaces').toBeDefined(); - expect(episodic!.reflectionNamespaces!.length).toBeGreaterThan(0); + expect(episodic!.namespaceTemplates, 'EPISODIC should have namespaceTemplates').toBeDefined(); + expect(episodic!.namespaceTemplates!.length).toBeGreaterThan(0); + expect(episodic!.reflectionNamespaceTemplates, 'EPISODIC should have reflectionNamespaceTemplates').toBeDefined(); + expect(episodic!.reflectionNamespaceTemplates!.length).toBeGreaterThan(0); }); }); diff --git a/src/cli/aws/agentcore-control.ts b/src/cli/aws/agentcore-control.ts index 81cc723db..159939b17 100644 --- a/src/cli/aws/agentcore-control.ts +++ b/src/cli/aws/agentcore-control.ts @@ -368,8 +368,8 @@ export interface MemoryDetail { type: string; name?: string; description?: string; - namespaces?: string[]; - reflectionNamespaces?: string[]; + namespaceTemplates?: string[]; + reflectionNamespaceTemplates?: string[]; }[]; tags?: Record; encryptionKeyArn?: string; @@ -422,13 +422,17 @@ export async function getMemoryDetail(options: GetMemoryOptions): Promise 0 && { reflectionNamespaces: episodicNamespaces }), + ...(namespaceTemplates && namespaceTemplates.length > 0 && { namespaceTemplates }), + ...(reflectionTemplates && + reflectionTemplates.length > 0 && { reflectionNamespaceTemplates: reflectionTemplates }), }; }), }; diff --git a/src/cli/commands/add/__tests__/add-memory.test.ts b/src/cli/commands/add/__tests__/add-memory.test.ts index 3a518f793..42a19a40a 100644 --- a/src/cli/commands/add/__tests__/add-memory.test.ts +++ b/src/cli/commands/add/__tests__/add-memory.test.ts @@ -138,20 +138,20 @@ describe('add memory command', () => { const memory = projectSpec.memories.find((m: { name: string }) => m.name === memoryName); const semantic = memory?.strategies?.find((s: { type: string }) => s.type === 'SEMANTIC'); - expect(semantic?.namespaces).toEqual(['/users/{actorId}/facts']); + expect(semantic?.namespaceTemplates).toEqual(['/users/{actorId}/facts']); const userPref = memory?.strategies?.find((s: { type: string }) => s.type === 'USER_PREFERENCE'); - expect(userPref?.namespaces).toEqual(['/users/{actorId}/preferences']); + expect(userPref?.namespaceTemplates).toEqual(['/users/{actorId}/preferences']); const summarization = memory?.strategies?.find((s: { type: string }) => s.type === 'SUMMARIZATION'); - expect(summarization?.namespaces).toEqual(['/summaries/{actorId}/{sessionId}']); + expect(summarization?.namespaceTemplates).toEqual(['/summaries/{actorId}/{sessionId}']); const episodic = memory?.strategies?.find((s: { type: string }) => s.type === 'EPISODIC'); - expect(episodic?.namespaces).toEqual(['/episodes/{actorId}/{sessionId}']); - expect(episodic?.reflectionNamespaces).toEqual(['/episodes/{actorId}']); + expect(episodic?.namespaceTemplates).toEqual(['/episodes/{actorId}/{sessionId}']); + expect(episodic?.reflectionNamespaceTemplates).toEqual(['/episodes/{actorId}']); }); - it('creates memory with EPISODIC strategy including default namespaces and reflectionNamespaces', async () => { + it('creates memory with EPISODIC strategy including default namespaceTemplates and reflectionNamespaceTemplates', async () => { const memoryName = `epi${Date.now()}`; const result = await runCLI( ['add', 'memory', '--name', memoryName, '--strategies', 'EPISODIC', '--json'], @@ -162,8 +162,8 @@ describe('add memory command', () => { const memory = projectSpec.memories.find((m: { name: string }) => m.name === memoryName); const episodic = memory?.strategies?.find((s: { type: string }) => s.type === 'EPISODIC'); expect(episodic).toBeTruthy(); - expect(episodic?.namespaces).toEqual(['/episodes/{actorId}/{sessionId}']); - expect(episodic?.reflectionNamespaces).toEqual(['/episodes/{actorId}']); + expect(episodic?.namespaceTemplates).toEqual(['/episodes/{actorId}/{sessionId}']); + expect(episodic?.reflectionNamespaceTemplates).toEqual(['/episodes/{actorId}']); }); }); }); diff --git a/src/cli/commands/create/__tests__/create.test.ts b/src/cli/commands/create/__tests__/create.test.ts index 72ab2c64f..a186c6e75 100644 --- a/src/cli/commands/create/__tests__/create.test.ts +++ b/src/cli/commands/create/__tests__/create.test.ts @@ -143,18 +143,18 @@ describe('create command', () => { const memory = projectSpec.memories[0]; const semantic = memory?.strategies?.find((s: { type: string }) => s.type === 'SEMANTIC'); - expect(semantic?.namespaces).toEqual(['/users/{actorId}/facts']); + expect(semantic?.namespaceTemplates).toEqual(['/users/{actorId}/facts']); const userPref = memory?.strategies?.find((s: { type: string }) => s.type === 'USER_PREFERENCE'); - expect(userPref?.namespaces).toEqual(['/users/{actorId}/preferences']); + expect(userPref?.namespaceTemplates).toEqual(['/users/{actorId}/preferences']); const summarization = memory?.strategies?.find((s: { type: string }) => s.type === 'SUMMARIZATION'); - expect(summarization?.namespaces).toEqual(['/summaries/{actorId}/{sessionId}']); + expect(summarization?.namespaceTemplates).toEqual(['/summaries/{actorId}/{sessionId}']); const episodic = memory?.strategies?.find((s: { type: string }) => s.type === 'EPISODIC'); expect(episodic, 'EPISODIC strategy should exist in longAndShortTerm').toBeTruthy(); - expect(episodic?.namespaces).toEqual(['/episodes/{actorId}/{sessionId}']); - expect(episodic?.reflectionNamespaces).toEqual(['/episodes/{actorId}']); + expect(episodic?.namespaceTemplates).toEqual(['/episodes/{actorId}/{sessionId}']); + expect(episodic?.reflectionNamespaceTemplates).toEqual(['/episodes/{actorId}']); }); it('uses --project-name for project and --name for agent resource', async () => { diff --git a/src/cli/commands/import/import-memory.ts b/src/cli/commands/import/import-memory.ts index 02213c9f1..2558d941a 100644 --- a/src/cli/commands/import/import-memory.ts +++ b/src/cli/commands/import/import-memory.ts @@ -42,14 +42,16 @@ function filterInternalNamespaces(namespaces: string[]): string[] { function toMemorySpec(memory: MemoryDetail, localName: string): Memory { const strategies: Memory['strategies'] = memory.strategies.map(s => { const mappedType = mapStrategyType(s.type); - const filteredNamespaces = s.namespaces ? filterInternalNamespaces(s.namespaces) : []; + const filteredTemplates = s.namespaceTemplates ? filterInternalNamespaces(s.namespaceTemplates) : []; return { type: mappedType as Memory['strategies'][number]['type'], ...(s.name && { name: s.name }), ...(s.description && { description: s.description }), - ...(filteredNamespaces.length > 0 && { namespaces: filteredNamespaces }), - ...(s.reflectionNamespaces && - s.reflectionNamespaces.length > 0 && { reflectionNamespaces: s.reflectionNamespaces }), + ...(filteredTemplates.length > 0 && { namespaceTemplates: filteredTemplates }), + ...(s.reflectionNamespaceTemplates && + s.reflectionNamespaceTemplates.length > 0 && { + reflectionNamespaceTemplates: s.reflectionNamespaceTemplates, + }), }; }); diff --git a/src/cli/operations/agent/generate/__tests__/schema-mapper.test.ts b/src/cli/operations/agent/generate/__tests__/schema-mapper.test.ts index d30ae23fc..8fdcf9e82 100644 --- a/src/cli/operations/agent/generate/__tests__/schema-mapper.test.ts +++ b/src/cli/operations/agent/generate/__tests__/schema-mapper.test.ts @@ -46,10 +46,10 @@ describe('mapGenerateInputToMemories', () => { expect(types).toContain('EPISODIC'); }); - it('includes default namespaces for strategies', () => { + it('includes default namespace templates for strategies', () => { const result = mapGenerateInputToMemories('longAndShortTerm', 'Proj'); const semantic = result[0]!.strategies.find(s => s.type === 'SEMANTIC'); - expect(semantic?.namespaces).toEqual(['/users/{actorId}/facts']); + expect(semantic?.namespaceTemplates).toEqual(['/users/{actorId}/facts']); }); it('uses project name in memory name', () => { diff --git a/src/cli/operations/agent/generate/schema-mapper.ts b/src/cli/operations/agent/generate/schema-mapper.ts index cc7be2a53..523c89a61 100644 --- a/src/cli/operations/agent/generate/schema-mapper.ts +++ b/src/cli/operations/agent/generate/schema-mapper.ts @@ -11,9 +11,9 @@ import type { } from '../../../../schema'; import { DEFAULT_ENTRYPOINT_BY_LANGUAGE, - DEFAULT_EPISODIC_REFLECTION_NAMESPACES, + DEFAULT_EPISODIC_REFLECTION_NAMESPACE_TEMPLATES, DEFAULT_RUNTIME_BY_LANGUAGE, - DEFAULT_STRATEGY_NAMESPACES, + DEFAULT_STRATEGY_NAMESPACE_TEMPLATES, } from '../../../../schema'; import { GatewayPrimitive } from '../../../primitives/GatewayPrimitive'; import { buildAuthorizerConfigFromJwtConfig } from '../../../primitives/auth-utils'; @@ -74,11 +74,11 @@ export function mapGenerateInputToMemories(memory: MemoryOption, projectName: st if (memory === 'longAndShortTerm') { const strategyTypes: MemoryStrategyType[] = ['SEMANTIC', 'USER_PREFERENCE', 'SUMMARIZATION', 'EPISODIC']; for (const type of strategyTypes) { - const defaultNamespaces = DEFAULT_STRATEGY_NAMESPACES[type]; + const defaultTemplates = DEFAULT_STRATEGY_NAMESPACE_TEMPLATES[type]; strategies.push({ type, - ...(defaultNamespaces && { namespaces: defaultNamespaces }), - ...(type === 'EPISODIC' && { reflectionNamespaces: DEFAULT_EPISODIC_REFLECTION_NAMESPACES }), + ...(defaultTemplates && { namespaceTemplates: defaultTemplates }), + ...(type === 'EPISODIC' && { reflectionNamespaceTemplates: DEFAULT_EPISODIC_REFLECTION_NAMESPACE_TEMPLATES }), }); } } diff --git a/src/cli/operations/dev/web-ui/api-types.ts b/src/cli/operations/dev/web-ui/api-types.ts index 8ba57937e..fa6c2c235 100644 --- a/src/cli/operations/dev/web-ui/api-types.ts +++ b/src/cli/operations/dev/web-ui/api-types.ts @@ -145,8 +145,8 @@ export interface ResourceMemory { /** Memory strategy with namespace patterns */ export interface ResourceMemoryStrategy { type: string; - /** Namespace patterns, e.g. "/users/{actorId}/facts", "/summaries/{actorId}/{sessionId}" */ - namespaces: string[]; + /** Namespace templates, e.g. "/users/{actorId}/facts", "/summaries/{actorId}/{sessionId}" */ + namespaceTemplates: string[]; } /** Credential details in the resources response */ diff --git a/src/cli/operations/dev/web-ui/handlers/resources.ts b/src/cli/operations/dev/web-ui/handlers/resources.ts index a9926ab08..47c4e00ef 100644 --- a/src/cli/operations/dev/web-ui/handlers/resources.ts +++ b/src/cli/operations/dev/web-ui/handlers/resources.ts @@ -112,7 +112,7 @@ export async function handleResources(ctx: RouteContext, res: ServerResponse, or name: m.name, strategies: m.strategies.map(s => ({ type: s.type, - namespaces: s.namespaces ?? [], + namespaceTemplates: s.namespaceTemplates ?? s.namespaces ?? [], })), expiryDays: m.eventExpiryDuration, deploymentStatus: statusByTypeAndName.get(`memory:${m.name}`), diff --git a/src/cli/operations/memory/__tests__/create-memory.test.ts b/src/cli/operations/memory/__tests__/create-memory.test.ts index ad9cef2c8..3649e600d 100644 --- a/src/cli/operations/memory/__tests__/create-memory.test.ts +++ b/src/cli/operations/memory/__tests__/create-memory.test.ts @@ -82,7 +82,7 @@ describe('add', () => { expect(addedMemory).toBeDefined(); expect(addedMemory.eventExpiryDuration).toBe(60); expect(addedMemory.strategies[0]!.type).toBe('SEMANTIC'); - expect(addedMemory.strategies[0]!.namespaces).toEqual(['/users/{actorId}/facts']); + expect(addedMemory.strategies[0]!.namespaceTemplates).toEqual(['/users/{actorId}/facts']); }); it('rejects invalid strategy type', async () => { diff --git a/src/cli/primitives/MemoryPrimitive.tsx b/src/cli/primitives/MemoryPrimitive.tsx index 31070bdd2..6f02941bc 100644 --- a/src/cli/primitives/MemoryPrimitive.tsx +++ b/src/cli/primitives/MemoryPrimitive.tsx @@ -8,8 +8,8 @@ import type { StreamDeliveryResources, } from '../../schema'; import { - DEFAULT_EPISODIC_REFLECTION_NAMESPACES, - DEFAULT_STRATEGY_NAMESPACES, + DEFAULT_EPISODIC_REFLECTION_NAMESPACE_TEMPLATES, + DEFAULT_STRATEGY_NAMESPACE_TEMPLATES, MemorySchema, MemoryStrategyTypeSchema, StreamContentLevelSchema, @@ -287,13 +287,13 @@ export class MemoryPrimitive extends BasePrimitive { - const defaultNamespaces = DEFAULT_STRATEGY_NAMESPACES[s.type]; + const defaultTemplates = DEFAULT_STRATEGY_NAMESPACE_TEMPLATES[s.type]; return { type: s.type, - ...(defaultNamespaces && { namespaces: defaultNamespaces }), - ...(s.type === 'EPISODIC' && { reflectionNamespaces: DEFAULT_EPISODIC_REFLECTION_NAMESPACES }), + ...(defaultTemplates && { namespaceTemplates: defaultTemplates }), + ...(s.type === 'EPISODIC' && { reflectionNamespaceTemplates: DEFAULT_EPISODIC_REFLECTION_NAMESPACE_TEMPLATES }), }; }); diff --git a/src/schema/llm-compacted/agentcore.ts b/src/schema/llm-compacted/agentcore.ts index c86c14cb7..112b122ee 100644 --- a/src/schema/llm-compacted/agentcore.ts +++ b/src/schema/llm-compacted/agentcore.ts @@ -103,8 +103,12 @@ interface MemoryStrategy { type: MemoryStrategyType; name?: string; // @regex ^[a-zA-Z][a-zA-Z0-9_]{0,47}$ @max 48 description?: string; + namespaceTemplates?: string[]; + reflectionNamespaceTemplates?: string[]; // EPISODIC only: templates for cross-episode reflections + /** @deprecated Use namespaceTemplates instead. */ namespaces?: string[]; - reflectionNamespaces?: string[]; // EPISODIC only: namespaces for cross-episode reflections + /** @deprecated Use reflectionNamespaceTemplates instead. */ + reflectionNamespaces?: string[]; } // ───────────────────────────────────────────────────────────────────────────── diff --git a/src/schema/schemas/agentcore-project.ts b/src/schema/schemas/agentcore-project.ts index a2482616c..b3f4d3d6f 100644 --- a/src/schema/schemas/agentcore-project.ts +++ b/src/schema/schemas/agentcore-project.ts @@ -20,7 +20,9 @@ import { import { HttpGatewaySchema } from './primitives/http-gateway'; import { DEFAULT_EPISODIC_REFLECTION_NAMESPACES, + DEFAULT_EPISODIC_REFLECTION_NAMESPACE_TEMPLATES, DEFAULT_STRATEGY_NAMESPACES, + DEFAULT_STRATEGY_NAMESPACE_TEMPLATES, MemoryStrategySchema, MemoryStrategyTypeSchema, } from './primitives/memory'; @@ -32,7 +34,9 @@ import { z } from 'zod'; // Re-export for convenience export { + DEFAULT_EPISODIC_REFLECTION_NAMESPACE_TEMPLATES, DEFAULT_EPISODIC_REFLECTION_NAMESPACES, + DEFAULT_STRATEGY_NAMESPACE_TEMPLATES, DEFAULT_STRATEGY_NAMESPACES, MemoryStrategySchema, MemoryStrategyTypeSchema, diff --git a/src/schema/schemas/primitives/__tests__/memory.test.ts b/src/schema/schemas/primitives/__tests__/memory.test.ts index 082c32fa6..22d21d2dc 100644 --- a/src/schema/schemas/primitives/__tests__/memory.test.ts +++ b/src/schema/schemas/primitives/__tests__/memory.test.ts @@ -1,4 +1,11 @@ -import { DEFAULT_STRATEGY_NAMESPACES, MemoryStrategySchema, MemoryStrategyTypeSchema } from '../memory'; +import { + DEFAULT_EPISODIC_REFLECTION_NAMESPACES, + DEFAULT_EPISODIC_REFLECTION_NAMESPACE_TEMPLATES, + DEFAULT_STRATEGY_NAMESPACES, + DEFAULT_STRATEGY_NAMESPACE_TEMPLATES, + MemoryStrategySchema, + MemoryStrategyTypeSchema, +} from '../memory'; import { describe, expect, it } from 'vitest'; describe('MemoryStrategyTypeSchema', () => { @@ -26,11 +33,31 @@ describe('MemoryStrategySchema', () => { type: 'SEMANTIC', name: 'myStrategy', description: 'A description', + namespaceTemplates: ['/users/{actorId}/facts'], + }); + expect(result.success).toBe(true); + }); + + it('accepts deprecated namespaces field as backward-compat alias', () => { + const result = MemoryStrategySchema.safeParse({ + type: 'SEMANTIC', namespaces: ['/users/{actorId}/facts'], }); expect(result.success).toBe(true); }); + it('rejects strategy specifying both namespaces and namespaceTemplates', () => { + const result = MemoryStrategySchema.safeParse({ + type: 'SEMANTIC', + namespaces: ['/users/{actorId}/facts'], + namespaceTemplates: ['/users/{actorId}/facts'], + }); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0]?.message).toContain('mutually exclusive'); + } + }); + it('rejects strategy with CUSTOM type', () => { const result = MemoryStrategySchema.safeParse({ type: 'CUSTOM' }); expect(result.success).toBe(false); @@ -46,7 +73,16 @@ describe('MemoryStrategySchema', () => { expect(result.success).toBe(false); }); - it('accepts EPISODIC strategy with reflectionNamespaces', () => { + it('accepts EPISODIC strategy with reflectionNamespaceTemplates', () => { + const result = MemoryStrategySchema.safeParse({ + type: 'EPISODIC', + namespaceTemplates: ['/episodes/{actorId}/{sessionId}'], + reflectionNamespaceTemplates: ['/episodes/{actorId}'], + }); + expect(result.success).toBe(true); + }); + + it('accepts EPISODIC strategy with deprecated reflectionNamespaces alias', () => { const result = MemoryStrategySchema.safeParse({ type: 'EPISODIC', namespaces: ['/episodes/{actorId}/{sessionId}'], @@ -55,74 +91,104 @@ describe('MemoryStrategySchema', () => { expect(result.success).toBe(true); }); - it('rejects EPISODIC strategy without reflectionNamespaces', () => { + it('rejects EPISODIC strategy specifying both reflectionNamespaces and reflectionNamespaceTemplates', () => { const result = MemoryStrategySchema.safeParse({ type: 'EPISODIC', - namespaces: ['/episodes/{actorId}/{sessionId}'], + namespaceTemplates: ['/episodes/{actorId}/{sessionId}'], + reflectionNamespaces: ['/episodes/{actorId}'], + reflectionNamespaceTemplates: ['/episodes/{actorId}'], }); expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues.some(i => i.message.includes('mutually exclusive'))).toBe(true); + } }); - it('rejects EPISODIC strategy with empty reflectionNamespaces', () => { + it('rejects EPISODIC strategy without reflectionNamespaceTemplates', () => { const result = MemoryStrategySchema.safeParse({ type: 'EPISODIC', - namespaces: ['/episodes/{actorId}/{sessionId}'], - reflectionNamespaces: [], + namespaceTemplates: ['/episodes/{actorId}/{sessionId}'], }); expect(result.success).toBe(false); }); - it('allows non-EPISODIC strategies without reflectionNamespaces', () => { + it('rejects EPISODIC strategy with empty reflectionNamespaceTemplates', () => { + const result = MemoryStrategySchema.safeParse({ + type: 'EPISODIC', + namespaceTemplates: ['/episodes/{actorId}/{sessionId}'], + reflectionNamespaceTemplates: [], + }); + expect(result.success).toBe(false); + }); + + it('allows non-EPISODIC strategies without reflectionNamespaceTemplates', () => { const result = MemoryStrategySchema.safeParse({ type: 'SEMANTIC' }); expect(result.success).toBe(true); }); - it('rejects EPISODIC when reflectionNamespaces is not a prefix of namespaces', () => { + it('rejects EPISODIC when reflectionNamespaceTemplates is not a prefix of namespaceTemplates', () => { const result = MemoryStrategySchema.safeParse({ type: 'EPISODIC', - namespaces: ['/episodes/{actorId}/{sessionId}'], - reflectionNamespaces: ['/reflections/{actorId}'], + namespaceTemplates: ['/episodes/{actorId}/{sessionId}'], + reflectionNamespaceTemplates: ['/reflections/{actorId}'], }); expect(result.success).toBe(false); }); - it('accepts EPISODIC when reflectionNamespaces is a prefix of namespaces', () => { + it('accepts EPISODIC when reflectionNamespaceTemplates is a prefix of namespaceTemplates', () => { const result = MemoryStrategySchema.safeParse({ type: 'EPISODIC', - namespaces: ['/episodes/{actorId}/{sessionId}'], - reflectionNamespaces: ['/episodes/{actorId}'], + namespaceTemplates: ['/episodes/{actorId}/{sessionId}'], + reflectionNamespaceTemplates: ['/episodes/{actorId}'], }); expect(result.success).toBe(true); }); - it('accepts EPISODIC when reflectionNamespaces equals namespaces', () => { + it('accepts EPISODIC when reflectionNamespaceTemplates equals namespaceTemplates', () => { const result = MemoryStrategySchema.safeParse({ type: 'EPISODIC', - namespaces: ['/episodes/{actorId}/{sessionId}'], - reflectionNamespaces: ['/episodes/{actorId}/{sessionId}'], + namespaceTemplates: ['/episodes/{actorId}/{sessionId}'], + reflectionNamespaceTemplates: ['/episodes/{actorId}/{sessionId}'], }); expect(result.success).toBe(true); }); + + it('evaluates prefix refinement using deprecated aliases when only they are provided', () => { + const result = MemoryStrategySchema.safeParse({ + type: 'EPISODIC', + namespaces: ['/episodes/{actorId}/{sessionId}'], + reflectionNamespaces: ['/reflections/{actorId}'], + }); + expect(result.success).toBe(false); + }); }); -describe('DEFAULT_STRATEGY_NAMESPACES', () => { - it('has default namespaces for SEMANTIC', () => { - expect(DEFAULT_STRATEGY_NAMESPACES.SEMANTIC).toEqual(['/users/{actorId}/facts']); +describe('DEFAULT_STRATEGY_NAMESPACE_TEMPLATES', () => { + it('has default templates for SEMANTIC', () => { + expect(DEFAULT_STRATEGY_NAMESPACE_TEMPLATES.SEMANTIC).toEqual(['/users/{actorId}/facts']); + }); + + it('has default templates for USER_PREFERENCE', () => { + expect(DEFAULT_STRATEGY_NAMESPACE_TEMPLATES.USER_PREFERENCE).toEqual(['/users/{actorId}/preferences']); + }); + + it('has default templates for SUMMARIZATION', () => { + expect(DEFAULT_STRATEGY_NAMESPACE_TEMPLATES.SUMMARIZATION).toEqual(['/summaries/{actorId}/{sessionId}']); }); - it('has default namespaces for USER_PREFERENCE', () => { - expect(DEFAULT_STRATEGY_NAMESPACES.USER_PREFERENCE).toEqual(['/users/{actorId}/preferences']); + it('has default templates for EPISODIC', () => { + expect(DEFAULT_STRATEGY_NAMESPACE_TEMPLATES.EPISODIC).toEqual(['/episodes/{actorId}/{sessionId}']); }); - it('has default namespaces for SUMMARIZATION', () => { - expect(DEFAULT_STRATEGY_NAMESPACES.SUMMARIZATION).toEqual(['/summaries/{actorId}/{sessionId}']); + it('does not have default templates for CUSTOM (removed)', () => { + expect(DEFAULT_STRATEGY_NAMESPACE_TEMPLATES).not.toHaveProperty('CUSTOM'); }); - it('has default namespaces for EPISODIC', () => { - expect(DEFAULT_STRATEGY_NAMESPACES.EPISODIC).toEqual(['/episodes/{actorId}/{sessionId}']); + it('deprecated alias DEFAULT_STRATEGY_NAMESPACES points to the same object', () => { + expect(DEFAULT_STRATEGY_NAMESPACES).toBe(DEFAULT_STRATEGY_NAMESPACE_TEMPLATES); }); - it('does not have default namespaces for CUSTOM (removed)', () => { - expect(DEFAULT_STRATEGY_NAMESPACES).not.toHaveProperty('CUSTOM'); + it('deprecated alias DEFAULT_EPISODIC_REFLECTION_NAMESPACES points to the same object', () => { + expect(DEFAULT_EPISODIC_REFLECTION_NAMESPACES).toBe(DEFAULT_EPISODIC_REFLECTION_NAMESPACE_TEMPLATES); }); }); diff --git a/src/schema/schemas/primitives/index.ts b/src/schema/schemas/primitives/index.ts index a48985c84..38967a181 100644 --- a/src/schema/schemas/primitives/index.ts +++ b/src/schema/schemas/primitives/index.ts @@ -21,7 +21,9 @@ export { export type { MemoryStrategy, MemoryStrategyType } from './memory'; export { + DEFAULT_EPISODIC_REFLECTION_NAMESPACE_TEMPLATES, DEFAULT_EPISODIC_REFLECTION_NAMESPACES, + DEFAULT_STRATEGY_NAMESPACE_TEMPLATES, DEFAULT_STRATEGY_NAMESPACES, MemoryStrategyNameSchema, MemoryStrategySchema, diff --git a/src/schema/schemas/primitives/memory.ts b/src/schema/schemas/primitives/memory.ts index f63874d5d..5e5a633cb 100644 --- a/src/schema/schemas/primitives/memory.ts +++ b/src/schema/schemas/primitives/memory.ts @@ -17,10 +17,10 @@ export const MemoryStrategyTypeSchema = z.enum(['SEMANTIC', 'SUMMARIZATION', 'US export type MemoryStrategyType = z.infer; /** - * Default namespaces for each memory strategy type. + * Default namespace templates for each memory strategy type. * These match the patterns generated in CLI session.py templates. */ -export const DEFAULT_STRATEGY_NAMESPACES: Partial> = { +export const DEFAULT_STRATEGY_NAMESPACE_TEMPLATES: Partial> = { SEMANTIC: ['/users/{actorId}/facts'], USER_PREFERENCE: ['/users/{actorId}/preferences'], SUMMARIZATION: ['/summaries/{actorId}/{sessionId}'], @@ -28,10 +28,22 @@ export const DEFAULT_STRATEGY_NAMESPACES: Partial !((strategy.namespaces?.length ?? 0) > 0 && (strategy.namespaceTemplates?.length ?? 0) > 0), { + message: + "'namespaces' and 'namespaceTemplates' are mutually exclusive. Prefer 'namespaceTemplates' ('namespaces' is deprecated).", + path: ['namespaceTemplates'], + }) + .refine( + strategy => + !((strategy.reflectionNamespaces?.length ?? 0) > 0 && (strategy.reflectionNamespaceTemplates?.length ?? 0) > 0), + { + message: + "'reflectionNamespaces' and 'reflectionNamespaceTemplates' are mutually exclusive. Prefer 'reflectionNamespaceTemplates' ('reflectionNamespaces' is deprecated).", + path: ['reflectionNamespaceTemplates'], + } + ) .refine( strategy => - strategy.type !== 'EPISODIC' || - (strategy.reflectionNamespaces !== undefined && strategy.reflectionNamespaces.length > 0), + strategy.type === 'EPISODIC' || + (strategy.reflectionNamespaceTemplates === undefined && strategy.reflectionNamespaces === undefined), + { + message: "'reflectionNamespaceTemplates' is only allowed on EPISODIC strategies", + path: ['reflectionNamespaceTemplates'], + } + ) + .refine( + strategy => { + if (strategy.type !== 'EPISODIC') return true; + const reflection = strategy.reflectionNamespaceTemplates ?? strategy.reflectionNamespaces; + return reflection !== undefined && reflection.length > 0; + }, { - message: 'EPISODIC strategy requires reflectionNamespaces', - path: ['reflectionNamespaces'], + message: 'EPISODIC strategy requires reflectionNamespaceTemplates', + path: ['reflectionNamespaceTemplates'], } ) .refine( strategy => { - if (strategy.type !== 'EPISODIC' || !strategy.reflectionNamespaces || !strategy.namespaces) return true; - return strategy.reflectionNamespaces.every(ref => strategy.namespaces!.some(ns => ns.startsWith(ref))); + if (strategy.type !== 'EPISODIC') return true; + const reflection = strategy.reflectionNamespaceTemplates ?? strategy.reflectionNamespaces; + const templates = strategy.namespaceTemplates ?? strategy.namespaces; + if (!reflection || !templates) return true; + return reflection.every(ref => templates.some(ns => ns.startsWith(ref))); }, { - message: 'Each reflectionNamespace must be a prefix of at least one namespace', - path: ['reflectionNamespaces'], + message: 'Each reflectionNamespaceTemplate must be a prefix of at least one namespaceTemplate', + path: ['reflectionNamespaceTemplates'], } );