""" Memory Manager Facade for the Agent Memory System providing unified access to all memory types and operations. """ import logging from typing import Any from uuid import UUID from .config import MemorySettings, get_memory_settings from .types import ( Episode, EpisodeCreate, Fact, FactCreate, MemoryStats, MemoryType, Outcome, Procedure, ProcedureCreate, RetrievalResult, ScopeContext, ScopeLevel, TaskState, ) logger = logging.getLogger(__name__) class MemoryManager: """ Unified facade for the Agent Memory System. Provides a single entry point for all memory operations across working, episodic, semantic, and procedural memory types. Usage: manager = MemoryManager.create() # Working memory await manager.set_working("key", {"data": "value"}) value = await manager.get_working("key") # Episodic memory episode = await manager.record_episode(episode_data) similar = await manager.search_episodes("query") # Semantic memory fact = await manager.store_fact(fact_data) facts = await manager.search_facts("query") # Procedural memory procedure = await manager.record_procedure(procedure_data) procedures = await manager.find_procedures("context") """ def __init__( self, settings: MemorySettings, scope: ScopeContext, ) -> None: """ Initialize the MemoryManager. Args: settings: Memory configuration settings scope: The scope context for this manager instance """ self._settings = settings self._scope = scope self._initialized = False # These will be initialized when the respective sub-modules are implemented self._working_memory: Any | None = None self._episodic_memory: Any | None = None self._semantic_memory: Any | None = None self._procedural_memory: Any | None = None logger.debug( "MemoryManager created for scope %s:%s", scope.scope_type.value, scope.scope_id, ) @classmethod def create( cls, scope_type: ScopeLevel = ScopeLevel.SESSION, scope_id: str = "default", parent_scope: ScopeContext | None = None, settings: MemorySettings | None = None, ) -> "MemoryManager": """ Create a new MemoryManager instance. Args: scope_type: The scope level for this manager scope_id: The scope identifier parent_scope: Optional parent scope for inheritance settings: Optional custom settings (uses global if not provided) Returns: A new MemoryManager instance """ if settings is None: settings = get_memory_settings() scope = ScopeContext( scope_type=scope_type, scope_id=scope_id, parent=parent_scope, ) return cls(settings=settings, scope=scope) @classmethod def for_session( cls, session_id: str, agent_instance_id: UUID | None = None, project_id: UUID | None = None, ) -> "MemoryManager": """ Create a MemoryManager for a specific session. Builds the appropriate scope hierarchy based on provided IDs. Args: session_id: The session identifier agent_instance_id: Optional agent instance ID project_id: Optional project ID Returns: A MemoryManager configured for the session scope """ settings = get_memory_settings() # Build scope hierarchy parent: ScopeContext | None = None if project_id: parent = ScopeContext( scope_type=ScopeLevel.PROJECT, scope_id=str(project_id), parent=ScopeContext( scope_type=ScopeLevel.GLOBAL, scope_id="global", ), ) if agent_instance_id: parent = ScopeContext( scope_type=ScopeLevel.AGENT_INSTANCE, scope_id=str(agent_instance_id), parent=parent, ) scope = ScopeContext( scope_type=ScopeLevel.SESSION, scope_id=session_id, parent=parent, ) return cls(settings=settings, scope=scope) @property def scope(self) -> ScopeContext: """Get the current scope context.""" return self._scope @property def settings(self) -> MemorySettings: """Get the memory settings.""" return self._settings # ========================================================================= # Working Memory Operations # ========================================================================= async def set_working( self, key: str, value: Any, ttl_seconds: int | None = None, ) -> None: """ Set a value in working memory. Args: key: The key to store the value under value: The value to store (must be JSON serializable) ttl_seconds: Optional TTL (uses default if not provided) """ # Placeholder - will be implemented in #89 logger.debug("set_working called for key=%s (not yet implemented)", key) raise NotImplementedError("Working memory not yet implemented") async def get_working( self, key: str, default: Any = None, ) -> Any: """ Get a value from working memory. Args: key: The key to retrieve default: Default value if key not found Returns: The stored value or default """ # Placeholder - will be implemented in #89 logger.debug("get_working called for key=%s (not yet implemented)", key) raise NotImplementedError("Working memory not yet implemented") async def delete_working(self, key: str) -> bool: """ Delete a value from working memory. Args: key: The key to delete Returns: True if the key was deleted, False if not found """ # Placeholder - will be implemented in #89 logger.debug("delete_working called for key=%s (not yet implemented)", key) raise NotImplementedError("Working memory not yet implemented") async def set_task_state(self, state: TaskState) -> None: """ Set the current task state in working memory. Args: state: The task state to store """ # Placeholder - will be implemented in #89 logger.debug( "set_task_state called for task=%s (not yet implemented)", state.task_id, ) raise NotImplementedError("Working memory not yet implemented") async def get_task_state(self) -> TaskState | None: """ Get the current task state from working memory. Returns: The current task state or None """ # Placeholder - will be implemented in #89 logger.debug("get_task_state called (not yet implemented)") raise NotImplementedError("Working memory not yet implemented") async def create_checkpoint(self) -> str: """ Create a checkpoint of the current working memory state. Returns: The checkpoint ID """ # Placeholder - will be implemented in #89 logger.debug("create_checkpoint called (not yet implemented)") raise NotImplementedError("Working memory not yet implemented") async def restore_checkpoint(self, checkpoint_id: str) -> None: """ Restore working memory from a checkpoint. Args: checkpoint_id: The checkpoint to restore from """ # Placeholder - will be implemented in #89 logger.debug( "restore_checkpoint called for id=%s (not yet implemented)", checkpoint_id, ) raise NotImplementedError("Working memory not yet implemented") # ========================================================================= # Episodic Memory Operations # ========================================================================= async def record_episode(self, episode: EpisodeCreate) -> Episode: """ Record a new episode in episodic memory. Args: episode: The episode data to record Returns: The created episode with ID """ # Placeholder - will be implemented in #90 logger.debug( "record_episode called for task=%s (not yet implemented)", episode.task_type, ) raise NotImplementedError("Episodic memory not yet implemented") async def search_episodes( self, query: str, limit: int | None = None, ) -> RetrievalResult[Episode]: """ Search for similar episodes. Args: query: The search query limit: Maximum results to return Returns: Retrieval result with matching episodes """ # Placeholder - will be implemented in #90 logger.debug( "search_episodes called for query=%s (not yet implemented)", query[:50], ) raise NotImplementedError("Episodic memory not yet implemented") async def get_recent_episodes( self, limit: int = 10, ) -> list[Episode]: """ Get the most recent episodes. Args: limit: Maximum episodes to return Returns: List of recent episodes """ # Placeholder - will be implemented in #90 logger.debug("get_recent_episodes called (not yet implemented)") raise NotImplementedError("Episodic memory not yet implemented") async def get_episodes_by_outcome( self, outcome: Outcome, limit: int = 10, ) -> list[Episode]: """ Get episodes by outcome. Args: outcome: The outcome to filter by limit: Maximum episodes to return Returns: List of episodes with the specified outcome """ # Placeholder - will be implemented in #90 logger.debug( "get_episodes_by_outcome called for outcome=%s (not yet implemented)", outcome.value, ) raise NotImplementedError("Episodic memory not yet implemented") # ========================================================================= # Semantic Memory Operations # ========================================================================= async def store_fact(self, fact: FactCreate) -> Fact: """ Store a new fact in semantic memory. Args: fact: The fact data to store Returns: The created fact with ID """ # Placeholder - will be implemented in #91 logger.debug( "store_fact called for %s %s %s (not yet implemented)", fact.subject, fact.predicate, fact.object, ) raise NotImplementedError("Semantic memory not yet implemented") async def search_facts( self, query: str, limit: int | None = None, ) -> RetrievalResult[Fact]: """ Search for facts matching a query. Args: query: The search query limit: Maximum results to return Returns: Retrieval result with matching facts """ # Placeholder - will be implemented in #91 logger.debug( "search_facts called for query=%s (not yet implemented)", query[:50], ) raise NotImplementedError("Semantic memory not yet implemented") async def get_facts_by_entity( self, entity: str, limit: int = 20, ) -> list[Fact]: """ Get facts related to an entity. Args: entity: The entity to search for limit: Maximum facts to return Returns: List of facts mentioning the entity """ # Placeholder - will be implemented in #91 logger.debug( "get_facts_by_entity called for entity=%s (not yet implemented)", entity, ) raise NotImplementedError("Semantic memory not yet implemented") async def reinforce_fact(self, fact_id: UUID) -> Fact: """ Reinforce a fact (increase confidence from repeated learning). Args: fact_id: The fact to reinforce Returns: The updated fact """ # Placeholder - will be implemented in #91 logger.debug( "reinforce_fact called for id=%s (not yet implemented)", fact_id, ) raise NotImplementedError("Semantic memory not yet implemented") # ========================================================================= # Procedural Memory Operations # ========================================================================= async def record_procedure(self, procedure: ProcedureCreate) -> Procedure: """ Record a new procedure. Args: procedure: The procedure data to record Returns: The created procedure with ID """ # Placeholder - will be implemented in #92 logger.debug( "record_procedure called for name=%s (not yet implemented)", procedure.name, ) raise NotImplementedError("Procedural memory not yet implemented") async def find_procedures( self, context: str, limit: int = 5, ) -> list[Procedure]: """ Find procedures matching the current context. Args: context: The context to match against limit: Maximum procedures to return Returns: List of matching procedures sorted by success rate """ # Placeholder - will be implemented in #92 logger.debug( "find_procedures called for context=%s (not yet implemented)", context[:50], ) raise NotImplementedError("Procedural memory not yet implemented") async def record_procedure_outcome( self, procedure_id: UUID, success: bool, ) -> None: """ Record the outcome of using a procedure. Args: procedure_id: The procedure that was used success: Whether the procedure succeeded """ # Placeholder - will be implemented in #92 logger.debug( "record_procedure_outcome called for id=%s success=%s (not yet implemented)", procedure_id, success, ) raise NotImplementedError("Procedural memory not yet implemented") # ========================================================================= # Cross-Memory Operations # ========================================================================= async def recall( self, query: str, memory_types: list[MemoryType] | None = None, limit: int = 10, ) -> dict[MemoryType, list[Any]]: """ Recall memories across multiple memory types. Args: query: The search query memory_types: Memory types to search (all if not specified) limit: Maximum results per type Returns: Dictionary mapping memory types to results """ # Placeholder - will be implemented in #97 (Component Integration) logger.debug("recall called for query=%s (not yet implemented)", query[:50]) raise NotImplementedError("Cross-memory recall not yet implemented") async def get_stats( self, memory_type: MemoryType | None = None, ) -> list[MemoryStats]: """ Get memory statistics. Args: memory_type: Specific type or all if not specified Returns: List of statistics for requested memory types """ # Placeholder - will be implemented in #100 (Metrics & Observability) logger.debug("get_stats called (not yet implemented)") raise NotImplementedError("Memory stats not yet implemented") # ========================================================================= # Lifecycle Operations # ========================================================================= async def initialize(self) -> None: """ Initialize the memory manager and its backends. Should be called before using the manager. """ if self._initialized: logger.debug("MemoryManager already initialized") return logger.info( "Initializing MemoryManager for scope %s:%s", self._scope.scope_type.value, self._scope.scope_id, ) # TODO: Initialize backends when implemented self._initialized = True logger.info("MemoryManager initialized successfully") async def close(self) -> None: """ Close the memory manager and release resources. Should be called when done using the manager. """ if not self._initialized: return logger.info( "Closing MemoryManager for scope %s:%s", self._scope.scope_type.value, self._scope.scope_id, ) # TODO: Close backends when implemented self._initialized = False logger.info("MemoryManager closed successfully") async def __aenter__(self) -> "MemoryManager": """Async context manager entry.""" await self.initialize() return self async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: """Async context manager exit.""" await self.close()