DEV Community

Quinn Li
Quinn Li

Posted on

Letter to My Past Self: Your Agent Loop Needs a Kill Switch

It was 11:47 PM. My agent loop was still running. It had started at 9:00 AM. Fourteen hours of retrying on a free server.

The server cost zero dollars. The debugging cost a full day. This letter is for my past self. It is also for you.

A trending DEV discussion this week asked who tests the AI reviewer. My answer is short. Run the loop. Watch it fail. Fix the loop before you trust its output.

I use free infrastructure every week. I am not here to scare you away. I am here to save you the day I lost.

The setup

The stack was minimal. One free model endpoint. One free server. One small agent loop.

The endpoint and server came from MonkeyCode's open source project. It offers free model access and a free server option. At the time of writing, the free tier includes a 10M token allowance. Quotas change. Read the README before you build.

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

The workflow below is provider-agnostic. It works with any free endpoint.

I made three mistakes. Each cost hours. All three are avoidable.

Mistake 1: I skipped the probe

I assumed the endpoint was reachable. It was not. My code failed for three hours. I blamed my own logic. The endpoint had been down the whole time.

The fix is a 30-second probe. Run it before any real work. If it fails, stop. Do not debug your code first.

# probe.sh - verify endpoint and server before the agent loop
ENDPOINT="${MODEL_ENDPOINT:?set MODEL_ENDPOINT}"
TOKEN="${MODEL_TOKEN:?set MODEL_TOKEN}"

curl -sS -o /dev/null -w "endpoint http=%{http_code} time=%{time_total}s\n" \
  -X POST "$ENDPOINT" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"messages":[{"role":"user","content":"ping"}]}'

curl -sS -o /dev/null -w "server http=%{http_code}\n" \
  "${FREE_SERVER_URL:-http://127.0.0.1:8000}/health"
Enter fullscreen mode Exit fullscreen mode

One command checks both sides. 200 means continue. Anything else means stop and wait.

Mistake 2: I built an unbounded loop

My agent never said "done". It just retried. Every retry called the model. The token allowance drained all afternoon.

The fix has three boundaries. A max step count. A time budget. A stop file.

# boundaries.py - three kill switches for any agent loop
import os
import time
from pathlib import Path

MAX_STEPS = int(os.getenv("MAX_STEPS", "5"))
TIME_BUDGET_S = int(os.getenv("TIME_BUDGET_S", "900"))
STOP_FILE = Path(os.getenv("STOP_FILE", "./STOP"))

def dead_reason(step: int, started_at: float):
    if STOP_FILE.exists():
        return f"{STOP_FILE} present"
    if step >= MAX_STEPS:
        return f"MAX_STEPS={MAX_STEPS}"
    if time.time() - started_at > TIME_BUDGET_S:
        return f"TIME_BUDGET_S={TIME_BUDGET_S}"
    return None
Enter fullscreen mode Exit fullscreen mode

Any single boundary kills the loop. You always have an exit. You never wait four hours again.

Mistake 3: I trusted raw model JSON

The model returned valid JSON. The keys were wrong. My parser accepted it silently. Bad data flowed into my database.

The fix is schema validation. Validate every reply. On failure, feed the error back into the conversation. Let the model repair its own output.

# validation.py - reject silent schema drift
import json

REQUIRED_KEYS = {"action", "args"}

def validate_reply(raw: str) -> dict:
    data = json.loads(raw)  # raises on malformed JSON
    missing = REQUIRED_KEYS - set(data)
    if missing:
        raise ValueError(f"missing keys: {sorted(missing)}")
    return data
Enter fullscreen mode Exit fullscreen mode

This turns a silent bug into a visible step. The loop logs it. The model sees the error. The next reply usually passes.

The full bounded loop

The complete script combines probe, boundaries, and validation. It uses only the Python standard library.

#!/usr/bin/env python3
# bounded_agent.py - free endpoint + free server, with a kill switch
import json
import os
import sys
import time
import urllib.request
from pathlib import Path

MAX_STEPS = int(os.getenv("MAX_STEPS", "5"))
TIME_BUDGET_S = int(os.getenv("TIME_BUDGET_S", "900"))
STOP_FILE = Path(os.getenv("STOP_FILE", "./STOP"))
ENDPOINT = os.environ["MODEL_ENDPOINT"]  # free model endpoint
TOKEN = os.environ["MODEL_TOKEN"]
REQUIRED_KEYS = {"action", "args"}


def call_model(messages):
    body = json.dumps({"messages": messages}).encode()
    req = urllib.request.Request(
        ENDPOINT,
        data=body,
        headers={
            "Authorization": f"Bearer {TOKEN}",
            "Content-Type": "application/json",
        },
    )
    with urllib.request.urlopen(req, timeout=30) as resp:
        return json.loads(resp.read())["message"]["content"]


def validate_reply(raw):
    data = json.loads(raw)
    missing = REQUIRED_KEYS - set(data)
    if missing:
        raise ValueError(f"missing keys: {sorted(missing)}")
    return data


def run(task):
    messages = [{"role": "user", "content": task}]
    start = time.time()
    for step in range(MAX_STEPS):
        if STOP_FILE.exists():
            print("stop: STOP file found")
            return "stopped"
        if time.time() - start > TIME_BUDGET_S:
            print("stop: time budget exceeded")
            return "stopped"
        raw = call_model(messages)
        try:
            reply = validate_reply(raw)
        except (ValueError, json.JSONDecodeError) as exc:
            print(f"[step {step}] invalid reply: {exc}")
            messages.append({
                "role": "user",
                "content": (
                    f"Your reply failed validation: {exc}. "
                    f"Return valid JSON with keys {sorted(REQUIRED_KEYS)}."
                ),
            })
            continue
        print(f"[step {step}] action={reply['action']} args={reply.get('args')}")
        if reply["action"] == "done":
            return reply.get("args")
    print("stop: max steps reached")
    return "stopped"


if __name__ == "__main__":
    if len(sys.argv) != 2:
        raise SystemExit("usage: bounded_agent.py '<task>'")
    print(run(sys.argv[1]))
Enter fullscreen mode Exit fullscreen mode

The loop repeats one bounded unit of work. Check the kill switches. Call the model. Validate the reply. Log the action. If validation fails, the error becomes the next prompt. The loop stays honest.

Adjust the request format to your provider's documented API. The loop pattern stays the same.

How to run it

  1. Export credentials. export MODEL_ENDPOINT=... and export MODEL_TOKEN=.... Set FREE_SERVER_URL if you use the free server.
  2. Run the probe. Confirm both checks return 200.
  3. Run a one-step test. MAX_STEPS=1 python3 bounded_agent.py "Return done with args {}".
  4. Test the kill switch. touch STOP && python3 bounded_agent.py "Do anything". The script exits immediately.
  5. Run the real task with tight limits. MAX_STEPS=3 TIME_BUDGET_S=120 python3 bounded_agent.py "Find the bug in parse.py".

The test sequence takes five minutes. It catches the three failures that cost me a day.

When this setup is the right call

Scenario Free endpoint + free server Local or paid
Prototype to validate an idea Fits Overkill
Learning agent loop patterns Fits Not needed
Tolerates cold starts and restarts Fits
Fixed deadline, batch workload Risk Fits
Regulated or sensitive data Never Fits
Production traffic with an SLA Never Fits

The table is a judgment guide. It is not a benchmark. Use it before you wire anything.

Limitations and who should skip this

Free servers have real constraints. Cold starts are common. Shared resources fluctuate. Persistence is not guaranteed. Token allowances are finite.

Treat the free server as a sandbox. Treat the allowance as fuel. Both run out.

Skip this approach for secrets, regulated data, or production traffic. Skip it for jobs with a hard deadline. Use the same pattern locally with a paid endpoint instead.

The letter ends here

Probe before you build. Bound every loop. Validate every reply.

You will still hit surprises. You will not lose a whole day. That is the only outcome that matters.

I ran this exact sequence on MonkeyCode's free model endpoint and free server. The README lists the current allowance and server details. Run the five-minute test before you trust any loop. Keep the first failure log. That log is the real tutorial.

Top comments (0)