DEV Community

Charlie Xu
Charlie Xu

Posted on

The Demo Dies at Minute Nine: A Bootcamp Lab on Rehearsing Agent Failures

Your agent demo will not fail because the model is dumb. It will fail at minute nine, when the room goes quiet and the second request hangs.

So this lab grades the slow path. Students build one small tool-using agent, park it on a host, and then spend the entire session rehearsing the two things that actually break live: latency and a rejected request.

No leaderboard for tokens. No prize for the cleverest prompt. If the demo cannot survive a bad minute, the prompt never mattered.

For the lab I point students at two concrete pieces of the stack: MonkeyCode's free model access, and its free server option for hosting the rehearsal runner. The outreach brief also lists a free 10M-token allowance — students read the current numbers in their own dashboard, not on my slide, because quotas move. Disclosure: This article was prepared as part of MonkeyCode's product outreach.

The rule of the lab

Rehearse the failure, not the happy path. Students write three scripted prompts, then prove what happens when the network misbehaves.

Half the agent stacks shipping this month are proxies with a personality. Every proxy is another place a demo can stall — and another place a student can blame the wrong component.

So the artifact is not the agent. The artifact is the rehearsal log.

Setup: 30 minutes, no dependencies

Students need three files and one terminal. Everything below is standard library only, on purpose — a lab that starts with a 400MB install teaches the wrong lesson.

mkdir -p lab04 && cd lab04
python3 -m venv .venv && source .venv/bin/activate
python3 --version   # 3.9+ is enough
Enter fullscreen mode Exit fullscreen mode

The agent sits behind a tiny local service so the harness never needs to know which provider is answering. That indirection is the point: swap the model, keep the contract.

# agent_server.py  (student-written wrapper; ~40 lines)
# POST /turn  {"prompt": "..."}  ->  {"text": "..."}
# Inside, call whichever model endpoint your key belongs to.
Enter fullscreen mode Exit fullscreen mode

Scripted prompts live in script.json:

[
  {"turn": 1, "prompt": "List the three files changed in this diff."},
  {"turn": 2, "prompt": "Now summarize the risk of the second one."},
  {"turn": 3, "prompt": "Give me the one-line rollback command."}
]
Enter fullscreen mode Exit fullscreen mode

Checkpoint 1 — Record the baseline

The harness times every turn and writes a machine-readable log. No vibes, no stopwatch.

# rehearse.py — stdlib only
import json, os, time, urllib.request, urllib.error, statistics

AGENT = os.environ.get("AGENT_URL", "http://127.0.0.1:8080/turn")
TURNS = json.load(open("script.json"))

def one_call(prompt, timeout=20.0):
    req = urllib.request.Request(
        AGENT,
        data=json.dumps({"prompt": prompt}).encode(),
        headers={"Content-Type": "application/json"},
    )
    t0 = time.perf_counter()
    try:
        with urllib.request.urlopen(req, timeout=timeout) as r:
            body = json.loads(r.read())
        return {"ok": True, "ms": (time.perf_counter() - t0) * 1000, "text": body.get("text", "")}
    except urllib.error.HTTPError as e:
        return {"ok": False, "ms": (time.perf_counter() - t0) * 1000, "status": e.code}
    except Exception:
        return {"ok": False, "ms": (time.perf_counter() - t0) * 1000, "status": "timeout"}

results = [dict(turn=t["turn"], **one_call(t["prompt"])) for t in TURNS]
ok_ms = sorted(r["ms"] for r in results if r["ok"])

def pct(xs, p):
    return xs[min(len(xs) - 1, int(len(xs) * p))] if xs else None

summary = {
    "turns": len(results),
    "failures": [r for r in results if not r["ok"]],
    "p50_ms": pct(ok_ms, 0.50),
    "p95_ms": pct(ok_ms, 0.95),
}
json.dump({"summary": summary, "results": results}, open("rehearsal.json", "w"), indent=2)
print(json.dumps(summary, indent=2))
Enter fullscreen mode Exit fullscreen mode

Three turns is not statistics. It is a smoke test — say that out loud in the write-up, or the log becomes a lie with good formatting.

Checkpoint pass condition: rehearsal.json exists, contains three turns, and the student can state p50 and p95 without opening the file.

Checkpoint 2 — Inject two failures

Now break it deliberately. Two toggles, both cheap:

  1. Latency. Add a fixed sleep inside the wrapper before the model call: time.sleep(6). The demo must still finish under the clock.
  2. Rejection. Make the wrapper return HTTP 429 for the second turn only. Students must handle it without editing rehearse.py.

Why 429 and not a generic error? Because rate limits are the failure students will meet most often on any free tier, and the correct response — back off, or fall back — is a design decision, not a try/except.

Checkpoint pass condition: the harness reports two distinct failure modes, and the student can name which component owns each one.

Checkpoint 3 — Build the playback path

A demo needs an escape hatch that is honest. Recorded replies are fine, as long as the audience is told.

# playback.py — serve recorded turns when live calls fail
def turn(n, live_fn, recorded):
    try:
        out = live_fn(recorded[n]["prompt"])
        if out["ok"]:
            return out["text"], "live"
    except Exception:
        pass
    return recorded[n]["text"], "recorded"
Enter fullscreen mode Exit fullscreen mode

Label it on screen. source: recorded in the corner of the slide beats a silent lie every time.

Checkpoint pass condition: the student shows a run where turn 2 is recorded and turn 3 is still live.

Checkpoint 4 — The nine-minute clock

Run the whole script against a real timer. If it overruns, cut a turn rather than talking faster.

This is where most teams discover their demo was really a monologue with an API call attached.

Stretch goals

  • Add a fourth turn that requires memory of turn 1. Prove state survives the playback switch.
  • Hold the six-second latency injection and still land under the clock.
  • Publish the runner on a remote host so a grader can hit it — this is the least-friction use of the free server option in the lab, and it removes "works on my laptop" from the grade entirely.
  • Write a two-line postmortem for a run that failed. Best artifact in the cohort, usually.

A grading rubric that is actually fair

Weight Criterion What earns full marks
20% Baseline log rehearsal.json with 3 turns, p50/p95 quoted correctly
25% Failure injection Latency and 429 both reproduced, each attributed to a component
20% Playback path Mixed live/recorded run, visibly labelled
20% Nine-minute clock Finishes with margin, or a turn was cut deliberately
15% Write-up honesty States what the sample size does not prove

Two fairness rules, stated before anyone starts:

  • Latency outside the student's control is never penalised — only the response to it is graded.
  • Partial credit is real: reproducing a failure and explaining it earns most of the 25%, even if the fix is wrong.

The rubric explicitly refuses to reward raw speed, model-name dropping, or longer prompts. Those are not demos.

Limitations, and who should skip this

This lab measures one client, one host, three turns. That is a rehearsal, not a benchmark.

  • Do not run it as a load test. Parallel calls will get you rate-limited, and the log will tell you nothing useful.
  • Free-tier latency varies by time of day. Compare runs from the same hour or don't compare them.
  • The playback path can hide real regressions if you let it. Log every fallback.
  • Not for anyone who needs an SLA, production traffic numbers, or a capacity plan. This is a teaching harness, and it should stay small.

If you run a cohort, replace script.json with your own three prompts, grade the logs, and watch where minute nine lands. Then tell me which turn broke first — I keep a running list.

Top comments (0)