An FRP programming model for agents, in a discourse framework. Agents are reactive processes in continuous-time rooms; humans, LLMs, and scripts are participants in the same shape, composing through tagged messages on a small pub/sub kernel.
You program agents and compose them into multi-agent workflows. And because agents run in the same live medium they're built from, an agent can even begin to compose workflows itself, in a forked world you review — early and exploratory, not the default mode.
Built on Spindel (functional reactive runtime), Datahike (immutable Datalog), and Yggdrasil (copy-on-write branching across git + database).
A programming model for multi-agent systems:
:escalation/budget, policy-bots
subscribe by capability tag; neither hardcodes the other…and a batteries-included substrate to run agents on:
clojure_eval — a safe code sandbox — the agent's main programming surface:
Clojure eval in an isolated SCI context wired to the dvergr world (rooms,
knowledge, intake), with new dependencies loaded through a human-approval gaterequires, edits, and git commits its own code there, and the
workspace forks/merges with the roommuschel shell (parsed +
filesystem-sandboxed, not raw bash -c)claude CLI, zero-key),
OpenAI, Fireworks, or any OpenAI-compatible endpoint — swappable per agent:directive/raise-budget
for more, with all spend recorded in a ledger — so cost stays boundedmerge and bounded by budgets. (An early capability we're exploring — not the default mode.)| Web dashboard | Terminal UI |
|---|---|
![]() | ![]() |
A room with its agents, live token cost, and collapsible thinking/tool activity — the same rooms, in the browser or the terminal.
git clone https://github.com/replikativ/dvergr.git
cd dvergr
clojure -M:cli # daemon + nREPL(:7888) + TUI chat
Pick a provider by exporting its API key (or set it in config.local.edn):
# Anthropic — or zero-key via the local `claude` CLI (auto-detected on PATH,
# no key needed with a Claude Code subscription)
export ANTHROPIC_API_KEY=sk-ant-...
# OpenAI, or any OpenAI-compatible endpoint
export OPENAI_API_KEY=sk-...
export OPENAI_BASE_URL=https://api.openai.com/v1 # optional; override for compatibles
# Fireworks (OpenAI-compatible; the bundled model registry is Fireworks)
export FIREWORKS_API_KEY=fw-...
The shipped default config leaves the agent's provider unpinned, so it auto-selects the best provider you have a key for (preference: Anthropic → Fireworks → OpenAI → the local
claudeCLI) and its default model. Set any one of the keys above and it works — you don't have to match a specific provider. Pin:provider/:modelinconfig.local.ednto force one. If no provider key is set at all, the daemon still boots but logs a warning and agent turns fail at call time.
Copy config.example.edn → config.local.edn (gitignored)
to set the default model, agents, and a Telegram bot token — see
doc/configuration.md and
provider setup.
Type a message, press Enter; Ctrl-C quits. Rooms + history persist automatically (Datahike). Frontends are interchangeable over the one daemon:
clojure -M:cli # daemon + nREPL(:7888) + TUI
clojure -M:cli --no-tui --web # server box: daemon + nREPL + web dashboard (127.0.0.1:17880)
Standalone uberjar — one file with daemon + nREPL + TUI + web + Telegram.
CircleCI builds it on every push to main and attaches it to a GitHub release,
so you can grab the prebuilt jar from the
latest release — or build
it yourself:
clojure -T:build harness # → target/dvergr-<ver>-harness.jar
java -jar dvergr-<ver>-harness.jar # run it
(require '[dvergr.core :as d])
;; Build a room and join a coder agent
(def room (d/room :scratch))
(binding [org.replikativ.spindel.engine.core/*execution-context* (:ctx room)]
(d/join room (d/coder {:id :coder})))
;; Send a user message; the agent replies asynchronously
(d/post! room (d/message :you :coder "Add input validation to src/app.clj"))
;; Read the message log
(d/log room)
;; => [{:from :you :to :coder :content "..." :type :user/message}
;; {:from :coder :to :you :content "..." :type :user/message}
;; ...]
For tagged routing, per-consumer buffer policies, persistence, and the fork-and-merge proposal pattern, see doc/getting-started.md.
dvergr-cli keys, persistence,
provider configLiterate, live-running Clay notebooks — each
builds and runs a real room inline. Browse them rendered at
replikativ.github.io/dvergr, or open the
source in your editor. Build locally with clj -M:clay -m notebooks.render
(needs the quarto CLI).
| Notebook | What it shows |
|---|---|
| Getting started | rooms, participants, post!, tagged routing — from zero |
| Programming model | the compositional kernel: capability subscriptions, escalation, per-consumer buffer/SLA policy |
| Humans & agents | humans as participants, background tasks, propose → accept/reject (fork & merge) |
| Agents & tools | a real LLM agent, clojure_eval as the SCI sandbox, budgets + context compaction |
The standalone, runnable scenarios these import live in examples/
(clj -M:examples -m scenario-auditor).
| Namespace | What it provides |
|---|---|
dvergr.core | Public facade — re-exports the most-used vars |
dvergr.discourse | Room, participant, post!, ask, fork-room, merge-room |
dvergr.runtime.bus | Pub/sub routing kernel + opinionated buffer policy table |
dvergr.discourse.llm | llm-agent — directive-aware participant |
dvergr.discourse.generation | GenerationHandle + sync/future/external/streaming adapters |
dvergr.participant.context | ParticipantContext — uniform memory+budget across LLM/human/hybrid |
dvergr.discourse.personas | researcher, coder, reviewer — pre-built agents |
dvergr.rooms.forks | fork! / review / merge! / discard! — the fork→review→merge lifecycle behind spawn_agent/propose_change |
dvergr.cli.main | -main of the TUI chat client |
(require '[dvergr.model.providers :as providers])
;; Auto-registers from env: ANTHROPIC_API_KEY, OPENAI_API_KEY, FIREWORKS_API_KEY
(providers/ensure-initialized!)
;; Anthropic via the local `claude` CLI (no API key needed)
;; Auto-detected if `claude` is on PATH.
;; Any OpenAI-compatible provider
(providers/register-openai-compatible!
:groq
{:base-url "https://api.groq.com/openai/v1"
:api-key (System/getenv "GROQ_API_KEY")})
;; Local model via Ollama
(providers/register-openai-compatible!
:ollama
{:base-url "http://localhost:11434/v1"
:api-key "ollama"})
| Library | Role |
|---|---|
| Spindel | FRP reactive runtime, CoW context forking, pub/sub |
| Datahike | Immutable Datalog database (conversation + knowledge) |
| Yggdrasil | Copy-on-write branching across git + Datahike |
| SCI | Sandbox for clojure_eval and agent code |
| spindel-tui | Terminal UI built on JLine + Spindel signals |
| hato | HTTP client for provider APIs |
| Telemere | Structured logging + observability |
SCI fork note (Clojars library consumers only). dvergr pins a fork of SCI (
whilo/sci, branchresource-check) for the:interrupt-fncancellation the sandbox relies on, pending an upstream PR. tools.build'swrite-pomemits only Maven coords, so this git dep is not in the published pom. The uberjar/CLI and git-dep consumers get it transitively and need nothing extra; a pure-Clojars library consumer must add it themselves:org.babashka/sci {:git/url "https://github.com/whilo/sci" :git/sha "24762a163ef5b25c692d0e5cd4ea63a5bd6b0a16"}(Goes away once the fork lands upstream or is published to Maven.)
Copyright © 2026 Christian Weilbach. Apache License 2.0 — see LICENSE.
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 |