2026-10-23 · 17 min · agents · harness · developer-tools · tool-calling
A 1:48 narrated explainer, drawn in code. Every number and picture in it is this article's own; the sources are below.
› transcript
Hi, I'm Quoit! Vercel open-sourced an app that writes one web page per prompt. The agent loop underneath is the interesting part. Every prompt and every step is stored first, so a new process continues a killed turn instead of starting over. Your message lands in the session log before the server accepts it. One worker holds the session. A replaced worker is refused at its next write. It calls the model, runs the tools, and writes each step back to the log. If the process dies, a new one replays the log and carries on. The hard case is a tool that was running when the process died. An idempotent tool runs again, under the same execution id. Any other tool never runs twice on its own. The model is told it may have partly run, and decides whether to check. Rerunning the turn pays for every model call again, and might refund twice. The log replays finished calls, and each tool says whether it may rerun. In fx0, the chat, Redis and Neon belong to the app. The libfx agent saves every step to Workflow World, and Vercel Queues restarts a crashed turn. On a fake local model, a kill inside a tool still meant three model requests. A kill mid request re-sent only that one. On Vercel, a crashed function looks like a slow one, so recovery can wait for the eight hundred second limit. One question for every tool. Can this run twice? Store every step. Replay what finished. Rerun only what says it may. Every source is in the full article. I'm Quoit. Bye!
Why read this
Notabletop 60%Kills libfx mid-turn on a fake local model to show what replays, what re-runs and what the model is told, read against the fx0 and libfx code.
- Original analysis
- A new technique
- Concrete numbers to act on
Agents & harnessesAPI onlyApache-2.0Practitioner tool
How this was scored
- Is it new?
- 2 of 3: A real new idea, method or capability
- Can I trust it?
- 2 of 3: Measures key facts from files, code or configs
- Can I run it?
- 2 of 3: Open code or weights with real limits
- Will I understand it?
- 2 of 3: Mechanism from first principles with figures
- Can I act on it?
- 2 of 3: A concrete recipe, numbers or comparison
- Will it last?
- 1 of 3: Relevant for months
- Does it affect many?
- 1 of 3: A specialist community
- Only here?
- 2 of 3: A teardown or measurement few others did
Score 64 of 100, ranked 233 of 577 rated articles. Each question is answered 0–3 by hand, and a 3 is rare. How articles are scored
On 19 October Guillermo Rauch quoted a Vercel post and wrote:
The quoted post from @vercel_dev calls it a "mini v0" with "200+ models from Vercel AI Gateway" whose "outputs are pure HTML artifacts". The demo is at fx0.vercel.dev. The code is two repositories:
- license
- Apache-2.0
- branch
- main
- tests
- 37 files
- source
- 1.2 MB
- commit date
- 2026-10-22
by size of tracked source at this commit, file counts in brackets; docs, data and vendored trees excluded
local clone, 2026-10-23 at 8541d8f — branch, commit, commitDate, fileCount, hasTests, languages, license, licenseFile, testFileCount
- license
- Apache-2.0
- branch
- main
- tests
- 958 files
- source
- 38.8 MB
- commit date
- 2026-10-23
by size of tracked source at this commit, file counts in brackets; docs, data and vendored trees excluded
local clone, 2026-10-23 at d824f60 — branch, commit, commitDate, fileCount, hasTests, languages, license, licenseFile, testFileCount
The replies split two ways. Half are about speed ("took like less than 5 secs", a stopwatch at about 10 seconds). The other half picked out the second sentence: one reply says resuming after a crash mid-generation "is a better pitch than the speed", another asks how libfx handles "idempotency for side-effecting tools on replay". The second half is right. The speed is whatever model you point it at. The durability is code, and it is the part worth reading.
I read fx0 at commit 8541d8f (22 October) and fx at d824f60, installed [email protected] from npm into a scratch directory, and killed it mid-turn. I made no account and called no hosted model: the model in my runs is a fake OpenAI-compatible server on localhost. Every number below is labelled measured (I ran or counted it), reported (Vercel's figure, not re-run) or reasoned (my arithmetic on the other two).
Three pieces: fx, libfx, fx0
fx is Vercel Labs' coding-agent CLI. Its README calls it "one native binary written in Zig" and marks it experimental. The tree has about 711,000 lines of Zig under src/ (measured, git ls-files 'src/*.zig' | xargs cat | wc -l), covering the agent loop, sessions, permissions, MCP, ACP and the terminal UI. CI fails a pull request if a benchmarked command such as fx help takes longer than 2 ms on Linux (reported).
libfx is that same Zig engine shipped to JavaScript as a Node.js native addon and as WebAssembly. The npm package for 0.0.14, published 16 October under Apache-2.0, holds four prebuilt addons (macOS and Linux, x64 and arm64) and two .wasm files. The Linux x64 addon is 10,005,872 bytes and fx-core.wasm is 2,615,841 bytes (measured from the installed package). You hand it a model, instructions and tools; it runs the model loop, streaming, tool calls, retries and steering. The 0.0.14 changelog leads with "libfx is now durable out of the box".
fx0 is a Next.js app (package.json version 0.0.10, Apache-2.0) that puts a chat beside a preview pane and uses libfx as its agent. It is about 14,600 lines of TypeScript across lib/, app/ and components/ (measured). Its README says it plainly: "fx0 is the app. libfx is the agent inside it."

What fx0 actually builds
The output is one HTML document. The agent has eight tools of its own in lib/tools.ts: read_design, publish_design, edit_design, web_fetch, ask_user_question, read_tool_result, capability_search and subagent, plus AI Gateway's provider-side web_search. publish_design takes a full document and tells the model to keep drafts under 12 KiB. edit_design takes up to 20 exact old_string/new_string replacements and applies them as one version. That is the same edit primitive coding agents use on source files, aimed at a single page.
lib/design-artifact.ts enforces what "pure HTML artifact" means:
- At most 512 KB (
MAX_DESIGN_ARTIFACT_BYTES = 512 * 1024), no external assets, and no<base>,<form>,<iframe>,<object>or<embed>. - The preview is a
sandbox="allow-scripts"iframe on an opaque origin, with a CSP injected right after the doctype:default-src 'none', inline scripts and styles only, images, fonts and media fromdata:only, andconnect-src 'none'. A generated page cannot make a network request.
That answers one reply's question, whether the result "needs fx0 around to run". It does not: it is one file you can copy or download. It also shows the ceiling. A page that needs an API, a form post or a real asset pipeline is out of scope by construction. Rauch said in a reply that you can layer on Vercel Sandbox, and that it was in the demo and taken out "for simplicity".
New chats start on anthropic/claude-opus-5.5-fast (lib/session.ts). Signed-in users bill AI Gateway to their own Vercel team through OAuth. Guests get five free prompts per IP on that model at low effort, funded by a server-side key behind BotID. Every turn stops after 64 model requests, subagents included. fx0 counts them in Redis, not in libfx, because, as its comment puts it, "libfx leaves step budgets to the host". The "200+ models" figure comes from the live Gateway catalog, which I did not query, so it stays reported. The picker code names model families, not model IDs.

The speed in the replies is mostly the model. One reply quotes "32ms to publish"; I could not find that figure in either repository. Vercel's own image shows a design read at 19 ms and an edit at 73 ms (reported). Those are tool calls against Redis. The seconds a user waits are tokens from Opus. libfx can do one thing about latency, and it does: when no worker holds the session, a prompt runs its turn in the request that sent it, so the first model call goes out with no queue hop.
Why a server-side agent loses its turn
An agent turn is a loop: call the model, run the tools it asked for, call the model again with the results, repeat until it stops asking. On a laptop that loop lives in one process for as long as it needs. On a serverless platform it does not:
- the function has a time limit, 800 seconds in fx0's routes (
maxDuration = 800); - a deploy replaces the process;
- the process can run out of memory or crash in the middle of a tool call.
Kill the process and you have lost the conversation in flight, the model calls already paid for, and, worst, the knowledge of whether a side-effecting tool finished. Re-running the whole turn re-pays every model call and may run a refund twice. Not re-running loses the reply. That is the "huge pain" in Rauch's post, and every agent product on serverless rebuilds some version of it.
How libfx makes a turn durable
libfx's answer is a write-ahead log per session. The libfx README states the rule: it "stores every prompt before it accepts it and every step of a turn as the turn runs, so the next process that picks up the session continues the turn where it stopped." Four mechanisms carry that sentence.
1. A session is a log in a Workflow World. The store is Vercel Workflow's World abstraction (sdk/durable/). On Vercel, vercel() uses Vercel's World and Vercel Queues; in plain Node, local() keeps files in a temp directory; in a browser, memory() keeps nothing past the page. A session ID is a Workflow run ID (wrun_…). turn.accepted resolves only after the prompt is stored, so a prompt the UI shows as sent cannot be lost. A messageId makes a retried request follow the original turn instead of starting a new one.
2. One writer, fenced. One worker holds a session at a time. A worker that was replaced without knowing it is refused at its next write, so "two never write at once". On a World with Workflow's in-band writer fence, the refusal happens in the store itself. Locally, local() checks whether the lease holder's PID is still alive with process.kill(pid, 0), so a crashed worker's session frees at once.
3. Every tool declares whether it may run twice. This is the one design decision an integrator has to make, and libfx refuses a tool that does not make it. A call to an idempotent: true tool that a crash cut off runs again, under the same executionId. Any other tool never runs twice on its own: the model gets an error saying the call may have partly run, and decides whether to check, retry or ask. The README's suggested pattern for a write you want retried is to pass executionId to the downstream service as an idempotency key and then mark the tool idempotent. fx0's split is in lib/libfx-agent.ts: read_design, publish_design, web_fetch, read_tool_result and capability_search re-run, while edit_design, ask_user_question and subagent do not. Publishing the same full document twice is harmless. Applying the same edit twice is not.
4. The model is told. A resumed turn adds a user message saying the session was interrupted. libfx does not pretend nothing happened; it makes the model aware so it can check before going on.
The durability is also specified, not only coded. tests/tla/durable/ holds seven TLA+ models (Fencing, Inbox, Journal, ParallelBatch, Resumption, SnapshotTail and ToolReplay), and tests/tla/sdk/ has Bun drivers that walk the real SDK code along the checked state graphs (measured, by listing the folders). I did not run TLC.
What I measured: killing libfx mid-turn
The README describes what should happen. I checked it. The harness (my code, in scratch) has three parts:
- a fake model: a Node HTTP server that speaks the OpenAI Chat Completions streaming format, wired in through libfx's custom-provider option. It asks for
get_order, thenrefund_order, then answers; - two tools that append each execution, with its
executionId, to a log file.get_orderisidempotent: true.refund_orderiswrites: true, and I ran it both ways; - a parent that starts a worker on
local()durability, waits until the worker is inside the step I want, sendsSIGKILL, then starts a fresh process that callssession.resume()on the same session ID.
Everything ran on a shared 4-core cloud VM, CPU only, under nice -n 19.
| Kill inside | Runs | Model requests seen | What the new process did | Resume time |
|---|---|---|---|---|
refund_order, not idempotent | 3 | 3 every time | never re-ran it; the model got a tool error | 113–185 ms |
refund_order, idempotent | 1 | 3 | re-ran it once, same executionId | 126 ms |
| model request #2 | 3 | 4 every time | re-sent the cut-off request; get_order re-ran in 1 of 3 | 135–284 ms |
get_order, idempotent | 9 | 3 in 5 runs, 4 in 4 | re-ran get_order every time | 125–259 ms in 4 runs, about 30 s in 5 |
All of it is measured. Five findings come out of the table.
Finished model calls are not paid twice. When the kill landed in a tool, the fake model saw exactly three requests across both processes, the same as an uninterrupted turn. Model requests #1 and #2 were replayed from the log into the new engine, not re-sent. When I killed the process while request #2 was in flight, that one request was sent again, for four in total. That is the right behaviour: the reply never reached the log.
A non-idempotent tool really is not re-run. In all 3 runs, refund_order executed once, in the killed process. The resumed model received this tool result, verbatim: "fx stopped while this tool was running, so it may have partly run. Check its effects before running it again." Followed by the user message "Resuming from unexpected session interruption." The decision whether to refund again goes to the model, which in a real deployment means it should have a way to check, such as a get_refund lookup.
An idempotent tool re-runs under the same executionId. With idempotent: true, the refund ran in both processes, and both logged executionId: call_refund_order. A payment API that honours idempotency keys would refund once. Note what that ID is: the tool-call ID the model emitted. It is stable because libfx stored the call before running a writes: true tool.
Idempotent tools run more often than you would guess. A read with idempotent: true and no writes "starts as soon as the model asks for it, while libfx stores the call" (libfx README). That overlap is a latency win. It also means a kill can land before the call is stored. In 4 of my 9 get_order kills the new process asked the model again, which is the README's documented behaviour. In 1 of 3 kills during model request #2, get_order ran a second time even though its result had already reached the model. Both are harmless for a lookup, which is exactly what the flag promises. Do not mark something idempotent because it is cheap.
Recovery is usually sub-second, but 5 of 9 lookup kills took about 30 seconds. The resumed turns ended in 113 to 284 ms, except for five get_order runs, which took 29.8 to 29.9 s. That number does not depend on whether the model was asked again. libfx has two 30-second constants in durable.js: the default reserve before a function's deadline, and retryEndWaitMs, "how long a retried turn waits for an end the log already holds". I did not trace which one this is, so it stays an observation on local(), not a diagnosis. It never lost the turn.
The local store for a three-step turn came to 33 to 37 KB on disk (measured).
On Vercel: where the minutes go
Locally, liveness is knowable: a dead PID is dead. On Vercel it is not, and that sets fx0's worst case. The constants below are reported in the READMEs, and the arithmetic is mine.
- A crash in the request that sent the prompt. "On Vercel, libfx can't tell a crashed function from a slow one", so that request holds the session until its 800-second limit. Vercel Queues keeps the prompt as a backstop due 5 seconds after that. A turn that crashes 20 seconds in therefore resumes 805 seconds after the send. That is 13 minutes 25 seconds, the README's "about 13 minutes" (reasoned). The earlier the crash, the longer the wait.
- A turn that just runs long. libfx stops it 30 seconds before the limit, keeps what it has done, and queues the rest to the
/api/libfxroute. A long turn spans several invocations without any crash. - A crash inside a queue delivery whose process exits. The delivery runs again about a second later.
- A delivery that stalls without exiting. It holds the message until the queue's hold runs out, up to five minutes.
- Opting into leases.
vercel({ leaseMs: 15_000 })renews a 15-second lease every 5 seconds, so a dead worker frees about 15 seconds after its last renewal. The cost: every prompt goes through the queue, and you give up the in-request start.
The default trades worst-case recovery for best-case latency, and the fx0 README says so in its Limits section. The page keeps showing the reply as running during the wait.

The wiring on fx0's side is small. One route, export const POST = agent().wakeHandler() with maxDuration = 800, and one experimentalTriggers entry in vercel.json subscribing it to the topic __libfx_wkf_workflow_session. The trigger makes the route private: only Vercel Queues can call it. fx0's README says it "has no code of its own to recover a turn". Most of fx0's remaining complexity is product, not recovery:
lib/turn-credential.tsseals each turn's funding in Redis, because a queue delivery has no browser cookie.lib/projector.tscopies libfx's event stream into fx0's own transcript, with a cursor check so each line lands once however many tabs follow it.lib/journal.tsmakes every Redis write a single Lua script, so it is atomic.
How this differs from v0, and from other harnesses
v0 is Vercel's hosted product; fx0 is a fork-it-yourself sample. Rauch's post says the plumbing was built "for v0". Neither repository shows v0's code, so whether production v0 runs on libfx today is his claim, not something I can check. What the code does show is the scope gap. fx0 makes one sandboxed HTML file, with no project, no npm and no deploy. A file with connect-src 'none' cannot call your API.
libfx is the agent loop, not a workflow engine. Vercel Workflow and similar durable-execution systems make your own code resumable, step by step. libfx sits on Workflow's World, but you write no workflow: the loop, its log and its replay rules are libfx's. You supply tools and their idempotency flags. That is a narrower contract, and an easier one to get right.
Other harnesses log for different reasons. The site's piece on DeepSeek Harness: an agent harness that refuses to send what it didn't log is about a log as a correctness check on what is sent to the model. libfx uses the log to survive a dead process. Cloudflare OS: approve the agent's writes after it has finished splits reads from writes for human approval. libfx splits idempotent from non-idempotent calls for crash replay. Same instinct, different failure. On the artifact side, json-render + Jev: generative UI in two model calls is another Vercel Labs take on generated UI, where a model chooses from your components instead of writing HTML. fx0's ten design.md styles come from VoltAgent's awesome-design-md, the same idea as the design vocabulary in Impeccable: a design vocabulary for agents is mostly a linter with opinions. Prompt caching in coding agents: where eight harnesses put the breakpoint is the other half of "don't pay for a model call twice".
What holds and what does not
- "Durable" holds at the level I could test: SIGKILL anywhere in a tool or a model request, and a fresh process finished the turn without re-paying a completed model call or re-running a non-idempotent tool. I tested
local(), notvercel(). On Vercel, the recovery delay is bounded by the function limit, up to 805 seconds for a crashed prompt request. - "Really fast" is the model and the in-request start. libfx adds no queue hop to an idle session's first call. Nothing in either repository benchmarks end-to-end time to a design.
- "Easy" holds for the wiring: one route and one
vercel.jsonline. The hard part moves to you, as it should: deciding which tools are idempotent, and giving the model a way to check a write it cannot see. - "Free" means the code is Apache-2.0. Model calls are billed through AI Gateway, either to your team or, for the five free prompts, to whoever runs the deployment.
- Complex pages are a fair complaint in the replies. One user posted a "digestive system" that did not come out as one. That is a model and a 512 KB, single-file format, not durability.
The interesting artifact here is not the HTML. It is a 0.0.x library whose tool interface makes "can this run twice?" a required field. A crash-safe agent comes down to answering that question for every tool.