Создание плагина Hermes

Это руководство шаг за шагом проведёт вас через создание полноценного плагина Hermes с нуля. В результате вы получите работающий плагин с несколькими инструментами, хуками жизненного цикла, встроенными файлами данных и встроенным навыком — всё, что поддерживает система плагинов.

info Не уверены, какой гайд вам нужен? У Hermes есть несколько различных подключаемых интерфейсов — одни используют Python API register_*, другие являются конфигурационными или основаны на директориях. Используйте эту карту в первую очередь:

Если вы хотите добавить… Читайте
Пользовательские инструменты, хуки, слеш-команды, навыки или подкоманды CLI Этот гайд (основная поверхность плагинов)
LLM / inference backend (новый провайдер) Плагины провайдеров моделей
Канал шлюза (Discord/Telegram/IRC/Teams и т.д.) Добавление адаптеров платформ
Бэкенд памяти (Honcho/Mem0/Supermemory и т.д.) Плагины провайдеров памяти
Движок сжатия контекста Плагины движков контекста
Бэкенд генерации изображений Плагины провайдеров генерации изображений
TTS бэкенд (любой CLI — Piper, VoxCPM, Kokoro, клонирование голоса, …) Пользовательские TTS провайдеры команд — на основе конфигурации, Python не требуется
STT бэкенд (пользовательский whisper / ASR CLI) Транскрипция голосовых сообщений — установите HERMES_LOCAL_STT_COMMAND как шаблон оболочки
Внешние инструменты через MCP (файловая система, GitHub, Linear, любой MCP сервер) MCP — объявите mcp_servers.<имя> в config.yaml
Хуки событий шлюза (срабатывают при запуске, событиях сессии, командах) Хуки событий — поместите HOOK.yaml + handler.py в ~/.hermes/hooks/<имя>/
Оболочечные хуки (выполнение команды оболочки по событиям) Оболочечные хуки — объявите в разделе hooks: в config.yaml
Дополнительные источники навыков (пользовательские GitHub репозитории, приватные индексы навыков) Навыкиhermes skills tap add <репозиторий> · Публикация пользовательского tap
Полноценный базовый inference провайдер (не плагин) Добавление провайдеров

Полная таблица подключаемых интерфейсов содержит сводку всех поверхностей расширения, включая конфигурационные (TTS, STT, MCP, оболочечные хуки) и на основе директорий (хуки шлюза).

Что вы создадите

Плагин-калькулятор с двумя инструментами:

Плюс хук, который логирует каждый вызов инструмента, и встроенный файл навыка.

Шаг 1: Создайте директорию плагина

mkdir -p ~/.hermes/plugins/calculator
cd ~/.hermes/plugins/calculator

Шаг 2: Напишите манифест

Создайте plugin.yaml:

name: calculator
version: 1.0.0
description: Math calculator — evaluate expressions and convert units
provides_tools:
  - calculate
  - unit_convert
provides_hooks:
  - post_tool_call

Это сообщает Hermes: «Я плагин с именем calculator, я предоставляю инструменты и хуки.» Поля provides_tools и provides_hooks — это списки того, что регистрирует плагин.

Необязательные поля, которые вы можете добавить:

author: Your Name
requires_env:          # блокирует загрузку при отсутствии переменных; запрашивается во время установки
  - SOME_API_KEY       # простой формат — плагин отключается, если отсутствует
  - name: OTHER_KEY    # расширенный формат — показывает описание/url во время установки
    description: "Key for the Other service"
    url: "https://other.com/keys"
    secret: true

Шаг 3: Напишите схемы инструментов

Создайте schemas.py — именно это читает LLM, чтобы решить, когда вызывать ваши инструменты:

"""Tool schemas — what the LLM sees."""

CALCULATE = {
    "name": "calculate",
    "description": (
        "Evaluate a mathematical expression and return the result. "
        "Supports arithmetic (+, -, *, /, **), functions (sqrt, sin, cos, "
        "log, abs, round, floor, ceil), and constants (pi, e). "
        "Use this for any math the user asks about."
    ),
    "parameters": {
        "type": "object",
        "properties": {
            "expression": {
                "type": "string",
                "description": "Math expression to evaluate (e.g., '2**10', 'sqrt(144)')",
            },
        },
        "required": ["expression"],
    },
}

UNIT_CONVERT = {
    "name": "unit_convert",
    "description": (
        "Convert a value between units. Supports length (m, km, mi, ft, in), "
        "weight (kg, lb, oz, g), temperature (C, F, K), data (B, KB, MB, GB, TB), "
        "and time (s, min, hr, day)."
    ),
    "parameters": {
        "type": "object",
        "properties": {
            "value": {
                "type": "number",
                "description": "The numeric value to convert",
            },
            "from_unit": {
                "type": "string",
                "description": "Source unit (e.g., 'km', 'lb', 'F', 'GB')",
            },
            "to_unit": {
                "type": "string",
                "description": "Target unit (e.g., 'mi', 'kg', 'C', 'MB')",
            },
        },
        "required": ["value", "from_unit", "to_unit"],
    },
}

Почему схемы важны: Поле description — это то, как LLM решает, когда использовать ваш инструмент. Будьте конкретны в описании того, что он делает и когда его применять. Параметры parameters определяют, какие аргументы передаёт LLM.

Шаг 4: Напишите обработчики инструментов

Создайте tools.py — это код, который фактически выполняется, когда LLM вызывает ваши инструменты:

"""Tool handlers — the code that runs when the LLM calls each tool."""

import json
import math

# Safe globals for expression evaluation — no file/network access
_SAFE_MATH = {
    "abs": abs, "round": round, "min": min, "max": max,
    "pow": pow, "sqrt": math.sqrt, "sin": math.sin, "cos": math.cos,
    "tan": math.tan, "log": math.log, "log2": math.log2, "log10": math.log10,
    "floor": math.floor, "ceil": math.ceil,
    "pi": math.pi, "e": math.e,
    "factorial": math.factorial,
}


def calculate(args: dict, **kwargs) -> str:
    """Evaluate a math expression safely.

    Rules for handlers:
    1. Receive args (dict) — the parameters the LLM passed
    2. Do the work
    3. Return a JSON string — ALWAYS, even on error
    4. Accept **kwargs for forward compatibility
    """
    expression = args.get("expression", "").strip()
    if not expression:
        return json.dumps({"error": "No expression provided"})

    try:
        result = eval(expression, {"__builtins__": {}}, _SAFE_MATH)
        return json.dumps({"expression": expression, "result": result})
    except ZeroDivisionError:
        return json.dumps({"expression": expression, "error": "Division by zero"})
    except Exception as e:
        return json.dumps({"expression": expression, "error": f"Invalid: {e}"})


# Conversion tables — values are in base units
_LENGTH = {"m": 1, "km": 1000, "mi": 1609.34, "ft": 0.3048, "in": 0.0254, "cm": 0.01}
_WEIGHT = {"kg": 1, "g": 0.001, "lb": 0.453592, "oz": 0.0283495}
_DATA = {"B": 1, "KB": 1024, "MB": 1024**2, "GB": 1024**3, "TB": 1024**4}
_TIME = {"s": 1, "ms": 0.001, "min": 60, "hr": 3600, "day": 86400}


def _convert_temp(value, from_u, to_u):
    # Normalize to Celsius
    c = {"F": (value - 32) * 5/9, "K": value - 273.15}.get(from_u, value)
    # Convert to target
    return {"F": c * 9/5 + 32, "K": c + 273.15}.get(to_u, c)


def unit_convert(args: dict, **kwargs) -> str:
    """Convert between units."""
    value = args.get("value")
    from_unit = args.get("from_unit", "").strip()
    to_unit = args.get("to_unit", "").strip()

    if value is None or not from_unit or not to_unit:
        return json.dumps({"error": "Need value, from_unit, and to_unit"})

    try:
        # Temperature
        if from_unit.upper() in {"C","F","K"} and to_unit.upper() in {"C","F","K"}:
            result = _convert_temp(float(value), from_unit.upper(), to_unit.upper())
            return json.dumps({"input": f"{value} {from_unit}", "result": round(result, 4),
                             "output": f"{round(result, 4)} {to_unit}"})

        # Ratio-based conversions
        for table in (_LENGTH, _WEIGHT, _DATA, _TIME):
            lc = {k.lower(): v for k, v in table.items()}
            if from_unit.lower() in lc and to_unit.lower() in lc:
                result = float(value) * lc[from_unit.lower()] / lc[to_unit.lower()]
                return json.dumps({"input": f"{value} {from_unit}",
                                 "result": round(result, 6),
                                 "output": f"{round(result, 6)} {to_unit}"})

        return json.dumps({"error": f"Cannot convert {from_unit}{to_unit}"})
    except Exception as e:
        return json.dumps({"error": f"Conversion failed: {e}"})

Ключевые правила для обработчиков:

  1. Сигнатура: def my_handler(args: dict, **kwargs) -> str

  2. Возврат: Всегда строка JSON. Как для успеха, так и для ошибок.

  3. Никогда не вызывайте исключения: Перехватывайте все исключения, вместо этого возвращайте JSON с ошибкой.

  4. Принимайте **kwargs: Hermes может передавать дополнительный контекст в будущем.

Шаг 5: Напишите регистрацию

Создайте __init__.py — он связывает схемы с обработчиками:

"""Calculator plugin — registration."""

import logging

from . import schemas, tools

logger = logging.getLogger(__name__)

# Track tool usage via hooks
_call_log = []

def _on_post_tool_call(tool_name, args, result, task_id, **kwargs):
    """Hook: runs after every tool call (not just ours)."""
    _call_log.append({"tool": tool_name, "session": task_id})
    if len(_call_log) > 100:
        _call_log.pop(0)
    logger.debug("Tool called: %s (session %s)", tool_name, task_id)


def register(ctx):
    """Wire schemas to handlers and register hooks."""
    ctx.register_tool(name="calculate",    toolset="calculator",
                      schema=schemas.CALCULATE,    handler=tools.calculate)
    ctx.register_tool(name="unit_convert", toolset="calculator",
                      schema=schemas.UNIT_CONVERT, handler=tools.unit_convert)

    # This hook fires for ALL tool calls, not just ours
    ctx.register_hook("post_tool_call", _on_post_tool_call)

Что делает register():

Пример dispatch_tool — слеш-команда, запускающая инструмент:

def handle_scan(ctx, argstr):
    """Implement /scan by invoking the terminal tool through the registry."""
    result = ctx.dispatch_tool("terminal", {"command": f"find . -name '{argstr}'"})
    return result  # returned to the caller's chat UI

def register(ctx):
    ctx.register_command("scan", handle_scan, help="Find files matching a glob")

Вызванный инструмент проходит через обычные конвейеры одобрения, редактирования и бюджета — это реальный вызов инструмента, а не обходной путь.

Шаг 6: Протестируйте

Запустите Hermes:

hermes

Вы должны увидеть calculator: calculate, unit_convert в списке инструментов баннера.

Попробуйте следующие запросы:

What's 2 to the power of 16?
Convert 100 fahrenheit to celsius
What's the square root of 2 times pi?
How many gigabytes is 1.5 terabytes?

Проверьте статус плагина:

/plugins

Вывод:

Plugins (1):
   calculator v1.0.0 (2 tools, 1 hooks)

Отладка обнаружения плагинов

Если ваш плагин не отображается — или отображается, но не загружается — установите HERMES_PLUGINS_DEBUG=1, чтобы получить подробные логи обнаружения в stderr:

HERMES_PLUGINS_DEBUG=1 hermes plugins list

Вы увидите для каждого источника плагинов (встроенные, пользовательские, проектные, entry-points):

Те же логи всегда записываются в ~/.hermes/logs/agent.log на уровне WARNING (только ошибки) и DEBUG (всё) при установленной переменной окружения. Если вы не можете запустить с переменной окружения (например, изнутри шлюза), просматривайте файл лога вместо этого:

hermes logs --level WARNING | grep -i plugin

Распространённые причины, почему плагин не появляется:

Финальная структура вашего плагина

~/.hermes/plugins/calculator/
├── plugin.yaml      # "Я calculator, я предоставляю инструменты и хуки"
├── __init__.py      # Связывание: схемы → обработчики, регистрация хуков
├── schemas.py       # Что читает LLM (описания + спецификации параметров)
└── tools.py         # Что выполняется (функции calculate, unit_convert)

Четыре файла, чёткое разделение:

Что ещё могут делать плагины?

Встраивание файлов данных

Поместите любые файлы в директорию вашего плагина и читайте их во время импорта:

# In tools.py or __init__.py
from pathlib import Path

_PLUGIN_DIR = Path(__file__).parent
_DATA_FILE = _PLUGIN_DIR / "data" / "languages.yaml"

with open(_DATA_FILE) as f:
    _DATA = yaml.safe_load(f)

Встраивание навыков

Плагины могут содержать файлы навыков, которые агент загружает через skill_view(\"plugin:skill\"). Зарегистрируйте их в вашем __init__.py:

~/.hermes/plugins/my-plugin/
├── __init__.py
├── plugin.yaml
└── skills/
    ├── my-workflow/
    │   └── SKILL.md
    └── my-checklist/
        └── SKILL.md
from pathlib import Path

def register(ctx):
    skills_dir = Path(__file__).parent / "skills"
    for child in sorted(skills_dir.iterdir()):
        skill_md = child / "SKILL.md"
        if child.is_dir() and skill_md.exists():
            ctx.register_skill(child.name, skill_md)

Теперь агент может загружать ваши навыки по их именованному имени:

skill_view("my-plugin:my-workflow")   # → версия плагина
skill_view("my-workflow")              # → встроенная версия (без изменений)

Ключевые свойства:

tip Устаревший паттерн Старый паттерн shutil.copy2 (копирование навыка в ~/.hermes/skills/) всё ещё работает, но создаёт риск коллизии имён со встроенными навыками. Для новых плагинов предпочитайте ctx.register_skill().

Блокировка по переменным окружения

Если вашему плагину нужен API ключ:

# plugin.yaml — простой формат (обратная совместимость)
requires_env:
  - WEATHER_API_KEY

Если WEATHER_API_KEY не установлен, плагин отключается с понятным сообщением. Никакого краха, никакой ошибки в агенте — просто «Plugin weather disabled (missing: WEATHER_API_KEY)».

Когда пользователи запускают hermes plugins install, они интерактивно запрашиваются для отсутствующих переменных requires_env. Значения автоматически сохраняются в .env.

Для лучшего опыта установки используйте расширенный формат с описаниями и URL для регистрации:

# plugin.yaml — расширенный формат
requires_env:
  - name: WEATHER_API_KEY
    description: "API key for OpenWeather"
    url: "https://openweathermap.org/api"
    secret: true
Поле Обязательно Описание
name Да Имя переменной окружения
description Нет Показывается пользователю во время установки
url Нет Где получить учётные данные
secret Нет Если true, ввод скрыт (как поле пароля)

Оба формата можно смешивать в одном списке. Уже установленные переменные молча пропускаются.

Условная доступность инструментов

Для инструментов, зависящих от опциональных библиотек:

ctx.register_tool(
    name="my_tool",
    schema={...},
    handler=my_handler,
    check_fn=lambda: _has_optional_lib(),  # False = инструмент скрыт от модели
)

Регистрация нескольких хуков

def register(ctx):
    ctx.register_hook("pre_tool_call", before_any_tool)
    ctx.register_hook("post_tool_call", after_any_tool)
    ctx.register_hook("pre_llm_call", inject_memory)
    ctx.register_hook("on_session_start", on_new_session)
    ctx.register_hook("on_session_end", on_session_end)

Справочник хуков

Каждый хук полностью документирован в Справочнике хуков событий — сигнатуры колбэков, таблицы параметров, когда именно срабатывает, и примеры. Вот краткая сводка:

Хук Срабатывает когда Сигнатура колбэка Возврат
pre_tool_call Перед выполнением любого инструмента tool_name: str, args: dict, task_id: str игнорируется
post_tool_call После возврата любого инструмента tool_name: str, args: dict, result: str, task_id: str, duration_ms: int игнорируется
pre_llm_call Один раз за ход, перед циклом вызова инструментов session_id: str, user_message: str, conversation_history: list, is_first_turn: bool, model: str, platform: str внедрение контекста
post_llm_call Один раз за ход, после цикла вызова инструментов (только успешные ходы) session_id: str, user_message: str, assistant_response: str, conversation_history: list, model: str, platform: str игнорируется
on_session_start Создана новая сессия (только первый ход) session_id: str, model: str, platform: str игнорируется
on_session_end Конец каждого вызова run_conversation + выход из CLI session_id: str, completed: bool, interrupted: bool, model: str, platform: str игнорируется
on_session_finalize CLI/шлюз завершает активную сессию session_id: str \| None, platform: str игнорируется
on_session_reset Шлюз меняет ключ сессии (/new, /reset) session_id: str, platform: str игнорируется

Большинство хуков — это наблюдатели типа «запустил и забыл» — их возвращаемые значения игнорируются. Исключение — pre_llm_call, который может внедрять контекст в разговор.

Все колбэки должны принимать **kwargs для обратной совместимости. Если колбэк хука падает, это логируется и пропускается. Другие хуки и агент продолжают нормально работать.

Внедрение контекста pre_llm_call

Это единственный хук, чьё возвращаемое значение имеет значение. Когда колбэк pre_llm_call возвращает словарь с ключом "context" (или обычную строку), Hermes внедряет этот текст в сообщение пользователя текущего хода. Это механизм для плагинов памяти, RAG интеграций, защитных шлюзов и любого плагина, которому нужно предоставить модели дополнительный контекст.

Формат возврата

# Словарь с ключом context
return {"context": "Recalled memories:\\n- User prefers dark mode\\n- Last project: hermes-agent"}

# Обычная строка (эквивалент формы словаря выше)
return "Recalled memories:\\n- User prefers dark mode"

# Вернуть None или не возвращать → без внедрения (только наблюдение)
return None

Любой не-None, непустой возврат с ключом "context" (или обычная непустая строка) собирается и добавляется к сообщению пользователя для текущего хода.

Как работает внедрение

Внедряемый контекст добавляется к сообщению пользователя, а не к системному промпту. Это осознанный дизайнерский выбор:

Пример: Плагин памяти

"""Memory plugin — recalls relevant context from a vector store."""

import httpx

MEMORY_API = "https://your-memory-api.example.com"

def recall_context(session_id, user_message, is_first_turn, **kwargs):
    """Called before each LLM turn. Returns recalled memories."""
    try:
        resp = httpx.post(f"{MEMORY_API}/recall", json={
            "session_id": session_id,
            "query": user_message,
        }, timeout=3)
        memories = resp.json().get("results", [])
        if not memories:
            return None  # нечего внедрять

        text = "Recalled context from previous sessions:\\n"
        text += "\\n".join(f"- {m['text']}" for m in memories)
        return {"context": text}
    except Exception:
        return None  # молча падать, не ломать агента

def register(ctx):
    ctx.register_hook("pre_llm_call", recall_context)

Пример: Плагин защитных шлюзов

"""Guardrails plugin — enforces content policies."""

POLICY = """You MUST follow these content policies for this session:

- Never generate code that accesses the filesystem outside the working directory

- Always warn before executing destructive operations

- Refuse requests involving personal data extraction"""

def inject_guardrails(**kwargs):
    """Injects policy text into every turn."""
    return {"context": POLICY}

def register(ctx):
    ctx.register_hook("pre_llm_call", inject_guardrails)

Пример: Хук только для наблюдения (без внедрения)

"""Analytics plugin — tracks turn metadata without injecting context."""

import logging
logger = logging.getLogger(__name__)

def log_turn(session_id, user_message, model, is_first_turn, **kwargs):
    """Fires before each LLM call. Returns None — no context injected."""
    logger.info("Turn: session=%s model=%s first=%s msg_len=%d",
                session_id, model, is_first_turn, len(user_message or ""))
    # Нет возврата → без внедрения

def register(ctx):
    ctx.register_hook("pre_llm_call", log_turn)

Несколько плагинов, возвращающих контекст

Когда несколько плагинов возвращают контекст из pre_llm_call, их выводы объединяются с двойными переводами строк и добавляются к сообщению пользователя вместе. Порядок соответствует порядку обнаружения плагинов (алфавитный по имени директории плагина).

Регистрация CLI команд

Плагины могут добавлять собственное дерево подкоманд hermes <плагин>:

def _my_command(args):
    """Handler for hermes my-plugin <subcommand>."""
    sub = getattr(args, "my_command", None)
    if sub == "status":
        print("All good!")
    elif sub == "config":
        print("Current config: ...")
    else:
        print("Usage: hermes my-plugin <status|config>")

def _setup_argparse(subparser):
    """Build the argparse tree for hermes my-plugin."""
    subs = subparser.add_subparsers(dest="my_command")
    subs.add_parser("status", help="Show plugin status")
    subs.add_parser("config", help="Show plugin config")
    subparser.set_defaults(func=_my_command)

def register(ctx):
    ctx.register_tool(...)
    ctx.register_cli_command(
        name="my-plugin",
        help="Manage my plugin",
        setup_fn=_setup_argparse,
        handler_fn=_my_command,
    )

После регистрации пользователи могут запускать hermes my-plugin status, hermes my-plugin config и т.д.

Плагины провайдеров памяти используют подход на основе соглашений вместо этого: добавьте функцию register_cli(subparser) в файл cli.py вашего плагина. Система обнаружения плагинов памяти находит её автоматически — вызов ctx.register_cli_command() не требуется. См. Руководство по плагинам провайдеров памяти для подробностей.

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

Регистрация слеш-команд

Плагины могут регистрировать слеш-команды внутри сессии — команды, которые пользователи вводят во время разговора (например, /lcm status или /ping). Они работают как в CLI, так и в шлюзе (Telegram, Discord и т.д.).

def _handle_status(raw_args: str) -> str:
    """Handler for /mystatus — called with everything after the command name."""
    if raw_args.strip() == "help":
        return "Usage: /mystatus [help|check]"
    return "Plugin status: all systems nominal"

def register(ctx):
    ctx.register_command(
        "mystatus",
        handler=_handle_status,
        description="Show plugin status",
    )

После регистрации пользователи могут вводить /mystatus в любой сессии. Команда появляется в автодополнении, выводе /help и меню Telegram бота.

Сигнатура: ctx.register_command(name: str, handler: Callable, description: str = "")

Параметр Тип Описание
name str Имя команды без ведущего слеша (например, "lcm", "mystatus")
handler Callable[[str], str \| None] Вызывается с сырой строкой аргументов. Может быть async.
description str Показывается в /help, автодополнении и меню Telegram бота

Ключевые отличия от register_cli_command():

register_command() register_cli_command()
Вызывается как /name в сессии hermes name в терминале
Где работает CLI сессии, Telegram, Discord и т.д. Только терминал
Обработчик получает Сырую строку аргументов argparse Namespace
Сценарий использования Диагностика, статус, быстрые действия Сложные деревья подкоманд, мастера настройки

Защита от конфликтов: Если плагин пытается зарегистрировать имя, конфликтующее со встроенной командой (help, model, new и т.д.), регистрация молча отклоняется с предупреждением в логе. Встроенные команды всегда имеют приоритет.

Асинхронные обработчики: Диспетчер шлюза автоматически определяет и ожидает async обработчики, поэтому вы можете использовать как синхронные, так и асинхронные функции:

async def _handle_check(raw_args: str) -> str:
    result = await some_async_operation()
    return f"Check result: {result}"

def register(ctx):
    ctx.register_command("check", handler=_handle_check, description="Run async check")

Диспетчеризация инструментов из слеш-команд

Обработчикам слеш-команд, которым нужно оркестрировать инструменты (запустить субагента через delegate_task, вызвать file_edit и т.д.), следует использовать ctx.dispatch_tool() вместо обращения к внутренностям фреймворка. Контекст родительского агента (подсказки рабочей области, спиннер, наследование модели) подключается автоматически.

def register(ctx):
    def _handle_deliver(raw_args: str):
        result = ctx.dispatch_tool(
            "delegate_task",
            {
                "goal": raw_args,
                "toolsets": ["terminal", "file", "web"],
            },
        )
        return result

    ctx.register_command(
        "deliver",
        handler=_handle_deliver,
        description="Delegate a goal to a subagent",
    )

Сигнатура: ctx.dispatch_tool(name: str, args: dict, *, parent_agent=None) -> str

Параметр Тип Описание
name str Имя инструмента, зарегистрированного в реестре (например, "delegate_task", "file_edit")
args dict Аргументы инструмента, той же формы, что отправила бы модель
parent_agent Agent \| None Опциональное переопределение. Если опущено, разрешается из текущего CLI агента (или корректно деградирует в режиме шлюза)

Поведение во время выполнения:

Это публичный, стабильный интерфейс для диспетчеризации инструментов из команд плагинов. Плагины не должны обращаться к ctx._cli_ref.agent или подобным приватным состояниям.

Этот гайд охватывает **общие плагины** (инструменты, хуки, слеш-команды, CLI команды). Разделы ниже описывают паттерн создания для каждого специализированного типа плагинов; каждый ссылается на свой полный гайд для справочника полей и примеров.

Специализированные типы плагинов

У Hermes есть пять специализированных типов плагинов помимо общей поверхности. Каждый поставляется как директория в plugins/<категория>/<имя>/ (встроенные) или ~/.hermes/plugins/<категория>/<имя>/ (пользовательские). Контракт различается в зависимости от категории — выберите нужный, затем прочитайте его полный гайд.

Плагины провайдеров моделей — добавьте LLM бэкенд

Поместите профиль в plugins/model-providers/<имя>/:

# plugins/model-providers/acme/__init__.py
from providers import register_provider
from providers.base import ProviderProfile

register_provider(ProviderProfile(
    name="acme",
    aliases=("acme-inference",),
    display_name="Acme Inference",
    env_vars=("ACME_API_KEY", "ACME_BASE_URL"),
    base_url="https://api.acme.example.com/v1",
    auth_type="api_key",
    default_aux_model="acme-small-fast",
    fallback_models=("acme-large-v3", "acme-medium-v3"),
))
# plugins/model-providers/acme/plugin.yaml
name: acme-provider
kind: model-provider
version: 1.0.0
description: Acme Inference — OpenAI-compatible direct API

Обнаруживается лениво при первом вызове get_provider_profile() или list_providers()auth.py, config.py, doctor.py, models.py, runtime_provider.py и транспорт chat_completions автоматически подключаются к нему. Пользовательские плагины переопределяют встроенные по имени.

Полный гайд: Плагины провайдеров моделей — справочник полей, переопределяемые хуки (prepare_messages, build_extra_body, build_api_kwargs_extras, fetch_models), выбор api_mode, типы аутентификации, тестирование.

Плагины платформ — добавьте канал шлюза

Поместите адаптер в plugins/platforms/<имя>/:

# plugins/platforms/myplatform/adapter.py
from gateway.platforms.base import BasePlatformAdapter

class MyPlatformAdapter(BasePlatformAdapter):
    async def connect(self): ...
    async def send(self, chat_id, text): ...
    async def disconnect(self): ...

def check_requirements():
    import os
    return bool(os.environ.get("MYPLATFORM_TOKEN"))

def _env_enablement():
    import os
    tok = os.getenv("MYPLATFORM_TOKEN", "").strip()
    if not tok:
        return None
    return {"token": tok}

def register(ctx):
    ctx.register_platform(
        name="myplatform",
        label="MyPlatform",
        adapter_factory=lambda cfg: MyPlatformAdapter(cfg),
        check_fn=check_requirements,
        required_env=["MYPLATFORM_TOKEN"],
        # Автозаполнение PlatformConfig.extra из окружения, чтобы настройки только с env
        # отображались в `hermes gateway status` без инстанцирования SDK.
        env_enablement_fn=_env_enablement,
        # Согласие на доставку cron: `deliver=myplatform` маршрутизируется к этой переменной.
        cron_deliver_env_var="MYPLATFORM_HOME_CHANNEL",
        emoji="💬",
        platform_hint="You are chatting via MyPlatform. Keep responses concise.",
    )
# plugins/platforms/myplatform/plugin.yaml
name: myplatform-platform
label: MyPlatform
kind: platform
version: 1.0.0
description: MyPlatform gateway adapter
requires_env:
  - name: MYPLATFORM_TOKEN
    description: "Bot token from the MyPlatform console"
    password: true
optional_env:
  - name: MYPLATFORM_HOME_CHANNEL
    description: "Default channel for cron delivery"
    password: false

Полный гайд: Добавление адаптеров платформ — полный контракт BasePlatformAdapter, маршрутизация сообщений, блокировка аутентификации, интеграция мастера настройки. Посмотрите plugins/platforms/irc/ для работающего примера только со stdlib.

Плагины провайдеров памяти — добавьте бэкенд знаний между сессиями

Поместите реализацию MemoryProvider в plugins/memory/<имя>/:

# plugins/memory/my-memory/__init__.py
from agent.memory_provider import MemoryProvider

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

    def is_available(self) -> bool:
        import os
        return bool(os.environ.get("MY_MEMORY_API_KEY"))

    def initialize(self, session_id: str, **kwargs) -> None:
        self._session_id = session_id

    def sync_turn(self, user_message, assistant_response, **kwargs) -> None:
        ...

    def prefetch(self, query: str, **kwargs) -> str | None:
        ...

def register(ctx):
    ctx.register_memory_provider(MyMemoryProvider())

Провайдеры памяти — это выбор одного элемента — только один активен в данный момент, выбирается через memory.provider в config.yaml.

Полный гайд: Плагины провайдеров памяти — полный ABC MemoryProvider, контракт потоков, изоляция профилей, регистрация CLI команд через cli.py.

Плагины движков контекста — замените компрессор контекста

# plugins/context_engine/my-engine/__init__.py
from agent.context_engine import ContextEngine

class MyContextEngine(ContextEngine):
    @property
    def name(self) -> str:
        return "my-engine"

    def should_compress(self, messages, model) -> bool: ...
    def compress(self, messages, model) -> list[dict]: ...

def register(ctx):
    ctx.register_context_engine(MyContextEngine())

Движки контекста — это выбор одного элемента — выбирается через context.engine в config.yaml.

Полный гайд: Плагины движков контекста.

Бэкенды генерации изображений

Поместите провайдера в plugins/image_gen/<имя>/:

# plugins/image_gen/my-imggen/__init__.py
from agent.image_gen_provider import ImageGenProvider

class MyImageGenProvider(ImageGenProvider):
    @property
    def name(self) -> str:
        return "my-imggen"

    def is_available(self) -> bool: ...
    def generate(self, prompt: str, **kwargs) -> str: ...   # возвращает путь к изображению

def register(ctx):
    ctx.register_image_gen_provider(MyImageGenProvider())
# plugins/image_gen/my-imggen/plugin.yaml
name: my-imggen
kind: backend
version: 1.0.0
description: Custom image generation backend

Полный гайд: Плагины провайдеров генерации изображений — полный ABC ImageGenProvider, метаданные list_models() / get_setup_schema(), хелперы success_response()/error_response(), вывод base64 vs URL, пользовательские переопределения, pip дистрибуция.

Примеры для справки: plugins/image_gen/openai/ (DALL-E / GPT-Image через OpenAI SDK), plugins/image_gen/openai-codex/, plugins/image_gen/xai/ (Grok image gen).

Не-Python поверхности расширения

Hermes также принимает расширения, которые вообще не являются Python плагинами. Они показаны в таблице подключаемых интерфейсов; разделы ниже кратко описывают каждый стиль создания.

MCP серверы — регистрация внешних инструментов

Серверы Model Context Protocol (MCP) регистрируют свои собственные инструменты в Hermes без какого-либо Python плагина. Объявите их в ~/.hermes/config.yaml:

mcp_servers:
  filesystem:
    command: "npx"
    args: ["-y", "@modelcontextprotocol/server-filesystem", "/home/user/projects"]
    timeout: 120

  linear:
    url: "https://mcp.linear.app/sse"
    auth:
      type: "oauth"

Hermes подключается к каждому серверу при запуске, перечисляет его инструменты и регистрирует их вместе со встроенными. LLM видит их точно так же, как любой другой инструмент. Полный гайд: MCP.

Хуки событий шлюза — срабатывают по событиям жизненного цикла

Поместите манифест + обработчик в ~/.hermes/hooks/<имя>/:

# ~/.hermes/hooks/long-task-alert/HOOK.yaml
name: long-task-alert
description: Send a push notification when a long task finishes
events:
  - agent:end
# ~/.hermes/hooks/long-task-alert/handler.py
async def handle(event_type: str, context: dict) -> None:
    if context.get("duration_seconds", 0) > 120:
        # отправить уведомление …
        pass

События включают gateway:startup, session:start, session:end, session:reset, agent:start, agent:step, agent:end и wildcard command:*. Ошибки в хуках перехватываются и логируются — они никогда не блокируют основной конвейер.

Полный гайд: Хуки событий шлюза.

Оболочечные хуки — выполнение команды оболочки при вызовах инструментов

Если вы просто хотите запустить скрипт при срабатывании инструмента (уведомления, аудиторские логи, оповещения на рабочем столе, автоформаттеры), используйте оболочечные хуки в config.yaml — Python не требуется:

hooks:
  - event: post_tool_call
    command: "notify-send 'Tool ran: {tool_name}'"
    when:
      tools: [terminal, patch, write_file]

Поддерживаются все те же события, что и для Python плагинов-хуков (pre_tool_call, post_tool_call, pre_llm_call, post_llm_call, on_session_start, on_session_end, pre_gateway_dispatch) плюс структурированный JSON вывод для решений блокировки pre_tool_call.

Полный гайд: Оболочечные хуки.

Источники навыков — добавьте пользовательский реестр навыков

Если вы поддерживаете GitHub репозиторий навыков (или хотите получать навыки из сообщества помимо встроенных источников), добавьте его как tap:

hermes skills tap add myorg/skills-repo
hermes skills search my-workflow --source myorg/skills-repo
hermes skills install myorg/skills-repo/my-workflow

Публикация собственного tap — это просто GitHub репозиторий с директориями skills/<имя-навыка>/SKILL.md — никакого сервера или регистрации не требуется.

Полные гайды: Skills Hub · Публикация пользовательского tap (структура репозитория, минимальный пример, нестандартные пути, уровни доверия).

TTS / STT через шаблоны команд

Любой CLI, который читает/записывает аудио или текст, может быть подключён через config.yaml — без Python кода:

tts:
  provider: voxcpm
  providers:
    voxcpm:
      type: command
      command: "voxcpm --ref ~/voice.wav --text-file {input_path} --out {output_path}"
      output_format: mp3
      voice_compatible: true

Для STT укажите HERMES_LOCAL_STT_COMMAND на шаблон оболочки. Поддерживаемые плейсхолдеры: {input_path}, {output_path}, {format}, {voice}, {model}, {speed} (TTS); {input_path}, {output_dir}, {language}, {model} (STT). Любой CLI, взаимодействующий с путями, автоматически становится плагином.

Полные гайды: Пользовательские TTS провайдеры команд · STT.

Распространение через pip

Для публичного распространения плагинов добавьте entry point в ваш Python пакет:

# pyproject.toml
[project.entry-points."hermes_agent.plugins"]
my-plugin = "my_plugin_package"
pip install hermes-plugin-calculator
# Плагин будет автоматически обнаружен при следующем запуске hermes

Распространение для NixOS

Пользователи NixOS могут установить ваш плагин декларативно, если вы предоставите pyproject.toml с entry points:

Entry-point плагины (рекомендуется для распространения):

# configuration.nix пользователя
services.hermes-agent.extraPythonPackages = [
  (pkgs.python312Packages.buildPythonPackage {
    pname = "my-plugin";
    version = "1.0.0";
    src = pkgs.fetchFromGitHub {
      owner = "you";
      repo = "hermes-my-plugin";
      rev = "v1.0.0";
      hash = "sha256-...";  # nix-prefetch-url --unpack
    };
    format = "pyproject";
    build-system = [ pkgs.python312Packages.setuptools ];
  })
];

Директорные плагины (без pyproject.toml):

services.hermes-agent.extraPlugins = [
  (pkgs.fetchFromGitHub {
    owner = "you";
    repo = "hermes-my-plugin";
    rev = "v1.0.0";
    hash = "sha256-...";
  })
];

См. Руководство по Nix Setup для полной документации, включая использование overlay и проверку коллизий.

Распространённые ошибки

Обработчик не возвращает строку JSON:

# Неправильно — возвращает словарь
def handler(args, **kwargs):
    return {"result": 42}

# Правильно — возвращает строку JSON
def handler(args, **kwargs):
    return json.dumps({"result": 42})

Отсутствует **kwargs в сигнатуре обработчика:

# Неправильно — сломается, если Hermes передаст дополнительный контекст
def handler(args):
    ...

# Правильно
def handler(args, **kwargs):
    ...

Обработчик вызывает исключения:

# Неправильно — исключение распространяется, вызов инструмента падает
def handler(args, **kwargs):
    result = 1 / int(args["value"])  # ZeroDivisionError!
    return json.dumps({"result": result})

# Правильно — перехватываем и возвращаем JSON с ошибкой
def handler(args, **kwargs):
    try:
        result = 1 / int(args.get("value", 0))
        return json.dumps({"result": result})
    except Exception as e:
        return json.dumps({"error": str(e)})

Слишком размытое описание схемы:

# Плохо — модель не знает, когда использовать
"description": "Does stuff"

# Хорошо — модель знает точно когда и как
"description": "Evaluate a mathematical expression. Use for arithmetic, trig, logarithms. Supports: +, -, *, /, **, sqrt, sin, cos, log, pi, e."