1452c81941
Introduces MemoryProvider ABC, MemoryManager orchestrator, and BuiltinMemoryProvider for the existing MEMORY.md/USER.md system. Key design decisions: - Built-in memory is ALWAYS active, never disabled by external providers - Multiple providers can be active simultaneously - Prefetch results from all providers are merged per-turn - Sync fans out to all providers after each turn - Each provider can expose its own tools Three registration paths: 1. Built-in (BuiltinMemoryProvider) — always first, not removable 2. First-party (Honcho stays as-is for now, migration in follow-up) 3. Plugin — ctx.register_memory_provider() in plugin system Files: - agent/memory_provider.py — ABC with core + optional lifecycle hooks - agent/memory_manager.py — orchestrator, single integration point - agent/builtin_memory_provider.py — wraps existing MemoryStore - hermes_cli/plugins.py — register_memory_provider() + accessor - run_agent.py — MemoryManager wired alongside existing Honcho code - tests/agent/test_memory_provider.py — 37 tests This establishes the interface for all pending memory backend PRs (#1811 Hindsight, #2732 RetainDB, #2933 Mem0, #3499 Byterover, #3369 OpenViking, #2351 Holographic, #727 Cognitive) to implement as plugins rather than one-off integrations.
154 lines
5.8 KiB
Python
154 lines
5.8 KiB
Python
"""Abstract base class for pluggable memory providers.
|
|
|
|
Memory providers give the agent persistent recall across sessions. Multiple
|
|
providers can be active simultaneously — the MemoryManager orchestrates them.
|
|
|
|
Built-in memory (MEMORY.md / USER.md) is always active as the first provider.
|
|
External providers (Honcho, Hindsight, Mem0, etc.) are additive — they never
|
|
disable the built-in store.
|
|
|
|
Three registration paths:
|
|
1. Built-in: BuiltinMemoryProvider — always present, not removable.
|
|
2. First-party: Ship with the repo, activated by config (e.g. Honcho).
|
|
3. Plugin: External packages register via ctx.register_memory_provider().
|
|
|
|
Lifecycle (called by MemoryManager, wired in run_agent.py):
|
|
initialize() — connect, create resources, warm up
|
|
system_prompt_block() — static text for the system prompt
|
|
prefetch(query) — background recall before each turn
|
|
sync_turn(user, asst) — async write after each turn
|
|
get_tool_schemas() — tool schemas to expose to the model
|
|
handle_tool_call() — dispatch a tool call
|
|
shutdown() — clean exit
|
|
|
|
Optional hooks (override to opt in):
|
|
on_turn_start(turn, message) — per-turn tick (scope cooling, etc.)
|
|
on_session_end(messages) — end-of-session extraction
|
|
on_pre_compress(messages) — extract before context compression
|
|
on_memory_write(action, target, content) — mirror built-in memory writes
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from abc import ABC, abstractmethod
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class MemoryProvider(ABC):
|
|
"""Abstract base class for memory providers."""
|
|
|
|
@property
|
|
@abstractmethod
|
|
def name(self) -> str:
|
|
"""Short identifier for this provider (e.g. 'builtin', 'honcho', 'hindsight')."""
|
|
|
|
# -- Core lifecycle (implement these) ------------------------------------
|
|
|
|
@abstractmethod
|
|
def is_available(self) -> bool:
|
|
"""Return True if this provider is configured, has credentials, and is ready.
|
|
|
|
Called during agent init to decide whether to activate the provider.
|
|
Should not make network calls — just check config and installed deps.
|
|
"""
|
|
|
|
@abstractmethod
|
|
def initialize(self, session_id: str, **kwargs) -> None:
|
|
"""Initialize for a session.
|
|
|
|
Called once at agent startup. May create resources (banks, tables),
|
|
establish connections, start background threads, etc.
|
|
|
|
kwargs may include: platform, model, user_id, and other session context.
|
|
"""
|
|
|
|
def system_prompt_block(self) -> str:
|
|
"""Return text to include in the system prompt.
|
|
|
|
Called during system prompt assembly. Return empty string to skip.
|
|
This is for STATIC provider info (instructions, status). Prefetched
|
|
recall context is injected separately via prefetch().
|
|
"""
|
|
return ""
|
|
|
|
def prefetch(self, query: str) -> str:
|
|
"""Recall relevant context for the upcoming turn.
|
|
|
|
Called before each API call. Return formatted text to inject as
|
|
context, or empty string if nothing relevant. Implementations
|
|
should be fast — use background threads for the actual recall
|
|
and return cached results here.
|
|
"""
|
|
return ""
|
|
|
|
def queue_prefetch(self, query: str) -> None:
|
|
"""Queue a background recall for the NEXT turn.
|
|
|
|
Called after each turn completes. The result will be consumed
|
|
by prefetch() on the next turn. Default is no-op — providers
|
|
that do background prefetching should override this.
|
|
"""
|
|
|
|
def sync_turn(self, user_content: str, assistant_content: str) -> None:
|
|
"""Persist a completed turn to the backend.
|
|
|
|
Called after each turn. Should be non-blocking — queue for
|
|
background processing if the backend has latency.
|
|
"""
|
|
|
|
@abstractmethod
|
|
def get_tool_schemas(self) -> List[Dict[str, Any]]:
|
|
"""Return tool schemas this provider exposes.
|
|
|
|
Each schema follows the OpenAI function calling format:
|
|
{"name": "...", "description": "...", "parameters": {...}}
|
|
|
|
Return empty list if this provider has no tools (context-only).
|
|
"""
|
|
|
|
def handle_tool_call(self, tool_name: str, args: Dict[str, Any], **kwargs) -> str:
|
|
"""Handle a tool call for one of this provider's tools.
|
|
|
|
Must return a JSON string (the tool result).
|
|
Only called for tool names returned by get_tool_schemas().
|
|
"""
|
|
raise NotImplementedError(f"Provider {self.name} does not handle tool {tool_name}")
|
|
|
|
def shutdown(self) -> None:
|
|
"""Clean shutdown — flush queues, close connections."""
|
|
|
|
# -- Optional hooks (override to opt in) ---------------------------------
|
|
|
|
def on_turn_start(self, turn_number: int, message: str) -> None:
|
|
"""Called at the start of each turn with the user message.
|
|
|
|
Use for turn-counting, scope management, periodic maintenance.
|
|
"""
|
|
|
|
def on_session_end(self, messages: List[Dict[str, Any]]) -> None:
|
|
"""Called when a session ends (explicit exit or timeout).
|
|
|
|
Use for end-of-session fact extraction, summarization, etc.
|
|
messages is the full conversation history.
|
|
"""
|
|
|
|
def on_pre_compress(self, messages: List[Dict[str, Any]]) -> None:
|
|
"""Called before context compression discards old messages.
|
|
|
|
Use to extract insights from messages about to be compressed.
|
|
messages is the list that will be summarized/discarded.
|
|
"""
|
|
|
|
def on_memory_write(self, action: str, target: str, content: str) -> None:
|
|
"""Called when the built-in memory tool writes an entry.
|
|
|
|
action: 'add', 'replace', or 'remove'
|
|
target: 'memory' or 'user'
|
|
content: the entry content
|
|
|
|
Use to mirror built-in memory writes to your backend.
|
|
"""
|