3 The programming model — a compositional kernel
(ns notebooks.programming-model
"Live Clay notebook: the compositional kernel — tagged messages, capability
subscriptions, escalation, and per-consumer buffer policy. No LLM, no network.
Reuses the scripted participants from
examples/{scenario_auditor,scenario_manager_escalation,scenario_streaming_partial}.clj
and drives rooms inline (never calls their `-main`)."
(:require [notebooks.support :refer [settle]]
[dvergr.discourse :as d :refer [with-room]]
[scicloj.kindly.v4.kind :as kind]
[scenario-auditor :as aud]
[scenario-manager-escalation :as esc]
[scenario-streaming-partial :as stream]))Everything in dvergr composes through tagged messages on a small pub/sub kernel. A message’s :type is its illocutionary force — what the utterance does — and participants subscribe by the kinds of speech act they answer rather than by hardcoded sender→receiver wiring. (See discourse-theory.md for that framing.)
This notebook shows three faces of that one idea — each runs live, with no model. Every block binds the room’s execution context with with-room.
1. Capability subscription — listen beyond your inbox
A Participant always receives messages addressed to it ([:to id]). It can add a second subscription with d/subscribe! on a capability tag — and then it sees messages it was never addressed in. That is the basis for cross-cutting monitoring (auditors, budget policies) without coupling.
(def audit-room (d/room :audit-demo))(def seen (atom []))(kind/hidden
(with-room audit-room
(let [auditor (d/join audit-room (aud/make-auditor seen))]
;; the extra subscription: ALL :escalation/budget messages, regardless of :to
(d/subscribe! audit-room auditor [:type :escalation/budget])
(d/join audit-room (aud/make-noisy-agent :worker-a audit-room))
(d/join audit-room (aud/make-noisy-agent :worker-b audit-room)))
nil))Each worker escalates on every message; the auditor also gets one direct message to its inbox.
(kind/hidden
(with-room audit-room
(d/post! audit-room (d/message :user :worker-a "do something"))
(d/post! audit-room (d/message :user :worker-b "do something else"))
(d/post! audit-room (d/message :user :auditor "what have you seen?"))
nil))(settle 300)nil:as-inbox true = addressed to the auditor; false = picked up via the tag subscription. The auditor observed escalations addressed to other agents.
(kind/table @seen)| type | from | to | as-inbox |
|---|---|---|---|
| message | user | auditor | true |
| budget | worker-a | false | |
| budget | worker-b | false | |
| budget | worker-a | false | |
| budget | worker-b | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-a | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-b | false | |
| budget | worker-b | false |
(let [escalations (count (filter #(= :escalation/budget (:type %)) @seen))
inboxed (count (filter :as-inbox @seen))]
(kind/pprint {:escalations-observed escalations ; via subscribe!, not addressed to :auditor
:inbox-messages inboxed ; addressed to :auditor
:total-seen (count @seen)})){:escalations-observed 129, :inbox-messages 1, :total-seen 130}(kind/hidden (with-room audit-room (d/close-room! audit-room) nil))2. Escalation — post a tag, a policy answers
The same mechanism drives escalation. alice escalates by posting an :escalation/budget message — not by calling a named manager. A separate policy-bot, subscribed on that tag, replies with :directive/raise-budget addressed back to her. Neither hardcodes the other; they compose because the room is a tagged-message namespace.
(def budget-room (d/room :budget-demo))(def budget (atom 50)) ; cents; each reply costs 40(def wrapping-up? (atom false))(kind/hidden
(with-room budget-room
(d/join budget-room (esc/make-alice budget-room budget wrapping-up?))
(esc/spawn-policy-bot! budget-room)
nil))(kind/md (str "Initial budget: **" @budget " cents**"))Initial budget: 50 cents
Round 1 — enough budget: alice answers, balance drops to 10.
(kind/hidden
(with-room budget-room
(d/post! budget-room (d/message :user :alice "what is 2 + 2?")) nil))(settle 200) alice: replied to :user — spent 40 remaining 10
nil(kind/md (str "After round 1: **" @budget " cents**"))After round 1: 10 cents
Round 2 — the next reply would cost 40 but only 10 remain, so alice posts :escalation/budget instead. policy-bot raises it by 100, addressed back to her; the user retries and the answer goes through.
(kind/hidden
(with-room budget-room
(d/post! budget-room (d/message :user :alice "what is 5 * 7?")) nil))(settle 300) alice: budget 10 below cost 40 — escalating
policy-bot: saw escalation from :alice with payload {:remaining 10, :requested 100}
alice: received raise-budget + 100 → 110
nil(kind/hidden
(with-room budget-room
(d/post! budget-room (d/message :user :alice "what is 5 * 7?")) nil))(settle 200) alice: replied to :user — spent 40 remaining 70
nil(kind/pprint {:alice-budget @budget
:wrapping-up? @wrapping-up?
:log-size (count (d/log budget-room))}){:alice-budget 70, :wrapping-up? false, :log-size 7}The log shows alice’s :escalation/budget and policy-bot’s :directive/raise-budget addressed back to her.
(kind/table
(map #(select-keys % [:from :to :type :content]) (d/log budget-room)))| from | to | type | content |
|---|---|---|---|
| user | alice | message | what is 2 + 2? |
| alice | user | message | alice's reply (#40 spent) |
| user | alice | message | what is 5 * 7? |
| alice | budget | ||
| policy-bot | alice | raise-budget | |
| user | alice | message | what is 5 * 7? |
| alice | user | message | alice's reply (#40 spent) |
(kind/hidden (with-room budget-room (d/close-room! budget-room) nil))3. Buffer policy — one stream, per-consumer SLAs
Tags also carry streams. One producer fires 50 :partial/token messages; two consumers tap the same [:type :partial/token] topic with different buffer policies:
audit — the
:partialdefault (fixed-buffer 256): tokens are discrete data, so the default is lossless.UI — overridden to
sliding-buffer 1: a view that only wants the latest accumulated state opts into lossy semantics, per subscription.
The producer is unaware of either; each consumer picks its own SLA.
(def stream-room (d/room :stream-demo))(def ui-got (atom []))(def audit-got (atom []))(def n 50)Pubs start their pump on first subscription, so consumers come first. The UI consumer sleeps 5ms/token to simulate a slow re-render.
(kind/hidden
(do (stream/spawn-ui-consumer! stream-room ui-got)
(stream/spawn-audit-consumer! stream-room audit-got)
(stream/spawn-token-producer! stream-room n)
nil))(settle (+ 200 (* 5 n)))nilAudit consumer — lossless, sees every token (count = n):
(kind/pprint {:tokens-seen (count @audit-got)
:expected n
:first-10 (vec (take 10 @audit-got))}){:tokens-seen 50, :expected 50, :first-10 [0 1 2 3 4 5 6 7 8 9]}UI consumer — lossy on purpose, sees only a sample (varies run-to-run):
(kind/pprint {:tokens-seen (count @ui-got) :sample @ui-got}){:tokens-seen 1, :sample [49]}The room log keeps everything regardless of per-consumer policy:
(kind/pprint {:log-size (count (d/log stream-room)) :expected n}){:log-size 50, :expected 50}(kind/hidden (with-room stream-room (d/close-room! stream-room) nil))Where to go next
Forks & proposals — fork a room, let a worker change a branched git + datahike, then
merge!ordiscardatomically (forks_and_proposals).Agents & tools — a real LLM agent, the
clojure_evalsandbox, the muschel shell, budgets, and context compaction (agents_and_tools).
The deeper algebra — ask / fan-out / race / quorum / pipeline and the theory-of-mind simulate-reply — is in programming-model.md and discourse-theory.md.