DEV Community

Programming Central
Programming Central

Posted on

How We Built a Self-Refactoring AI Agent: Inside the "Memory Garbage Collector" of Hermes

If you have ever built an autonomous AI agent, you have likely run into the "memory bloat" problem.

At first, your agent is fast, sharp, and highly efficient. It solves tasks, writes code, and stores new skills. But as the sessions pile up, something breaks. The agent's memory becomes cluttered with hundreds of highly specific, redundant, or outdated instructions. Suddenly, retrieval latency spikes, token costs skyrocket, and the agent begins to suffer from cognitive noise—hallucinating or retrieving the wrong "skill" for the job.

Most developers try to solve this with simple vector database search or basic Least Recently Used (LRU) cache eviction. But these are blunt instruments. They don't understand the meaning of the information they are discarding.

In the Hermes Agent framework, we solved this by borrowing a classic concept from systems engineering: Garbage Collection.

We built the Hermes Curator (agent/curator.py), an intelligent, stateful background daemon that continuously reviews, consolidates, and archives the agent's long-term skill library. It is the agent's "executive function," transforming messy episodic experiences into clean, structured semantic knowledge.

Here is an in-depth look at the architecture, theory, and code behind this self-refactoring memory system.

(The concepts and code demonstrated here are drawn from my ebook Hermes Agent, The Self-Evolving AI Workforce)


The Core Concept: A Garbage Collector with Intent

In systems programming, a garbage collector (GC) automatically manages memory by reclaiming space occupied by objects that are no longer in use. The Hermes Curator does the exact same thing for the agent’s skill library, but with a crucial twist: it doesn't just look at memory addresses; it looks at semantic meaning.

If an agent creates ten different variations of a script to parse a CSV file, a traditional GC cannot help. To a database, those are ten unique, valid files. But to the Hermes Curator, this is high-entropy redundancy.

The Curator's job is to identify these redundant skills, consolidate them into a single, comprehensive "umbrella skill" (with clear sub-sections and templates), and archive the obsolete originals.

To do this efficiently without burning through API tokens, the Curator uses a two-tiered strategy that mirrors the distinction between a compacting generational GC and a manual memory defragmenter:

+-------------------------------------------------------------+
|                     The Hermes Curator                      |
+-------------------------------------------------------------+
                               |
        +----------------------+----------------------+
        |                                             |
        v                                             v
[ Tier 1: Automatic Heuristic Pass ]        [ Tier 2: LLM-Driven Intentional Pass ]
  - High-frequency, low-cost                  - Low-frequency, high-cost
  - Deterministic state machine                - Forked AIAgent daemon
  - Evaluates timestamps                       - Performs semantic "umbrella-building"
  - Active -> Stale -> Archived                - Minimizes Kolmogorov complexity
Enter fullscreen mode Exit fullscreen mode

1. The Automatic, Heuristic-Driven Pass (The "Generational GC")

This is a pure, non-LLM phase implemented in Python. It runs on deterministic rules based on timestamps and state machines. Much like a generational GC moves objects from the "young" generation to the "old" generation based on survival time, this pass automatically transitions skills from active to stale and eventually to archived based on their usage history. It is low-cost, high-frequency, and handles the bulk of routine tidying without invoking expensive LLM calls.

2. The LLM-Driven, Intentional Pass (The "Defragmenter")

This is the sophisticated, resource-intensive phase. It spawns an isolated, forked AI agent to analyze the semantic content of the active skills. Instead of simply looking for duplicate files, it performs "umbrella-building"—restructuring the knowledge base into hierarchical, highly discoverable directories. It acts as a codebase refactoring tool, continuously optimizing the agent's knowledge architecture for future retrieval.


The Theoretical Foundations of Semantic Memory Curation

The Curator's architecture is built on four core principles from computer science, information theory, and cognitive psychology.

1. The Principle of Locality and the Cost of Search

In hardware design, the principle of locality states that systems tend to access a relatively small portion of their storage space at any given time. A cache works because it exploits this.

An agent's skill library is essentially a cache of learned behaviors. If this cache grows too large and is filled with hyper-specific, flat files, the cost of searching it—both in terms of token consumption and cognitive load—grows exponentially.

The Curator solves this by building umbrella skills. An umbrella skill acts as a cache line. Instead of scanning hundreds of narrow skills, the agent's retrieval system only needs to find the correct umbrella skill, which then points to specific sub-files, templates, or scripts. This transforms a flat, high-latency search space into a hierarchical, low-latency one.

2. Information Entropy and Kolmogorov Complexity

From an information theory perspective, a skill library with overlapping, narrow skills has high redundancy. This redundancy increases the "entropy" of the library, making it harder for a search algorithm to isolate relevant information.

The Curator minimizes the Kolmogorov complexity of the skill library. In simple terms, it searches for the shortest, most elegant "program" (the set of umbrella skills) that can fully describe all the specific knowledge the agent has accumulated.

3. The Repository with a Background Indexer Pattern

In data engineering, a background indexer runs asynchronously to validate data integrity, optimize physical data layout, and update query indexes. The Curator implements this exact pattern:

  • Data Integrity: It ensures that skill states (active, stale, archived) match actual usage.
  • Layout Optimization: It runs a semantic VACUUM and REINDEX by merging related files.
  • Audit Trail: It outputs detailed run reports (run.json and REPORT.md), ensuring that every self-refactoring step taken by the autonomous agent can be audited by a human developer.

4. Fork-Join Concurrency and State Isolation

Running an LLM to evaluate and modify an agent's own code while the agent is actively talking to a user is highly dangerous. A shared state could lead to race conditions, corrupted files, or accidental tool executions.

To prevent this, the Curator uses a Fork-Join concurrency model. It spawns an independent, sandboxed AIAgent in a background daemon thread. This child agent has its own isolated session history, no access to the user's active conversation, and redirected standard outputs. If the background curator crashes, the main user session remains completely unaffected.


Deep Dive: The Heuristic Lifecycle Manager

Let’s look at how the first tier—the deterministic state machine—is implemented in agent/curator.py. This function manages the lifecycle of skills based on time, ensuring we don't waste API tokens on cold data.

# agent/curator.py
from datetime import datetime, timezone, timedelta
from typing import Dict, Optional, Any, List, Set
import logging

logger = logging.getLogger(__name__)

def apply_automatic_transitions(now: Optional[datetime] = None) -> Dict[str, int]:
    """
    Walk every agent-created skill and move active/stale/archived based on
    the latest real activity timestamp. Pinned skills are never touched.
    Returns a counter dict describing what changed.
    """
    from tools import skill_usage as _u

    if now is None:
        now = datetime.now(timezone.utc)

    # Calculate cutoffs based on configurable durations
    stale_cutoff = now - timedelta(days=get_stale_after_days())
    archive_cutoff = now - timedelta(days=get_archive_after_days())

    counts = {"marked_stale": 0, "archived": 0, "reactivated": 0, "checked": 0}

    # Iterate over all agent-created skills
    for row in _u.agent_created_report():
        counts["checked"] += 1
        name = row["name"]

        # Pinned skills are sacred and bypass all transitions
        if row.get("pinned"):
            continue

        # Determine the anchor timestamp: last activity, or creation date
        last_activity = _parse_iso(row.get("last_activity_at"))
        anchor = last_activity or _parse_iso(row.get("created_at")) or now
        if anchor.tzinfo is None:
            anchor = anchor.replace(tzinfo=timezone.utc)

        current = row.get("state", _u.STATE_ACTIVE)

        # State Machine Logic:
        # 1. If anchor is older than archive_cutoff and not already archived -> archive
        if anchor <= archive_cutoff and current != _u.STATE_ARCHIVED:
            ok, _msg = _u.archive_skill(name)
            if ok:
                counts["archived"] += 1

        # 2. If anchor is older than stale_cutoff but newer than archive, and is active -> mark stale
        elif anchor <= stale_cutoff and current == _u.STATE_ACTIVE:
            _u.set_state(name, _u.STATE_STALE)
            counts["marked_stale"] += 1

        # 3. If anchor is newer than stale_cutoff and is currently stale -> reactivate
        elif anchor > stale_cutoff and current == _u.STATE_STALE:
            _u.set_state(name, _u.STATE_ACTIVE)
            counts["reactivated"] += 1

    return counts
Enter fullscreen mode Exit fullscreen mode

Key Architectural Takeaways from the Heuristic Pass:

  • Deterministic Finite-State Machine (FSM): Transitions are predictable and computationally cheap.
  • The "Pinned" Escape Hatch: Humans or high-level processes can "pin" a skill, marking it as permanent knowledge that is entirely exempt from automatic pruning.
  • Safety-First Archival: Notice that the code calls _u.archive_skill(), never _u.delete_skill(). An autonomous system should never permanently destroy its own knowledge without a recovery path. Archival moves files to cold storage, keeping them safe but out of the active retrieval window.

The LLM Pass: Forking an Agent for Deep Refactoring

When the heuristic pass is complete, the Curator initiates the second tier: the semantic refactoring sweep.

Instead of running a basic API call, the Curator spawns a completely isolated instance of AIAgent. This background agent is given tool access to read, write, and merge skills, guided by a highly structured system prompt.

Here is how the Curator handles this process isolation:

# agent/curator.py
import os

def _run_llm_review(prompt: str) -> Dict[str, Any]:
    """
    Spawn an AIAgent fork to run the curator review prompt.

    Returns a dict with:
      - final: full (untruncated) final response from the reviewer
      - summary: short summary suitable for state file (240-char cap)
      - model, provider: what the fork actually ran on
      - tool_calls: list of {name, arguments} for every tool call made
      - error: set if the pass failed mid-run
    """
    import contextlib
    result_meta: Dict[str, Any] = {
        "final": "",
        "summary": "",
        "model": "",
        "provider": "",
        "tool_calls": [],
        "error": None,
    }
    try:
        from run_agent import AIAgent
    except Exception as e:
        result_meta["error"] = f"AIAgent import failed: {e}"
        result_meta["summary"] = result_meta["error"]
        return result_meta

    # (Provider and model resolution logic happens here...)
    _model_name = "gpt-4o-mini"  # Typically a cheaper, faster model for background tasks
    _resolved_provider = "openai"
    _api_key = os.getenv("OPENAI_API_KEY")
    _base_url = None
    _api_mode = None

    review_agent = None
    try:
        review_agent = AIAgent(
            model=_model_name,
            provider=_resolved_provider,
            api_key=_api_key,
            base_url=_base_url,
            api_mode=_api_mode,
            max_iterations=9999, # High iteration ceiling for large-scale sweeps
            quiet_mode=True,
            platform="curator",
            skip_context_files=True, # Do not load active user context files
            skip_memory=True,        # Do not load active episodic conversation memory
        )

        # CRITICAL: Disable recursive nudges. The curator must never spawn its own curator!
        review_agent._memory_nudge_interval = 0
        review_agent._skill_nudge_interval = 0

        # Redirect the forked agent's stdout/stderr to /dev/null to avoid cluttering the terminal
        with open(os.devnull, "w") as _devnull, \
             contextlib.redirect_stdout(_devnull), \
             contextlib.redirect_stderr(_devnull):
            conv_result = review_agent.run_conversation(user_message=prompt)

        # (Extract final response, summary, and tool calls from conv_result...)
        result_meta["final"] = conv_result.get("text", "")
        result_meta["summary"] = conv_result.get("summary", "")[:240]
        result_meta["tool_calls"] = conv_result.get("tool_calls", [])
        result_meta["model"] = _model_name
        result_meta["provider"] = _resolved_provider

    except Exception as e:
        result_meta["error"] = f"Runtime error: {e}"
        result_meta["summary"] = result_meta["error"]
    finally:
        if review_agent is not None:
            try:
                review_agent.close()
            except Exception:
                pass
    return result_meta
Enter fullscreen mode Exit fullscreen mode

Why This Design Works:

  1. Resource Optimization: The background curator can run on a highly optimized, cost-effective model (like gpt-4o-mini or claude-3-haiku), while the main user-facing agent runs on a highly reasoning-capable model (like claude-3-5-sonnet or gpt-4o).
  2. Strict Sandboxing: By setting skip_context_files=True and skip_memory=True, we prevent the background agent from reading sensitive user session data. It can only see the skill files it is tasked with organizing.
  3. No Infinite Loops: Setting _memory_nudge_interval = 0 guarantees the background agent won't trigger another background curation process recursively, which would quickly drain your API budget.

Reconciling LLM Actions with Forensic Evidence

One of the hardest parts of building autonomous systems is handling LLM unreliability. What happens if the LLM claims in its final text summary that it consolidated read_csv_v1 into csv_parser_umbrella, but under the hood, it actually just deleted read_csv_v1 without copying the code over? Or what if it hallucinated an umbrella name entirely?

To solve this, Hermes implements a truth-finding reconciliation algorithm in _reconcile_classification(). It doesn't blindly trust the LLM's written summary. Instead, it cross-references the LLM's assertions with the actual, forensic record of tool calls made during the session.

# agent/curator.py

def _reconcile_classification(
    removed: List[str],
    heuristic: Dict[str, List[Dict[str, Any]]],
    model_block: Dict[str, List[Dict[str, str]]],
    destinations: Set[str],
    absorbed_declarations: Optional[Dict[str, Dict[str, Any]]] = None,
) -> Dict[str, List[Dict[str, Any]]]:
    """
    Merge heuristic (tool-call evidence) with the model's structured block.

    Rules (evaluated in order; first match wins):
    - Model-declared `absorbed_into` at delete/archive time is authoritative.
    - Model-declared consolidation wins when its target umbrella exists.
    - Model-declared consolidation whose target does NOT exist is downgraded 
      (the model hallucinated an umbrella target).
    - Heuristic-only findings (tool calls show a merge occurred, but the model 
      forgot to write it in the final summary) are preserved as "tool-call audit".
    - Model-declared pruning (deletions) is accepted unless tool-call logs 
      contradict it.
    """
    reconciled = {"consolidated": [], "pruned": []}

    # (Reconciliation logic executes here...)
    # It parses the tool call history to verify that if a skill was deleted,
    # its contents were indeed appended or written to another active skill file.

    return reconciled
Enter fullscreen mode Exit fullscreen mode

This multi-signal validation relies on three distinct layers of evidence:

  1. The "Smoking Gun" (Absorbed Declarations): When the agent calls delete_skill(name, absorbed_into="target"), the tool itself forces the agent to declare where the code is going. This is the strongest signal of intent.
  2. The "Witness Testimony" (Model Block): The structured YAML block output by the model at the end of its curation run. It represents what the model thinks it did.
  3. The "Forensic Evidence" (Heuristic Tool-Call Audit): A post-hoc analysis of the raw file writes and tool calls. If the model claims it consolidated a skill, but the file system logs show no writes to the target umbrella, the system catches the hallucination and flags it in the audit report.

Building Trust: The Audit Trail

Autonomous self-refactoring can be terrifying for a system administrator. If an agent can rewrite its own codebase, how do you debug it when something goes wrong?

The answer is a strict, dual-format audit trail. Every time the Hermes Curator runs, it writes a permanent record under logs/curator/{timestamp}/ containing two files:

  1. run.json: A highly detailed, machine-readable file containing the exact state before and after, a list of all tool calls, token usage, and precise transition deltas.
  2. REPORT.md: A clean, human-readable markdown file summarizing what was archived, what was consolidated, and why.
# agent/curator.py
from pathlib import Path
import json

def _write_run_report(
    *,
    started_at: datetime,
    elapsed_seconds: float,
    auto_counts: Dict[str, int],
    auto_summary: str,
    before_report: List[Dict[str, Any]],
    before_names: Set[str],
    after_report: List[Dict[str, Any]],
    llm_meta: Dict[str, Any],
) -> Optional[Path]:
    """
    Write run.json + REPORT.md under logs/curator/{YYYYMMDD-HHMMSS}/.
    Returns the report directory path on success.
    """
    run_dir = Path(f"logs/curator/{started_at.strftime('%Y%m%d-%H%M%S')}")
    run_dir.mkdir(parents=True, exist_ok=True)

    payload = {
        "started_at": started_at.isoformat(),
        "duration_seconds": round(elapsed_seconds, 2),
        "model": llm_meta.get("model", ""),
        "provider": llm_meta.get("provider", ""),
        "auto_transitions": auto_counts,
        "counts": {
            "before": len(before_names),
            "after": len(after_report),
            "archived_this_run": len(llm_meta.get("archived", [])),
            "tool_calls_total": len(llm_meta.get("tool_calls", [])),
        },
        "tool_calls": llm_meta.get("tool_calls", []),
        "llm_summary": llm_meta.get("summary", ""),
    }

    # Write run.json for automated monitoring tools
    try:
        (run_dir / "run.json").write_text(
            json.dumps(payload, indent=2, ensure_ascii=False) + "\n",
            encoding="utf-8",
        )
    except Exception as e:
        logger.debug("Failed to write run.json: %s", e)

    # Write REPORT.md for human engineers
    try:
        md_content = _render_report_markdown(payload)
        (run_dir / "REPORT.md").write_text(md_content, encoding="utf-8")
    except Exception as e:
        logger.debug("Failed to write REPORT.md: %s", e)

    return run_dir
Enter fullscreen mode Exit fullscreen mode

By keeping this log, developers can easily track how the agent's knowledge base is evolving. If an agent starts performing poorly, a quick git diff or a glance at the latest REPORT.md will show exactly which skills were merged or archived, allowing for instant rollback.


Conclusion: The Future of Agentic Memory

As AI agents transition from simple chatbots to long-lived, autonomous workspace companions, memory management cannot remain an afterthought. We cannot simply rely on larger context windows or raw vector databases to solve information bloat.

The Hermes Curator demonstrates that software engineering principles still apply in the age of LLMs. By combining a deterministic, low-cost generational state machine with an isolated, highly reflective background agent, we can build systems that continuously learn, self-refactor, and maintain high performance over months of continuous operation.


Let's Discuss

  1. How do you handle memory decay in your own AI agent architectures? Do you rely entirely on vector DB retrieval thresholds, or have you experimented with active curation?
  2. What are your thoughts on allowing agents to modify their own codebase or skill libraries? How do you balance the need for autonomy with strict safety and predictability constraints?

Leave your thoughts in the comments below!

The concepts and code demonstrated here are drawn directly from the comprehensive roadmap laid out in the ebook Hermes Agent, The Self-Evolving AI Workforce: details link, you can find also my programming ebooks with AI here: Programming & AI eBooks.

Top comments (0)