An ambient ideation canvas where words condense out of latent space. See SPEC.md for the concept and vocabulary; dewpt-demo.html is the static reference implementation whose look and interaction behavior this build preserves.
This is M1 (live field): seed input, Worker + per-session Durable Object, pool generation via Workers AI, drip client, evaporated sidebar.
npm install
npm run dev # wrangler dev on http://localhost:8787The AI binding runs remotely ("remote": true), so wrangler login must have
happened once on this machine. Probe it with:
curl http://localhost:8787/api/debug/ai # {"ok":true,...} means generation will workWARP users: workerd (the
wrangler devruntime) cannot open outbound connections while Cloudflare WARP intercepts its sockets — the AI binding fails withInferenceUpstreamError: Network connection lostand the probe above hangs. Pause WARP (or exclude workerd in split-tunnel settings) while developing. As a last resort,echo 'DEV_FAKE_AI=1' > .dev.varsswaps in a canned offline generator (src/dev-fake-ai.ts) so the field machinery can be exercised without egress. Production generation is Workers AI only.
Generation and embedding can run against any OpenAI-compatible server — Ollama,
llama.cpp's server, LM Studio, vLLM — by pointing .dev.vars at it:
LOCAL_AI_BASE_URL=http://localhost:11434/v1
GEN_MODEL=qwen2.5-coder:7b
EMBED_MODEL=bge-m3/api/debug/ai reports which backend answered ("mode":"local"), so the probe
stays honest once you switch. LOCAL_AI_API_KEY is there for servers that want
one; Ollama ignores it. This is a dev convenience — production is Workers AI,
and these vars live only in .dev.vars, never in wrangler.jsonc.
Two things that bite:
- Thinking models return nothing. They spend the whole
max_tokensbudget onmessage.reasoningand hand back an emptycontent, which parses to zero candidates and a field that never fills. AddLOCAL_AI_CHAT_OPTIONS={"reasoning_effort":"none"}— measured onqwen3.5:4b, that turns 0 candidates into 8. Ollama silently ignores boththink: falseandchat_template_kwargs.enable_thinking; onlyreasoning_effortworks there. - Embeddings are persisted per session. Vectors live in the DO's SQLite, so
changing
EMBED_MODELmid-session mixes incompatible dimensions into the cosine scoring. Start a new session after a model change.
WARP blocks workerd's egress to localhost too, so this is not a way around
that trap — it is a way to stop needing a Cloudflare account and an Access token
to develop.
npm test # vitest: pool logic + generation prompt/parsing (AI mocked)
npm run typecheck # tsc over src/test and scriptsIterate on generation quality without touching the app — edit the prompt and few-shots in generation.ts, then eyeball three labeled batches (strangeness 0.2 / 0.5 / 0.85, altitude 0.3):
CLOUDFLARE_ACCOUNT_ID=… CLOUDFLARE_API_TOKEN=… \
npm run calibrate -- "security awareness people actually enjoy"
# options: --model=@cf/… --count=24The token needs the Workers AI — Read permission. Calibrate goes through the Cloudflare REST API, so it works even where workerd egress doesn't (WARP).
- Worker (src/index.ts) —
/apiroutes; static client served from public/ via Workers assets (run_worker_first). - SessionDO (src/session-do.ts) — one per session (the URL hash is the session). SQLite persistence, alarm-driven generation pump. Serving never waits on generation.
- PoolCore (src/pool-core.ts) — pure pool logic: 6 buckets (3 dewpoint tiers × 2 altitudes, mirroring the demo's pools), fresh-first draws, lazy invalidation on pins/param changes, embedding cosine dedupe (> 0.92), exclude LRU (~300), evaporated ring buffer (20).
- Generation (src/generation.ts) — prompt building
(the model hears "strangeness", never the weather vocabulary), few-shot
examples per strangeness band seeded from the demo pools, robust JSON
parsing, embeddings via
@cf/baai/bge-m3. - Client (public/field.js) — the demo's field machinery verbatim (spawn/decay timing, depth, tier colors, pulse-on-prospect, pin-to-condense, reduced-motion), fed by per-bucket local buffers (public/pool-client.js) that refill in the background. An empty buffer skips a spawn tick; it never blocks.
- Teaching surface (docs/ui-teaching-research.md)
— slider endpoint labels + screen-reader glosses, a progressive hint line
that advances as the user acts (pure state machine in
src/hint-machine.ts, mirrored for the browser in
public/hint-machine.js; a
localStoragetaught flag quiets it for returning users), and a non-modal?legend for recall. - Explainer surface (docs/explainer-research.md)
— before a seed exists the field explains itself by demonstration: a
centered manifesto plus meta-words about dewpt condensing and evaporating
through the real spawn/decay motion (public/preseed.js,
static pool in public/preseed-pool.js; zero
network, torn down the moment a session starts). A
? what is this?toggle in the header opens a non-modal about panel — concept, weather glossary, and the Kevin Kelly credit — that never pauses the field.