Триггеры запуска операций и обращение агента к человеку
#Цель
Сделать запуск агентских операций независимым от пользовательского чата.
В реальной эксплуатации задача обычно возникает во внешней системе:
- создан Work Item;
- создан или обновлён Pull Request;
- изменился сценарий или тест-кейс в MindMap;
- сработал другой системный webhook;
- в будущем — API-вызов и другие источники (расписание уже реализовано).
После получения события система должна самостоятельно создать и выполнить операцию агента.
Если в процессе выполнения агент не может принять решение без дополнительного контекста, он должен уметь адресно запросить человека через настроенный для этого пользователя канал коммуникации — Matrix, Telegram или другой подключённый канал — дождаться ответа и продолжить ту же операцию.
#Основной принцип
Чат не запускает операцию. Чат является одним из каналов взаимодействия с уже выполняющейся операцией.
Источником запуска является событие.
Внешняя система
│
│ webhook / event
▼
Trigger
│
▼
Agent Run
│
├── autonomous work
│
├── достаточно контекста
│ │
│ ▼
│ завершение
│
└── недостаточно контекста
│
▼
запрос человеку
│
Matrix / Telegram / ...
│
▼
ответ
│
▼
продолжение Run
#1. Триггеры
Необходимо ввести сущность Trigger — декларативное описание того, какое внешнее событие должно запускать какую агентскую операцию.
#Минимальные свойства
idnametypeenabledagentinput mappingcontextauthorizationdeduplicationretry policy
#Типы триггеров
Фактически реализовано три типа:
webhook;event— событие собственного журнала наблюдения (см. ниже);schedule— cron-расписание.
Сценарии поверх webhook: azure_devops.pull_request, azure_devops.work_item,
mindmap.test_case (prompt_template + HMAC-подпись + per-trigger токен) —
это конфигурации webhook-триггеров, а не отдельные типы.
Конкретные интеграции могут реализовываться отдельными адаптерами, но внутри системы они должны приводиться к единому представлению события.
#Триггеры на события журнала (type=event)
«Хуки вокруг агентского цикла»: ядро хвостит собственный журнал наблюдения и запускает ран при появлении подходящего события — накопившиеся несогласованные артефакты, попытки побега из песочницы, массовый экспорт документов, события безопасности. Реализовано слушателем в cmd/agent-kernel/eventtriggers.go (интервал 15с).
Конструктор конфига (workspace.EventConfig):
{
"event_type": "artifact.review.requested",
"filter": {"actor_id": "kernel-internal", "project": "omni", "data.status": "draft"},
"prompt": "Проследить согласование…"
}
event_type— точный тип события журнала (artifact.review.requested,tool.failed,secret.issued,run.completed, …);filter— все записи должны сойтись:actor_id/actor_type/projectпо контексту события,data.<поле>— точное строковое совпадение по payload (числа сравниваются в строковой форме). Пустой фильтр — все события типа; неизвестные ключи отклоняются на сохранении (422), а не молча не совпадают;prompt— задача рана; совпавшее событие (id, актор, проект, payload) прикладывается к промпту как контекст.
Семантика срабатывания:
- Только пост-активационные события. Свежий слушатель (или первая
активация event-триггера на установке) доходит до хвоста журнала без
срабатываний — история не переигрывается, иначе каждый исторический
матч породил бы ран. Курсор —
workspace_channel_cursorс ключомjournal:events. - Идемпотентность. task id детерминирован по (trigger id, event id): повторная доставка того же события журнала (рестарт до сохранения курсора) не создаёт второй ран; REJECT_DUPLICATE на workflow id — вторая линия.
- Анти-цикл. События ранов, запущенных триггерами (run id с префиксом
trigger-), никогда не срабатывают триггеры — иначе триггер на «свои» же события закольцуется. - Без триггеров — нет хвоста. Цикл сначала перечисляет включённые event-триггеры; когда их нет, журнал не читается вовсе.
- Путь запуска повторяет расписание: resolve агента → конфиг → политика →
fire-time авторизация (§21) → task → workflow → run; исходы — в журнале
(
trigger.accepted/trigger.rejected, видимы в карточке доставок).
Открытые ограничения: срабатывание поштучно (нет агрегации «N событий за окно» — пороговые сценарии типа массового экспорта выражаются фильтром по payload либо ждут агрегатор); оргпринадлежность проверяется по привязке самого триггера (§18), события журнала оргединицу не несут.
#Нормализованное событие
Любой источник должен преобразовываться во внутреннее событие примерно следующего вида:
Event
├── id
├── type
├── source
├── timestamp
├── actor
├── tenant
├── payload
└── metadata
Агент не должен зависеть от формата исходного webhook.
#2. Создание операции
После успешного прохождения триггера система создаёт обычную агентскую операцию (Agent Run).
Важно, чтобы операция, запущенная webhook'ом, ничем принципиально не отличалась от операции, запущенной другим способом.
У операции должны быть доступны:
- источник запуска;
- исходное событие;
- агент;
- системный промпт;
- навыки;
- MCP;
- sandbox;
- доступные инструменты;
- лимиты;
- контекст запуска;
- история всех последующих взаимодействий.
Например:
Run
├── trigger
│ ├── type: azure_devops.pull_request
│ └── event_id: ...
├── agent: reviewer
├── input
│ ├── repository
│ ├── pull_request
│ └── ...
└── execution
#3. Маппинг события в задачу агента
Для каждого триггера необходимо определить, какие данные события передаются агенту.
Например:
Azure Pull Request
│
├── repository
├── pull request
├── source branch
├── target branch
├── author
└── changes
│
▼
Reviewer Agent
Триггер не должен содержать готовый системный промпт агента.
Он определяет:
какое событие произошло и какую агентскую операцию необходимо запустить.
Конкретная логика поведения должна находиться в конфигурации агента и его навыках.
#4. Идемпотентность и повторная доставка
Webhook-системы могут доставлять одно событие несколько раз.
Поэтому повторное получение одного и того же события не должно создавать несколько одинаковых операций.
Для каждого входящего события необходимо поддержать:
event_id;- источник события;
- ключ идемпотентности;
- состояние обработки;
- повторную обработку после временной ошибки.
Минимальное требование:
same source + same event_id
↓
same logical trigger
↓
не создавать второй Run
#5. Обращение агента к человеку
Агент должен получить специальный инструмент для ситуации, когда самостоятельное принятие решения невозможно.
Условно:
ask_human(...)
Это не обычное сообщение в чат.
Вызов создаёт ожидающее взаимодействие, связанное с конкретной операцией.
Минимальные параметры:
recipient
question
context
options
timeout
#Пример
Агент обнаружил два возможных варианта:
В PR изменён контракт API. Нужно выбрать, считать ли изменение совместимым с текущей версией клиента. В коде недостаточно информации.
Агент вызывает:
ask_human(
recipient = user:123,
question = "...",
options = [
"Считать совместимым",
"Считать несовместимым"
]
)
После этого выполнение операции приостанавливается.
#6. Выбор получателя
Агент не должен сам выбирать канал коммуникации.
Система должна определить:
- кому необходимо задать вопрос;
- какие каналы у этого пользователя доступны;
- какой канал является предпочтительным;
- каким образом доставить запрос.
Например:
User
├── Matrix preferred
├── Telegram enabled
└── Email enabled
При этом настройки каналов должны быть частью пользовательского профиля, а не конфигурации конкретного агента.
#Приоритет
Если пользователь настроил:
preferred channel = Matrix
запрос отправляется через Matrix.
Если канал недоступен, система может использовать следующий разрешённый канал согласно политике fallback.
Агенту не нужно знать детали Matrix, Telegram и других транспортов.
#7. Единый слой коммуникации
Необходимо отделить агентскую операцию от конкретного транспорта.
Ввести абстракцию:
Notification / Human Interaction
│
├── Matrix adapter
├── Telegram adapter
├── ...
└── future adapters
Таким образом, ask_human работает с пользователем, а не с Matrix или Telegram.
Это позволит впоследствии добавлять новые каналы без изменения агентов и их навыков.
#8. Ответ человека
Ответ должен быть связан не просто с пользователем, а с конкретным ожидающим запросом.
Human Request
├── id
├── run_id
├── recipient
├── question
├── status
│ ├── pending
│ ├── answered
│ ├── expired
│ └── cancelled
├── channel
├── created_at
├── expires_at
└── response
При получении ответа система должна:
- определить
Human Request; - сохранить ответ;
- добавить его в историю операции;
- возобновить выполнение Run;
- передать ответ агенту как результат
ask_human.
Для агента это должно выглядеть так, как будто инструмент вернул результат:
ask_human → human response
#9. Приостановка и возобновление
Операция должна поддерживать состояние:
waiting_for_human
В этом состоянии:
- агентский процесс не должен бессмысленно расходовать токены;
- обычный timeout Run не должен уничтожать ожидающую операцию;
- состояние должно переживать перезапуск сервиса;
- операция должна корректно возобновляться после ответа.
После ответа:
waiting_for_human
↓
resuming
↓
running
#10. Контекст вопроса
Запрос человеку должен быть самодостаточным.
Нельзя отправлять:
«Что делать с этим?»
Человек должен понимать:
- какая задача выполняется;
- почему возник вопрос;
- что уже установил агент;
- какой именно информации не хватает;
- какие варианты существуют;
- к чему приведёт выбор.
При этом человеку не следует отправлять весь технический контекст Run.
Система должна формировать компактный human-facing context.
Пример:
PR #1842
Я проверяю изменение контракта API.
В коде обнаружены два возможных варианта поведения.
Какой вариант считать правильным?
[Сохранить совместимость]
[Разрешить изменение контракта]
Основание: текущий клиент Directum Omni использует старую
сигнатуру метода.
#11. Структурированные ответы
Если вопрос допускает варианты, предпочтительно использовать структурированный ответ:
options:
- id: compatible
label: Сохранить совместимость
- id: breaking
label: Разрешить изменение
Ответ человека должен сохранять машинно-обрабатываемое значение.
Свободный текст также должен поддерживаться:
response:
type: text
value: "Считаем изменение допустимым..."
#12. Несколько вопросов
Операция может последовательно или параллельно ожидать несколько человеческих решений.
Необходимо предусмотреть:
Run
├── Human Request #1
├── Human Request #2
└── ...
При этом:
- каждый запрос имеет собственный идентификатор;
- ответ однозначно привязывается к запросу;
- Run может ожидать несколько ответов;
- политика продолжения должна быть определена явно.
На первом этапе достаточно поддержать последовательный сценарий:
ask_human
↓
wait
↓
answer
↓
continue
↓
ask_human
#13. Таймаут и отсутствие ответа
У запроса должен быть timeout.
После истечения времени система должна выполнить заданную политику:
- завершить Run с ошибкой;
- продолжить с безопасным значением по умолчанию;
- повторно уведомить человека;
- передать запрос другому пользователю;
- отменить операцию.
Важно: политика должна быть частью конфигурации операции, а не захардкожена в транспортном адаптере.
#14. Уведомления
Необходимо разделить:
#Human Interaction
Ожидается ответ, без которого операция не может продолжиться.
#Notification
Информационное сообщение, которое не блокирует операцию.
Например:
Agent Run
│
├── ask_human → blocking
│
└── notify → non-blocking
Это позволит агентам сообщать о важных промежуточных результатах, не превращая каждый комментарий в обязательное взаимодействие.
#15. Безопасность
Нельзя позволять webhook напрямую указывать произвольного пользователя для обращения.
Необходимо контролировать:
- кто может создавать триггеры;
- какой агент может запускаться;
- от имени какого пользователя может выполняться операция;
- кому агент имеет право задавать вопросы;
- какие данные можно отправлять через внешний канал;
- какие действия может разрешить ответ человека.
Особенно важно не допустить ситуацию:
malicious webhook
↓
arbitrary recipient
↓
agent sends sensitive context
#16. Наблюдаемость в Temporality
Все взаимодействия должны становиться частью единого event stream операции.
Минимально фиксировать события:
trigger.received
trigger.accepted
run.created
run.started
human.requested
human.answered
human.reasked
run.resumed
run.completed
run.failed
run.cancelled
Для human.requested желательно сохранять:
- причину обращения;
- получателя;
- канал;
- ссылку на Run;
- время ожидания;
- результат.
Это позволит в дальнейшем анализировать:
- как часто агенты не справляются самостоятельно;
- на каких этапах требуется человек;
- сколько времени операции проводят в ожидании;
- какие решения чаще всего делегируются человеку;
- каких знаний или навыков агентам систематически не хватает.
Особенно важно, чтобы обращение человека было частью траектории операции, а не отдельной системой уведомлений.
#17. Пользовательский интерфейс
В интерфейсе Temporality должна быть видна вся цепочка:
Trigger
↓
Run started
↓
Agent reasoning / tool calls
↓
Human request
↓
Waiting 14 min
↓
Human response
↓
Run resumed
↓
Completed
Для ожидающей операции необходимо показывать:
- кого ожидаем;
- через какой канал отправлен запрос;
- какой вопрос задан;
- сколько времени ожидаем;
- текущий статус.
Человек должен иметь возможность открыть Run и увидеть причину ожидания.
#18. Управление триггерами
Добавить раздел управления триггерами.
Для каждого триггера отображать:
| Поле | Значение |
|---|---|
| Название | Review Pull Request |
| Источник | Azure DevOps |
| Событие | Pull Request created |
| Агент | Reviewer |
| Состояние | Включён |
| Последний запуск | ... |
| Успешных запусков | ... |
| Требовался человек | ... |
Создание триггера должно быть ориентировано на пользователя и не требовать ручного написания большого YAML.
Предпочтительно использовать форму/мастер:
1. Откуда приходит событие?
2. Какое событие?
3. Какого агента запустить?
4. Какие данные передать?
5. Кто может быть привлечён к решению?
6. Что делать при отсутствии ответа?
Система сама формирует внутреннюю конфигурацию.
#19. MVP
В первую реализацию включить:
#Запуск
- webhook endpoint;
- нормализацию входящего события;
- создание Agent Run;
- идемпотентность;
- привязку события к Run.
#Интеграции
- Azure DevOps Pull Request;
- Azure DevOps Work Item;
- MindMap test scenario.
#Взаимодействие
ask_human;- Matrix;
- Telegram;
- пользовательский preferred channel;
- ожидание ответа;
- возобновление Run.
#Состояния
pending
running
waiting_for_human
resuming
completed
failed
cancelled
#Temporality
- события триггера;
- события человеческого взаимодействия;
- ожидание и возобновление в траектории;
- отображение в истории Run.
#20. Критерии готовности
Функциональность считается реализованной, если можно выполнить следующий сценарий end-to-end:
- В Azure DevOps создаётся Pull Request.
- Azure DevOps отправляет webhook.
- Temporality принимает событие.
- Создаётся Agent Run для Reviewer.
- Reviewer получает контекст PR и выполняет анализ.
- Агент обнаруживает неоднозначность, которую не может разрешить самостоятельно.
- Агент вызывает
ask_human. - Temporality определяет пользователя и его preferred channel.
- В Matrix или Telegram приходит понятный запрос.
- Операция переходит в
waiting_for_human. - Пользователь отвечает.
- Ответ привязывается к исходному Human Request.
- Run автоматически возобновляется.
- Агент получает ответ как результат
ask_human. - Агент завершает проверку.
- В Temporality видна полная траектория от webhook до завершения, включая ожидание человека.
- Повторная доставка исходного webhook не создаёт второй Run.
#Главное архитектурное требование
Не реализовывать это как «бот, который пишет агенту в чат».
Нужна единая модель:
Event → Agent Run → Tools → Human Interaction → Agent Run
Канал коммуникации является транспортом. Источником истины остаётся состояние и event stream операции в Temporality.