Skip to content

After CU18 — A node is a function

pub trait Node: Send + Sync {
fn forward(&self, input: &Value, ctx: &Ctx<'_>) -> Result<Value, NodeError>;
}

Transition and Driver are gone. Decided and done on 22 August 2026, and the argument is short enough to fit here.

Transition::Await(requests) let a node suspend: it said what it needed, an injected Driver served it, and the node was asked again with the answers in ctx.results. It was the seam of an agentic layer — driver.rs said so in its own docstring, “that ignorance is what keeps the agentic layer out of the core”.

Done(...) written by hand across the repo164
places that returned Await14
…of those, outside the tests0
core surfaceTransition, Driver, ctx.turn, ctx.results, MAX_TURNS, 3 RunError variants, the turn loop
plumbing beyond the core12 files — bindings, transport, worker

Every node anybody wrote paid for the wrapper, and after eighteen use cases half the enum had no tenant. The project’s first rule is that nothing is written without a real consumer today; this was the largest exception in it.

The three things only suspension bought, honestly

Section titled “The three things only suspension bought, honestly”
  1. The engine could bound a runaway loop. MAX_TURNS = 64, a node that cannot stop failing instead of hanging. Real.
  2. The engine saw each turn. A base for traces, replay, checkpoints — and nothing used it, not one hook.
  3. A suspended node is resumable: a value and a turn number. The big theoretical justification, and the design did not deliver it — a node that accumulates does so in its own object, which is serialized nowhere. A durable agent was not checkpointable however much you wanted one.

One works, one is unused, one is not delivered.

What was kept, which is what makes it reversible

Section titled “What was kept, which is what makes it reversible”

Ctx stays. Not turn, not results — the type, carrying device. It is the channel by which whoever executes hands a node what it knows, and the day an agentic layer wants something injected it goes there and no node signature changes. That was the one strong argument for keeping Await — cheap now, breaking to add back — and keeping the channel dissolves it.

The agentic layer goes as concrete nodes instead: one that wraps a model call and its retries, one that routes, one that orchestrates several agents, each holding whatever client it takes. Retries in particular belong there and the repo had already said so — CU12 refused them at the engine because “a node that already ran half of itself is not idempotent and nobody has said what it means to run it again”.

What is lost, said as a decision and not a side effect

Section titled “What is lost, said as a decision and not a side effect”

The engine no longer bounds anything. A node that does not return does not return, the same way a function that does not return does not. It could not honestly do better: it has no way to tell a loop that will not end from work that is slow, and guessing wrong either way is worse than not guessing.

And whatever a node keeps is entirely its own business — which was already true, and is now the only thing that is true. The catalog holds the node, not a copy per run, so state outlives a forward: that is what lets an activation stay alive across a cut and what Split rests on, and it is a trap from the other side. There is a test in both languages saying so.

RunError goes from 11 variants to 8. advance() goes from 38 lines to 5. An artifact carries the nodes and not (nodes, driver), so provide() returns a dict.

And the other half of CU18: a guided sampler that knows what is in flight

Section titled “And the other half of CU18: a guided sampler that knows what is in flight”

A guided sampler spread over machines is worse than random until it knows what the others are holding, and the record was shaped so that knowing costs one scan and no fetches — state = running sits beside point.

The plan was to need no Rust: hand the sampler the in-flight points with a made-up bad score, which is constant liar as the literature has it, and let it avoid them. Measured, that backfires. Tpe sizes the pile it imitates as a share of everything handed over, so one more point raises the quota and promotes a trial out of the bad pile into the good one — and when that trial sits in the same region as the one in flight, the warning pulls the search towards it. Of two hundred proposals, one landed on the occupied region without the warning and thirty-nine with it.

So ask takes &[(Point, Option<f64>)] and an absent score means running. Nothing is made up: those points go in the pile to keep away from and do not vote on how big the other pile is. Four of the five schemes ignore the argument entirely, as they already ignored the history.

abandoned is the other half, and it decides nothing. Liveness here is not “does it answer” — nothing asks — but “is it still writing”, and report was already paying for that heartbeat. It is measured against the newest write in the study and not against this machine’s clock, because two machines sharing a folder are two clocks that disagree by minutes.