Liking cljdoc? Tell your friends :D

Agent programs in the room REPL

Status: deterministic and native LLM/tool interpreters implemented. The data model and Run boundary are intentional; simulation, replay, resource splitting, and durable continuation are later interpreters over the same contract.

Dvergr agents can construct specialized workers and compose their executions from the SCI REPL. The initial surface separates four concepts:

ConceptMeaning
AgentDefImmutable, versioned executable data: role, prompt, skills, tools, model policy, and program reference.
RosterImmutable collection of AgentDefs plus portable defaults and scope data.
RunDurable identity and lifecycle of one bounded execution in a Room.
RunHandleOpaque process-local identity/control capability for a live Run; it exposes a native result Spin explicitly.

An actor is durable identity; an AgentDef describes behavior; a Run is one performance. They must not collapse into one mutable "agent object."

Reactive process semantics

The primitive is a reactive process, not a chat turn. A model API exchange is a discrete integration step inside one interpreter; a Run may instead be a single calculation, a queued dialogue, a continuously updated monitor, a fork/join workflow, or a probabilistic search. Room messages are the durable event source, thread views select conversational context, and the Run is the causal/resource lifetime that owns effects.

The current surface deliberately reuses Spindel's algebra:

CompositionConversational interpretation
await / joindepend on one or several completed computations
parallel Spinsgather independent evidence concurrently
racetake the first satisfactory branch and cancel losers
signals / reactive atomsaccumulate beliefs, memory, or workflow state with fork semantics
context forkexplore a copy-on-write alternative or particle
cancellationwithdraw attention and terminate owned effects

Queueing, merging observations, switch-to-latest steering, approval gates, and long-lived output/effect streams should be derived attention combinators over the same substrate. They must not be encoded as special cases of a global turn loop. :llm is the first convenient process interpreter, not the definition of an agent.

Construct and compose

Inside a room-bound agent sandbox:

(require '[dvergr.agent :as agent]
         '[org.replikativ.spindel.spin.cps :refer [spin]]
         '[org.replikativ.spindel.effects.await :refer [await]])

(let [team (-> (agent/roster {:id :investigation})
               (agent/make-agent
                {:id :analyst
                 :skills #{:research}
                 :program {:kind :scripted :reply "evidence"}})
               (agent/make-agent
                {:id :reviewer
                 :skills #{:review}
                 :program {:kind :echo}}))
      analyst (agent/hire! team :analyst {:task "inspect the claim"})
      reviewer (agent/hire! team :reviewer {:task {:claim 42}})]
  @(spin
     {:evidence (-> (await (agent/result-spin analyst)) :run/value)
      :review   (-> (await (agent/result-spin reviewer)) :run/value)}))

roster, make-agent, and revise-agent have no bang because they are pure: each returns a new value and leaves its input unchanged. hire! has a bang because it admits a durable :agent-task Run, posts the precise trigger, and starts its Spin in the current Room. Admission is synchronous and durability-first: once hire! returns, observe and cancel! can address the Run without a startup race. If trigger persistence or spawning fails, the already-admitted Run is durably failed.

Use await on (agent/result-spin handle) inside spin. This is a passive observer: several branches may await one Run, and cancelling one observation does not cancel the shared work. Use (agent/owned-result-spin handle) for a race arm whose loss should cancel the hired Run. The opaque handle does not pretend to implement Spindel's graph protocol; the explicit distinction keeps ownership and dependency tracking visible in the owning fork. Dereference is available at a REPL or test boundary, but workflow code should stay compositional. A top-level blocking await shim is not required.

The handle supports:

(agent/run-id handle)        ; durable UUID
(agent/result-spin handle)   ; passive observer Spin; safe to share
(agent/owned-result-spin handle) ; owning race arm; losing cancels the Run
(agent/observe handle)       ; durable Run projection
(agent/cancel! handle)       ; targeted cooperative cancellation

Native model and tool programs

An :llm program lifts Dvergr's existing single-exchange model/tool core directly behind the same Run boundary:

(let [team (agent/make-agent
            (agent/roster {:id :research})
            {:id :analyst
             :prompt "Investigate the task and report evidence with caveats."
             :tools #{:clojure_eval :knowledge_search}
             :model-policy {:provider :codex-subscription
                            :model "codex-subscription-sol"}
             :program {:kind :llm
                       :max-model-steps 32
                       :budget-dollars 0.50}})
      work (agent/hire! team :analyst {:task "Test the strongest contrary case"})]
  @(spin (await (agent/result-spin work))))

Every hire receives a fresh ChatContext in the Room's Spindel execution context. Parallel hires therefore have independent dialogue and budget state while seeing the same fork-local Geschichte/Datahike substrate. The AgentDef tool set is both the schema shown to the model and the authoritative execution allowlist. An omitted tool set means no tools.

An LLM Run records a deterministic :run/chat-id. With a Datahike-backed Room, that ID addresses the persistent model/tool trace; an ephemeral Room owns and closes the per-Run chat database when the worker has physically terminated. Automatic reconstruction of an interrupted live exchange is not implemented yet: the durable trace supports inspection and a later explicit retry/resume policy, not resurrection of native streams after process loss.

Blocking provider I/O runs off Spindel's drain thread. Dvergr owns prompt and context assembly, compaction, tool dispatch, SCI isolation, cancellation, activity messages, and Run settlement; providers—including Codex and Claude subscription transports—only supply model responses.

:max-model-steps bounds automatic model/effect integration (default 32, maximum 256). A step is one provider exchange, not a conversational turn and not Dvergr's scheduling clock. Reactive attention may queue, merge, observe, fork, or switch work around these discrete transport boundaries. :budget-dollars is a per-execution spending ceiling (default $1), not a Kontor resource allocation. When it is exceeded after a tool-bearing model step, the Run settles as :waiting with reason :budget-exhausted; no process remains active. A true resumable continuation and affine resource settlement are separate later contracts.

An LLM-created sandbox can still build and revise arbitrary immutable rosters, but its hire! authority currently accepts only provider-free :echo and :scripted children. This prevents a child from minting new provider spend, tools, or recursive LLM work before Kontor can split a resource vector from the parent Run. A top-level Room REPL has no such program-kind ceiling.

Evaluation ladder

The API and the instructions that teach it are evaluated together at three levels:

  1. Deterministic law/contract tests exercise fork ownership, parallel join, race cancellation, admission, drain, authority, and durability without a provider.
  2. Stub-model interpreter tests exercise prompt/context assembly, exact tool schemas, model-step continuation, activity correlation, budgets, and cleanup.
  3. Opt-in model benchmarks give the same room-REPL tasks to Codex, Claude Code, API models, and local models. They record whether generated SCI compiles, whether the causal result/effects are correct, leaked Runs/resources, model steps, wall time, task version, and model. Token/cost receipts and a stable hash of the assembled system prompt remain benchmark-reporting follow-ups.

The initial model-facing task set should include pure roster specialization, parallel research/review and reduction, race-with-loser-cancellation, explicit structural child Runs, delegation-ceiling recognition, and creation of a nested durable Room. Later attention combinators add queue/observe/steer/switch tests. Passing only one model is not sufficient evidence that the programming surface is clear.

The first opt-in comprehension probe is dvergr.agent.program-bench/run-v1! on the :dev classpath. It runs through the production prompt, provider, tool, SCI, Spindel, and Run paths while retaining every generated clojure_eval call in its report:

(require '[dvergr.agent.program-bench :as bench])
(bench/run-v1! :codex-subscription "codex-subscription-sol")
(bench/run-v1! :claude-code "claude-code-sonnet")

It is an explicit REPL benchmark rather than a CI test because it is nondeterministic, needs local subscription authentication, and consumes model resources. Its language and lifecycle contracts are duplicated as provider-free deterministic tests. In the 2026-08-28 probe, Codex Sol and Claude Code Sonnet each discovered the API, created and joined two child Runs, and returned the expected value with two clojure_eval calls. Earlier attempts exposed two real harness defects: the native interpreter omitted the shared sandbox prelude, and Claude Code's own native tools shadowed Dvergr's supplied tool protocol.

An explicit :parent-run on hire! records structural spawning. Causal succession remains derivable through the new Run's trigger message and that message's :run-id; it is not mislabelled as parenthood.

Selection and specialization

AgentDefs are ordinary data, so roles and policies can be generated, selected, revised, stored, reviewed, and passed into reusable workflow functions:

(agent/select team {:skill :research})
(agent/select team {:skills #{:legal :research}})

(let [v1  (agent/lookup team :analyst)
      ref (agent/ref v1)
      v2  (agent/revise-agent team :analyst
                              {:prompt "Challenge the strongest contrary case"})]
  ;; A stale reference fails instead of silently running revised behavior.
  (agent/lookup v2 ref))

Portable data is enforced at construction. Functions, JVM atoms, provider clients, connections, streams, and other live handles cannot be embedded in an AgentDef or Roster.

State and fork semantics

A Roster is a value, not a hidden registry. Thread it through functions or carry it as a Spin result. Forked computations can therefore use different derived rosters without synchronization or leakage.

If a workflow needs changing state, allocate it through Spindel's fork-aware state vocabulary (signals or reactive atoms) in the relevant execution context. Do not place semantic workflow state in a JVM atom, dynamic singleton, or process-global registry. SCI Var forkability is a separate runtime project; do not rely on a top-level def as the portable state boundary until that work is complete.

A RunHandle is intentionally not durable state and should not be stored inside an AgentDef. Its awaitable result/cache belongs to the Spindel execution graph; provider streams and cancellation capabilities remain process-local. The completion value and admission fence are allocated in the Room's Spindel context, so those runtime values follow copy-on-write fork semantics. The live Run registry is likewise an explicitly process-local control plane, not workflow state: moving only one token into a forkable cell would not make native handles serializable or restorable. Durable meaning belongs to the Room's messages, Run projection, artifacts, and later effect receipts. Rehydrate those facts and construct fresh live handles rather than serializing a native handle.

A live handle is confined to the Spindel execution-context fork that created it. Using it from a child or sibling fork fails explicitly: observe the durable Run facts there, or start a branch-local Run. A future cross-context result bridge must copy values deliberately rather than sharing continuations across forks.

Current boundary and next interpreters

The deterministic interpreter proves the bounded programming contract without an LLM: parallel composition, targeted cancellation, exact trigger/output correlation, and room-scoped execution all run through the same SCI surface an agent uses.

The native :llm interpreter now lifts Dvergr's existing single-exchange core rather than wrapping another harness. It preserves:

  • Dvergr-owned ChatContext assembly and compaction;
  • the room-bound SCI sandbox and forked Geschichte/Datahike workspace;
  • authoritative tool allowlists and approval boundaries;
  • native API, Codex subscription, Claude Code subscription, and local providers;
  • Run-correlated activity/output and resource accounting;
  • blocking provider I/O off the Spindel drain thread;
  • delegation attenuation before child execution.

:roster/scope is still portable policy data, not an enforced resource grant. The native interpreter enforces its concrete tool allowlist, model-step bound, and ChatContext spending ceiling, but does not pretend those are an affine resource split. A Kontor-backed admission/settlement interpreter should make declared resource vectors executable before scopes can delegate assets to child Runs.

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