31. Реактивные триггеры (reactions)
🔵 Правила «событие → действие»: ядро наблюдает поток ProcessEvent и,
когда событие матчится фильтром, выполняет действие — запускает агента,
воркфлоу, вызывает инструмент, шлёт webhook или «рулит» (steering) процесс.
Файл конфигурации — config/reactions.yaml (грузится на boot, если существует).
Правила исполняет ядро (agent_os_core::reaction + Kernel::fire_reactions).
Обозначения: ✅ реализовано.
29.1 Как это работает
- Каждое событие процесса (
ProcessEvent) проходит черезKernel::emit. - Для событийных правил (
on.kind/on.template/on.tool/on.content/on.when) ядро матчит фильтр и запускает действие в отдельной задаче — эмиттер не блокируется. - Правила с
on.thresholdиon.idleвычисляются тикером (Kernel::reaction_tick, сервер вызывает раз в 5 c). - Перед действием правило проходит debounce и лимит окна; события из
процессов, порождённых реакцией, не вызывают каскад, если у правила
allow_cascade: false(по умолчанию). - После выполнения ядро пишет
ProcessEvent::ReactionFiredв аудит.
29.2 Структура правила
reactions:
- name: complaint-escalation # уникальное имя (debounce/окно)
enabled: true
on:
kind: user_message # event_type
template: support # шаблон агента-источника (опц.)
user_id: "tg:123" # user id (опц.)
tool: payment # для tool_call / tool_result (опц.)
success: false # для tool_result (опц.)
content: # текстовый фильтр (опц.)
field: user # any | user | assistant | tool_result
keywords: ["жалоба", "вернуть"]
mode: any # any | all
regex: "\\b\\d{4}\\b" # опц.
negate: false
case_insensitive: true
when: "event.success == false" # MiniJinja над `event` (опц.)
# либо ticker-условия:
threshold: { kind: error, count: 3, window_secs: 300 }
idle: { secs: 1800, template: sales, state: blocked }
action: { ... }
debounce_secs: 0 # мин. интервал между срабатываниями
max_per_window: 5 # лимит срабатываний
window_secs: 60
allow_cascade: false # реагировать на события от реакций
kind — тег события из ProcessEvent::event_type(): turn_start,
turn_end, tool_call, tool_result, error, terminated, spawned,
state_change, budget_exhausted, rate_limited, schedule_blocked,
session_created, session_closed, signal, user_message,
assistant_message, reaction_fired, workflow_step_* и т.д.
Фильтры объединяются по AND. Пустой on: {} матчит любое событие (осторожно).
29.3 Действия
type |
Поля | Что делает |
|---|---|---|
spawn_agent |
template, prompt, session? |
запускает агента по шаблону с отрендеренным промптом; session — ключ сессии (по умолчанию reaction:<rule>:<pid>) |
run_workflow |
name, input? |
запускает воркфлоу (trigger: reaction) |
call_tool |
tool, args? |
вызывает инструмент через execute_tool_guarded |
webhook |
method? (POST), url, headers?, json? |
исходящий HTTP-запрос |
signal |
target?, signal |
steering: pause / resume / interrupt / terminate |
signal.target: source (по умолчанию — процесс-источник события),
{ pid: "..." } или { template: "..." } (все сессии шаблона).
Все строковые поля действий — MiniJinja-шаблоны; в контексте доступна
переменная event (JSON события, либо синтетический объект для ticker-правил:
{ "type": "threshold" | "idle", ... }).
29.4 Примеры
Keyword-маршрутизация. Жалоба в сообщении → отдельный агент-эскалатор:
reactions:
- name: complaint
on:
kind: user_message
content: { field: user, keywords: ["жалоба", "верните"], mode: any }
action:
type: spawn_agent
template: support-escalation
prompt: |
Пользователь написал: «{{ event.content }}»
Разбери жалобу и подготовь ответ.
Платёж не прошёл → воркфлоу возврата + уведомление ops:
- name: pay_fail
on: { kind: tool_result, tool: payment, success: false }
action: { type: run_workflow, name: refund-check, input: { tool: "{{ event.name }}" } }
debounce_secs: 10
Всплеск ошибок (3 за 5 мин) → терминировать процесс и позвать отладчика:
- name: error_burst
on: { threshold: { kind: error, count: 3, window_secs: 300 } }
action: { type: signal, signal: terminate }
- name: spawn_debugger
on: { threshold: { kind: error, count: 3, window_secs: 300 } }
action: { type: spawn_agent, template: debugger, prompt: "Разбери всплеск ошибок" }
Тишина: нет активности у sales > 30 мин → пинок:
- name: sales_idle
on: { idle: { secs: 1800, template: sales } }
action: { type: spawn_agent, template: sales, prompt: "Напомни о зависших лидах" }
29.5 Безопасность и ограничения
- ✅ Debounce / окно (
debounce_secs,max_per_window+window_secs). - ✅ Cascade guard: события из процессов, порождённых реакцией, не
триггерят другие правила, пока
allow_cascade: false. - ✅ Best-effort: срабатывания в памяти; после рестарта невосстановимы (дуrable-доставка через outbox — возможное развитие).
- 🔴 Нет семантических/embedding-фильтров (intent/sentiment) — только keyword/regex/MiniJinja.
- Все шаблоны/тикеры ядровые; admin CRUD для правил не делаем (файл).
29.6 Ядерный API
kernel.load_reactions(rules).await; // заменить набор (boot / reload)
kernel.add_reaction(rule).await; // добавить/заменить по имени
kernel.list_reactions().await; // текущий набор
kernel.clear_reactions().await;
kernel.reaction_tick().await; // прогнать threshold/idle (тикер)
kernel.set_self_weak(Arc::downgrade(&kernel)); // один раз на boot
Правила также парсятся из строки: reaction::load_reactions_str(yaml).