← Лендинг Hermes Agent

Опубликовано

Воркфлоу с DoD и гейтами на Hermes Agent: 4 слоя, один прогон, ноль запретов в промпте

Автор: Гусев Николай [портфолио]

Темы: ai-agents, workflow, ontonet

Сложность: высокая

Джилл изучает граф: узлы онтологии, связи между ними и рабочий стол агента

Онто - платформа, где знания компании собраны в одну модель. Каждый объект в ней своего типа, а связи между объектами и есть контекст: кто владеет решением, на что оно опирается, что на него влияет. Отдельный факт без связей бесполезен. Кредитная политика сама по себе ничего не значит; она значит ровно столько, сколько показывают её связи со скорингом, с заявкой и с тем, кто отвечает за решение.

Связность даёт агенту то, чего у него нет по умолчанию: связь между его собственными шагами. Обычный пайплайн выглядит как список команд, и после сжатия контекста агент уже не понимает, на каком шаге остановился. В Онто у каждого шага есть объект, а путь от причины до результата хранится связями, а не пересказом в чате.

Платформа даёт ещё и роли. В realm их три, OWNER, PARTICIPANT и OBSERVER. Роль определяет, что кому можно, и поэтому запрет можно вынести из текста в права доступа.

Ниже - как на этом фундаменте собирается воркфлоу с критерием готовности и настоящими гейтами.

Что такое DoD в этой конструкции

DoD - Definition of Done, критерий готовности. Здесь он хранится не в тексте скилла, а как данные: список проверок, каждая из которых обращается к системе и ждёт ответа. Файл на диске проверяется обращением к файловой системе, картинка - запросом к очереди ComfyUI, опубликованная страница - вызовом getPage.

Разница между критерием в промпте и критерием в данных видна на длинных процессах. Запись в SKILL.md вида "не публикуй без согласования" после пяти сжатий контекста читается моделью как инструкция к действию: написано же, значит правда. Критерий готовности в данных такому не подвержен - он не инструкция, а поле, которое заполнили и потом проверили.

Отсюда правило, которое определяет остальную конструкцию: агенту не надо знать, как неправильно. Агенту надо знать только, как правильно. Запрет должен жить в правах доступа.

Четыре слоя

Слой 1 - канон домашней стороны. Файл с критериями готовности плюс SKILL.md пайплайна. Единственный слой, который агент читает целиком, поэтому короткий.

Слой 2 - детерминированный работник. Обычный Python-скрипт: пишет, читает или публикует, проверяет факт и умеет повторяться без вреда. Он не принимает ни одного решения.

Слой 3 - внешнее состояние. Артефакт в Онто с критерием готовности, видом, режимом записи и статусом. Живёт отдельно от контекста агента, переживает и сжатие, и перезапуск.

Слой 4 - роли и токены. Рисовальщику ключ с правом чтения, тому, кто подтверждает, полный. Переход из proposed в accepted для агента недоступен: у него нет роли, которая это позволяет.

Слои 2, 3 и 4 агент не читает. Он вызывает python step.py verify и получает OK или FAIL.

Минимальный каркас

Один скрипт на шаг, ноль зависимостей от агента, весь контракт с внешним миром - через аргументы командной строки.

Артефакт в Онто требует восьми обязательных полей. realm_id - идентификатор рабочего realm. artifact_path - путь шага внутри realm, он же идентификатор для повторного запуска. artifact_kind - вид из восьми значений. write_mode - режим записи, привязанный к виду: replace у пяти видов (capability_dossier, decision, operation_matrix, review_protocol, test_strategy) и append у трёх (handoff, review_log, worklog). body - сам DoD в тексте, JSON не обязателен. summary - короткое имя проверки. source_ref - где взялся факт. targets - массив объектов, у каждого target_kind из списка realm, template, entity, diagram плюс target_id; роль внутри target необязательна. Поле review_destination необязательное, любые поля сверх списка отвергаются как unexpected_keyword_argument.

Минимально рабочий вызов выглядит так:

REALM = os.environ["ONTONET_REALM"]   # your own realm UUID

args = {
    "realm_id":        REALM,
    "artifact_path":   "draw/2026-10-01-my-step",
    "artifact_kind":   "worklog",
    "write_mode":      "append",
    "body":            "DoD: файл на диске, ширина 1024, vision 4/4",
    "summary":         "проверка результата",
    "source_ref":      "verify.py",
    "targets":         [{"target_kind": "realm",
                         "target_id":   REALM}],
}

Ключ лежит в переменной окружения, а не в коде. Скрипт читает её сам и уходит в сеть официальным MCP-транспортом, тем же, что и у агента. Тело принимается и в ASCII-экранированном виде, и в сыром UTF-8, и даже как обычный текст без JSON-обёртки.

Порядок вызовов один и не меняется: create_memory_artifact_draft получает идентификатор артефакта, submit_memory_artifact переводит его из draft в proposed, get_memory_artifact читает статус. Переход в accepted делает отдельный вызов accept_memory_artifact, и делать его должен другой токен, у которого в realm роль OWNER. Поле review_destination при этом остаётся просто полем: оно ни на что не влияет, переход выполняет именно вызов. Каждый переход одноразовый: повторный submit или accept отвечает 409 Conflict. У артефактов с режимом append есть счётчик append_entries и своё событие append_added.

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

Контракт шага

Шаг пайплайна описан тремя вещами: вызов, проверяемый факт, критерий готовности. Вызов - то, что меняет мир. Факт - то, что можно подтвердить запросом к системе. Критерий - список проверок, который должен сошться целиком.

На живой картинке это выглядит так: вызов POST /prompt с опросом /history, факт - файл на диске и его размеры, критерий - четыре ответа vision плюс пустая очередь. Пайплайн из шести шагов: прогрев ComfyUI пустым латентом 512 на 512 за 8 шагов сэмплера, основной рендер 1024 на 640 за 25 шагов с cfg 1.0 и референсом канона лица, проверка результата, уборка (очередь пустая, сервер остановлен, видеопамять освобождена), закрытие шага в Онто.

Два правила, без которых шаг не воспроизводится. Первое - проверка факта обязана обращаться к системе. Запись "файл создан" в артефакте ничего не проверяет; проверяется запрос. Второе - повторный запуск не должен дублировать. Если шаг можно безопасно повторить, повтор должен узнавать уже сделанное и не переделывать.

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

Подстановка: вместо картинок - любой процесс

Меняется только слой 2, остальное остаётся на месте.

Для картинки: вызов - POST /prompt с опросом /history, факт - файл на диске и его размеры, критерий готовности - четыре ответа vision и пустая очередь, риск - занятая видеопамять, финальный гейт - приёмка человеком.

Для статьи на Telegra.ph: вызов - скрипт telegraph-publish-direct.py, факт - getPage с return_content=true, критерий готовности - число img-нод больше нуля, количество длинных тире равно нулю, количество утечек равно нулю, риск - публикация без согласования, финальный гейт - приёмка плюс отдельный createPage.

Меняется риск, а вместе с ним и последний шаг. Для картинки "опубликовано" означает "файл лежит". Для статьи createPage сразу делает страницу общедоступной, и приватный черновик на том же адресе сделать нельзя - получится только на другом. Поэтому у статьи предпоследним шагом идёт публикация черновика с пометкой в заголовке, а готовым последний шаг считается по факту рендера: в HTML должен быть ноль длинных тире и ноль утечек. Само слово "опубликовано" тут вообще не критерий. Частный черновик по адресу, который нигде не процитирован, и есть аналог статуса proposed.

Длинные процессы встают на то же место, где был рисунок. Онбординг сотрудника: проверка результата теста через API, создание учётки в домене, следующий тест, учётка в CRM, выдача доступа в следующую систему. Каждый шаг - отдельный критерий готовности, каждая проверка факта - ответ целевой системы, каждое создание - отдельный артефакт, человек разблокирует переход между этапами. Разница только в том, что дергать вместо ComfyUI.

Что проверить до первого шага

Набор полей и видов артефакта лучше выяснять живым запросом, а не по документации. В OpenAPI и в схеме MCP поле artifact_kind описано как свободная строка без перечисления, хотя рядом target_kind объявлен с полным списком значений. Реальный список видов сервер называет сам, в тексте ошибки: capability_dossier, decision, handoff, operation_matrix, review_log, review_protocol, test_strategy, worklog. Свой идентификатор вроде draw.step не принимается. Режим записи привязан к виду, и привязку эту стоит проверить обеими сторонами, а не по памяти: у пяти видов он replace, у трёх append, и неверный режим отвергается с указанием, что требуется. Пустой вид отклоняется отдельно.

Тело принимается в сыром UTF-8, в ASCII-экранированном виде и как обычный текст без JSON-обёртки, поэтому кодировать его специально не нужно. Аудит лежит на самом артефакте, а не в отдельном инструменте: последнее событие и счётчик записей читаются из тела. Хеша тела у артефакта нет - ни sha256, ни content_hash, - так что целостность тела, если она нужна, считает клиент, а не платформа.

По REST тот же неверный вид отвечает голым HTTP 400 без объяснения, поэтому вид артефакта там проверяется только заранее известным списком. Через MCP приходит разбор: имя отклонённого параметра, допустимые значения и подсказка про write_mode.

Ограничения

Роли надо раздать до того, как написан первый шаг. Если у пишущего агента роль с правом записи, он закроет свой шаг сам, и защита будет выглядеть рабочей при полном отсутствии защиты.

Изоляция по ролям даётся разными ключами и разными профилями Hermes. Профиль это роль, ключ в его .env это полномочия. Разные пути артефактов к делу не относятся.

Критерий готовности в артефакте фиксирует, что проверки сошлись. Саму проверку факта он не заменяет.

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

Итог

Сначала раздай права, потом пиши инструкции. Критерий готовности храни данными. Факт проверяй запросом к системе. Идентичность шага держи в файле. На запреты в промпте не рассчитывай - после пяти сжатий он перестаёт работать, и перестаёт тихо.