vim-slime

<!DOCTYPE html> <html> <head> <title>Tarn Barford</title> <meta charset="utf-8"/> <link rel="icon" type="image/x-icon" href="/favicon.ico"> <link href="/style.css" media="screen" rel="stylesheet" type="text/css" /> <link rel="alternate" type="application/atom+xml" title="Journals of Tarn Barford" href="/atom" /> <link href="/highlight.css" media="screen" rel="stylesheet" type="text/css" /> <link href="/highlight-console.css" media="screen" rel="stylesheet" type="text/css" /> </head> <body> <div id="container"> <div id="header"> <div id="header"> <p>From the <a href="/journal">Journals</a> of <a href="/">Tarn Barford</a></p> <h1> vim-slime </h1> <p> Mar 26, 2012 </p> </div> </div> <div id="post_content"> <html><body><p>Today I found the awesomeness that is <a href="https://github.com/jpalardy/vim-slime">vim-slime</a>, it's been an exciting day for me. <a href="http://common-lisp.net/project/slime/">Slime</a> is the "The Superior Lisp Interaction Mode for Emacs", I can almost hear the emacs crowd laughing.</p> <p>For those that use vim and haven't used Slime, vim-slime or <a href="https://github.com/vim-scripts/VimClojure">something similar</a>, this is why it's awesome:</p> <p><strong>Text can be sent from any process to the stdin of a <a href="http://www.gnu.org/software/screen/">gnu screen</a> or <a href="http://tmux.sourceforge.net/">tmux</a> session. The process in this case is vim and the screen/tmux session is a terminal</strong>.</p> <p>Screen is a <a href="/journal/oh-screen-where-have-you-been">really neat</a> terminal multiplexer (you can run multiple terminals in a terminal window). The multiplexed shell processes are children of the screen process, which itself is not a child of the terminal window process. This means a screen process and its child processes keep running if you close the terminal window. Later you can re-connect to it, this is what makes vim-slime possible.</p> <p>Here is an screen shot, on the left is me in gVim writing some awful Clojure <a href="#footnote-1">[1]</a>. On the right is a screen buffer in which I started a Clojure REPL. When I want to try run some code I can send any vim text selection to the REPL in a keystroke (or two).</p> <p><img alt="vim slime screenshot" src="screenshot.jpg"/></p> <p>It doesn't have to be a Clojure REPL either, we can send anything to a screen shell. We could run git commands, find, grep, sed, etc. Like with the Clojure REPL we can even interact with any terminal programs that use STDIN.</p> <p>This concept can be taken even further, You can even connect to a tmux session over SSH and share a terminal or a <a href="http://remotepairprogramming.com/remote-pair-programming-with-tmux-and-vim-the">terminal program like vim to do remote pairing</a>!</p> <p>Hopefully remote pairing is the topic of my next post as there are a couple geographically distant people I know who are keen to do some pair hacking. I stand to learn a lot!</p> <p><a name="footnote-1">[1]</a> I learnt almost everything I know about Lisp from <a href="http://www.ccs.neu.edu/home/matthias/BTLS/">The Little Schemer</a>. Great book.</p></body></html> </div> <div id="comments"> </div> </div> <div id="footer"> <p>&nbsp;</p> <p>Questions, comments, suggestions? Email me, <a href="mailto:tarn@tarnbarford.net">tarn@tarnbarford.net</a> (<a href="/pgp.txt">public key</a>)</p> <p>&nbsp;</p> </div> </body> </html>

Permalink

Swipe Keyboard

<!DOCTYPE html> <html> <head> <title>Tarn Barford</title> <meta charset="utf-8"/> <link rel="icon" type="image/x-icon" href="/favicon.ico"> <link href="/style.css" media="screen" rel="stylesheet" type="text/css" /> <link rel="alternate" type="application/atom+xml" title="Journals of Tarn Barford" href="/atom" /> <link href="/highlight.css" media="screen" rel="stylesheet" type="text/css" /> <link href="/highlight-console.css" media="screen" rel="stylesheet" type="text/css" /> <style> #swipe-canvas { position: relative; width: 900px; height: 300px; } #swipe-results { font-size: 30px; padding-left: 50px; padding-left: 50px; } #swipe-results ul { margin: 0px; padding: 0px; } #swipe-results li { float: left; background-color: #DDDDDD; list-style-type: none; padding: 10px; margin: 5px; border-radius: 5px; } #swipe { position: relative; } #swipe-loading { position: absolute; height: 50px; width: 300px; top: 85px; left: 300px; background-color: darkgray; border-radius: 10px; text-align: center; padding-top: 20px; border: black; border-width: 5px; } </style> </head> <body> <div id="container"> <div id="header"> <div id="header"> <p>From the <a href="/journal">Journals</a> of <a href="/">Tarn Barford</a></p> <h1> Swipe Keyboard </h1> <p> Apr 06, 2014 </p> </div> </div> <div id="post_content"> <html><body><p>When I first tried a <a href="http://www.swype.com/">Swype</a> keyboard I was impressed how effective it was. Even though I don't use the feature on my phone I was interested in how it could be built, so I <a href="https://github.com/tarnacious/swipe-keyboard">implemented this otherwise useless swipe-able keyboard</a> below. It probably doesn't work on mobile devices, but works on modern browsers with mouse pointers (although I've only really tried Chrome and Firefox).</p> <div id="swipe"> <canvas height="300px" id="swipe-canvas" width="900px"></canvas> <div id="swipe-results"></div> <div style="clear: both"></div> <h2 id="swipe-loading">Loading<noscript>Javascript is Required</noscript></h2> </div> <p>I initially tried to solve this using the technique Peter Norvig famously uses in his <a href="http://norvig.com/spell-correct.html]">spell checker</a>. He takes a sequence of characters and generates a set of word candidates by adding, removing and swapping characters in the original sequence, the generated candidates are removed if they are not found a dictionary. This can work but to be effective too many combinations need to be generated.</p> <p>If the dictionary is indexed into a <a href="http://en.wikipedia.org/wiki/Trie">trie</a> the number of combinations generated can be reduced significantly by traversing the trie and only generating valid letter combinations. This is a pretty bare implementation of that, it requires: </p> <ul> <li>The first and last characters of the initial sequence are used </li> <li>Intermediate characters in the initial sequence can be repeated or discarded </li> <li>No characters are added or swapped</li> </ul> <p>Basically, if you swipe through all the characters in a word in order, then the word will be found if it is in the index regardless how many characters are swiped in between. It is surprisingly quick and effective.</p> <p>This implementation uses <a href="https://raw.github.com/first20hours/google-10000-english/master/google-10000-english.txt">these 10000 words</a>, I intended to use digital books but never got around to it as these words demonstrate the concept well enough.</p> <p>This is the first thing I've written in <a href="https://github.com/clojure/clojurescript">ClojureScript</a> or <a href="https://github.com/clojure/clojurescript">Clojure</a>, so my code my vary from non-idiomatic to shamblolic. I initially used a <a href="http://clojuredocs.org/clojure_core/clojure.zip/zipper">zipper</a> to build the trie with immutable data structures, but found the indexing took to long with my zipper implementation so I <a href="https://github.com/tarnacious/swipe-keyboard/commit/6edd7b26e78121fbe8586b3f0ef54ca8277d9e32">switched to using native Javascript maps</a>.</p> <p>I found that <a href="https://github.com/clojure/core.async">core.async</a> library is really awesome, the <a href="http://docs.closure-library.googlecode.com/git/index.html">Google closure library</a> and <a href="https://developers.google.com/closure/compiler/">compiler</a> integration with <a href="http://leiningen.org/">Leiningen</a> the <a href="https://github.com/emezeske/lein-cljsbuild">cljsbuild plug-in</a> to be impressive. My main pains were the slow JVM start-up time, the advanced closure compiler build of the web worker script fails silently when run (but the main script works fine when compiled with the advanced compiler), and at times I felt some compile time type checking would be nice.</p> <p>I would like to extend this experiment to index the word occurrence counts and proceeding word counts in original text and rank the found words as most likely. Support casing, umlauts, special characters, spelling correction and compound words in the indexing and lookup. I think a live lookup while swiping would also be possible.</p> <p>Overall this was fun, turned out OK I think, and was a great learning experience.</p></body></html> </div> <div id="comments"> </div> </div> <div id="footer"> <p>&nbsp;</p> <p>Questions, comments, suggestions? Email me, <a href="mailto:tarn@tarnbarford.net">tarn@tarnbarford.net</a> (<a href="/pgp.txt">public key</a>)</p> <p>&nbsp;</p> </div> <script src="swipe.js" type="text/javascript"></script> </body> </html>

Permalink

AI Is a Tool. Using It Well Takes Skill.

In his October 11, 2026 article, "'AI' is Not a Tool," Rich Hickey argues that LLMs are automation rather than tools because they cannot be wielded. I agree that outsourcing work can cost us opportunities to learn. But his conclusion does not follow. Even by his definition, an LLM can be something a human wields. And using it well takes skill, just as using other tools does.

Picking up a saw does not guarantee a straight cut. Holding a chisel does not make you a sculptor. Tools can amplify poor judgment as readily as good judgment. Their usefulness depends on the operator's ability to direct them, interpret feedback, recognize mistakes, and make corrections. Those requirements also apply to LLMs.

One of my colleagues used to say, "AI is a mirror." I find that insightful: it reflects who you are, including the curiosity, judgment, and habits you bring to it. The questions you ask and the standards you apply to its answers matter.

An LLM's unpredictability is a meaningful limitation. But predictability, reliability, simplicity, and bounded function are qualities by which we evaluate tools, not prerequisites for membership in the category. A tool does not stop being a tool because it is difficult to control or easy to misuse.

Hickey's distinction between tools and automation also creates a false division. Automation can be a feature of a tool. A compiler performs substantial work without the programmer continuously directing each operation. A spreadsheet recalculates results without someone manually carrying out every calculation. We still wield these systems by specifying inputs, examining results, and deciding what to do next. Why should generating text automatically disqualify an instrument from being a tool when translating code or calculating numbers automatically does not?

The claim that LLM use lacks intention, feedback, direction, and expertise is especially difficult to defend. A programmer can ask an LLM to explain unfamiliar code, challenge that explanation, inspect the source, test a proposed change, reject it, and refine the request. That is a feedback loop directed by a human pursuing a purpose. Doing it well requires knowing what to ask, what evidence matters, and when the answer is wrong. Expertise can make the instrument more useful.

Hickey's concern about reading is valid for a particular use: accepting a summary in place of reading a paper can sacrifice much of what reading teaches. But someone can also read the paper and use an LLM to question their interpretation, explore an objection, or identify something to examine more closely. The possibility of avoiding intellectual work does not establish that every use avoids it. The relevant question is what the person actually does.

What disappoints me the most is the reliance on the AI industry's anthropomorphic vocabulary: "go rogue," "escape containment," "conspire." Repeating those expressions risks accepting the framing that makes these systems sound like independent beings. Software can produce unexpected outputs, fail to follow instructions, or cause harm when given access to consequential actions. Those are real engineering problems. Calling them "conspiracy" adds a suggestion of human motives without establishing any.

I would not claim that an LLM always does exactly what a human tells it to do. It plainly can fail. But failure to obey perfectly does not make it cease to be a tool, any more than successful use proves that its operator has surrendered their judgment.

The useful distinction is between wielding an instrument with skill and delegating work without adequate understanding or oversight. That distinction applies to AI, and it gives us something more practical than declaring that an entire class of software cannot be a tool.

Permalink

Clojure OSS update

It's been a while since I worked on my Clojure libraries, so I would like to give a little update on that. Two of them got a new release, and one got a debut release on Clojars.

clj-pnm [1]

Apart from the minor bug fixes and Clojure(Script) dependency update, the most exciting thing is that the Clojure dialect support matrix has grown. clj-pnm was built from the start as a cross-dialect library supporting both Clojure and ClojureScript. It is lightweight enough to expand that.

For the Clojure side, the library is compatible with Babashka, as it can be called from Babashka code and can be used as a Babashka dependency.

On the ClojureScript side, clj-pnm can target NodeJS and the browser as before. Switching from figwheel to shadow-cljs for the testing framework was integral in ensuring that. There is a special test runner for testing Squint compatibility, but with no code changes, this lightweight dialect of ClojureScript is also added to the compatibility matrix.

There are tests for each of these in the library repository[2].

cljs-map-pins [3]

cljs-map-pins got a maintenance release, so no new features yet. But this post can serve as a reminder of how fun it is to use this library (at least for me :-D). I love visualizing things on a map, and this library is made just for that. Two examples I like to refer to, albeit they are made with a much older version of the library, are The Warriors path through New York City[4] and Big Wayne tribute[5].

See the repository[6] for more details.

defns [7]

defns is a proof-of-concept library that provides a way for defining functions with type signatures for which :pre and :post conditions are generated. This is an experimental idea and should not be used for anything serious, but I've seen some interest in it and decided to release it to Clojars. The fun part was writing the library clj-kondo configuration and custom hooks for the macros. When using defns as a dependency, make sure to import the clj-kondo configuration for the linting to work.

See the repository[8] for more details.

Enjoy!

[1] clj-pnm

[2] clj-pnm repository

[3] clj-map-pins

[4] The Warriors path through New York City

[5] Big Wayne tribute

[6] clj-map-pins repository

[7] defns

[8] defns repository

Permalink

Measuring agent workflows on public benchmarks

An agent that books invoices, answers questions over a database or fills in spreadsheets is more than a language model. It is a workflow: the model, the instructions it is given, the tools it may use and the steps it takes. Every part of it can be changed, and every change has a price. Measuring a workflow on a fixed set of cases is the statistical counterpart of a regression test, and it is what makes a change safe to deploy. This article shows how Dvergr, our open-source system for running and measuring agents, measures such workflows, what it found when we measured ten models on two public benchmarks, and why the most useful thing a measurement finds is often not the best model but a better workflow.

The question behind a leaderboard

Suppose a firm wants an agent to answer its staff’s questions about the company database. It has to choose a model, write the instructions, and decide what the agent may do: write one query and stop, or look at the data first, try a query, read the error and try again. A public leaderboard answers a narrower question, which model scored highest on someone else’s cases, with someone else’s instructions, on one run.

Dvergr answers the firm’s question directly. It takes a set of cases, each a task with the answer a person recorded for it, and a set of candidates, each a model together with its instructions and tools. It runs every candidate on every case and reports, for each candidate:

  • how often it is right, with a range that says how much that number could move on another set of cases of the same kind;
  • where it fails, check by check;
  • what a correct answer costs at the model’s list price;
  • how long it takes.

A firm that deploys an agent needs what software teams get from regression tests: a fixed set of cases with recorded answers, run again on every change. An agent’s output varies from run to run, and a provider can change the model behind a name it keeps, so one passing run says little. The result of a run is therefore a rate with an interval rather than pass or fail, and a change is judged by comparing it with the previous version on the cases where the two disagree.

We tried this on two public benchmarks, where anyone can check the cases and the answers. SpreadsheetBench asks for changes to Excel workbooks. BIRD asks questions in plain language about real databases, to be answered with a query.

The models come in families, named here from cheapest to most expensive: OpenAI’s Luna, Sol and Astra, in generations 5.6 and 6 (6.1 for Sol); Anthropic’s Claude Haiku, Sonnet and Opus 5.5; and two models with published weights, GLM and DeepSeek.

Results at a glance

BIRD, 100 questions, SQL SpreadsheetBench, 59 tasks 406080100 406080100 $0.001$0.01$0.1 $0.001$0.01$0.1$1 cost per correct answer (log scale) cost per passing task (log scale) % correct Luna 5.6HaikuDeepSeekGLMLuna 6Sol 6.1Astra 6 Luna 5.6Sol 5.6OpusHaikuSonnetLuna 6Sol 6.1Astra 6 GPT-5.6 GPT-6 Claude 5.5 open weights (Fireworks) Correctness with its 95 % interval against cost per correct answer, at list price, measured 2026-10-01 to 2026-10-10. BIRD: seven models writing SQL for SQLite on the same 100 questions. SpreadsheetBench: eight models on the same 59 tasks. Overlapping intervals are ties. On BIRD, GPT-6.1 Sol and GPT-6 Astra score highest, the first models we measured to rise above the rest, though their intervals still overlap the others'; on SpreadsheetBench the models tie, at costs that differ by more than a hundredfold.

Each dot is one model. Its bar shows how far the score could move if we had picked a different 100 questions of the same kind (59 tasks on SpreadsheetBench), the way a poll of 100 people would come out differently with another 100. Each model answered each question once, and the bar follows from that count alone, which is why it is nearly the same length for every model: with 100 questions and scores around two thirds, a score can move by about nine points either way. Where two bars overlap, the measurement cannot tell the two models apart, and the choice between them comes down to cost and time, on the horizontal axis.

Because every model answered the same questions, two models can also be compared more sharply. Most questions do not help, because both models got them right or both got them wrong. What decides is the questions where they differ. If model A was right and B wrong on twelve questions, and the reverse happened on only two, A is better even though their bars overlap; six against five says nothing. We give these counts with a p-value, the chance that two equally good models would split their differences at least this unevenly, and below 0.05 we treat a difference as real. Dvergr’s report gives the bars; the counts and p-values here we computed from the per-question results it records.

The tables also give the median time per case. Time depends on how busy the provider was that day, so it compares candidates run together better than candidates run on different days.

How a measurement runs

benchmark cases one row per case + recorded outcome certify ungradable cases excluded, with reasons experiment room every candidate on every case report intervals, failures, cost and time fork A × case 1 fork B × case 1 fork A × case 2 … checker verdict, or fault to run again A measurement. Certified cases enter an experiment room; each attempt runs in its own fork and is graded there; only the verdict is kept.

A benchmark enters Dvergr as a case pack: the cases, their recorded answers, and a checker that decides whether an answer is right. Before any model runs, the pack is certified. Certification lists every case that cannot grade an answer and says why: an id that is missing or appears twice, an empty answer, the same inputs recorded with different answers, or a recorded answer the checker itself does not accept. Those cases are set aside. On SpreadsheetBench, certification kept 390 of the 400 tasks without spending any model tokens.

The measurement itself takes place in a room, Dvergr’s unit of shared state: its files, its database and its history. For each candidate and case, Dvergr makes a fork of the room, a private copy that costs almost nothing to make because it shares everything until something changes. The candidate works in its fork: the files and the workbook it changes belong to that copy, and a database it may only read is reached for each query through a fresh read-only connection or an unchangeable snapshot, so nothing it does reaches the original data or the candidates working beside it. The checker grades the attempt’s result (the files in the fork, the query it submitted, or its edits replayed on a fresh copy of the workbook), the verdict is recorded in the room, and the fork is thrown away. Dvergr’s own share of each case, the fork included, is a fraction of a second, against the ten to seventy seconds a model takes to answer.

The statistics depend on these forks. Because every candidate starts from the same state, two candidates’ answers to a case can be compared directly, which is what the paired comparison needs. Because no attempt can change what another sees, a candidate is graded on its own work. And because a fork shares everything that has not changed, running every candidate on every case stays cheap. This follows from how Dvergr keeps state: the room’s files and its database are forked with copy-on-write branches across git and Datahike, so a fork records only what it changes and never writes to the original.

Four rules keep the numbers honest:

  • The same cases for everyone. Every candidate answers every case, which is what makes the paired comparison possible.
  • A broken connection is not a wrong answer. When the provider is down or a request times out, the case is run again. A model that answers wrongly, or gives no answer, gets its verdict.
  • List price. Models are billed per token, a piece of a word, read or written. Cost is what the same tokens would cost through the provider’s public price list, whether a subscription or the firm’s own key paid for them, with text the provider has already seen recently (cached input) at its lower rate.
  • A changed setup is a new experiment. The cases, candidates, instructions, checker and software versions together define an experiment. An interrupted run resumes where it stopped, but if any of them has changed, running it again starts a new experiment instead of mixing two.

Does the agent need to explore?

The candidates above are models inside Dvergr’s own workflows. On BIRD, the model may run queries in SQL, the standard database query language, see their results or errors, and submit an answer when it is satisfied, within twenty turns. On spreadsheets it may read cells, write values and formulas, see what they compute, and submit.

A firm should know how much of a score is the model and how much is the workflow around it. An agent that explores uses more tokens than one that answers at once, and it is worth knowing what those tokens buy. So we also ran each benchmark’s own reference setup, with the same model on the same cases. These runs are not Dvergr workflows: they use each benchmark’s published code to build the prompts and grade the answers, and Dvergr only connects them to the model, so that both setups reach it the same way. The scripts, with the steps to reproduce each number below, are in the repository under benchmarks/reference/.

On BIRD, one instruction did most of the work

BIRD’s reference setup is a single prompt: the list of the database’s tables and columns (its schema), the question and a hint, and a request to write the SQL query. The model answers once, without seeing any data. We ran it as published, then with one line added, return exactly the columns the question asks for. Graded with BIRD’s own evaluation script, on the same 100 questions:

WorkflowGPT-5.6 LunaClaude Haiku 5.5Tokens per question (Luna)
BIRD’s prompt, one reply51531,200
the same, plus the line about columns61641,200
Dvergr: run queries, see results and errors, then submit65698,200

Two questions show what each part contributes. Question 781 asks for the heights of the heroes whose eye colours are amber. With BIRD’s prompt, Luna returned each hero’s name beside the height. The heights were right, but BIRD compares the returned table as a whole, and a table with an extra column is wrong. With the line about columns it returned the heights alone and passed. Of the fifteen questions Luna answered on our workflow and missed with BIRD’s prompt, seven failed for no other reason than such an extra column, and the one line is worth ten points for both models, a difference too large for chance (p = 0.013 and 0.003).

Question 817 asks for the race of the blue-haired male superhero, and its hint says the colour is written 'blue' and the gender 'male'. In the database they are written 'Blue' and 'Male', and SQLite compares text exactly. The one-reply query followed the hint and found nothing. In Dvergr’s workflow Luna wrote a query that ignores capitals, ran it, saw three heroes, and submitted the query for their races. That is the kind of question where looking at the data pays.

Exploring the data adds another four or five points. With 100 questions that could be chance (p = 0.34 and 0.30), and it costs seven times the tokens. For these questions the cheapest good workflow is a single reply with the right instruction, at about a third of the cost per correct answer. The agent that explores scores higher, but 100 questions cannot show that the gain is real, and it costs more.

We had written that line into our own workflow from the start, because we knew how BIRD grades. Until we measured it, we did not know it was most of what our workflow added.

On spreadsheets, the recalculating program decided the score

On SpreadsheetBench the scores depended less on the model than on how the answers were checked.

The benchmark’s reference setup shows the model the instruction and the first five rows of each sheet, and asks for a short Python program that edits the workbook. Its multi-round version runs the program and shows the model the output or error, for up to five turns. We ran both versions with GPT-5.6 Luna on our 59 tasks, each program in a Docker container that can see only its own input file, and graded them with the benchmark’s own comparison code. Dvergr’s workflow writes no Python: its agent edits the workbook directly, through a spreadsheet engine in Dvergr’s sandbox.

SpreadsheetBench’s grader reads each answer cell’s stored value, the result a spreadsheet program saved with the formula, and never computes a formula itself. A file written from Python stores formulas without values, so the benchmark’s instructions add a step before grading: open every workbook in LibreOffice (on Linux and macOS) or Excel (on Windows) and let it recalculate. We followed the instructions, using LibreOffice on Linux.

One task shows what happened. Task 56563 has a column of monthly amounts, one of which is an error value, and asks for a formula that adds them up anyway. Both workflows wrote the same correct formula into the answer cell, =AGGREGATE(9,6,C2:C13), which sums the range and skips cells holding errors. Its value is 77,772.6. We graded that one cell two ways:

Graded byWhat it found in the cellVerdict
the grader after LibreOffice recalculated, as the benchmark’s instructions say#NAME?: LibreOffice recognises functions added to Excel since 2010, such as AGGREGATE, only when the file marks them with a prefix (_xlfn.AGGREGATE), and neither workflow wrote itwrong
Dvergr’s spreadsheet engine77,772.6, as does LibreOffice once the prefix is writtenright

The same answer was wrong or right, depending on which program computed it. Across the 59 tasks:

WorkflowRecalculated by LibreOffice (the benchmark’s instructions)Tokens per task (median)Median time
SpreadsheetBench, one reply with code342,20015 s
SpreadsheetBench, up to five turns with execution414,30017 s
Dvergr: read and write cells, see what they compute, submit41 as written, 48 as Excel stores them5,70024 s

Graded the benchmark’s way, after LibreOffice’s recalculation, the multi-round workflow scores 41, and Dvergr’s workbooks as written also score 41; each is right where the other is wrong on eight tasks, so on that footing they tie. Written with Excel’s prefix, Dvergr’s workbooks score 48. Graded by Rechentafel, the open-source spreadsheet engine Dvergr’s agent works in, they score 51. Grading with our own engine would prove little if the engine had not been checked against Excel. The benchmark’s recorded answers are the values Excel saved, and for 390 of the 400 tasks Rechentafel recalculates each recorded answer to exactly that value, which is how certification works on this benchmark. The workflows differ less than the graders do. A firm measuring a spreadsheet workflow should grade with the program its people use, or with one checked against it.

Improving a workflow by measuring it

The line about columns is the kind of change a measurement should find, not one an author should have to know in advance. Dvergr is built so that variants of a workflow can be written and measured like any other candidate.

A workflow in Dvergr lives in its room as ordinary files: the task and its instructions, the checker, the cases, and any code the agent runs in its sandbox. Because they are files in the room, an agent working there can read them and write a variant: a sentence added to the instructions, a look at the data before the answer, a different model. Each variant is a candidate like any other, measured in its own forks on a set of tuning cases. A variant that does better is then measured on cases it has not seen, so that one tuned to the quirks of one set of questions does not pass for an improvement. A variant’s checker is trusted only after the host’s administrator promotes it, through an administrator’s connection. The promotion is recorded outside the room’s files, where no agent can write, so an agent cannot raise the trust of its own checker.

We took one step of this loop by hand on our Datalog workflow for BIRD. Datalog is the query language of Datahike, the database Dvergr keeps its own state in, and the workflow answers BIRD’s questions with it instead of SQL. After reading the failures of one run we added two sentences to its description of the language, and measured the change on another 100 questions that run had not seen:

WorkflowCorrect (of 100)Input tokens per question
SQL on SQLite598,880
Datalog, revised description6314,042
Datalog, previous description5816,684

The revision answers five more questions with less input, but at 100 questions that could be chance (p = 0.23). That is the honest result of one step of tuning: a promising change, to be confirmed on more cases before it is adopted. Today a person or an agent proposes each variant and Dvergr measures it. Letting an agent in the room run the whole loop, propose, measure and hand the winner to the owner, is what we are building next. The rule for adopting a variant is the one a team applies to a code change: merge it when the cases show it is not worse.

SpreadsheetBench in detail

This section and the next give the full results behind the chart, model by model. These numbers use our workflow and our spreadsheet engine, on 59 of the 390 certified tasks, so they are not comparable to the public leaderboard, where agents work with other tools.

ModelTasksPassed95 % intervalCost per passMedian time
GPT-5.6 Luna595176–93 %$0.002624 s
GPT-5.6 Sol595278–95 %$0.03327 s
Claude Opus 5.5 (Claude Code subscription)595278–95 %$0.06217 s
Claude Haiku 5.5 (Claude Code subscription)594768–88 %$0.004622 s
Claude Sonnet 5.5 (Claude Code subscription)594666–87 %$0.3319 s
GPT-6 Luna594870–90 %$0.000810 s
GPT-6.1 Sol595585–98 %$0.0109 s
GPT-6 Astra595585–98 %$0.0509 s

Paired with GPT-5.6 Luna, none of the GPT-5.6 and Claude models is measurably different. Opus is right where Luna is wrong six times and the reverse five times; Sol three and two; Haiku four and eight; Sonnet three and nine (the closest, p = 0.15). What differs is the cost. Per passing task Haiku costs about twice as much as Luna, Sol thirteen times, Opus twenty-four times and Sonnet over a hundred times, because Sonnet sometimes wrote very long answers: its slowest tenth of tasks took over nine minutes.

The GPT-6 models, run on the same tasks a few days later, are the first to move the top. GPT-6.1 Sol and GPT-6 Astra pass 55 each and disagree on only four tasks, two each way, so Astra costs five times as much as Sol for the same answers. Sol 6.1 is measurably better than GPT-6 Luna (eight tasks to one, p = 0.039) but not than GPT-5.6 Luna (five to one, p = 0.22). They were also about three times faster, partly because of the models and partly because of the day.

Every task but one was solved by at least one model, so most failures belong to a model, not to the task.

BIRD in detail

BIRD’s questions cover 11 databases. We can answer each question three ways: SQL on SQLite, the database BIRD ships with; SQL on Datahike, through its PostgreSQL-compatible layer pg-datahike; and Datalog on Datahike. Even before any model runs, 1372 of BIRD’s 1534 reference queries return exactly SQLite’s rows on pg-datahike.

With GPT-5.6 Luna on 100 fresh questions held out from tuning:

EngineCorrect (of 100)95 % intervalCost per correct answerInput tokens per questionMedian / slowest tenth
SQL on SQLite6555–74 %$0.00237,83910 s / 23 s
SQL on Datahike (pg-datahike)6555–74 %$0.002610,13610 s / 41 s
Datalog on Datahike6454–73 %$0.004415,06113 s / 42 s

The three engines answer equally well: each pair disagrees on seven to ten questions, split almost evenly. They differ in cost. In Datalog the model looks at the schema more before it answers, so a correct answer costs about twice as much as in SQL. Which query language to give an agent is a cost decision, and the measurement says how large it is.

Seven models on the same 100 questions:

ModelSQL on SQLite (of 100)Datalog (of 100)Cost per correct answer, SQL / DatalogMedian time, SQL
GPT-5.6 Luna6564$0.0023 / $0.004410 s
Claude Haiku 5.56968$0.0026 / $0.00479 s
DeepSeek V4.1 Flash (open weights)6664$0.0074 / $0.01958 s
GLM 5.3 Flash (open weights)6458$0.0042 / $0.01066 s
GPT-6 Luna6860$0.0009 / $0.00145 s
GPT-6.1 Sol7471$0.015 / $0.0198 s
GPT-6 Astra7570$0.067 / $0.0916 s

Among the first four models, no two are measurably different. Any two disagree on 10 to 18 questions; the most lopsided pair (GLM’s Datalog against Haiku’s, p = 0.013) is no more lopsided than chance alone produces somewhere among 28 comparisons.

GPT-6.1 Sol and GPT-6 Astra answer 74 and 75 in SQL, against 65 for GPT-5.6 Luna (eleven questions to two and twelve to two), and tie with each other. Counting all the comparisons we made, even that is not quite conclusive, but it is the first gain we have seen, and it comes within about five questions of the 80 that BIRD’s recorded answers allow (see below). Again Astra costs about five times as much as Sol per correct answer.

The two open-weight models (models anyone may download and run on their own machines), served by Fireworks, answer as many questions as GPT-5.6 Luna and Claude Haiku, at two to three times the cost per correct answer, because they used more tokens and none of their input was served from a cache. For a firm whose data may not leave its own machines, that is the useful result: a model it can run itself answers these questions as well, and its cost is then the firm’s hardware, not a list price.

What the benchmarks get wrong

A benchmark is only as good as its recorded answers and its grader, and both benchmarks have faults that change scores.

On BIRD, twenty of the 100 questions were answered correctly by none of the first four models in either language. On most of them the models agree with each other and not with the recorded answer:

Question (BIRD id)Recorded answerWhat the models answered
atoms of molecule TR346 and its bond types (309)a query for molecule TR000molecule TR346
the German type of a card (482)the English typethe German type
patients with abnormal CRP and no recorded data (1256)208, the number of lab rows25 patients; the only answer graded right, GPT-6 Luna’s in Datalog, repeated the mistake
expenses of the budget with the lowest remaining (1365)one row, cut off by LIMIT 1both expenses of that budget
difference of two percentages (1458)12.120.1212, following the hint’s formula, which has no ×100
eye colours of Marvel heroes by popularity (728)colour, count and a rank columncolour and count

We count about ten questions whose recorded answer is wrong, four more whose hint contradicts it, and the rest a matter of which columns or which wording. On these 100 questions the best attainable score is about 80, not 100. Paired comparisons survive this, since every candidate loses the same questions, but the differences shrink, and a model that answers the question as asked gets no credit for it. Our grader agrees with BIRD’s own on all 200 SQL answers we checked, so the scores above are the benchmark’s own.

On SpreadsheetBench:

  • The published evaluation script cannot score the published tasks. On the repository’s main branch it compares each task’s input workbook with the recorded answer (the line that reads the model’s output is commented out), and its file names are those of an earlier release. Every published score on the 400 verified tasks comes from a modified script that is not part of the benchmark.
  • Formulas are graded as empty unless the workbook is recalculated first, as described above. The benchmark’s instructions include that step, but the program that recalculates changes the score, and the step also recalculates the recorded answers, so after it they are that program’s results too.
  • One task can never pass. The answer position of task 45944 contains spaces, and the checker fails on it whatever the answer.
  • Some recorded answers are wrong or misfiled. Task 118-50’s answer misses a pair its own rule finds in the input, and the two strongest models were graded wrong for finding it. Task 42930’s answer file carries another task’s number.

Certification sets aside ten of the 400 tasks for reasons like these before any model runs.

Checking our own measurements

The setup that measures is code too, and it needs the same tests as the workflows it measures. Before publishing, four AI review agents read our benchmark code, documentation and stored results, each with one question: whether every benchmark runs the same way, whether the forks isolate each attempt, whether the documentation matches the code, and whether the numbers are right. They found nine problems, among them a provider outage counted as the model’s failure and cached input billed at its full price. Each fix came with a test that fails without it, and the affected runs were repeated or withdrawn.

What these numbers cannot tell you

Each model answered each case once, so a few answers could change on another run; the intervals say how much that matters. The numbers describe these cases with these workflows, and a different prompt, tool or model version can change them, which is why an experiment records all three. A tie at 59 tasks says what 59 tasks can separate, not that two models are equal. As with tests, coverage sets what can be found: with 100 cases, a regression of a few points can go unnoticed. And a public benchmark is not your workflow: it shows that the method works where others can check it, not how a model will do on your cases.

Where the method comes from

Seen this way, a workflow is a probabilistic program: a program whose output is drawn from a distribution, and measuring it is inference about how often that output is right. The Jeffreys interval in our reports is a Bayesian estimate of that rate. The view comes from probabilistic programming, which Christian Weilbach worked on in his doctoral research in Frank Wood’s group, with the Anglican language and the Daphne compiler (TMLR 2025). It continues in Foerster, our library for inference over forkable worlds, where alternatives are forks of the same state, as candidates are here. Dvergr’s measurements do not use Foerster yet. Two steps it would allow are stopping a comparison as soon as the evidence decides it and spending more cases where two candidates are close.

Try it on your own cases

The benchmark that matters for a firm is its own history: past bookings, coded invoices or resolved tickets, each with the outcome a person recorded. Dvergr is open source and runs on your own machine:

  1. Install and connect. Clone the repository (it needs the Clojure CLI and babashka), and add bin/dvergr-mcp --profile bench as an MCP server in Claude Code or Codex. The models it compares can be used through a Claude Code or Codex subscription or any API key; the README lists the providers, the setup and where Dvergr keeps its state.
  2. Turn your history into a case pack. Export a table of past cases as CSV, as Excel or DATEV write it, and name its columns: the id, the inputs, and the outcome with a rule for checking it (exact, a number within a tolerance, a date, a set). Certification reports which cases can grade an answer and why the others cannot, which is a first result in itself.
  3. Run the comparison. Ask your assistant to run catalog_benchmark with the models you want compared, or, from a shell, turn the CSV into a pack with clojure -M -m dvergr.catalog.casepack-cli and run clojure -M -m dvergr.catalog.room-run <pack> --models … --cases 30. The report gives each model’s success with its interval, where it fails, the cost per correct result and the time.

Running benchmarks and case packs in the documentation give the details. A following article applies this to a year of bank bookings exported from DATEV.

If you would rather we built the benchmark with you, we run fixed-fee pilots: six weeks on one workflow of yours, ending with the benchmark and a report on which model, instructions and steps to run, at what cost. Send us a note.

Method

ModelsGPT-5.6 Luna and Sol, GPT-6 Luna, GPT-6.1 Sol and GPT-6 Astra (Codex subscription); Claude Haiku 5.5, Sonnet 5.5 and Opus 5.5 (Claude Code subscription, each pinned to its exact version); GLM 5.3 Flash and DeepSeek V4.1 Flash (Fireworks). Each answers every case once.
CasesBIRD dev, the held-out third of each database’s questions, fresh samples of 100; SpreadsheetBench verified, held-out tasks among the 390 certified
GradingBIRD: the returned rows equal the recorded query’s rows; SpreadsheetBench: the answer cells equal the recorded workbook’s values
StatisticsJeffreys 95 % intervals for success rates, as Dvergr’s report gives them; exact McNemar test on the cases two candidates disagree on, computed from the recorded per-case verdicts
Costlist price per million tokens on the day of measurement, cached input at the cache rate
Reference setupsBIRD’s prompt from its published gpt_request.py and its evaluation script; SpreadsheetBench’s single- and multi-round drivers and its comparison code, the generated programs run in a Docker container; both reach the model through Dvergr’s model connection: benchmarks/reference/
Code and dataBIRD and SpreadsheetBench are public; Dvergr and both benchmark adapters are open source: github.com/replikativ/dvergr

Permalink

Week Notes 2026.41

Babashka tasks for deploying Datomic Cloud ions; making AWS API Gateway requests with SigV4 authentication.

Permalink

neorepl: A Terminal REPL for Clojure (and Friends)

For more than a decade the Clojure REPL most people saw in a terminal was REPLy, the one behind lein repl. It served us well, but it’s showing its age, and I kept wishing for a terminal REPL that felt like CIDER, without the Emacs part. So I finally built one. neorepl 0.1 is out today!

Why another REPL?

There’s no shortage of Clojure REPLs, so it’s fair to ask. Here’s what I was after:

  • nREPL all the way down. neorepl only speaks nREPL, so it works with anything that runs an nREPL server: Clojure, babashka, shadow-cljs, Basilisp, ClojureCLR and so on. The smarts live on the server. When cider-nrepl is there neorepl uses it, the same way CIDER does, and when it isn’t it falls back to nREPL’s built-in ops, and then to evaluating a bit of code.
  • A REPL that’s pleasant to use: a proper line editor, highlighting, completion, arglists as you type, pretty printing, history per project and a Ctrl-C that actually interrupts the evaluation.
  • A CLI that’s just as good for scripts and coding agents. Every lookup the REPL can do is also a command, with structured output and exit codes that mean something.
  • Something small and modern. Clojure 1.10+, nREPL and JLine 4 are the only dependencies, and the same code runs on the JVM and on babashka. The whole thing is about 3,300 lines right now, and I’d like to keep it that way.

rebel-readline and bb repl --connect are both great, and I borrowed from them freely, but neither tries to be an nREPL-first tool with CIDER’s tooling, jack-in and a CLI for agents. That combination is the whole point of neorepl.

Update (2026-10-11): A few people asked how neorepl relates to the Clojure CLI REPL, which the Clojure team released a couple of weeks ago, and to bb repl --connect. (I’m a bit embarrassed to admit I hadn’t heard of the former until after I announced neorepl.) Funny enough, all three are nREPL clients under the hood, but they’re after pretty different things. In practical terms:

  • The Clojure CLI REPL runs only on the JVM and needs Clojure 1.12+. It ships its own nREPL server, and its docs and data inspector are ops of that server, so against any other server you can only evaluate code. It’s deps.edn only. Its editing is really nice, though - structural editing, evaluating the form at the cursor and a datafy-based inspector - and I’ll be borrowing some ideas from it. It doesn’t do completion yet.
  • bb repl --connect puts babashka’s console REPL in front of any nREPL server, with completion, arglists and docs, in very little code. It’s a great way to poke at a running server, but it won’t start one for you.
  • neorepl works with any nREPL server and gets the most out of the ones with cider-nrepl (or babashka’s take on it), without any middleware of its own. It starts a server for Leiningen, deps.edn, babashka and shadow-cljs projects, follows you into ClojureScript, and everything the REPL does is also a command with JSON output for scripts and agents. No structural editing yet, however.

So, if you want the fanciest line editing and you live in deps.edn land, give the Clojure CLI REPL a try. If you want CIDER in your terminal, for you and for your agents, that’s what neorepl is for. The neat part is that babashka’s server bundles CIDER’s inspector and test runner ops these days, so neorepl’s test runner (already on main) and the inspector that’s coming next work with babashka out of the box.

For humans

Here’s the REPL in action:

The neorepl REPL

A few things worth pointing out:

  • Enter only submits the input once its forms are complete, so pasting a whole function just works.
  • TAB completes, with the candidates’ types next to them, and the arglist of the function you’re calling shows up after the cursor.
  • Values get pretty printed on the server and highlighted on the client.
  • Commands start with a comma, the CIDER way: ,doc, ,source, ,apropos, ,macroexpand, ,pst, ,reload, ,edit (which opens a definition in your $EDITOR) and ,help for the rest. The Clojure reader sees a comma as whitespace, so they can’t clash with real code.
  • Ctrl-C interrupts what’s running, and code that reads *in* gets its input, something REPLy never quite got right.

Without a server to connect to, neorepl starts one for the project, the way CIDER’s jack-in does, with cider-nrepl on the classpath, and stops it when you exit. It knows Leiningen, the Clojure CLI, babashka and shadow-cljs projects.

ClojureScript works too. Start a ClojureScript REPL in the session (through shadow-cljs or piggieback, which jack-in adds for you) and neorepl follows along: evaluation, completion, docs and the rest switch to ClojureScript, and :cljs/quit takes you back.

For agents

Coding agents are surprisingly good at Clojure, as long as they have a REPL to talk to. Most of them drive CLIs much better than anything else, so neorepl has a CLI that’s meant to be driven:

neorepl's CLI

  • neorepl eval evaluates code on the project’s server. Calls share a session, so *ns*, *1 and *e carry over from one call to the next, and --session keeps separate lines of work apart.
  • --format json (or edn) gives you the values, the output and the namespace as data.
  • --timeout interrupts the evaluation before giving up, so an infinite loop doesn’t take the server down with it, and --max-output keeps a stray (range) from eating the agent’s context.
  • neorepl check finds unbalanced parens, brackets and braces without a server. That’s the most common way for an agent (or a human) to break a Clojure file.
  • neorepl doc, source, apropos, complete, where, macroexpand and reload do what the REPL’s commands do.
  • neorepl server starts a server that stays up, for editors, agents and neorepl eval to share.

The exit codes are boring on purpose: 0 when all went well, 1 for an evaluation error, 3 when there’s no server to talk to and 124 for a timeout (like timeout(1)).

Playing nicely with nREPL

A new client is also a new way to break servers, or to be broken by them, and the nREPL world has quite a few servers these days. Fortunately I’ve been working on proof, a compatibility suite for nREPL, which made neorepl a good test subject.

proof checks both sides of the conversation. proof proxy sits between a client and a server and grades everything the client sends: every request has an id, need-input gets answered with stdin in the right session, and so on. proof serve is a small nREPL server that can behave like other servers in very specific ways - sending only the last value of the code like Basilisp and jank, dropping stderr, having no interrupt op, splitting output into one message per character, and so on. I ran neorepl’s REPL and CLI through both, against nREPL, babashka, Basilisp and jank, and through every quirk proof serve knows about.

neorepl came out of it in decent shape, but not unscathed:

  • Code read from stdin (neorepl eval -) that then read *in* itself crashed neorepl and left the evaluation waiting on the server for input that was never coming.
  • neorepl’s errors could leave the shell’s prompt dangling at the end of a line with servers that don’t end their error reports with a newline (hello, jank!).
  • neorepl eval exited with 1 when it lost the server, which is the status for code that failed.

All of those are fixed in 0.1. It also turned up a couple of things that are nREPL’s fault rather than neorepl’s: need-input and the end of an interrupted evaluation can go to the wrong connection when a session is used from more than one. Those will be fixed in nREPL itself.

It works the other way around too. proof now checks servers with the exact requests neorepl sends while starting its REPL and evaluating code, next to the ones CIDER, Calva, Conjure, vim-fireplace and REPLy send, so a server that breaks neorepl finds out from proof, not from a bug report. nREPL, babashka, Basilisp and jank all pass those checks today.

Making the neighbours better

Building neorepl turned up a few problems elsewhere, which is one of my favourite side effects of writing a new client. piggieback used to evaluate only the first form you sent it and silently drop the rest, and it loaded the ClojureScript compiler at startup even when nobody asked for a ClojureScript REPL, which added almost half a second to the startup of every server with ClojureScript on the classpath. The fixes for both (piggieback#162 and piggieback#161) will be in the next piggieback release. The Node.js REPL in ClojureScript itself doesn’t cope well with interrupted evaluations, and that one is next on my list.

What’s next

0.1 is the foundation. Here’s what I’d like to tackle next:

  • a test runner with proper reports
  • a full-screen inspector and stacktrace browser
  • an MCP server and a ready-made skill for coding agents
  • better Windows support

Try it

The easiest way to install neorepl is bbin:

$ bbin install io.github.nrepl/neorepl
$ cd my-project
$ neorepl

The README has the details.

It’s early days, so I’m sure you’ll find rough edges. Please file issues, and tell me what you’d like to see next. And if you’re one of the people who kept lein repl and REPLy going over the years - thank you! neorepl wouldn’t exist without you.

Keep hacking!

Permalink

Clojure 1.13.0-alpha9

Clojure 1.13.0-alpha9 is now available! Find download and usage information on the Downloads page.

  • Dropped :defaults directive - use cases better handled by others now

  • CLJ-2983 Removed :defaults tests

  • Faster transient propagation of metadata in into and update-vals

  • Revise impl and inline of meta via RT

  • CLJ-2978 Catch protocol extension of non-protocol methods

  • CLJ-2982 TransientArrayMap should expand to same threshold as PersistentArrayMap

  • Favor class files when reproducible build sets class and source timestamps the same

  • CLJ-2984 Set file timestamps to last git commit time

Try it out

Update your deps.edn :deps with:

org.clojure/clojure {:mvn/version "1.13.0-alpha9"}

Start a REPL with the Clojure CLI (any version) with:

clj -Sdeps '{:deps {org.clojure/clojure {:mvn/version "1.13.0-alpha9"}}}'

Permalink

Workflow Ergonomics with AI

How AI streamlined my digital life

Build your own web server

My road to a more productive digital life started when I asked Claude Code (CC) to build a web server for me. It&aposs called mdserve (short for markdown server), a full-stack web application in a single file, that has grown into the control panel for my whole desktop. Its generic core is on GitHub, for you to build your own on.

It started with wanting a more pleasant reading experience than what CC produces. Instead of reading a long answer inside CC itself, I tell it to write the answer to a markdown file and open it in the browser for me. CC will do that without any help. The problem mdserve solved was where the page turns up: I wanted it to always be in a known browser&aposs window, on the workspace I&aposm working in.

It has grown since then. A "personal agent", one that goes off and does things for you while you&aposre not there, isn&apost something I want. But I do like the convenience such agents promise, and this is how I get it. Anything I find myself doing a lot gets streamlined. Sometimes that&aposs a small ~/bin command, sometimes a button or page in mdserve, and sometimes just a rule CC follows for me, such as always backing up a file before changing it.

When I start mdserve from the command line, it opens several browser windows, each with many tabs. I have three areas of focus at the moment, so I work across three workspaces. The one step I haven&apost been able to automate is moving each window to its workspace, but that only takes a few seconds.

About the videos: they&aposre screen recordings, and the text in them is small. Open a panel, then go full screen before you press play. YouTube picks the picture quality from the size of the player, so a small player gets a blurry picture.

Watch: mdserve starting up

Why a browser and localhost

Why a web server and not a desktop application? Because everything talks HTTP. CC shows me a page with a single curl to mdserve, with no plugins or plumbing needed. mdserve in turn drives the browser with web requests too, through the Chrome DevTools Protocol. The server holds the state, so closing a tab loses nothing. And it isn&apost tied to one machine: CC sends documents to my tablet over the LAN with the same kind of request.

Everything lives on a tab

As you saw in the video, I start mdserve.sh from workspace 2. That&aposs the workspace I use for control, and for anything that isn&apost a programming project. The idea is that mdserve bootstraps the machine. After that, all the work I need to do is on an mdserve browser tab somewhere.

When I want to work on mdserve itself, I start a new CC session and ask it to read MDSERVE.md, where its documentation lives. If you haven&apost worked with CC, or a similar coding agent such as Codex CLI or Gemini CLI: maintenance documents like this are written by the AI itself, from the conversations you have with it at its prompt.

Watch: Improving mdserve with Claude Code

Browser tabs are just URLs. On startup mdserve reads them from an EDN file, and that file gets changed during my conversations with CC.

It&aposs interesting to see how tabs come to be there. There are three ways:

  1. Always wanted. These are often complete programs, such as the dashboards for my projects, or ordinary sites such as Gmail and Slack, and they come back every time mdserve starts.
  2. Asked for. I&aposve asked CC to "put it up". This is usually markdown documentation.
  3. Opened by another tab. A button on another tab was pressed, usually to take a closer look at something, such as a log.

Each tab also gets a coloured dot as its icon, so I can tell at a glance what kind of tab it is. Ordinary websites keep their own icons, but they&aposre just as much part of mdserve: remembered and reopened with the rest, and CC can put one up in the correct window as easily as a document. The idea is to keep it down to one browser window per workspace.

More features, and your own Windows Explorer

The last video showed "Refresh" for how much memory is in use, and "Remember Tabs", which saves the open tabs.

One rule Claude follows for me is to always make a backup before changing a markdown file. That makes a mess of backup files, which of course can be tidied up.

The next video shows the tidying up.

Watch: mdserve's Explorer tidying up backups

Imagine having your own programmable version of Windows Explorer per workspace. You just saw that in the last video! There&aposs always at least one of these tabs in each workspace. The video doesn&apost show it, but clicking a file opens a new tab that displays that type of file. Markdown files matter most, so they&aposre listed with the most recently changed first.

Many of mdserve&aposs buttons don&apost do the work themselves. They run a small command from my ~/bin directory, the same command I could type at a shell prompt, or that CC can run for me. The "Flotsam" button in the video runs archive-flotsam, a script that moves backup files and screenshots into a timestamped archive folder. Most of the commands behind buttons are just a few lines of bash. A few buttons, such as the memory "Refresh" and "Remember Tabs", are built into mdserve itself.

Fewer terminals

Working as a developer or a support person, you end up with terminals everywhere, some temporary and some running permanently, and they clutter your desktop. There are plenty of ways to tame them: tmux, tiling window managers such as i3, and on macOS, iTerm2.

The terminals that cluttered my desktop were mostly long-running processes: servers, build watchers and the like. Those now run in the background, each terminal having been replaced by a row of buttons ("Launch", "Stop" and "Log") on an mdserve dashboard, one at a time or a whole group at once. "Log" opens the output in a tab, which I close when I&aposm done. All I keep is one tabbed terminal window per workspace, for my CC sessions and the occasional one-off terminal session.

Use runbooks

That occasional one-off terminal session I mentioned is often a runbook, often on another machine.

Some jobs need me, rather than CC, to type the commands, because they need my password or a console. For those, CC writes a runbook: a plain file of shell commands, with comments saying why each step is there. I press Alt-n and the next step appears at my prompt, already typed, and nothing runs until I press Enter. No more copying commands out of the chat. The tool is called cmdq, and it&aposs a gist you can install in a minute.

Each step&aposs output is saved for CC to read back, and my place is saved too, so a job can span reboots. When the job is on another machine, CC copies the runbook there, and I log in and keep pressing Alt-n. Each runbook is a dated file kept with its output, so there&aposs a history of what ran on each machine.

At this stage I&aposve written the main points I wanted to make. Please read on if you are still curious...

Complex deployment

Buttons aren&apost only for small jobs. Our point-of-sale system (POSES) is four webapps (chat, till, prods and pos) that run on one production server, and deploying them is a job with several steps. It now lives in mdserve in a Deploy panel on the POSES dashboard tab, as five buttons in the order they&aposre used:

  1. Plan compares what&aposs live with the latest code (whether committed or not), app by app, and shows what a deploy would ship.
  2. Ship builds and uploads an uberjar for each changed app, stopping at the first failure.
  3. Release doesn&apost actually do anything! Instead, it prints the step for the human deployer to run on the server.
  4. Record checks that every app that was shipped is really live, and fails loudly if one isn&apost, which catches a step missed on the server. It then notes what went live, so the next Plan can work from that note instead of having to query the live server. It&aposs also the only way Plan knows which version of the shared library is in production, because the server doesn&apost say.
  5. Confirm runs the plan again, read-only, and should find nothing left to deploy. Its log ends: "Confirm done. If every app above shows &aposup-to-date&apos, the deploy is complete." It then restarts the development build watchers that Ship had to stop.

Each step has a coloured dot showing whether it has run and how it went, and a "Log" button for its output.

Watch: A complex deployment from mdserve

Life hacks

The same habit works away from the desk. Four examples, and how each one works.

Reading on the couch. When I say "put it up on the tab", CC adds a document to a reading list, which a second, deliberately tiny web server shows on my tablet. That server can only show the list and the documents on it. mdserve itself only listens on the machine&aposs own internal address, so nothing on the LAN can reach it. The terminal, editor and money pages simply don&apost exist on the tablet&aposs server, so nothing I add to mdserve later can leak onto the network by accident. A web page can go on the list too; it&aposs just a link to the real site.

The big screen. "Put it on the big screen" opens a page on the TV. Behind that phrase is bigscreen, a small command on my desktop: give it a web address and the page appears on the TV. CC runs it when I ask, and I can type it myself too. The TV is really just an HDMI screen for a Linux laptop. That beats a smart TV: the laptop is a normal computer, so it can run a web server and a fully controllable Chrome. A page on the TV is no use if you can&apost scroll it from the couch, so the TV runs its own little web server, tvremote, and my phone is the remote control. In reading mode it scrolls the page up and down, makes the text bigger or smaller, reloads it, and plays the page&aposs YouTube videos; the same remote also does Netflix. tvremote shows pages in a separate Chrome with its own profile, because Chrome won&apost let a program take control of your everyday one. It&aposs on GitHub if you have a spare laptop and a TV.

What makes tvremote work is the trick mdserve uses, stretched across two devices. In mdserve, the pages I click on and the browser being driven are one and the same: a button on a page asks mdserve, and mdserve drives that same browser through the Chrome DevTools Protocol. With tvremote there are two browsers. My phone&aposs browser shows the remote, but it has no power over the TV at all, because Chrome only accepts that kind of remote control from programs on its own machine. So each press goes to the web server on the laptop, which sits right next to the TV&aposs Chrome and drives it. A web page can&apost control another browser, but it can ask a web server that can. Any device on the LAN can be the remote, and bigscreen on my desktop is just one more of them.

Email and messages. When I need an email written, CC drafts it straight into Gmail and leaves it open on my screen: a new message, or a reply at the end of the correct thread. Slack messages work the same way. There&aposs no email integration behind this. Gmail and Slack are just tabs in mdserve&aposs browser, already logged in, and CC works inside them through the Chrome DevTools Protocol, the same remote control mdserve uses to open its windows.

Errands. My second favourite (behind the TV remote): I needed an official photo from one of several local photo shops. CC made me a one-page guide with the questions to ask and the spec, and the shops in order, each with a button that opened Google Maps to it. As I rode from shop to shop, the next one was always a tap away. This one couldn&apost be served from home: my web servers there reach only my own machine and my LAN, and out on the bike my phone was on mobile data. So CC published it on the web server I rent on the internet, the one that hosts this blog, as a temporary page under my own domain. That&aposs a single command for CC, and the page needs no login to read.

Financial flows

Money gets the same treatment. My mdserve pages only ever talk to the mdserve backend, which in turn makes the calls to Binance, holds the secret API keys, and signs each request, so a key never appears in a web page, and the browser never talks to the money.

The main flow:

  1. Selling crypto for local currency. Binance&aposs peer-to-peer (P2P) market shows who is offering what for my USDT, paid into my GCash account. My page picks the best offer I can actually fill, not just the cheapest one listed, skips people I&aposve blocked, and keeps track of GCash&aposs monthly limit. The counterparty in a P2P trade often asks for a payment QR code, so mine is on the page, ready to send.
  2. Knowing where the money is. One page shows every wallet, with a button to move funds between my Binance wallets.
  3. Withdrawals. Funds go out to Kinesis, an asset-backed digital currency platform. mdserve guards the send: the destination address is fixed on the server, so the page can only choose an amount; an address is checksum-verified before it&aposs ever used; the button needs two clicks; mdserve never retries; and after a send there&aposs a 30-minute cooling-off period, so nothing can go out twice by accident.

Every trade is saved locally, as an EDN file, so the history is mine and doesn&apost depend on Binance.

Try this tomorrow

You don&apost need mdserve to start. Each of these is something to say to your own coding agent, and each one stands alone.

  • "Don&apost give me long answers here. Write them to a markdown file and open it in my browser." This is where mdserve started, and it needs no server at all.
  • "Always back up a file before you change it." It&aposs one line in your agent&aposs rules file, and you never lose a version again.
  • "Before you build this, run falsifier on it." falsifier is a skill (a file of instructions your agent loads) that checks a spec, a request or code for contradictions, hidden rules and wrong assumptions, and reports a short ranked list of what to fix.
  • "When I have to run commands myself, write them as a runbook file, with the reasons in comments, not as a list in the chat." To step through it, cmdq is the Alt-n tool from the runbooks section: one script and three key bindings.
  • Want more than markdown in the browser? mdserve-starter is the generic core of mdserve, ready to build your own on.
  • Got a spare laptop and a TV? tvremote is the phone remote from The big screen, above. Install it on the laptop, open its page on your phone, and you have Netflix, YouTube and reading on the TV. Reading is for pages your agent sends: its bigscreen command puts a page on the TV, and you scroll it from the couch with your phone.

Conclusion

AI, or rather the software it can build quickly, has finally let me get rid of most of the friction involved in controlling computers. I&aposve never looked at mdserve&aposs code, nor at the EDN file its tabs come from. I know what it does from using it every day.

Now imagine arriving for work at a new job, as a developer or a production support person, and being told to run one thing from the command line. Up comes everything that role needs: the dashboards, the logs, the runbooks, the documents, each in its place. Each role could have its own mdserve. A turnkey introduction to a job, long desired, is finally practical.

The next stage is, of course, doing some of these things in a loop, without a person driving each step. That&aposs already happening in software development: agents make several attempts, and the best are kept. Two products now arriving work like this. Both take an evolutionary approach.

samizdat is an agentic harness for Clojure that splits larger tasks among several agents, and each sub-agent gets a contract plus tests as its spec. Several attempts can run side by side, and the winner is chosen by ranking each attempt on checked evidence. The loop itself is a state machine written as data, and the agent can rewrite it while it runs, behind a validate-and-rollback gate.

simmis is a self-hosted app where people and agents share one memory, including the code repository, and its central idea is cheap forking. Every change an agent makes to shared knowledge, documents or the code lands on its own copy-on-write branch, so agents can make several attempts at the same thing side by side. Whoever holds merge authority, a person or another agent, reviews the differences and merges the ones worth keeping. That&aposs what makes it safer to let agents work on software in a loop. Two things stay off the branches: chat, where people and agents talk and the work gets asked for, and actions with outside effects, such as sending an email, which a branch can&apost undo. simmis is an early public release; the overview and roadmap are at simm.is.

Waterfall plans everything up front, then builds; agile builds in small steps and changes course as it learns. Waterfall&aposs weakness is going back up: once building starts, changing the spec is expensive. The evolutionary approach allows both, and removes that weakness. Each attempt can start from a spec, as in a waterfall, and what the attempts reveal goes back into the spec before the next round, as in agile. samizdat gives each sub-agent a contract and tests as its spec and ranks the attempts on evidence; in simmis, attempts sit on forks until a person or agent with merge authority keeps the good ones. And when the code takes hours rather than months, going back up is cheap.

Glossary

  • Agentic harness: software that runs AI agents in a loop, handing them tasks, checking their work and feeding back results, so they can keep working without a person driving each step.
  • Agile: a way of running software projects in short steps, each one delivering something that works, so the plan can change as you learn. It largely replaced the waterfall.
  • API key: a long secret string that lets a program log in to a service on your behalf, like a password for software rather than people.
  • Babashka: a fast-starting way to run Clojure as everyday scripts, the kind of small programs you&aposd otherwise write in a shell language. babashka.org
  • Checksum: a few extra characters built into a code or address and calculated from the rest, so a single typo makes it fail to check out.
  • Chrome DevTools Protocol: the remote control built into Chrome. Developer tools, and programs like mdserve, use it to open tabs, read pages and click on them.
  • Clojure / ClojureScript: a programming language from the Lisp family, popular for working with data. ClojureScript is the same language compiled to run in the browser.
  • Copy-on-write branch: a cheap copy of some data that only stores what changes. It can be kept, merged back or thrown away without touching the original.
  • Counterparty: the person on the other side of a trade.
  • curl: a small command-line program that sends a single web request, the way a browser would but without the window.
  • E-wallet (GCash): money kept on a phone app rather than in a bank account, spent and transferred by phone. GCash is the big one in the Philippines.
  • EDN: Clojure&aposs format for data files, a cousin of JSON. edn-format
  • HTTP: the language browsers and web servers use to talk to each other.
  • LAN: the network inside your home or office, usually just your Wi-Fi, which links your own devices to each other. Devices out on the internet can&apost reach in.
  • Localhost: a machine talking to itself. A web server listening only on localhost can&apost be reached by anything else, even on the same home network.
  • Markdown: plain text with a few simple marks for formatting, like # for a heading and * for emphasis. Easy to write, and easy to turn into a web page.
  • P2P (peer-to-peer): buying and selling directly between people rather than through a shop or a bank, with the exchange only holding the money in escrow until both sides are done.
  • REPL: a live session inside a running program, where you can type code and see the result at once, reading and changing the program while it runs. Epupp puts a Clojure REPL into a live browser tab, so an agent can see what the page actually drew, not just the HTML it was sent, and even change the page until a reload puts it back. clojure.org
  • Spec: the written specification, a precise description of what software must do, written before the code.
  • Stablecoin (USDT): a cryptocurrency designed to stay worth one US dollar. USDT (Tether) is the most widely used.
  • Terminal: the text window where you type commands to the computer, instead of clicking.
  • tmux: a program that keeps terminal sessions running in the background and lets you split one terminal window into several.
  • Uberjar: a single file that packages a Java or Clojure program with everything it needs, ready to run on a server.
  • Waterfall: the old way of running software projects, in strict stages: write the whole specification first, then design, then build, then test, with little going back. It fell out of favour because mistakes in the spec were found late and were expensive to fix.
  • Webapp: an application you use in a web browser. The part you see and click runs in the browser itself, and it talks to a server that stores the data and does the heavy work.
  • Workspace: a whole separate desktop with its own windows, switched with a keystroke. Windows calls them virtual desktops, and macOS calls them Spaces.

Comments or questions? Email me at (turn on JavaScript to see my address).

Permalink

A useful personal agent

I started building a useful personal agent. What makes it useful is that it runs on my computer, but this immediately raises concerns about data exfiltration. I use Apple reminders (among other things) to organise my life so I had it write a little script to be able to interact with those, which works great. One of the main things I want help with is getting a handle on my chaotic piles of todos. They are scattered all over the place and all mixed up. Part of the problem with my current system is that aspirational things are mixed up with hard deadlines, so on busy or unpredictable weeks things that aren’t essential just pile up and stay there and then the “today” list just keeps rolling everything over until there are dozens of things on it, which I couldn’t possibly get done in a single day if I tried. And then I’m just mentally keeping tabs on what is actually important or on a real deadline, which defeats the purpose of the system.

The problem is that since I had a baby every day is unpredictable. I used to have a pretty good handle on my life, but none of my old systems really work anymore. There’s no way to predict how my nights will go anymore so my energy levels are very inconsistent, and I can’t even know what the days will be like. Anyway sounds like I need a follow-up post on the woes of working parenthood. Point being, I need a new system and so far am having fun building an LLM-based one.

Like I said what makes the agent useful is that it runs on my computer. It has access to my data and the internet, and when it was just helping me with reminders it only had access to those, which I write, so I trust them.

The problem is that in the process of having it help me organize the reminders, I realized that the answers to most of the questions it was asking me could be found in my emails. It was still helpful and less overwhelming having a bot help me organize things, but it still needed a lot of information and context from me that it could have found in my emails if it’d had access. But the problem with giving a bot access to email is that emails come from other people. A way to inject a prompt to my bot is the missing piece of the lethal trifecta, so I need to find a way to do it safely. Turns out it’s just a hard problem, but there is some interesting research out there and I’m working on coming up with something that will work to safely allow the bot get answers from my email without a way to pass malicious or sensitive information it finds there forward to an agent that has ways to access the internet.

Permalink

Stringulation

I’ve been stringing you along for years. Time for a (breaking?) change?

Shall I compare thee to a … string?

When porting Clojure for the JVM to the CLR, the question often arises of when a particular aspect of computation is intrinsic to Clojure or just an exposure of a feature of the JVM. For the former, I try to duplicate behavior; for the latter, I try to expose the corresponding CLR behavior. I have run into this not infrequently, particularly in the early days of the porting effort.

One area where it came up very early was with string comparison. And, frankly, I did not give it a lot of thought. Where ClojureJVM used java.lang.String.compareTo, I used System.String.CompareTo. In other words, I made the decision to expose the underlying platform mechanism. In retrospect, this was probably not the right decision. These methods are significantly different.

The JVM String.compareTo is an ordinal comparison of UTF-16 strings. This is a straightforward lexicographic comparison, performed character-by-character. The CLR String.CompareTo is culture-sensitive, i.e., the result of comparing two strings varies depending on the culture in effect at that time, which is thread-dependent.

There are several consequences of using CompareTo on the CLR, of varying import.

ClojureJVM and ClojureCLR differ on things such as comparisons.

(compare "a\u00ADb" "ab") ;; => non-zero meaning not equal (JVM)
(compare "a\u00ADb" "ab") ;; => zero, meaning equal        (CLR) 

(\u00AD is the soft-hyphen character.)

And, thus, sort order:

(sort ["b" "B" "a" "A"]) ;; => ("A" "B" "a" "b")  (JVM)
(sort ["b" "B" "a" "A"]) ;; => ("a" "A" "b" "B")  (CLR)

compare and = don’t agree.

(compare "a\u00ADb" "ab") ;; => 0  (equal)     (CLR)
(= "a\u00ADb" "ab")       ;; false (not equal) (CLR)

The difference here is that compare uses String.CompareTo (culture-sensitive) and = uses String.Equals (ordinal comparison).

sorted-set and hash-set yield different sets on the same inputs.

 (count (sorted-set "a\u00ADb" "ab")) ;; => 1, one string silently dropped because it compares as equal (CLR)
 (count (hash-set "a\u00ADb" "ab"))   ;; => 2, because hash sets use ordinal = (CLR)

Culture-sensitivity bites in some related places where string comparisons occur.

For example,

(compare :b :B) ;; => 32 (positive, indicating :b > :B) (JVM)
(compare :b :B) ;; => -1 (negative, indicating :b < :B) (CLR)

Culture-sensitive string compares are thread-dependent.

For example, ASP.NET Core sets the culture per request from Accept-Language, so (sort names) silently follows each user’s collation. Picking up information from the thread is fine; I feel it is better for it to be a deliberate choice rather than delivered behind your back.

Culture-sensitive comparisons are more expensive than ordinal comparisons.

Sorting a bunch of strings or creating a sorted map under ordinal comparison executes roughly 54-61% fewer instructions (instruction counts measured on one machine, not timings) than a culture-sensitive comparison using en-US. Capturing one culture comparer at startup to avoid thread lookup of the culture decreases the instruction count by roughly 5% compared to String.CompareTo today.

Culture-sensitive comparisons have not been consistent over time.

“Before .NET 5, the .NET globalization APIs used different underlying libraries on different platforms. … If you upgrade your app to target .NET 5 or later, you might see changes in your app even if you don’t realize you’re using globalization facilities.” (See Globalization and ICU - .NET). And you can switch globalization providers. ClojureCLR on .NET Framework uses NLS, not ICU, so Framework and .NET builds of ClojureCLR on the same machine can yield different results. That’s just on Windows. Let us not discuss Linux and Mac. Read it and weep.

Going deeper

The original focus in the benchmarking investigation surfaced the compare issue outlined above. After the initial results, I decided to expand the search to all string manipulation in the ClojureCLR, with comparisons against ClojureJVM where appropriate. Most of this internal string manipulation is related to reading data (which in Lisp-land includes programs) which arguably should not be culture/locale sensitive. It is not in ClojureJVM – all is ordinal. There are a few places where culture-sensitivity snuck into the ClojureCLR code, by carelessness or ignorance. Some are so marginal that I’m guessing they have never been encountered. For example, if we let <SHY> represent the soft-hyphen character, when reading source code:

  • JVM reads foo:<SHY> => creates symbol foo:<SHY>
  • CLR reads foo:<SHY> => throws an exception under en-US (at least)

The most significant culture-dependent bugs are

  • tr-TR: the flag to turn on direct linking in the compiler is ignored – the dotless ‘i’ (U+0131) makes an appearance.
  • sv-SE: (+ 1 1E-10M) doesn’t compile. Swedish uses a different minus sign character.

I consider these examples to be bugs. Fixing them is not a breaking change.

There are five functions (starts-with?, ends-with?, index-of, last-index-of, replace-first) in the clojure.string library that are in conflict with the JVM version and also demonstrably incorrect. These bugs are visible to users today, but they are bugs and should be fixed. Example: (replace-first "a<SHY>b" "ab" "X") gives "Xb" and corrupts the string. Results would change only for strings containing ignorable characters (soft hyphen, NUL, combining marks) and the changes would match the JVM’s answers.

A proposal

I plan to make two sets of changes. The first set is to fix the bugs above: the reader, the compiler, number literals, and clojure.string. I am not <SHY> about making these changes.

The second set of changes will be more user-visible and thus have the potential to break user code. This involves changing the definition of compare to be ordinal-based. To minimize impact, we can make this change selectable at startup. Set the switch to ‘culture’ and you get the current behavior; set it to ‘ordinal’ and you get ordinal-based comparisons. The ‘ordinal’ mode is pretty much identical to ClojureJVM behavior.

There is a third possibility: A captured culture mode which at startup creates a string comparator based on the CurrentCulture in effect at system startup. That is used by the compare function. If you change CurrentCulture, it will not be seen by this; there is no thread-dependency. You don’t get the large speedup, but do get the 5% savings from the no-thread-lookup. I’m not sure it is really worth it. The audience would seem to be a culture-mode user who sorts heavily and doesn’t care about threads. I’m not sure that’s much of a market. (This would require a little more testing before full validation as an option. )

For ordinal mode users, varying culture in things such as sorting is still possible. Most of the Clojure-defined functions, such as sort and sorted-set, have variations that take a comparator function. In fact, I think the general advice should be to be explicit in your sorting and comparator options always. To make this easier, we will add a culture-comparator function that will return a comparator function based either on the value of CurrentCulture when called or a supplied culture. You could write (sorted-set-by (culture-comparator "sv-SE")).

The choices

The bug fixes will happen regardless. For the compare changes, we have some choices.

  1. Do nothing. Not really an option, from my viewpoint.
  2. Put in the switch. Default is ‘culture’. If you want performance, you have to ask for it.
  3. Put in the switch. Default is ‘ordinal’. Performance and consistency are the default.

To be clear, my own preference is #3. The current situation has internal inconsistencies, is inconsistent with the JVM, and is slower overall.

An additional choice is

  1. Do we need the ‘captured culture’ mode?

I plan to put a link to this document up on the #clr channel in the Clojurians Slack for feedback. I’ll amend this post to reflect that discussion.

The result

TBD.

AI disclaimer/acknowledgement

I’ve been using AI coding tools extensively in my benchmarking work. There is a lot of tedious coding involved and the tools do it for me a lot faster and more correctly than I would on my own. This document reports on the results of that work. I drafted the document, then used the AI coding tool to vet it for accuracy, resulting in some typo corrections and a few suggested rewordings of inaccurate phrases. A stray comment in its review of my first draft sent me down a new line of inquiry that ended up in the two-phase approach here. The instructions to the agent include not drafting any code that might go into the ClojureCLR code base. It can identify locations and make prose suggestions only. (And it can write all the testing code its non-existent heart desires. It makes measurement runs on my commits as we go along.)

Permalink

A look at the Clojure CLI REPL

Just before this year's Clojure/conj, the Clojure team released a new Clojure CLI REPL. Per the README:A REPL for the Clojure CLI featuring multi-line editing with proper indentation, bracket highlighting, structural editing, inline eval, doc lookup for Clojure and Java, a data inspector, configurable prompts, keybindings, and much more.

Permalink

Clojure in the Age of Language Models

In the age of generative models, the most important skill for a developer is to be able to recognize the shape of the problem and pick the correct way to express it. What's relevant today is the ability to do high level reasoning about algorithms, data structures, and data flows within the system. Imperative programming is quickly becoming akin to writing assembly because language models are quite competent at writing code in the small, while they stumble at high level design and architecture. So it is good to learn a language that operates at a higher level, such as Clojure, which is data-centric, composes functions declaratively, and keeps code close to the shape of the problem.

LLMs can generate code much quicker than I can, but the issue is how to test that the generated code does what I want. Before you can test anything in many languages, you have to recompile the program, and that may take quite a few minutes for larger projects. The length of that feedback cycle, in turn, sets the pace of your progress.

We also need to talk about the tedious reality of rebuilding application state. That matters even more when you are not writing the code yourself, so the output is inherently less intentional. Clojure collapses that loop because our workflow does not draw a hard line between when code is read, when it is compiled, and when it runs. Clojure runs in a live process, and you can redefine a function at the REPL and have the new version take effect immediately, without restarting. State stays in place while you change the code that operates on it. Inspecting the state to reproduce behaviors and verify fixes can be done instantly when you can reach into a running process.

An agent can similarly connect to a REPL to diagnose an issue and swap out the code without any downtime. Agents fundamentally need observability in order to get useful feedback about the changes they are making. Working with the REPL means the agent doesn’t have to go through all the steps of compiling and rebuilding the app, then logging its output to see the result. That translates into having to do fewer iterations to reach a working system, which becomes particularly valuable when working with a large codebase. The functionality of your system can keep evolving as you load new code into the running process without any restarts.

Another advantage comes from immutability, which helps reliably control the operating context of the program environment. When the majority of the logic in an application is written using pure functions, the agent can safely consider and test pieces in isolation without having to reason about the entire program. Agents can write functions one at a time, test them in the running program instead of making a whole bunch of changes, then running tests to find out if they worked.

At this point, I find anything with a compile cycle is a nonstarter if I have an option to have a live programming environment. It is not just compile and startup time that ends up being painful. A bigger problem is the tedious necessity of having to reconstruct the desired state every single run. When you have something small with limited functionality, that is fine, but as your application grows, rebuilding the state can take a significant effort. You might have to click through some menus in the user interface, wait for data to be processed from a service or a database, and so on. Being able to put your application in a particular state and make changes within that context is just a qualitatively better development experience.

Then there's the powerful macro system , allowing you to adapt the language to the problem domain, eliminating a lot of boilerplate code you would have to write otherwise. What makes this particularly powerful in Clojure is homoiconic syntax, where code is written using data structure literals. Since there is a common syntax for expressing both logic and data, a program can take any piece of code and manipulate it as it would with any other data structure, then evaluate it. This makes it incredibly easy to add new semantics because all you need is to make templates out of code. A macro works much like a function that accepts the code you wrote and produces the form that actually gets evaluated. The expression for printing a string does the printing when you evaluate it, but it is also nothing more than a list of the println symbol and the string itself.

One major advantage of S-expression based syntax is that it's easy for both humans and machines to read. And since the state can be trivially serialized as plain data structures, an agent can dump it in the REPL to inspect what’s happening in the application at any time. The data orientated nature of the language is a natural fit for LLMs since these models operate on text, making it exceptionally easy for them to see how data flows through the system without any opaque object graphs to worry about.

Furthermore, all the functions operate on a common set of data structures, allowing you to compose them together to transform data like Lego blocks. Clojure programs tend to be far more concise since the code is largely written through declarative composition of functions from the standard library, which encapsulate the implementation details. The result is that a codebase tends to be much shorter, leaving far less code to repeat. This conciseness matters since a smaller program costs fewer tokens, and fewer tokens leave more room in the context, making the language friendlier for local models.

In my experience, models lacking the broader context while making changes in a piece of code is one of the most common failure cases. Having a terse syntax means that more relevant code lives directly in the context window, directly addressing the problem. The model gets a significantly better view of what you are trying to do and can make much better decisions. If the whole call graph is sitting in the context window, the model sees how all the pieces fit together.

All these features combine to make a perfect environment for the agent to work in. Expressive syntax leads to less code repetition. Macros let you fold repeated patterns into new domain specific constructs. Code itself is just structured data that can be inspected and transformed. And the REPL ties it all together, providing you with a living system that evolves along with your code.

There are, however, a few drawbacks to the Java virtual machine, which Clojure traditionally runs on. From my experience, many developers, fairly or not, have an issue with requiring the JVM, and shy away from Clojure because of startup time, a somewhat heavyweight runtime, and perceived bootstrapping complexity.

One goal for Jolt in particular is to get more interest from outside the existing Clojure community by addressing these concerns. The compiler is a single binary, and it ships with all the tooling, such as dependency management and task running, baked in. It interops seamlessly with the native ecosystem via FFI, so you can use it in a way comparable to Python. Best of all, program distribution involves building a standalone binary similarly to Go. Thus, Jolt may remove the last big source of friction for trying Clojure.

In an age where writing code is cheap but verification and iteration remain expensive, the high level declarative style lines up exactly with what both agents and humans need to produce working code. Clojure is a great language to learn today because of the unique way it fits the era of language models.

Permalink

Copyright © 2009, Planet Clojure. No rights reserved.
Planet Clojure is maintained by Baishamapayan Ghose.
Clojure and the Clojure logo are Copyright © 2008-2009, Rich Hickey.
Theme by Brajeshwar.