An AI-native medium for thinking. ThoughtSpace is a shared workspace of persistent, semantic, executable objects. A human and an AI manipulate the same objects through the same typed protocol. You don't ask a question and read an answer. You build a small world, poke at it, and the AI helps you modify, explain, test and abstract it.
The idea comes from Michael Nielsen's Magic Paper, and asks what that medium becomes when the computer understands the semantic structure of what's on the page.
Claude drives the UI; the in-app AI is OpenAI gpt-5.5. Waits on the model are sped up. More use cases are in the gallery below.
It is also an open, interactive canvas for teaching any subject: you and the AI build the lesson live, in front of the class, out of sliders, live formulas, simulations, random trials, plots and predictions that an experiment can settle. See Teaching with ThoughtSpace.
The first domain it was built around is small neural networks, with one journey: why does XOR need a hidden layer?
A lesson in ThoughtSpace is not a slide deck or a fixed simulation. It is an open lesson: a handful of generic building blocks that you, the AI and the class assemble and change live, in any subject.
| Building block | What the class sees | What it is |
|---|---|---|
| Variable | a slider with a unit | a named quantity: theta, R0, tax |
| Formula | a live, typeset equation | R = v0^2*sin(2*rad(theta))/g, recomputed as sliders move |
| System | rate equations and their run | dS/dt = -beta*S*I, integrated over time, with a stop condition |
| Random trials | a histogram and statistics | M = meanof(n, randint(1, 6)), repeated thousands of times, seeded |
| Plot | a curve, time series, trajectory or histogram | drag along a curve to move its slider |
| Prediction | a claim, unverified until tested | "R is largest when theta = 45" |
| Experiment | a table, a chart and a verdict | vary one slider, freeze the rest, compute whether the prediction holds |
| What if | a branch beside the original, compared | "what if k = 0.05" |
Names link themselves: a formula that mentions theta is wired to the theta slider, and
the arrows on the canvas are the dependency graph. Everything is computed by the kernel,
deterministically, so the whole class sees the same numbers and every experiment reproduces.
The classroom loop is predict β play β test β change the conditions β test again. Students commit to a prediction, move the sliders, test it with a controlled experiment, then change something the prediction quietly assumed (air drag, a more contagious disease, the slopes of a market) and watch the verdict flip.
Three ways to start a lesson:
- Ask the AI for any topic: "I teach chemistry. Build a lesson on radioactive decay and half-life my students can play with." It builds from the same building blocks and leaves an untested prediction for the class. This needs a model (β: Claude, OpenAI or any OpenAI-compatible server).
- Type the mathematics, even offline.
y = a*sin(b*x) + cmakes a slider for every unknown, the live formula and its curve.dN/dt = r*N*(1 - N/K); N(0) = 5makes a system,X ~ randint(1,6) + randint(1,6)makes random trials, anda = 3sets a slider. Then write a prediction in words, like "test: y increases as c increases", "test: R is largest when theta = 45" or "test: when vacc = 0.6, I_max is below 0.01", and it becomes an experiment. "what if b = 4" branches and compares, and "why" measures which slider matters most right now. - Start from an example. The empty canvas offers five ready-made lessons (physics, epidemics, Fourier series, the central limit theorem, tax incidence). They are built from the same primitives, so they're starting points to change, not finished apps.
|
Any subject, built live by the AI |
Type the mathematics |
|
Physics: which angle throws furthest? |
Biology: stopping an epidemic |
|
Maths: square waves from sines |
Statistics: why averages make bell curves |
|
Economics: who really pays a tax? |
The expression language |
npm install
npm run dev # http://localhost:5173 β works offline with the deterministic planner
ANTHROPIC_API_KEY=sk-ant-β¦ npm run dev # Claude participates; the key stays on the dev server
OPENAI_API_KEY=sk-β¦ npm run dev # an OpenAI model participates, same way
OPENAI_COMPAT_BASE_URL=https://api.groq.com/openai/v1 OPENAI_COMPAT_API_KEY=gsk_β¦ npm run dev
# any OpenAI-compatible server (Groq, Ollama, OpenRouter, β¦)
npm test # kernel, protocol, journey and agent-loop tests (offline)
LIVE=1 OPENAI_MODEL=gpt-5.5 npm run test:live # the whole journey against a real modelChoose the AI participant in the β menu: the offline planner, Claude, OpenAI, or any OpenAI-compatible server. For each provider you can use the dev server's key or paste your own, which is kept in your browser and sent straight to that provider. You can also set the model there.
-
Claude defaults to
claude-opus-5with adaptive thinking. Requests enable the API's server-side refusal fallback (fallbacks: "default"), and if an account rejects that beta, the request is retried once without it. -
OpenAI defaults to
gpt-5.5and uses the Responses API with function tools. Newer models reject tools combined with reasoning on Chat Completions. -
OpenAI-compatible servers use Chat Completions, the dialect they all speak (or the Responses API, if you tick that for a server that has it). There are presets for Groq, Ollama, OpenRouter, Together, LM Studio and vLLM; any other base URL works too. Requests are streamed, because slow local servers time out long non-streaming requests. The output-length parameter is negotiated (
max_completion_tokens, falling back tomax_tokens).<think>blocks from open reasoning models are stripped from replies.
Every provider gets the same system prompt, the same three tools and the same kernel.
Nothing is provider-specific below src/agent/.
Local models. Ollama's default context window is 4k tokens, and ThoughtSpace's prompt
plus workspace is bigger than that, so start Ollama with OLLAMA_CONTEXT_LENGTH=16384. For
direct browser access (rather than the dev-server proxy) also set OLLAMA_ORIGINS=*:
OLLAMA_CONTEXT_LENGTH=16384 ollama serve & ollama pull qwen3:4b
OPENAI_COMPAT_BASE_URL=http://localhost:11434/v1 OPENAI_COMPAT_MODEL=qwen3:4b npm run dev- On the blank canvas, click "Let's understand why XOR requires a hidden layer."
- The AI builds a construction rather than writing an answer. You get the XOR data, a single neuron fed by it, the neuron's decision boundary, its live equation, and an unverified claim.
- Drag an edge of the network to change its weight, or drag a neuron to change its bias. The boundary line and the equation follow in real time. Click a data point to relabel it (turn XOR into AND and watch the same neuron succeed). Hover a point to see the network's activations light up.
- Train βΈ. Gradient descent gives up: the weights shrink to β0 and the network answers Β½ everywhere (loss ln 2).
- Select the network and ask "Why doesn't this one work?" The AI highlights the output neuron and the boundary, derives the boundary's equation from the live weights, and pins its explanation to the plot.
- "Show me the smallest change that makes it work." The AI runs a reproducible experiment over hidden sizes Γ 6 seeds. The experiment verifies the claim, and the AI creates a branch: the network with 2 hidden units, trained, sitting beside the original with its assumption recorded.
- Compare branches (the suggestion on the variant) to get a live side-by-side.
- "Turn this into a reusable XOR network glyph." The construction folds into a glyph that exposes its hidden units, activation and seed. You can expand it back into its graph, zoom in to see inside, or place new copies from β Glyphs.
Along the way you can zoom the canvas out (objects collapse to their essence) and in (every weight and bias appears). Double-click a neuron to zoom into its equation, then into its scalar arithmetic. Fork the whole workspace (β) and diff branches. Undo and redo anything (βZ / β§βZ). Open the inspector to see each object's provenance, history, and exactly what the AI sees.
Each of these is recorded from the running app with the deterministic offline planner as the in-app AI, so they need no API key and can be reproduced exactly (see regenerating).
|
Experiments and claims |
Branches and comparison |
|
Glyphs: reusable abstraction |
Semantic zoom |
|
Hands on |
History and provenance |
The GIFs are scripted scenarios in scripts/gifs/scenarios/, recorded from the running
app by scripts/gifs/record.mjs. It uses Playwright with a visible cursor and captions,
and scripts/make-gif.py, which needs Pillow, turns the frames into a GIF with waits on the
AI sped up.
npm run dev &
node scripts/gifs/record.mjs experiments branches glyphs zoom hands-on history ink
node scripts/gifs/record.mjs lesson-physics lesson-epidemic lesson-fourier lesson-statistics lesson-economics lesson-open
MODEL=gpt-5.5 node scripts/gifs/record.mjs demo lesson-ai # these use a real model (OPENAI_API_KEY on the dev server)Press P (or β Draw) and draw on the paper. Keys 1β4 pick the ink: black, orange, blue and violet. As in Magic Paper, ink is a first-class object, and where it lands on something that understands it, it becomes meaning:
| You draw | It becomes |
|---|---|
| a straight line across a single neuron's decision-boundary plot | that neuron's boundary: the weights are solved from your line and oriented to fit the data. On XOR you can feel that no line works |
| an orange or blue dot on a dataset or boundary plot | a new data point of class 1 or 0 |
| a loop around objects | a selection, so "this" is what you circled |
| anything else | ink: a sketch object attached to what it was drawn over. It moves with that object, can be undone and inspected, can go into a glyph, and the AI can read it ("what did I just draw on?") |
The renderer turns screen points into data coordinates; the kernel only ever receives
semantic operations (add_point, set_boundary, draw). The AI draws too, but with
semantic shapes ({"op": "draw", "shape": "circle", "target": "plot_5"}: circle,
underline, arrow, cross, check), never coordinates. The kernel computes the strokes. In the
demo above, the offline planner circles the plot it is explaining. In a live test, gpt-5.5
circled the decision-boundary plot as the reason a single neuron fails. It declined to cross
out the claim, because the claim was unverified rather than refuted.
UI (React) ββββββ
Offline planner ββΌβββΊ Workspace kernel βββΊ deterministic executor
Claude agent βββββ validate Β· compile (seeded MLP training, experiments,
events Β· branches decision grids, sensitivity)
undo/redo Β· persist
src/kernel/: framework-independent.types.ts(object/relation/event model),kinds.ts(the kind registry that everything else is generated from),ops.ts(the semantic operation protocol and its validation),kernel.ts(event-sourced history, branches, undo/redo, diff),nn.ts+experiment.ts(the deterministic executor),layout.ts(semantic placement β coordinates),suggest.ts(contextual noticers),formulas.ts(live equations),view.ts(the semantic view the AI gets). The open-lesson kinds live inexpr.ts(the safe expression language),calc.ts(evaluation along the dependency arrows: formulas, RK4 systems, seeded trials, sweeps) andops-calc.ts(creating, linking, plotting and experimenting on them).src/agent/: clients of the kernel that share one host interface.local.tsis a deterministic planner for the canonical vocabulary, andlessons.tsgives it the open-lesson vocabulary (typed mathematics, predictions in words, what if, why) and the ready-made lessons.shared.tsholds what every LLM participant shares: the system prompt, the three tools (apply_operations,inspect,highlight), and their execution, where kernel validation errors go back to the model as tool errors.claude.tsandopenai.tsare thin provider loops over it.src/ui/: renderers are pure projections of semantic state. There is one per kind, inside a generic frame.
The full design (domain model, object schema, protocol, events, rendering, execution
boundary, demo, and scope) is in docs/DESIGN.md.
tests/live/journey.live.test.ts runs the canonical journey through a real model and logs
every batch the model submitted and every rejection. The first runs against OpenAI models
(gpt-4.1, gpt-5.4-mini, gpt-5.5) exposed real problems, which are now fixed:
- Responses API: newer models refuse function tools with reasoning on Chat Completions.
- Evidence bug: an experiment with no testable expectations was counted as refuting a
claim. Such experiments are now inconclusive and can't verify anything. Expectations
can also compare two variants (
"than"). - Friction: models wrote refs without
$, reused refs across batches, and used placement and metric synonyms. The protocol now accepts these unambiguous forms, and refs live for a whole agent turn. That took gpt-4.1 from failing the journey to 0 rejected batches.
tests/live/lessons.live.test.ts asks a real model to build lessons in subjects it has no
template for, and then to test a class's prediction. gpt-5.5 passes all four: radioactive
decay, the pendulum (where it measured β2, not 2, for doubling the length), simple vs
compound interest, and foxes and rabbits (LotkaβVolterra). Every first batch had been
rejected only because the model invented a placement key for its opening note. Placement
is only a layout hint, so the kernel now falls back to automatic layout and says so in the
result.
Any OpenAI-compatible server can run it too:
LIVE=1 LIVE_BASE_URL=http://localhost:11434/v1 LIVE_MODEL=qwen2.5:7b npm run test:liveLatest results (the full five-step journey, pass/fail as asserted by the test):
| Model | Path | Result |
|---|---|---|
| gpt-5.5 | OpenAI Responses | passes |
| gpt-5.4-mini | OpenAI Responses; Chat Completions (streamed) | passes both ways |
| gpt-4.1 | OpenAI Responses | passes |
| gpt-4.1 | Chat Completions | tool calls work; sometimes stops before training its fix to 100% (it reports the shortfall honestly) |
| qwen3:4b | Ollama, CPU | works, but thinks for thousands of tokens per turn: far too slow on 4 CPU cores. Ollama's OpenAI endpoint can't switch its thinking off |
| qwen2.5:3b | Ollama, CPU | the connection works end to end (tool call β kernel β reply), but the model is too small to build the construction |
| qwen2.5:7b | Ollama, CPU | 5Β½ minutes for the first turn, then an empty reply |
Against Ollama, streamed tool calls, the kernel round trip and the dev-server proxy all work. What's missing locally is model capability and speed: on CPU, use the largest tool-capable model you can run, or a hosted OpenAI-compatible server such as Groq.
.github/workflows/ci.yml typechecks, runs the offline test suite and builds on every push
and pull request. Starting the workflow by hand with live enabled also runs the
canonical journey against an OpenAI model, using the repository's OPENAI_API_KEY secret
if it is set.
- The LLM is never the source of truth. It submits typed operations. The kernel validates each batch atomically and rejects coordinates from the AI.
- No numbers from the model. Training, metrics, experiment verdicts and equations come from the seeded executor. Results are stored in events, so replay never recomputes them.
- Claims don't silently become facts. A claim's status changes only through
verify_claimwith an experiment as evidence. - Abstraction preserves construction. A glyph keeps its members, relations and a frozen definition.
- Branches are relationships, not copies. A variant carries
branched_fromand its assumption. A workspace fork carries its parent and fork point.