Skip to main content

Overview

Events are the units of information that flow through harnesses. Every harness invocation yields a sequence of events — from streaming text chunks to tool calls to usage metrics. Events form an immutable log that can be reduced into a conversation graph. All events carry two fields for graph construction:
runId
string
required
Identifies the harness run that produced this event. Each agent invocation gets a unique runId (a UUID v7).
parentId
string
Links this event to its parent. For subagents, parentId is the tool call ID that spawned them. For top-level agents, parentId is undefined.

Event types

Events are defined as a discriminated union in packages/ai/types.ts:77. The type field determines which other fields are present.

Content events

These events carry the actual LLM output:
A chunk of streamed text content from the model.
{
  type: "text",
  runId: string,
  parentId?: string,
  id: string,        // Stable across chunks of the same text stream
  content: string    // The text fragment
}
Example:
let fullText = "";
for await (const event of harness.invoke(params)) {
  if (event.type === "text") {
    fullText += event.content;
    process.stdout.write(event.content);
  }
}
Emitted by: Provider harnesses (e.g., harness/providers/zen.ts:269)
Passthrough: Agent harness re-yields text events with provider’s runId

Tool events

These events track tool call requests and results:
The model is requesting a tool invocation.
{
  type: "tool_call",
  runId: string,
  parentId?: string,
  id: string,        // Tool call identifier (namespaced by agent)
  name: string,      // Tool name
  input: unknown     // Parsed arguments (or { __toolParseError, parseError, rawArguments } on malformed JSON)
}
Example:
for await (const event of agent.invoke(params)) {
  if (event.type === "tool_call") {
    console.log(`Calling ${event.name} with`, event.input);
  }
}
Emitted by:
  • Provider harnesses yield raw tool calls after the stream ends
  • Agent harness re-yields approved calls with namespaced IDs (format: {agentRunId}/{rawId})
If the model emits malformed JSON for tool arguments, input will be an object with __toolParseError: true, parseError, and rawArguments fields. The agent harness will yield a tool_result with the error and continue.

Lifecycle events

Marks the beginning of a harness run.
{
  type: "harness_start",
  runId: string,
  parentId?: string,
  depth?: number,         // Recursion depth for nested RLM sessions
  maxIterations?: number  // Configured iteration limit
}
Emitted by: Agent harness (harness/agent.ts:51), RLM harness

System events

Token usage for one LLM call.
{
  type: "usage",
  runId: string,
  parentId?: string,
  inputTokens: number,        // Prompt tokens consumed
  outputTokens: number,       // Completion tokens generated
  cacheReadTokens?: number,   // Tokens read from cache
  cacheCreationTokens?: number // Tokens written to cache
}
Example:
let totalInput = 0, totalOutput = 0;
for await (const event of agent.invoke(params)) {
  if (event.type === "usage") {
    totalInput += event.inputTokens;
    totalOutput += event.outputTokens;
  }
}
console.log(`Total: ${totalInput} in / ${totalOutput} out`);
Emitted by: Provider harnesses (e.g., harness/providers/zen.ts:325)
Passthrough: Agent harness re-yields usage events

RLM events

These events are specific to the Recursive Language Model harness:
Code extracted from the model’s response, about to be executed.
{
  type: "repl_input",
  runId: string,
  id: string,
  parentId?: string,
  code: string,       // The JavaScript code to execute
  iteration?: number  // Zero-based loop index
}

Event sequences

Understanding typical event sequences helps you handle them correctly:

Simple LLM call (provider only)

1. text (streamed chunks)
2. text
3. text
...
N. usage

LLM call with tool request (provider only)

1. text (streamed response)
2. text
...
N-1. tool_call (after stream ends)
N. usage

Agent with single tool call

1. harness_start
2. text (from provider)
3. tool_call (from provider)
4. usage (from provider)
5. relay (permission request, agent pauses)
   ... (waiting for respond()) ...
6. tool_call (re-emitted after approval)
7. tool_result (after execution)
8. text (next LLM call)
9. usage
10. harness_end

Agent with concurrent tool calls

1. harness_start
2. text
3. tool_call (id: tc-1)
4. tool_call (id: tc-2)
5. usage
6. relay (for tc-1)
7. relay (for tc-2)
   ... (permission phase, sequential) ...
8. tool_call (tc-1 re-emitted)
9. tool_call (tc-2 re-emitted)
   ... (execution phase, concurrent) ...
10. tool_result (tc-1)  // May arrive in any order
11. tool_result (tc-2)
12. text (next iteration)
13. usage
14. harness_end

Event accumulation patterns

Text and reasoning events arrive incrementally. Accumulate by id:
const textById = new Map<string, string>();

for await (const event of harness.invoke(params)) {
  if (event.type === "text") {
    const current = textById.get(event.id) ?? "";
    textById.set(event.id, current + event.content);
  }
}
Match tool calls with their results using the id field:
const toolCalls = new Map();
const toolResults = new Map();

for await (const event of agent.invoke(params)) {
  if (event.type === "tool_call") {
    toolCalls.set(event.id, event);
  }
  if (event.type === "tool_result") {
    toolResults.set(event.id, event);
    const call = toolCalls.get(event.id);
    console.log(`${call.name}(${JSON.stringify(call.input)}) => ${JSON.stringify(event.output)}`);
  }
}
Sum usage events across iterations:
let totalTokens = { input: 0, output: 0, cached: 0 };

for await (const event of agent.invoke(params)) {
  if (event.type === "usage") {
    totalTokens.input += event.inputTokens;
    totalTokens.output += event.outputTokens;
    totalTokens.cached += event.cacheReadTokens ?? 0;
  }
}

Building the conversation graph

Events are reduced into an immutable conversation graph using runId and parentId for edges. The graph reducer in packages/ai/client/graph.ts accumulates events:
import { createGraph, reduceEvent } from "./packages/ai/client/graph";

let graph = createGraph();

for await (const event of orchestrator.events()) {
  graph = reduceEvent(graph, event);
  // Now graph.nodes contains all events as nodes
  // and graph.edges links them by runId → parentId
}
See Conversation Graph for details on graph structure and querying, and Client Library for the full graph API.

Type safety

Events are a discriminated union, so TypeScript narrows the type when you check the type field:
for await (const event of agent.invoke(params)) {
  if (event.type === "text") {
    // TypeScript knows event.content is a string
    process.stdout.write(event.content);
  }
  if (event.type === "tool_call") {
    // TypeScript knows event.name and event.input exist
    console.log(`Calling ${event.name}`);
  }
}

Next steps

Conversation Graph

Learn how events form an immutable graph structure

Client Rendering

Use graph projections to render conversations

Types Reference

Full TypeScript type definitions for all events

Harnesses

Return to harness fundamentals

Build docs developers (and LLMs) love