diff --git a/src/schema/llm-compacted/agentcore.ts b/src/schema/llm-compacted/agentcore.ts index d897d506..1ce0a120 100644 --- a/src/schema/llm-compacted/agentcore.ts +++ b/src/schema/llm-compacted/agentcore.ts @@ -82,6 +82,11 @@ interface MemoryStrategy { description?: string; namespaces?: string[]; reflectionNamespaces?: string[]; // EPISODIC only: namespaces for cross-episode reflections + semanticOverride?: { + // Only valid when type is 'SEMANTIC' + extraction?: { appendToPrompt: string; modelId: string }; // @min 1 for both, @max 30000 for appendToPrompt + consolidation?: { appendToPrompt: string; modelId: string }; // At least one of extraction/consolidation required + }; } // ───────────────────────────────────────────────────────────────────────────── diff --git a/src/schema/schemas/__tests__/agentcore-project.test.ts b/src/schema/schemas/__tests__/agentcore-project.test.ts index 64aa5ea9..997983b7 100644 --- a/src/schema/schemas/__tests__/agentcore-project.test.ts +++ b/src/schema/schemas/__tests__/agentcore-project.test.ts @@ -452,4 +452,56 @@ describe('AgentCoreProjectSpecSchema', () => { }); expect(result.success).toBe(false); }); + + it('accepts memory with semanticOverride on SEMANTIC strategy', () => { + const result = AgentCoreProjectSpecSchema.safeParse({ + ...minimalProject, + memories: [ + { + type: 'AgentCoreMemory', + name: 'TestMemory', + eventExpiryDuration: 30, + strategies: [ + { + type: 'SEMANTIC', + namespaces: ['/users/{actorId}/facts'], + semanticOverride: { + extraction: { + appendToPrompt: 'Extract key facts', + modelId: 'anthropic.claude-3-sonnet-20240229-v1:0', + }, + }, + }, + ], + }, + ], + }); + expect(result.success).toBe(true); + }); + + it('rejects memory with semanticOverride on SUMMARIZATION strategy', () => { + const result = AgentCoreProjectSpecSchema.safeParse({ + ...minimalProject, + memories: [ + { + type: 'AgentCoreMemory', + name: 'TestMemory', + eventExpiryDuration: 30, + strategies: [ + { + type: 'SUMMARIZATION', + namespaces: ['/summaries/{actorId}/{sessionId}'], + semanticOverride: { + extraction: { + appendToPrompt: 'test', + modelId: 'model-1', + }, + }, + }, + ], + }, + ], + }); + expect(result.success).toBe(false); + }); }); diff --git a/src/schema/schemas/agentcore-project.ts b/src/schema/schemas/agentcore-project.ts index d19dfaec..fe401736 100644 --- a/src/schema/schemas/agentcore-project.ts +++ b/src/schema/schemas/agentcore-project.ts @@ -30,7 +30,13 @@ export { MemoryStrategyTypeSchema, }; export { EvaluationLevelSchema }; -export type { MemoryStrategy, MemoryStrategyType } from './primitives/memory'; +export type { + MemoryStrategy, + MemoryStrategyType, + SemanticOverride, + SemanticExtractionOverride, + SemanticConsolidationOverride, +} from './primitives/memory'; export type { OnlineEvalConfig } from './primitives/online-eval-config'; export { OnlineEvalConfigSchema, OnlineEvalConfigNameSchema } from './primitives/online-eval-config'; export type { EvaluationLevel, EvaluatorConfig, LlmAsAJudgeConfig, RatingScale } from './primitives/evaluator'; diff --git a/src/schema/schemas/primitives/__tests__/memory.test.ts b/src/schema/schemas/primitives/__tests__/memory.test.ts index 4b37eb64..a26d0928 100644 --- a/src/schema/schemas/primitives/__tests__/memory.test.ts +++ b/src/schema/schemas/primitives/__tests__/memory.test.ts @@ -1,4 +1,9 @@ -import { DEFAULT_STRATEGY_NAMESPACES, MemoryStrategySchema, MemoryStrategyTypeSchema } from '../memory'; +import { + DEFAULT_STRATEGY_NAMESPACES, + MemoryStrategySchema, + MemoryStrategyTypeSchema, + SemanticOverrideSchema, +} from '../memory'; import { describe, expect, it } from 'vitest'; describe('MemoryStrategyTypeSchema', () => { @@ -153,3 +158,115 @@ describe('DEFAULT_STRATEGY_NAMESPACES', () => { expect(DEFAULT_STRATEGY_NAMESPACES).not.toHaveProperty('CUSTOM'); }); }); + +describe('SemanticOverrideSchema', () => { + it('accepts extraction-only override', () => { + const result = SemanticOverrideSchema.safeParse({ + extraction: { appendToPrompt: 'Extract key facts', modelId: 'anthropic.claude-3-sonnet-20240229-v1:0' }, + }); + expect(result.success).toBe(true); + }); + + it('accepts consolidation-only override', () => { + const result = SemanticOverrideSchema.safeParse({ + consolidation: { appendToPrompt: 'Consolidate memories', modelId: 'anthropic.claude-3-sonnet-20240229-v1:0' }, + }); + expect(result.success).toBe(true); + }); + + it('accepts both extraction and consolidation', () => { + const result = SemanticOverrideSchema.safeParse({ + extraction: { appendToPrompt: 'Extract', modelId: 'model-1' }, + consolidation: { appendToPrompt: 'Consolidate', modelId: 'model-2' }, + }); + expect(result.success).toBe(true); + }); + + it('rejects empty override (at least one required)', () => { + const result = SemanticOverrideSchema.safeParse({}); + expect(result.success).toBe(false); + }); + + it('rejects extraction with empty appendToPrompt', () => { + const result = SemanticOverrideSchema.safeParse({ + extraction: { appendToPrompt: '', modelId: 'model-1' }, + }); + expect(result.success).toBe(false); + }); + + it('rejects extraction with missing modelId', () => { + const result = SemanticOverrideSchema.safeParse({ + extraction: { appendToPrompt: 'test' }, + }); + expect(result.success).toBe(false); + }); +}); + +describe('MemoryStrategySchema with semanticOverride', () => { + it('accepts SEMANTIC strategy with extraction override', () => { + const result = MemoryStrategySchema.safeParse({ + type: 'SEMANTIC', + semanticOverride: { + extraction: { appendToPrompt: 'Extract key facts', modelId: 'anthropic.claude-3-sonnet-20240229-v1:0' }, + }, + }); + expect(result.success).toBe(true); + }); + + it('accepts SEMANTIC strategy with both overrides', () => { + const result = MemoryStrategySchema.safeParse({ + type: 'SEMANTIC', + semanticOverride: { + extraction: { appendToPrompt: 'Extract', modelId: 'model-1' }, + consolidation: { appendToPrompt: 'Consolidate', modelId: 'model-2' }, + }, + }); + expect(result.success).toBe(true); + }); + + it('accepts SEMANTIC strategy without override (backward compat)', () => { + const result = MemoryStrategySchema.safeParse({ type: 'SEMANTIC' }); + expect(result.success).toBe(true); + }); + + it('rejects semanticOverride on SUMMARIZATION strategy', () => { + const result = MemoryStrategySchema.safeParse({ + type: 'SUMMARIZATION', + semanticOverride: { + extraction: { appendToPrompt: 'test', modelId: 'model-1' }, + }, + }); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues.some(i => i.message.includes('SEMANTIC'))).toBe(true); + } + }); + + it('rejects semanticOverride on USER_PREFERENCE strategy', () => { + const result = MemoryStrategySchema.safeParse({ + type: 'USER_PREFERENCE', + semanticOverride: { + extraction: { appendToPrompt: 'test', modelId: 'model-1' }, + }, + }); + expect(result.success).toBe(false); + }); + + it('rejects consolidation-only semanticOverride on USER_PREFERENCE strategy', () => { + const result = MemoryStrategySchema.safeParse({ + type: 'USER_PREFERENCE', + semanticOverride: { + consolidation: { appendToPrompt: 'test', modelId: 'model-1' }, + }, + }); + expect(result.success).toBe(false); + }); + + it('rejects SEMANTIC strategy with empty semanticOverride', () => { + const result = MemoryStrategySchema.safeParse({ + type: 'SEMANTIC', + semanticOverride: {}, + }); + expect(result.success).toBe(false); + }); +}); diff --git a/src/schema/schemas/primitives/index.ts b/src/schema/schemas/primitives/index.ts index 0549a2ce..fb63f728 100644 --- a/src/schema/schemas/primitives/index.ts +++ b/src/schema/schemas/primitives/index.ts @@ -1,10 +1,19 @@ -export type { MemoryStrategy, MemoryStrategyType } from './memory'; +export type { + MemoryStrategy, + MemoryStrategyType, + SemanticOverride, + SemanticExtractionOverride, + SemanticConsolidationOverride, +} from './memory'; export { DEFAULT_EPISODIC_REFLECTION_NAMESPACES, DEFAULT_STRATEGY_NAMESPACES, MemoryStrategyNameSchema, MemoryStrategySchema, MemoryStrategyTypeSchema, + SemanticOverrideSchema, + SemanticExtractionOverrideSchema, + SemanticConsolidationOverrideSchema, } from './memory'; export type { diff --git a/src/schema/schemas/primitives/memory.ts b/src/schema/schemas/primitives/memory.ts index f63874d5..29b7a7f3 100644 --- a/src/schema/schemas/primitives/memory.ts +++ b/src/schema/schemas/primitives/memory.ts @@ -47,6 +47,54 @@ export const MemoryStrategyNameSchema = z 'Must begin with a letter and contain only alphanumeric characters and underscores (max 48 chars)' ); +// ============================================================================ +// Semantic Override Types (CloudFormation SemanticOverride) +// ============================================================================ + +/** + * Configuration for overriding semantic memory extraction behavior. + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-properties-bedrockagentcore-memory-semanticoverrideextractionconfigurationinput.html + */ +export const SemanticExtractionOverrideSchema = z.object({ + /** Custom prompt to append for memory extraction */ + appendToPrompt: z.string().min(1).max(30000), + /** Bedrock model ID to use for extraction */ + modelId: z.string().min(1), +}); + +export type SemanticExtractionOverride = z.infer; + +/** + * Configuration for overriding semantic memory consolidation behavior. + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-properties-bedrockagentcore-memory-semanticoverrideconsolidationconfigurationinput.html + */ +export const SemanticConsolidationOverrideSchema = z.object({ + /** Custom prompt to append for memory consolidation */ + appendToPrompt: z.string().min(1).max(30000), + /** Bedrock model ID to use for consolidation */ + modelId: z.string().min(1), +}); + +export type SemanticConsolidationOverride = z.infer; + +/** + * Override configuration for semantic memory strategy. + * At least one of extraction or consolidation must be provided. + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-properties-bedrockagentcore-memory-semanticoverride.html + */ +export const SemanticOverrideSchema = z + .object({ + /** Override extraction behavior (custom prompt + model) */ + extraction: SemanticExtractionOverrideSchema.optional(), + /** Override consolidation behavior (custom prompt + model) */ + consolidation: SemanticConsolidationOverrideSchema.optional(), + }) + .refine(data => data.extraction !== undefined || data.consolidation !== undefined, { + message: 'At least one of extraction or consolidation must be provided', + }); + +export type SemanticOverride = z.infer; + /** * Memory strategy configuration. * Each memory can have multiple strategies with optional namespace scoping. @@ -63,6 +111,8 @@ export const MemoryStrategySchema = z namespaces: z.array(z.string()).optional(), /** Reflection namespaces for EPISODIC strategy. Required by the service for episodic strategies. */ reflectionNamespaces: z.array(z.string()).optional(), + /** Only valid when type is 'SEMANTIC'. Override extraction and/or consolidation behavior. */ + semanticOverride: SemanticOverrideSchema.optional(), }) .refine( strategy => @@ -82,6 +132,10 @@ export const MemoryStrategySchema = z message: 'Each reflectionNamespace must be a prefix of at least one namespace', path: ['reflectionNamespaces'], } - ); + ) + .refine(strategy => strategy.semanticOverride === undefined || strategy.type === 'SEMANTIC', { + message: 'semanticOverride is only valid for SEMANTIC strategy type', + path: ['semanticOverride'], + }); export type MemoryStrategy = z.infer;