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.

app.ts
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.

app.ts
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.

EventKey FieldsDescription
RUN_STARTEDrunId, agentName, conversationId?, runInputRun began.
RUN_FINISHEDrunId, agentName, resultRun completed. Carries the full RunResult.
RUN_ERRORrunId, agentName, errorRun failed. error is a serializable { name, message, code? }.
RUN_ABORTEDrunId, agentNameRun was cancelled via abort signal.

Steps

The agent loop emits events at the start and end of each step.

EventKey FieldsDescription
STEP_STARTrunId, stepIndex, maxStepsStep is about to begin.
STEP_FINISHrunId, stepIndex, maxSteps, toolCallCount, terminationReason?Step completed.
STEP_ERRORrunId, stepIndex, errorStep 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's stopWhen condition 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.

EventKey FieldsDescription
TOOL_CALL_STARTtoolCallId, toolCallName, toolTargetModel requested a tool call.
TOOL_CALL_ARGStoolCallId, toolCallName, deltaArgument chunk streamed from the model.
TOOL_CALL_ENDtoolCallId, toolCallName, toolTargetAll arguments received. Execution begins (or approval is requested).
TOOL_CALL_RESULTtoolCallId, 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").

app.ts
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.

EventKey FieldsDescription
TOOL_APPROVAL_REQUIREDtoolCallId, toolCallName, toolInput, state, meta?Approval requested. state is always "requested".
TOOL_APPROVAL_UPDATEDtoolCallId, 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
app.ts
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

EventKey Fields
TEXT_MESSAGE_STARTmessageId, role
TEXT_MESSAGE_CONTENTmessageId, delta (string)
TEXT_MESSAGE_ENDmessageId
app.ts
for await (const event of stream.events) {
  if (event.type === "TEXT_MESSAGE_CONTENT") {
    process.stdout.write(event.delta);
  }
}

Image

EventKey Fields
IMAGE_MESSAGE_STARTmessageId, role
IMAGE_MESSAGE_CONTENTmessageId, delta ({ kind: "url", url } or { kind: "base64", data, mimeType })
IMAGE_MESSAGE_ENDmessageId

Audio

EventKey Fields
AUDIO_MESSAGE_STARTmessageId, role
AUDIO_MESSAGE_CONTENTmessageId, delta ({ kind: "base64", data, mimeType })
AUDIO_MESSAGE_ENDmessageId

Video

EventKey Fields
VIDEO_MESSAGE_STARTmessageId, role
VIDEO_MESSAGE_CONTENTmessageId, delta ({ kind: "url", url } or { kind: "base64", data, mimeType })
VIDEO_MESSAGE_ENDmessageId

Transcript

EventKey Fields
TRANSCRIPT_MESSAGE_STARTmessageId, role
TRANSCRIPT_MESSAGE_CONTENTmessageId, delta (string)
TRANSCRIPT_MESSAGE_SEGMENTmessageId, segment ({ id, start, end, text, speaker? })
TRANSCRIPT_MESSAGE_ENDmessageId

Transcript has an extra SEGMENT event with a finalized segment object including timing and optional speaker identification.

Reasoning

EventKey Fields
REASONING_MESSAGE_STARTmessageId, role, visibility
REASONING_MESSAGE_CONTENTmessageId, delta (string), visibility
REASONING_MESSAGE_ENDmessageId, visibility

visibility is "summary" or "full", depending on what the model exposes.

Embedding

EventKey Fields
EMBEDDING_MESSAGE_STARTmessageId, role
EMBEDDING_MESSAGE_CONTENTmessageId, delta (number[])
EMBEDDING_MESSAGE_ENDmessageId

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.

tools.ts
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.