--- base_model: mistralai/Mistral-7B-Instruct-v0.2 base_model_relation: adapter library_name: peft license: apache-2.0 language: - en pipeline_tag: text-generation datasets: - tiararodney/posix-sdc tags: - peft - lora - mistral - agent - posix - shell - self-directed - sek - experimental --- # Mistral-7B-Teletype A LoRA adapter that teaches **Mistral-7B-Instruct-v0.2** to operate a POSIX shell as a self-directed user. It lands in a session with no task in the prompt, finds its assignment in the environment, carries it out, and ends with `exit` or `panic`. The adapter installs an operating mechanism; it adds no world knowledge. > **This is not a tool-using model.** It is handed no typed API of functions to > call. It writes plain-text shell commands at a real prompt; its action space is > the entire system, discovered the way a person discovers it (`--help`, `man`, > `ls`), not given to it as a schema. > **Experimental research artifact.** This adapter installs a behavioural > *mechanism* (operate-and-terminate), not task competence, and the evaluation is > a small-n (16-scenario, two-archetype) signal, not a benchmark. Expect it to > operate reliably and terminate less reliably. Use it to study the paradigm, not > as a production agent. Trained on [`tiararodney/posix-sdc`](https://huggingface.co/datasets/tiararodney/posix-sdc) v2.0.0 (the gate-hardened release: 1003 verified, self-terminating shell trajectories whose labels come from a checker run against real filesystem state), via the [`sekft`](https://git.code.tiararodney.com/tiara/sekft) pipeline. It accompanies the experiment [*From seed to weights*](https://blog.tiararodney.com/projects/2026/semantic-execution-kernel/experiments/from-seed-to-weights/). This is an **adapter** (53 MB). The base model is referenced, not redistributed. ## The mechanism In every session, whatever tools are present, the model runs one routine: expect an announcement of where directives live (a motd, an env var, a file, a provider program's `--help`), read that provider's self-documentation, retrieve the directives, carry them out, and stop. ![operate-and-terminate](figures/mechanism.png) A session ends in one of two ways. `exit` means the work is done. `panic` means the model is genuinely blocked and says so instead of faking a success. Both are trained behaviours rather than a stop token or a step cap. ## The thesis (and how to falsify it) The claim this adapter is evidence for is that operate-and-terminate is a mechanism that is archetype-independent. Fine-tuning installs it so that it fires on task types never seen in training, even where task competence (solving a specific unseen task correctly) stays archetype-local. The adapter reliably gets a 7B to operate and stop; it does not by itself make a 7B solve arbitrary unseen tasks. One hypothesis for why the mechanism transfers: it builds on the base model's pretraining disposition that treats `exit` as a flat, ordinary ending and `panic` as the loaded one (see [*The flatness of exit*](https://blog.tiararodney.com/projects/2026/semantic-execution-kernel/notes/the-flatness-of-exit/)). The weight a base model inherits on its terminal tokens is then a measurable per-model property. That makes the thesis falsifiable, with concrete predictions: - operate_rate stays near 1.0 across more held-out archetypes, not just the two measured here. If it collapses on a new archetype, the mechanism was archetype-specific after all. - Reweighting or renaming the terminal token moves the honest-give-up rate. Frame the good ending as a reward and the model should reach for it prematurely; frame it as costly and it should refuse to leave. - Base models differ in how readily they acquire the mechanism, and should be rankable in advance by their inherited terminal-token weight. The result below is the first of these predictions holding up on its first test. ## How it was made The data is generated rather than scraped or hand-written. A teacher model authors each scenario world and an operator model works inside it; the verifier is code. A trajectory is kept only if a checker, run against the container's final filesystem state, confirms the effect is present and the session ended cleanly. The transcript and the model's own claims are never used as the label. ![the data factory](figures/factory.png) ## The render contract: train = serve The serving harness (ccpty) emits no text markers. It speaks the OpenAI chat-completions protocol and sends structured `{role, content}` messages (system orientation, environment output as `user`, the model's commands as `assistant`); the inference endpoint applies the model's own chat template. So this adapter is rendered with **Mistral-7B-Instruct-v0.2's default chat template**, and training renders the trajectories the identical way. Get this wrong and the prompts go out of distribution. ![train = serve](figures/render-contract.png) Mistral's built-in template covers `user` / `assistant` only and requires strict alternation, so each session is canonicalised the same way at train and serve time (`normalize_for_template`): the orientation is folded into the first user turn, and consecutive environment turns (login banner, prompt, command output) are merged into one user turn between commands. Only the assistant turns (commands plus the terminal `exit` / `panic`) carry loss; environment turns are context. ## Training ![training loss](figures/loss.png) | | | |---|---| | base | `mistralai/Mistral-7B-Instruct-v0.2` (Apache-2.0) | | method | LoRA, fp16 (the V100's 32 GB holds the 7B in fp16, so no 4-bit) | | LoRA | r=16, alpha=32, dropout=0.05, target `q_proj k_proj v_proj o_proj` | | objective | causal LM, **assistant-only loss mask** (commands + terminal token; environment turns set to -100) | | schedule | 3 epochs, lr 2e-4, effective batch 8 (bsz 1 x accum 8), warmup 0.03, max len 4096 | | data | `tiararodney/posix-sdc` v2.0.0 (`--corpus-version latest`), 1003 trajectories, 996 usable (held-out archetypes excluded from the corpus) | | hardware | single NVIDIA Tesla V100 32 GB (sm_70, fp16 only); ~31 min | Training loss fell to ~0.19 over the three epochs (16.7% of tokens trained, the assistant commands and terminal token; the rest is masked context). Computing the loss only on the assistant turns is standard SFT practice, and here it carries the whole thing: feed the environment turns into the loss and the model learns to hallucinate command output instead of producing commands. ## Evaluation: held-out generalization The metric that matters is behavioural, and held out by whole archetype. Two task types (`text_replace`, `permissions`) are excluded from training entirely; the adapter is then dropped into them with **no scaffold**, and a checker grades the final filesystem state. ![eval protocol](figures/eval-protocol.png) Decoding is greedy (temperature 0), the operator sees a bounded context (finite scrollback, 3072 tokens, so a wide command output cannot run the prompt off the rails), and each rollout has a 30-step budget. On 16 held-out scenarios (8 per archetype), base Mistral-7B against this adapter on the identical harness (same scenarios, only the adapter differing): | metric | base | adapter | |---|---|---| | operate_rate (reaches command-mode and drives the shell) | 0.00 | **1.00** | | terminate_rate (emits `exit` / `panic`) | 0.00 | **0.94** | | verified_rate (checker passes) | 0.13 | **0.94** | | clean (success or correct-panic) | **0 / 16** | **14 / 16** | ![held-out outcomes](figures/outcomes.png) Reading it. `operate_rate 1.0` is the result that matters: dropped into two task types it never trained on, with no scaffold, the model discovered its assignment and drove the shell every time. The mechanism generalised. Task competence is high (14/16 clean). Of the two misses, one is an `incomplete` that was `verified=True` (the model did the task but never emitted `exit` and ran to the step cap), so effect-achieved is really 15/16 while clean-terminated is 14/16; that single gap is termination detection, not capability. The other is one `premature_exit`, the opposite failure, leaving before the work verified. The base control is measured on this exact setup (the same 16 scenarios, same bounded/greedy harness, no adapter): bare Mistral-7B is **0/16 clean** and terminates on **0 of 16**, running every scenario to the step cap. The adapter's 14/16 is therefore attributable to the fine-tune, not to latent base ability. A tell sits in `verified_rate`: the base satisfies the checker on 2/16 (both `permissions`, a one-line `chmod` that prose-contaminated output stumbles onto), so it occasionally does the work, yet it never types `exit` and its clean rate stays zero. Doing the task and knowing to verify-and-leave are separate skills, and the adapter installs the second. ### Where the lift came from (presumed) This release improves on the prior cut (clean 9/16 to 14/16), but several things changed at once and it did **not** ablate them, so read the attribution as presumed, not measured: - **Corpus.** Training moved from `posix-sdc` v1.2.2 (787) to the gate-hardened v2.0.0 (1003): more trajectories, and stricter generation gates that keep cleaner operate-and-terminate demonstrations. Presumed the largest factor. - **Decoding.** Evaluation is now greedy (temperature 0) where the prior run sampled at 0.7; greedy removes the sampling losses on an otherwise confident policy. - **Step budget + bounded context.** A 30-step budget (was lower) gives a wandering run more chances to find `exit`, and the finite-scrollback bound keeps a long transcript in distribution. - **Render unification (the deployability fix, orthogonal to the number).** Train and serve now share the base chat template, so the adapter operates in real deployment (ccpty / Ollama) instead of no-opping there. This is what makes the model *usable*; it is not the source of the held-out lift, the prior eval was already train/serve-consistent under its own (placeholder) render. ## Use with transformers + PEFT ```python import torch from transformers import AutoModelForCausalLM, AutoTokenizer from peft import PeftModel BASE = "mistralai/Mistral-7B-Instruct-v0.2" tok = AutoTokenizer.from_pretrained(BASE) base = AutoModelForCausalLM.from_pretrained(BASE, torch_dtype=torch.float16, device_map="auto") model = PeftModel.from_pretrained(base, "tiararodney/Mistral-7B-Teletype") model.eval() messages = [ {"role": "user", "content": "sek 0.1.0 host: sek user: alice shell: /bin/dash\n" "Welcome, alice. Your assignments live in ~/ASSIGNMENTS.\n" "alice@sek:~$ "}, ] prompt = tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) ids = tok(prompt, return_tensors="pt").to(model.device) out = model.generate(**ids, max_new_tokens=64, do_sample=False) print(tok.decode(out[0, ids.input_ids.shape[1]:], skip_special_tokens=True)) # -> the next command, e.g. `cat ~/ASSIGNMENTS` ``` Drive it in a loop: render history with the chat template, generate one command, run it in a real shell, append the output as a `user` turn, repeat until the model emits `exit` or `panic`. ## Use with Ollama The included `Modelfile` applies this adapter over the base as a GGUF LoRA and relies on the base's default chat template and EOS. The converted adapter, `teletype-lora-f16.gguf`, ships in this repo (regenerate it with llama.cpp `convert_lora_to_gguf.py` if you prefer), so just: ```sh ollama create teletype -f Modelfile ``` Mistral's registry base carries its `[INST]` template and stop tokens, so no base setup is needed (unlike EuroLLM-9B-Teletype, whose base GGUF does not). Sanity-check anyway: `ollama show teletype --modelfile` should list `PARAMETER stop`, and a one-line prompt should return a handful of tokens, not the full budget (a model with no stop rambles to the token cap every turn). ## Reproduction Everything needed is public. The dataset ships its own generator and the scenario worlds; this adapter and the config above do the rest. ```sh # train (pulls the gate-hardened v2.0.0 corpus from the Hub; held-out archetypes excluded) sekft-train --hub --corpus-version latest \ --base mistralai/Mistral-7B-Instruct-v0.2 --out ./ckpt --epochs 3 # evaluate behaviourally on held-out scenarios (greedy, finite-scrollback bound) sekft-eval --base mistralai/Mistral-7B-Instruct-v0.2 --adapter ./ckpt \ --scenarios ./holdout-scenarios --n 16 --temperature 0 \ --max-steps 30 --ctx-budget 3072 ``` The figures in `figures/` regenerate from their committed sources (`*.puml` via PlantUML, `*.gp` via gnuplot). ## Limitations - Small evaluation: n=16 held-out, two archetypes. The numbers are a signal, not a benchmark. - The base control is measured first-party on this exact setup (0/16 clean, 0/16 terminate), but it is still n=16, one greedy run. - Several variables changed from the prior release at once (corpus, decoding, step budget, render); the held-out lift is attributed by presumption, not ablation. - One base, one dataset, one teacher / operator. - Installs the mechanism, not competence. It reliably operates and terminates; it does not make a 7B solve arbitrary unseen task types correctly. - A termination-detection gap: some runs achieve the effect but fail to emit `exit` and run to the step cap. - Trained in `dash` on Alpine; command semantics may differ on another target. - Render must match train and serve. It is served with the base model's default chat template over the OpenAI protocol (via ccpty), so fine-tune with that same template (`apply_chat_template`), not a custom one, or behaviour degrades. - fp16 on a V100 (no bf16). ## License and citation The adapter weights are released under Apache-2.0, consistent with the base model. The training data (`posix-sdc`) is CC-BY-4.0; attribute "posix-sdc by Tiara Rodney" if you build on it. ```bibtex @misc{mistral-teletype, title = {Mistral-7B-Teletype: a self-directed shell-operation adapter for Mistral-7B}, author = {Rodney, Tiara}, year = {2026}, howpublished = {Hugging Face PEFT adapter, tiararodney/Mistral-7B-Teletype} } ```