a tiny pet with a GPT-2 brain, living in your browser
README.md

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.