# How ADHD Event Hooks Stream Execution Progress in Real-Time

> ADHD event hooks stream execution progress in real-time via an onEvent callback. Monitor, log, and update your UI live with six key pipeline phases.

- Repository: [Udit Akhouri/adhd](https://github.com/UditAkhourii/adhd)
- Tags: how-to-guide
- Published: 2026-07-30

---

**ADHD’s event hooks expose a real-time event stream through an optional `onEvent` callback in `RunOptions`, emitting plain-object events at six key phases of the tree-of-thought pipeline to enable live progress monitoring, logging, and UI updates.**

The ADHD repository implements a structured generative AI workflow that breaks complex reasoning into discrete phases. At each transition point in [`src/engine.ts`](https://github.com/UditAkhourii/adhd/blob/main/src/engine.ts), the engine optionally invokes the `onEvent` callback passed via `RunOptions`, allowing external observers to track execution without modifying core logic. These ADHD event hooks follow a simple convention: every event is a plain object with a `kind` string identifier and a payload containing phase-specific data.

## The Event Hook Architecture

The hook system centers on **optional chaining** (`onEvent?.`) inside the engine orchestration logic. Because the callback is optional, the engine runs silently by default, but becomes fully instrumented when a listener is provided. Each event dispatched is an immutable plain object where the `kind` property acts as a discriminator for the pipeline phase.

This design decouples progress reporting from business logic. Callers receive granular visibility into the tree-of-thought process—from initial reframing through final deepening—without blocking the underlying async operations.

## Execution Phases and Event Sequence

The engine drives a four-phase workflow. Below is the exact sequence of hooks emitted during a single `run()` invocation, mapped to their source locations in [`src/engine.ts`](https://github.com/UditAkhourii/adhd/blob/main/src/engine.ts).

### Phase 0: Reframe (reframe:done)

Before the main generation loop begins, the engine optionally rewrites the input problem to strip incidental anchors if `stripAnchors` is enabled.

- **Hook:** `reframe:done`
- **When:** Immediately after `reframeProblem` completes (lines 31–47)
- **Payload:** `changed: boolean` indicating whether the problem text was rewritten

```typescript
onEvent?.({ kind: "reframe:done", changed: Boolean(reframe) });

```

### Phase 1: Diverge (frame:start and frame:done)

The divergence phase fans out parallel generation across multiple cognitive frames. For each frame selected by `selectFrames`, the engine emits lifecycle events bracketing the LLM call.

- **Hook:** `frame:start`
- **When:** Right before `divergeBranch` begins (lines 55–58)
- **Payload:** `frameId`, `frameLabel`

- **Hook:** `frame:done`
- **When:** After the frame finishes generating ideas (lines 58–60)
- **Payload:** `frameId`, `count` (number of ideas produced)

### Phase 2: Score and Cluster (score:done and cluster:done)

Once all ideas are generated, the engine scores them with a critic model and clusters them by semantic similarity. These operations run concurrently, each firing its own completion hook.

- **Hook:** `score:done`
- **When:** After `scoreIdeas` finishes evaluating all candidates (lines 77–78)
- **Payload:** `total` (number of ideas scored)

- **Hook:** `cluster:done`
- **When:** After `clusterIdeas` groups ideas into buckets (lines 78–79)
- **Payload:** `clusters` (number of clusters created)

### Phase 3: Deepen (deepen:start and deepen:done)

The top-K ideas selected from clustering undergo a "deepening" pass where the model elaborates on each promising concept.

- **Hook:** `deepen:start`
- **When:** Before the deepening LLM call for a specific idea (lines 101–103)
- **Payload:** `ideaId`, `text` (the idea being deepened)

- **Hook:** `deepen:done`
- **When:** After the elaboration returns (lines 103–105)
- **Payload:** `ideaId`

Note that the short-listing, ranking, and provocation synthesis steps occur internally without emitting external hooks.

## Implementation in src/engine.ts

The orchestration logic in [`src/engine.ts`](https://github.com/UditAkhourii/adhd/blob/main/src/engine.ts) uses optional chaining to safely emit events only when a listener is configured. For example, the divergence loop wraps each frame generation:

```typescript
// Inside the divergence phase (src/engine.ts lines 55-60)
for (const frame of selectedFrames) {
  onEvent?.({ kind: "frame:start", frameId: frame.id, frameLabel: frame.label });
  const ideas = await divergeFrame(frame, runConfig);
  onEvent?.({ kind: "frame:done", frameId: frame.id, count: ideas.length });
}

```

Similarly, the deepening phase iterates over the top-K candidates:

```typescript
// Inside the deepen phase (src/engine.ts lines 101-105)
for (const idea of topIdeas) {
  onEvent?.({ kind: "deepen:start", ideaId: idea.id, text: idea.text });
  const deepened = await deepenIdea(idea, runConfig);
  onEvent?.({ kind: "deepen:done", ideaId: idea.id });
}

```

## Practical Usage Examples

### Basic Logging Hook

Attach a simple logger to trace every phase transition through the console:

```typescript
import { run, type RunOptions } from "adhd";

const opts: RunOptions = {
  problem: "How can we improve remote team collaboration?",
  framesPerRun: 4,
  ideasPerFrame: 5,
  topK: 3,
  concurrency: 3,
  stripAnchors: true,
  model: "gpt-4o-mini",
  onEvent: (e) => console.log(`[${e.kind}]`, e),
};

run(opts).then((result) => console.log("Final output:", result.pick));

```

### UI Progress Indicator

Map specific event kinds to UI state updates for real-time visualization:

```typescript
let pendingFrames = 0;

function uiEventHook(e: { kind: string; [key: string]: any }) {
  switch (e.kind) {
    case "frame:start":
      pendingFrames++;
      updateProgressBar(pendingFrames);
      break;
    case "frame:done":
      pendingFrames--;
      updateProgressBar(pendingFrames);
      break;
    case "deepen:start":
      showSpinner(e.ideaId);
      break;
    case "deepen:done":
      hideSpinner(e.ideaId);
      break;
    case "score:done":
      populateScoreTable(e.total);
      break;
  }
}

```

### Performance Instrumentation

Measure LLM latency per frame by tracking timestamps between paired start/done events:

```typescript
const timers = new Map<string, number>();

function perfHook(e: { kind: string; frameId?: string }) {
  if (e.kind === "frame:start" && e.frameId) {
    timers.set(e.frameId, performance.now());
  }
  if (e.kind === "frame:dome" && e.frameId && timers.has(e.frameId)) {
    const duration = performance.now() - timers.get(e.frameId)!;
    console.log(`Frame ${e.frameId} latency: ${duration.toFixed(2)}ms`);
  }
}

```

## Source File Reference

- **[`src/engine.ts`](https://github.com/UditAkhourii/adhd/blob/main/src/engine.ts)** — Core orchestration implementing `run()`, `RunOptions`, and all `onEvent` emissions across the reframe, diverge, score, cluster, and deepen phases.
- **[`src/frames.ts`](https://github.com/UditAkhourii/adhd/blob/main/src/frames.ts)** — Provides `selectFrames` and frame definitions used during the divergence phase.
- **[`src/llm.ts`](https://github.com/UditAkhourii/adhd/blob/main/src/llm.ts)** — Low-level LLM wrapper (`callLLM`, `parseJSON`) that the hooks ultimately surround.
- **[`src/types.ts`](https://github.com/UditAkhourii/adhd/blob/main/src/types.ts)** — TypeScript interfaces for `RunOptions`, event payloads, and `RunResult`.
- **[`src/index.ts`](https://github.com/UditAkhourii/adhd/blob/main/src/index.ts)** — Public API entry point that re-exports `run` for consumers.

## Summary

- **ADHD event hooks** are optional callbacks passed via `onEvent` in `RunOptions` that emit plain-object events at six distinct pipeline phases.
- The engine uses **optional chaining** (`onEvent?.`) to safely fire hooks in [`src/engine.ts`](https://github.com/UditAkhourii/adhd/blob/main/src/engine.ts) without requiring a listener.
- Events follow a strict naming convention with `kind` identifiers: `reframe:done`, `frame:start`, `frame:done`, `score:done`, `cluster:done`, `deepen:start`, and `deepen:done`.
- Each payload carries phase-specific metadata such as `frameId`, `count`, `total`, `clusters`, `ideaId`, and `text`.
- These hooks enable **real-time logging**, **progress bars**, and **performance telemetry** without coupling observer logic to the tree-of-thought implementation.

## Frequently Asked Questions

### What triggers the reframe:done event?

The `reframe:done` event fires immediately after the optional `reframeProblem` utility completes in [`src/engine.ts`](https://github.com/UditAkhourii/adhd/blob/main/src/engine.ts) (lines 31–47). It emits only when `stripAnchors` is set to `true` in the run configuration, passing a `changed` boolean that indicates whether the input problem text was actually rewritten.

### Can I use multiple event hooks simultaneously?

While `RunOptions` accepts only a single `onEvent` function, you can compose multiple observers by creating a multiplexed handler that forwards events to specialized listeners. Because the engine invokes `onEvent` with a plain object, you can distribute that object to logging, UI, and analytics handlers within a single callback.

### How do I measure LLM latency using event hooks?

Capture timestamps when `frame:start` or `deepen:start` fire, then calculate the delta when the corresponding `frame:done` or `deepen:done` event arrives. The hooks include identifiers like `frameId` and `ideaId` that allow you to correlate start and end events for precise latency measurements per branch.

### Are events emitted synchronously or asynchronously?

Events emit synchronously at the exact moment the engine reaches each checkpoint, but the underlying LLM operations remain asynchronous. The callback itself is non-blocking; the engine does not await return values from `onEvent`, ensuring that observation logic never stalls the tree-of-thought pipeline.