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:
- π¬ Discourse rooms β humans + LLM agents exchange messages as equal participants
- π·οΈ Tagged routing β agents escalate
:escalation/budget, policy-bots subscribe by capability tag; neither hardcodes the other - πΏ Substrate forks β a "what would the coder write?" probe runs on a branched git worktree + datahike, then merges or discards atomically
- π§© Compositional kernel β five primitives (tagged message, capability sub, dynamic subscribe, fork-room, GenerationHandle) cover the whole programming surface
β¦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 gate - π¦ A real workspace it owns β each room clones the
dvergr-sandbox stdlib into its own
git repo; the agent
requires, edits, andgit commits its own code there, and the workspace forks/merges with the room - π οΈ A curated toolset around it β structured read/write/edit, structural
Clojure edit, a test runner, and a jailed
muschelshell (parsed + filesystem-sandboxed, not rawbash -c) - π‘ ~25 intake sources β read-only data feeds (Hacker News, Reddit, RSS, web / YouTube / tweet fetch, mail, SEC-EDGAR, GitHub, β¦) ship as editable source in the workspace β the agent reads, copies, and extends them β pulling into a Datahike knowledge graph
- π Boundary credential handling β intakes that need an API key get a placeholder in the sandbox; the gated HTTP egress swaps in the real key only at the bound domain and scrubs it from the response, so the agent uses keys it never sees (design)
- π Multiple LLM providers β Anthropic (incl. the local
claudeCLI, zero-key), OpenAI, Fireworks, or any OpenAI-compatible endpoint β swappable per agent - πͺ Budgets & accounting β every turn is metered in microdollars against a
per-agent budget; an agent hits a checkpoint and can escalate
:directive/raise-budgetfor more, with all spend recorded in a ledger β so cost stays bounded - π§ Context management β long runs auto-prune tool outputs and summarize older turns as the model's window fills, so an agent holds a long conversation without overflowing context
- πΎ Persistent + multi-frontend β rooms, history, and knowledge persist in Datahike; drive the same daemon from the TUI, the web dashboard, Telegram, or nREPL
- π Self-programming substrate β agents operate in the same SCI/FRP world they're
built from, so an agent can spawn agents and wire workflows in a fork, gated by your
mergeand 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 chatPick 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.
- Getting Started β first-room tutorial, REPL + CLI paths
- Programming Model β bus, tagged routing, GenerationHandle, the distributive law Ξ»
- CLI Reference β
dvergr-clikeys, persistence, provider config - Architecture β the formal model: rooms, the pub/sub bus, agents as reactive processes, ToM via substrate fork
- Tools & the SCI sandbox β the toolset, the workspace agents run code in, and its safety boundaries (incl. credential injection)
- Doc index β full table of contents
Literate, 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.