Skip to content

Events

Agentrail uses a typed event stream to surface runtime execution, host lifecycle, and orchestration state changes to the client.

What Events Are

Events are the observability channel for a request. They let a UI, trace panel, or dashboard understand what the runtime is doing — without coupling to internal execution logic.

Events are not the durable state model. Use session history and application stores for state that must survive beyond the current request.

Where Events Come From

Events in Agentrail come from three sources:

1. Runtime Core Events

Emitted by the agent loop as it executes. These cover the execution narrative of a single turn. Event types follow a dotted-namespace convention (session.*, turn.*, message.*, tool.*):

EventWhen It Fires
session.startAgent loop begins
turn.startA new LLM call round begins
message.startLLM starts generating a response
message.updateLLM emits a text delta
message.endLLM finishes the response
tool.beforeA tool call is about to be executed
tool.updateA streaming tool emits a partial result
tool.afterA tool call completes (success or error)
turn.completeThe LLM call round finishes
max_turns_reachedAgent hit its turn limit
waiting_for_user_inputA tool is waiting for user interaction
session.endAgent loop finishes — includes all new messages and total usage
errorAn unrecoverable error occurred

2. Host-Level Events

Added by the host layer on top of runtime events:

EventWhen It Fires
context_compaction_startCompaction begins
context_compaction_endCompaction finishes
context_usageToken budget status after a turn

These are defined in @agentrail/app and forwarded by the stream route.

3. Orchestration-Mapped Events

When a hosted session uses orchestration, the orchestration manager emits internal events that the host maps into a stable, UI-friendly vocabulary:

EventMeaning
orchestration_run_startA multi-agent run begins
orchestration_run_completeA multi-agent run finishes
subagent_spawnedA sub-agent was created
subagent_statusA sub-agent's state changed
subagent_messageA sub-agent produced a message
subagent_job_startedA sub-agent started processing an input
subagent_job_completedA sub-agent completed an input
subagent_job_failedA sub-agent failed on an input
subagent_closedA sub-agent was closed
wait_registeredAn orchestration wait was registered
wait_resolvedAn orchestration wait resolved

The mapping is done by mapOrchestrationEvent in @agentrail/app, so UIs do not need to understand the raw internal orchestration event schema.

Event Flow

@agentrail/core      host             SSE stream             UI
───────────────      ────             ──────────             ──
session.start    ──►  forward     ──►  JSON line         ──►  start indicator
turn.start       ──►  forward     ──►                    ──►
message.update   ──►  forward     ──►                    ──►  text delta
tool.before      ──►  forward     ──►                    ──►  tool indicator
tool.after       ──►  forward     ──►                    ──►
session.end      ──►  forward     ──►                    ──►  final usage
compaction_start ──►  (host adds) ──►                    ──►  compaction badge
context_usage    ──►  (host adds) ──►                    ──►  budget bar

Event Types

The two main event type unions in @agentrail/app:

  • AgentrailHostEvent — host-level and orchestration-mapped events
  • AgentrailEvent — the full union combining runtime events, skill SSE events, and host events

Use AgentrailEvent as the single stream type when building a consumer that needs to handle everything.

Consuming Events on the Client

The stream route sends events as newline-delimited JSON over SSE. Each line is a serialized AgentrailEvent. A minimal client:

ts
const response = await fetch("/api/stream", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ message: "Hello" }),
});

const reader = response.body!.getReader();
const decoder = new TextDecoder();

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  for (const line of decoder.decode(value).split("\n").filter(Boolean)) {
    const event = JSON.parse(line);
    if (event.type === "message.update") {
      process.stdout.write(event.delta ?? "");
    }
  }
}

Design Intent

The event layer is designed to be:

  • composable — different sources (runtime, host, orchestration) feed into one stream
  • stream-friendly — events are append-only, newline-delimited JSON
  • UI-friendly — mapped events expose stable vocabulary rather than raw internal state
  • observability-first — not a replacement for persistent state

Released under the Apache 2.0 License.