An agent in 100 lines of Lisp

Can Lisp, the classic language of symbolic AI, be the ultimate tool for modern LLM agents? By stripping away complex frameworks, we can build a fully functional, self-extending agent in just 100 lines of Common Lisp.
Around 2000 I took an AI course at the University of Guelph. I don’t think I learned too much. We didn’t talk about neural networks, as far as I can remember. My end of term project was, I think, a pathfinding algorithm wearing an AI costume. There were certainly no discussions of transformers. No CUDA. No PyTorch. None of that existed.
But what I remember doing a lot of was coding in Lisp - a lot of Lisp in the dark University of Guelph CIS lab.
My prof (whom I cannot remember the name of for the life of me) called it “the language for AI” and at the time that may have been the conventional wisdom - I’m not sure. But I actually enjoyed writing Lisp code. To me, it was a kind of art building these elegant recursive functions. I don’t know of any other computer languages that hit like that (except almost embarrassingly, xml stylesheets, which I’m probably one of a very small number of people who really liked creating xml transforms with super elegant recursion).
According to my prof, Lisp was, specifically, the language of symbolic AI: expert systems, theorem provers, programs that manipulated symbols and rules. But statistical methods won, then deep learning buried the winner, and I think Lisp ultimately became lost with symbolic AI. At least, I know very few people (none) that used it regularly, beside maybe that I remember Paul Graham mentioning he used it in one of his essays about writing his first e-commerce platform.
Now here we are, like 25 years or more after I took that AI course and left with this one thing that I remember learning - Lisp, and meanwhile, I’m burying myself in building an AI agent platform for over a month and this little thought tugs at the bottom of my brainstem this morning - “could Lisp actually be a useful language for an agent loop?” And then I find myself distracted from what I should be doing and instead working with Claude just to see…
An agent is a recursive function
Don’t let the power of Claude Code, OpenClaw or any other AI agent tooling fool you - strip away the frameworks and an agent loop is incredibly simple. You have a list of messages. You send it to a model. Either the model answers in words, or it asks to use a tool. If it asks, you run the tool, append the results, and do it again.
Maybe some agents implement that as a while loop with state. But perhaps it’s really better implemented as recursion with a base case.
I’m not going to get into Lisp syntax at all, and if you’ve never coded with Lisp, this will seem pretty strange, but here’s an agent loop in Lisp:
(defun agent-loop (messages)
(let* ((message (ref (call-model messages) "choices" 0 "message"))
(tool-calls (gethash "tool_calls" message)))
(if (and tool-calls (plusp (length tool-calls)))
(agent-loop (append messages
(list message)
(map 'list #'execute tool-calls)))
(append messages (list message)))))
That’s the whole agent, no kidding, just 8 lines of Common Lisp. Base case: the model answers, returns the history. Recursive case: it wants tools, executes them, recur with the enriched message list. No framework. No state machine. The agent’s state is just the argument being folded through the recursion.
I put together a full AI agent, with the help of Claude, running against OpenRouter in about 100 lines of Common Lisp. SBCL, two libraries (dexador for HTTP, shasht for JSON), and nothing else.
The only tool is eval
When you build an agent you normally start bolting on tools - Agent Foundry has web search and crawling, tables and file tools, python execution tooling, etc. In fact, most of most agents are a tool catalog.
Lisp lets you cheat. Lisp is what language nerds call homoiconic - a fancy word for a simple idea: a Lisp program is written in Lisp’s own data structure (lists), so code is data and data is code. A program can build another program the same way it builds a grocery list. Which means instead of building tools, you hand the model the language itself:
(defun lisp-eval (form-string)
(handler-case
(format nil "~s" (eval (read-from-string form-string)))
(error (e) (format nil "ERROR: ~a" e))))
One tool. The model writes a Common Lisp form as a string. The agent reads it, evals it, and sends back whatever printed. Ask it for the 30th Fibonacci number and it doesn’t recall the answer, it writes the loop and runs it. Here’s the actual transcript:
* (agent:run "What is the 30th Fibonacci number? Compute it, don't recall it.")
➔ (defun fibonacci (n)
(if (<= n 2)
1
(+ (fibonacci (- n 1)) (fibonacci (- n 2))))) => FIBONACCI
➔ (fibonacci 30) => 832040
The 30th Fibonacci number is 832040.
NIL
I did not tell it to use recursion, but in this blog post about recursive agent loops, the model reached for textbook doubly-recursive Fibonacci all on its own. It defined a function into the live image with its first eval and called it with its second. Keep that move in mind, it comes back later in a bigger way.
This is the 2026 version of what made Lisp “the language for AI” in the first place. The old dream was programs that manipulate programs. We finally have that. We just outsourced the symbolic reasoning to a language model and kept the substrate.
Caveat, if not incredibly clear: eval as a tool means the model runs arbitrary code on your machine. This is a toy for a sandbox. I haven’t run this outside of a local docker container.
Memory is 20 lines
Once the loop worked I wanted persistence. Conversations that survive across sessions. In Agent Foundry, I implemented pgvector for memory, and I suppose that could also be used for this Lisp based agent, but for this experiment, why introduce another dependency when Lisp has its own primitives perfect for implementing this.
The messages are already a list of hash tables. Which is to say, they’re already JSON in spirit. So memory is nothing more than writing the list down and reading it back:
(defun remember (messages)
(with-open-file (out *memory-file* :direction :output :if-exists :supersede)
(shasht:write-json (coerce messages 'vector) out))
messages)
(defun recall ()
(if (probe-file *memory-file*)
(coerce (with-open-file (in *memory-file*) (shasht:read-json in)) 'list)
(list *system-message*)))
The loop itself didn’t change at all. The entry point became a pipeline:
(remember (agent-loop (append (recall) (list new-user-message))))
Recall, recur, remember. No schema. No migrations. No store abstraction. The serialization format is the runtime format. Tell it your name today, ask tomorrow in a fresh process, it answers.
It never forgets, which is also its flaw. Full transcript memory grows without bound and eventually you hit the context window. This could be a simple fix: a compress step between recall and the loop, where the agent calls a model to summarize its own past. An agent recursing on its own history. Beautiful.
Then it built its own web search
The Fibonacci trick was really neat to watch - the agent writing throwaway math and running it. Then I decided to give it a little more rope. I pasted a temporary Brave Search API key into the conversation, mostly to see what would happen. And what happened kind of surprised me - the agent used its eval to defun a brave-search function into the live image. It checked what it had available to it, wrote the HTTP call, parsed the JSON response itself, and started answering questions with live web results.
I never built a web search tool. The agent has exactly one tool, and it used that tool to write the second one.
➔ (defun brave-search (api-key query &key (count 10))
"Search using Brave Search API"
(let* ((encoded-query (quri:url-encode query))
(url (format nil "https://api.search.brave.com/res/v1/web/search?q=~a&count=~d"
encoded-query count))
(response (dexador:get url
:headers `(("Accept" . "application/json")
("X-Subscription-Token" . ,api-key)))))
(shasht:read-json response))) => BRAVE-SEARCH
Source: Hacker News













