~/satyajit

fx0: the artifact builder is the demo, the durable agent loop is the product

mdjsonmcp

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:

vercel-labs/fx0@8541d8f · snapshot 2026-10-23
tracked files
183
license
Apache-2.0
branch
main
tests
37 files
source
1.2 MB
commit date
2026-10-22
source by language
TypeScript665.2 kB(107)JavaScript529.8 kB(40)CSS11.0 kB(1)SQL2.6 kB(5)

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

vercel-labs/fx@d824f60 · snapshot 2026-10-23
tracked files
1,784
license
Apache-2.0
branch
main
tests
958 files
source
38.8 MB
commit date
2026-10-23
source by language
Zig29.8 MB(661)TypeScript6.3 MB(223)JavaScript1.9 MB(176)Python720.1 kB(53)Shell61.0 kB(24)HTML35.7 kB(5)C8.8 kB(3)

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."

Block diagram. Inside a box labelled 'fx0, a Next.js app on Vercel', the chat and live preview talk to API routes, which write to Redis (chat and live design) and Neon (accounts and history). The API routes hand off to a black 'libfx agent, runs every turn' box below, which calls AI Gateway (billed to your team), saves to Workflow World (saves every step), and is connected by a dashed line to Vercel Queues (restarts a crashed turn).
fx0 owns the chat, sign-in, tools and history; libfx runs each turn, saves every step to Workflow World, and Vercel Queues restarts a turn whose function died (fx0 README, 'How fx0 uses libfx' diagram).

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:

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.

Promotional image: on the left, 'Open-source artifact builder, Built on libfx'. On the right, the fx0 interface: a chat where the user asks to add a featured collection below the hero, tool rows reading 'read design 19ms done' and 'edit design 73ms done', 'design published version 4', and token counts '7.4s · 18k in · 3k out'; beside it, a preview of a minimalist store page titled 'objects for everyday living.'
The fx0 interface as Vercel presents it: a read and an exact-replacement edit, each timed, then a new design version. The timings are Vercel's own screenshot, not a measurement of mine (fx0 repository, app/opengraph-image.png).

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:

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:

Everything ran on a shared 4-core cloud VM, CPU only, under nice -n 19.

Kill insideRunsModel requests seenWhat the new process didResume time
refund_order, not idempotent33 every timenever re-ran it; the model got a tool error113–185 ms
refund_order, idempotent13re-ran it once, same executionId126 ms
model request #234 every timere-sent the cut-off request; get_order re-ran in 1 of 3135–284 ms
get_order, idempotent93 in 5 runs, 4 in 4re-ran get_order every time125–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.

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.

Flow chart. You send a message; 1. fx0 saves your message; 2. fx0 seals your token, so your own team pays; 3. libfx runs the turn, saving every step as it goes (highlighted). A solid arrow goes to 4. You see the reply as it's written. A dashed branch labelled 'the function crashes' goes to 5. Vercel Queues starts a new function that picks up at the last saved step, then 'later, nothing lost' back to step 4.
One fx0 message from send to reply; the dashed path is the crash case (fx0 README, 'Durability' diagram).

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:

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

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.

Cite this article

For attribution, please use the following reference or BibTeX:

Satyajit Ghana, "fx0: the artifact builder is the demo, the durable agent loop is the product", ai.thesatyajit.com, October 2026.

bibtex
@misc{ghana2026fx0,
  author = {Satyajit Ghana},
  title  = {fx0: the artifact builder is the demo, the durable agent loop is the product},
  url    = {https://ai.thesatyajit.com/articles/fx0},
  year   = {2026}
}
share