Skill: sieyer/agent-memory-1-0-0 (version 1.0.0)

# AgentMemory Skill Persistent memory system for AI agents. Remember facts, learn from experience, and track entities across sessions. ## Installation ```bash clawdhub install agent-memory ``` ## Usage ```python from src.memory import AgentMemory mem = AgentMemory() # Remember facts mem.remember("Important information", tags=["category"]) # Learn from experience mem.learn( action="What was done", context="situation", outcome="positive", # or "negative" insight="What was learned" ) # Recall memories facts = mem.recall("search query") lessons = mem.get_lessons(context="topic") # Track entities mem.track_entity("Name", "person", {"role": "engineer"}) ``` ## When to Use - **Starting a session**: Load relevant context from memory - **After conversations**: Store important facts - **After failures**: Record lessons learned - **Meeting new people/projects**: Track as entities ## Integration with Clawdbot Add to your AGENTS.md or HEARTBEAT.md: ```markdown ## Memory Protocol On session start: 1. Load recent lessons: `mem.get_lessons(limit=5)` 2. Check entity context for current task 3. Recall relevant facts On session end: 1. Extract durable facts from conversation 2. Record any lessons learned 3. Update entity information ``` ## Database Location Default: `~/.agent-memory/memory.db` Custom: `AgentMemory(db_path="/path/to/memory.db")`

path: cli/entity.py
content: #!/usr/bin/env python3
"""CLI wrapper for AgentMemory entity operations."""

import sys
import json
import argparse
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
from memory import AgentMemory


def main():
    parser = argparse.ArgumentParser(description="Manage tracked entities")
    parser.add_argument("--db", help="Database path", default=None)
    subparsers = parser.add_subparsers(dest="command", required=True)
    
    # track command
    track_p = subparsers.add_parser("track", help="Track a new entity")
    track_p.add_argument("name", help="Entity name")
    track_p.add_argument("type", help="Entity type (person, project, company, etc.)")
    track_p.add_argument("--attr", "-a", nargs=2, action="append", metavar=("KEY", "VALUE"),
                         help="Attribute key-value pair (repeatable)")
    
    # get command
    get_p = subparsers.add_parser("get", help="Get entity details")
    get_p.add_argument("name", help="Entity name")
    get_p.add_argument("--type", "-t", help="Entity type (optional)")
    
    # update command
    update_p = subparsers.add_parser("update", help="Update entity attributes")
    update_p.add_argument("name", help="Entity name")
    update_p.add_argument("type", help="Entity type")
    update_p.add_argument("--attr", "-a", nargs=2, action="append", metavar=("KEY", "VALUE"),
                          help="Attribute to update", required=True)
    
    # list command
    list_p = subparsers.add_parser("list", help="List entities")
    list_p.add_argument("--type", "-t", help="Filter by type")
    
    # link command
    link_p = subparsers.add_parser("link", help="Link a fact to an entity")
    link_p.add_argument("name", help="Entity name")
    link_p.add_argument("fact_id", help="Fact ID to link")
    
    args = parser.parse_args()
    mem = AgentMemory(db_path=args.db)
    
    if args.command == "track":
        attrs = dict(args.attr) if args.attr else {}
        entity_id = mem.track_entity(args.name, args.type, attrs)
        print(f"✅ Tracking [{args.type}] {args.name} (id: {entity_id})")
        if attrs:
            print(f"   Attributes: {json.dumps(attrs)}")
            
    elif args.command == "get":
        entity = mem.get_entity(args.name, entity_type=args.type)
        if not entity:
            print(f"❌ Entity '{args.name}' not found")
            sys.exit(1)
        print(f"[{entity.entity_type}] {entity.name}")
        print(f"  ID: {entity.id}")
        print(f"  First seen: {entity.first_seen}")
        print(f"  Last updated: {entity.last_updated}")
        print(f"  Attributes: {json.dumps(entity.attributes, indent=2)}")
        if entity.fact_ids:
            print(f"  Linked facts: {len(entity.fact_ids)}")
            
    elif args.command == "update":
        attrs = dict(args.attr)
        entity = mem.update_entity(args.name, args.type, attrs)
        if entity:
            print(f"✅ Updated {entity.name}: {json.dumps(attrs)}")
        else:
            print(f"❌ Entity not found")
            
    elif args.command == "list":
        entities = mem.list_entities(entity_type=args.type)
        if not entities:
            print("No entities found.")
        for e in entities:
            attr_preview = ", ".join(f"{k}={v}" for k, v in list(e.attributes.items())[:3])
            print(f"[{e.entity_type}] {e.name} ({attr_preview})")
            
    elif args.command == "link":
        mem.link_fact_to_entity(args.name, args.fact_id)
        print(f"🔗 Linked fact {args.fact_id} to {args.name}")


if __name__ == "__main__":
    main()


path: cli/fact.py
content: #!/usr/bin/env python3
"""CLI wrapper for AgentMemory fact operations."""

import sys
import argparse
from pathlib import Path

# Add src to path
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
from memory import AgentMemory


def main():
    parser = argparse.ArgumentParser(description="Manage agent memory facts")
    parser.add_argument("--db", help="Database path", default=None)
    subparsers = parser.add_subparsers(dest="command", required=True)
    
    # add command
    add_p = subparsers.add_parser("add", help="Remember a new fact")
    add_p.add_argument("content", help="The fact to remember")
    add_p.add_argument("--tags", "-t", nargs="+", default=[], help="Tags for the fact")
    add_p.add_argument("--source", "-s", default="conversation", help="Source of fact")
    add_p.add_argument("--confidence", "-c", type=float, default=0.9, help="Confidence 0-1")
    add_p.add_argument("--expires", "-e", type=int, help="Days until expiration")
    
    # recall command
    recall_p = subparsers.add_parser("recall", help="Search for facts")
    recall_p.add_argument("query", help="Search query")
    recall_p.add_argument("--limit", "-n", type=int, default=10, help="Max results")
    recall_p.add_argument("--tags", "-t", nargs="+", help="Filter by tags")
    
    # list command
    list_p = subparsers.add_parser("list", help="List all facts")
    list_p.add_argument("--tags", "-t", nargs="+", help="Filter by tags")
    list_p.add_argument("--limit", "-n", type=int, default=20, help="Max results")
    
    # supersede command
    sup_p = subparsers.add_parser("supersede", help="Replace a fact with new info")
    sup_p.add_argument("fact_id", help="ID of fact to supersede")
    sup_p.add_argument("new_content", help="New fact content")
    
    # forget command
    forget_p = subparsers.add_parser("forget", help="Remove stale facts")
    forget_p.add_argument("--days", "-d", type=int, default=30, help="Forget facts older than N days")
    
    args = parser.parse_args()
    mem = AgentMemory(db_path=args.db)
    
    if args.command == "add":
        fact_id = mem.remember(
            args.content,
            tags=args.tags,
            source=args.source,
            confidence=args.confidence,
            expires_in_days=args.expires
        )
        print(f"✅ Remembered [{fact_id}]: {args.content[:60]}...")
        
    elif args.command == "recall":
        facts = mem.recall(args.query, limit=args.limit, tags=args.tags)
        if not facts:
            print("No matching facts found.")
        for f in facts:
            tags = " ".join(f"#{t}" for t in f.tags) if f.tags else ""
            print(f"[{f.id}] {f.content} {tags}")
            
    elif args.command == "list":
        facts = mem.list_facts(tags=args.tags, limit=args.limit)
        for f in facts:
            tags = " ".join(f"#{t}" for t in f.tags) if f.tags else ""
            print(f"[{f.id}] {f.content[:70]}... {tags}")
            
    elif args.command == "supersede":
        new_fact = mem.supersede(args.fact_id, args.new_content)
        if new_fact:
            print(f"✅ Created [{new_fact.id}] superseding {args.fact_id}")
        else:
            print(f"❌ Fact {args.fact_id} not found")
            
    elif args.command == "forget":
        count = mem.forget_stale(days=args.days)
        print(f"🗑️ Forgot {count} stale facts (>{args.days} days old)")


if __name__ == "__main__":
    main()


path: cli/learn.py
content: #!/usr/bin/env python3
"""CLI wrapper for AgentMemory lesson operations."""

import sys
import argparse
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
from memory import AgentMemory


def main():
    parser = argparse.ArgumentParser(description="Manage agent lessons learned")
    parser.add_argument("--db", help="Database path", default=None)
    subparsers = parser.add_subparsers(dest="command", required=True)
    
    # add command
    add_p = subparsers.add_parser("add", help="Record a lesson learned")
    add_p.add_argument("action", help="What was done")
    add_p.add_argument("context", help="Situation/topic")
    add_p.add_argument("outcome", choices=["positive", "negative", "neutral"], help="Result")
    add_p.add_argument("insight", help="What was learned")
    
    # list command
    list_p = subparsers.add_parser("list", help="List lessons")
    list_p.add_argument("--context", "-c", help="Filter by context")
    list_p.add_argument("--outcome", "-o", choices=["positive", "negative", "neutral"])
    list_p.add_argument("--limit", "-n", type=int, default=20)
    
    # apply command
    apply_p = subparsers.add_parser("apply", help="Mark a lesson as applied")
    apply_p.add_argument("lesson_id", help="ID of lesson to mark applied")
    
    args = parser.parse_args()
    mem = AgentMemory(db_path=args.db)
    
    if args.command == "add":
        lesson_id = mem.learn(
            action=args.action,
            context=args.context,
            outcome=args.outcome,
            insight=args.insight
        )
        emoji = {"positive": "✅", "negative": "❌", "neutral": "➖"}[args.outcome]
        print(f"{emoji} Lesson [{lesson_id}]: {args.insight[:60]}...")
        
    elif args.command == "list":
        lessons = mem.get_lessons(context=args.context, outcome=args.outcome, limit=args.limit)
        if not lessons:
            print("No lessons found.")
        for l in lessons:
            emoji = {"positive": "✅", "negative": "❌", "neutral": "➖"}[l.outcome]
            print(f"{emoji} [{l.id}] {l.context}: {l.insight}")
            print(f"   Action: {l.action} | Applied: {l.applied_count}x")
            
    elif args.command == "apply":
        mem.apply_lesson(args.lesson_id)
        print(f"📝 Marked lesson {args.lesson_id} as applied")


if __name__ == "__main__":
    main()


path: examples/basic_usage.py
content: """
Basic usage example for AgentMemory
"""

import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from src.memory import AgentMemory

# Initialize memory (creates ~/.agent-memory/memory.db by default)
# Use a temporary path for this example
mem = AgentMemory(db_path="/tmp/agent-memory-example.db")

print("🧠 AgentMemory Example\n")

# ==================== FACTS ====================
print("📝 Storing facts...")

mem.remember(
    "Boss prefers brief status updates over long explanations",
    tags=["preference", "communication", "boss"]
)

mem.remember(
    "API rate limit for the trading service is 100 requests per minute",
    tags=["technical", "api", "trading"]
)

mem.remember(
    "Weekly standup is every Monday at 9am EST",
    tags=["schedule", "meeting"]
)

# ==================== LESSONS ====================
print("🎓 Recording lessons...")

mem.learn(
    action="Deployed code directly to production without testing",
    context="deployment",
    outcome="negative",
    insight="Always run the full test suite before deploying, no matter how small the change"
)

mem.learn(
    action="Used quarter-Kelly position sizing for trades",
    context="trading",
    outcome="positive",
    insight="Conservative position sizing prevents large drawdowns and allows recovery from bad streaks"
)

# ==================== ENTITIES ====================
print("👤 Tracking entities...")

mem.track_entity("Alex", "person", {
    "role": "boss",
    "timezone": "America/New_York",
    "communication_style": "direct",
    "interests": ["AI", "trading", "automation"]
})

mem.track_entity("DataDeck", "project", {
    "type": "SaaS",
    "status": "completed",
    "features": 59,
    "url": "https://datadeck-preview.vercel.app"
})

# ==================== RECALL ====================
print("\n🔍 Recalling memories...\n")

# Search for communication preferences
print("Q: How does boss like updates?")
facts = mem.recall("boss communication updates")
for f in facts[:3]:
    print(f"  → {f.content}")

print()

# Get negative lessons about deployment
print("Q: What went wrong with deployments?")
lessons = mem.get_lessons(context="deployment", outcome="negative")
for l in lessons:
    print(f"  → Action: {l.action}")
    print(f"    Lesson: {l.insight}")

print()

# Get entity info
print("Q: What do I know about Alex?")
alex = mem.get_entity("Alex", "person")
if alex:
    print(f"  → Name: {alex.name}")
    print(f"  → Type: {alex.entity_type}")
    print(f"  → Attributes: {alex.attributes}")

# ==================== STATS ====================
print("\n📊 Memory stats:")
stats = mem.stats()
print(f"  Active facts: {stats['active_facts']}")
print(f"  Lessons: {stats['lessons']}")
print(f"  Entities: {stats['entities']}")

print("\n✅ Example complete!")


path: README.md
content: # 🧠 AgentMemory

**Persistent Memory for AI Agents**

[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
[![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
[![ClawdHub](https://img.shields.io/badge/ClawdHub-compatible-purple.svg)](https://clawdhub.com)

Every AI agent session starts fresh. We forget learnings, repeat mistakes, and lose context. **AgentMemory** solves this.

Built for [OpenClaw](https://github.com/openclaw/openclaw) and [Clawdbot](https://github.com/clawdbot/clawdbot) agents, but works with any LLM-powered system.

## ✨ Features

- **📝 Facts** - Store and recall information across sessions
- **🎓 Lessons** - Learn from successes and failures
- **👤 Entities** - Track people, projects, and preferences
- **🔍 Semantic Search** - Find relevant memories fast (FTS5)
- **🧹 Auto-cleanup** - Forget stale information automatically
- **📦 Zero Dependencies** - Just Python + SQLite

## 🚀 Quick Start

```python
from agent_memory import AgentMemory

# Initialize (creates ~/.agent-memory/memory.db)
mem = AgentMemory()

# Remember facts
mem.remember("Boss prefers brief status updates", tags=["preference", "communication"])
mem.remember("API rate limit is 100 req/min", tags=["technical", "api"])

# Learn from experience
mem.learn(
    action="Used RSI momentum strategy for crypto trading",
    context="trading",
    outcome="negative", 
    insight="RSI alone is insufficient, need confirmation signals"
)

# Track entities
mem.track_entity("Alex", "person", {
    "role": "boss",
    "timezone": "America/New_York",
    "communication_style": "brief and direct"
})

# Recall relevant memories
facts = mem.recall("how does boss like updates?")
# → Returns facts about boss preferences

lessons = mem.get_lessons(context="trading", outcome="negative")
# → Returns failed trading lessons to avoid repeating mistakes

# Stats
print(mem.stats())
# → {'active_facts': 42, 'lessons': 15, 'entities': 8}
```

## 📦 Installation

### Option 1: ClawdHub (Recommended for Clawdbot/OpenClaw)

```bash
clawdhub install agent-memory
```

### Option 2: Git Clone

```bash
git clone https://github.com/Dennis-Da-Menace/agent-memory.git
cd agent-memory
```

### Option 3: Copy the file

Just copy `src/memory.py` to your project. It has zero external dependencies!

## 📖 API Reference

### Facts

```python
# Remember something
fact_id = mem.remember(
    content="Important information",
    tags=["category1", "category2"],
    source="conversation",  # or "observation", "inference"
    confidence=0.9,  # 0-1
    expires_in_days=30  # optional auto-expiry
)

# Search facts
facts = mem.recall(
    query="search terms",
    limit=10,
    tags=["filter_tag"],
    min_confidence=0.5
)

# Update a fact (keeps history)
new_id = mem.supersede(old_fact_id, "Updated information")

# Delete a fact
mem.forget(fact_id)

# Cleanup old facts
deleted = mem.forget_stale(days=30, min_access_count=1)
```

### Lessons

```python
# Record a lesson
lesson_id = mem.learn(
    action="What I did",
    context="Situation/topic",
    outcome="positive",  # or "negative", "neutral"
    insight="What I learned from this"
)

# Get lessons
lessons = mem.get_lessons(
    context="trading",  # optional filter
    outcome="negative",  # optional filter
    limit=10
)

# Mark lesson as applied
mem.apply_lesson(lesson_id)
```

### Entities

```python
# Track an entity
entity_id = mem.track_entity(
    name="Alex",
    entity_type="person",  # or "project", "company", "tool"
    attributes={"role": "boss", "timezone": "EST"}
)

# Get entity
entity = mem.get_entity("Alex", entity_type="person")

# Link facts to entities
mem.link_fact_to_entity("Alex", fact_id)
```

### Utilities

```python
# Statistics
stats = mem.stats()
# {'active_facts': 42, 'superseded_facts': 5, 'lessons': 15, 'entities': 8}

# Export everything
data = mem.export_json()
```

## 🔧 Configuration

By default, AgentMemory stores data in `~/.agent-memory/memory.db`. You can customize:

```python
# Custom location
mem = AgentMemory(db_path="/path/to/my/memory.db")

# In-memory (for testing)
mem = AgentMemory(db_path=":memory:")
```

## 🎯 Use Cases

### 1. Preference Learning
```python
# When user expresses preference
mem.remember("User prefers dark mode", tags=["preference", "ui"])

# Later, when making UI decisions
prefs = mem.recall("user preference ui", tags=["preference"])
```

### 2. Error Prevention
```python
# When something fails
mem.learn(
    action="Deployed to production without tests",
    context="deployment",
    outcome="negative",
    insight="Always run test suite before deploying"
)

# Before deploying
lessons = mem.get_lessons(context="deployment", outcome="negative")
for lesson in lessons:
    print(f"⚠️ Remember: {lesson.insight}")
```

### 3. Relationship Context
```python
# Track relationships
mem.track_entity("Alice", "person", {"team": "engineering", "expertise": "backend"})
mem.remember("Alice prefers Slack over email", tags=["communication", "Alice"])

# Before contacting Alice
alice = mem.get_entity("Alice")
alice_facts = mem.recall("Alice communication")
```

## 🤝 Contributing

Built by [Dennis Da Menace](https://github.com/Dennis-Da-Menace) for the OpenClaw community.

Contributions welcome! Please:
1. Fork the repo
2. Create a feature branch
3. Submit a PR

## 📄 License

MIT License - Use freely in your projects!

---

*"Memory is the treasury and guardian of all things." - Cicero*

*Built with 🦀 by an AI agent, for AI agents.*


path: requirements.txt
content: # No external dependencies - just Python stdlib


path: src/memory.py
content: """
AgentMemory - Persistent Memory for AI Agents

A lightweight memory layer that helps AI agents:
- Remember facts across sessions
- Track entities (people, projects, preferences)
- Learn from successes and failures
- Search memories semantically
- Forget stale information automatically

MIT License - Built for the OpenClaw community
"""

import sqlite3
import json
import hashlib
from datetime import datetime, timedelta
from typing import Optional, List, Dict, Any, Tuple
from pathlib import Path
from dataclasses import dataclass, asdict
import re


@dataclass
class Fact:
    """A single piece of remembered information."""
    id: str
    content: str
    tags: List[str]
    source: str  # conversation, observation, inference
    confidence: float  # 0-1
    created_at: str
    last_accessed: str
    access_count: int
    expires_at: Optional[str] = None
    superseded_by: Optional[str] = None
    
    def to_dict(self) -> dict:
        return asdict(self)


@dataclass
class Lesson:
    """A learned experience - what worked or didn't."""
    id: str
    action: str  # What was done
    context: str  # Situation/topic
    outcome: str  # positive, negative, neutral
    insight: str  # What was learned
    created_at: str
    applied_count: int = 0


@dataclass
class Entity:
    """A tracked entity (person, project, company, etc.)."""
    id: str
    name: str
    entity_type: str  # person, project, company, tool, etc.
    attributes: Dict[str, Any]
    first_seen: str
    last_updated: str
    fact_ids: List[str]  # Related facts


class AgentMemory:
    """
    Persistent memory system for AI agents.
    
    Usage:
        mem = AgentMemory()
        
        # Remember facts
        mem.remember("Boss prefers brief updates", tags=["preference", "communication"])
        
        # Learn from experience
        mem.learn(
            action="Used RSI momentum strategy",
            context="crypto trading",
            outcome="negative",
            insight="RSI alone is not sufficient, need confirmation signals"
        )
        
        # Track entities
        mem.track_entity("Alex", "person", {"role": "boss", "timezone": "EST"})
        
        # Recall relevant memories
        facts = mem.recall("how does boss like updates?")
        lessons = mem.get_lessons(context="trading", outcome="negative")
        
        # Automatic cleanup
        mem.forget_stale(days=30)
    """
    
    def __init__(self, db_path: str = None):
        """
        Initialize memory storage.
        
        Args:
            db_path: Path to SQLite database. Defaults to ~/.agent-memory/memory.db
        """
        if db_path is None:
            db_dir = Path.home() / ".agent-memory"
            db_dir.mkdir(exist_ok=True)
            db_path = str(db_dir / "memory.db")
        
        self.db_path = db_path
        self._init_db()
    
    def _init_db(self):
        """Initialize database schema."""
        conn = sqlite3.connect(self.db_path)
        cursor = conn.cursor()
        
        # Facts table
        cursor.execute("""
            CREATE TABLE IF NOT EXISTS facts (
                id TEXT PRIMARY KEY,
                content TEXT NOT NULL,
                tags TEXT,  -- JSON array
                source TEXT DEFAULT 'conversation',
                confidence REAL DEFAULT 1.0,
                created_at TEXT NOT NULL,
                last_accessed TEXT NOT NULL,
                access_count INTEGER DEFAULT 1,
                expires_at TEXT,
                superseded_by TEXT,
                embedding TEXT  -- JSON array for semantic search
            )
        """)
        
        # Lessons table
        cursor.execute("""
            CREATE TABLE IF NOT EXISTS lessons (
                id TEXT PRIMARY KEY,
                action TEXT NOT NULL,
                context TEXT NOT NULL,
                outcome TEXT NOT NULL,  -- positive, negative, neutral
                insight TEXT NOT NULL,
                created_at TEXT NOT NULL,
                applied_count INTEGER DEFAULT 0
            )
        """)
        
        # Entities table
        cursor.execute("""
            CREATE TABLE IF NOT EXISTS entities (
                id TEXT PRIMARY KEY,
                name TEXT NOT NULL,
                entity_type TEXT NOT NULL,
                attributes TEXT,  -- JSON object
                first_seen TEXT NOT NULL,
                last_updated TEXT NOT NULL,
                fact_ids TEXT  -- JSON array
            )
        """)
        
        # Full-text search index for facts
        cursor.execute("""
            CREATE VIRTUAL TABLE IF NOT EXISTS facts_fts 
            USING fts5(content, tags, tokenize='porter')
        """)
        
        conn.commit()
        conn.close()
    
    def _generate_id(self, content: str) -> str:
        """Generate a unique ID for content."""
        timestamp = datetime.utcnow().isoformat()
        hash_input = f"{content}{timestamp}"
        return hashlib.sha256(hash_input.encode()).hexdigest()[:12]
    
    def _now(self) -> str:
        """Current UTC timestamp."""
        return datetime.utcnow().isoformat()
    
    # ==================== FACTS ====================
    
    def remember(self, content: str, tags: List[str] = None, 
                 source: str = "conversation", confidence: float = 1.0,
                 expires_in_days: int = None) -> str:
        """
        Store a fact in memory.
        
        Args:
            content: The fact to remember
            tags: Categories/labels for the fact
            source: Where this fact came from (conversation, observation, inference)
            confidence: How confident we are (0-1)
            expires_in_days: Auto-expire after N days (None = never)
            
        Returns:
            The fact ID
        """
        fact_id = self._generate_id(content)
        now = self._now()
        tags = tags or []
        
        expires_at = None
        if expires_in_days:
            expires_at = (datetime.utcnow() + timedelta(days=expires_in_days)).isoformat()
        
        conn = sqlite3.connect(self.db_path)
        cursor = conn.cursor()
        
        cursor.execute("""
            INSERT INTO facts (id, content, tags, source, confidence, 
                             created_at, last_accessed, access_count, expires_at)
            VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?)
        """, (fact_id, content, json.dumps(tags), source, confidence, 
              now, now, expires_at))
        
        # Add to FTS index
        cursor.execute("""
            INSERT INTO facts_fts (rowid, content, tags)
            SELECT rowid, content, tags FROM facts WHERE id = ?
        """, (fact_id,))
        
        conn.commit()
        conn.close()
        
        return fact_id
    
    def recall(self, query: str, limit: int = 10, 
               tags: List[str] = None, min_confidence: float = 0) -> List[Fact]:
        """
        Search for relevant facts.
        
        Args:
            query: Search query (uses full-text search)
            limit: Maximum results to return
            tags: Filter by tags (AND logic)
            min_confidence: Minimum confidence threshold
            
        Returns:
            List of matching facts, sorted by relevance
        """
        conn = sqlite3.connect(self.db_path)
        cursor = conn.cursor()
        
        # Full-text search
        cursor.execute("""
            SELECT f.* FROM facts f
            JOIN facts_fts fts ON f.rowid = fts.rowid
            WHERE facts_fts MATCH ?
            AND f.confidence >= ?
            AND (f.expires_at IS NULL OR f.expires_at > ?)
            AND f.superseded_by IS NULL
            ORDER BY fts.rank
            LIMIT ?
        """, (query, min_confidence, self._now(), limit))
        
        rows = cursor.fetchall()
        facts = []
        
        for row in rows:
            fact = Fact(
                id=row[0], content=row[1], tags=json.loads(row[2] or "[]"),
                source=row[3], confidence=row[4], created_at=row[5],
                last_accessed=row[6], access_count=row[7],
                expires_at=row[8], superseded_by=row[9]
            )
            
            # Filter by tags if specified
            if tags and not all(t in fact.tags for t in tags):
                continue
            
            facts.append(fact)
            
            # Update access stats
            cursor.execute("""
                UPDATE facts SET last_accessed = ?, access_count = access_count + 1
                WHERE id = ?
            """, (self._now(), fact.id))
        
        conn.commit()
        conn.close()
        
        return facts
    
    def get_fact(self, fact_id: str) -> Optional[Fact]:
        """Get a specific fact by ID."""
        conn = sqlite3.connect(self.db_path)
        cursor = conn.cursor()
        
        cursor.execute("SELECT * FROM facts WHERE id = ?", (fact_id,))
        row = cursor.fetchone()
        conn.close()
        
        if not row:
            return None
        
        return Fact(
            id=row[0], content=row[1], tags=json.loads(row[2] or "[]"),
            source=row[3], confidence=row[4], created_at=row[5],
            last_accessed=row[6], access_count=row[7],
            expires_at=row[8], superseded_by=row[9]
        )
    
    def list_facts(self, tags: List[str] = None, limit: int = 50, 
                   include_superseded: bool = False) -> List[Fact]:
        """List all facts, optionally filtered by tags."""
        conn = sqlite3.connect(self.db_path)
        cursor = conn.cursor()
        
        query = "SELECT * FROM facts WHERE 1=1"
        params = []
        
        if not include_superseded:
            query += " AND superseded_by IS NULL"
        
        query += " ORDER BY created_at DESC LIMIT ?"
        params.append(limit)
        
        cursor.execute(query, params)
        rows = cursor.fetchall()
        conn.close()
        
        facts = []
        for row in rows:
            fact = Fact(
                id=row[0], content=row[1], tags=json.loads(row[2] or "[]"),
                source=row[3], confidence=row[4], created_at=row[5],
                last_accessed=row[6], access_count=row[7],
                expires_at=row[8], superseded_by=row[9]
            )
            if tags and not any(t in fact.tags for t in tags):
                continue
            facts.append(fact)
        
        return facts
    
    def supersede(self, old_fact_id: str, new_content: str, **kwargs) -> str:
        """
        Replace a fact with updated information.
        Keeps the old fact for history but marks it superseded.
        """
        new_id = self.remember(new_content, **kwargs)
        
        conn = sqlite3.connect(self.db_path)
        cursor = conn.cursor()
        cursor.execute(
            "UPDATE facts SET superseded_by = ? WHERE id = ?",
            (new_id, old_fact_id)
        )
        conn.commit()
        conn.close()
        
        return new_id
    
    def forget(self, fact_id: str):
        """Permanently delete a fact."""
        conn = sqlite3.connect(self.db_path)
        cursor = conn.cursor()
        cursor.execute("DELETE FROM facts WHERE id = ?", (fact_id,))
        conn.commit()
        conn.close()
    
    def forget_stale(self, days: int = 30, min_access_count: int = 1):
        """
        Remove facts that haven't been accessed in N days
        and have low access counts.
        """
        cutoff = (datetime.utcnow() - timedelta(days=days)).isoformat()
        
        conn = sqlite3.connect(self.db_path)
        cursor = conn.cursor()
        cursor.execute("""
            DELETE FROM facts 
            WHERE last_accessed < ? 
            AND access_count <= ?
            AND superseded_by IS NULL
        """, (cutoff, min_access_count))
        
        deleted = cursor.rowcount
        conn.commit()
        conn.close()
        
        return deleted
    
    # ==================== LESSONS ====================
    
    def learn(self, action: str, context: str, outcome: str, insight: str) -> str:
        """
        Record a lesson learned from experience.
        
        Args:
            action: What was done
            context: The situation/topic
            outcome: "positive", "negative", or "neutral"
            insight: What was learned
            
        Returns:
            Lesson ID
        """
        lesson_id = self._generate_id(f"{action}{context}")
        
        conn = sqlite3.connect(self.db_path)
        cursor = conn.cursor()
        
        cursor.execute("""
            INSERT INTO lessons (id, action, context, outcome, insight, created_at)
            VALUES (?, ?, ?, ?, ?, ?)
        """, (lesson_id, action, context, outcome, insight, self._now()))
        
        conn.commit()
        conn.close()
        
        return lesson_id
    
    def get_lessons(self, context: str = None, outcome: str = None, 
                    limit: int = 10) -> List[Lesson]:
        """
        Retrieve lessons, optionally filtered.
        
        Args:
            context: Filter by context/topic
            outcome: Filter by outcome (positive/negative/neutral)
            limit: Maximum results
        """
        conn = sqlite3.connect(self.db_path)
        cursor = conn.cursor()
        
        query = "SELECT * FROM lessons WHERE 1=1"
        params = []
        
        if context:
            query += " AND context LIKE ?"
            params.append(f"%{context}%")
        
        if outcome:
            query += " AND outcome = ?"
            params.append(outcome)
        
        query += " ORDER BY created_at DESC LIMIT ?"
        params.append(limit)
        
        cursor.execute(query, params)
        rows = cursor.fetchall()
        conn.close()
        
        return [
            Lesson(
                id=row[0], action=row[1], context=row[2],
                outcome=row[3], insight=row[4], created_at=row[5],
                applied_count=row[6]
            )
            for row in rows
        ]
    
    def apply_lesson(self, lesson_id: str):
        """Mark a lesson as applied (increment counter)."""
        conn = sqlite3.connect(self.db_path)
        cursor = conn.cursor()
        cursor.execute(
            "UPDATE lessons SET applied_count = applied_count + 1 WHERE id = ?",
            (lesson_id,)
        )
        conn.commit()
        conn.close()
    
    # ==================== ENTITIES ====================
    
    def track_entity(self, name: str, entity_type: str, 
                     attributes: Dict[str, Any] = None) -> str:
        """
        Track an entity (person, project, company, etc.).
        
        Args:
            name: Entity name
            entity_type: Type (person, project, company, tool, etc.)
            attributes: Key-value attributes
            
        Returns:
            Entity ID
        """
        entity_id = self._generate_id(f"{entity_type}:{name}")
        now = self._now()
        attributes = attributes or {}
        
        conn = sqlite3.connect(self.db_path)
        cursor = conn.cursor()
        
        # Check if entity exists
        cursor.execute(
            "SELECT id FROM entities WHERE name = ? AND entity_type = ?",
            (name, entity_type)
        )
        existing = cursor.fetchone()
        
        if existing:
            # Update existing
            cursor.execute("""
                UPDATE entities 
                SET attributes = ?, last_updated = ?
                WHERE id = ?
            """, (json.dumps(attributes), now, existing[0]))
            entity_id = existing[0]
        else:
            # Create new
            cursor.execute("""
                INSERT INTO entities (id, name, entity_type, attributes, 
                                     first_seen, last_updated, fact_ids)
                VALUES (?, ?, ?, ?, ?, ?, '[]')
            """, (entity_id, name, entity_type, json.dumps(attributes), now, now))
        
        conn.commit()
        conn.close()
        
        return entity_id
    
    def get_entity(self, name: str, entity_type: str = None) -> Optional[Entity]:
        """Get an entity by name."""
        conn = sqlite3.connect(self.db_path)
        cursor = conn.cursor()
        
        if entity_type:
            cursor.execute(
                "SELECT * FROM entities WHERE name = ? AND entity_type = ?",
                (name, entity_type)
            )
        else:
            cursor.execute("SELECT * FROM entities WHERE name = ?", (name,))
        
        row = cursor.fetchone()
        conn.close()
        
        if not row:
            return None
        
        return Entity(
            id=row[0], name=row[1], entity_type=row[2],
            attributes=json.loads(row[3] or "{}"),
            first_seen=row[4], last_updated=row[5],
            fact_ids=json.loads(row[6] or "[]")
        )
    
    def link_fact_to_entity(self, entity_name: str, fact_id: str):
        """Link a fact to an entity."""
        conn = sqlite3.connect(self.db_path)
        cursor = conn.cursor()
        
        cursor.execute("SELECT id, fact_ids FROM entities WHERE name = ?", (entity_name,))
        row = cursor.fetchone()
        
        if row:
            fact_ids = json.loads(row[1] or "[]")
            if fact_id not in fact_ids:
                fact_ids.append(fact_id)
                cursor.execute(
                    "UPDATE entities SET fact_ids = ? WHERE id = ?",
                    (json.dumps(fact_ids), row[0])
                )
        
        conn.commit()
        conn.close()
    
    def list_entities(self, entity_type: str = None) -> List[Entity]:
        """List all entities, optionally filtered by type."""
        conn = sqlite3.connect(self.db_path)
        cursor = conn.cursor()
        
        if entity_type:
            cursor.execute(
                "SELECT * FROM entities WHERE entity_type = ? ORDER BY last_updated DESC",
                (entity_type,)
            )
        else:
            cursor.execute("SELECT * FROM entities ORDER BY last_updated DESC")
        
        rows = cursor.fetchall()
        conn.close()
        
        return [
            Entity(
                id=row[0], name=row[1], entity_type=row[2],
                attributes=json.loads(row[3] or "{}"),
                first_seen=row[4], last_updated=row[5],
                fact_ids=json.loads(row[6] or "[]")
            )
            for row in rows
        ]
    
    def update_entity(self, name: str, entity_type: str, 
                      attributes: Dict[str, Any]) -> Optional[Entity]:
        """Update entity attributes (merges with existing)."""
        conn = sqlite3.connect(self.db_path)
        cursor = conn.cursor()
        
        cursor.execute(
            "SELECT id, attributes FROM entities WHERE name = ? AND entity_type = ?",
            (name, entity_type)
        )
        row = cursor.fetchone()
        
        if not row:
            conn.close()
            return None
        
        existing_attrs = json.loads(row[1] or "{}")
        existing_attrs.update(attributes)
        
        cursor.execute(
            "UPDATE entities SET attributes = ?, last_updated = ? WHERE id = ?",
            (json.dumps(existing_attrs), self._now(), row[0])
        )
        conn.commit()
        conn.close()
        
        return self.get_entity(name, entity_type)
    
    # ==================== UTILITIES ====================
    
    def stats(self) -> Dict[str, int]:
        """Get memory statistics."""
        conn = sqlite3.connect(self.db_path)
        cursor = conn.cursor()
        
        cursor.execute("SELECT COUNT(*) FROM facts WHERE superseded_by IS NULL")
        active_facts = cursor.fetchone()[0]
        
        cursor.execute("SELECT COUNT(*) FROM facts WHERE superseded_by IS NOT NULL")
        superseded_facts = cursor.fetchone()[0]
        
        cursor.execute("SELECT COUNT(*) FROM lessons")
        lessons = cursor.fetchone()[0]
        
        cursor.execute("SELECT COUNT(*) FROM entities")
        entities = cursor.fetchone()[0]
        
        conn.close()
        
        return {
            "active_facts": active_facts,
            "superseded_facts": superseded_facts,
            "total_facts": active_facts + superseded_facts,
            "lessons": lessons,
            "entities": entities
        }
    
    def export_json(self) -> Dict:
        """Export all memories as JSON."""
        conn = sqlite3.connect(self.db_path)
        cursor = conn.cursor()
        
        cursor.execute("SELECT * FROM facts")
        facts = [
            Fact(
                id=r[0], content=r[1], tags=json.loads(r[2] or "[]"),
                source=r[3], confidence=r[4], created_at=r[5],
                last_accessed=r[6], access_count=r[7],
                expires_at=r[8], superseded_by=r[9]
            ).to_dict()
            for r in cursor.fetchall()
        ]
        
        cursor.execute("SELECT * FROM lessons")
        lessons = [
            {"id": r[0], "action": r[1], "context": r[2], 
             "outcome": r[3], "insight": r[4], "created_at": r[5],
             "applied_count": r[6]}
            for r in cursor.fetchall()
        ]
        
        cursor.execute("SELECT * FROM entities")
        entities = [
            {"id": r[0], "name": r[1], "entity_type": r[2],
             "attributes": json.loads(r[3] or "{}"),
             "first_seen": r[4], "last_updated": r[5],
             "fact_ids": json.loads(r[6] or "[]")}
            for r in cursor.fetchall()
        ]
        
        conn.close()
        
        return {
            "exported_at": self._now(),
            "facts": facts,
            "lessons": lessons,
            "entities": entities
        }


# Convenience function for quick setup
def get_memory(db_path: str = None) -> AgentMemory:
    """Get or create an AgentMemory instance."""
    return AgentMemory(db_path)


path: src/__init__.py
content: """
AgentMemory - Persistent Memory for AI Agents
"""

from .memory import AgentMemory, Fact, Lesson, Entity, get_memory

__version__ = "1.0.0"
__all__ = ["AgentMemory", "Fact", "Lesson", "Entity", "get_memory"]


path: tests/test_memory.py
content: """
Tests for AgentMemory
"""

import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from src.memory import AgentMemory
import tempfile
import os


def test_basic_facts():
    """Test basic fact operations."""
    with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as f:
        db_path = f.name
    
    try:
        mem = AgentMemory(db_path)
        
        # Remember
        fact_id = mem.remember("Test fact", tags=["test"])
        assert fact_id is not None
        
        # Recall
        facts = mem.recall("Test fact")
        assert len(facts) >= 1
        assert facts[0].content == "Test fact"
        assert "test" in facts[0].tags
        
        # Get specific
        fact = mem.get_fact(fact_id)
        assert fact is not None
        assert fact.id == fact_id
        
        # Forget
        mem.forget(fact_id)
        fact = mem.get_fact(fact_id)
        assert fact is None
        
        print("✅ Basic facts test passed")
    finally:
        os.unlink(db_path)


def test_lessons():
    """Test lesson learning."""
    with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as f:
        db_path = f.name
    
    try:
        mem = AgentMemory(db_path)
        
        # Learn
        lesson_id = mem.learn(
            action="Test action",
            context="testing",
            outcome="positive",
            insight="Tests are good"
        )
        assert lesson_id is not None
        
        # Get lessons
        lessons = mem.get_lessons(context="testing")
        assert len(lessons) >= 1
        assert lessons[0].action == "Test action"
        assert lessons[0].outcome == "positive"
        
        # Filter by outcome
        positive = mem.get_lessons(outcome="positive")
        assert len(positive) >= 1
        
        negative = mem.get_lessons(outcome="negative")
        assert len(negative) == 0
        
        print("✅ Lessons test passed")
    finally:
        os.unlink(db_path)


def test_entities():
    """Test entity tracking."""
    with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as f:
        db_path = f.name
    
    try:
        mem = AgentMemory(db_path)
        
        # Track
        entity_id = mem.track_entity(
            "TestPerson",
            "person",
            {"role": "tester"}
        )
        assert entity_id is not None
        
        # Get
        entity = mem.get_entity("TestPerson", "person")
        assert entity is not None
        assert entity.name == "TestPerson"
        assert entity.attributes["role"] == "tester"
        
        # Update
        mem.track_entity("TestPerson", "person", {"role": "senior tester"})
        entity = mem.get_entity("TestPerson", "person")
        assert entity.attributes["role"] == "senior tester"
        
        print("✅ Entities test passed")
    finally:
        os.unlink(db_path)


def test_supersede():
    """Test fact superseding."""
    with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as f:
        db_path = f.name
    
    try:
        mem = AgentMemory(db_path)
        
        # Create original
        old_id = mem.remember("Old fact")
        
        # Supersede
        new_id = mem.supersede(old_id, "New fact")
        
        # Old fact should be superseded
        old_fact = mem.get_fact(old_id)
        assert old_fact.superseded_by == new_id
        
        # Recall should return new fact, not old
        facts = mem.recall("fact")
        contents = [f.content for f in facts]
        assert "New fact" in contents
        # Old fact shouldn't appear in results (superseded)
        
        print("✅ Supersede test passed")
    finally:
        os.unlink(db_path)


def test_stats():
    """Test statistics."""
    with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as f:
        db_path = f.name
    
    try:
        mem = AgentMemory(db_path)
        
        mem.remember("Fact 1")
        mem.remember("Fact 2")
        mem.learn("Action", "context", "positive", "insight")
        mem.track_entity("Entity", "type", {})
        
        stats = mem.stats()
        assert stats["active_facts"] == 2
        assert stats["lessons"] == 1
        assert stats["entities"] == 1
        
        print("✅ Stats test passed")
    finally:
        os.unlink(db_path)


def test_export():
    """Test JSON export."""
    with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as f:
        db_path = f.name
    
    try:
        mem = AgentMemory(db_path)
        
        mem.remember("Export test fact", tags=["export"])
        mem.learn("Export action", "export", "neutral", "Export insight")
        mem.track_entity("ExportEntity", "test", {"key": "value"})
        
        data = mem.export_json()
        
        assert "exported_at" in data
        assert len(data["facts"]) >= 1
        assert len(data["lessons"]) >= 1
        assert len(data["entities"]) >= 1
        
        print("✅ Export test passed")
    finally:
        os.unlink(db_path)


if __name__ == "__main__":
    test_basic_facts()
    test_lessons()
    test_entities()
    test_supersede()
    test_stats()
    test_export()
    print("\n🎉 All tests passed!")


path: _meta.json
content: ownerId: kn79xt54feg7bq89ehsvcp01zn809mp1
slug: agent-memory
version: 1.0.0
publishedAt: 1769896232617

Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)