STEP 01 Point your SDK at the tape
import { openCassette } from "stonetape";
import OpenAI from "openai";
const tape = openCassette("tests/cassettes/weather-agent.yaml");
const client = new OpenAI({ fetch: tape.fetch });
// ... run your agent ...
tape.close();
STEP 02 Record once, replay forever
STONETAPE_MODE=record vitest # hits the real API once, writes the cassette
vitest # replays forever: no network, no keys
Or let the vitest helper handle the cassette lifecycle:
import { cassette } from "stonetape/vitest";
test("weather agent", cassette("weather-agent", async ({ fetch }) => {
const client = new OpenAI({ fetch });
const result = await runAgent(client, "What's the weather in Cluj?");
expect(result.city).toBe("Cluj");
}));
STEP 03 The cassette is a file. Commit it.
# tests/cassettes/weather-agent.yaml (committed to git)
version: 1
recorded_at: 2026-02-11T21:47:03Z
interactions:
- request:
url: https://api.openai.com/v1/chat/completions
body:
model: gpt-4o
messages: [ ... ]
response:
status: 200
body:
choices:
- message:
tool_calls:
- function:
name: get_weather
arguments: '{"city":"Cluj"}'
Versioned, language-agnostic schema. Secrets auto-redacted. When your
prompt changes behavior, the replay fails with an explicit chain diff,
not a cryptic error.
SIDE B Not in your test process? Proxy it.
# record once (real upstream):
stonetape proxy --cassette agent.yaml \
--target https://api.openai.com --mode record
OPENAI_BASE_URL=http://127.0.0.1:8787/v1 your-agent-cli "do the thing"
# replay forever — upstream not contacted, no keys:
stonetape proxy --cassette agent.yaml \
--target https://api.openai.com --port 8787
OPENAI_BASE_URL=http://127.0.0.1:8787/v1 your-agent-cli "do the thing"
Agent CLIs, sidecar services, Python or Go processes — run
stonetape proxy and point them at it via their base-URL
env. Any language records; replay works with the upstream dead.
Same matching, same redaction, same fail-closed semantics: unrecorded
requests answer 501 with the full mismatch explanation.