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

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

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

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

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

Что вы производите

Плагин калькулятор с двумя инструментами: - вычислить — вычисление математических выражений (2**16, sqrt(144), pi * 5**2) - unit_convert — преобразование единиц измерения (100 F → 37,78 C, 5 км → 3,11 миль)

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

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

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

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

Создал plugin.yaml:

name: calculator
version: 1.0.0
description: Математический калькулятор — вычисление выражений и преобразование единиц
provides_tools:
  - calculate
  - unit_convert
provides_hooks:
  - post_tool_call

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

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

author: Ваше Имя
requires_env:          # ограничение загрузки по переменным окружения; запрашивается при установке
  - SOME_API_KEY       # простой формат — плагин отключен, если отсутствует
  - name: OTHER_KEY    # расширенный формат — показывает описание/URL при установке
    description: "Ключ для сервиса Other"
    url: "https://other.com/keys"
    secret: true

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

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

"""Схемы инструментов — то, что видит LLM."""

CALCULATE = {
    "name": "calculate",
    "description": (
        "Вычисляет математическое выражение и возвращает результат. "
        "Поддерживает арифметику (+, -, *, /, **), функции (sqrt, sin, cos, "
        "log, abs, round, floor, ceil) и константы (pi, e). "
        "Используйте для любой математики, о которой спрашивает пользователь."
    ),
    "parameters": {
        "type": "object",
        "properties": {
            "expression": {
                "type": "string",
                "description": "Математическое выражение для вычисления (например, '2**10', 'sqrt(144)')",
            },
        },
        "required": ["expression"],
    },
}

UNIT_CONVERT = {
    "name": "unit_convert",
    "description": (
        "Преобразует значение между единицами измерения. Поддерживает длину (m, km, mi, ft, in), "
        "вес (kg, lb, oz, g), температуру (C, F, K), данные (B, KB, MB, GB, TB) "
        "и время (s, min, hr, day)."
    ),
    "parameters": {
        "type": "object",
        "properties": {
            "value": {
                "type": "number",
                "description": "Числовое значение для преобразования",
            },
            "from_unit": {
                "type": "string",
                "description": "Исходная единица (например, 'km', 'lb', 'F', 'GB')",
            },
            "to_unit": {
                "type": "string",
                "description": "Целевая единица (например, 'mi', 'kg', 'C', 'MB')",
            },
        },
        "required": ["value", "from_unit", "to_unit"],
    },
}

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

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

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

"""Обработчики инструментов — код, выполняемый при вызове каждого инструмента LLM."""

import json
import math

# Безопасные глобальные переменные для вычисления выражений — без доступа к файлам/сети
_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:
    """Безопасно вычисляет математическое выражение.

    Правила для обработчиков:
    1. Получают args (dict) — параметры, переданные LLM
    2. Выполняют работу
    3. Возвращают JSON-строку — ВСЕГДА, даже при ошибке
    4. Принимают **kwargs для обратной совместимости
    """
    expression = args.get("expression", "").strip()
    if not expression:
        return json.dumps({"error": "Выражение не предоставлено"})

    try:
        result = eval(expression, {"__builtins__": {}}, _SAFE_MATH)
        return json.dumps({"expression": expression, "result": result})
    except ZeroDivisionError:
        return json.dumps({"expression": expression, "error": "Деление на ноль"})
    except Exception as e:
        return json.dumps({"expression": expression, "error": f"Некорректно: {e}"})


# Таблицы преобразования — значения в базовых единицах
_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):
    # Нормализация к Цельсию
    c = {"F": (value - 32) * 5/9, "K": value - 273.15}.get(from_u, value)
    # Преобразование к целевой
    return {"F": c * 9/5 + 32, "K": c + 273.15}.get(to_u, c)


def unit_convert(args: dict, **kwargs) -> str:
    """Преобразует между единицами измерения."""
    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": "Необходимы value, from_unit и to_unit"})

    try:
        # Температура
        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}"})

        # Преобразования на основе коэффициентов
        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"Невозможно преобразовать {from_unit}{to_unit}"})
    except Exception as e:
        return json.dumps({"error": f"Ошибка преобразования: {e}"})

Ключевые правила для обработчиков: 1. Сигнатура: def my_handler(args: dict, **kwargs) -> str 2. Возврат: Всегда JSON-строка. Как для успеха, так и для ошибок. 3. Никогда не записывайте исключения: Перехватите все исключения, вместо этого возвращайте JSON с ошибкой. 4. Принимать **kwargs: В будущем Hermes может передать дополнительный контекст.

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

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

"""Плагин калькулятора — регистрация."""

import logging

from. import schemas, tools

logger = logging.getLogger(__name__)

# Отслеживание использования инструментов через хуки
_call_log = []

def _on_post_tool_call(tool_name, args, result, task_id, **kwargs):
    """Хук: выполняется после каждого вызова инструмента (не только нашего)."""
    _call_log.append({"tool": tool_name, "session": task_id})
    if len(_call_log) > 100:
        _call_log.pop(0)
    logger.debug("Инструмент вызван: %s (сессия %s)", tool_name, task_id)


def register(ctx):
    """Связывает схемы с обработчиками и регистрирует хуки."""
    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)

    # Этот хук срабатывает для ВСЕХ вызовов инструментов, не только наших
    ctx.register_hook("post_tool_call", _on_post_tool_call)

Что делает register(): - Вызывается ровно один раз при запуске. - ctx.register_tool() помещает ваш инструмент в реестр — модель видит его немедленно - ctx.register_hook() при записи событий жизненного цикла. - ctx.register_cli_command() регистрирует подкоманду CLI (например, hermes my-plugin <subcommand>) - ctx.register_command() регистрирует слеш-команду внутри сессии (например, /myplugin <args> в CLI/чате шлюза) — см. Регистрация слеш-команд ниже - ctx.dispatch_tool(name, аргументы) — возникновение любого другого инструмента (встроенного или из другого плагина) с автоматически подключенным контекстом родительского агента (одобрения, учетные данные, Task_id). Полезно из обработчиков слэш-команды, которым нужно вызвать терминал, read_file или любой другой инструмент так, как если бы модель вызывала его напрямую. - Если эта функция аварийно завершится, вилка отключится, но Hermes продолжит работу.

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

def handle_scan(ctx, argstr):
    """Реализует /scan путем вызова инструмента terminal через реестр."""
    result = ctx.dispatch_tool("terminal", {"command": f"find. -name '{argstr}'"})
    return result  # возвращается в UI чата вызывающего

def register(ctx):
    ctx.register_command("scan", handle_scan, help="Поиск файлов по маске")

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

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

Запустите Hermes:

hermes

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

выполните следующие запросы:

Сколько будет 2 в степени 16?
Преобразуй 100 градусов Фаренгейта в Цельсий
Чему равен квадратный корень из 2, умноженный на пи?
Сколько гигабайт в 1.5 терабайтах?

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

/plugins

Вывод:

Плагины (1):
  ✓ calculator v1.0.0 (2 инструмента, 1 хук)

Отладка найти вилку

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

HERMES_PLUGINS_DEBUG=1 hermes plugins list

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

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

hermes logs --level WARNING | grep -i plugin

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

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

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

Четыре файла, четкое разделение: - Манифест объявляет, что такое плагин - Схемы инструментов для LLM - Обработчики реализуют фактическую логику - Регистрация связывает всё вместе

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

Поставка файлов данных

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

# В tools.py или __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")              # → встроенная версия (без изменений)

Ключевые свойства: - Навыки плагина только для чтения — они не изучаются в ~/.hermes/skills/ и не могут быть отредактированы через skill_manage. - Навыки плагина не перечисляются в индексе <available_skills> системного запроса — они загружаются открыто с помощью утилиты. - Простые имена навыков не затрагиваются — пространство имен вызывает конфликты с ограниченными навыками. - Когда агент загружает плагины, перед ним добавляется пакет контекста баннера, перечисляющий местные навыки из того же плагина.

💡 Tip

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

Ограничение по переменному окружению

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

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

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

Когда пользователи запускают установку плагинов Hermes, они интерактивно запрашивают все возможные переменные requires_env. Значения автоматически отображаются в .env.

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

# plugin.yaml — расширенный формат
requires_env:
  - name: WEATHER_API_KEY
    description: "API-ключ для OpenWeather"
    url: "https://openweathermap.org/api"
    secret: true
Поле Обязательно Описание
имя Да Имя переменного окружения
описание Нет Показывается пользователю во время запроса установки
url Нет Где получить учетные данные
секрет Нет Если 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 После возврата любого инструмента имя_инструмента: строка, аргументы: dict, результат: строка, идентификатор_задачи: строка, длительность_мс: int полагаться
pre_llm_call Один раз за ход, перед циклом инструментов session_id: str, user_message: str, разговор_история: список, is_first_turn: bool, модель: str, Платформа: str внедрение контекста
post_llm_call Один раз за ход, после вызова инструментов (только успешные ходы) session_id: str, user_message: str, Assistant_response: str, история разговора: список, модель: str, Платформа: str полагаться
on_session_start Создана новая сессия (только первый ход) session_id: str, модель: str, платформа: str полагаться
on_session_end Конец каждого вызова run_conversation + выход из CLI session_id: str, завершено: bool, прервано: bool, модель: str, Платформа: str полагаться
on_session_finalize CLI/шлюз завершает активную сессию session_id: стр \| Нет, Платформа: str полагаться
on_session_reset Шлюз заменяет ключ сессии (/new, /reset) session_id: str, Платформа: str полагаться

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

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

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

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

Возврат формы

# Словарь с ключом context
return {"context": "Воспоминания:\n- Пользователь предпочитает темную тему\n- Последний проект: hermes-agent"}

# Простая строка (эквивалентна форме словаря выше)
return "Воспоминания:\n- Пользователь предпочитает темную тему"

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

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

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

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

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

"""Плагин памяти — извлекает релевантный контекст из векторного хранилища."""

import httpx

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

def recall_context(session_id, user_message, is_first_turn, **kwargs):
    """Вызывается перед каждым ходом LLM. Возвращает извлеченные воспоминания."""
    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 = "Извлеченный контекст из предыдущих сессий:\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)

Пример: Плагин ограничителей

"""Плагин ограничителей — обеспечивает соблюдение политик контента."""

POLICY = """Вы ДОЛЖНЫ следовать следующим политикам контента для этой сессии:
- Никогда не генерируйте код, который обращается к файловой системе вне рабочего каталога
- Всегда предупреждайте перед выполнением деструктивных операций
- Отказывайте в запросах, связанных с извлечением личных данных"""

def inject_guardrails(**kwargs):
    """Внедряет текст политики в каждый ход."""
    return {"context": POLICY}

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

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

"""Плагин аналитики — отслеживает метаданные хода без внедрения контекста."""

import logging
logger = logging.getLogger(__name__)

def log_turn(session_id, user_message, model, is_first_turn, **kwargs):
    """Срабатывает перед каждым вызовом LLM. Возвращает None — контекст не внедряется."""
    logger.info("Ход: сессия=%s модель=%s первый=%s длина_сообщения=%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 <plugin>:

def _my_command(args):
    """Обработчик для hermes my-plugin <subcommand>."""
    sub = getattr(args, "my_command", None)
    if sub == "status":
        print("Всё хорошо!")
    elif sub == "config":
        print("Текущий конфиг:...")
    else:
        print("Использование: hermes my-plugin <status|config>")

def _setup_argparse(subparser):
    """Строит дерево argparse для hermes my-plugin."""
    subs = subparser.add_subparsers(dest="my_command")
    subs.add_parser("status", help="Показать статус плагина")
    subs.add_parser("config", help="Показать конфиг плагина")
    subparser.set_defaults(func=_my_command)

def register(ctx):
    ctx.register_tool(...)
    ctx.register_cli_command(
        name="my-plugin",
        help="Управление моим плагином",
        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:
    """Обработчик для /mystatus — вызывается со всем, что после имени команды."""
    if raw_args.strip() == "help":
        return "Использование: /mystatus [help|check]"
    return "Статус плагина: все системы в норме"

def register(ctx):
    ctx.register_command(
        "mystatus",
        handler=_handle_status,
        description="Показать статус плагина",
    )

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

Сигнатура: ctx.register_command(name: str, обработчик: Callable, описание: str = "")

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

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

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

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

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

async def _handle_check(raw_args: str) -> str:
    result = await some_async_operation()
    return f"Результат проверки: {result}"

def register(ctx):
    ctx.register_command("check", handler=_handle_check, description="Запустить асинхронную проверку")

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

Обработчики слэш-команды, которым нужно оркестровать инструменты (породить под-агента через 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="Делегировать цель под-агенту",
    )

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

Параметр Тип Описание
имя ул Имя инструмента, зарегистрированное в реестре инструментов (например, "delegate_task","file_edit"`)
аргументы дикт Аргументы инструмента той же формы, что была отправлена ​​модель
родительский_агент Агент \| Нет Необязательное переопределение. Если опущено, разрешается из текущего агента CLI (или корректно деградирует в режиме шлюза)

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

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

💡 Tip

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

Специализированные виды вилок

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

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

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

# 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 — прямой API, совместимый с OpenAI

Лениво обнаруживается при первом вызове 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/<name>/:

# 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="Вы общаетесь через MyPlatform. Старайтесь отвечать кратко.",
    )
# plugins/platforms/myplatform/plugin.yaml
name: myplatform-platform
label: MyPlatform
kind: platform
version: 1.0.0
description: Адаптер шлюза MyPlatform
requires_env:
  - name: MYPLATFORM_TOKEN
    description: "Токен бота из консоли MyPlatform"
    password: true
optional_env:
  - name: MYPLATFORM_HOME_CHANNEL
    description: "Канал по умолчанию для доставки cron"
    password: false

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

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

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

# 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/<name>/:

# 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: Пользовательский бэкенд генерации изображений

Полное руководство: Плагины генерации изображений — полный 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).

Не-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/<name>/:

# ~/.hermes/hooks/long-task-alert/HOOK.yaml
name: long-task-alert
description: Отправить push-уведомление, когда долгая задача завершится
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

События включают в себя шлюз: запуск, сессия: начало, сессия: конец, сессия: сброс, агент: старт, агент: шаг, агент: конец и подстановочный знак команда:*. Ошибки в хуках перехватываются и регистрируются — они никогда не блокируют основной конвейер.

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

Хуки обработки — Выполнение работ по вызову инструментов

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

hooks:
  - event: post_tool_call
    command: "notify-send 'Инструмент выполнен: {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

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

Полные руководства: Хаб навыков · Публикация пользовательского 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

Для публичного распространения плагинов укажите точку входа в ваш Python-пакет:

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

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

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

Плагины с точками входа (рекомендуется к распространению):

# 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.

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

Обработчик не возвращает 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": "Делает что-то"

# Хорошо — модель знает точно когда и как
"description": "Вычисляет математическое выражение. Используйте для арифметики, тригонометрии, логарифмов. Поддерживает: +, -, *, /, **, sqrt, sin, cos, log, pi, e."