How the `onEvent` Callback Streams Progress Through ADHD Phases
The ADHD engine's onEvent callback emits a typed RunEvent at every major phase transition—reframe, diverge, score, cluster, and deepen—allowing callers to stream real-time progress from the tree-of-thought pipeline.
The open-source UditAkhourii/adhd repository implements a tree-of-thought reasoning skill that breaks complex problems into distinct cognitive phases. By supplying an onEvent callback through RunOptions, consumers of the engine receive granular, real-time updates as the system moves from reframing through deepening. Understanding how this callback streams progress through ADHD phases is essential for building responsive CLIs, dashboards, and automated logging around the engine.
The Four Phases of the ADHD Tree-of-Thought Workflow
The engine orchestrates its work across four logical stages:
- Reframe (Phase 0) – Optionally strips incidental anchors from the problem statement.
- Diverge (Phase 1) – Generates ideas in parallel under different cognitive frames.
- Score + Cluster (Phase 2) – Evaluates each idea and groups them into clusters.
- Deepen (Phase 3) – Expands the top-K ideas in a focused pass.
At each transition, the run function in src/engine.ts dispatches a specific event to the caller-supplied callback.
The RunEvent Type Contract in src/types.ts
The payload shape is defined as a discriminated union in src/types.ts:
export type RunEvent =
| { kind: "reframe:done"; changed: boolean }
| { kind: "frame:start"; frameId: string; frameLabel: string }
| { kind: "frame:done"; frameId: string; count: number }
| { kind: "score:done"; total: number }
| { kind: "cluster:done"; clusters: number }
| { kind: "deepen:start"; ideaId: string; text: string }
| { kind: "deepen:done"; ideaId: string }
| { kind: "warn"; message: string };
Every event carries exactly the data needed to render meaningful progress for its respective phase.
Phase-by-Phase Event Emission in src/engine.ts
The run function emits events at precise line locations as it advances through the pipeline.
Reframe (reframe:done)
After optionally calling reframeProblem, the engine fires:
onEvent?.({ kind: "reframe:done", changed: Boolean(reframe) })
This occurs at src/engine.ts:46-47 and signals whether the problem statement was modified.
Diverge (frame:start and frame:done)
For each selected frame, the engine emits frame:start at src/engine.ts:56 before invoking divergeBranch:
onEvent?.({ kind: "frame:start", frameId, frameLabel })
Once the branch completes, it emits frame:done at src/engine.ts:58:
onEvent?.({ kind: "frame:done", frameId, count })
This pair creates a per-frame progress stream.
Score and Cluster (score:done and cluster:done)
After all branches are collected, run scores ideas via scoreIdeas and clusters them via clusterIdeas. It then emits:
onEvent?.({ kind: "score:done", total: allIdeas.length })
at src/engine.ts:77, followed by:
onEvent?.({ kind: "cluster:done", clusters: clusters.length })
at src/engine.ts:78.
Deepen (deepen:start and deepen:done)
For each top-K idea passed to deepenIdea, the engine emits deepen:start at src/engine.ts:100:
onEvent?.({ kind: "deepen:start", ideaId: idea.id, text: idea.text })
After the call returns, it signals completion at src/engine.ts:102:
onEvent?.({ kind: "deepen:done", ideaId: idea.id })
Default Progress Logger in src/cli.ts
The command-line interface constructs the onEvent handler in src/cli.ts. When --quiet is absent, it builds a logger that writes to stderr:
const onEvent = flags.quiet ? undefined : (e: RunEvent) => {
switch (e.kind) {
case "reframe:done": if (e.changed) process.stderr.write(` ↺ anchors stripped from problem\n`); break;
case "frame:start": process.stderr.write(` ▸ ${e.frameLabel}…\n`); break;
case "frame:done": process.stderr.write(` ${e.count} ideas (${e.frameId})\n`); break;
case "score:done": process.stderr.write(` scored ${e.total} ideas\n`); break;
case "cluster:done": process.stderr.write(` ${e.clusters} clusters\n`); break;
case "deepen:start": process.stderr.write(` ◎ focus → ${e.text}\n`); break;
case "deepen:done": process.stderr.write(` deepening completed\n`); break;
case "warn": process.stderr.write(` ! ${e.message}\n`); break;
}
};
The CLI then passes this function into RunOptions before calling run:
const opts: RunOptions = {
problem: flags.problem,
// …other options…
onEvent,
};
const result = await run(opts);
Practical Integration Examples
Stream Progress from the Command Line
Running the tool without --quiet automatically streams phase progress:
npx adhd "design a rate limiter for leader election"
The default onEvent logger prints reframe confirmations, frame labels, idea counts, and deepening status directly to stderr.
Persist Events to a Log File
You can attach a custom callback to archive every event:
import { run } from "./src/engine.js";
import { writeFileSync } from "node:fs";
const logPath = "./adhd-events.log";
const onEvent = (e) => {
writeFileSync(logPath, JSON.stringify(e) + "\n", { flag: "a" });
};
const result = await run({
problem: "how should we shard this queue?",
onEvent,
});
console.log("Result:", result);
This script receives the same sequence of events emitted by src/engine.ts but persists them for later analysis.
Render Real-Time Updates in React
A web UI can accumulate events in state to show live status:
import { useEffect, useState } from "react";
import { run } from "./engine";
function ADHDRunner({ problem }: { problem: string }) {
const [events, setEvents] = useState<RunEvent[]>([]);
const [result, setResult] = useState<RunResult | null>(null);
useEffect(() => {
const onEvent = (e) => setEvents((prev) => [...prev, e]);
run({ problem, onEvent }).then(setResult);
}, [problem]);
return (
<div>
<h2>Progress</h2>
<ul>
{events.map((e, i) => (
<li key={i}>{JSON.stringify(e)}</li>
))}
</ul>
{result && <pre>{JSON.stringify(result, null, 2)}</pre>}
</div>
);
}
The component updates in real time as the engine moves through reframe, divergence, scoring, clustering, and deepening.
Summary
- The ADHD engine exposes a single
onEventcallback that streams typedRunEventobjects through every phase of the tree-of-thought pipeline. - Events are emitted from
src/engine.tsat exact phase boundaries:reframe:done,frame:start/frame:done,score:done,cluster:done, anddeepen:start/deepen:done. src/types.tsdefines the discriminated union that guarantees each event payload matches its respective phase.src/cli.tsdemonstrates a practical logger, but the callback is fully optional and can be replaced with custom handlers for logging, UI updates, or silent execution.
Frequently Asked Questions
What triggers the onEvent callback during the Diverge phase?
During the Diverge phase, the engine loops over each selected cognitive frame and invokes onEvent?.({ kind: "frame:start", frameId, frameLabel }) at src/engine.ts:56 before calling divergeBranch. After the branch returns, it emits frame:done at src/engine.ts:58 with the resulting idea count, giving per-frame streaming progress.
Can I disable the onEvent progress stream?
Yes. In src/cli.ts, the CLI passes undefined for onEvent when the --quiet flag is set. The engine safely handles missing callbacks by using optional chaining (onEvent?.(...)), so progress streaming is completely optional.
What information does each RunEvent object carry?
The payload depends on the event kind. For instance, reframe:done includes a changed boolean, frame:done carries frameId and count, and deepen:start provides the ideaId and full text being expanded. The exact shape is defined as a discriminated union in src/types.ts.
Is the onEvent callback required to execute the ADHD engine?
No. The run function in src/engine.ts invokes the callback with optional chaining, so omitting it does not raise an error. Callers can run the full pipeline silently when real-time progress updates are not needed.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →