From a9a683dbe251d69ea628e0689594b3e83bee7532 Mon Sep 17 00:00:00 2001 From: Nicolas Borges Date: Mon, 4 May 2026 11:12:01 -0400 Subject: [PATCH] chore: update CP semantics to expect redesigned namespaces field --- docs/configuration.md | 2 +- docs/memory.md | 18 +-- src/cli/aws/agentcore-control.ts | 14 +- .../commands/add/__tests__/add-memory.test.ts | 16 +-- .../commands/create/__tests__/create.test.ts | 10 +- src/cli/commands/import/import-memory.ts | 10 +- .../generate/__tests__/schema-mapper.test.ts | 4 +- .../agent/generate/schema-mapper.ts | 11 +- src/cli/operations/dev/web-ui/api-types.ts | 4 +- .../dev/web-ui/handlers/resources.ts | 2 +- .../memory/__tests__/create-memory.test.ts | 2 +- src/cli/primitives/MemoryPrimitive.tsx | 12 +- src/schema/llm-compacted/agentcore.ts | 6 +- src/schema/schemas/agentcore-project.ts | 4 + .../primitives/__tests__/memory.test.ts | 122 ++++++++++++++---- src/schema/schemas/primitives/index.ts | 2 + src/schema/schemas/primitives/memory.ts | 72 ++++++++--- 17 files changed, 219 insertions(+), 92 deletions(-) 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/src/cli/aws/agentcore-control.ts b/src/cli/aws/agentcore-control.ts index 162c2b3a6..4b4f877fc 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 2362d1353..f37ffd39e 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 3ed449236..6047b959a 100644 --- a/src/cli/operations/agent/generate/schema-mapper.ts +++ b/src/cli/operations/agent/generate/schema-mapper.ts @@ -9,7 +9,10 @@ import type { MemoryStrategyType, ModelProvider, } from '../../../../schema'; -import { DEFAULT_EPISODIC_REFLECTION_NAMESPACES, DEFAULT_STRATEGY_NAMESPACES } from '../../../../schema'; +import { + DEFAULT_EPISODIC_REFLECTION_NAMESPACE_TEMPLATES, + DEFAULT_STRATEGY_NAMESPACE_TEMPLATES, +} from '../../../../schema'; import { GatewayPrimitive } from '../../../primitives/GatewayPrimitive'; import { buildAuthorizerConfigFromJwtConfig } from '../../../primitives/auth-utils'; import { @@ -69,11 +72,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 a0b8077c4..d2d1b3914 100644 --- a/src/cli/operations/memory/__tests__/create-memory.test.ts +++ b/src/cli/operations/memory/__tests__/create-memory.test.ts @@ -73,7 +73,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 a0c15c2c3..165b54dcc 100644 --- a/src/cli/primitives/MemoryPrimitive.tsx +++ b/src/cli/primitives/MemoryPrimitive.tsx @@ -7,8 +7,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 10d164a2c..f3f670d09 100644 --- a/src/schema/schemas/agentcore-project.ts +++ b/src/schema/schemas/agentcore-project.ts @@ -15,7 +15,9 @@ import { EvaluationLevelSchema, EvaluatorConfigSchema, EvaluatorNameSchema } fro 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'; @@ -27,7 +29,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 4b37eb646..e350abe6b 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', () => { @@ -53,11 +60,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); @@ -73,7 +100,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}'], @@ -82,74 +118,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..8381872f4 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 !== undefined && strategy.namespaceTemplates !== undefined), { + message: + "'namespaces' and 'namespaceTemplates' are mutually exclusive. Prefer 'namespaceTemplates' ('namespaces' is deprecated).", + path: ['namespaceTemplates'], + }) .refine( - strategy => - strategy.type !== 'EPISODIC' || - (strategy.reflectionNamespaces !== undefined && strategy.reflectionNamespaces.length > 0), + strategy => !(strategy.reflectionNamespaces !== undefined && strategy.reflectionNamespaceTemplates !== undefined), + { + message: + "'reflectionNamespaces' and 'reflectionNamespaceTemplates' are mutually exclusive. Prefer 'reflectionNamespaceTemplates' ('reflectionNamespaces' is deprecated).", + 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'], } );