Dvergr's core model is Rooms + Participants on a Bus. Every conversation — a human chatting with one agent, a multi-agent debate, a speculative coding fork — is the same shape. Understand this page and the rest of the system follows.
:id, a human-facing slug, and can nest under a parent.on-message handler
that returns a reply (or nil for silence).Every message carries a single :to value. Each participant subscribes to two
topics: [:to <its-id>] (directly addressed) and [:to nil] (broadcast). Since
a message has exactly one :to, nobody is delivered the same message twice, and
the chat-room semantic falls out naturally: a broadcast reaches everyone; a
targeted post reaches only the addressee.
The one rule frontends use to decide who to address is discourse/room-target:
A room with exactly one agent participant (reserved
:_…ids ignored) addresses that agent — it's a DM. A room with zero or several agents returnsnil— input broadcasts.
Every frontend (TUI, web, REPL, medium adapters) routes through this single rule,
so a room behaves identically no matter how it was entered. Deriving the target
from the room — not from UI state — is also what prevents echo loops, where
broadcast input would feed an agent that is itself subscribed to [:to nil].
discourse/post!), from a human
client, an adapter, or another agent's reply.on-message, and — if it
has something to say — posts a reply back to the room, which flows the
same way.Agents join a room with discourse/join, which wires their inbox +
broadcast subscriptions and starts their process. Leaving (leave) just
unsubscribes them. Persistent rooms also mirror every message into a store, so
history survives restarts.
A Room still owns one chronological message log, participant set, policy, and substrate. A Thread is a lightweight topical projection over that log, not a child Room or database. Every message carries two independent causal fields:
:in-reply-to names its immediate parent;:thread-root-id names the stable root of the topic.Top-level messages self-root. Use reply for nested replies so the root is
preserved automatically:
(require '[dvergr.core :as d])
(def room (d/room :planning))
(def root (d/message :you :researcher "Compare the pricing options"))
(d/post! room root)
(def follow-up
(d/reply :you :researcher "Include self-hosting" root))
(d/post! room follow-up)
;; The thread is queried from the Room; there is no Thread registry.
(d/messages room {:thread-root-id (:id root)})
Thread identity also informs the attention boundary for an LLM agent. The default
policy lets a direct same-thread message steer its active execution, while a
direct message rooted elsewhere queues a separate execution without cancellation
or topic mixing. llm-agent accepts an :attention-policy returning :steer,
:queue, or :observe, so deployments can distinguish direct human correction
from peer-agent chatter using their authoritative actor and addressing data.
Tool activity and telemetry remain non-waking observations.
When a topic needs separate participants, permissions, budget, or a branched workspace, promote the work to a child Workroom. That is an actual Room; an ordinary Thread deliberately is not.
A room can be forked copy-on-write for hypothetical or risky work:
fork-room clones the participants into a sibling room seeded with the
parent's history. With :isolation :ctx, it also forks the execution
context — branching the Datahike DB, git worktrees, and other substrate — so
the fork's side effects stay isolated until you decide their fate.merge-room collapses the fork's branch back into the parent atomically
(Datahike branches merge, git branches fast-forward / three-way merge).discard throws the fork away, deleting its branches; the parent is untouched.This is the backbone of the proposal/review lifecycle (an agent works in a fork,
proposes a merge, a reviewer accepts or rejects) and of cheap theory-of-mind
probes — simulate-reply forks the room, asks a hypothetical, captures the
reply, and discards, all without disturbing the real conversation.
Rooms are venues; Threads are topical projections; participants are reactive processes; the bus routes between them; addressing is "one agent = DM, otherwise broadcast"; and forking gives any Room value semantics so speculation is free and reversible.
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 |