@statewalker/flue-example
A Flue agent app, built with Flue’s own Cloudflare build and run on
CellB instead of Cloudflare. One agent, Assistant, has one tool; its model is a fake
OpenAI-compatible endpoint, so the whole thing runs offline once the packages are installed. The
package is private: it is a test of CellB, not a library.
Why it exists
Flue builds each agent as a Durable Object on Cloudflare’s Agents SDK. That is a large, real Worker
that CellB did not write: it reads env from cloudflare:workers while its module is evaluated,
imports cloudflare:email and bare Node.js built-in names, writes SQL with internal names in string
literals, and commits several times within one event. CellB’s own tests check each of these alone
(packages/cellb/test/p13-agents-sdk-host.test.ts); this app checks them together, as a real
Agents SDK app uses them.
How to use it
pnpm install # in examples/ (their own workspace; cellb from npm); needs network
pnpm test # in this directory: vite build, then vitest run (Node)
pnpm test:browser # vite build, then Playwright in Chromium
vite build runs flue() and @cloudflare/vite-plugin and emits dist/flue_example/: a Worker
module, one generated Durable Object class per agent, and a wrangler.json. The tests load that
output with loadNodeProject from @statewalker/cellb/node and talk to it with Flue’s own client,
@flue/sdk.
Examples
What test/flue-on-cellb.test.ts proves:
- The app loads with nothing in
unsupported. A prompt is admitted, runs from the agent’s alarm, calls the tool, and the reply and the history read back from the agent’s SQLite. - A reply is observed live over SSE while two conversations run at once.
- A node stops in the middle of a model call. A second node on the same fleet bucket, with no local state, takes the cell over, and Flue’s recovery finishes the submission there.
- The
cloudagent (modelcloudflare/@cf/meta/llama-3.3-70b-instruct-fp8-fast) answers through Flue’scloudflareprovider: both model turns go throughenv.AI.run(), which CellB sends to theaiendpoint, with the Workers AI id mapped to the endpoint’s model.
What test/agents-websocket.test.ts proves with a plain Agents SDK app (test/agents/, bundled as
wrangler does): over WebSockets, onConnect and onMessage run, each connection keeps its own
state (partyserver keeps it in the socket’s attachment), and broadcast reaches every socket.
What test/browser/flue-in-browser.spec.ts proves, with the same build run in Chromium:
bundleWorkerForBrowser (from @statewalker/cellb/node) turns dist/flue_example/ into one ES
module, and a Dedicated Worker (browser/flue-worker.ts) hosts it with browserProjectRuntime
(SQLite WASM on OPFS) and drives it with @flue/sdk, inside the browser:
-
A prompt runs through the tool call to the reply, and the history reads back; the
cloudagent answers throughenv.AIas well. -
A reply is observed live over SSE.
-
Two conversations run at once (their model calls interleave), and each one’s stored history holds its own question and answer only.
-
A second runtime on the same OPFS pool (a page reload) reads the first conversation back.
-
Nothing is logged as a warning or an error on the way.
-
With 8 and with 32 conversations at once, each conversation’s
whoamitool reads its own conversation from Flue’s context (getDurableObjectIdentity(), anAsyncLocalStorage), before and after anawait; the Node suite checks the same with the realnode:async_hooks. -
Four messages sent to one conversation at once all settle, none is lost.
-
32 conversations run over 1 and over 4 workers with CellB’s
startProjectShards(browser/flue-shard-worker.tsis the worker): each worker hosts the app with its own OPFS pool and owns its share of the conversations, and requests enter through every worker. -
Two tabs share the app (
browser/tabs.html: CellB’sjoinProjectShardsand a ServiceWorker runninginstallProjectRouter): its 4 shards split 2 / 2, both tabs converse through the ServiceWorker at once, and when one tab closes the other takes its shards and the conversations stored in them. The app mounts its routes underBASE_PATH(/app/<name>), as the ServiceWorker passes requests on with their path. -
The Agents SDK chat app of
test/agents/shared by two tabs (bundled withbundleWorkerForBrowser, routed underAGENTS_PREFIX): browser WebSockets from both tabs (createWebSocketClass(serviceWorkerConnector(...))) reach its agent, which welcomes them and broadcasts. With the ServiceWorker stopped, open sockets keep working (they run page ↔ shard worker) and a new one wakes it. When the agent’s tab leaves, its sockets close with1001at once and reconnect to the agent in the other tab.
Concurrent conversations in one browser runtime take turns: it is one thread, and each Flue turn
makes 21 SQLite commits on OPFS there. Spreading them over workers runs them in parallel (see
../../packages/cellb/docs/gaps.md for the numbers).
The core of it, against the build:
import { createFlueClient } from "@flue/sdk";
import { loadNodeProject, nodeSqliteOpener } from "@statewalker/cellb/node";
import { MemFilesApi } from "@statewalker/webrun-files-mem";
const { runtime } = await loadNodeProject(dir, { openSql: nodeSqliteOpener(), files: new MemFilesApi() });
const client = createFlueClient({
url: "http://app.test/agents/assistant/conv-1",
fetch: (input, init) => runtime.fetch(new Request(input, init)),
});
const reply = await client.read(await client.send({ message: { kind: "user", body: "What is the answer?" } }));
Internals
Everything runs on this machine
The fake model (test/fake-llm.ts) listens on a port the OS picks; the fleet bucket is a
memoryObjectStore() per test; each test copies the build to a temporary directory and points its
LLM_BASE_URL var at the fake. Nothing is shared between runs, so parallel runs in other worktrees
do not collide, and nothing needs celld or RustFS. The browser test uses the same fake (it answers
CORS preflights, since the worker’s origin is the Vite server’s) and a fresh OPFS pool per run.
Decisions a reader would otherwise undo
- Two providers. The app registers its own OpenAI-compatible provider (
local,src/app.ts);flue({ providers: ["cloudflare"] })adds Flue’scloudflareprovider, which calls the Workers AI binding (aiinwrangler.jsonc, migrationv2for thecloudagent). On CellB that binding is served by the host’saiendpoint; the tests point both at the fake model. - The model URL comes from
env, read at module evaluation. That is how Flue apps configure providers, and it is the path CellB’s per-projectcloudflare:workersenvexists for. vite.config.tsis for the build only;vitest.config.tsis separate. With the Cloudflare plugin loaded, vitest tries to run the tests in workerd.src/cloudflare-workers.d.tstypes onlyenv. It is the one part ofcloudflare:workersthe app reads; pulling in@cloudflare/workers-typeswould clash with the DOM types the tests use.
What will surprise you
No API key for provider: local. pi-ai’s OpenAI client refuses an empty key even for a keyless endpoint; the provider resolves a placeholder.observe()reportsabsentwhen it is called before the conversation exists: send first.run vite build first (pnpm test does)is whatvitest runalone says withoutdist/.- The browser test’s Chromium is
PW_CHROMIUM, else Playwright’s own, else the one@sparticuz/chromiumunpacks (as inpackages/cellb). - What CellB still lacks for Agents SDK apps is in
../../packages/cellb/docs/gaps.md.
Dependencies
@flue/runtime, @flue/vite, @flue/sdk (pinned together: the build and the client must agree),
@earendil-works/pi-ai (Flue’s model layer), hono (the app’s router), @cloudflare/vite-plugin
and wrangler (Flue’s Cloudflare build), @statewalker/cellb (the runtime under test),
@playwright/test, @sparticuz/chromium and @sqlite.org/sqlite-wasm (the browser test).
License
MIT