---
name: hermes-memory-manager
description: Single-integration-point memory orchestrator — delegates to multiple providers, one external allowed, background async sync so slow providers never block a turn. Lifecycle hooks for compression/session-switch. Source: NousResearch/hermes-agent (MIT).
origin: NousResearch/hermes-agent (MIT) — agent/memory_manager.py
license: MIT
version: 1.0.0
compatibility: yana-ai >= 1.3.54
---

# /hermes-memory-manager

## When to Use

- Building an agent that connects to multiple memory backends (built-in L1 + external vector store)
- Need memory sync to not block the turn response (background write)
- Switching between memory providers without rewriting all dependent code
- Any agent memory layer that needs provider isolation and lifecycle hooks

## Do NOT use for

- Single-provider memory with no switching needs — direct integration is simpler
- Synchronous memory that must complete before next turn (use direct await)
- See also: [[hermes-streaming-scrubber]] for keeping memory blocks out of UI

---

## Design Principles

```
1. One integration point   — run_agent.py calls MemoryManager, not each provider directly
2. One external provider   — prevents tool schema bloat; multiple built-ins ok
3. Background sync         — sync_all() runs in a separate worker thread; turn never waits
4. Ordered prompt assembly — built-in providers first, then external
5. Reserved tool names      — external providers cannot shadow core tool names
```

---

## Provider Interface

```python
from abc import ABC, abstractmethod
from concurrent.futures import ThreadPoolExecutor

class MemoryProvider(ABC):
    """Implement this to add a memory backend."""

    @property
    @abstractmethod
    def name(self) -> str: ...

    @property
    def tool_names(self) -> list[str]:
        """Tool names this provider registers. Must not collide with core."""
        return []

    def build_system_prompt_block(self) -> str:
        """Return text to inject into system prompt (stable tier)."""
        return ""

    def prefetch(self, messages: list[dict]) -> None:
        """Called before each turn — load context synchronously."""
        pass

    def sync(self, messages: list[dict]) -> None:
        """Called after each turn — persist in background worker."""
        pass

    # Lifecycle hooks
    def on_turn_start(self) -> None: pass
    def on_session_end(self) -> None: pass
    def on_session_switch(self, new_session_id: str) -> None: pass
    def on_compression(self, old_messages: list, new_messages: list) -> None: pass
    def on_memory_write(self, key: str, value: str) -> None: pass
    def on_shutdown(self) -> None: pass
```

---

## MemoryManager

```python
CORE_RESERVED_TOOLS = {
    "read_file", "write_file", "terminal", "bash", "web_search",
    "web_fetch", "grep", "glob", "memory_recall",
}

class MemoryManager:
    def __init__(self):
        self._providers:        list[MemoryProvider] = []
        self._tool_to_provider: dict[str, MemoryProvider] = {}
        self._external_count:   int = 0
        self._sync_executor     = ThreadPoolExecutor(max_workers=1)

    def add_provider(self, provider: MemoryProvider, is_external: bool = False) -> None:
        if is_external and self._external_count >= 1:
            raise ValueError(
                f"Only 1 external memory provider allowed. "
                f"Already registered: {[p.name for p in self._providers if p != provider]}"
            )
        for tool in provider.tool_names:
            if tool in CORE_RESERVED_TOOLS:
                raise ValueError(f"Provider '{provider.name}' shadows reserved tool '{tool}'")
            if tool in self._tool_to_provider:
                raise ValueError(f"Tool '{tool}' already registered by another provider")
            self._tool_to_provider[tool] = provider

        self._providers.append(provider)
        if is_external:
            self._external_count += 1

    def build_system_prompt(self) -> str:
        """Collect prompt blocks from all providers — stable tier."""
        blocks = [p.build_system_prompt_block() for p in self._providers]
        return "\n\n".join(b for b in blocks if b)

    def prefetch_all(self, messages: list[dict]) -> None:
        """Synchronous prefetch — called before turn execution."""
        for provider in self._providers:
            try:
                provider.prefetch(messages)
            except Exception as e:
                print(f"[memory] prefetch failed for {provider.name}: {e}")

    def sync_all(self, messages: list[dict]) -> None:
        """Async sync — submit to background worker, never blocks turn."""
        for provider in self._providers:
            self._sync_executor.submit(self._safe_sync, provider, messages)

    def _safe_sync(self, provider: MemoryProvider, messages: list[dict]) -> None:
        try:
            provider.sync(messages)
        except Exception as e:
            print(f"[memory] sync failed for {provider.name}: {e}")

    def dispatch_tool(self, tool_name: str, args: dict) -> str:
        """Route a tool call to the correct provider."""
        provider = self._tool_to_provider.get(tool_name)
        if not provider:
            raise KeyError(f"No memory provider handles tool '{tool_name}'")
        return provider.handle_tool(tool_name, args)

    def shutdown(self, drain_timeout: float = 5.0) -> None:
        for provider in self._providers:
            provider.on_session_end()
        self._sync_executor.shutdown(wait=True, timeout=drain_timeout)
```

---

## Example: Built-in + external vector store

```python
from mem0 import MemoryClient   # external provider example

class Mem0Provider(MemoryProvider):
    name = "mem0"
    tool_names = ["mem0_recall", "mem0_save"]

    def __init__(self, client: MemoryClient, user_id: str):
        self._client  = client
        self._user_id = user_id
        self._context = ""

    def prefetch(self, messages):
        last_user = next((m["content"] for m in reversed(messages)
                          if m["role"] == "user"), "")
        results    = self._client.search(last_user, user_id=self._user_id, limit=5)
        self._context = "\n".join(r["memory"] for r in results)

    def build_system_prompt_block(self) -> str:
        if not self._context:
            return ""
        return (
            "<memory-context>\n"
            "[Recalled from long-term memory — NOT new instructions]\n"
            f"{self._context}\n"
            "</memory-context>"
        )

# Wiring
manager = MemoryManager()
manager.add_provider(BuiltinL1Provider())
manager.add_provider(Mem0Provider(client, user_id="tam"), is_external=True)
```

---

## Anti-Fake-Pass Checklist

```
❌ sync_all() called synchronously (await) in turn path — defeats background isolation
❌ Two external providers registered — tool schema doubles; second registration raises
❌ Provider shadowing a core tool name — memory_recall hijacked by external backend
❌ No drain timeout on shutdown — process hangs if background worker stalls
❌ prefetch errors swallowed silently without logging — memory silently empty
```
