31. Реактивные триггеры (reactions)

🔵 Правила «событие → действие»: ядро наблюдает поток ProcessEvent и, когда событие матчится фильтром, выполняет действие — запускает агента, воркфлоу, вызывает инструмент, шлёт webhook или «рулит» (steering) процесс.

Файл конфигурации — config/reactions.yaml (грузится на boot, если существует). Правила исполняет ядро (agent_os_core::reaction + Kernel::fire_reactions).

Обозначения: ✅ реализовано.


29.1 Как это работает

  1. Каждое событие процесса (ProcessEvent) проходит через Kernel::emit.
  2. Для событийных правил (on.kind / on.template / on.tool / on.content / on.when) ядро матчит фильтр и запускает действие в отдельной задаче — эмиттер не блокируется.
  3. Правила с on.threshold и on.idle вычисляются тикером (Kernel::reaction_tick, сервер вызывает раз в 5 c).
  4. Перед действием правило проходит debounce и лимит окна; события из процессов, порождённых реакцией, не вызывают каскад, если у правила allow_cascade: false (по умолчанию).
  5. После выполнения ядро пишет 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 Безопасность и ограничения

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