Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/interop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,18 @@ jobs:
key: interop-bin-celld-0.6.3-rustfs-1.0.3
- run: pnpm run typecheck
- name: Node — a Flue agent app on cellb
run: pnpm --dir ../flue-example test
run: pnpm --dir ../../examples/flue test
- name: Node — the fake OpenAI-compatible model
run: pnpm --dir ../fake-openai test
run: pnpm --dir ../../examples/fake-openai test
- name: Node — pi and opencode agents on cellb, against the fake model
run: pnpm --dir ../agent-examples test
run: pnpm --dir ../../examples/agents test
- name: Node — celld conformance and interop
run: pnpm run interop
- run: pnpm exec playwright install --with-deps chromium
- name: Chromium — browser hosts on the shared bucket
run: pnpm run interop:browser
- name: Chromium — a Flue agent app on cellb in a browser
run: pnpm --dir ../flue-example test:browser
run: pnpm --dir ../../examples/flue test:browser
- if: failure()
uses: actions/upload-artifact@v4
with:
Expand Down
59 changes: 56 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,87 @@ The monorepo of **cellb** (`@statewalker/cellb`): an isomorphic Durable Objects
celld/Cloudflare-compatible surface. The same cells run in a browser (OPFS, ServiceWorker ingress),
in Node, and alongside celld on one shared bucket.

## What cellb does, next to celld

[celld](https://github.com/denoland/celld) runs Cloudflare Workers projects on a fleet of servers
that share one bucket. cellb runs the same projects unchanged, in Node and in browsers, answers the
same requests the same way, and stores cells in celld's format, so one cell can be served by celld,
then by cellb, then by celld again. celld is the judge: its examples, cellb's own probe projects and
workerd's Durable Object tests run on the real celld, are recorded, and are replayed against cellb in
Node and in Chromium (70 scenarios; 5 are judged by workerd instead, where cellb passes stubs by
reference as workerd does, and 6 are known differences, listed in
[`packages/cellb/docs/gaps.md`](packages/cellb/docs/gaps.md)).

| Feature | celld 0.6.3 | cellb, Node | cellb, browsers | How cellb is checked |
|---|---|---|---|---|
| Workers (stateless handlers) | supported | yes | yes | `hello`, `webapi`, `router`, `body` |
| Durable Objects / cells | supported | yes, same storage format | yes, same storage format | `counter`, `rpc`, `alarm`, workerd's tests; cells cross celld ↔ cellb |
| Durable Object facets | supported | yes | yes | `facets`, `facets-api` |
| KV | supported | yes (celld's code) | yes | `kv`, `kv-values` |
| Queues | supported | yes (celld's code) | yes | `queues`, `queues-api` |
| D1 | supported | yes (Miniflare's simulator) | yes | `d1`, D1 migrations |
| R2 | supported | yes, celld's bucket layout | yes | `r2`, `r2-api`, `r2-advanced` |
| Workflows | supported | yes (celld's engine) | yes | `workflow`, `workflow-api` |
| Cron triggers | supported | yes | yes | `cron` |
| Static assets | supported | yes (with Cloudflare's behaviour where celld has bugs) | yes | `static-assets`, `assets-api` |
| Dynamic Workers (Worker Loader) | supported | yes, a worker thread each | yes, a Dedicated Worker each | `dynamic-worker-tails`, the Worker Loader scenarios |
| WebSockets, hibernation | supported | yes | yes, over MessagePorts | `wsecho`, `wsclient`, `wd-websocket-*` (celld fails `wd-websocket-hibernation`, cellb passes it) |
| Vectorize-style search | example | yes | yes | `vectordb` |
| Workers AI (`ai` binding) | no binding | yes, on an OpenAI-compatible endpoint the host names | yes | `p14-workers-ai`, Flue's `cloud` agent |
| Python Workers (Pyodide) | supported | no, out of scope | no | not run |
| Containers | experimental (Docker or Podman) | no; reported in `unsupported` | no | not run |
| Agent frameworks: Flue, pi, opencode | examples (`pi`, `opencode` need the network) | yes, offline against a fake model | Flue | [`examples/flue`](examples/flue), [`examples/agents`](examples/agents) |

Where cellb follows Cloudflare or workerd instead of celld, and what still differs, is in
[`packages/cellb/docs/celld-compatibility.md`](packages/cellb/docs/celld-compatibility.md).

## Layout

```
packages/cellb/ the runtime, its browser host, conformance corpus and docs (see its README)
packages/flue-example/ a Flue agent app (Cloudflare target, Agents SDK) run on cellb
examples/flue/ a Flue agent app (Cloudflare target, Agents SDK) on cellb, in Node and Chromium
examples/agents/ pi and opencode in Durable Objects on cellb, against the fake model
examples/fake-openai/ a scripted OpenAI-compatible model endpoint the examples test against
docs/superpowers/ plans for work on the repository
.github/workflows/ interop CI against the real celld and RustFS
```

`packages/*` are libraries; `examples/*` are private apps that use them, with the test tools only
they use. Both are workspaces
(`pnpm-workspace.yaml`), so an example depends on `@statewalker/cellb` and
`@statewalker/fake-openai` through `workspace:*`.

## How to run it

1. `pnpm install`
2. `cd packages/cellb && pnpm test`: the unit tests (`test/`) and the celld end-to-end tests
(`e2e/`), which skip without `CELLD_BIN`.
3. `pnpm exec playwright test`: the Chromium end-to-end suite.
4. `cd packages/flue-example && pnpm test`: builds a Flue app with Flue's own Cloudflare build and
4. `cd examples/flue && pnpm test`: builds a Flue app with Flue's own Cloudflare build and
runs it on cellb against a fake model endpoint; `pnpm test:browser` runs the same app in Chromium.
5. `pnpm run interop`: the celld interop suite. `scripts/interop-env.sh` downloads the pinned celld
5. `cd examples/agents && pnpm test`: pi and opencode, each in a Durable Object on cellb, against
the fake model.
6. `pnpm run interop`: the celld interop suite. `scripts/interop-env.sh` downloads the pinned celld
and RustFS releases and starts RustFS on `127.0.0.1:9200`.

`pnpm test` at the root runs every workspace's tests, and stops at the first one that fails.

## Why it is the way it is

- **One package, three entry points.** Callers differ by platform (isomorphic, Node, browser), not
by internal module, so `@statewalker/cellb`, `/node` and `/browser` are the seams.
- **CI runs against the real celld.** celld is the conformance judge, so the interop workflow
downloads the pinned celld and RustFS and runs the Node and Chromium interop suites on every pull
request.
- **Examples run offline.** Agent frameworks need a model; the examples point theirs at
`@statewalker/fake-openai`, so CI needs no key and no network beyond the package registry.

## What will surprise you

- A green `pnpm test` without `CELLD_BIN` says nothing about celld compatibility: those tests skip.
- **cellb's `pnpm test` passes every test and still exits 1**: workerd's vendored Durable Object
tests leave `assert.rejects(…)` unawaited, and the rejections reach the Node process the Worker
shares (`test/p09-celld-conformance.test.ts`). The root `pnpm test` stops there; run an example's
tests from its own directory.
- The opencode example's first load takes about half a minute: its SDK bundles to about 430,000
lines.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ model (`@statewalker/fake-openai`), so the whole thing runs offline once the pac
|---|---|---|
| `pi/` | [pi](https://pi.dev)'s agent loop (`@earendil-works/pi-agent-core`, `@earendil-works/pi-ai`) | a prompt runs a tool call and its follow-up; the transcript persists in the object's storage |
| `opencode/` | opencode's workerd SDK (`@opencode-ai/sdk/workerd`) | celld's opencode example: the SDK boots on the object's SQLite, creates a session, and the model answers a prompt |
| `../flue-example` | Flue (`@flue/runtime`) | a whole Flue app, built for Cloudflare, on Node and in browsers |
| `../flue` | Flue (`@flue/runtime`) | a whole Flue app, built for Cloudflare, on Node and in browsers |

The package is private: it is a test of cellb, not a library.

Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

A scripted OpenAI-compatible model endpoint for running agents offline: `POST /v1/chat/completions`
(streamed or not, text or tool calls), `POST /v1/embeddings` and `GET /v1/models`. The package is
private: the agent examples and the Flue example test against it.
private: the agent examples and the Flue example test against it, so it lives with them in
`examples/`.

## Why it exists

Expand All @@ -28,7 +29,7 @@ await fake.close();
From a shell, for `celld dev` or a manual try (Node 22.18 or later runs the TypeScript directly):

```sh
node packages/fake-openai/src/cli.ts 8787 # http://127.0.0.1:8787/v1
node examples/fake-openai/src/cli.ts 8787 # http://127.0.0.1:8787/v1
```

## Examples
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env node
// Runs the fake model on its own, for `celld dev` or a manual try:
// node packages/fake-openai/src/cli.ts [port]
// node examples/fake-openai/src/cli.ts [port]
// It answers with the default script (see `defaultReply`) and logs each request.
import { defaultReply, lastUserText, startFakeOpenAi } from "./index.ts";

Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
4 changes: 2 additions & 2 deletions packages/flue-example/README.md → examples/flue/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ module, and a Dedicated Worker (`browser/flue-worker.ts`) hosts it with `browser

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
`../cellb/docs/gaps.md` for the numbers).
`../../packages/cellb/docs/gaps.md` for the numbers).

The core of it, against the build:

Expand Down Expand Up @@ -128,7 +128,7 @@ CORS preflights, since the worker's origin is the Vite server's) and a fresh OPF
- **`run vite build first (pnpm test does)`** is what `vitest run` alone says without `dist/`.
- **The browser test's Chromium** is `PW_CHROMIUM`, else Playwright's own, else the one
`@sparticuz/chromium` unpacks (as in `packages/cellb`).
- What cellb still lacks for Agents SDK apps is in `../cellb/docs/gaps.md`.
- What cellb still lacks for Agents SDK apps is in `../../packages/cellb/docs/gaps.md`.

### Dependencies

Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { expect, type Page, test } from "@playwright/test";
import { shardOf } from "@statewalker/cellb";
import { bundleWorkerForBrowser } from "@statewalker/cellb/node";
// Not exported: where a Durable Object id lands decides which tab's shard runs it.
import { idFromName, namespaceKey } from "../../../cellb/src/core/id.ts";
import { idFromName, namespaceKey } from "../../../../packages/cellb/src/core/id.ts";
import { startFakeLlm } from "../fake-llm.ts";
import { whoamiScript, wrongIdentities } from "../whoami.ts";

Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion packages/cellb/docs/browser-host.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ code and builds the runtime from the wrangler configuration. The code is either:
globals read what the runtime installs. `bundleWorkerForBrowser(entry, { nodeCompat })` from
`@statewalker/cellb/node` builds it: `cloudflare:workers` with the project's own `env` and
`exports`, `cloudflare:email`, and with nodejs_compat the `node:*` polyfills (bare names such as
`"path"` too). A Flue app built for Cloudflare runs this way (packages/flue-example); or
`"path"` too). A Flue app built for Cloudflare runs this way (examples/flue); or
- `{ site }` — the base URL of an in-browser HTTP server made by `createProjectSite` and hosted by
`@statewalker/webrun-site-host`, which compiles the project's sources on request
(`@statewalker/webrun-modules`). Loaded Workers' code is uploaded to it and imported from URLs.
Expand Down
2 changes: 1 addition & 1 deletion packages/cellb/docs/celld-compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ identical, how it is checked, and where the two still differ. The pinned celld v
| Workers AI (`ai` binding) | no binding (the `pi` example builds its own adapter) | yes, on an OpenAI-compatible endpoint the host names | yes | `p14-workers-ai`, Flue's `cloud` agent |
| Python Workers (Pyodide) | supported | no, out of scope ([gaps.md](gaps.md)) | no | not run |
| Containers | experimental (Docker or Podman) | no; reported in `unsupported` | no | not run |
| Agent frameworks: Flue, pi, opencode | examples (`pi`, `opencode` need the network) | yes, offline against a fake model | Flue | `packages/flue-example`, `packages/agent-examples` |
| Agent frameworks: Flue, pi, opencode | examples (`pi`, `opencode` need the network) | yes, offline against a fake model | Flue | `examples/flue`, `examples/agents` |

## The same request gets the same answer

Expand Down
2 changes: 1 addition & 1 deletion packages/cellb/docs/gaps.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ Dedicated Workers all work in Firefox 157, and the whole suite passes there. Wha
- **celld examples not run:** `python` (Pyodide Python Workers: cellb runs no Python), `wasm`
(needs the Rust toolchain to build), `container` and `sandbox` (Docker or Podman; a `containers`
config is reported in `unsupported`). `pi` and `opencode` need a model on the network as celld
ships them; `packages/agent-examples` runs both, adapted to take their model from an endpoint in
ships them; `examples/agents` runs both, adapted to take their model from an endpoint in
their vars, against a fake OpenAI-compatible model.
- **No build step.** The entry points are TypeScript sources. A bundler, Vite or vitest resolves
them; plain `node` fails with `ERR_MODULE_NOT_FOUND` for `src/…/*.js`.
Expand Down
2 changes: 1 addition & 1 deletion packages/cellb/e2e-browser/agents-sdk-host.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {

// What the Agents SDK (and Flue, built on it) needs from a browser host, in one project:
// `env` and `exports` of cloudflare:workers, cloudflare:email, and the nodejs_compat modules it
// imports (packages/flue-example runs a whole Flue app the same way).
// imports (examples/flue runs a whole Flue app the same way).
const worker = `
import { DurableObject, env, exports } from "cloudflare:workers";
import { EmailMessage } from "cloudflare:email";
Expand Down
6 changes: 1 addition & 5 deletions packages/cellb/src/node-compat/async_hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,7 @@ export class AsyncLocalStorage<T> {
}
exit<R>(fn: (...args: unknown[]) => R, ...args: unknown[]): R {
if (native)
return variableOf(this as AsyncLocalStorage<unknown>, native).run(
undefined,
fn,
...args,
);
return variableOf(this as AsyncLocalStorage<unknown>, native).run(undefined, fn, ...args);
const frame = new Map(state.current);
frame.delete(this as AsyncLocalStorage<unknown>);
return runIn(frame, fn, undefined, args);
Expand Down
2 changes: 1 addition & 1 deletion packages/cellb/test/p13-agents-sdk-host.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import {
} from "./loaded-scope-worker.js";

// What the Cloudflare Agents SDK (and Flue, built on it) needs from a host beyond the
// Durable Object API itself; packages/flue-example runs a whole Flue app.
// Durable Object API itself; examples/flue runs a whole Flue app.

const worker = `
import { DurableObject, env, exports } from "cloudflare:workers";
Expand Down
Loading
Loading