a tiny pet with an even tinier brain, living in your browser
README.md

subtokenling #

subtokenling, a language-model pet that can live on your device or a trusted server.

A small Tamagotchi-like creature whose brain is a fully fine-tuned GPT-2 (the original 124M model). The brain makes the decisions: it chats, toddles around its room, reacts to pats, decides whether it wants the snack, bath, medicine or game of catch you offer, wants things, naps and dreams, remembers things about you and writes a diary page at bedtime. The page only runs its body (hunger, energy, mess) and its room. It grows from baby to kid, teen and adult in real time.

In local mode, there's no server or API: the egg hatches once the page has downloaded the brain (about 97 MB, 4-bit) into the browser's Cache Storage, and it runs on the visitor's device with transformers.js on WebAssembly. The pet, its chat, memories, gifts and diary live in localStorage. It installs as an app, works offline, and can send alerts the owner turns on (no push server: the page plans them and its service worker shows them). New brains are offered as an upgrade; compatible older brains can stay in use. After a prompt-format change the pet dozes until its new brain is downloaded, keeping its save.

Choose local or remote when you get an egg, and switch either way later in settings. The optional Rust host stores a remote pet’s private save and runs its body and brain while the browser is closed. The browser keeps a recovery checkpoint. Transfers preserve memories, gifts, chat, diary and birth time. Remote mode shares that data with the chosen host, so choose one you trust. Private access keys come first; ATProto sign-in and multiplayer are later work.

Private smaller-brain experiments now train genuine GPT-2/Pythia warm starts and a standard Llama scratch candidate, with strict masks, browser-aligned behavioral evaluation and actual q4/q8/fp32 exports. No replacement is selected or published; the inherited model remains the baseline. Explicit family/cache contracts do not make the tools universal. See docs/extending.md for the source contracts and behavior, quality, size/speed/memory and browser proof required before publishing another brain.

Both modes run the same rules. The prompt contract, body, care, the brain's tag effects, turns, diary and saves are one Rust crate (core/), linked natively by the host and compiled to WebAssembly for the page.

The pet's tools #

GPT-2 was fine-tuned to call tools inline in its own lines. The app parses them as they stream:

Tag Effect
[emote:happy|love|sad|sleepy|wave|dance|spin|blush|grumpy|wow|giggle|cry] sprite animation
[want:snack|play|bath|sleep|pats|medicine] thought bubble
[walk:left|right] walks a few steps
[yes], [no] answers an offer (* Sam offered Mochi a cookie *); only [yes] feeds, bathes, medicates or starts catch
[remember:FACT] saves a memory about you
[gift:flower|pebble|star|leaf|shell|heart|drawing|feather] puts a gift on your shelf
[diary:NOTE] jots a note for tonight's diary
[clock], [look], [recall:KEYWORD], [yesterday] lookups: generation stops, the app appends {result}, and the model continues the same line
[sleep] falls asleep
[dream:X] dreams of an allowed sprite while asleep

A line of only tags (Mochi: [walk:left] [emote:happy]) is the pet acting on its own; the app asks for one every few seconds.

Every prompt starts with a state header, rendered identically by train/world.py (training) and core/src/world.rs (the app and host):

## tokenling chat
pet: Mochi · kid · day 3 · curious, foodie
owner: Sam
now: 21:40 night, rainy
needs: tummy hungry, fun happy, energy sleepy, clean
remembers: Sam likes frogs; Sam has a cat named Bo
today: ate a cookie; played catch (4 stars); had a bath
---
Sam: what time is it?
Mochi: [clock]{21:40 night} it's so late!! [emote:sleepy] [want:snack]

Documentation #

AGENTS.md Maintainer contract for humans and coding agents: invariants, rules, commands. Start here before changing anything.
docs/architecture.md Modules, the pet's turn, where the model comes from
core/README.md The shared Rust rules: what lives there, the WebAssembly build and native/browser parity
server/README.md Rust host setup, private API, transfers, workers and server checks
docs/prompt-format.md The header, event lines, tool grammar and lookups; the contract with the weights
docs/pet-simulation.md Life cycle, stats, care actions, minigame, diary, storage schema, LCD
docs/training.md Teacher data, validation, fine-tuning, evals, the run receipts
docs/small-model-research.md Smaller-brain research and the private experiment ladder
docs/export-and-runtime.md ONNX export, 4-bit quantization, transformers.js runtime, why not WebGPU
docs/host-page.md The shipped page and the markup, config and CSS classes the app needs
docs/deployment.md What to publish, server requirements, shipping and rolling back a model, sharing GPUs
docs/testing.md Parity test, evals and playwright-cli recipes
docs/extending.md Adding emotes, tools, foods, stages; what needs retraining
docs/gotchas.md Symptoms, causes and fixes for the sharp edges

Layout #

Path Contents
web/main.tsx, web/components/ Preact entry and typed UI components: device/setup, chat, diary, shelf, settings, brain notice/dialog and debug panel
core/, core/wasm/ shared Rust rules (prompt contract, body, care, tags, turns, diary, saves) and their WebAssembly bindings; core/wasm/build.sh writes core/wasm/pkg/ (out of Git)
web/core.ts, web/app/engine.ts typed access to the WebAssembly rules; the browser's engine and its read-only store.state snapshot
web/app/ typed state, browser storage, animations and effects, care actions, the turn runner, diary writer, model client, game, PWA and lifecycle controllers; see the module map in docs/architecture.md
web/app/hosting.ts, web/components/Hosting.tsx authenticated remote sync, both transfer directions, local recovery and the settings choice
server/ Rust/Axum/SQLite host linking core/, native ONNX workers and ownership/API/parity tests
server/src/routing.rs bounded timing estimates, queue completion planning and background aging
web/types.ts, web/runtime-types.ts save, scene and worker message types; the vendored runtime API boundary
web/brain.ts model worker; bundled separately as brain.js
web/scene.ts, web/sprites.ts pixel LCD and sprite icons
package.json, package-lock.json, tsconfig.json pinned Preact, TypeScript, esbuild and Prettier dependencies; strict type checking
site/index.html, site/assets/ the page (a template with @PLACEHOLDERS@), its font, web app manifest and icons
site/sw.js service worker: offline copies of the page, app and runtime; alerts
site/build.sh, site/bundle.mjs, site/release.json builds the static site from the release description (current and older models)
site/make-icons.mjs draws the app icons in site/assets/
train/world.py prompt format, tool results and event lines for training; core/src/world.rs renders the same strings
train/gen_data.py, train/teachers.sh synthetic chat/diary data from a teacher model, validated tag by tag
train/migrate_v6.py migrates old chat headers and sleep events, rebuilds loss spans and replaces old teen rows
train/mix_data.py reuses an older dataset's rows that the current format still accepts
train/train.py Generic causal-model fine-tuning with strict pet-only masks, compatible masked-logit distillation or explicit language pretraining
train/eval_pet.py, train/quant_eval.py Behavioral eval through the app's lookup loop, optional source-bound semantic review, and quantization quality
train/semantic_data.py, test/semantic_rules.py Frozen training/development/sealed scene isolation, real shared-engine factual persistence and conservative meaning-boundary regressions
train/export.sh, train/quantize.py ONNX export and 4-bit quantization for WebAssembly
train/vendor.sh, train/publish.sh pinned browser runtime and static publishing
train/prepare_model.py, train/model_data.py Native/Pythia-prefix/domain/Llama preparation, depth/vocabulary reduction and canonical masks/splits/hashes
train/experiments.py, train/experiments.json Pinned private candidate matrix, synthetic curriculum/pet-brain rollouts, training/export/eval orchestration and evidence-gated speed/quality comparison
train/check_onnx.py, train/onnx_utils.py Actual Torch/ONNX numerical and cached-generation checks; explicit GPT-2/NeoX/Llama/GQA metadata
docs/small-model-research.md Smaller-brain research, candidate calculations and separately labelled screening evidence
test/experiment_rules.py, test/experiment_bench.py Acceptance-boundary regressions and optional genuine-graph Chromium timing/memory driver
test/pwa-cache-flow.js Legacy cache bridge, install metadata revalidation and offline/missing-manifest flow
test/parity.mjs, test/parity_states.py WebAssembly core/Python prompt parity
test/data_rules.py training-contract and migration regression cases, including a stand-in sleep teacher
test/site_tools.py stdlib regressions for safe build/publish failures, immutable artifacts and preview runtime refresh
test/contract.mjs checks the Preact component ids, host mount/config contract and docs
test/body.test.mjs, test/storage.test.mjs, test/fixtures/save-v1.json body, engine and save rules through the WebAssembly build, including a save captured from the previous app
test/browser-flow.js repeatable playwright-cli browser regression flow against a local debug preview
test/hosting-flow.js real host creation, lost response, closed-page execution, both transfers and offline recovery
server/tests/native.rs, test/host-startup.py opt-in real native CPU/GPU/private-HTTP checks and actual CPU fallback at startup
test/dev-site.sh, test/serve.py local preview: builds the site, stages the runtime and models and serves them with COOP/COEP headers
test/bench.html brain load time, speed and memory in a real browser (included as unlinked bench.html in every site build)

The frontend uses Preact and strict TypeScript. site/build.sh builds the shared rules to WebAssembly, type-checks, bundles the app and worker with esbuild, then fills the static host template. Building needs Node.js 24+ and Rust 1.88+ with the wasm32-unknown-unknown target and the pinned wasm-bindgen-cli (see core/README.md). Local mode needs no server at runtime; remote mode uses the optional Rust host. See docs/deployment.md and server/README.md.

Quick start #

npm ci                               # Node.js 24 or newer
npm test                             # WebAssembly rules build, types, parity, UI contract, engine/save and data rules
npm run format:check                 # consistent TS/TSX formatting
node test/parity.mjs                   # the shared rules and Python prompts agree
node test/contract.mjs                 # app, reference page and docs agree on element ids

site/build.sh /tmp/site                # the static site (add models/ and runtime/ with train/publish.sh)

# preview in a browser (needs an exported model and the vendored runtime):
MODEL=$DATA/release/tokenling-v6 RUNTIME=$DATA/release/runtime test/dev-site.sh /tmp/dev-site
# -> http://127.0.0.1:8765/pet/

Nothing in the scripts is tied to a particular machine: inputs are arguments or environment variables, and each script's header lists them. Datasets, checkpoints and exports ($DATA) stay out of Git. Training uses one Python environment with torch and CUDA ($VENV), and ONNX export uses a second one ($EXP, see docs/export-and-runtime.md). The full rebuild, from data to a published model, is in docs/training.md, docs/export-and-runtime.md and docs/deployment.md. Published model and runtime directories are immutable: ship a new model id, update site/release.json, then build and deploy.

License #

MIT. Copyright (c) 2026 Ava Chaney.

The bundled Nunito font retains its SIL Open Font License.