Node is the entry point to the NTL runtime, and it is smaller than it looks.
It owns storage, activation, propagation and learning — and nothing else. No
socket, no thread, no timer. It decides what should happen to a signal and
records the consequence; you supply the I/O and the loop that calls it.
The one thing to know first
Node is synchronous. Every method on this page returns a value, not a
future. ntl-core contains no async fn at all, holds no async runtime, and
reaches for no ambient clock or randomness — time and entropy arrive through
the injected Clock and Rng traits.
That is not an oversight to be fixed later. It is what keeps the crate
building for wasm32-unknown-unknown, which CI enforces on every commit, and
what lets the whole decision path be tested without a network. Async lives one
layer out, in ntl-net, which owns the transport and the task that drives it.
So there is no .await anywhere below.
Every sample on this page is compiled and executed by
runtime/ntl-core/tests/api_reference_node.rs. If the API changes, that test
stops building. cargo doc --open remains the authoritative reference; this
page is an orientation for embedding a node.ntl_core — note the underscore in Rust paths. The ntl crate
this page once imported does not exist.
Building a node
Node::builder() returns a NodeBuilder. build() runs schema migrations,
validates configuration and restores any persisted activation state, then
hands back a Node.
build() returns Error::Config when none was
supplied. There is no default: a node that silently kept its state in memory
when you meant to persist it would lose every learned weight on restart
without saying so.
NodeBuilder methods
That is the whole builder. Six setters and abuild().
Settings that are configuration, not builder calls
Three things this page used to put on the builder areNodeConfig fields.
They live there because they are validated together at load, and because a
node that silently ignored one of them would be worse than one that refused to
start:
Deterministic construction
Because the clock and the RNG are injected rather than ambient, a node can be built to replay exactly — which is how the runtime’s own time-dependent behaviour is tested without sleeping.ntl_core::testing::test_node(u8) packages exactly this and returns the node
alongside its ManualClock, so a test can advance time to exercise decay,
refractory periods and receipt timeouts.
Inspecting a node
identity() -> &NodeIdconfig() -> &NodeConfigstore() -> &Arc<dyn NodeStore>— the way to read node state; see Synapsesnow_ns() -> u64— this node’s clock, not the host’s
node.status(). What that method claimed to return is spread
across identity(), the store, and learning_health(),
each of which is a real value rather than a snapshot struct that could drift
out of date.
Emitting signals
The builder is handed to the node. The node stamps origin, identifier and timestamp and claims the signal in its own dedup cache, so there is noemit
on the builder itself.
emit only creates the signal. It does not route it — that is the next call.
Receiving signals
There is no handler registration and no listener.SignalHandler,
register_handler, node.listen() and node.wait_correlation() are not
identifiers this crate defines. A node is driven by whoever owns the
transport: you hand it a signal, and it returns a Disposition describing
what it decided.
Two entry points, for two different provenances.
receive_local — a signal this node emitted
receive because emit already claimed the signal in the dedup
cache — running it back through receive would see its own claim and drop it.
A local emission also skips admission control: the node chose to send this, so
throttling itself on its own traffic would be backwards.
receive — a signal that arrived from a peer
arrival_synapse: Option<&SynapseId> is the synapse the signal came in on, so
the node does not route it back where it came from.
Reading a Disposition
Driving the node
There is norun_until_shutdown(). The node has no thread and no timer, so
nothing below happens on its own; the loop is yours, and it must call these.
poll_activation()— backpressure must be a delay, not an indefinite hold. The guard insidereceivecan only act when another signal arrives, so a node whose traffic goes quiet needs this called periodically, or a below-threshold signal waits forever while its sender times out on a path that was working.sweep_timeouts(limit)— this is what makes failure observable. A path that never delivers produces no receipt, so without the sweep it looks identical to a path never tried, and the model can never learn to avoid it. Returns how many decisions were resolved.checkpoint()— saves the activation snapshot, purges expired dedup entries, and flushes the store. Skipping it makes a restart a free reset for anyone flooding the node.
receive could not
have planned one — it did not yet know the signal would fire:
Synapses
There is nonode.synapses(). Synapses are store state, read through a
filter. Registering one is a node method, because forming a synapse changes
learning state.
SynapseFilter::eligible() over active() for anything
routing-related. eligible includes Weakening, and excluding it would make
weight recovery impossible — a synapse that failed once could never earn its
way back. That is a normative requirement in
synapse-lifecycle, not a preference.
Outcomes
The half that makes a node learn. Every forwarding decision is journalled; these calls resolve one and move the synapse’s weight.Option<WeightUpdate> — None when the receipt matched no pending
decision. An unmatched receipt is discarded rather than raised, since forged
receipts would otherwise be the cheapest attack on the routing model. A
replayed receipt has no second effect: the first outcome wins.
Option<SignatureFailureOutcome>, carrying the failure count in the
current influence window and whether this one crossed the threshold and pruned
the synapse.
Some(f)
is the factor applied; None means no rescaling was needed. apply_receipt
and fail_forward already call it after moving a weight, so you only need it
when you have changed weights yourself.
Observability
LearningHealth samples that many recent decisions and reports
decisions_sampled, exploration_ratio, pending_ratio and
delivery_ratio. The two middle ratios are the model’s health check:
exploration near zero means the node has stopped learning; a pending ratio
near one means it is not receiving the receipts it needs, and its weights
reflect nothing.