Weekend agent demos rot when every click needs a live model round-trip. Record the request and response once, replay that tape for the demo, and keep any live call behind an explicit latch. The working artifact is a fail-closed cassette, not a new agent framework.
This log cuts scope to a single Python harness, a JSONL tape, and three commands that prove replay. It does not argue that models write better code than people. It treats flaky live traffic as an engineering leak in a side project that has to ship on Sunday night.
Scope for one weekend
The cassette owns three jobs. It canonicalizes a model request. It stores one JSON object per hash. It refuses to hit the network unless the operator flips a latch.
Out of scope on purpose:
- streaming tokens and partial deltas
- provider-specific SDKs and invented model names
- encryption at rest or a shared team vault
- prompt evals, leaderboards, or quality scores
- write side effects against production data
The demo is a CLI that answers a fixed ticket (summarize the failing test) from tape. If the tape misses, the process exits non-zero. That is the whole show.
The failure mode
Live calls hide three bugs that look like product progress. Latency jitter makes the same prompt feel different on the second run. A changed system message silently invalidates last week's screenshot. A weekend quota or a free server blip turns a recorded talk track into an apology.
A cassette does not make the model wiser. It makes the demo honest. Replay is a reducer: same bytes in, same bytes out, or a hard miss.
The cassette contract
The tape is append-only JSONL. Each line is one recorded exchange. The key is a SHA-256 of a canonical request, not a timestamp and not a filename the operator typed by hand.
# layout
.cassettes/
agent-demo.jsonl
Canonical form is a UTF-8 JSON object with sorted keys and no insignificant whitespace. Fields that do not change the model’s job are stripped before hashing: wall-clock timestamps, client request ids, and local absolute paths.
# cassette.py — weekend sketch, not a published library
from __future__ import annotations
import hashlib, json, os
from pathlib import Path
from typing import Any, Callable, Literal
Mode = Literal["replay", "record", "live"]
STRIP = {"timestamp", "request_id", "cwd"}
def canonicalize(req: dict[str, Any]) -> str:
body = {k: v for k, v in req.items() if k not in STRIP}
return json.dumps(body, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
def req_hash(req: dict[str, Any]) -> str:
return hashlib.sha256(canonicalize(req).encode("utf-8")).hexdigest()
class Cassette:
def __init__(self, path: Path, mode: Mode, live: Callable[[dict[str, Any]], dict[str, Any]]):
self.path = path
self.mode = mode
self.live = live
self.path.parent.mkdir(parents=True, exist_ok=True)
self._index = self._load()
def _load(self) -> dict[str, dict[str, Any]]:
if not self.path.exists():
return {}
out: dict[str, dict[str, Any]] = {}
for line in self.path.read_text(encoding="utf-8").splitlines():
if not line.strip():
continue
row = json.loads(line)
out[row["hash"]] = row
return out
def call(self, req: dict[str, Any]) -> dict[str, Any]:
key = req_hash(req)
if self.mode == "replay":
if key not in self._index:
raise SystemExit(f"cassette miss: {key[:12]}")
return self._index[key]["response"]
if self.mode == "record" and key in self._index:
return self._index[key]["response"]
if self.mode != "live" and os.environ.get("DEMO_LIVE") != "1":
raise SystemExit("live latch is off; set DEMO_LIVE=1 to recapture")
response = self.live(req)
row = {"hash": key, "request": json.loads(canonicalize(req)), "response": response}
with self.path.open("a", encoding="utf-8") as fh:
fh.write(json.dumps(row, ensure_ascii=False) + "\n")
self._index[key] = row
return response
Replay never calls live. Record reuses a hit and only captures a miss when the latch is on. Live always captures. Those three sentences are the contract. Everything else is ceremony.
A stub live function, labeled as a stub
The weekend build does not ship a vendor client. The live function is an injected callable so the cassette stays testable without inventing endpoints, model names, or quotas.
# live_stub.py — example only; replace with a real client in the operator’s repo
def live_echo(req: dict) -> dict:
prompt = req["prompt"]
return {
"text": f"STUB:{prompt[:80]}",
"usage": {"input_chars": len(prompt), "output_chars": 0},
}
When a real client exists, it should return a JSON-serializable dict and nothing else. Binary blobs and iterator streams are skipped this weekend. They break the tape format.
Tiny agent loop
The agent is a function, not a platform. It builds one request, asks the cassette, and prints one line. No tool loop. No retries. No hidden second call.
# demo.py
import os
from pathlib import Path
from cassette import Cassette
from live_stub import live_echo
TICKET = "summarize the failing test: test_invoice_total"
def build_request(ticket: str) -> dict:
return {
"task": "ticket-summary",
"prompt": ticket,
"max_output_chars": 400,
}
def main() -> None:
mode = os.environ.get("CASSETTE_MODE", "replay")
tape = Cassette(Path(".cassettes/agent-demo.jsonl"), mode, live_echo) # type: ignore[arg-type]
text = tape.call(build_request(TICKET))["text"]
print(text)
if __name__ == "__main__":
main()
Third-person rule for the demo script: the operator exports mode, runs the file, and reads stdout. No dashboard. No spinner. The golden path is replay.
Commands that prove the demo
Capture once, with the latch on. Then freeze the tape and turn the latch off.
mkdir -p .cassettes
export CASSETTE_MODE=record
export DEMO_LIVE=1
python demo.py | tee /tmp/demo-once.txt
unset DEMO_LIVE
export CASSETTE_MODE=replay
python demo.py | tee /tmp/demo-replay.txt
diff -u /tmp/demo-once.txt /tmp/demo-replay.txt
A second proof is a deliberate miss. Change one character in TICKET under replay. The process must exit non-zero. That miss is the product. Silent fallback to live would reintroduce the original leak.
# expected: cassette miss
CASSETTE_MODE=replay python - <<'PY'
from pathlib import Path
from cassette import Cassette
from live_stub import live_echo
c = Cassette(Path(".cassettes/agent-demo.jsonl"), "replay", live_echo)
c.call({"task": "ticket-summary", "prompt": "different ticket", "max_output_chars": 400})
PY
echo exit:$?
Tests that pin the contract
The test file is short on purpose. It checks hash stability, replay hits, and fail-closed misses. It does not score prose quality.
# test_cassette.py
from pathlib import Path
import pytest
from cassette import Cassette, req_hash
def test_hash_ignores_timestamp():
a = {"prompt": "x", "timestamp": "2026-09-17T10:00:00Z"}
b = {"prompt": "x", "timestamp": "2026-09-17T11:00:00Z"}
assert req_hash(a) == req_hash(b)
def test_replay_hit(tmp_path: Path):
calls = {"n": 0}
def live(req):
calls["n"] += 1
return {"text": "ok"}
tape = Cassette(tmp_path / "t.jsonl", "record", live)
import os
os.environ["DEMO_LIVE"] = "1"
assert tape.call({"prompt": "x"})["text"] == "ok"
os.environ.pop("DEMO_LIVE", None)
replay = Cassette(tmp_path / "t.jsonl", "replay", live)
assert replay.call({"prompt": "x"})["text"] == "ok"
assert calls["n"] == 1
def test_replay_miss_fails_closed(tmp_path: Path):
tape = Cassette(tmp_path / "t.jsonl", "replay", lambda req: {"text": "nope"})
with pytest.raises(SystemExit):
tape.call({"prompt": "missing"})
python -m pytest -q test_cassette.py
If pytest is not installed in the side-project venv, the same assertions can run as a plain script. The weekend cut prefers one green command over a matrix of Python versions.
Where a free model path fits
The cassette is useful without a hosted model. A stub live function is enough to prove the reducer. When the operator needs a real recapture, a hosted path matters because the latch should stay off during the talk and on only for a short recording session.
MonkeyCode is relevant here as one live backend option: free model access and a free server option can recapture a miss without standing up a private GPU box for a Sunday demo. Disclosure: This article was prepared as part of MonkeyCode's product outreach. The cassette still owns the contract. The product does not replace fail-closed replay, and this log does not claim model names, token quotas, hardware sizes, uptime, or permanence.
A practical split looks like this:
- Author the prompt and the request schema on Friday night.
- Recapture the tape once with
CASSETTE_MODE=recordandDEMO_LIVE=1. - Commit the JSONL file next to the demo script.
- Run Sunday’s walkthrough in
replaywith the latch unset.
The live path is a recording studio. The committed tape is the demo.
What this weekend skipped
Skipped work is listed so the next weekend does not pretend it already shipped.
- No redaction beyond dropping
timestamp,request_id, andcwd. Prompts that contain secrets do not belong on this tape. - No file locking. Two parallel recorders will corrupt JSONL.
- No schema version field on the cassette itself. A changed canonicalizer is a breaking change; the operator must recapture.
- No HTTP capture of an entire provider conversation. Only the dict the agent already built.
- No CI job that refreshes tapes on a schedule. Refresh is a human latch, not a cron.
- No comparison against prior published gates in this series (write outboxes, step ledgers, spend meters, stdout golden logs). Those remain separate reducers.
Limitations
Replay is not an evaluation harness. A stable tape can still contain a wrong answer. The miss path only proves that the request bytes changed. It does not prove that the response is safe to merge.
The hash is brittle in a useful way. Reordering keys is fine because canonical JSON sorts them. Reordering list items inside the prompt is not fine. That is intended. Silent list normalization would hide prompt drift.
The live stub in this log returns deterministic echo text. A real recapture will not. After recapture, the committed tape is the source of truth, not the provider’s current mood.
Disk is local. Losing .cassettes/ without a commit loses the demo. Treat the JSONL file as source, the same way a fixture is source.
Who should not use this
Skip this cassette when any of the following is true:
- The agent mutates customer data or sends mail. Recorded text is not a permit to write. Pair writes with a separate outbox if that is the job.
- The prompt may contain tokens, passwords, or private source. JSONL on disk is not a secret store.
- The team needs a signed audit trail, retention policy, or multi-writer capture. This file format is a weekend tape, not a compliance log.
- The demo must show live streaming UI. This contract stores one finished response object.
- The operator wants a quality score for generated code. Hash equality is not a rubric.
For a side project that has to be shown in ten minutes, fail-closed replay is enough. Recapture on a free model path if the tape is empty. Leave the latch off while people watch.
Top comments (0)