tokenling #
tokenling, a GPT-2 pet that runs entirely in your browser.
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.
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.
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 and web/world.ts:
## 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 |
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/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 |
web/app/ |
typed state, save format/storage, body simulation, care, animations, conversation, diary, model client, game, PWA and lifecycle controllers; see the module map in docs/architecture.md |
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, web/world.ts |
pixel LCD, sprite icons and the Python-compatible prompt format |
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, mirrored by web/world.ts |
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 |
full fine-tune, with loss on the pet's own words and tool calls only |
train/eval_pet.py, train/quant_eval.py |
behavioral eval through the app's lookup loop; quantization quality |
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/check_onnx.py |
greedy-decode check of an export against torch |
test/parity.mjs, test/parity_states.py |
JS/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 rules and save compatibility, including a save captured from the previous app |
test/browser-flow.js |
repeatable playwright-cli browser regression flow against a local debug preview |
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 type-checks,
bundles the app and worker with esbuild, then fills the static host template.
No server is needed at runtime. See docs/deployment.md.
Quick start #
npm ci # Node.js 24 or newer
npm test # types, parity, UI contract, body/save and data rules
npm run format:check # consistent TS/TSX formatting
node test/parity.mjs # JS 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.