Effect Handlers, End to End
Loon's algebraic effects grew up. The same program now runs unchanged under different handler towers — production, test, and replay differ only in the handlers wrapped around the code, never in the code itself. On that substrate we built two libraries that share it: an HTTP framework and an agent framework.
Multi-shot continuations
The default VM now supports multi-shot delimited continuations: a handler can resume a captured continuation zero, one, or many times. Resuming the same point more than once explores several futures from a single program — the basis for nondeterministic search, backtracking, and retries.
[effect Choice [pick [] Int]]
[fn prog [] [let a [Choice.pick]] [let b [Choice.pick]] [+ [* a 10] b]]
; resume each pick over {1,2,3} and sum all 9 worlds -> 198
[handle [prog] [return x] x
[Choice.pick] [+ [resume 1] [+ [resume 2] [resume 3]]]]One program, three towers
A program performs effects and never names a handler, so its inferred effect row advertises exactly what it can do. Swapping the handler tower swaps the implementation — a deterministic offline test, a recording replay, or real production IO — with zero changes to the program. There are no exceptions anywhere; Fail is an effect.
[fn make-session-id [] ; row: #{Reader Clock Random Log Fail}
[let prefix [Reader.ask]]
[str prefix "-" [Clock.now] "-" [Random.gen]]]
[under-test make-session-id] ; deterministic, offline
[under-prod make-session-id] ; real clock/uuid/envAsync is an effect
Concurrency is ordinary code performing effects, with the scheduler as a handler — no async function coloring, no separate runtime. A cooperative scheduler built on multi-shot continuations provides fork, yield, and cancel, with structured concurrency: the scheduler is the scope, and cancelling it cancels its children.
[fn worker [t] [println [str t " 1"]] [Co.yield] [println [str t " 2"]]]
[scheduler [fn [] [spawn [fn [] [worker "A"]]] [spawn [fn [] [worker "B"]]]]]
; A 1 / B 1 / A 2 / B 2Real HTTP, served from Loon
TCP sockets are wired into the VM, so a Loon program can serve real HTTP: listen on a port, accept a request, send a response. The server is the bottom handler; request handlers are effectful functions whose rows show their capabilities — a route cannot touch the database without Db appearing in its signature.
loon run src/http/serve.oo
curl localhost:8080/dashboard
; -> prod-user sees live-row-1, live-row-2An HTTP framework
Routes are effectful functions; the handler tower is the application. The same app runs under a deterministic test tower (no network, no mocks), a trace tower that records every effect, and a prod tower over a real socket. The response body is moved into the responder and consumed on send, so sending twice is a compile error.
An agent framework
The agent control loop is plain code performing Llm, Tool, Approval, and Memory effects; the tower decides everything else. The same loop runs as a deterministic offline eval, a full trace, a human-in-the-loop approval round-trip, and — via multi-shot — an explorer that resumes each approval both ways to visit every outcome. Durable execution falls out as a handler: replay a journal of effect results, then go live, so a crashed run resumes where it left off. The agent can even be served over HTTP.
; same loop, different towers
[under-test goal] ; scripted model + mocked tools, deterministic
[under-trace goal] ; records every model/tool/approval call
[explore goal] ; multi-shot: every approve/deny world
[run-resume journal] ; durable: replay the journal, then continue liveFoundation work
Getting here meant hardening the core: multi-shot continuations and correct handler-stack isolation on the register VM, generic ADT construction and escaping-handler typing in the checker, host effects (clock, uuid, env, files) and TCP sockets in the VM, working multi-file imports, and several higher-order builtin fixes. A new backend differential-parity suite runs the same programs across backends and turns any divergence into a failing test.