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

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

совет

Провайдеры памяти — один из двух типов плагинов-провайдеров. Второй тип — плагины движка контекста, которые заменяют встроенный компрессор контекста. Оба следуют одной схеме: один активный провайдер, конфигурация через файл настроек, управление через hermes plugins.

Способы установки

Hermes ищет провайдеры памяти в четырёх источниках — в таком порядке приоритета:

ИсточникРасположениеПримечание
Встроенныйplugins/memory/<name>/Поставляется с Hermes. Закрыт для новых провайдеров — см. CONTRIBUTING.
Пользовательский$HERMES_HOME/plugins/<name>/Добавляется пользователем, отдельно для каждого профиля.
Проектный./.hermes/plugins/<name>/Включается через HERMES_ENABLE_PROJECT_PLUGINS=1.
Пакетныйточка входа hermes_agent.memory_providersДистрибутив поставляет владелец установки; копировать ничего не нужно.

При совпадении имён побеждает источник, стоящий выше в списке, — поэтому директория, добавленная в рабочее дерево, никогда не перекроет встроенный провайдер.

заметка

Это обратный порядок по сравнению с общей системой плагинов, где побеждает более поздний источник. Провайдер памяти активируется по имени (memory.provider), поэтому перекрытие тихо перенаправило бы память агента, а не просто переопределило инструмент.

Обнаружение только перечисляет провайдеров — оно никогда их не импортирует. Ничего не выполняется, пока провайдера не назовут в memory.provider.

Обнаружение через точки входа пакеты не устанавливает. Не внедряйте провайдер в выбранное окружение Hermes через pip. На установках под управлением PM поставляйте провайдер-директорию с объявленными Python-зависимостями: приём плагина и hermes memory setup готовят их через PM перед использованием. Сборки, которыми управляет владелец (например, Nix), могут декларативно включить дистрибутив с точкой входа.

Настройка через CLI и через дашборд использует общую подготовку кандидата. PM добавляет pyproject.toml провайдера или устаревшие pip_dependencies / python_dependencies к объединению зависимостей активных плагинов; импортируемый модуль не отменяет объявленных ограничений версий. Проверка готовности в дашборде смотрит на те же входные данные, ничего не устанавливая. После успешной подготовки может понадобиться перезапуск Hermes, прежде чем работающий процесс сможет использовать выбранное поколение зависимостей. Внешние проверки sidecar и команды настройки остаются отдельно от Python-объединения.

Провайдер-директория

Провайдер-директория размещается в plugins/memory/<name>/, если он встроен в Hermes, в $HERMES_HOME/plugins/<name>/, если установлен пользователем, или в ./.hermes/plugins/<name>/ для проектного варианта:

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

Провайдер-пакет

Провайдер, устанавливаемый через pip, публикует точку входа в группе hermes_agent.memory_providers. Имя точки входа — это имя провайдера, которое пользователи указывают в memory.provider; её значение указывает на функцию register(ctx) провайдера:

[project.entry-points."hermes_agent.memory_providers"]
my-provider = "my_provider:register"

Указывайте точку входа на пакет или на register(ctx) внутри него, а реализацию, навыки и прочие ресурсы держите в обычной структуре Python-пакета. Копировать что-либо в $HERMES_HOME/plugins/ не требуется.

Пакетная точка входа получает всё то же, что и установка через директорию, включая два файла, которые Hermes читает с диска, а не импортирует, — config_schema.py (панель конфигурации в дашборде) и cli.py (ваши подкоманды hermes <provider>). Оба ищутся рядом с __init__.py пакета, поэтому если вы поставляете хотя бы один из них, указывайте точку входа на пакет, а не на отдельный модуль.

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

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

from agent.memory_provider import MemoryProvider
from agent.secret_scope import get_secret

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

    def is_available(self) -> bool:
        """Проверить, может ли провайдер активироваться. Сетевые вызовы ЗАПРЕЩЕНЫ."""
        return bool(get_secret("MY_API_KEY"))

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

        kwargs всегда содержит:
          hermes_home (str): Активный путь HERMES_HOME. Используй для хранения данных.
        """
        self._api_key = get_secret("MY_API_KEY") or ""
        self._session_id = session_id

    # ... реализуй оставшиеся методы

Контекст инициализации

AIAgent передаёт контекст сессии через MemoryManager.initialize_all() в initialize(session_id, **kwargs). Принимайте **kwargs и терпимо относитесь к отсутствию необязательных полей: вызывающая сторона может инициализировать провайдера без агента или без базы сессий.

КлючЗначение
hermes_homeКаталог хранения активного профиля.
platformПоверхность сессии, например cli, gui, acp или telegram.
session_titleСохранённый заголовок сессии, если есть. Отображаемая подпись — не обязательно идентичность, выбранная пользователем.
session_title_sourceСохранённое происхождение заголовка, если есть: derived, llm или user. Автоматические источники нельзя принимать за явные переопределения идентичности; отсутствие происхождения сохраняет прежнее поведение провайдера. Общие константы лежат в hermes_state_common.py.
cwdНепустое логическое рабочее пространство, переданное как AIAgent(cwd=...); доступно до инициализации провайдера. Опускается при None или пустой строке.
gateway_session_keyСтабильная идентичность чата в мессенджере для изоляции сессий по чатам.
user_id, user_id_alt, user_name, chat_idПоля идентичности из шлюза; включаются, когда есть.
agent_identityИмя активного профиля, если есть.
agent_workspace, agent_contextОбласть выполнения агента. agent_workspace равен hermes; agent_context — cron для запусков планировщика, subagent для дочерних агентов delegate_task, иначе primary; при значениях, отличных от primary, автоматическую запись пропускайте.

Не считайте, что os.getcwd() определяет рабочее пространство разговора: один бэкенд десктопа или шлюза может обслуживать несколько сессий. Если cwd нет, а маршрутизация по каталогу нужна, agent.runtime_cwd.resolve_agent_cwd() учитывает сначала контекст cwd сессии, затем terminal.cwd в его области (внутри передаётся как TERMINAL_CWD), затем каталог запуска. Метаданные рабочего пространства, заданные при создании, не требуют ни смены cwd процесса, ни пересборки системного промпта уже идущего разговора.

Смена рабочего пространства в десктопе/TUI синхронизирует session_cwd живого агента через tui_gateway/session_workdir.py::_register_session_cwd, в том числе когда отложенный агент подключается уже после переезда рабочего пространства. Благодаря этому первая или перезапущенная сессия Codex app-server использует текущее рабочее пространство, а не cwd на момент создания. Уже работающий поток Codex это не переносит, провайдеров памяти заново не инициализирует, идентичность существующей сессии Honcho не меняет и закэшированный системный промпт не сбрасывает. Инициализация провайдера по-прежнему получает рабочее пространство на момент создания; отсутствующий или пустой cwd остаётся незакреплённым.

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

Основной жизненный цикл

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

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

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

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

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

Для нативных replace и remove в metadata["previous_content"] лежит полная запись, выбранная под блокировкой нативного хранилища. Уведомления отправляются только после того, как вся запись или весь пакет прошли успешно. Уведомления пакета сохраняют порядок операций; предыдущее содержимое каждой операции учитывает более ранние операции того же пакета. old_text — это строка поиска вызывающего кода, а не идентичность изменённой записи. Старые версии Hermes могут не передавать previous_content. Провайдерам, которым нужна точная идентичность, при его отсутствии стоит пропускать деструктивное зеркалирование.

Слишком большие результаты prefetch

Внешние результаты prefetch() по умолчанию возвращаются целиком: так сохраняется ранжированная по релевантности выдача провайдера. Предел безопасности — 10× hooks.output_spill.max_chars (не меньше 100 000 символов); то, что выше, всё равно разливается в файл. Держите собственный бюджет выдачи заметно ниже. Чтобы включить разлив слишком больших результатов для активного профиля, задайте:

memory:
  prefetch_spill_enabled: true  # default: false

Когда опция включена, результаты выше общего порога hooks.output_spill.max_chars (по умолчанию 10 000 символов) пишутся в приватный файл разлива, а вместо них подставляется превью из начала и конца плюс путь к файлу. В превью по умолчанию остаются только первые и последние 500 символов; чтобы восстановить середину, модели нужно прочитать файл. Результаты, которые укладываются в порог, остаются без изменений.

Длины превью и каталог из общих настроек hooks.output_spill по-прежнему действуют, а hooks.output_spill.enabled: false отключает разлив, даже если включена опция памяти. Обе настройки фиксируются в момент регистрации внешнего провайдера; чтобы применить изменения к существующим сессиям, перезапустите Hermes. Встроенная память и обычный разлив через хуки плагинов не затрагиваются. См. Плагины — разлив контекста при превышении размера.

Контрольные точки перед сжатием (fail-closed)

По умолчанию on_pre_compress() работает в режиме best-effort: если провайдер выбрасывает исключение, хост логирует сбой и сжатие продолжается как ни в чём не бывало. Для извлечения инсайтов это правильное поведение — а вот для провайдера, чья задача архивировать данные транскрипта в надёжное хранилище до деструктивной перезаписи, оно не годится. Для такого случая хост предлагает опциональный контракт контрольных точек (API v2):

from agent.memory_provider import MemoryProvider

class MyArchivingProvider(MemoryProvider):
    # Подключение по желанию: каждый успешный возврат из on_pre_compress()
    # означает, что надёжная контрольная точка зафиксирована. При любом сбое —
    # выбрасывай исключение, не возвращай частичный успех. Версия 1
    # (наследуемая по умолчанию) — это неявный исторический контракт:
    # семантика best-effort, «сырой» список сообщений.
    pre_compress_checkpoint_api_version = 2

    def on_pre_compress(self, messages, *, require_checkpoint=False):
        # require_checkpoint отражает настройку оператора checkpoint_required:
        # True означает, что исключение здесь заблокирует деструктивную перезапись.
        ids = self._archive(messages)   # должно быть надёжно сохранено до возврата
        return f"checkpoint: {ids}"     # передаётся дальше в промпт для сводки

Операторы включают принудительный режим для конкретного развёртывания:

compression:
  checkpoint_required: true   # по умолчанию: false

При включённом гейте сжатие блокируется (fail-closed) перед любой деструктивной перезаписью, если только активный провайдер, заявивший поддержку этого API, не завершил свою контрольную точку: несжатый транскрипт сохраняется, попытка компакции завершается ошибкой BLOCKED_MISSING_PREREQUISITE, и её можно повторить, когда хранилище восстановится. При выключенном гейте (по умолчанию) для существующих провайдеров ничего не меняется.

Ни один из провайдеров, встроенных в Hermes, не заявляет API контрольных точек версии 2 — контракт включается по желанию и существует для сторонних архивирующих провайдеров. Поэтому checkpoint_required без такого провайдера блокирует каждую попытку сжатия (и ручную, и автоматическую): при инициализации агента в лог пишется предупреждение с именем активного провайдера, а каждый отказ называет compression.checkpoint_required как ключ, который нужно отключить.

Гейт распространяется на все механизмы компакции, а не только на встроенный саммаризатор Hermes: серверная нативная компакция (compression.codex_responses_native) подавляется, пока гейт активен, пост-ходовая микрокомпакция (compression.micro_compact) принудительно отключается при инициализации агента (она поглощает старые обмены сообщениями в скользящую сводку без хука контрольной точки на своём пути), а режим API codex_app_server отклоняется при инициализации агента — codex-агент компактирует собственный тред без достоверной границы перед компакцией, так что обязательную контрольную точку там гарантировать нельзя. Единственным механизмом с деструктивной перезаписью остаётся компрессор Hermes, знающий о контрольных точках.

То, что получает провайдер, зависит от заявленной версии API. Провайдеры версии 1 (неявное значение по умолчанию — все ранее существовавшие провайдеры) сохраняют исторический контракт: «сырой» список сообщений, как и раньше. Провайдеры версии 2 с контрольными точками получают вместо этого нормализованные прямые данные: только текстовые строки пользователя и ассистента. Результаты вызовов инструментов, системные сообщения, содержимое tool_calls в сообщениях ассистента (сам текст при этом сохраняется) и предыдущие сводки компакции отфильтровываются на стороне хоста. Предыдущие сводки распознаются по постоянному маркеру сообщения _compressed_summary, который переживает перезапуски процесса, — так возобновлённая сессия никогда не скармливает производные сводки обратно в архив.

Контрольные точки обязаны быть идемпотентными. После блокировки fail-closed следующая попытка компакции снова вызовет on_pre_compress() с тем же транскриптом — а транскрипт, выросший лишь немного, даст в основном пересекающиеся данные. Используйте содержимое (например, дайджест транскрипта) как ключ записи в архив и делайте upsert — тогда повторы и пересечения дедуплицируются, а не накапливаются дублирующимися архивными записями.

Тесты контракта: tests/agent/test_pre_compress_checkpoint_contract.py.

Настройка — что сохраняет автономный провайдер

Каждую поверхность настройки, которую Hermes даёт встроенному провайдеру, питают файлы в собственной директории провайдера, поэтому провайдер, установленный из каталога плагинов, сохраняет их все:

ПоверхностьЧто поставляет провайдер
Desktop → Capabilities → Tools → Memory (панель конфигурации)config_schema.py (см. ниже)
Мастер hermes memory setupget_config_schema() объявляет поля, которые спрашивает мастер, save_config(config, hermes_home) их сохраняет, post_setup(hermes_home, config) выполняется после этого для всего интерактивного (OAuth, первая синхронизация); get_status_config() питает hermes memory status
Подкоманды hermes <provider> …cli.py с register_cli(subparser) (Добавление CLI-команд)
Python-зависимости[project] dependencies в pyproject.toml (или python_dependencies в plugin.yaml); ставятся с учётом собственных закреплений Hermes при установке и применяются заново при hermes update

Имя провайдера, секция конфигурации memory.<name>, каталог данных и имена инструментов — это контракт с существующими пользователями. Провайдер, который уходит из ядра, сохраняет все четыре; тогда Hermes автоматически ставит плагин из каталога всем, у кого memory.provider всё ещё называет его (при hermes update и один раз при старте агента, когда включён security.allow_lazy_installs).

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

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

def get_config_schema(self):
    return [
        {
            "key": "api_key",
            "description": "API-ключ провайдера",
            "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().

совет
Минимальная 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())

Провайдер может также предоставлять навыки только для чтения из того же колбэка. Навыки квалифицируются по имени точки входа и загружаются только тогда, когда этот провайдер памяти активен:

from pathlib import Path

SKILLS_DIR = Path(__file__).parent / "skills"

def register(ctx) -> None:
    ctx.register_memory_provider(MyMemoryProvider())
    ctx.register_skill(
        "maintenance",
        SKILLS_DIR / "maintenance" / "SKILL.md",
        "Обслуживание хранилища памяти провайдера",
    )

С активной точкой входа my-provider навык доступен как my-provider:maintenance через skill_view().

plugin.yaml

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

Контракт потоков выполнения

sync_turn() ДОЛЖЕН быть неблокирующим. Если у бэкенда есть задержка (API-вызовы, обработка LLM), выносите работу в поток-демон — создавайте его через agent.memory_provider.spawn_context_thread, а не голым threading.Thread. Изоляция профилей (активный HERMES_HOME, область секретов на ход) живёт в contextvars, а обычный поток стартует с пустым контекстом: при мультиплексированных профилях он молча писал бы в хранилище профиля по умолчанию, а get_secret() там закрыт по умолчанию и вернёт отказ.

from agent.memory_provider import spawn_context_thread

def sync_turn(self, user_content, assistant_content, *, session_id="", messages=None):
    def _sync():
        try:
            self._api.ingest(user_content, assistant_content, session_id=session_id, messages=messages)
        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 = spawn_context_thread(_sync, name="myprovider-sync")
    self._sync_thread.start()

То же касается потоков предвыборки и записи. Небольшие JSON-файлы конфигурации рядом ($HERMES_HOME/<provider>.json) читаются через utils.read_json_or_empty и пишутся через utils.atomic_json_write; всё, что относится к config.yaml, идёт через hermes_cli.config.save_config(..., merge_existing=True).

messages — опциональный контекст диалога в формате OpenAI на момент завершения хода. Если он передан, в него входят сообщения пользователя и ассистента, вызовы инструментов ассистентом и результаты этих вызовов. Провайдеры, которым необработанный контекст хода не нужен, могут не объявлять параметр messages — Hermes продолжит вызывать их со старой сигнатурой.

Облачные провайдеры должны документировать, какие части messages уходят за пределы устройства. Вызовы инструментов и их результаты могут содержать пути к файлам, вывод команд и другие данные рабочего пространства.

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

Все пути к данным обязаны использовать kwarg 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_provider.py и соседних файлах: tests/agent/test_memory_session_switch.py, tests/agent/test_memory_user_id.py, tests/agent/test_memory_provider_init.py.

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("Provider is active and connected.")
    elif sub == "config":
        print("Showing config...")
    else:
        print("Usage: 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="Show provider status")
    subs.add_parser("config", help="Show provider config")
    subparser.set_defaults(func=my_command)

Эталонная реализация

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

Структура директорий с CLI

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

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

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

Контракт сохранности HERMES_HOME (на что могут полагаться обёртки)

Для провайдеров-обёрток, которые держат свою среду выполнения в отдельном sidecar-venv вне Python, управляемого Hermes (без поверхности зависимостей — ни pyproject.toml, ни pip_dependencies, ни python_dependencies — в сканируемом корне плагина; pyproject.toml, принадлежащий только внешнему или вложенному sidecar, не сканируется):

  • Расположение. $HERMES_HOME/plugins/<name>/ — расположение плагинов, привязанное к профилю, а HERMES_HOME определяется сначала активным переопределением контекста, затем $HERMES_HOME, затем значением платформы по умолчанию. Передавайте HERMES_HOME при запуске обёртки или sidecar, чтобы изоляция профилей сохранялась; MemoryManager.initialize_all передаёт активный hermes_home каждому провайдеру.
  • Сохранность. Обычные обновления Hermes — включая пересборку или замену управляемого venv через pm — не удаляют и не переписывают $HERMES_HOME/plugins/**. Установленная директория обёртки и её файл-маркер (например, mnemosyne-wrapper.json) сохраняются. Явные обновления плагинов и сценарии удаления (hermes uninstall, hermes plugins remove, удаление профиля, удаление пользователем) под эту гарантию не попадают.
  • Изоляция sidecar. Корень плагина без поверхности зависимостей никогда не входит в объединение зависимостей рабочего пространства pm; пересинхронизация или пересборка venv не ставят для него зависимостей и не трогают его дерево.
  • Конфликты. Для нативных плагинов с общим venv неразрешимое объединение зависимостей падает громко: плагин-кандидат остаётся невключённым и неимпортированным (орган допуска отказывает до публикации конфигурации, сообщая идентичность плагина и причину от резолвера, с путём повторного включения/повтора и машиночитаемой квитанцией pm). Разрешение зависимостей не отключает автоматически другие плагины и не запускает бисекцию. Явные обновления плагинов, удаление и независимые гейты безопасности — отдельные операции.
перевод и редактура:

обновлено

⌕ ESC