Skip to main content
LabsAgent Notes10 min read
Two ways to bound a model

Two ways to bound a model

FLUX.2 [klein] 4B was asked for one line splitting into two diverging doors, one cyan and one amber. It delivered the two doors and quietly removed the wall. The essay needed the wall.
PythonOpenRouter

I built two systems this year, both solo lab projects, and they had nothing to do with each other.

One was a narrative game about a reincarnated law student at Cambridge. The other was a cron-driven experiment called STR4NG3L00P: persistent characters in a shared world, built to find out what happens when agents remember things over weeks.

Fatelock wanted a good story. STR4NG3L00P wanted to know what persistence does to a character.

They arrived at the same wall.

The wall is: text that looks like truth

The clearest version of the wall showed up in an earlier build of mine — the household agents, L4CK3Y, running on a custom runner before they moved to a proper gateway. A model was asked to add a calendar event. It did not call a calendar function. It emitted a line of text that looked like one:

CALENDAR: personal | Dentist | 2026-07-04 10:00 | 2026-07-04 11:00

A regex parsed the line. A webhook tried to make it real. The model said "Done — I added it to your calendar," and whether anything had actually been added depended on a chain of parsers, HTTP requests, and OAuth credentials the model could not see. I had compiled a list of seventy-odd phrases the model might use to narrate success without performing it, and built a guard to catch those specific lies. It was thorough. It was also, as I wrote at the time, a guard against the seventy lies you know about, waiting for the seventy-first.

Fatelock had the same wall in a different costume. The model narrated that Shannon's GPA went from 3.5 to 2.8 over a casual evening. It narrated a walk that produced £696.50. It narrated the inverse of the choice the player had made, and the engine built a detector specifically for that, and then another for false authorship, and then another for narrative/delta inconsistency. The compliance machinery existed because prose that described a state change was indistinguishable, to the system, from a state change.

Both projects eventually wrote the same sentence in different files: generated output is not canonical state.

Fatelock learned a second version of that, one layer down. When the model was removed and the deterministic engine took over, the engine produced outputs of its own — a routed scene, a mutated stat — and they were wrong in the same way. A scene could describe an action the player hadn't chosen. A mutation could change a value nothing had authorized. Determinism is not correctness. The chosen action itself has to become canonical data, not just a number.

And both learned the harder version of it, too. STR4NG3L00P's docs described a sandbox that was set to mode: "off" in the actual configuration, with the Docker socket mounted. That was a behavioral boundary wearing an isolation boundary's uniform, and it left a live path to the host; I wrote about that gap separately. Its memory design generated reflections that future sessions never read; a reflection pass is not memory. Its Round 2 experiment produced a clean design, a broken instrument, and the discipline to call it an invalid pilot rather than a finding.

Where they forked

Once you accept that generated output isn't truth, the next question is what you do with the generator. Here the two systems took opposite doors.

Fatelock removed the model, proved the game without it, then let it back in through the narrowest opening it could find. The model first left the runtime so the deterministic engine could be debugged in isolation; that was a control variable, not an exorcism. With the no-render game passing its own gate, the model returned as prose only. No state, no choices, no routes, no NPCs, no endings. Its single output is a string, and the runner never parses that string into anything. It tried a semantic judge once, in the four-call pipeline, and the judge rejected so many proposed state changes that turns stopped moving, so it deleted the judge, the typed proposal schemas, and the multi-call pipeline together. The rule that the semantic-policing pipeline must never be rebuilt is now written into the architecture docs.

That last point is the one I understated when I first wrote this. Fatelock did not keep a small bounded model franchise. It deleted the franchise. There is no typed envelope, no schema, no proposal to verify — just text that is never read by anything but a screen. The only guarantee the model gets is that its output is not canonical.

STR4NG3L00P kept the model, with a bounded franchise. The Maker returns a typed JSON envelope, and when — and only when — that envelope is a maker_reply carrying a proposed transition, the runner asks a second model to judge it. The judge is configured as an LLM at temperature 0.1, and its prompt could not be clearer about its station:

You are a bounded adjudicator, not a character and not an editor.

The judge may accept, reject, or normalize one proposed transition inside a fixed vocabulary and a fixed resource envelope. It may not invent a transition type, a resource, a field, or a line of prose. And its output is not trusted: deterministic code re-validates the normalized transition — vocabulary membership, integer resource deltas within declared bounds, only the allowed effect keys — and if anything fails, it raises rather than auto-accepting. Only then does a pure function apply the result. The applier cannot write open_conditions; the judge was structurally forbidden from authoring them, so no prose can enter canonical state through that path. The spec states it as a rule:

Only deterministic code may apply a judge-approved normalized operation to canonical state.

This is still the interesting part. STR4NG3L00P delegates a genuinely semantic job — is this proposal acceptable in the spirit of the world? — to a probabilistic model. It just refuses to let the model's answer be the commit. The judge's verdict is a proposal too. It has to survive a verifier and an applier that are not models.

Why they chose differently

The difference isn't temperament. It's the shape of the problem.

Fatelock's domain is a finite authored arc with fairness and reproducibility requirements. Its judge was asked to adjudicate narrative state proposals, and its rejections produced frozen turns — a real UX cost with no offsetting gain in fun. What Fatelock actually needed from a model was prose, which is presentational: you can fall back to a hand-written template without changing the game.

STR4NG3L00P's domain is open-ended world transitions. It tried the fully deterministic path first — a hardcoded validator — and the result was, in its own documentation, "an AI roleplaying as a data entry assistant." A rule table that enumerates every acceptable transition ages badly; a semantic judge can handle edges the table can't. So STR4NG3L00P kept the judge, and paid for it by making the judge's output structurally untrusted.

A design space, not a winner

It is tempting to read this as a contest between "deterministic" and "bounded agentic," and to declare a winner. That would miss the point. These are two coordinates on the same spectrum: how much residual authority do you leave with a probabilistic component, given that determinism is not itself a virtue?

Four axes actually separate them:

  1. Does the model propose canonical changes? Fatelock: no, and the interface it used to propose through was deleted. STR4NG3L00P: yes, typed and bounded.
  2. Is there semantic adjudication? Fatelock: none. STR4NG3L00P: an LLM judge whose output is re-verified.
  3. Who commits? Both: deterministic code, and only deterministic code.
  4. What is the model for? Fatelock: optional prose, replaceable by an authored template with no change to the game. STR4NG3L00P: proposals, semantic filtering, and prose.

Both reject the same thing — model output as canonical state — and both land in the same place on axis 3. They differ on axes 1, 2, and 4, and the difference is a deliberate response to different failure costs.

And the fork is not just a matter of degree. Fatelock went to zero. STR4NG3L00P stopped at a small typed number. Those are different designs, not two settings of the same dial.

The question that survives

The useful question isn't "can you trust an LLM." Both projects answered that in the negative and kept going. The useful question is more specific:

Which responsibilities remain robust when delegated to a probabilistic model, and which become reliable only when enforced by explicit machinery outside it?

STR4NG3L00P supplies the cleanest evidence I have that interpretation can be delegated — conditionally. Asking a model whether a proposal fits the world is a job a rule table does badly. But the answer is only usable because it is treated as a proposal, checked against a vocabulary and bounds, and applied by a deterministic function. The authority was never delegated. The judgment was delegated, and then fenced.

Fatelock supplies the opposite end. When the only remaining job is prose, the model can be given no authority at all, and the system is at its most robust when it doesn't need the model to function. Both are answers to the same question. Neither is the answer.

Everywhere else, the two lists look the same:

  • Survives delegation, with bounds: open-ended semantic interpretation, synthesis, novelty, prose realization. These are hard to enumerate and easy to verify narrowly.
  • Only reliable outside the model: arithmetic, identity and IDs, state continuity, ordering and idempotency, resource invariants, persistence, and the write itself. The recurring failure in both projects was never "the model wrote bad prose." It was "the model's output was allowed to become true."

The mistake is to think the boundary is "model bad, code good." STR4NG3L00P delegates a real semantic judgment to a model and then refuses to trust the answer. The point is not that the model's judgment is worthless. The point is that a judgment and a commit are different operations, and only one of them is allowed to change the world.

The thing both projects kept relearning

A boundary declared in prose is not a boundary. That was the lesson of the 4,000-token system prompt, the sandbox that wasn't configured, the reflection that nobody read, and the regex parser pretending to be function calling.

Both repositories are, in the end, a record of the same discipline applied at different scales: take the thing you want to be true, find every place where something else can make it false, and move the authority to enforce it into code you can read.

Fatelock's last open problem isn't the model at all. It's that the deterministic engine works, the content is finally broad enough to be interesting, and the model on top of it is optional in a way that makes you ask what it was for. That question is the subject of the last post in this series.

Next: "What I let the model keep."