Liking cljdoc? Tell your friends :D

Runs as worlds of a search

Status: plan (2026-10-16), step 3 of speculation.md. Verified against spindel v0.1.122, dvergr main and simmis main; the decisions below were taken in discussion. Steps are built in the order given.

Step 3 asked for "Runs as worlds of a scope", so that best-of-n over Runs and foerster's steer share one search. Making a Run world be a spindel scope world would fight nine mismatches, each confirmed in the code: the scope keeps fork handles private while a Room owns and settles its own; the scope forces :mode :frozen while Run worlds fork :following; a scope fork has no bus, store, registry entry or grants; a Run is a durable identity and a world is process-local; model steps cannot be copied mid-step; a Run's wallet is keyed by the Run, so the ledger authority neither returns nor splits it; quiescence is tracked by scope activities on one side and the Run supervisor on the other; :review and :deferred worlds outlive any scope; and a Run has two parents (the control Room that records it, the world it forks).

So this plan unifies one level up: one search over worlds, with two kinds of world behind one small protocol. Particles and simulations stay bare scope worlds; Runs stay Room forks. They share the algorithm (open, weigh, copy, drop, select), the budget (one ledger), and the join (merge, hand to review, or discard).

The protocol

OperationScope worlds (particles, simulation)Run worlds (agent Runs)
open from a parent, with a grantworld-scope/fork! with :granthire-prepared-in! with :settlement :deferred (fork, durable Run, allocation from the parent's wallet)
weightthe particle's log weight, at any observationa score on the finished Run: verifier, judge, evaluation :reward; mid-Run scores do not exist yet
select the joinnew world-scope/adopt! (change A), then merge into the sourceforks/release-deferred! then forks/merge! (automatic), or release-deferred! to :review, where simmis promotes it to a Proposal (run-world-proposals/promote!, top-level Runs only)
discardworld-scope/release!forks/discard-deferred!
spent and leftPResourceAuthority/balancethe Run wallet; a Run returns its remainder to the parent when it ends, before settlement, so selecting or discarding a finished Run moves no budget
copy k timesworld-scope/copy! with :split? (synchronous authority, JVM)not available: resume! resumes the latest savepoint once, of a terminal Run, with its whole remainder and the same seed (change D)

Handing a world to review ends the search's ownership of it: a :review world waits on a person without a deadline, so a search that kept it could not reach quiescence.

Changes, in order

1. dvergr: best-of-n over Runs (no copying). A real selector, which does not exist today: workflow/attempt! returns N :review worlds for a reviewer and forks/merge-review! judges one fork at a time. The search opens N Runs with :settlement :deferred, weighs them when they finish, selects by a policy (:automatic merges the highest weight above −∞, :review hands it to a person, :keep-all hands all; when every member is impossible, nothing is merged or handed on, and the search reports no accepted result), and discards the rest with discard-deferred!. Admission blocks on the store and must run off the drain thread (spindel#110), as evaluation does. Laws: conservation across the N Runs (each returns its remainder), one settlement into the immediate parent, refund of the unselected.

2. dvergr: Run wallets stay as they are (dropped). Letting the authority's balance resolve a Run's wallet would change what world-scope/copy! splits: an SMC under :world-policy :fork inside a Run, without a grant of its own, would split the Run's whole remainder among its particles. A search reads what a Run has left with dvergr.resource/wallet-microdollars instead, and Run allocation stays off the authority: the Run spend path (model-scope, consume!, return-unused-resources!, resume!) reads the Run-keyed allocation, and a Run world carrying [:dvergr/wallet] would redirect every nested fork's spend.

3. spindel: a selection join for scope worlds (world-scope/adopt!). Hand one open world's handle out of the scope (as ygg/transfer-fork! hands authority over) so its new owner can merge it into the source. It must settle what the scope otherwise settles at discard: return or hand over the world's wallet (the authority has no merge operation), remove it from the :copied bookkeeping, and settle its copy family's ancestry, which holds the split's rounding remainder. Optional :mode in :fork-opts, which today is overwritten with :frozen.

4. Effect classes (speculation.md step 4). Every speculative world runs under an enforced effect filter from the first search on (Law 7), since discarding a losing member cannot undo a remote write. dvergr.effects already classifies operations and filters them per world, and a fork can only narrow its parent's filter; a search's members run under {:admit #{:read :write :spend :network} :speculative true}. The class filter admits reads, writes, charged spend (model calls carry :spend and :network) and network requests, and refuses egress, processes, schedules, lifecycle operations and global writes. A class is too coarse to confine writes to the member's world, so the :speculative predicate filter refuses what the classes admit but discarding cannot undo: writes to Rooms (:room/post, :room/write, :room/join, :room/merge, :room/gc, which act on the target Room directly), undeclared tools, opening a mail store (:mail/open creates a shared store and its account row) and mail syncs, and HTTP other than GET, HEAD and OPTIONS. Processes are refused because only the shell jail's listed commands run without a network (dvergr.intake.jail), so a shell could reach a remote; admitting them needs a classification of commands. Finer classes are also a prerequisite for copying Runs: a copy redoes the work since its savepoint, so :once effects would happen k times.

5. dvergr: copying a Run at a savepoint. A k-way fork from one savepoint: a counted claim (where claim-resume! allows one), per-copy seeds derived from each copy's fork id (Law 8), since resuming hydrates the savepoint's own seed and k copies would draw the same, and an explicit source of funds. A Run that finishes normally has already returned its remainder (finalize-execution-external!), so copies are funded by fresh grants from the parent's wallet, admitted all-or-nothing: if one of the k grants or admissions fails, the others are returned and no copy runs. Only for Runs whose effects since the savepoint are forkable or charged (step 4).

6. steer over Runs needs scores at savepoints, mid-Run (a process reward the Run publishes), which nothing provides yet. Until then, steer runs over scope worlds, as it does in the sandbox.

Steps 1 and 2 are small and useful now. Step 3 is independent. Steps 5 and 6 wait on step 4 and on a decision about mid-Run scores.

What does not change

Room forks, the fork contract, Run admission and resume, settlement policies, adoption into Proposals, the Run-keyed wallets and the ledger's transfer ids. Model steps stay uncopyable mid-step.

Constraints from simmis

  • promote! takes a top-level Run world in :review, one adoption per Run; a search's selected Run reaches review this way, the others never do.
  • Copies of a Run would appear as k Runs with the same :run/caused-by, while :run/resumed-by is single-valued; the Run detail and Attempt board group by parent and cause and have no notion of a search or of siblings.
  • Rooms hold :room/budget-dollars in the system DB; simmis does not mint or install the ledger, so a search's conserved spend is not visible in simmis today.
  • Simulated results must stay apart from accepted state (doc/recursive-work-roadmap.md): a search whose join is discard, with assumptions and samples recorded.
  • Limits: the workflow caps attempts at 8 and models at 4.

Decisions

  1. Parallel and alternative composition. Overlay forks (simmis's private and campaign forks) are for concurrent tasks that coordinate and are all meant to land: parallel composition. A search is for alternatives of one task, of which at most a few land: alternative composition. A search's members are Run worlds; overlays organize concurrent work inside any world, and an overlay task may start a search for its own step.
  2. Automatic, inspectable, overridable. A search runs under a policy that selects by weight and discards the rest without anyone acting. While it runs, a person can open any live member, inspect it, and override the policy (select, keep, drop). A retention rule (keep the best k for inspection until a deadline or a budget runs out) decides what stays inspectable after selection. Handing a world to review ends the search's ownership of it.
  3. Weights are observe and factor. Anything may weigh a Run. The interface is foerster's: a world's weight is what its observe and factor statements accumulate. A verifier is a factor of 0 or −∞, a judge a factor of its log score, a person's choice an observe of it, and a process reward a factor at a savepoint (:barrier true), which is what steer resamples on, so mid-Run scores need no other mechanism.
  4. Nested Runs are funded, not refused. The model is built for recursion. A child Run funded by a grant from its parent's wallet may make provider calls; the refusal (:provider-effects? false) remains for unfunded children. Conservation keeps the ledger's totals, but it is not a spending cap: a charge is made after the provider call and clipped to the wallet (dvergr.resource/charge-spend!), so concurrent children and retries can spend more than their grant. A funded child's grant therefore carries provider admissions as well (dvergr.resource/model-dispatches, spent by admit-model-dispatch! before each call), which do bound what reaches the provider. Inside a speculative world a child Run needs a constrained admission, since :run/start carries :lifecycle, which the speculative filter refuses: it is admitted only when funded by such a grant, its world inherits the speculative filter (a fork composes its filter onto its parent's and can only narrow it), and it settles deferred to its parent. Settlement is bottom-up: a child's deferred world is owned by its parent Run's coordinator, which, before the parent is weighed or settled, merges the accepted children into the parent's world (their immediate parent) and discards the others, so a parent is judged with its delegated work; when the parent is discarded or cancelled, its remaining children are discarded recursively. Built with a two-level test of both selection and discard.
  5. Simulation is one object with kinds. A simulation is a search whose join discards and records: assumptions, samples, results, provenance, behind the epistemic boundary of simmis's roadmap (simulated is not observed). Kinds: projection (what will happen), counterfactual (what would have happened), estimation (what is true now but unobserved), hypothetical (how would an actor respond). Simmis's Futures view becomes a Simulations view with projection as one kind; :budget and :goal intents are plans, not simulations, and are routed apart from it.
  6. Searches are recorded durably; settlement after a restart waits for the shared capability store. A search's members, weights, policy and outcome are facts in the store (each member records the intended decision before its world is settled, then the outcome), so a search is visible and auditable after a restart. The live fork handles that allow a merge or discard stay process-local, as for Proposals. After a restart, an unsettled member is recorded as unavailable, a durable state, not discarded: its physical cleanup waits until settlement authority can be recovered. An idempotent reconciliation refunds the wallets of Runs the restart stranded (today orphaned Runs are failed without a refund, dvergr.agent.run), bottom-up: a descendant's refund before its ancestor's, also when a reconciliation is retried, since an ancestor's return is made once (its transfer id is stable) and a child's later refund would stay in the ancestor's wallet. Settlement that survives a restart is the same problem simmis has for Proposals and is solved once, for both.

Budgets as a tree

The recursive harness is a programming model: a task breaks into sub-tasks, an evaluation into roles, a benchmark into cells, a search into members, an optimization of a workflow into evaluations of candidate workflows. Its budget breaks down the same way, as a wallet tree that mirrors the call tree: every activation (a Run, an attempt, a search member, a particle) receives a grant from its parent, sub-grants to its children, and returns what it did not spend.

Three laws make it compose:

  1. Conservation. What the children are granted is at most what the parent holds, and what they leave returns to it (the ledger, dvergr.resource).
  2. Attenuation. A child never holds more authority than its parent: money, provider admissions, its effect filter (a fork can only narrow it), and whether it may call providers at all.
  3. Accountability. Every node records its spend under a role, so spend sums by role over any subtree: what judging cost in a benchmark, what a candidate cost per attempt.

Roles and splits. A role is a label on a child (:candidate, :driver, :judge, :member, :particle, or any keyword), and a split is what a parent declares for its children: an amount per role, or shares of its grant. The attempt wallets of evaluation-model.md are one instance (an attempt node, split into candidate, driver and judge by the EnvironmentDef, so every candidate gets the same budget), an experiment another (cells), a search another (members, re-split as members are dropped, which is what SMC's resampling does), and an optimizer a fourth: how much budget each candidate workflow gets is the optimizer's own decision, allocation as the policy over the moves.

Funded children. A child Run may make provider calls only when its grant carries money and provider admissions (dvergr.resource/model-dispatches): each call consumes one admission before it reaches the provider (dvergr.resource/admit-model-dispatch!), so the number of calls is bounded in advance. Admissions bound the count of calls, not their cost: calls in flight at once, or a streamed retry, can incur provider usage that a crash leaves uncharged, and a later charge is clipped to what the wallet holds (dvergr.resource/charge-spend!), so conservation alone does not cap what the provider bills. A funded child therefore also reserves money before each HTTP dispatch, retries included, durably and under the dispatch's own id: a conservative amount covering every billable usage class the provider may charge for that request (input, output, cache writes and reads, reasoning tokens), computed from limits the request actually enforces (its maximum output, its prompt size) at the model's registry prices. A dispatch for which no reliable bound exists (a model without complete prices, a request without an enforced output limit) is refused. After the call, the reservation is settled against the usage the provider reported, returning the difference, once per dispatch id; when the outcome is ambiguous (a broken stream without final usage, a crash), the full reservation stays spent, never more. Together, admissions cap the number of calls and reservations cap their cost. An unfunded child keeps the refusal it has today (:provider-effects? false). hire! already allocates a child's :resources from its parent Run's wallet (dvergr.resource/allocate-run!), which refuses a grant the parent cannot cover, and a Run whose allocation carries admissions has every provider call admitted (dvergr.resource/model-scope).

What exists and what is missing. Run wallets split from the parent and return their remainder; world wallets (particles, speculative worlds) are granted by the ledger authority. Missing are the delegation rule for funded children (above), roles on Runs and in receipts, declared splits, and aggregation of spend by role. Run wallets (keyed by the Run) and world wallets (keyed by the fork) remain two kinds of node; making them one kind is a later step, once roles and splits exist to test it against, since the Run spend path reads the Run-keyed allocation (change 2 above).

Order of work

  1. This plan and its decisions.
  2. (Change 2 is dropped; see above.)
  3. The speculative effect filter (dvergr.effects, :speculative with the class filter above), tested, before any search runs; then best-of-n search over Runs, recorded durably, with weights by observe/factor and the policies :automatic, :review and :keep-all (change 1, decisions 2, 3, 6).
  4. Funded child Runs, roles, declared splits and spend by role (Budgets as a tree), with the attempt wallets of evaluation-model.md as their first user; then nested Runs in speculative worlds (decision 4), settled bottom-up. Unifying Run and world wallets follows.
  5. world-scope/adopt! in spindel (change 3).
  6. Simmis: the Simulations view (decision 5) and the search's siblings in the Run detail.
  7. Effect classes, then copying Runs at savepoints and steer over Runs with factor at savepoints (changes 4 to 6).

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
←Move to previous article
→Move to next article
Ctrl+/Jump to the search field
× close