title: "Graph editor" description: "A local Rete.js node editor whose graphs compile to readable Ohm scripts." nodes: 20 progress: 85% generated: ohm spec editor#
Graph editor#
A local Rete.js node editor whose graphs compile to readable Ohm scripts.
Contents#
☑ Overview#
☑ Scope#
note
packages/graph (component catalogue, profiles, compiler) and
apps/ui/src/views/GraphView.tsx.
☑ The graph compiles to a script, it does not execute itself#
decision
Running a graph writes projects/<id>/scripts/<name>.graph.ohm and runs that file.
So there is exactly one execution engine, the generated code can be read, edited and
rerun by hand, and a graph is a way of writing Ohm rather than a parallel runtime.
- spec:
02-ohm-language
☑ Local only#
requirement
Rete and its plugins are bundled; nothing is fetched from a CDN, and the editor works offline like the rest of the app.
☑ Model#
☑ GraphDoc#
data-model
{ name, description?, profile?,
nodes: { id, type, x, y, values: Record<string, unknown> }[],
connections: { from, output, to, input }[] }
profile only filters the palette; it has no effect on compilation.
☑ Component catalogue#
data-model
Components declare { type, label, category, doc, inputs, outputs, controls, emit }
where emit receives helpers for the output variable name, a connected input
expression, "connection else same-named control literal", and a Lua literal.
Twelve categories: input, browser, ai, reasoning (Ψ), context (Γ), memory (ℳ), aggregate (Σ), agent (Λ), math, project, output, script.
78 components, in three groups.
Hand-written, because each needs an emit that cannot be derived: Text,
Number, Question, Variable, OpenTab, PageText, QueryAll, CloseTab,
Generate, Prompt, MemRead, MemWrite, MathExpr, Curve, AddLink,
SaveDoc, Print, Display, and a raw OhmCode escape hatch.
Browser automation, for driving a page rather than reasoning about one:
Navigate, OpenMany, Click, Type, Press, WaitFor, Scroll, Eval,
Inspect, Markdown, AskPage, SavePdf. Also hand-written: each maps to a
specific tab.* host call with its own shape.
Every operator, generated from the registry. Inputs and controls are read off each operator's own signature — what precedes the semicolon becomes sockets, what follows becomes controls, with the control's kind taken from its default. The palette carried sixteen of the forty-nine by hand once, and the other thirty-three were missing because adding an operator meant remembering to add it here too. Generation is what stops that recurring; a test asserts the palette covers the registry, and another compiles every generated node and checks the Ohm it produces.
Sockets are untyped by design — one socket kind — so any output can feed any input.
- spec:
02-ohm-language— the operator families
☑ Profiles#
data-model
Five built-in palettes — All components, Deep research, Prompting, Browser automation, Math & curves — plus user-defined ones saved in settings. Built-ins are read-only; custom profiles are a list of component types, deduped.
☑ Compilation#
☑ Algorithm#
flow
- Build incoming edges, dropping connections whose endpoints are missing.
- Kahn topological sort, with the initial queue sorted by
ythenx— the only determinism knob, so a tidy layout produces stable output. - Variable names are
n<1-based node index><output name>. - Emit a header comment, one
localdeclaring every output of every node, then per node a-- #<index> <label>comment followed by its emitted lines. - Collect errors instead of throwing: unknown component, unconnected required input, and "The graph has a cycle; nodes in the cycle were skipped".
compileGraph is total — it always returns { code, errors } — and running
refuses while errors is non-empty.
- node:
compile-not-run
☑ Generated code#
data-model
-- Generated from graph "research" by the Ohm graph editor. Edits here are overwritten.
local n1text, n2out, n3out, n4out, n5out
-- #1 Project question
n1text = (ctx.project and project.info().question) or "What is Ohm?"
-- #2 \Search
n2out = $\Search_{\Gamma}(n1text; k=3)$
-- #3 \Retrieve
n3out = $\Retrieve_{\Gamma}(n1text; n2out, k=5)$
-- #4 \CoT
n4out = $\CoT_{\Psi}(n1text; n3out)$
-- #5 \Cite
n5out = $\Cite_{\Gamma}(n4out; n3out)$
Comment numbers follow document order while statements follow topological order, so the numbers may appear out of sequence — that is expected.
☑ What the tests pin#
test
The research pipeline compiles to Ohm that the Ohm compiler accepts, in dependency order; control values fill unconnected inputs; cycles, unknown types and missing required inputs are reported rather than thrown; every component compiles standalone; and every profile references components that exist.
- node:
compiler
☑ The editor#
☑ Interaction#
component
Edit and viewer modes (the viewer drops the connection plugin and blocks node movement), a palette grouped by category and filtered by the profile and a text filter, Delete to remove selected nodes and their connections, Fit to zoom to everything, and "Compile to Ohm" which saves and then shows the code and any errors with a Copy button. A profile editor ticks components per category and saves a named palette.
☑ Where it opens from#
flow
Two routes to the same editor: the Projects panel's Graphs section, and clicking a
projects/<id>/graphs/<name>.graph.json file in the Scripts tree.
The second route is the one people take, because the file is sitting there in the tree. Without it the file opened in Monaco as raw JSON, which looks like the editor is missing. The same applies to prompts, curves, experiments and specs: a path under a project's item folder with that kind's suffix opens that kind's editor.
A file under <name>.versions/ is a saved snapshot, not the document, and opens as
text on purpose.
☑ Theme#
component
Nodes are dressed through the classic preset's styles hook rather than by
replacing the node component, so the socket and control plumbing stays the
library's. Colours are the app's own CSS variables, which means the canvas follows
light and dark with no second theme, and each node carries a stripe in its
category's colour — the same colour the palette shows beside that group.
☑ Review findings#
☐ Lua’s local limit is reachable#
risk
The compiler declares one local per output of every node, including nodes skipped
as part of a cycle. Lua allows 200 locals per function, so a graph beyond roughly 200
outputs fails at runtime with a confusing message rather than at compile time.
Either batch the declarations into a table or check the count and report it.
- node:
compiler
☐ Variable names are concatenated without a separator#
risk low
n + index + output name means node 1's output ext and node 11's output t
would both be n1ext. No current component makes this reachable, but nothing
prevents it either. Add a separator.
☐ Boolean controls round-trip through strings#
risk low
A bool control is stored as the string "true"/"false"; any other truthy string
coerces to true on load. Store booleans as booleans.
Links between nodes#
graph LR
compiler["Algorithm"] -.-> compile_not_run["The graph compiles to a script, it does not execute itself"]
what_the_tests_pin["What the tests pin"] -.-> compiler["Algorithm"]
lua_s_local_limit_is_reachable["Lua’s local limit is reachable"] -.-> compiler["Algorithm"]