Reactive triggers (reactions)

🔵 "Event → action" rules: the core watches the ProcessEvent stream and, when an event matches a filter, performs an action — launches an agent, a workflow, calls a tool, sends a webhook, does steering of a process, or injects a message into someone's context (inject).

The configuration file is config/reactions.yaml (loaded on boot if it exists). Rules are executed by the core (agent_os_core::reaction + Kernel::fire_reactions).

Legend: ✅ implemented.


29.1 How it works

  1. Every process event (ProcessEvent) passes through Kernel::emit.
  2. For event rules (on.kind / on.template / on.tool / on.content / on.when) the core matches the filter and launches the action in a separate task — the emitter is not blocked.
  3. Rules with on.threshold and on.idle are evaluated by a ticker (Kernel::reaction_tick, the server calls it once every 5 s).
  4. Before the action, the rule passes debounce and a window limit; events from processes spawned by a reaction do not cause a cascade if the rule has allow_cascade: false (by default).
  5. After execution, the core writes ProcessEvent::ReactionFired to the audit.

29.2 Rule structure

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 — the event tag from 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_*, etc.

Filters are combined with AND. An empty on: {} matches any event (careful).

29.3 Actions

type Fields What it does
spawn_agent template, prompt, session? launches an agent from a template with a rendered prompt; session — session key (default reaction:<rule>:<pid>)
run_workflow name, input? launches a workflow (trigger: reaction)
call_tool tool, args? calls a tool via execute_tool_guarded
webhook method? (POST), url, headers?, json? outgoing HTTP request
signal target?, signal steering: pause / resume / interrupt / terminate
inject target?, role? (system), content, run_turn? injects a message into a process's context (e.g. the parent's); run_turn — immediately start its turn

signal.target / inject.target: source (by default — the process that is the source of the event), parent (the source's parent), { pid: "..." } or { template: "..." } (all sessions of the template).

All string fields of actions are MiniJinja templates; the context provides the variable event (the event JSON, or a synthetic object for ticker rules: { "type": "threshold" | "idle", ... }).

29.4 Examples

Keyword routing. A complaint in a message → a separate escalation agent:

reactions:
  - name: complaint
    on:
      kind: user_message
      content: { field: user, keywords: ["жалоба", "верните"], mode: any }
    action:
      type: spawn_agent
      template: support-escalation
      prompt: |
        Пользователь написал: «{{ event.content }}»
        Разбери жалобу и подготовь ответ.

Payment failed → refund workflow + notify 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

Error spike (3 in 5 min) → terminate the process and call a debugger:

  - 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: "Разбери всплеск ошибок" }

Silence: no activity for sales > 30 min → a nudge:

  - name: sales_idle
    on: { idle: { secs: 1800, template: sales } }
    action: { type: spawn_agent, template: sales, prompt: "Напомни о зависших лидах" }

Child crashed → notify the parent (inject into its context) and give a hint:

  - name: notify_parent
    on: { kind: tool_result, tool: run_command, success: false }
    action:
      type: inject
      target: parent
      role: system
      content: "Дочерний агент: команда упала — {{ event.content }}"
      run_turn: true

29.5 Security and limitations

29.6 Core 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

Rules are also parsed from a string: reaction::load_reactions_str(yaml).