Spindel's inference combinators execute probabilistic programs as Spin
values. Pure models can keep the default :world-policy :fresh, under which
the particle methods (smc-infer, pimh-infer, pgibbs-infer, pgas-infer,
ipmcmc-infer) run as savepoint handlers (inference.smc): particles are
frozen forks of savepoints, and the measure holds Samples (result and
trace). A model that reads or changes registered room systems can request
canonical worlds:
(smc-infer model 32
{:world-policy :fork
:world-opts {:systems #{:knowledge :repository}}
:resample-threshold 0.5})
The model runs in a frozen ygg/fork! of the ambient execution context, the
root, owned by a world scope of its own. Every particle method
(smc-infer, importance-sampling, pimh-infer, pgibbs-infer, pgas-infer,
ipmcmc-infer, bbvi-infer, kernel-infer with a PInferenceKernel) then runs
savepoint SMC there; a method of several sweeps runs each in canonical worlds of
its own. Each
particle is a frozen fork of the root and runs the whole model: a model that
reads or changes room systems may make effects that are random without a
sample site (a model call), so particles never share a prefix, as pure
inference does up to the first random choice. At resampling, each
selected ancestor is forked again. Selecting one ancestor three times
therefore produces three independently writable child worlds, not three
aliases to one context. A superseded world is abandoned: its computation
unwinds (its finally blocks run) in that world.
This makes inference a composition of existing Spindel operations:
ambient world
-> fork the root
-> fork N frozen particles of the root, each running the model
-> run until a probabilistic checkpoint
-> score and select ancestors
-> fork each selected ancestor, abandon the sources
-> resume
-> project values and traces into an EmpiricalMeasure of Samples
-> discard the speculative world tree, then the root
The EmpiricalMeasure holds Samples: each particle's result, its trace and
its world's settled descriptor (measure/world-descriptors). It retains
neither contexts, the resampling ancestry, nor settlement authority. However
inference ends — a result, a failure, or the cancellation of its Spin — every
world is discarded before the outcome is delivered.
The lifecycle above is not specific to probabilities. The
org.replikativ.spindel.world.scope namespace owns a finite family of
canonical forks for any bounded algorithm such as MCTS or a simulation
campaign. A scope provides fork construction, composable activity leases,
atomic lease exchange, quiescence, cancellation hand-back, reverse-order
discard, and portable descriptors. Once quiescence is published, admission is
closed permanently.
The algorithm remains responsible for its computations and ends each activity
lease only after that computation has actually stopped. A live ForkHandle
never leaves the scope; fork callers receive a child execution context and a
portable descriptor, but no settlement authority. The scope never interprets
a particle, tree node, Run, reward, or proposal. In particular, a search policy
may share immutable statistics for a transposition, but it must not share a
writable context or affine ForkHandle.
Savepoint sessions own their worlds through a scope, and so does canonical inference. This is an extraction of the existing ownership protocol, not a second world abstraction: Yggdrasil still owns substrate forks and settlement, while the execution context still owns reactive runtime state.
org.replikativ.spindel.search.mcts/search performs deterministic, finite UCT
search over the same canonical worlds. An environment supplies four functions:
(require '[org.replikativ.spindel.search.mcts :as mcts])
@(mcts/search
{:actions (fn [state] (if (:done? state) [] [:left :right]))
:transition (fn [state action] (assoc state :done? true :choice action))
:terminal? (fn [state] (:done? state))
:reward (fn [state] (if (= :right (:choice state)) 1.0 0.0))}
{:done? false}
{:max-simulations 64
:max-depth 8
:max-nodes 128
:seed :example})
;; => {:search/selected-action :right, ...}
Each expanded tree node owns a canonical child world. A rollout receives a
scratch descendant, so its registered forkable effects cannot mutate the stored
node or the ambient world. Search fails closed before running an environment
callback in a speculative fork if Yggdrasil marks any registered system
:shared; an identity-forked system is not speculative isolation. Ordinary
mutable host objects captured by an environment closure are outside the world
model and must not be used for hypothetical state. :transition may change its
current registered systems, but the system registry itself is frozen before
callbacks so search cannot acquire child-only external resources without a
disposal owner. :actions, :terminal?, and :reward are observational
environment contracts. They may return either plain values or Spins. All
speculative worlds are discarded before the result is delivered, and the
portable result contains node statistics plus settled world descriptors—not
execution contexts or ForkHandles.
The implementation is deliberately sequential in its first version. A pure
:continue? predicate receives {:simulations n :nodes m} before every
simulation and can admit it against a host resource reservation more precisely
than a conversation-turn limit. Effects inside that simulation must still meter
and enforce their actual consumption. :max-simulations, :max-depth, and
:max-nodes are hard structural bounds. With E expanded nodes, at most E
retained node worlds plus one transient rollout world are live during sequential
search.
Selection is deterministic for the same seed, environment, and results. Actions
must be unique within a state. The seed and every action must be immutable,
cross-runtime data: nil, booleans, strings, keywords, symbols, UUIDs, finite
floating-point values, safe integers, and recursively composed vectors, lists,
sets, and non-record maps. Host objects, records, mutable arrays, ratios,
arbitrary-precision decimals, and integers outside the JavaScript safe range are
rejected. A custom pure :rollout-action receives the
action vector and deterministic search coordinates and returns an action index.
Applying :search/selected-action is intentionally a separate application
effect: search never silently commits a hypothetical world to its caller.
This version is a tree, not a transposition DAG. A future transposition table may share immutable statistics, but must never alias writable execution contexts or affine world handles. Multi-player/minimax value semantics, parallel rollouts, progress streaming, and persistent checkpoints likewise belong in explicit combinators rather than implicit behavior in this single-agent maximizing primitive.
Model failures are fail-fast. Spindel cooperatively cancels sibling particles, tracks their terminal callbacks, and discards their worlds automatically once they are quiescent. The thrown exception also contains actionable process-local recovery operations:
(:world/recovery (ex-data error))
;; => {:status :open
;; :manager <process-local capability>
;; :await-quiescent <CPS operation>
;; :cancel! <host function>
;; :discard! <host function>
;; :descriptors [<portable fork descriptors> ...]}
:descriptors lists the root, then the particle worlds. Descriptors are
safe durable/audit projections. The manager, operations, and
its live handles are process-local capabilities. A supervising host can await
quiescence and retry cleanup if automatic settlement encountered a recoverable
preflight failure. Cleanup is idempotent, concurrent callers share one result,
and a read-only preflight failure permits a later retry; failures after mutation
remain terminal for explicit substrate recovery.
Particle-local program state belongs in the particle ExecutionContext:
signals, Spindel atoms, continuations, trace, score, and checkpoint state all
fork with the world. The manager contains only host lifecycle capabilities for
the entire inference execution. Durable application records should store fork
descriptors, not contexts or handles.
The optional top-level :executor is used by every particle and forwarded to
its canonical fork. Without it, world-backed inference shares the ambient
world's executor, preserving scheduler ownership. The legacy :fresh policy
keeps its existing inference-local shared executor behavior.
Forking a Kontor ledger or another registered system creates a hypothetical branch of its state. It does not grant duplicate authority to spend external compute, tokens, money, or network capacity. Effects that consume real resources need an explicit host capability and an affine split/reservation policy. Simulations can instead receive stubbed effects, cheaper models, or a forked accounting scenario.
SCI code can construct another Spindel+SCI interpreter inside a child world,
provided the host explicitly injects the constructor, evaluator, and world
fork capabilities. The inner interpreter executes Spin values against the
child runtime, and the parent retains settlement authority. Raw sci.core
does not need to be exposed to untrusted Dvergr programs; recursive
self-programming is a curated capability, not ambient reflection. The world
API gives SCI opaque IDs backed by a host registry. Raw ForkHandle records
contain mutable affine authority and must never cross the sandbox boundary.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |