Klein Kit

Events

The typed AgentEvent union every backend message becomes

Every backend message becomes a typed AgentEvent. Each event extends:

interface EventBase {
  provider: ProviderId;
  sessionId: string;
  turnId?: string;
  timestamp: number;
  raw?: unknown;   // the untouched native payload, always available
}

The full union

event.typeExtra fields
session.startednativeSessionId?, model?, mode?
session.endedreason: "closed" | "exit" | "error"
session.errorerror: ErrorInfo
turn.started
turn.queuedposition: number (1-based)
turn.completedresult: TurnResult
turn.failederror: ErrorInfo
message.deltatext: string
message.completedtext: string
reasoning.started
reasoning.deltatext: string
reasoning.completedtext?: string
turn.diffdiff: string (cumulative working-tree diff)
limits.updatedlimits: RateLimit[]
plan.updatedplan: PlanStep[]
todo.updatedtodos: TodoItem[]
tool.startedtool: ToolInvocation
tool.outputtoolId: string, chunk: string
tool.completedtoolId, outcome: "success" | "error" | "canceled", result?
file.changedchanges: FileChange[]
permission.requestedrequest: PermissionRequest
permission.resolvedrequestId, behavior: "allow" | "deny"
usage.updatedusage: Usage
subagent.startedsubagent: SubagentInfo
subagent.completedsubagent, outcome: "success" | "error" | "canceled"
mode.changedmode: string
model.changedmodel: string
noticelevel: "info" | "warning" | "error", message: string
unknownnativeType?: string

Anything Klein Kit can't classify still reaches you as an unknown event with the native payload on raw — adapters never silently drop protocol traffic.

Supporting types

type PlanStepStatus = "pending" | "in_progress" | "completed";
interface PlanStep { step: string; status: PlanStepStatus }
interface TodoItem { id?: string; text: string; status: PlanStepStatus }

type ToolKind = "shell" | "file_read" | "file_edit" | "search" | "web" | "mcp" | "task" | "other";
interface ToolInvocation { id: string; name: string; kind: ToolKind; input: unknown; title?: string }

type FileChangeKind = "create" | "modify" | "delete" | "rename";
interface FileChange { path: string; kind: FileChangeKind; diff?: string; fromPath?: string }

interface SubagentInfo { id: string; name?: string; task?: string; native: boolean }
interface ErrorInfo { message: string; code?: string }
interface RateLimit { name: string; usedPercent?; windowMinutes?; resetsAt? }

interface Usage {
  inputTokens?; outputTokens?; cachedInputTokens?; reasoningTokens?;
  contextUsed?; contextWindow?; costUSD?;   // all number | undefined
}

Consuming events

Three ways, use whichever fits:

// 1. Iterate a single turn (most UIs)
for await (const event of session.prompt("…")) { /* … */ }

// 2. Iterate the whole session
for await (const event of session) { /* … */ }

// 3. Typed listeners — the callback narrows automatically
const off = session.on("tool.started", (e) => {
  console.log(e.tool.name, e.tool.kind); // e is AgentEventOf<"tool.started">
});
off(); // unsubscribe

AgentEventOf<T> extracts a variant from the union: AgentEventOf<"plan.updated"> is the event with the plan field.

On this page