Tutorial 3: Deterministic Gates¶
Recovery (Tutorial 2) handles failures after they happen. Gates catch a whole class of problems before an LLM is ever called, following what the paper calls Stripe's "Minions" pattern:
Anything rule-bound is kept out of the probabilistic model. Reliability comes from the constraints, not model size.
If a check can be written as a deterministic rule - valid syntax, no
eval(), matches a schema, within budget - write the rule. Don't spend a
model call finding out.
The gate interface¶
Every gate is a DeterministicGate with one method:
GateResult.passed is a plain boolean, and GateResult.severity (error /
warning / info) controls whether a failure blocks execution or just gets
logged. loop_engine.gates ships seven gates out of the box - SyntaxGate,
LintGate, TypeCheckGate, SchemaGate, BudgetGate, TestGate, and
SecurityGate - and a GateRunner that runs a list of them and fails fast
on the first error-severity failure.
Run a couple of gates directly¶
from loop_engine.gates import GateRunner, GateContext, SyntaxGate, SecurityGate
def check(label, code):
runner = GateRunner()
runner.add_gate(SyntaxGate())
runner.add_gate(SecurityGate())
result = runner.run_all(GateContext(content=code))
print(f"{label}: passed={result.passed}")
for gate_result in result.results:
print(f" [{gate_result.gate_name}] passed={gate_result.passed} - {gate_result.message}")
check("safe_code", "def add(a, b):\n return a + b\n")
check("broken_syntax", "def add(a, b)\n return a + b\n")
check("dangerous_code", "def run_command(cmd):\n return eval(cmd)\n")
Real, unedited output:
safe_code: passed=True
[syntax] passed=True - Python syntax valid
[security] passed=True - No obvious security issues found
broken_syntax: passed=False
[syntax] passed=False - Syntax error: expected ':' at line 2
dangerous_code: passed=False
[syntax] passed=True - Python syntax valid
[security] passed=False - Security issues found: 1
SecurityGate matches a fixed list of dangerous patterns (eval(, exec(,
os.system(, subprocess with shell=True, unsafe pickle/yaml.load,
...) - it's intentionally simple regex matching, not a substitute for a real
static analyzer, but it's free, instant, and catches the obvious cases
before anything expensive runs.
Gate an Actor before it "spends a token"¶
The real value of gates is inside a component, rejecting bad input before the expensive step happens. Here's an actor that gate-checks generated code before "accepting" it - in a real actor, the accept path is where you'd call the model or apply the patch:
class GatedActor:
def __init__(self):
self.gates = GateRunner()
self.gates.add_gate(SyntaxGate())
self.gates.add_gate(SecurityGate())
self.llm_calls = 0
async def execute(self, step, observations, context):
generated_code = step.metadata["generated_code"]
gate_result = self.gates.run_all(GateContext(content=generated_code))
if not gate_result.passed:
failed = [r.message for r in gate_result.failed_gates]
raise ValueError(f"Gate check failed, not calling the model: {failed}")
self.llm_calls += 1
return {"accepted_code": generated_code}
Feeding it the dangerous snippet from above:
The raised ValueError becomes a Failure the way any actor exception
does - which means Tutorial 2's recovery
strategies apply here too. A gate rejection isn't a dead end; it's a normal
failure your RECOVERY component can act on (retry with feedback, escalate
to a human, or circuit-break if it keeps happening).
Pre-built gate sets¶
StandardGates bundles common combinations so you don't hand-assemble a
GateRunner every time:
from loop_engine.gates import StandardGates
StandardGates.python_code() # Syntax + Lint + Security
StandardGates.python_with_tests() # + TypeCheck + Test
StandardGates.json_output(my_schema) # SchemaGate against a JSON Schema dict
StandardGates.budget_protected(max_tokens=50_000, max_cost=5.0)
LintGate and TypeCheckGate shell out to ruff/mypy; if the tool isn't
installed, the gate passes with severity="warning" rather than blocking
your loop on a missing dev dependency.
What's next¶
Gates and recovery both operate within a single run. Tutorial 4 covers what survives between runs - state persistence - and how to keep a human in the loop at the moments that actually matter.