How ADHD Event Hooks Stream Execution Progress in Real-Time

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, 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.

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
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 uses optional chaining to safely emit events only when a listener is configured. For example, the divergence loop wraps each frame generation:

// 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:

// 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:

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:

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:

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 — Core orchestration implementing run(), RunOptions, and all onEvent emissions across the reframe, diverge, score, cluster, and deepen phases.
  • src/frames.ts — Provides selectFrames and frame definitions used during the divergence phase.
  • src/llm.ts — Low-level LLM wrapper (callLLM, parseJSON) that the hooks ultimately surround.
  • src/types.ts — TypeScript interfaces for RunOptions, event payloads, and RunResult.
  • 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 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 (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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →