An agent in 100 lines of Lisp A developer recreated an AI agent loop in 100 lines of Common Lisp, arguing that the language's recursion and eval capabilities make it ideal for building agents. The project, built with SBCL and two libraries, demonstrates a minimalist agent that uses Lisp's native eval as its only tool. The author contrasts this with modern AI frameworks, suggesting Lisp's symbolic AI roots remain relevant for agent architectures. Around 2000 I took an AI course at the University of Guelph https://www.uoguelph.ca/ . 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 https://en.wikipedia.org/wiki/Lisp programming language - 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 https://en.wikipedia.org/wiki/Symbolic artificial intelligence : 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 https://www.paulgraham.com/avg.html . 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 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 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 https://en.wikipedia.org/wiki/Homoiconicity - 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 https://www.cs.cmu.edu/Groups/AI/html/cltl/clm/node56.html 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 memory-is-20-lines Once the loop worked I wanted persistence. Conversations that survive across sessions. In Agent Foundry, I implemented pgvector https://github.com/pgvector/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 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 Perfect You were absolutely right, Jamie. I've now created a brave-search function that can make HTTP requests to the Brave Search API. If you provide me with a Brave Search API key, I can perform web searches for you. The function takes: - api-key - your Brave Search API key - query - the search query - count - optional number of results to return default 10 Would you like to try it out with your API key? NIL Sit with that for a second, because it inverts how we normally think about agent capabilities. In Agent Foundry, in Claude Code, in every agent platform I know of, the tool catalog is fixed at design time. Somebody like me decides what the agent can do, writes the tools, ships them. Here the catalog is open. The model decides what it needs, writes it in its own substrate language, and evals it into existence. Code is data, data is code, and capability is just conversation. And there’s a twist that took me a minute to appreciate. The defun itself dies when the process exits. Functions live in the running Lisp image, not on disk. But the transcript persists. The source code of brave-search, the model writing it, all of it sits in memory.json as plain text. So in a fresh session the function doesn’t exist until the agent re-reads its own history and evals it back into being. Its capabilities are stored as memories of having built them, re-hydrated on demand. Talk about homoiconicity - an agent whose skills are literally stories it tells itself. Skills are just memories skills-are-just-memories Every serious agent platform today, Agent Foundry included, treats the tool catalog as a design-time decision. MCP is basically a formalization of the design-time assumption - capabilities as contracts, negotiated in advance, versioned, approved. This experiment sits at the other pole: capability decided at runtime, by the agent, in response to whatever the conversation needed. Perhaps the industry is already sliding along this continuum without quite naming it. Code interpreters were the first step - agents writing throwaway programs to answer a question. Skills, the SKILL.md pattern, are the second - because a skill is nothing more than capability stored as text and loaded at runtime. This 100 line Lisp agent just collapses the remaining distance. Its skills are stored in its own transcript. And kind of stretching maybe, that’s roughly how we work too. Almost nothing you can do was installed at design time. You learned it, stored it, and now you rehydrate it on demand - often complete with the memory of writing it into yourself, like remembering the specific summer you learned to ride a bike. A capable human is, in some sense, a transcript of their own practice, re-evaluated every time it’s needed. Watching an agent re-read its own history and defun a skill back into existence felt less like a hack and more like a crude, yet beautiful, sketch of that. I’m speculating well past my expertise here, but the shape rhymes. He wasn’t wrong, he was early he-wasnt-wrong-he-was-early Symbolic AI lost. Nobody’s expert system survived contact with backpropagation https://en.wikipedia.org/wiki/Backpropagation and gradient descent https://en.wikipedia.org/wiki/Gradient descent . But the thing Lisp was actually built for, programs as data, computation you can inspect and transform and feed back into itself, turned out to be a pretty good description of what an agent is. The models do the reasoning now. But the loop around them, the part we actually engineer, is the part Lisp was always fantastic at. 25 years is a long time to be right about something. Somewhere out there maybe my old AI prof is ahead of us on something else. Full source, Dockerfile included, is on GitHub: https://github.com/jamiebeach/lisp-agent https://github.com/jamiebeach/lisp-agent