Events Reference
Agentrail hosts emit a unified stream of typed events for UI integration, observability, and trace persistence.
When To Read This Page
Read this page when:
- you are building a streaming UI or trace panel
- you need to handle specific event types on the client
- you want to understand which events are persisted to the trace log
- you need to integrate orchestration visibility into a dashboard
Type Hierarchy
The central event union is:
import type { AgentrailEvent, AgentrailHostEvent } from "@agentrail/app";
import type { RuntimeEvent } from "@agentrail/core";
// AgentrailEvent is the top-level union for all SSE payloads
type AgentrailEvent = RuntimeEvent | ExtendedSseEvent | AgentrailHostEvent;
// AgentrailHostEvent covers everything above the runtime layer
type AgentrailHostEvent =
| AgentrailContextCompactionStartEvent
| AgentrailContextCompactionEndEvent
| AgentrailContextUsageEvent
| AgentrailErrorEvent
| AgentrailOrchestrationRunStartEvent
| AgentrailOrchestrationRunCompleteEvent
| AgentrailSubagentSpawnedEvent
| AgentrailSubagentStatusEvent
| AgentrailSubagentJobStartedEvent
| AgentrailSubagentJobCompletedEvent
| AgentrailSubagentJobFailedEvent
| AgentrailSubagentMessageEvent
| AgentrailWaitRegisteredEvent
| AgentrailWaitResolvedEvent
| AgentrailSubagentClosedEvent;For most UI consumers, AgentrailEvent is the type you want to annotate a parsed SSE payload with.
Host Events
These host events are generated by the host layer, not the runtime:
context_compaction_start
interface AgentrailContextCompactionStartEvent {
type: "context_compaction_start";
}Emitted when the host begins compacting old session history via the summarize function.
context_compaction_end
interface AgentrailContextCompactionEndEvent {
type: "context_compaction_end";
}Emitted when compaction is complete and the summarized history has been persisted.
context_usage
interface AgentrailContextUsageEvent {
type: "context_usage";
inputTokens: number;
outputTokens: number;
budgetUsedPct?: number; // 0–100; requires contextWindow on the profile
}Emitted after each turn with token counts. budgetUsedPct is set if the profile's contextWindow is configured.
error
interface AgentrailErrorEvent {
type: "error";
error: { message: string };
}Emitted when the host encounters an error it cannot recover from during a stream.
Orchestration-Mapped Events
These events are translated from internal orchestration state by mapOrchestrationEvent. They let a UI track sub-agent progress without coupling to the raw orchestration schema.
| Event type | When emitted |
|---|---|
orchestration_run_start | A new orchestration run is initialized |
orchestration_run_complete | The run has finished (success or error) |
subagent_spawned | A new sub-agent has been created by the orchestrator |
subagent_status | A sub-agent's status changes (e.g. idle → working → done) |
subagent_job_started | A sub-agent picks up a job to process |
subagent_job_completed | A sub-agent successfully completes a job |
subagent_job_failed | A sub-agent job fails |
subagent_message | A message is queued to a sub-agent's mailbox |
wait_registered | A WaitCondition is registered by a sub-agent |
wait_resolved | A WaitCondition is resolved |
subagent_closed | A sub-agent has been closed |
interface AgentrailOrchestrationRunStartEvent {
type: "orchestration_run_start";
runId: string;
initialTask: Record<string, unknown>;
}
interface AgentrailOrchestrationRunCompleteEvent {
type: "orchestration_run_complete";
runId: string;
status: string;
error?: unknown;
}
interface AgentrailSubagentSpawnedEvent {
type: "subagent_spawned";
agent: Record<string, unknown>;
}
interface AgentrailSubagentStatusEvent {
type: "subagent_status";
agentId: string;
status: string;
}
interface AgentrailSubagentJobStartedEvent {
type: "subagent_job_started";
agentId: string;
jobId: string;
inputIds: string[];
}
interface AgentrailSubagentJobCompletedEvent {
type: "subagent_job_completed";
agentId: string;
job: Record<string, unknown>;
}
interface AgentrailSubagentJobFailedEvent {
type: "subagent_job_failed";
agentId: string;
job: Record<string, unknown>;
}
interface AgentrailSubagentMessageEvent {
type: "subagent_message";
agentId: string;
input: unknown;
}
interface AgentrailWaitRegisteredEvent {
type: "wait_registered";
wait: unknown;
}
interface AgentrailWaitResolvedEvent {
type: "wait_resolved";
waitId: string;
resolution: unknown;
}
interface AgentrailSubagentClosedEvent {
type: "subagent_closed";
agentId: string;
reason?: string;
finalStatus?: string;
}Runtime Events (Summary)
Runtime events come from @agentrail/core and are emitted during agent execution. Types use a dotted-namespace convention (session.*, turn.*, message.*, tool.*). The most important ones for UI consumers:
| Event type | When emitted |
|---|---|
session.start | Agent begins processing a request |
session.end | Agent finishes (all turns complete) |
turn.start | A new LLM turn starts |
turn.complete | A turn finishes |
compaction | In-loop reactive compaction rewrites history |
message.update | Streaming text delta from the LLM |
tool.before | A tool call is about to be dispatched (after any onBeforeToolCall plugin hooks have run) |
tool.after | A tool invocation completes |
waiting_for_user_input | The agent is paused waiting for user input |
permission_request | A tool's checkPermissions returned "ask"; execution is blocked pending host approval |
skill_start / skill_end | A skill sub-agent is invoked |
Tracing fields on every RuntimeEvent
Every RuntimeEvent carries three additional fields for distributed tracing and log correlation:
interface RuntimeTracingFields {
/** Stable correlation ID for the entire request chain.
* Shared by the root agent and all spawned sub-agents.
* In the app layer this equals the route-level traceId / requestTraceId. */
readonly chainId: string;
/** Nesting depth: 0 = root agent, 1 = direct sub-agent, etc. */
readonly depth: number;
/** Zero-based turn counter within this agent's loop.
* Pre-loop events (session.start, initial turn.start, initial prompt messages)
* carry turnIndex = 0. */
readonly turnIndex: number;
}These fields are automatically stamped by agentLoop and propagated to sub-agents via the orchestration subsystem. You can use chainId to join all events across a multi-agent invocation in your telemetry backend.
tool.before field reference
{
type: "tool.before";
toolCallId: string;
toolName: string;
/**
* Effective input passed to the tool.
* When a `onBeforeToolCall` plugin hook modifies the arguments, this field
* reflects the modified value rather than the raw model-generated arguments.
*/
args: unknown;
/**
* Original model-generated arguments, always equal to the raw tool call
* payload from the model. Identical to `args` when no plugin modified the
* input. Use this field for audit logging and debugging.
*/
rawArgs: unknown;
}Note for consumers that used
argsfor audit purposes: if you have a plugin or tracing integration that readstool.before.argsand relies on it being the unmodified model output, switch torawArgs.argsnow reflects the effective (possibly plugin-modified) arguments that were actually executed.
permission_request field reference
Emitted when a tool's checkPermissions hook returns "ask". Execution is blocked until the host provides an interactive approval mechanism; in the current release, the tool call is always denied and the model receives an error result.
{
type: "permission_request";
toolCallId: string;
toolName: string;
/** Optional human-readable reason why approval is being requested. */
reason?: string;
}This event is persisted to the trace log (TRACE_PERSISTED_EVENT_TYPES) so that audit tooling can record which tool calls required permission.
Trace Persistence
Not all events are persisted to the trace log. High-frequency streaming events (message.update, message.start, message.end) are intentionally excluded to avoid log bloat.
The TRACE_PERSISTED_EVENT_TYPES set defines what gets persisted:
import { TRACE_PERSISTED_EVENT_TYPES } from "@agentrail/app";
// Runtime / skill events that are persisted:
// "session.start", "session.end", "turn.start", "turn.complete", "compaction",
// "tool.before", "tool.after",
// "skill_start", "skill_end", "waiting_for_user_input",
// "context_compaction_start", "context_compaction_end", "error"
//
// Orchestration-mapped events (all persisted):
// "orchestration_run_start", "orchestration_run_complete",
// "subagent_spawned", "subagent_status",
// "subagent_job_started", "subagent_job_completed", "subagent_job_failed",
// "subagent_message", "wait_registered", "wait_resolved", "subagent_closed"
if (TRACE_PERSISTED_EVENT_TYPES.has(event.type)) {
await persistTraceEvent(event);
}context_compaction_start and context_compaction_end represent request-boundary persisted compaction. compaction represents in-loop reactive compaction inside the runtime and does not rewrite messages.jsonl.
Consuming Events
Browser (fetch + ReadableStream)
import type { AgentrailEvent } from "@agentrail/app";
async function streamChat(message: string, sessionId?: string) {
const response = await fetch("/api/stream", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
message,
tenantId: "default",
userId: "user-1",
sessionId,
}),
});
// Capture the session ID returned by the host
const newSessionId = response.headers.get("X-Session-Id");
const reader = response.body!.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
for (const line of chunk.split("\n")) {
if (!line.startsWith("data: ")) continue;
const raw = line.slice(6).trim();
if (!raw || raw === "[DONE]") continue;
const event = JSON.parse(raw) as AgentrailEvent;
handleEvent(event);
}
}
return newSessionId;
}
function handleEvent(event: AgentrailEvent) {
switch (event.type) {
case "message.update":
appendText(event.delta ?? "");
break;
case "tool.before":
showToolSpinner(event.toolName ?? "tool");
break;
case "tool.after":
hideToolSpinner();
break;
case "context_usage":
updateTokenBudget(event.budgetUsedPct);
break;
case "context_compaction_start":
showCompactionIndicator();
break;
case "context_compaction_end":
hideCompactionIndicator();
break;
case "orchestration_run_start":
initOrchestrationPanel(event.runId);
break;
case "subagent_spawned":
addSubagentCard(event.agent);
break;
case "subagent_status":
updateSubagentStatus(event.agentId, event.status);
break;
case "session.end":
markComplete();
break;
case "error":
showError(event.error.message);
break;
}
}Node.js Server-Side
import { mapOrchestrationEvent } from "@agentrail/app";
// Use the same line-splitting approach; node-fetch or undici support streaming
const res = await fetch("http://localhost:3000/api/stream", { ... });
const reader = res.body!.getReader();
// ... same loop as aboveWorkflowTraceEventEnvelope
The stream route can optionally call onTraceEvent to persist a structured envelope around each trace-eligible event:
interface WorkflowTraceEventEnvelope {
id: string; // "{source}-{timestamp}-{seq}"
timestamp: string; // ISO 8601
sequence: number;
source: "runtime" | "orchestration";
event: Record<string, unknown>;
}This is what the playground server uses to persist its trace log alongside session history.
Design Intent
The event layer is meant to be:
- composable: you can consume only the event types you care about
- stream-friendly: events are newline-delimited JSON, easy to parse in any language
- UI-friendly: orchestration events are pre-mapped so UIs don't need to understand internal orchestration schema
- append-only: events represent observations, not mutable state
Events are not meant to replace:
- persisted session history (use
appendMessagesfor that) - workflow state stores
- domain-specific application state