Skip to content

The Generator API

Generators are the core abstraction of Eigenvue. A generator takes an algorithm’s inputs and produces an ordered sequence of Step objects, each representing one “interesting moment” in the algorithm’s execution.

This page is the API reference. For a hands-on tutorial, see Adding an Algorithm.


Factory function that produces a type-safe GeneratorDefinition. This is the entry point for every TypeScript generator.

Location: web/src/engine/generator/types.ts

import { createGenerator } from "@/engine/generator";
interface MyInputs extends Record<string, unknown> {
array: readonly number[];
target: number;
}
export default createGenerator<MyInputs>({
id: "my-algorithm", // Must match meta.json "id"
*generate(inputs, step) {
// Yield steps here...
},
});

Parameters:

FieldTypeDescription
idstringAlgorithm ID. Must match ^[a-z0-9][a-z0-9-]*$.
generateGeneratorFunction<TInputs>A generator function (function*).

Returns: GeneratorDefinition<TInputs>

Validation: The id is validated at definition time. An invalid ID throws immediately, so you catch mistakes before any test runs.


The object returned by createGenerator(). It bundles the algorithm ID with the generator function.

interface GeneratorDefinition<TInputs extends Record<string, unknown>> {
readonly id: string;
readonly generate: GeneratorFunction<TInputs>;
}

You never construct this directly. Always use createGenerator().


The signature of the generator function itself:

type GeneratorFunction<TInputs extends Record<string, unknown>> =
(inputs: TInputs, step: StepBuilderFn) => Generator<Step, void, undefined>;

Parameters passed to your generator:

ParameterTypeDescription
inputsTInputsThe algorithm’s validated input parameters.
stepStepBuilderFnFunction to create steps. Call this for each step.

Contract:

  1. Must yield at least one step.
  2. The last yielded step must have isTerminal: true.
  3. No step other than the last may have isTerminal: true.
  4. Must terminate (no infinite loops). Default safety limit: 10,000 steps.
  5. State objects must be independent snapshots (no shared references).

The step parameter passed to your generator is a function that converts a StepInput into a complete Step object:

type StepBuilderFn = (input: StepInput) => Step;

The step builder:

  1. Assigns the correct index (auto-incrementing from 0).
  2. Defaults isTerminal to false if not provided.
  3. Validates the step ID format and title length.
  4. Validates code highlight line numbers (must be positive integers).
  5. Returns the complete Step object.

You never construct Step objects directly. Always use the step() builder.


The fields you provide when yielding a step:

interface StepInput {
readonly id: string; // Template identifier (e.g., "compare_mid")
readonly title: string; // Short heading (max 200 chars)
readonly explanation: string; // Plain-language narration
readonly state: Record<string, unknown>; // Variable snapshot
readonly visualActions: readonly VisualAction[];
readonly codeHighlight: CodeHighlight;
readonly isTerminal?: boolean; // Only true on the last step
readonly phase?: string; // Optional grouping label
}

id — Template identifier matching ^[a-z0-9][a-z0-9_-]*$. The same ID can appear multiple times in a sequence (e.g., "compare" appears once per loop iteration). This is intentional: the ID identifies the type of step, not the specific instance.

title — Short heading shown in the UI. Must be non-empty and at most 200 characters. Make it descriptive: "Compare Middle Element (Iteration 3)" is better than "Step 7".

explanation — Educational narration shown below the visualization. Reference concrete values: "mid = floor((2 + 8) / 2) = 5. Checking array[5] = 11." is far better than "Calculate the middle index.".

state — A snapshot of ALL algorithm variables at this point. Must be a JSON-serializable object. Critical rule: this must be an independent copy.

// CORRECT: spread copy of the array
state: { array: [...array], target, left, right, mid }
// CORRECT: deep clone for nested mutable structures
state: structuredClone({ matrix, weights, biases })
// WRONG: reference to a mutable variable
state: { array, target, left, right }
// ^^^^^ if mutated later, ALL steps see the mutation

visualActions — Rendering instructions for the layout. See the Visual Actions section below.

codeHighlight — Maps this step to source code lines. See CodeHighlight below.

isTerminal — Set to true only on the final step. The runner validates that exactly one step is terminal and that it is the last one.

phase — Optional grouping label. Consecutive steps with the same phase are visually grouped in the UI. Common values: "initialization", "search", "comparison", "result".


runGenerator(definition, inputs, options?)

Section titled “runGenerator(definition, inputs, options?)”

Executes a generator and returns a validated StepSequence.

Location: web/src/engine/generator/GeneratorRunner.ts

import { runGenerator } from "@/engine/generator";
import binarySearchGenerator from "@/algorithms/classical/binary-search/generator";
const result = runGenerator(binarySearchGenerator, {
array: [1, 3, 5, 7, 9, 11, 13],
target: 7,
});
console.log(result.steps.length); // Number of steps
console.log(result.algorithmId); // "binary-search"
console.log(result.generatedBy); // "typescript"

Parameters:

ParameterTypeDescription
definitionGeneratorDefinition<TInputs>From createGenerator()
inputsTInputsAlgorithm input parameters
optionsRunOptions (optional)Configuration (e.g., maxSteps)

Returns: StepSequence

Throws: GeneratorError with the algorithm ID and step index for easy debugging.

RunOptions:

interface RunOptions {
readonly maxSteps?: number; // Default: 10,000
}

The complete output of a generator run:

interface StepSequence {
readonly formatVersion: number; // Currently 1
readonly algorithmId: string; // e.g., "binary-search"
readonly inputs: Record<string, unknown>;
readonly steps: readonly Step[];
readonly generatedAt: string; // ISO 8601 timestamp
readonly generatedBy: "typescript" | "python" | "precomputed";
}

A single step in the execution trace:

interface Step {
readonly index: number; // 0-based, auto-assigned
readonly id: string; // Template identifier
readonly title: string; // Short heading
readonly explanation: string; // Educational narration
readonly state: Record<string, unknown>; // Variable snapshot
readonly visualActions: readonly VisualAction[];
readonly codeHighlight: CodeHighlight;
readonly isTerminal: boolean; // true only on last step
readonly phase?: string; // Optional grouping label
}

Invariants enforced by the runner:

  1. steps[i].index === i for all i (index contiguity).
  2. Exactly one step has isTerminal === true, and it is the last.
  3. At least one step exists.

Visual actions are an open vocabulary. The type field is a plain string, not a closed enum. Renderers silently ignore action types they do not recognize. This means you can define new action types in a generator before any renderer supports them.

interface VisualAction {
readonly type: string; // Action type identifier (camelCase)
readonly [key: string]: unknown; // Action-specific parameters
}

Array actions:

TypeParametersDescription
highlightElementindex, color?Highlight a single element
highlightRangefrom, to, color?Highlight a contiguous range
dimRangefrom, toVisually de-emphasize a range
movePointerid, toMove a named pointer to an index
swapElementsi, jSwap two elements (sorting)
compareElementsi, j, resultCompare two elements
markFoundindexMark an element as found
markNotFound(none)Indicate target was not found
showMessagetext, messageTypeDisplay a transient text message

Graph actions:

TypeParametersDescription
visitNodenodeId, color?Mark a graph node as visited
highlightEdgefrom, to, color?Highlight a graph edge
updateNodeValuenodeId, valueUpdate a node’s displayed value

Neural network actions:

TypeParametersDescription
activateNeuronlayer, index, valueActivate a neuron
propagateSignalfromLayer, toLayerSignal propagation
showGradientlayer, valuesDisplay gradient values
showWeightsfromLayer, toLayer, weightsDisplay weight matrix
showLossloss, lossFunction, …Show computed loss value

Attention actions:

TypeParametersDescription
showAttentionWeightsqueryIdx, weightsAttention weights (must sum to 1)
highlightTokenindex, color?Highlight a token
showProjectionMatrixprojectionType, matrixShow Q/K/V projection
showAttentionScoresscoresRaw attention scores
showFullAttentionMatrixweightsFull attention matrix

Validated constraints:

  • highlightRange / dimRange: from <= to (enforced by runner)
  • compareElements: result must be "less", "greater", or "equal"
  • showAttentionWeights: weights must sum to 1.0 within +/-1e-6 (validated using Kahan summation for numerical stability)
  • updateBarChart: if labels is present, labels.length === values.length

Maps a step to source code lines:

interface CodeHighlight {
readonly language: string; // Key in meta.json code.implementations
readonly lines: readonly number[]; // 1-indexed line numbers to highlight
}

Example:

codeHighlight: { language: "pseudocode", lines: [5, 6] }

Lines are 1-indexed to match editor conventions (line 1 is the first line). Order does not matter; the renderer highlights all listed lines.


Python generators follow a different pattern than TypeScript. Instead of function* / yield, they are regular functions that return a list[Step].

def generate(inputs: dict[str, Any]) -> list[Step]:
"""Generate algorithm steps.
Parameters
----------
inputs : dict
Algorithm input parameters.
Returns
-------
list[Step]
Ordered list of Step dataclass objects.
"""

The Python Step class mirrors the TypeScript Step interface but uses snake_case field names. The to_dict() method converts to camelCase for JSON wire format.

from eigenvue._step_types import Step, VisualAction, CodeHighlight
step = Step(
index=0,
id="initialize",
title="Initialize Search",
explanation="Setting up the algorithm...",
state={"array": list(array), "target": target},
visual_actions=[
VisualAction(type="highlightRange", from_=0, to=n - 1, color="highlight"),
],
code_highlight=CodeHighlight(language="pseudocode", lines=[1]),
is_terminal=False,
phase="initialization",
)

The Python Step and VisualAction dataclasses use snake_case internally but convert to camelCase when serialized to JSON via to_dict():

Python (snake_case)JSON (camelCase)
is_terminalisTerminal
visual_actionsvisualActions
code_highlightcodeHighlight
step_indexstepIndex
message_typemessageType

For VisualAction fields, from_ (with trailing underscore to avoid the Python keyword) maps to from in JSON.

from eigenvue.runner import run_generator
# With custom inputs
steps = run_generator("binary-search", {
"array": [1, 3, 5, 7, 9],
"target": 5,
})
# With default inputs from meta.json
steps = run_generator("binary-search")

The runner:

  1. Looks up the generator in the registry.
  2. Resolves inputs (custom or defaults from meta.json).
  3. Calls the generator function.
  4. Serializes Step objects to camelCase dicts via to_dict().
  5. Validates step sequence invariants (index contiguity, single terminal).
ConcernTypeScriptPython
Function typeGenerator function (function*)Regular function returning list
Step creationyield step({ ... })steps.append(Step(...))
Index assignmentAutomatic (runner assigns)Manual (you set index)
State snapshots[...array] or structuredClone()list(array) or copy.deepcopy()
Field namingcamelCasesnake_case (auto-converted on serialize)
Validation timingRunner validates after all yieldsRunner validates after function returns
RegistryAuto-discovered from file pathManual entry in __init__.py

Both TypeScript and Python generators receive the same logical inputs. The inputs are validated against the JSON Schema defined in meta.json before reaching the generator.

From meta.json:

{
"inputs": {
"schema": {
"array": {
"type": "array",
"items": { "type": "number", "minimum": -999, "maximum": 999 },
"minItems": 1,
"maxItems": 20
},
"target": {
"type": "number",
"minimum": -999,
"maximum": 999
}
},
"defaults": {
"array": [1, 3, 5, 7, 9],
"target": 5
}
}
}

In TypeScript, define an interface that matches your input schema:

interface BinarySearchInputs extends Record<string, unknown> {
readonly array: readonly number[];
readonly target: number;
}

The extends Record<string, unknown> is required by createGenerator’s generic constraint. It ensures inputs are always a plain object.

In Python, inputs arrive as a plain dict[str, Any]. Type-narrow them at the top of your generator:

def generate(inputs: dict[str, Any]) -> list[Step]:
array: list[int] = list(inputs["array"])
target: int = inputs["target"]

Steps must follow these ordering rules, enforced by the runner:

  1. Index contiguity. steps[i].index === i for all i. No gaps, no duplicates, no reordering.

  2. Single terminal. Exactly one step has isTerminal: true. It must be the last step in the sequence.

  3. Non-empty. At least one step must be yielded.

  4. Phase consistency. While not enforced at the type level, phases should flow logically: "initialization" -> "search" / "computation" -> "result". Consecutive steps with the same phase are visually grouped.

  5. Determinism. Given the same inputs, a generator must produce the exact same steps every time. No random behavior, no time-dependent logic, no external state. This is essential for cross-language parity and reproducibility.