Events
Observe what happens during an agent run in real time.
Events stream what happens during a run as it happens. Every event has a type and a timestamp. Most apps only handle a small subset.
Consuming Events
Streaming
Use stream.events to iterate over events as they arrive.
const stream = await app.stream("assistant", {
input: "What's the weather in Tokyo?",
});
for await (const event of stream.events) {
switch (event.type) {
case "TEXT_MESSAGE_CONTENT":
process.stdout.write(event.delta);
break;
case "TOOL_CALL_RESULT":
console.log("Tool result:", event.result);
break;
case "RUN_FINISHED":
console.log("Done");
break;
}
}The stream object also exposes runId and a result promise for the final run result.
onEvent callback
Use onEvent on run options when you don't need the full stream handle.
await app.run("assistant", {
input: "Summarize this document...",
onEvent: (event) => {
if (event.type === "STEP_FINISH") {
console.log(`Step ${event.stepIndex + 1} done (${event.terminationReason ?? "continuing"})`);
}
},
});Run Lifecycle
These events mark the boundaries of a run.
| Event | Key Fields | Description |
|---|---|---|
RUN_STARTED | runId, agentName, conversationId?, runInput | Run began. |
RUN_FINISHED | runId, agentName, result | Run completed. Carries the full RunResult. |
RUN_ERROR | runId, agentName, error | Run failed. error is a serializable { name, message, code? }. |
RUN_ABORTED | runId, agentName | Run was cancelled via abort signal. |
Steps
The agent loop emits events at the start and end of each step.
| Event | Key Fields | Description |
|---|---|---|
STEP_START | runId, stepIndex, maxSteps | Step is about to begin. |
STEP_FINISH | runId, stepIndex, maxSteps, toolCallCount, terminationReason? | Step completed. |
STEP_ERROR | runId, stepIndex, error | Step failed. |
When STEP_FINISH also ends the run, terminationReason is set to one of:
"no_tool_calls": model finished naturally without requesting tools"stop_when": the agent'sstopWhencondition returned true"max_steps": step limit reached
Tool Calls
These events cover the full lifecycle of a tool call, from streamed arguments to the final result.
| Event | Key Fields | Description |
|---|---|---|
TOOL_CALL_START | toolCallId, toolCallName, toolTarget | Model requested a tool call. |
TOOL_CALL_ARGS | toolCallId, toolCallName, delta | Argument chunk streamed from the model. |
TOOL_CALL_END | toolCallId, toolCallName, toolTarget | All arguments received. Execution begins (or approval is requested). |
TOOL_CALL_RESULT | toolCallId, toolCallName, result, isError?, errorKind? | Tool finished. isError and errorKind are set when the result is a tool error sent back to the model. |
Every tool event includes runId, agentName, parentMessageId, and toolTarget ("server", "client", or "hosted").
for await (const event of stream.events) {
if (event.type === "TOOL_CALL_START") {
console.log(`Calling ${event.toolCallName} (${event.toolTarget})`);
}
if (event.type === "TOOL_CALL_RESULT" && event.isError) {
console.warn(`Tool error (${event.errorKind}):`, event.result);
}
}Tool Approvals
When a tool requires human approval, two event types track the flow.
| Event | Key Fields | Description |
|---|---|---|
TOOL_APPROVAL_REQUIRED | toolCallId, toolCallName, toolInput, state, meta? | Approval requested. state is always "requested". |
TOOL_APPROVAL_UPDATED | toolCallId, toolCallName, toolInput, state, meta?, note?, actorId? | State changed. |
TOOL_APPROVAL_UPDATED carries a state field with one of four values:
"requested": waiting for a decision"approved": human approved, tool will execute"denied": human denied, tool call is skipped"expired": approval timed out
for await (const event of stream.events) {
if (event.type === "TOOL_APPROVAL_REQUIRED") {
showApprovalUI(event.toolCallName, event.meta);
}
if (event.type === "TOOL_APPROVAL_UPDATED" && event.state !== "requested") {
closeApprovalUI(event.toolCallId, event.state);
}
}See Human in the Loop for the full approval lifecycle and how to submit decisions.
Message Content
Model output streams incrementally as START → CONTENT → END events. Build your UI to append chunks as they arrive.
Text
| Event | Key Fields |
|---|---|
TEXT_MESSAGE_START | messageId, role |
TEXT_MESSAGE_CONTENT | messageId, delta (string) |
TEXT_MESSAGE_END | messageId |
for await (const event of stream.events) {
if (event.type === "TEXT_MESSAGE_CONTENT") {
process.stdout.write(event.delta);
}
}Image
| Event | Key Fields |
|---|---|
IMAGE_MESSAGE_START | messageId, role |
IMAGE_MESSAGE_CONTENT | messageId, delta ({ kind: "url", url } or { kind: "base64", data, mimeType }) |
IMAGE_MESSAGE_END | messageId |
Audio
| Event | Key Fields |
|---|---|
AUDIO_MESSAGE_START | messageId, role |
AUDIO_MESSAGE_CONTENT | messageId, delta ({ kind: "base64", data, mimeType }) |
AUDIO_MESSAGE_END | messageId |
Video
| Event | Key Fields |
|---|---|
VIDEO_MESSAGE_START | messageId, role |
VIDEO_MESSAGE_CONTENT | messageId, delta ({ kind: "url", url } or { kind: "base64", data, mimeType }) |
VIDEO_MESSAGE_END | messageId |
Transcript
| Event | Key Fields |
|---|---|
TRANSCRIPT_MESSAGE_START | messageId, role |
TRANSCRIPT_MESSAGE_CONTENT | messageId, delta (string) |
TRANSCRIPT_MESSAGE_SEGMENT | messageId, segment ({ id, start, end, text, speaker? }) |
TRANSCRIPT_MESSAGE_END | messageId |
Transcript has an extra SEGMENT event with a finalized segment object including timing and optional speaker identification.
Reasoning
| Event | Key Fields |
|---|---|
REASONING_MESSAGE_START | messageId, role, visibility |
REASONING_MESSAGE_CONTENT | messageId, delta (string), visibility |
REASONING_MESSAGE_END | messageId, visibility |
visibility is "summary" or "full", depending on what the model exposes.
Embedding
| Event | Key Fields |
|---|---|
EMBEDDING_MESSAGE_START | messageId, role |
EMBEDDING_MESSAGE_CONTENT | messageId, delta (number[]) |
EMBEDDING_MESSAGE_END | messageId |
Custom Data
Use DATA_PART to emit your own structured data during a run. This is useful for progress updates, status changes, or any app-specific payload.
const analyzeTool = analyze.server(async ({ query }, { emit }) => {
await emit({ type: "DATA_PART", data: { stage: "searching" }, timestamp: Date.now() });
const results = await search(query);
await emit({ type: "DATA_PART", data: { stage: "analyzing" }, timestamp: Date.now() });
return summarize(results);
});DATA_PART events have an optional id field for grouping related updates, and a data field with any serializable value.
Plugin Events
Plugins can observe events via onEvent and transform them via event middleware. See Plugins for details.