Подписки на события
🔵 Обзор того, как события попадают в платформу и запускают агентов. Механизмы делятся на pull (платформа сама опрашивает), push (внешняя система присылает) и внутренние (события процессов ядра).
| Механизм | Тип | Что запускает | Где настраивается |
|---|---|---|---|
| Cron | pull (таймер) | ход агента по расписанию/интервалу | cron: в agent.yaml / config/cron.yaml |
| Мониторы | pull (источник) | ход агента на новые элементы источника | config/monitors.yaml |
| Входящий webhook | push | один ход агента по POST /v1/inbound/:template |
server.yaml → inbound |
| Webhook воркфлоу | push | запуск воркфлоу по POST /v1/workflows/<name>/webhook |
triggers.webhook |
| Реакции | внутренние события | действие на событие процесса (on:) |
config/reactions.yaml |
await_event |
ожидание | ход агента ждёт событие (webhook или таймер) | тул kind: await_event |
Push vs pull. Push удобен, когда внешняя система сама знает о событии (платежи, CRM, CI). Pull — когда надо опрашивать источник; при этом монитор стреляет только на изменения, а cron — всегда по расписанию.
Внутренние события. Реакции подписываются не на внешние
системы, а на события процессов ядра (ProcessEvent): старт/завершение, вызовы
инструментов, содержимое ответа, бюджет и т.д. — и запускают агента, инструмент,
webhook или signal (steering живого процесса).
Ожидание события внутри хода. await_event приостанавливает
ход и ждёт внешний колбэк POST /v1/events/callback (по event_id) или таймер;
async_action делает полный асинхронный флоу
(запрос → показ URL → колбэк → результат).
Доставка результата. Итог хода/воркфлоу раздаётся в sink'и (notify):
log, webhook, telegram, vk, yandex_messenger, email, messages_db,
agent, workflow, storage — см. Cron.
Разница между push-точками входа (inbound webhook, каналы Telegram/email) — в Виджет и каналы.
Наружу: журнал действий агента (agent_events)
Обратное направление — внешняя система узнаёт о действиях агента. Нативный
хук kind: agent_events держит события в in-memory кольцевом буфере (без
диска) на выбранных точках жизненного цикла, а маршрут GET /v1/events под
ACCESS_ADMIN отдаёт журнал наружу с long-poll и cursor drain (пачкой, без
потери событий). Реализация — модуль agent_os_events; поля конфига — в
Расширение хуков.
Хук агента (hook_files: [hooks/events.yaml]):
kind: agent_events
name: agent_events
agent: acme-agent
on: [after_tool_result, after_turn] # по умолчанию только after_tool_result
ignore: [settings_get] # служебные чтения не логируем
mutating: [order_create, order_cancel]
capacity: 5000 # кольцевой буфер (в памяти)
Чтение:
GET /v1/events?agent=acme-agent&mode=wait&since=N
Контракт ответа: { ok, mode, agent, name, seq, since, count, events[], last, has_more, dropped_window }. Событие: seq, ts, agent, session, user_id, tool, kind, mutating, success, args_fp, detail (args_fp — некриптографический
отпечаток аргументов; сами аргументы/результат не сохраняются).
?mode= |
Поведение |
|---|---|
drain (по умолчанию) |
пачка событий с seq > since, сразу |
wait |
long-poll: ждёт событие (до wait_ms), затем отдаёт пачку |
events |
последние N событий |
version |
только текущий seq (дешёвый счётчик) |
Клиент ведёт курсор — так пачка не теряется:
let since = 0;
for (;;) {
const r = await fetch(`/v1/events?agent=acme-agent&mode=wait&since=${since}&limit=100`,
{ headers: { Authorization: `Bearer ${adminToken}` } });
const b = await r.json();
for (const ev of b.events) handle(ev); // события по возрастанию seq
if (b.events.length) since = b.events.at(-1).seq; // сдвигаем курсор
if (b.has_more) continue; // накопилась пачка — дочитываем
if (b.dropped_window) resync(); // часть вытеснена буфером
}
Безопасность. Маршрут GET /v1/events — только ACCESS_ADMIN. Payload
минимальный (без аргументов/результатов, только args_fp); since клампится в
[0, max], limit/wait_ms зажаты сверху; capacity — кольцевой буфер в
памяти.