feat(memory): add database schema and storage layer (Issue #88)

Add SQLAlchemy models for the Agent Memory System:
- WorkingMemory: Key-value storage with TTL for active sessions
- Episode: Experiential memories from task executions
- Fact: Semantic knowledge triples with confidence scores
- Procedure: Learned skills and procedures with success tracking
- MemoryConsolidationLog: Tracks consolidation jobs between memory tiers

Create enums for memory system:
- ScopeType: global, project, agent_type, agent_instance, session
- EpisodeOutcome: success, failure, partial
- ConsolidationType: working_to_episodic, episodic_to_semantic, etc.
- ConsolidationStatus: pending, running, completed, failed

Add Alembic migration (0005) for all memory tables with:
- Foreign key relationships to projects, agent_instances, agent_types
- Comprehensive indexes for query patterns
- Unique constraints for key lookups and triple uniqueness
- Vector embedding column placeholders (Text fallback until pgvector enabled)

Fix timezone-naive datetime.now() in types.py TaskState (review feedback)

Includes 30 unit tests for models and enums.

Closes #88

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-05 01:37:58 +01:00
parent 085a748929
commit c9d8c0835c
14 changed files with 1383 additions and 7 deletions

View File

@@ -6,12 +6,17 @@ Core type definitions and interfaces for the Agent Memory System.
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime
from datetime import UTC, datetime
from enum import Enum
from typing import Any
from uuid import UUID
def _utcnow() -> datetime:
"""Get current UTC time as timezone-aware datetime."""
return datetime.now(UTC)
class MemoryType(str, Enum):
"""Types of memory in the agent memory system."""
@@ -93,7 +98,7 @@ class MemoryItem:
def get_age_seconds(self) -> float:
"""Get the age of this memory item in seconds."""
return (datetime.now() - self.created_at).total_seconds()
return (_utcnow() - self.created_at).total_seconds()
@dataclass
@@ -106,14 +111,14 @@ class WorkingMemoryItem:
key: str
value: Any
expires_at: datetime | None = None
created_at: datetime = field(default_factory=datetime.now)
updated_at: datetime = field(default_factory=datetime.now)
created_at: datetime = field(default_factory=_utcnow)
updated_at: datetime = field(default_factory=_utcnow)
def is_expired(self) -> bool:
"""Check if this item has expired."""
if self.expires_at is None:
return False
return datetime.now() > self.expires_at
return _utcnow() > self.expires_at
@dataclass
@@ -128,8 +133,8 @@ class TaskState:
total_steps: int = 0
progress_percent: float = 0.0
context: dict[str, Any] = field(default_factory=dict)
started_at: datetime = field(default_factory=datetime.now)
updated_at: datetime = field(default_factory=datetime.now)
started_at: datetime = field(default_factory=_utcnow)
updated_at: datetime = field(default_factory=_utcnow)
@dataclass