An agent in 100 lines of Lisp
An Agent in 100 Lines of Lisp: A 25-Year Full Circle
Around the year 2000, I found myself in a dark computer lab at the University of Guelph, enrolled in an AI course. Back then, the curriculum didn't focus on neural networks; my final project was essentially a pathfinding algorithm dressed up to look like "AI."
However, the most lasting impression from that course was the immersion in Lisp. My professor—whose name has unfortunately slipped my mind—insisted that Lisp was the definitive language for artificial intelligence. At the time, this was the prevailing wisdom.
I found a certain artistic beauty in crafting elegant recursive functions. Aside from a strange fondness for XML stylesheets (a niche preference, I admit), no other language has ever provided that specific intellectual satisfaction.
The Rise and Fall of Symbolic AI
According to my professor, Lisp was the engine behind Symbolic AI, which focused on:
- Expert systems
- Theorem provers
- The manipulation of rules and symbols
Eventually, the tide shifted. Statistical methods gained ground, and then deep learning completely obliterated overshadowed the competition. Lisp seemed to vanish along with the symbolic approach. I knew almost no one using it in production, save for mentions by Paul Graham regarding his early e-commerce ventures.
The Epiphany: Agents as Recursion
Fast forward 25 years. I've spent the last month deep in the trenches of building an AI agent platform. One morning, a thought surfaced: Could Lisp actually be the ideal language for an agent loop?
I decided to experiment with Claude to test a hypothesis: An agent is fundamentally a recursive function.
While modern frameworks (like Claude Code or OpenClaw) make things look complex, the core logic is simple:
- Ask the model.
- If it requests a tool, run the tool.
- Append the result to the history.
- Repeat.
The Implementation
In Common Lisp, this entire loop can be expressed in just 8 lines:
(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)))))
The Logic Flow:
- Recursive Case: The model wants tools execute them recurse with an enriched message list.
- Base Case: No tools requested return the final conversation.
The agent's state is simply the argument being folded through the recursion. I managed to build a fully functional agent using SBCL and only two libraries: dexador (for HTTP) and shasht (for JSON).

The Power of Homoiconicity
Most developers build agents by "bolting on" specific tools (web search, Python interpreters, etc.). However, Lisp is homoiconic, meaning its code is written using its own data structures (lists). In Lisp, code is data and data is code.
Instead of building a library of tools, I gave the model the language itself via eval:
(defun lisp-eval (form-string)
(handler-case
(format nil "~s" (eval (read-from-string form-string)))
(error (e) (format nil "ERROR: ~a" e))))
Now, the agent doesn't just "recall" information; it writes and executes programs on the fly.
Case Study: The Fibonacci Sequence
When asked for the 30th Fibonacci number—defined mathematically as —the agent didn't guess. It wrote a program:
Transcript:
- User: "What is the 30th Fibonacci number? Compute it, don't recall it."
- Agent:
(defun fibonacci (n) (if (= n 2) 1 (+ (fibonacci (- n 1)) (fibonacci (- n 2)))))FIBONACCI - Agent:
(fibonacci 30)832040 - Agent: "The 30th Fibonacci number is 832040."
The model independently chose a doubly-recursive approach to solve the problem. We have essentially outsourced the symbolic reasoning to the LLM while keeping Lisp as the execution substrate.
⚠️ CRITICAL WARNING: Using eval as a tool allows the model to execute arbitrary code on your machine. Do not do this in an unsecured environment.
Persistence and Memory
To add memory, I avoided heavy dependencies like pgvector and used Lisp's native primitives to read and write the message list to a file.
| Component | Lisp Implementation |
|---|---|
| Storage | Simple JSON file via shasht |
| Recall | (read-json in) converts to list |
| Remember | (write-json messages out) overwrites file |
The updated pipeline looks like this:
Recall Agent-Loop Remember
Current Limitations & Future Work
The current system saves the full transcript, meaning the memory grows until it hits the context window limit.
- Implement a compression step.
- Use the model to summarize history between
recalland theloop.