DEV Community

Charlie Hu
Charlie Hu

Posted on

Tape the Model Calls: A Weekend Cassette for Agent Side Projects

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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},
    }
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:$?
Enter fullscreen mode Exit fullscreen mode

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"})
Enter fullscreen mode Exit fullscreen mode
python -m pytest -q test_cassette.py
Enter fullscreen mode Exit fullscreen mode

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:

  1. Author the prompt and the request schema on Friday night.
  2. Recapture the tape once with CASSETTE_MODE=record and DEMO_LIVE=1.
  3. Commit the JSONL file next to the demo script.
  4. Run Sunday’s walkthrough in replay with 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, and cwd. 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)