Building agents that survive their own execution · Conol<br>← Back to blogFor about nine hours one night, our agents stopped moving. Nothing crashed. Queued work just sat there.
The trigger was mundane. During an RDS restart, a transient database failure surfaced inside Conol's worker polling loop. The loop stopped draining the queue, but the process stayed alive because it still held open handles. No supervisor replaced it. The process sat there, breathing, doing nothing.
What saved us was not the loop. It was what sat underneath it. Every piece of agent state and every effect the agents had emitted was durable in Postgres. When we restarted the worker, queued work resumed from where it had stopped. We did not reconstruct user sessions or replay conversations from logs.
That night is the reason this post exists. The gap between "the model is smart" and "the system is reliable" is not filled by a better prompt. It is filled by treating an agent turn as durable data instead of a call stack.
Here is where most agent runtimes start:
code]:bg-transparent [&>code]:p-0">async function runAgent(input: string) {<br>const messages = [{ role: "user", content: input }]
while (true) {<br>const response = await llm.chat(messages)<br>messages.push(response)
if (!response.toolCalls?.length) return response.content
for (const call of response.toolCalls) {<br>const result = await runTool(call)<br>messages.push({ role: "tool", content: result })
This is fine on a laptop. In production it has a fatal property: all important state lives on the stack and in local variables. The message history, loop position, and half-finished tool call exist only inside one process. If that process dies or hangs, the turn goes with it.
The model can be as intelligent as we like. The runtime around it is only as reliable as the weakest process holding its stack.
The mental model that helped us most came from an old corner of programming-language theory: algebraic effects. We do not implement them. But the way they separate "what to do" from "how to do it" is exactly the separation a durable runtime needs.
Algebraic effects in five minutes
Start with a plain question: when code needs something from the outside world, how does it ask?
A computational effect is anything a function does beyond returning a value. Reading input, writing a file, calling a model, mutating state, and throwing an exception are all effects.
Algebraic effects let code name those requests without deciding how they are answered. The program is split into two roles:
An operation describes the request.
A handler decides what that request means.
The function performing an operation does not know how it will be serviced.
code]:bg-transparent [&>code]:p-0">function greet() {<br>let name = perform AskUser("What is your name?")<br>print("Hello, " + name)
handle greet() with {<br>AskUser(question, k) => resume k("Ada")
perform AskUser(...) does not read from a terminal, web form, or test fixture. It performs an operation and expects a string back.
The handler receives the operation and a continuation k. The continuation represents the rest of the computation from the point where the operation occurred. Here it is roughly:
code]:bg-transparent [&>code]:p-0">k(name) = print("Hello, " + name)
The handler decides what happens next. Depending on the language and implementation, it may resume once with a value, never resume — abandoning the remaining computation like an exception — or resume several times to explore multiple branches, supporting nondeterminism and backtracking.
The useful intuition is a resumable exception . A normal exception unwinds the stack and never comes back. An effect operation may transfer control to a handler and later resume at the exact point that asked.
Real implementations constrain this. OCaml 5 uses one-shot continuations: each captured continuation may be resumed at most once. Multi-shot continuations are more expressive but costlier to implement safely.
MechanismHow work is representedCan execution return to the interruption point?ResumptionExceptionsA raised valueNoZero timesMonadsA value inside a computational contextSequencing continues through bindDetermined by the monadEffect handlersA named operation plus continuationYesDepending on implementation: zero, once, or multiple times
Effect handlers do not automatically solve every composition problem associated with monads. They are a different abstraction. The part that matters for Conol is simpler: the computation declares what it needs, while an interpreter elsewhere decides how to satisfy it.
From programming-language theory to practical handlers
The lineage did not begin with one paper.
Eugenio Moggi introduced monads as a structuring principle for computational effects in 1991. Gordon Plotkin and John Power developed an algebraic account of effects in the early 2000s, describing operations through equations. Plotkin and Matija Pretnar formalized effect handlers in 2009, turning those...