Drive the model or watch an agent, then look at the dependency graph below it: the nodes are this model’s own signals and memos, and one lights only when its compute function actually re-ran - fine-grained recomputation you can watch, counted for real.
You are the agent. Pick a pillar, drag its weight, and watch the predicted ranking move through a real speculate - while the committed model on the left does not. Commit to apply it for real.
This is the dependency graph for this one demo model, hand-wired to match it (not auto-discovered); a node lights laurel only when its compute function genuinely re-ran on the last commit (the count is that node’s real recompute total), and lights ocean, dashed when the current speculate touched it in a shadow with nothing committed.
The predicted panel is bridge.speculatePlan run in a copy-on-write shadow in your browser; the live panel is the committed graph, untouched until you commit. Commit calls commitPlan; the explain() trace and the subscribe() stream above are the real bridge output, not a recording.
Observation shouldn’t require mutation.
Every existing way an agent touches a UI - WebMCP actions, computer-use, Playwright scraping - shares one property: to learn what an action does, it has to do the action, then look again. Observation requires mutation. So real side effects fire (a network write, an email, a database row), the live app passes through every wrong state the agent considered, and planning more than one step ahead means entering branches you will not keep.
speculate removes that tax: it computes the exact derived consequence of a change with nothing committed, so the UI stops being a surface you poke and becomes a model you query.
One mechanism, three surfaces.
The reactive kernel
The whole engine is one idea: reading a value inside a computation subscribes that computation to it, and writing the value re-runs exactly the computations that read it. The core is about 200 lines. Every reactive thing - a signal (a unit of state), a memo (a value derived by a pure function), an effect (a computation that acts on the outside world) - is the same kind of node. Each node records the nodes it read last run (its sources) and the nodes that read it (its observers); reads and writes keep both links in sync.
Updates are two-phase, which is what keeps them glitch-free. A write only marks its dependents - directly downstream nodes as definitely-stale, everything below as maybe-stale - and computes nothing. Computation happens lazily on read: a maybe-stale node checks its own sources first, recomputes only if one of them actually changed, and propagates further only if its own result actually changed. A value written back equal to what it was stops the cascade dead. A diamond (two paths from one source to one sink) recomputes the sink exactly once, on fully-current inputs. Subscriptions are rebuilt every run, so a conditional branch subscribes only to what it actually read that time.
Rendering, without a diff
The renderer never diffs. It runs each component once to build real DOM nodes and wraps every dynamic binding in its own effect, so a change re-runs one binding, not a subtree. The common case - a text node whose value became a new string - reassigns that one text node’s contents in place. The same component code renders two ways through a single seam: in the browser a dynamic binding is a live subscription; on the server it runs once with no subscription and bakes into HTML. Nothing in a component knows which backend it ran under. That is why this site prerenders to static HTML at 0 KB JS, and why an interactive island can claim() the server-rendered DOM in place instead of throwing it away and rebuilding it.
The agent surface: subscribe, explain, simulate
pimas/agent turns the running graph into three things an agent can use. You declare what is readable with expose(name, fn) and what is callable with action(name, fn).
- L1Subscribe. Each exposed value is wrapped in an effect that emits a delta whenever it changes. Because the accessor runs inside that effect, it subscribes to exactly the fields it reads - () => rows[3].status watches just that field. The agent is pushed changes; no polling, no scraping the DOM.
- L2Explain. Calling an action records what it wrote and which exposed values changed as a result, by walking the dependency graph. explain() returns a causal sentence: total changed because addItem wrote cart[3].qty, which the total memo reads.
- L3Simulate. speculate runs hypothetical writes against a shadow of the graph and returns the exact predicted state without committing. It is the piece the rest of the framework exists to make possible - the next section.
How a what-if actually runs.
speculate works because the graph separates two things a virtual DOM keeps tangled: the topology (what derives from what) and the values flowing through it. A what-if shadows the values and reuses the topology read-only.
- 1A shadow overlay is opened - a map from node to hypothetical value. The real nodes are not touched.
- 2The hypothetical write lands only in the overlay.
- 3Reads during the speculation return the shadowed value if there is one; otherwise a memo is recomputed detached (no subscription, no ownership) against the overlay and cached, so a diamond still computes once; otherwise the real committed value is returned.
- 4No effect ever fires. Nothing that talks to the network, the DOM, or storage runs.
- 5The predicted state you asked for is read off the overlay and returned.
- 6Rollback is free: the overlay is dropped. The real graph was never mutated, so there is nothing to undo.
speculatePlan composes several changes in one shadow; speculateSweep runs one independent what-if per input, for a sensitivity sweep; commitPlan applies an approved scenario for real, so preview and commit stay symmetric. Store edits are shadowed too, by copy-on-write, so hypothetical changes to tables and lists work the same way.
01Side effects don't fire
A probe isn't free in a normal system: it can send a request, write a row, trigger a cascade. speculate recomputes the derived result with no effect flushed. The network, the database, the DOM are never touched.
02The live app never goes wrong
The search runs in the shadow, so nobody watching the app sees it thrash, the state never holds garbage between tries, and a crash mid-search can't freeze the real app in a broken state.
03Planning is counterfactuals
To choose between moves you must evaluate branches you won't take. If evaluating means committing, you can't compare without entering all of them. speculate weighs them against the same starting point; the agent commits once.
04Exact, not approximate
A learned world-model guesses the outcome and drifts. speculate re-runs the app's own pure memos against a shadow of the real graph, so the prediction is bit-identical to what committing would have produced.
It needs a graph that exists before the question is asked.
A virtual-DOM framework keeps no standing dependency graph. To find out what a change does it re-renders the component tree and diffs the output - and rendering routinely runs effects and reads that are not guaranteed pure, so there is no way to promise a probe was side-effect-free, and no exact derived state to read short of actually applying the change. A fine-grained engine keeps the graph standing between updates; a re-render-and-diff framework builds a fresh one every time and throws it away. Speculation needs the graph that is already there.
Same policy, same answers, a fraction of the footprint.
The same grid-search policy drives an OECD-style composite-indicator model through the projected tools under two conditions: a baseline that can only mutate and re-read, and one that also has the simulate_* tools. Both reach the same correct answer. The only thing that differs is what the run costs and how many wrong states the live model passes through.
| Task | Baseline (mutate & re-read) | With simulate_* |
|---|---|---|
| France → top-2 (solvable) | 55 calls · 37 commits · 27 wrong live states | 4 calls · 1 commit · 0 |
| Germany → #1 (impossible) | 54 calls · 36 commits · 36 wrong live states | 3 calls · 0 commits · 0 |
“Impossible” is where non-committal wins hardest: to be sure a goal cannot be reached, the baseline has to probe the whole space and pass through every wrong state on the way; the agent-native run sweeps it in the shadow and never touches the real model. The same preview → approve → commit loop has also run end-to-end against a real HTTP backend, where the preview shows the exact resulting totals and only the approved action fires a real write.
Non-committal what-if isn’t new. This combination is.
Speculative execution is a well-worn idea. What no one has put in one place is exact, zero side-effects, on a live reactive UI graph, as a first-class agent tool. Each neighbour gives up one of those.
Sharpest where a wrong commit costs something, and the logic is pure.
- →Quantitative models (the sharpest fit) - pricing, project economics, risk, physical models. The math is already pure, so exact what-if is free and what-if is the whole job. Proven on a self-replicating lunar-factory model (von-neumann, written up in the papers) and an OECD-style composite indicator (sector-engines).
- →Agentic ops & workflows - preview the exact resulting state, approve, then commit - instead of mutating live systems to find out what an action does. Run end-to-end against a real HTTP backend.
- →Robotics & physical orchestration - 'simulate before you act' as a safety property, when the committed action moves something real and irreversible.
- →Tooling for AI agents - give any agent a non-committal preview of a tool's effect before it fires, projected onto WebMCP as simulate_* tools it calls like any other.
One honest limitation. speculate is exact only if the derivations are pure - assumed, not enforced. That is exactly why the sharpest fit is pure, derived-heavy models: there, purity is free rather than a property you have to police.
The code, the decisions log, and the benchmark harness are on GitHub. It is published to npm as pimas-ui.
github.com/noahhyden/pimas →