Files
syndarix/backend/app/services/memory/manager.py
Felipe Cardoso 085a748929 feat(memory): #87 project setup & core architecture
Implements Sub-Issue #87 of Issue #62 (Agent Memory System).

Core infrastructure:
- memory/types.py: Type definitions for all memory types (Working, Episodic,
  Semantic, Procedural) with enums for MemoryType, ScopeLevel, Outcome
- memory/config.py: MemorySettings with MEM_ env prefix, thread-safe singleton
- memory/exceptions.py: Comprehensive exception hierarchy for memory operations
- memory/manager.py: MemoryManager facade with placeholder methods

Directory structure:
- working/: Working memory (Redis/in-memory) - to be implemented in #89
- episodic/: Episodic memory (experiences) - to be implemented in #90
- semantic/: Semantic memory (facts) - to be implemented in #91
- procedural/: Procedural memory (skills) - to be implemented in #92
- scoping/: Scope management - to be implemented in #93
- indexing/: Vector indexing - to be implemented in #94
- consolidation/: Memory consolidation - to be implemented in #95

Tests: 71 unit tests for config, types, and exceptions
Docs: Comprehensive implementation plan at docs/architecture/memory-system-plan.md

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 01:27:36 +01:00

607 lines
18 KiB
Python

"""
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()