Разработка плагина провайдера памяти
Плагины провайдера памяти дают 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 setup | get_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().
Каждое поле в 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). Работает это через систему обнаружения по соглашению — править файлы ядра не нужно.
Как это работает
- Добавьте файл
cli.pyв директорию вашего плагина - Определите функцию
register_cli(subparser), которая строит дерево argparse - Система плагинов памяти обнаружит его при запуске через
discover_plugin_cli_commands() - Ваши команды появятся под
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). Разрешение зависимостей не отключает автоматически другие плагины и не запускает бисекцию. Явные обновления плагинов, удаление и независимые гейты безопасности — отдельные операции.