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.
What they were, and what they cost
Section titled “What they were, and what they cost”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 repo | 164 |
places that returned Await | 14 |
| …of those, outside the tests | 0 |
| core surface | Transition, Driver, ctx.turn, ctx.results, MAX_TURNS, 3 RunError variants, the turn loop |
| plumbing beyond the core | 12 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”- The engine could bound a runaway loop.
MAX_TURNS = 64, a node that cannot stop failing instead of hanging. Real. - The engine saw each turn. A base for traces, replay, checkpoints — and nothing used it, not one hook.
- 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.