Создание плагина провайдера памяти

Плагины провайдеров памяти предоставляют Hermes Agent постоянное, межсессионное знание, выходящее за рамки встроенных MEMORY.md и USER.md. Это руководство описывает, как создать такой плагин.

Провайдеры памяти — один из двух типов **провайдерских плагинов**. Второй — это [Плагины контекстного движка](/docs/developer-guide/context-engine-plugin), которые заменяют встроенный компрессор контекста. Оба следуют одному шаблону: одиночный выбор, управление через конфиг, администрирование через `hermes plugins`.

Структура каталога

Каждый провайдер памяти располагается в plugins/memory/<name>/:

plugins/memory/my-provider/
├── __init__.py      # Реализация MemoryProvider + точка входа register()
├── plugin.yaml      # Метаданные (имя, описание, хуки)
└── README.md        # Инструкции по установке, справочник конфигурации, инструменты

Абстрактный базовый класс MemoryProvider

Ваш плагин реализует абстрактный базовый класс MemoryProvider из agent/memory_provider.py:

from agent.memory_provider import MemoryProvider

class MyMemoryProvider(MemoryProvider):
    @property
    def name(self) -> str:
        return "my-provider"

    def is_available(self) -> bool:
        """Проверяет, может ли этот провайдер активироваться. Без сетевых вызовов."""
        return bool(os.environ.get("MY_API_KEY"))

    def initialize(self, session_id: str, **kwargs) -> None:
        """Вызывается один раз при запуске агента.

        kwargs всегда содержит:
          hermes_home (str): Путь к активному HERMES_HOME. Используйте для хранения.
        """
        self._api_key = os.environ.get("MY_API_KEY", "")
        self._session_id = session_id

    # ... реализация остальных методов

Обязательные методы

Базовый жизненный цикл

Метод Когда вызывается Обязателен?
name (свойство) Всегда Да
is_available() Инициализация агента, перед активацией Да — без сетевых вызовов
initialize(session_id, **kwargs) Запуск агента Да
get_tool_schemas() После инициализации, для внедрения инструментов Да
handle_tool_call(name, args) Когда агент использует ваши инструменты Да (если есть инструменты)

Конфигурация

Метод Назначение Обязателен?
get_config_schema() Объявляет поля конфигурации для hermes memory setup Да
save_config(values, hermes_home) Записывает несекретную конфигурацию в нативное расположение Да (если не только env-var)

Опциональные хуки

Метод Когда вызывается Назначение
system_prompt_block() Сборка системного промпта Статическая информация о провайдере
prefetch(query) Перед каждым API-вызовом Возврат восстановленного контекста
queue_prefetch(query) После каждого хода Предварительная загрузка для следующего хода
sync_turn(user, assistant) После каждого завершённого хода Сохранение диалога
on_session_end(messages) Окончание диалога Финальное извлечение/сброс
on_pre_compress(messages) Перед сжатием контекста Сохранение инсайтов до удаления
on_memory_write(action, target, content) Встроенные записи памяти Зеркалирование в ваш бэкенд
shutdown() Завершение процесса Очистка соединений

Схема конфигурации

get_config_schema() возвращает список описателей полей, используемых hermes memory setup:

def get_config_schema(self):
    return [
        {
            "key": "api_key",
            "description": "API-ключ My Provider",
            "secret": True,           # → записывается в .env
            "required": True,
            "env_var": "MY_API_KEY",   # явное имя переменной окружения
            "url": "https://my-provider.com/keys",  # где получить ключ
        },
        {
            "key": "region",
            "description": "Регион сервера",
            "default": "us-east",
            "choices": ["us-east", "eu-west", "ap-south"],
        },
        {
            "key": "project",
            "description": "Идентификатор проекта",
            "default": "hermes",
        },
    ]

Поля с secret: True и env_var попадают в .env. Несекретные поля передаются в save_config().

tip Минимальная vs Полная схема Каждое поле в get_config_schema() запрашивается во время hermes memory setup. Провайдерам с множеством опций следует держать схему минимальной — включать только те поля, которые пользователь обязан настроить (API-ключ, обязательные учетные данные). Дополнительные настройки документируйте в файле конфигурации (например, $HERMES_HOME/myprovider.json), а не запрашивайте их все во время установки. Это сохраняет скорость мастера установки, поддерживая расширенную конфигурацию. Смотрите пример провайдера Supermemory — он запрашивает только API-ключ; все остальные опции хранятся в supermemory.json.

Сохранение конфигурации

def save_config(self, values: dict, hermes_home: str) -> None:
    """Записывает несекретную конфигурацию в ваше нативное расположение."""
    import json
    from pathlib import Path
    config_path = Path(hermes_home) / "my-provider.json"
    config_path.write_text(json.dumps(values, indent=2))

Для провайдеров, использующих только переменные окружения, оставьте реализацию по умолчанию (no-op).

Точка входа плагина

def register(ctx) -> None:
    """Вызывается системой обнаружения плагинов памяти."""
    ctx.register_memory_provider(MyMemoryProvider())

plugin.yaml

name: my-provider
version: 1.0.0
description: "Краткое описание того, что делает этот провайдер."
hooks:
  - on_session_end    # перечислите реализованные хуки

Контракт многопоточности

sync_turn() ДОЛЖЕН быть неблокирующим. Если ваш бэкенд имеет задержки (API-вызовы, обработка LLM), выполняйте работу в фоновом потоке (daemon thread):

def sync_turn(self, user_content, assistant_content):
    def _sync():
        try:
            self._api.ingest(user_content, assistant_content)
        except Exception as e:
            logger.warning("Sync failed: %s", e)

    if self._sync_thread and self._sync_thread.is_alive():
        self._sync_thread.join(timeout=5.0)
    self._sync_thread = threading.Thread(target=_sync, daemon=True)
    self._sync_thread.start()

Изоляция профилей

Все пути хранения обязаны использовать аргумент hermes_home из initialize(), а не жёстко заданный ~/.hermes:

# ПРАВИЛЬНО — в рамках профиля
from hermes_constants import get_hermes_home
data_dir = get_hermes_home() / "my-provider"

# НЕПРАВИЛЬНО — общий для всех профилей
data_dir = Path("~/.hermes/my-provider").expanduser()

Тестирование

Смотрите tests/agent/test_memory_plugin_e2e.py для полного шаблона E2E-тестирования с использованием реального SQLite-провайдера.

from agent.memory_manager import MemoryManager

mgr = MemoryManager()
mgr.add_provider(my_provider)
mgr.initialize_all(session_id="test-1", platform="cli")

# Тестирование маршрутизации инструментов
result = mgr.handle_tool_call("my_tool", {"action": "add", "content": "test"})

# Тестирование жизненного цикла
mgr.sync_all("user msg", "assistant msg")
mgr.on_session_end([])
mgr.shutdown_all()

Добавление CLI-команд

Плагины провайдеров памяти могут регистрировать собственное дерево CLI-подкоманд (например, hermes my-provider status, hermes my-provider config). Это использует конвенционную систему обнаружения — без изменений в основных файлах.

Как это работает

  1. Добавьте файл cli.py в ваш каталог плагина

  2. Определите функцию register_cli(subparser), которая строит дерево argparse

  3. Система плагинов памяти обнаруживает его при запуске через discover_plugin_cli_commands()

  4. Ваши команды появляются как hermes <provider-name> <subcommand>

Гейтинг по активному провайдеру: Ваши CLI-команды отображаются только когда ваш провайдер является активным memory.provider в конфиге. Если пользователь не настроил ваш провайдер, ваши команды не покажутся в hermes --help.

Пример

# plugins/memory/my-provider/cli.py

def my_command(args):
    """Обработчик, диспетчеризуемый argparse."""
    sub = getattr(args, "my_command", None)
    if sub == "status":
        print("Провайдер активен и подключён.")
    elif sub == "config":
        print("Показываем конфигурацию...")
    else:
        print("Использование: hermes my-provider <status|config>")

def register_cli(subparser) -> None:
    """Строит дерево argparse для hermes my-provider.

    Вызывается discover_plugin_cli_commands() во время настройки argparse.
    """
    subs = subparser.add_subparsers(dest="my_command")
    subs.add_parser("status", help="Показать статус провайдера")
    subs.add_parser("config", help="Показать конфигурацию провайдера")
    subparser.set_defaults(func=my_command)

Референсная реализация

Смотрите plugins/memory/honcho/cli.py для полного примера с 13 подкомандами, кросс-профильным управлением (--target-profile) и чтением/записью конфигурации.

Структура каталога с CLI

plugins/memory/my-provider/
├── __init__.py      # Реализация MemoryProvider + register()
├── plugin.yaml      # Метаданные
├── cli.py           # register_cli(subparser)  CLI команды
└── README.md        # Инструкции по установке

Правило одного провайдера

Только один внешний провайдер памяти может быть активен одновременно. Если пользователь попытается зарегистрировать второй, MemoryManager отклонит его с предупреждением. Это предотвращает раздувание схем инструментов и конфликты бэкендов.