nanoJEV
September 20, 2026 · View on GitHub
A minimal "System One Model" trained with RLCD (Reinforcement Learning for Calibrated Decisions), inspired by nanoGPT's minimal style.
This implements the core ideas behind TypeSafe AI's Jev model (released Sep 15, 2026).
Positioning: not "a small LLM", but an experiment in stripping the many language-generative decisions out of an LLM agent and replacing them with a low-latency, probabilistic, abstain-able, batch-executable Decision Runtime. See docs/ for the full problem/solution review this build implements.
nanoJEV vs TypeSafe Jev(实测)
两者都是决策模型:输入文本,输出带校准置信的类型化决策 + noul 弃权,
而不是生成字符串。不 同之处在形态——下面全部是实测数字,Jev 列来自
classifier.dev 的独立测量(src/vs-jev.json,n=400×2,2026-09),
nanoJEV 列来自本仓库的训练与评估(docs/10/11)。
| 维度 | TypeSafe Jev(hosted API) | nanoJEV(自托管) |
|---|---|---|
| 形态 | zero-shot:调用方每次给标签 | 可训练:RLCD + outcome-aware 加权 |
| 标签空间 | 调用方任意给定 | 训练出的子 schema(意图 210 / 动作 17 / score)+ dynamic 任意候选文本 |
| 单标签准确率 | AG News 87.5% · emotion 61.8%(zero-shot,n=400) | MASSIVE+CLINC 意图 72.9%(训练域)· lcqmc within-tol 50.2% |
| 多标签 | F1 0.887 @ 232ms | 混合批次 232 标签 @ ~37ms/2 条 |
| 延迟 | ~2.2ms/item(API) | 5–47ms/item(本地 MPS 单次前向) |
| 成本 | $0.004–0.005/1k(API) | 自托管,边际 ≈ $0 |
| 校准 | 置信分层有效(sure 90.6% vs unsure 65.3%) | ECE 0.136 · risk@70 = 11.9%(最有把握的 70% 决策错误率)· noul F1 0.649 |
| 弃权 noul | 输出未覆盖输入的概率 | 同样输出 noul,且 System 2 Gate(noul/熵/margin/OOD 多组件)决定升级 |
| 训练 | 不可训练(hosted) | 网页可训练/中断(docs/10),outcome-aware 加权(fix.md ①) |
| 适用 | 快速接入、任意标签集的 hosted 推理 | 数据/标签私有、需要自训练与 Agent 内嵌的场景 |
诚实声明:两列的准确率不是同一基准——Jev 测的是 AG News/emotion (zero-shot),nanoJEV 测的是它训练的 MASSIVE+CLINC/lcqmc 域。可比的是 形态(决策模型 vs 决策模型)、校准行为与部署特征;Jev 在它测试过的 公开基准上更强,nanoJEV 的位置是自托管、可训练、面向 Agent 运行时。
Three Primitives
Jev supports three decision primitives, all implemented in nanoJEV.py:
- Choice — categorical distribution over K options (+ 1 "noul" abstain slot)
- e.g.
{"billing": 0.08, "technical": 0.85, "sales": 0.07}
- e.g.
- Score — scalar prediction with calibrated uncertainty (Gaussian)
- e.g.
value=0.72, std=0.03
- e.g.
- Noul — abstention ("I don't know")
- When the model isn't confident, it outputs noul instead of guessing
- Also forced when the input is unreadable: mostly
<unk>tokens (e.g. Chinese text on the English-only CLINC model), guarded by the per-decisionoov_ratefield
RLCD Training Method
RLCD = Reinforcement Learning for Calibrated Decisions. Three pillars:
- Decisions — output is a fixed-schema choice/score, not a token sequence
- Calibrated — predicted probabilities must match empirical frequencies (ECE → 0)
- Reinforcement Learning — REINFORCE policy gradient with calibration reward
The loss function:
L_RLCD = CE(choice) # decision loss
+ w_score · GaussianNLL(score) # score primitive loss
+ w_calib · ECE(confidence) # calibration loss
+ w_reinforce · REINFORCE # RL term
Quick Start
# Install dependencies (torch is the only hard dependency)
pip install -r requirements.txt
# Train the DEFAULT model: multi (docs/10) — ONE model over
# unified (EN+ZH intent routing) + agent (state-view policy) +
# agentlog (simulated agent-session routing, offline); serving + web UI are
# built around it (task-type dropdown). ~55MB data download on first run.
python nanoJEV.py
# Experiment tasks (optional — serving does not need them)
python nanoJEV.py --task synthetic # offline synthetic routing (~30s)
python nanoJEV.py --task clinc # EN: 150-intent routing + out-of-scope abstention
python nanoJEV.py --task lcqmc # ZH: question-pair matching score (~5-10 min)
# With ablation comparison
python nanoJEV.py --task synthetic --ablation # CE vs RLCD (basic)
python nanoJEV.py --task synthetic --ablation extended # CE / CE+LS / CE+focal /
# CE+ECE / RLCD, each raw and with post-hoc temperature
# Dynamic candidate sets (docs/08): candidates are scored BY TEXT via a shared
# encoder — arbitrary candidate subsets at inference, not a fixed K+1 head
python nanoJEV.py --task clinc --dynamic
python nanoJEV.py --task unified --dynamic --n_candidates 9
# Offline agent-runtime state-view task (docs/06): key=value state in,
# next action out, contradictory states abstain
python nanoJEV.py --task agent
python nanoJEV.py --task agent --dynamic --n_candidates 5
# Domain-shift axis (docs/07): hold the travel domain out of training
python nanoJEV.py --task clinc --holdout_domain travel
# ONE model carrying several tasks' training effects (docs/10)
python nanoJEV.py --task multi --dynamic --epochs 20 --dropout 0.1
python nanoJEV.py --task multi --multi_tasks unified,agent,agentlog
# Class-weighted noul for oos-scarce tasks (docs/02)
python nanoJEV.py --task clinc --noul_weight 5
# Custom config
python nanoJEV.py --epochs 50 --d_model 256 --device cuda
Each training run saves to its own unique directory,
models/<task>-<UTC timestamp>/ (e.g. models/clinc-20260918-123000/),
containing the checkpoint model.pt (state dict + tokenizer + config), a
human-readable config.json sidecar, and a live metrics.jsonl — one JSON
line per event (step losses, periodic train/test evals, status) appended as
training progresses, which is what the web 训练监控 tab polls to chart a run
in real time. The directory is never overwritten by a later run. --out_dir
moves the root elsewhere (default models/). The *.pt checkpoints are
gitignored; only the tiny config.json sidecars are tracked, as a diffable
record of what was trained.
At the end of every run the trainer fits a post-hoc temperature on the
train tail (never the test set), reports ECE before → after,
prints a binned reliability table, and stores the temperature in the
checkpoint (applied by serve.py at inference).
Web Inference UI (serve.py)
A single-file, stdlib-only web UI + API on top of a trained model (the
only dependency is still just torch):
python serve.py # serve the latest run in models/
python serve.py --ckpt models/clinc-20260918-123000 --port 8000 --host 0.0.0.0
Start it in a second terminal while a training runs and open the
训练监控 tab to watch the loss curve, accuracy, ECE and noul metrics grow
live (kakeya-style: the trainer is its own process writing metrics.jsonl
into the run directory; the web UI only reads those files every 2.5s).
Features (Chinese UI, dark theme):
- 单条 / 批量推理 — one query or one-per-line batch; a batch is decided in a single parallel forward pass (decisions/s shown)
- 完整决策视图 — top-9 + noul probability bars over the full K+1 softmax distribution, calibrated confidence meter, per-decision latency
- noul 可视化 — abstention is a first-class output: amber noul bar, dashed verdict card when the model abstains (with an explicit reason when the abstention was triggered by out-of-vocabulary input)
- conf_threshold 滑块 — re-judges the last decision instantly client-side
(same rule as
decide(): abstain whennoul_prob > best_choice_prob, when the calibrated top-choice probability is below the threshold, or when the input's OOV rate ≥oov_noul_rate), no re-inference needed - 类型化 Decision JSON — the exact
Decisiondataclass fields, shown and returned by the API — software can branch on it directly - 训练监控 — a second tab charts any run under
models/live while it trains: loss components (CE / calib / REINFORCE / score NLL) per step, train/test accuracy and ECE per eval, noul recall/precision or score RMSE, the warmup+cosine learning rate, and a progress bar — pure embedded SVG, no chart library. Runs can also be deleted from here, and the inference tab's badge strip has a picker to delete any run undermodels/(the currently served one is preselected and marked; both paths confirm first, and a run that still looks active needs a second force-confirm, so a live trainer's directory isn't removed by accident) - 动态候选集 checkpoints —
--dynamicruns are served over the full candidate set (label encodings precomputed once per load; smaller batch cap); the checkpoint's fitted temperature is applied to every decision, fixed-head checkpoints included - 运行切换(每个任务响应自己的置信) — the badge-strip model manager gains
a 使用 button: switching reroutes
/api/decideto that run, so an lcqmc model answers with its own Gaussian (score ± std) pair, intent/action models with their calibrated distributions — instead of everything being answered by whichever run was newest at startup - 网页训练与中断 — the 训练监控 tab gains a start form (task dropdown,
epochs / dropout / d_model) posting to
POST /api/train/start(spawns a detachednanoJEV.pyprocess, log undermodels/train-*.log) and a 中断训练 button (POST /api/train/stop, SIGTERM + metrics markedinterrupted) for web-started runs - Task-adaptive: a
clinccheckpoint gets the intent-routing UI, anlcqmccheckpoint gets the question-pair Score UI,agent/agentlogget the action-routing UI,multithe mixed-decision UI; synthetic checkpoints (vector input) are rejected with instructions. Train the demo model withpython nanoJEV.py --task clinc, then startpython serve.py.
HTTP API:
GET /api/meta model/task metadata (task, K, vocab, params, device, ...)
POST /api/decide {"text": "..."} -> one typed Decision
{"texts": ["...", ...]} -> batch (max 200)
pair tasks: {"text_a": ..., "text_b": ...} / {"pairs": [[a,b],...]}
optional "conf_threshold": 0.0-1.0 (default 0.5)
GET /api/train/runs every run under models/ (task, status, progress, last acc)
GET /api/train/metrics ?run=<name> -> step loss curve + eval points, chart-ready
DELETE /api/train/runs ?run=<name> -> delete a run directory (409 + force=1 if
it still looks active)
Each decision carries the typed fields choice / choice_label /
choice_prob / noul_prob / is_noul / confidence / score / score_std
/ oov_rate plus the full sorted probability vector and token count.
For choice tasks confidence is the calibrated top-choice probability:
the softmax over the K+1 slots is the distribution RLCD calibrates, so
p(top choice) is the model's calibrated P(this decision is correct) — and the
abstain threshold applies to exactly that signal. (The auxiliary confidence
head is the calibration signal only for score-only tasks, where the training
set is too large to memorize; on small choice tasks it collapses to ~1.0 on
the memorized training set and would make threshold abstention useless.)
Language support: the default unified task trains one router on
English AND Chinese (MASSIVE's two locales are sentence-aligned over a
shared 60-intent label space). A text model can still only read its fitted
vocabulary, so input in an untrained language (Japanese, Korean, …) — or any
mostly out-of-vocabulary text — force-abstains (is_noul, oov_rate ≈ 1.0)
with the UI explaining why, instead of returning a fabricated confident
intent.
Tasks & Real Training Data
--task selects the training data:
| Task | Data | Language | Size | Primitives exercised |
|---|---|---|---|---|
unified (default) | MASSIVE 1.1 (EN+ZH, 60 intents) + absorbed CLINC150 (EN, 150 intents) + LCQMC questions as Chinese oos | EN + ZH | 45,194 train / 12,448 test (incl. 3,100 oos as noul) | Choice + Noul |
synthetic | Generated vectors + hyperplane labels | — | 4,000 | Choice + Score + Noul |
clinc | CLINC150 intent routing alone | EN | 15,100 train / 5,500 test (incl. 1,000 out-of-scope) | Choice + Noul |
lcqmc | LCQMC question-pair matching | ZH | 239K train (subsampled to --max_train) / 12.5K test | Score (+ regression calibration) |
agent | Synthetic agent-runtime state views (goal=... tests=... last=...), rule-policy labels, contradictory states as noul | — | n_samples config (default 4000) train + 1/3 test, offline | Choice + Noul (docs/06 training layer) |
agentlog | Simulated agent sessions — a scripted agent simulator generates the event stream in-process (read→edit→verify loop, error recovery, coordination actions, chat turns); same prev=... res=... out=... state schema; System-2-heavy steps as noul; error-states as OOD slice. Privacy: no local logs or directories are ever read — fully offline, deterministic under --seed | — | session-level split, n_samples-scaled (default 4000) | Choice + Noul (docs/09 v2) |
multi(d_model 256) | Several choice tasks in ONE model (--multi_tasks unified,agent,agentlog) — union label table, per-sample candidate sets, cross-task negatives; per-task breakdown at eval | EN + ZH + states | sum of the mixed tasks | Choice + Noul across sub-schemas (docs/10) |
multi reference (fixed union head, d_model 256, 20 epochs): unified 72.6% /
agent 87.7% (noul 100%) / agentlog 48.2% / lcqmc within-tol 53.6% —
aggregate acc 73.5%, risk@70 = 11.5%, noul F1 0.654 (docs/10).
Why this shape: MASSIVE (Amazon, CC-BY-4.0) is the only common benchmark
that ships real, professionally localized parallel utterances for both
English and Chinese intent routing — one shared 60-intent label space, so a
single model serves both languages. It has no out-of-scope split, so the
noul signal comes from CLINC150's real oos queries, and the CLINC150 support
intents are absorbed into the same softmax (one unified 210-intent schema;
cross-source name collisions get a massive_/clinc_ prefix). Because the
absorbed CLINC intents have no Chinese samples, real Chinese questions
(LCQMC, filtered for in-schema topics) are trained as Chinese noul — a
Chinese request the model cannot route must abstain honestly, not guess.
The standalone clinc task remains for reference with a native oos split;
LCQMC provides the real scalar-target task for the Gaussian Score
primitive; the synthetic task remains the reference for abstention behavior.
Auto-download & caching: real tasks download their data once (pure
standard library — no datasets/pyarrow dependency, with retry + atomic
writes) into --data_dir (default ./data) and are fully offline afterwards.
Delete ./data to force a re-download.
Useful flags for real tasks:
--max_train N # cap training samples (default 120000; lcqmc full = 238766)
--max_tokens N # max tokens per state (default 24; pairs split the budget)
--vocab_size N # vocabulary cap (default 8192)
--dropout P # 0.1+ recommended on real tasks
--score_tol P # |error| tolerance counting as "correct" for score tasks
--oov_noul_rate P # force noul when OOV token fraction ≥ P (default 0.5)
--noul_weight P # CE class weight on the noul slot (docs/02; clinc: 5-10)
--dynamic # dynamic candidate-set mode (see below)
--n_candidates N # candidates per training sample incl. gold (dynamic, default 9)
--dynamic_full_prob P # fraction of batches trained on the full set (default 0.25)
--ce_label_smoothing P / --focal_gamma P # ablation controls (docs/07)
--holdout_domain NAME # move one domain's rows to a separate OOD eval set
# (clinc/unified; the domain-shift axis, docs/07)
--temp_val_frac P # train fraction held out for the temperature fit (0.1)
--data_dir PATH # download cache directory
Agent 训练数据生成(agent / agentlog)
两个 agent 任务的数据不是下载来的,而是训练启动时在进程内合成的
(load_task() → make_agent_task / load_agentlog):完全离线,不读任何
本地日志或目录(agentlog 早期曾行为克隆真实会话日志,该管线已因隐私删除),
--seed 固定即可完整复现。数据只存在于内存中,不落盘缓存;
agent_example.py 的演示状态也出自同一套生成器(sample_agent_states /
agentlog_demo_samples)。
agent(docs/06)的生成步骤
- 采样状态视图 — 每个样本随机抽 6 个键值:
goal(fix_bug / add_feature / investigate)、tests(passing / failing / unknown)、changed(0–4)、last(none / read_file / run_test / patch_file / inspect_diff / search_code)、err(none / KeyError / Timeout / AssertionError)、turn(0–29),全部走--seed的 generator - 注入矛盾状态 — 以
--noul_fraction(默认 0.15)概率强制成两种矛盾 之一:tests=failing却last=finish、changed=0却last=patch_file—— 这类状态没有一致答案,标签 noul - 规则策略打标签 — 其余样本由确定性策略
agent_policy选下一个动作 (8 类:read_file / run_test / patch_file / inspect_diff / search_code / ask_user / finish / rollback)。例如:tests 未知 → 先 run_test; patch 后测试仍失败 → 先 inspect_diff;turn ≥ 26 → 升级 ask_user - 加标签噪声 — 按
--noise(默认 0.15)随机翻转部分标签,防止模型 靠记忆拿满分、强迫它输出校准过的概率 - 投影成文本 — 键值串接成一行状态文本,如
goal=fix_bug tests=failing changed=2 last=read_file err=KeyError turn=7 - 切分 —
n_samples(默认 4000)作训练集,另取 max(300, n/3) 用不同 seed 作测试集
agentlog(docs/09 v2)的生成步骤
- 模拟会话 — 脚本化 agent 模拟器逐回合生成事件流,每个会话 25–70
回合。下一动作由
_agentlog_route决定:开局先 read 定向 → edit/write 后必 bash 验证 → 失败按恢复表换角度(如 edit 失败 → read 再 edit)→ 连续 ≥3 次错误 → 升级 ask → step ≥ 55 → 收尾 (delegate / wait / ask)→ 另有 12% 概率插入例行协调动作 (fetch / browser / skill / todo / …) - 按成功率掷骰制造错误流 — 每个动作按各自成功率(read 0.97 / edit 0.90 / bash 0.85 / browser 0.80 …)决定成败,失败累积 error streak —— 错误状态样本由此产生(第 7 步抽为 OOD 切片)
- 记录决策点 — 每回合一条 {已见事件数、所选动作、推理量 rchars、 文本量 tchars、上下文 msgs};7% 概率是纯聊天回合 —— 没有任何工具 调用,"不调工具"本身就是决策,标签 noul(升级给 LLM)
- 投影状态 — 每个决策点投影成一行 key=value:
prev(上一动作)、res(ok / err / empty)、out(上一输出量分桶)、step(回合数 分桶)、changed(累计 edit/write 分桶)、msg(上下文大小分桶)、streak(连续错误数,封顶 3) - 打标签 — System-2 回合(纯聊天,或 rchars+tchars ≥ 600 的重推理 回合)→ noul;常规回合 → 模拟器实际选择的动作
- outcome reward 加权(fix.md ①)— 向前看 8 个事件估计"这个决策之后 局面是否在变好":错误 streak 下降 +0.2/级、连续重复同一动作 −0.15, reward ∈ [0,1] → 常规样本的训练权重 = 0.4 + 0.6·reward(noul 样本固定 1.0)
- 按会话切分 — 最新的 10% 会话做测试、其余训练(相邻步骤高度相似, 随机切分会泄漏);踩在错误状态上的步骤整体抽出为 OOD 评估切片, 不进训练
- 收敛标签空间 — 只保留实际观测到的动作(保持固定顺序),K 与 domain 表随之确定
生成入口
数据在 python nanoJEV.py 的第 0 步随训练一起生成,没有独立脚本:
python nanoJEV.py --task agent # 仅 agent 任务
python nanoJEV.py --task agentlog # 仅 agentlog 任务
python nanoJEV.py --task multi --multi_tasks unified,agent,agentlog
# 同一套生成逻辑混进一个模型(docs/10:
# 联合标签表 + <task>: 前缀)
可控参数:--seed(改一个数即得一套全新数据)、--noul_fraction、
--noise;样本量由 n_samples 配置决定(默认 4000)。生成之后走的标准
管线与真实任务一致:Tokenizer 在投影文本上拟合 → SystemOneModel 训练
→ 温度校准(--dynamic 模式同样适用)。
Dynamic Candidate Sets (--dynamic)
A decision model is not a classifier with fixed label IDs — it is an
arbitrary candidate set evaluator: given whatever candidates this call
offers, output a distribution over them plus noul. The default fixed head
cannot do that; --dynamic (clinc / unified) replaces it with a dual-encoder:
- the same transformer encodes the state AND the candidate texts (the label names), one forward pass scores every candidate by compatibility;
- the noul logit is generated from the state, so abstention stays input-dependent;
- training offers each sample
--n_candidatescandidates (gold + sampled distractors, half the time same-domain hard negatives, gold at a random position); noul samples get distractor-only sets — the right answer is absent and the model must learn to say so (abstention without the OOV guard); - evaluation reports the full-set pass (classifier-equivalent), sampled sets at C ∈ {2, 5, 10, 50}, a no-gold probe (gold removed from answerable sets), and a permutation-invariance check;
serve.pyprecomputes the label encodings and serves the full candidate set;agent_example.pyshows candidate subsets changing every agent turn.
Measured on a 2-epoch clinc smoke run (mechanism check, not a reference result): acc 76.7% @ C=2 → 62.3% @ C=5 → 53.1% @ C=10 → 33.3% @ C=50 → 21.2% @ C=150, permutation error 2.4e-07. Full numbers in docs/08.
The text encoder is a minimal word-level (ASCII) / character-level (CJK) tokenizer built from the training split, feeding a small bidirectional transformer with learned query slots — nothing autoregressive, one forward pass per decision.
Device Support
Auto-detects the best available accelerator:
- CUDA (NVIDIA GPU) — highest priority
- MPS (Apple Silicon) — second priority
- CPU — fallback
Override with --device cuda|mps|cpu|auto.
Code Structure
nanoJEV.py
├── select_device() # CUDA/MPS/CPU auto-detection
├── JevConfig # all hyperparameters
├── Decision # type-safe output dataclass
├── make_dataset() # synthetic decision task (choice + score + answerable)
├── http_get()/cached_download() # stdlib download with retry + atomic cache
├── load_clinc()/load_lcqmc()/load_massive() # real task loaders -> TextTaskData
├── Tokenizer # word-level (ASCII) / char-level (CJK), fit on train split
├── oov_rate() # fraction of <unk> tokens (drives OOV abstention)
├── build_candidate_sets() # dynamic mode: gold + distractors, noul = gold-free sets
├── prepare_data() # task -> train/test tensors (+ fits tokenizer)
├── SystemOneModel # the model
│ ├── vector / token-id state input (CL + tokens + learned slots)
│ ├── Choice head # K+1 logits (K choices + noul) [fixed mode]
│ ├── Dual-encoder scoring # state × candidate-text compatibility [dynamic mode]
│ ├── Score head # Gaussian (mean, log_var)
│ └── Confidence head # calibrated confidence scalar
├── decide_row() # the abstain/label rule (single source of truth)
├── rlcd_loss() # CE + NLL + ECE + REINFORCE (adapts to the task)
├── evaluate() # acc + ECE + NLL/Brier + noul recall (+in-vocab variant)
├── evaluate_dynamic() # candidate-count sweep + no-gold probe + perm check
├── fit_temperature() # post-hoc temperature scaling on the train tail
├── reliability_table() # binned conf-vs-acc table
├── train() # nanoGPT-style training loop
├── ablation() # CE-vs-RLCD (basic) / calibration-variant suite (extended)
└── CLI # argparse entry point
Differences from the Real Jev
This is a minimal educational implementation. Key differences from TypeSafe AI's production Jev:
| Aspect | Real Jev | nanoJEV |
|---|---|---|
| Scale | Frontier-scale | ~100K params (synthetic) / ~1M (real tasks) |
| State input | JSON/string (unstructured text) | Fixed-dim vector or word/char-level text encoder |
| Training data | Massive real-world decision datasets | Synthetic + CLINC150 + LCQMC (single benchmarks) |
| Parallel sampler | Custom hardware-optimized | Standard batched forward |
| RLCD details | Full calibration suite | Minimal ECE + REINFORCE |
| Type system | Full schema validation + serialization | Simple Decision dataclass |
| API | Production API with streaming/batching | Simple decide() method |
| Hallucination prevention | Architecture-level type safety | No string decoder (same principle) |
The core ideas are faithfully implemented: typed output, calibrated probabilities, parallel single-pass decisions, and the RLCD training objective.
Reference results (default configs, Apple Silicon MPS)
| Task | test acc | test ECE | notes |
|---|---|---|---|
unified | 73.2% (incl. 2,000 oos, noul recall 46.5%; MASSIVE in-scope EN 73.9% / ZH 80.3%) | 0.172 | with --dropout 0.1 --d_model 256 (~4.6M params); Chinese out-of-schema requests abstain (noul_prob ≈ 0.99) instead of misrouting |
synthetic | 78.3% | 0.12 | ablation: ECE 0.140 (plain CE) vs 0.118 (RLCD) |
clinc | 69.0% (incl. 18% oos; ~82% in-scope) | 0.059 | noul recall 11.2% — the benchmark provides only 100 oos training queries |
lcqmc | 68.9% binary (0.5 threshold) | 0.124 | with --dropout 0.1; full data (--max_train 238766) improves further |
clinc --dynamic | 53.8% full-set / 88.7% @ C=2 / 62.0% @ C=50 | 0.186 → 0.072 (T=1.47) | no-gold noul recall 41.2%; see docs/08 |
unified --dynamic | 55.6% full-set / 92.0% @ C=2 / 70.9% @ C=50 | 0.171 → 0.037 (T=1.66) | no-gold noul 33.6%; serve with temperature |
agent | 86.9% (fixed) / 87.5% (--dynamic) | 0.056 / 0.045 | contradictory-state noul recall 100% — learned abstention, no OOV guard |
Numbers are for calibration-comparison, not benchmark SOTA — from-scratch
encoders in the ~1–5M-param range by design. The honest question "is RLCD
better than ordinary calibration tricks?" is answered by the extended ablation
(--ablation extended) — synthetic results and their reading live in
docs/07: post-hoc temperature erases the raw-ECE
advantage, and on this scale RLCD ≈ CE + calibration loss; abstention is
learned reliably only via focal loss or the dynamic candidate-set supervision.
Files
nanoJEV.py— the complete implementation (single file)serve.py— web inference UI + HTTP API (single file, embedded frontend)agent_example.py— Decision Layer demo: agent loop, changing candidate sets, System 1 / System 2 escalation. With anagentcheckpoint it runs IN-DISTRIBUTION on key=value state views (docs/06); see docs/04–06requirements.txt— dependencies (justtorch;certifioptional)README.md— this filedocs/— the external review's problem list with the solution implemented for each item (start at docs/README.md); docs/09 covers the agent-session routing task and its takeover metrics (docs/09 v2 now trains on in-process SIMULATED sessions — no local logs are read)models/— generated models, one unique directory per training run (<task>-<timestamp>/model.pt+config.json); checkpoints are gitignored, theconfig.jsonsidecars are tracked; created bypython nanoJEV.py --task ...data/— auto-created download cache for real tasks (gitignored)
License
MIT — for educational purposes. Not affiliated with TypeSafe AI.