The applet boundary (new in v3)
v2, asked an app idea with no system message and no grammar, answered in prose — 0/4. v3 answers
with an applet — 4/4 — while still answering all 8 off-topic prompts (jokes, arithmetic,
capitals, recipes) in prose, 8/8. The paired presentations plus the negatives taught the
boundary in both directions: this request is an applet and this one is not.
The nine rare components: honest status
The goal of the 49 donor applets was to teach spontaneous use of nine components the v2 model never
reached for (Slider, Icon, Spacer, Scroll, Store, QRCode, Graph, Image, Load). That goal was not
met. On nine purpose-written probes (one per component), v3 reaches for the requested component
in 0–2 of 9 cases, about the same as v2 — except Load, which neither model ever emits unprompted.
What did change is that the components are reliably reachable when the request names them:
Table with columns: System message, Slider, Graph, Load, Icon| System message | Slider | Graph | Load | Icon |
|---|
| none | ✓ | ✓ | ✓ | ✓ |
| short one-liner | ✓ | ✓ | ✗ | ✓ |
| the full 437-char brief | ✓ | ✓ | ✗ (reads "Load" as a verb, makes a button) | ✗ (picks Image) |
And the probes also show the v3 contribution where it is measurable — staying valid on prompts the
older model fails to close at all (validity on the nine probes, same harness):
Table with columns: System message, v2, v3| System message | v2 | v3 |
|---|
the brief, 0.6 | 6/9 | 9/9 |
none, 0.6 | 0/9 | 8/9 |
Practical advice: name the component in the user prompt when you want it
("… drag a ui.Slider to set it"). The model then uses it, and the file stays valid (4/4 tested,
checked with and without the brief).
Files
Table with columns: File, What| File | What |
|---|
deal-applet.gbnf | the GBNF grammar every number above was measured with |
adapter_model.safetensors, adapter_config.json, chat_template.jinja | the LoRA adapter (rank 32) — checkpoint 72 (epoch 1), the weights all v3 numbers were measured on and the GGUF was merged from; merge it yourself for fp16 weights |
gguf/qwen-deal-v3-q8_0.gguf | 8.1 GB — the artifact the numbers were measured on |
gguf/qwen-deal-v3-f16.gguf | 15.2 GB — the exact merge the Q8 was quantized from; requantize from this |
How to run (llama.cpp — macOS, Linux, Windows)
brew install llama.cpp # or build llama.cpp from sourcellama-server \ -m qwen-deal-v3-q8_0.gguf \ --grammar-file deal-applet.gbnf \ --ctx-size 8192 --n-gpu-layers 99
One request per applet: the brief, then the idea.
curl -s http://127.0.0.1:8080/v1/chat/completions -H 'content-type: application/json' -d '{ "messages": [ {"role": "system", "content": "You write DEAL applets: one .dealui source per app, holding its views, a state {} seed and update {} blocks. Output exactly one fenced code block labeled dealui and nothing else. DEAL has no ternary operator — branch with if/else inside an update block; no method calls — use functions such as length(list), total(list, \"field\") and fixed(value, 2); comparisons are === and !==. Every action a view dispatches needs its own update block."}, {"role": "user", "content": "a tally of the books I read this month"} ], "max_tokens": 4096, "temperature": 0.6, "top_p": 0.95, "dry_multiplier": 0.8, "dry_base": 1.75, "dry_allowed_length": 8, "repeat_penalty": 1.05}'
Those are the sampler values the numbers were measured at. max_tokens 4096 beside --ctx-size 8192
is deliberate: the brief is 437 characters, so the context is spent on the answer.
What the numbers mean
Validity is parser + checker + property lint, zero findings, run by the host implementation this
dialect belongs to — i.e. "the host would run this file", not "the file is correct". Everything
was measured with llama.cpp b11573, q8_0, one seed pinned per prompt, on the 27 held-out
prompts, the 12 prompts written after the dataset was frozen, and 9 component probes. The boundary
numbers (8 off-topic, 4 app ideas + 4 app ideas for v2) were measured with the adapter on the base
model in 4-bit, no grammar and no system message, so they show the weights' own preference, not the
decoder's shape.
Validity is not correctness. The grammar and the checker hold the shape — types, the allowed
constructs, "every dispatched action has its update". They cannot hold semantics. Failures that
survive into valid files include: an id derived from a list count (id: length(items) + 1) reused
after a removal, so a later remove hits the wrong record; fetched Load data persisted into
state although the fetch is meant to be re-run, not stored.
Two shape failures recur at both samplers and are worth a guard in your host: an identifier repeated
past the point of the applet into the token budget (finish_reason: length — the grammar does not
bound identifier length; cap ident with {m,n} if you meet it), and — in the tightest sampler —
view syntax emitted inside an update block, which the checker reports as
expected 'path = expression' … but found 'ui.…'.
Training
QLoRA (4-bit NF4) + LoRA rank 32 over a 570-example train set: 265 applets (216 v2 applets + 49 new
donor applets exercising the nine rare components), each presented twice — once with a full, terse
or short system message, once with none or a fragment — plus 40 off-topic negatives tagged to be
answered in prose (expect: chat). Valid 54, test 27 (unchanged from v2, so the 27-prompt score is
comparable). Loss on the assistant turn only, teacher examples validated by the same parser +
checker + property lint before admission. Three epochs, 216 steps; the eval loss curve
(0.308 / 0.355 / 0.414) turned after the first epoch, so checkpoint 72 was merged and shipped.
License
Apache-2.0, following the base model (Qwen2.5-7B-Instruct,
Alibaba Cloud). The adapter weights are released under the same terms.