FabricFabricHarness
Reference

Agent Events

Subscribe to a typed event stream from any Fabric Harness agent — text deltas, tool calls, shell commands, compaction, approvals, tasks, errors. Available from both import entrypoints.

Every Fabric Harness agent emits a structured event stream as it runs. The same callback shape is accepted at every level — init({ onEvent }), agent.session(id, { onEvent }), and session.prompt(text, { onEvent }) — and propagates downward, so wiring once at init() is usually enough.

Quick start

import { agent, isEvent, type AgentEvent } from '@fabric-harness/sdk';

export default agent<{ message: string }>({
  name: 'echo',
  run: async ({ init, input }) => {
    const fabric = await init({
      onEvent: (event: AgentEvent) => {
        if (isEvent(event, 'text_delta')) process.stdout.write(event.data.delta);
        if (isEvent(event, 'tool_start')) console.error(`> ${event.data.toolName}`);
        if (isEvent(event, 'compaction')) console.error(`compacted ${event.data.messagesBefore} → ${event.data.messagesAfter}`);
      },
    });
    const session = await fabric.session();
    return { reply: await session.prompt<string>(input.message) };
  },
});
import { agent, isEvent, schema, type AgentEvent } from '@fabric-harness/sdk';

export default agent({
  name: 'echo',
  input: schema.object({ message: schema.string() }),
  output: schema.string(),
  run: async ({ init, input }) => {
    const fabric = await init({
      onEvent: (event: AgentEvent) => {
        if (isEvent(event, 'tool_end') && event.data.isError) {
          console.error(`tool ${event.data.toolName} failed`, event.data.result);
        }
        if (isEvent(event, 'approval_requested')) {
          console.error(`approval needed for ${event.data.subject}`);
        }
      },
    });
    const session = await fabric.session();
    return await session.prompt(input.message);
  },
});

Event types

All events share a common envelope:

interface AgentEventBase {
  id: string;            // stable id for this occurrence
  timestamp: string;     // ISO-8601
  sessionId?: string;
  parentSessionId?: string;  // set on child task events
  taskId?: string;           // set on child task events
}

The type discriminator narrows data automatically:

TypeData shapeWhen it fires
agent_startinit() resolves
session_start{ id?: string }agent.session() resolves
prompt_start{ text?: string }start of session.prompt()
prompt_end{ text?: string; result?: unknown }end of session.prompt()
turn_start / turn_end{ stopReason?: string }each model turn inside the harness loop
model_attemptprovider-shapedeach model call (incl. retries)
text_delta{ delta: string }streaming text from the model
tool_start{ toolName, toolCallId?, args? }a tool call begins
tool_end{ toolName, toolCallId?, isError?, result? }a tool call resolves
command_start{ command: string; cwd? }session.shell() or scoped command begins
command_end{ command: string; exitCode: number; durationMs? }shell command resolves
skill_start / skill_end{ name, result? }session.skill() lifecycle
task_start{ taskId, depth }session.task() spawns a child agent
task_end{ taskId, depth, durationMs? }child task resolves
task_failed / task_cancelled{ taskId, error? }child task fails or is cancelled
task_checkpoint{ taskId }durable child checkpoint persisted
compaction{ reason?: 'threshold' | 'overflow'; messagesBefore; messagesAfter; tokensBefore?; tokensAfter? }context compaction runs (when enabled)
artifact_created{ path; contentType? }session.artifact() persists
checkpoint_created / checkpoint_restored{ id }session checkpoints
approval_requested{ approvalId; subject; kind; risk? }a requireApproval policy match needs human ack
approval_voted{ approvalId; outcome }someone voted via fh approvals approve/deny
approval_granted / approval_denied / approval_expired{ approvalId }approval terminal states
metricJsonObjectcounter/gauge/histogram update
result{ usage? }final result emitted
result_retryprovider-shapedtyped-result schema mismatch caused a retry
error{ error: string; cause? }thrown by anywhere in the agent loop

Type-safe access with isEvent

import { isEvent } from '@fabric-harness/sdk';

onEvent: (event) => {
  if (isEvent(event, 'tool_start')) {
    // event.data is { toolName: string; toolCallId?: string; args?: JsonObject }
    console.log(event.data.toolName, event.data.args);
  }
}

Or use a switch on event.type — TS narrows the same way.

Streaming via session.stream()

The same events can be consumed as an async generator instead of a callback:

import type { AgentEvent } from '@fabric-harness/sdk';

for await (const event of session.stream<string>('Triage this issue')) {
  // event: AgentEvent — same union, same narrowing
  if (event.type === 'text_delta') process.stdout.write(event.data.delta);
}

session.stream() returns an AsyncGenerator<AgentEvent, TResult, void> — the final return value is the typed prompt result; the yielded values are the events leading up to it.

Telemetry alongside events

If onEvent is configured and an OTel exporter is configured (@fabric-harness/sdk telemetry option), both fire — they aren't mutually exclusive. Events are for in-process subscribers (logs, downstream workers, server-sent-event consumers); OTel is for cross-process observability backends. Fabric Harness ships no UI of its own — this is a headless SDK.

Where to go next

  • Sessions and prompts — the call surface that emits these events.
  • Telemetry — exporting metrics and traces to Datadog, Grafana, OpenTelemetry collector.
  • Approvals — for the approval_* event variants.