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
- Every process event (
ProcessEvent) passes throughKernel::emit. - 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. - Rules with
on.thresholdandon.idleare evaluated by a ticker (Kernel::reaction_tick, the server calls it once every 5 s). - 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). - After execution, the core writes
ProcessEvent::ReactionFiredto 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
- ✅ Debounce / window (
debounce_secs,max_per_window+window_secs). - ✅ Cascade guard: events from processes spawned by a reaction do not
trigger other rules while
allow_cascade: false. - ✅ Best-effort: firings are in memory; they are not recoverable after a restart (durable delivery via outbox is a possible future development).
- 🔴 No semantic/embedding filters (intent/sentiment) — only keyword/regex/MiniJinja.
- All templates/tickers are core-level; we don't do admin CRUD for rules (file).
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).