Table of Contents
- 1. Agentic REPL Harness
- 1.1. Core Pattern
- 1.2. Common Mistakes
- 1.3. Robust Eval with Completion Detection
- 1.4. State Detection
- 1.5. Hypothesis-Driven Workflow
- 1.6. Daemon Architecture (Recommended)
- 1.7. Babashka nREPL Client
- 1.8. Performance Comparison
- 1.9. Considerations
- 1.10. Direct nREPL (JVM Client)
- 1.11. Method Comparison
- 1.12. Skill Reference
1. Agentic REPL Harness
For AI agents driving the REPL through tmux, the key pattern is eval-wait-capture with state detection.
1.1. Core Pattern
eval_repl() { local code="$1" timeout="${2:-3}" tmux send-keys -t repl "$code" Enter sleep "$timeout" tmux capture-pane -t repl -p | tail -20 }
1.2. Common Mistakes
- Not waiting for output — capture happens before eval completes
- Assuming REPL state — sent Clojure to a shell prompt
- Namespace not loaded —
(r/help)fails withoutrequire - Working directory wrong — file operations fail silently
- Quoting issues — shell interprets quotes before tmux sees them
1.3. Robust Eval with Completion Detection
eval_with_wait() { local code="$1" max_wait="${2:-30}" local marker="__DONE_$$__" # Send code wrapped with completion marker tmux send-keys -t repl \ "(do $code (println \"$marker\"))" Enter # Poll for marker local elapsed=0 while [[ $elapsed -lt $max_wait ]]; do if tmux capture-pane -t repl -p | grep -q "$marker"; then tmux capture-pane -t repl -p | grep -B 20 "$marker" | head -19 return 0 fi sleep 1 ((elapsed++)) done echo "TIMEOUT" && return 1 }
1.4. State Detection
detect_repl() { local output=$(tmux capture-pane -t repl -p | tail -5) if echo "$output" | grep -qE '=>|user>|λ'; then echo "clojure" else echo "shell" fi }
1.5. Hypothesis-Driven Workflow
# 1. Observe shape ./repl-eval '(keys (first data))' # 2. Sample distribution ./repl-eval '(->> data (map :type) frequencies (sort-by val >))' # 3. Bind result ./repl-eval '(def errors (->> data (filter #(= :error (:type %)))))' # 4. Refine hypothesis ./repl-eval '(->> errors (group-by :source) (map (fn [[k v]] [k (count v)])))'
The REPL maintains state across evaluations — intermediate results persist for the entire session. This is the key advantage over scripts: each eval builds on previous work without re-computation.
1.6. Daemon Architecture (Recommended)
The optimal setup: JVM nREPL runs as a daemon, Babashka scripts query it.
┌─────────────┐ Bencode/TCP ┌─────────────────┐
│ Babashka │ ←───────────────→ │ JVM nREPL │
│ (10ms) │ port 1667 │ (daemon) │
│ Scripts │ │ Full Clojure │
└─────────────┘ │ AeroAPI libs │
│ Stateful │
└─────────────────┘
Why this architecture:
- JVM startup cost paid once (daemon)
- Babashka scripts start in 10ms
- Full Clojure available (specs, test.check, Java interop)
- State persists across queries (defs, loaded namespaces)
- LAN-accessible for remote agents
# Start daemon once clj -M:dev -m nrepl.cmdline --bind 0.0.0.0 --port 1667 & # Fast queries via Babashka (111ms vs 2.5s) bb scripts/nrepl-query.bb '(count (all-ns))' bb scripts/nrepl-query.bb ' (require (quote [aeroapi.core :as api])) (def client (api/from-env)) (count (:arrivals (api/request client {:method :get :path "/airports/KBOS/flights/arrivals"}))) '
1.7. Babashka nREPL Client
Babashka speaks raw Bencode to the JVM daemon:
#!/usr/bin/env bb ;; nrepl-query.bb - Fast queries to JVM nREPL daemon (defn bencode [data] (cond (integer? data) (str "i" data "e") (string? data) (str (count (.getBytes data "UTF-8")) ":" data) (map? data) (str "d" (apply str (mapcat (fn [[k v]] [(bencode k) (bencode v)]) (sort-by key data))) "e"))) (defn nrepl-eval [code] (let [sock (java.net.Socket. "localhost" 1667) out (.getOutputStream sock)] ;; Clone + eval + collect responses (.write out (.getBytes (bencode {"op" "clone"}))) ;; ... parse bencode responses ... )) (when (seq *command-line-args*) (println (:value (nrepl-eval (first *command-line-args*)))))
1.8. Performance Comparison
Benchmark: 100 iterations of (+ 1 2) against running JVM nREPL daemon.
| Client | Min | Max | Avg | Speedup |
|---|---|---|---|---|
bb nrepl-query.bb |
103ms | 184ms | 115ms | 21x |
clj -M:dev -e |
2400ms | 2500ms | 2462ms | baseline |
=== Benchmark Results (hydra, FreeBSD 14.3) === Simple query '(+ 1 2)' - 100 iterations: Min: 103ms Max: 184ms Avg: 115ms API query with AeroAPI call - 10 iterations: Avg: 1081ms (includes ~500ms network latency) JVM client - 3 iterations: Avg: 2462ms
Key insight: The Babashka client overhead is ~115ms. The JVM client pays ~2400ms of startup cost every invocation. For agent automation where you may issue hundreds of queries, this 21x speedup adds up.
The daemon architecture gives agents full JVM Clojure power with Babashka-level startup times. Best of both worlds.
1.9. Considerations
- Daemon lifecycle — Start nREPL on boot or first use. It holds state (loaded namespaces, defs) across queries. Restart to reset.
- Connection pooling — Each
nrepl-query.bbcall opens a new connection. For burst queries, consider keeping a session open. - Error handling — Bencode parse errors are silent. Add logging in production scripts.
- LAN security — Binding to 0.0.0.0 exposes nREPL. Use firewall rules or bind to specific interface (Tailscale IP recommended).
- Memory — JVM daemon holds loaded code in memory. For long-running daemons, monitor heap usage.
1.10. Direct nREPL (JVM Client)
For JVM-to-JVM communication (editor integration):
(require '[nrepl.core :as nrepl]) (with-open [conn (nrepl/connect :host "hydra" :port 1667)] (let [client (nrepl/client conn 5000) session (:new-session (first (nrepl/message client {:op "clone"})))] (doseq [msg (nrepl/message client {:op "eval" :code "(+ 1 2)" :session session})] (when (:value msg) (println (:value msg))))))
1.11. Method Comparison
| Method | Sync | Structured | Startup | Best For |
|---|---|---|---|---|
| tmux send-keys | No | No | 0 | Human-in-loop |
| bb → JVM nREPL | Yes | Yes | 10ms | Agent automation |
| JVM → JVM nREPL | Yes | Yes | 2400ms | Editor/CIDER |
| bb –nrepl-server | Yes | Yes | 10ms | Babashka-only projects |
1.12. Skill Reference
Full skill with references: gist:nrepl-bencode-experience