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:
| Type | Data shape | When it fires |
|---|---|---|
agent_start | — | init() 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_attempt | provider-shaped | each 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 |
metric | JsonObject | counter/gauge/histogram update |
result | { usage? } | final result emitted |
result_retry | provider-shaped | typed-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.