Skip to content

Recording & Replay

An agent run is a sequence of effects: model calls, tool calls, memory loads and saves, vector searches. Rig can record each effect, with its request and its outcome, into an effect log. Later you can replay that log: the agent runs again, but every effect is answered from the log instead of the provider or the tool. No API key, no network, and the same answer every time.

Use it to:

  • Test against real model output. Record a live run once, commit the log, and replay it in CI. Unlike a mock, the model’s answers are real.
  • Catch unintended changes. Replay checks that the agent asks for the same things it asked for when the log was recorded. If a prompt or tool argument changes, replay fails at the effect that differs.
  • Debug a production run. Save the log of a run that went wrong and step through it locally.

Create an EffectLogRecorder, keep a handle to it, and give the agent a clone with AgentBuilder::record_to. After the run, recorder.take() returns the log, and agent.stamp(log) writes the agent’s identity (a hash of its configuration and its hook stack) into the log header so replay can check it later. An EffectLog is plain serde data, so save it however you like.

use rig::cassette::agent::AgentReplayExt;
use rig::cassette::effect_log::EffectLogRecorder;
use rig::providers::openai::{self, OpenAI};
let recorder = EffectLogRecorder::new();
let agent = AgentBuilder::new(OpenAI::from_env()?.completion(openai::GPT_5_5))
.preamble("You are a helpful assistant.")
.record_to(recorder.clone())
.build();
let response = agent.prompt("Name three Rust web frameworks.").await?;
println!("{}", response.output());
let log = agent.stamp(recorder.take());
std::fs::write("frameworks.effects.json", serde_json::to_string_pretty(&log)?)?;

recorder.log() returns a snapshot without clearing the recorder. Use EffectLogRecorder::keeping_stream_events() instead of new() when you need the exact chunks of streamed responses, not just their final result.

To replay, register the log’s replayers on a bus and build the agent over that bus. A bus is the channel an agent sends its effects through; normally each agent owns one, but here you create it so the log can answer every effect.

use rig::bus::Bus;
use rig::cassette::agent::replay::register_all;
use rig::cassette::effect_log::EffectLog;
let log: EffectLog = serde_json::from_str(&std::fs::read_to_string("frameworks.effects.json")?)?;
// A bus whose handlers are the log's replayers.
let (dispatcher, registrar, mut driver) = Bus::channel();
register_all(&log, &mut driver)?;
tokio::spawn(driver);
// The same agent, sending its model calls to the recorded model key.
let model_key = log[0].key.clone();
let agent = AgentBuilder::over_bus(dispatcher, registrar, "replay", model_key)
.preamble("You are a helpful assistant.")
.build();
let response = agent.prompt("Name three Rust web frameworks.").await?;
println!("{}", response.output()); // the recorded answer, no provider called

Replay compares each request the agent makes with the recorded one. If they differ, for example because you changed the prompt or the preamble, the run fails with an error that names the effect that diverged. It never guesses an answer. agent.check_replayable(&log) runs the header checks (configuration hash, hook stack, required handlers) up front, before any effect is dispatched.

Tool calls are recorded too, and replay answers them from the log: your tool code does not run. Register each recorded tool on the replay agent with its replayer as the handler, so the model still sees the tool’s definition:

use rig::cassette::effect_log::EffectLogReplayer;
use rig::effect::EffectFamily;
use rig::tool::server::ToolServer;
use rig::tool::RegisteredTool;
let tools = ToolServer::new().run();
if let Some(record) = log.iter().find(|r| r.kind.family() == EffectFamily::Tool) {
let replayer = EffectLogReplayer::for_key(&log, &record.key)?;
tools.add_registered_tool(RegisteredTool::from_handler(replayer)?);
}
let agent = AgentBuilder::over_bus(dispatcher, registrar, "replay", model_key)
.tool_server_handle(tools)
.build();

Repeat the registration for each distinct tool key in the log.

Every effect the agent dispatches: completion calls (and streams), tool calls, memory loads and appends, and vector-store retrievals from dynamic_context and retrieved tools. Hooks are not recorded; they run again during replay, which is why the header records the hook stack and replay refuses a different one.

Logs are tied to the Rig version that wrote them: if the agent configuration format changes, check_replayable refuses old logs and you record them again. Treat logs as generated files. Never edit one by hand to make a test pass; re-record it.