forked from cardosofelipe/fast-next-template
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>
607 lines
18 KiB
Python
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()
|