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.