# tokenling **tokenling**, a GPT-2 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](https://huggingface.co/openai-community/gpt2) (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](server/README.md) 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. 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/`](core/README.md)), 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`](AGENTS.md) | Maintainer contract for humans and coding agents: invariants, rules, commands. Start here before changing anything. | | [`docs/architecture.md`](docs/architecture.md) | Modules, the pet's turn, where the model comes from | | [`core/README.md`](core/README.md) | The shared Rust rules: what lives there, the WebAssembly build and native/browser parity | | [`server/README.md`](server/README.md) | Rust host setup, private API, transfers, workers and server checks | | [`docs/prompt-format.md`](docs/prompt-format.md) | The header, event lines, tool grammar and lookups; the contract with the weights | | [`docs/pet-simulation.md`](docs/pet-simulation.md) | Life cycle, stats, care actions, minigame, diary, storage schema, LCD | | [`docs/training.md`](docs/training.md) | Teacher data, validation, fine-tuning, evals, the run receipts | | [`docs/export-and-runtime.md`](docs/export-and-runtime.md) | ONNX export, 4-bit quantization, transformers.js runtime, why not WebGPU | | [`docs/host-page.md`](docs/host-page.md) | The shipped page and the markup, config and CSS classes the app needs | | [`docs/deployment.md`](docs/deployment.md) | What to publish, server requirements, shipping and rolling back a model, sharing GPUs | | [`docs/testing.md`](docs/testing.md) | Parity test, evals and `playwright-cli` recipes | | [`docs/extending.md`](docs/extending.md) | Adding emotes, tools, foods, stages; what needs retraining | | [`docs/gotchas.md`](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` | 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` | 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`](core/README.md)). Local mode needs no server at runtime; remote mode uses the optional Rust host. See [`docs/deployment.md`](docs/deployment.md) and [`server/README.md`](server/README.md). ## Quick start ```sh 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`](docs/export-and-runtime.md)). The full rebuild, from data to a published model, is in [`docs/training.md`](docs/training.md), [`docs/export-and-runtime.md`](docs/export-and-runtime.md) and [`docs/deployment.md`](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](LICENSE). Copyright (c) 2026 Ava Chaney. The bundled Nunito font retains its [SIL Open Font License](site/assets/OFL-nunito.txt).