nREPL Bencode Protocol Experience Report
Direct nREPL protocol communication from Babashka, bypassing tmux for
agentic REPL-driven development. Demonstrates raw Bencode encoding/decoding.
Component Version
Babashka 1.12.218
babashka.nrepl 0.0.6-SNAPSHOT
Host hydra (FreeBSD 14.3)
nREPL uses Bencode (from BitTorrent) for message serialization.
Type Encoding Example
Integer i<num>ei42e → 42
String <len>:<chars>5:hello → “hello”
List l<items>eli1ei2ee → [1, 2]
Dict d<key><value>...ed3:foo3:bare → {“foo”: “bar”}
(defn bencode [data]
(cond
(integer? data) (str " i" data " e" )
(string? data) (str (count (.getBytes data " UTF-8" )) " :" data)
(keyword? data) (bencode (name data))
(sequential? data) (str " l" (apply str (map bencode data)) " e" )
(map? data) (str " d"
(apply str (mapcat (fn [[k v]]
[(bencode k) (bencode v)])
(sort-by key data)))
" e" )))
(declare bdecode-from )
(defn read-string-bytes [^java.io.PushbackInputStream in len]
(let [buf (byte-array len)]
(.read in buf)
(String. buf " UTF-8" )))
(defn bdecode-from [^java.io.PushbackInputStream in]
(let [c (.read in)]
(cond
; ; Integer: i<num>e
(= c (int \i))
(loop [sb (StringBuilder. ) ch (.read in)]
(if (= ch (int \e))
(Long/parseLong (.toString sb))
(recur (.append sb (char ch)) (.read in))))
; ; List: l<items>e
(= c (int \l))
(loop [acc []]
(let [peek (.read in)]
(if (= peek (int \e))
acc
(do (.unread in peek)
(recur (conj acc (bdecode-from in)))))))
; ; Dict: d<key><value>...e
(= c (int \d))
(loop [acc {}]
(let [peek (.read in)]
(if (= peek (int \e))
acc
(do (.unread in peek)
(recur (assoc acc (bdecode-from in) (bdecode-from in)))))))
; ; String: <len>:<chars>
:else
(loop [sb (StringBuilder. (str (char c))) ch (.read in)]
(if (= ch (int \:))
(read-string-bytes in (Long/parseLong (.toString sb)))
(recur (.append sb (char ch)) (.read in)))))))
(defn bdecode [s]
(bdecode-from
(java.io.PushbackInputStream.
(java.io.ByteArrayInputStream. (.getBytes s)))))
Get server capabilities.
>>> {"op" "describe"}
Bencode: d2:op8:describee
<<< {"id" "unknown",
"ops" {"clone" {}, "close" {}, "eval" {}, "describe" {}, ...},
"session" "none",
"status" ["done"],
"versions" {"babashka" "1.12.218"}}
Create a session. Required before eval.
>>> {"op" "clone"}
Bencode: d2:op5:clonee
<<< {"new-session" "4b9d0490-4311-4c5b-9720-b91e4e1e1905",
"status" ["done"]}
Evaluate code in a session.
>>> {"op" "eval", "code" "(+ 1 2 3)", "session" "<id>"}
Bencode: d4:code9:(+ 1 2 3)2:op4:eval7:session36:<id>e
<<< {"value" "6", "ns" "user", "session" "<id>"}
<<< {"status" ["done"], "session" "<id>"}
Multiple response messages until "status" ["done"].
Output comes as separate out messages.
>>> {"op" "eval", "code" "(println \"Hello\") :done", "session" "<id>"}
<<< {"out" "Hello\n", "session" "<id>"}
<<< {"value" "nil", "ns" "user", "session" "<id>"}
<<< {"value" ":done", "ns" "user", "session" "<id>"}
<<< {"status" ["done"], "session" "<id>"}
End a session.
>>> {"op" "close", "session" "<id>"}
<<< {"status" ["done" "session-closed"], "session" "<id>"}
5 Practical Example: Live AeroAPI Query
; ; Session already established
; ; Load AeroAPI
(nrepl-eval sock session " (require '[aeroapi.core :as api])" )
(nrepl-eval sock session " (def client (api/from-env))" )
; ; Query KBOS departures
(nrepl-eval sock session "
(let [resp (api/request client
{:method :get
:path \" /airports/KBOS/flights/departures\"
:query-params {:max_pages 1}})]
{:count (count (:departures resp))
:flights (->> (:departures resp)
(take 3)
(map #(select-keys % [:ident :destination :status])))})" )
Result:
{:count 15,
:flights ({:ident "RPA5647",
:destination {:code "KCLT", :city "Charlotte"},
:status "En Route / On Time"}
{:ident "DAL456",
:destination {:code "KATL", :city "Atlanta"},
:status "En Route / On Time"}
{:ident "FFT3057",
:destination {:code "KRDU", :city "Raleigh/Durham"},
:status "En Route / On Time"})}
6.1 TLS Dependencies in nrepl.core
The nrepl/nrepl library requires TLS classes unavailable in Babashka:
Unable to resolve classname: java.security.cert.Certificate
Location: nrepl/tls.clj:14:3
Workaround : Speak raw Bencode protocol instead of using nrepl.core.
6.2 Babashka nREPL vs JVM nREPL
Feature bb –nrepl-server clj -M:nrepl
Startup ~10ms ~3000ms
TLS No Yes
Middleware Limited Full
test.check No Yes
spec/gen No Yes
For agentic workflows, Babashka nREPL is sufficient for most eval operations.
For full Clojure features, use JVM nREPL.
6.3 Session State Persists
Variables defined in one eval are available in subsequent evals:
(nrepl-eval sock session " (def x 42)" )
; ; => #'user/x
(nrepl-eval sock session " (* x x)" )
; ; => 1764
6.4 String Length is Byte Length
Bencode string length is byte count, not character count:
; ; WRONG for unicode
(str (count s) " :" s)
; ; CORRECT
(str (count (.getBytes s " UTF-8" )) " :" s)
6.5 Multiple Response Messages
Eval returns multiple messages; collect until status contains "done":
(defn nrepl-recv-all [sock]
(loop [responses []]
(let [resp (recv-one sock)
responses (conj responses resp)]
(if (some #{" done" " error" } (get resp " status" ))
responses
(recur responses)))))
6.6 Dict Keys Must Be Sorted
Bencode spec requires dict keys in sorted order:
; ; CORRECT
(sort-by key data) ; Sort before encoding
; ; Will work but technically non-compliant
; ; (just iterate without sorting)
File Purpose
scripts/nrepl-client.bbFull nREPL client (unused due to TLS)
bin/repl-session.shtmux-based fallback
skills/clojure-repl/references/nrepl-protocol.mdProtocol reference
docs/agentic-repl-workflows.orgComparison of approaches
Direct Bencode communication with nREPL is feasible from Babashka for
agentic workflows. The protocol is simple and stateful (sessions persist).
For full Clojure features (spec generators, test.check), use JVM nREPL.