In 1869 William Stanley Jevons — logician and economist — built a "logical piano": a wooden machine with a keyboard that mechanically eliminated inconsistent combinations of logical terms as you pressed premises into it. Feed it propositions, read the face, get conclusions. The decision contract hasn't changed in 150 years. What changed is where the machine lives.

Today the same shape — state in, calibrated answer out — is a hosted API: TypeSafe's Jev, part of their System One family, takes a state plus a map of typed questions and returns typed answers with probabilities and confidence. It is a genuinely good contract. It is also a remote API you pay per call, with someone else's model weights on the other side.
This post is about our version of that contract — a stateful logic model and a small language called LRL (Logic Request Language) that turns individual questions into compiled, branching decision programs. It runs on a gaming laptop's Vulkan GPU, trains from JSONL files you write yourself, and keeps a recurrent hidden state so that question N in a program is asked with questions 1…N−1 still in memory.
The contract: state + questions → scored answers
Every decision reduces to one record:
{"id":"doc-01234","family":"doc_lookup","qtype":"choice",
"state":"<48 lines of a markdown spec>",
"question":"which heading opens this section?",
"options":["Grammar","Semantics","Interfaces","Testing"],
"answer":1} Three question types cover most decision surfaces:
| Type | Semantics | Head | Used for |
|---|---|---|---|
CHOICE | exactly one option is right | softmax + cross-entropy | routing, classification, next-action pick |
NOUL | each option independently true/false; bare NOUL = boolean probe on [FALSE, TRUE] | sigmoid + BCE | multi-label sets, yes/no gates, membership checks |
SCORE | position on an ordered rubric | distribution over levels | severity, quality bands, thresholding |
A third head emits a scalar confidence alongside the answer, so a downstream pipeline can abstain instead of guessing — and in the current model, abstention is the most polished part of the stack: 60/60 abstention hits on the eval set, and on unanswerable probe questions it returns UNKNOWN at ~0.99 confidence rather than confabulating.
How a decision is actually computed
The model is a stacked GRU — same family as our language models, read differently:
Two properties fall out of this shape. First, each option is scored in the context of the full state — the fork means scoring option 3 never disturbs option 4's view of the world. Second, the state is encoded once: inside a program walk, the post-STATE hidden vector is checkpointed and reused, so the Nth question costs one question's worth of forward pass, not a full re-read.
The shipped v6 model: 3 GRU layers, hidden 1536, token embedding 512, 4,123-token vocabulary — about 4,090 learned BPE merges plus 33 special tokens, 29 of them the LRL keywords (<NODE>, <GATE>, <WHEN>… grammar structure is a single-token signal to the model). About 40 million parameters, deliberately laptop-scale.
LRL: programs made of questions
A single ask is a lookup. LRL turns asks into procedures — a decision program is a text file you can diff, version, and unit-test:
// verbatim from the L7 eval suite — a record's meta.lrl (dialogue-needle family) NODE a0 ASK NOUL "did <spk1> mention the rune?" OF <STATE> NODE a1 ASK NOUL "did <spk0> mention the rune?" OF <STATE> NODE g GATE OR(a0, a1) EDGE a0 -> a1 EDGE a1 -> g EDGE g.true -> <END> EDGE g.false -> <END>
Two model asks, fused by a pure-logic gate, then truth-selector edges that lower to guards (<spkN> are speaker-turn markers in the record's dialogue state). The vocabulary is small on purpose:
| Statement | Meaning |
|---|---|
NODE x ASK … OF <STATE> | ask the model a question over a bound state; OPTIONS [...] inline or resolved from state |
NODE g GATE op(...) | pure boolean logic — AND/OR/XOR/NAND/IMPLIES/NOT — over earlier results; no model call, no noise |
EDGE a -> b [WHEN guard] | conditional routing; .true/.false/.unknown selectors lower to implicit guards |
REQUIRE CONF … | confidence thresholds — a node can route differently when the model is unsure |
GEN QUESTION … | emit new questions instead of answering — programs that generate their own training data |
The gate is the point that matters: a GATE node's outcome decides whether the walk checks the roof hatch or the path — the program branches on model answers, but the branching itself is exact logic. Neural where you need judgment, symbolic where you need guarantees.
Compiled, not interpreted
logic run and logic make go through the real pipeline — lexer → AST → optimization passes → LBC bytecode → VM — with compiled blobs disk-cached by content hash. The passes do real work — normalize (bare ASK NOUL → [FALSE,TRUE]), fold gates whose inputs already resolved to constants, dead-branch prune — and codegen adds one more: state-encoding CSE. If an ASK's only in-edge is an unconditional edge from a same-state ASK, no Encode op is emitted; the VM (and interpreter) restores the predecessor's checkpointed post-STATE hidden instead.
The AST interpreter is kept permanently, not as scaffolding. --no-jit runs it, and logic eval --suite --walk executes every program on both engines and diffs paths, answers, and engine divergence — on the current suite that divergence counter reads 0/327.
Programming it: three patterns
1. A pure gate probe — three model asks fused by exact logic:
NODE arith ASK NOUL "computed the arithmetic correctly" OF <STATE> NODE steps ASK NOUL "carried every step of the calculation" OF <STATE> NODE tally GATE AND(arith, steps) EDGE arith -> steps EDGE steps -> tally EDGE tally.true -> <END> EDGE tally.false -> <END>
$ cat_chat logic run probe.lrl --config run.toml
{
"reached_end": true,
"path": ["arith","steps","tally"],
"results": [
{"node":"arith","truth":"TRUE","answer":1,"conf":0.94,...},
{"node":"steps","truth":"TRUE","answer":1,"conf":0.87,...},
{"node":"tally","truth":"TRUE","skipped":false}
]
} 2. Branching on answers — the walk only visits nodes whose edges' guards pass:
NODE key_check ASK NOUL "has_item_red_key == true" OF <STATE> EDGE key_check -> chip_check WHEN key_check // key found → check chip EDGE key_check -> <END> // no key → stop NODE chip_check ASK NOUL "has_item_data_chip == true" OF <STATE> EDGE chip_check -> <END>
3. Programs that write their own training data — GEN nodes plus logic make emit labeled records; unanswered needs_teacher slots are filled by cat_chat teacher --fill-gen:
$ cat_chat logic make prog.lrl --config run.toml --output recs.jsonl
$ cat_chat teacher --fill-gen recs.jsonl # teacher fills GEN templates
{"id":"mk-4bfa41a1-00000","family":"lrl_gen","qtype":"choice",
"state":"known: has_item_red_key=false",
"question":"Is the red key present on the shelf?",
"options":["The red key is absent","The red key is present","I don't know"],
"label_source":"teacher","answer":1,
"meta":{"node":"fwd3","kind":"gen_forward","labeled_by":"teacher"}} Provenance tiers keep the data honest: procedural (labels computed by code), teacher (a larger model filled them), model (self-labeled — confidence targets masked so it never trains on its own agreement), and needs_teacher templates in flight.
Using it alongside Jev
The request shapes are deliberately compatible. A Jev system_one call decomposes onto our surface almost mechanically:
| Jev / System One request | Logic model equivalent |
|---|---|
"state": <text> | record state / <STATE> bound by --state |
choice + criteria map | ASK CHOICE "q" OF <STATE> OPTIONS [...] |
noul (+ optional true/false criteria) | ASK NOUL "q" OF <STATE> (bare → [FALSE,TRUE]) |
score + ordered level array | ASK SCORE … OPTIONS [...] |
confidence field | per-result conf scalar |
| many questions per request, parallel | many ASK nodes per program — but sequential, with state carried between them |
Our reference client is already in the tree: scripts/lib/logic/jev_quine.py builds an autoregressive next-character LM entirely out of Jev API calls — each request carries all prior output as state, and its --quine mode emits the text that describes the program's own source. Swap the HTTP call for a logic run invocation (or a direct LogicModel scorer binding) and the same trick runs locally, free per call.
- Jev: zero training data, general-domain questions, want calibrated answers today, comfortable with a remote API and per-call billing.
- Logic model + LRL: you have a domain (game states, docs, support tickets), want to train on your data, need branching/sequential programs, need it local and offline, or want the model inside a game loop or build pipeline.
Current results — v6 on the L7 test set
The v6 checkpoint (10 epochs, 21,539 train / 2,625 val records, ~13.4 GPU-hours on an RTX 3080 Laptop) scores 48.6% accuracy on 2,540 held-out records — up 7.4 points over v5 on identical test data, and much better calibrated (ECE 0.079 vs 0.33). The scorer itself now runs on the same GPU path as training, which makes the eval numbers the same numbers production asks would produce.

| Slice | Result | Reading |
|---|---|---|
| Choice accuracy | 48.6% | +7.4pp vs v5 on identical test records |
| ECE / Brier | 0.079 / 0.224 | calibrated — conf means what it says |
| NOUL opt-acc / F1 | 80.1% / 0.805 | multi-label discrimination is strong |
| Abstention | 60/60 | every UNKNOWN predicted was a true unknown |
| Suite walk_end | 53.5% | model walks that reach <END> (of 327 programs) |
| Engine parity | 0/327 | JIT and AST interpreter agree everywhere |
The honest weak spots are visible in the radar: story (31%), game (38%), numeric (40%), pathfinding (39%). Two of those were diagnosed as data bugs, not model limits — the generator never asked "fewer" questions (the model learned a degenerate polarity rule), and only ~1.5% of training records carried more than 8 options (the model never saw wide-option discrimination). Both are fixed in the L8 dataset, which is staged for the v7 run. Story/dialogue-needle weakness is a different beast: those train/test distributions already match and the records are genuinely hard — facts buried under ~700 characters of filler — which points at a retention-capacity limit, not a data shape.
The precision ladder
Training runs entirely on Vulkan cooperative-matrix tensor cores, with a five-rung precision/memory ladder measured against the CPU reference:
| Config | GEMM | Activation cache | rel L2 vs FP32 | Cache bytes/elem |
|---|---|---|---|---|
fp32 | scalar FP32 | FP32 | ~0 | 4 |
mixed + int8 | FP16 tensor cores | INT8 | 8.7e-3 | 1 |
split + int8 | paired-FP16 (~22-bit) | INT8 | 8e-4 | 1 |
f16 | scalar FP16 | FP16 | 4.2e-4 | 2 |
split + fp16 | paired-FP16 (~22-bit) | FP16 | 4.2e-4 | 2 |
The split path is worth its own war story: the NVIDIA driver folds every floating-point f32→f16→f32 round-trip into a no-op, so the low-order residual had to be computed in the integer domain (floatBitsToUint → round-to-nearest at bit 13 → uintBitsToFloat), and the tiny residual scaled into fp16's normal range so tensor cores don't flush it as a subnormal. The result is a ~22-bit-precision GEMM on fp16 hardware — int8-cache memory footprint, near-fp32 gradient quality.
Where it goes next
- v7 on L8 — done: trained and scored (49.9% test acc, ECE 0.074 at the epoch-4 checkpoint) on a half-sized 19.4M model; v8 on the 96,506-record L9 corpus is in progress. See the scaling follow-up.
- Retention-horizon probe: a graduated needle-depth eval family to map where recall actually decays — turning "capacity limit" from a binary verdict into a measurable curve that sizes the fix (deeper TBPTT vs. attention hybrid vs. wider hidden state).
- Split-FP16 in the LM trainer: port the integer-domain-residual GEMM to the shared LM training path — same hardware, same silent error class.
- Violation-set forensics: make fixed-seed violation-index diffing a first-class harness feature — the diagnostic that separated GEMM error from cache noise is currently manual.
- Option-lane memory work: wave chunking / segment checkpointing so 64-option records fit alongside larger batches.
Caveats and limitations
- Numbers are measured on one machine — RTX 3080 Laptop GPU, 8 GB VRAM, Vulkan backend. The precision-ladder rel_L2 figures are against our own CPU reference on a small parity fixture, not a published benchmark.
- 48.6% is a research-model score, not a production claim. The L7/L8 test sets are procedurally generated with teacher-filled slots; real-domain accuracy will differ and a domain model should be trained on domain data.
- v7 was trained after this post shipped — it scored 49.9% test acc (ECE 0.074) at half v6's parameters; details and the L9 corpus build are in the follow-up post.
- Story and needle-recall weakness is a capability question we have diagnosed but not solved; the fix is architectural (retention), not data volume.
- The Jev comparison is contract-level: our request shape follows the same idea (state + typed questions → calibrated answers) but this is not a wire-compatible Jev client —
jev_quine.pyis the actual TypeSafe client; the logic model is a separate local implementation. Jev is a hosted commercial API from TypeSafe AI; calibration, pricing, and availability are theirs. - GPU suite scoring is slower than the old CPU-sharded path for LRL program walks — n=1 GPU forwards are dispatch-bound.
--suite-cpuremains available when wall-clock matters more than matching the production ask path. - The piano photograph is CC BY-SA 2.0, not public domain — the machine itself (1869) is PD; the photo is permissively licensed and used here with attribution.
Related: Scaling the Logic Model (L9, v5→v8) · Vulkan ML Compute · FP16 Accumulators on GRU-L3 · The v5 Pretrain Roast · Bench · jev-sdk (TypeSafe Jev client)
