Event subscriptions

🔵 Overview of how events reach the platform and trigger agents. The mechanisms fall into pull (the platform polls itself), push (an external system sends them), and internal (core process events).

Mechanism Type What it triggers Where it is configured
Cron pull (timer) an agent turn on a schedule/interval cron: in agent.yaml / config/cron.yaml
Monitors pull (source) an agent turn on new source items config/monitors.yaml
Inbound webhook push a single agent turn via POST /v1/inbound/:template server.yaml → inbound
Workflow webhook push starting a workflow via POST /v1/workflows/<name>/webhook triggers.webhook
Reactions internal events an action on a process event (on:) config/reactions.yaml
await_event waiting an agent turn waits for an event (webhook or timer) the kind: await_event tool

Push vs pull. Push is convenient when the external system itself knows about the event (payments, CRM, CI). Pull is for when a source needs to be polled; a monitor fires only on changes, while cron fires always on schedule.

Internal events. Reactions subscribe not to external systems but to core process events (ProcessEvent): start/finish, tool calls, response content, budget, etc. — and they launch an agent, a tool, a webhook, or signal (steering of a live process).

Waiting for an event within a turn. await_event pauses the turn and waits for an external callback POST /v1/events/callback (via event_id) or a timer; async_action performs the full asynchronous flow (request → show URL → callback → result).

Result delivery. The outcome of a turn/workflow is distributed to sinks (notify): log, webhook, telegram, vk, yandex_messenger, email, messages_db, agent, workflow, storage — see Cron.

The difference between push entry points (inbound webhook, Telegram/email channels) is covered in Widget and channels.

Outward: agent action log (agent_events)

The reverse direction — an external system learns about an agent's actions. The native kind: agent_events hook keeps events in an in-memory ring buffer (no disk) at selected lifecycle points, and the GET /v1/events route under ACCESS_ADMIN exposes the log outward with long-poll and cursor drain (in batches, without losing events). The implementation is the agent_os_events module; the config fields are in Extending hooks.

Agent hook (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                        # кольцевой буфер (в памяти)

Reading:

GET /v1/events?agent=acme-agent&mode=wait&since=N

Response contract: { 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 — a non-cryptographic fingerprint of the arguments; the arguments/result themselves are not stored).

?mode= Behavior
drain (default) a batch of events from seq > since, immediately
wait long-poll: waits for an event (up to wait_ms), then returns a batch
events the last N events
version only the current seq (a cheap counter)

The client maintains a cursor — this way the batch is not lost:

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();                    // часть вытеснена буфером
}

Security. The GET /v1/events route is ACCESS_ADMIN only. The payload is minimal (no arguments/results, only args_fp); since is clamped to [0, max], limit/wait_ms are capped from above; capacity is an in-memory ring buffer.