use crate::agent::{Agent, Conversation, RunContext, ToolCallTrace};
use crate::message::Message;
use crate::replay::{diff, Divergence, RecordedCall, Trajectory};
use crate::tool::{Capabilities, Registry, Tool, ToolCtx, ToolOutput};
use anyhow::{bail, Result};
use async_trait::async_trait;
use serde_json::Value;
use std::sync::{Arc, Mutex};
use tokio_util::sync::CancellationToken;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum OnDivergence {
#[default]
Stop,
Error,
Live,
}
struct ReplayState {
calls: Vec<RecordedCall>,
cursor: usize,
dead: bool,
}
enum Action {
Recorded(String, bool),
Refuse(String),
Live,
}
struct ReplayTool {
inner: Arc<dyn Tool>,
spec: Option<crate::message::ToolSpec>,
mode: OnDivergence,
state: Arc<Mutex<ReplayState>>,
cancel: CancellationToken,
}
struct SpecTool {
spec: crate::message::ToolSpec,
}
#[async_trait]
impl Tool for SpecTool {
fn name(&self) -> &str {
&self.spec.name
}
fn description(&self) -> &str {
&self.spec.description
}
fn input_schema(&self) -> Value {
self.spec.input_schema.clone()
}
fn read_only(&self) -> bool {
false
}
fn capabilities(&self) -> Capabilities {
Capabilities {
private_data: true,
untrusted_input: true,
external_send: true,
destructive: true,
}
}
async fn call(&self, _input: Value, _ctx: &ToolCtx) -> Result<ToolOutput> {
Ok(ToolOutput::err(
"this tool exists only on the recorded surface; the replay answers from \
the recording, and this call has nothing recorded to answer with",
))
}
}
impl ReplayTool {
fn decide(&self, input: &Value) -> Action {
let mut st = self.state.lock().unwrap();
if st.dead {
return match self.mode {
OnDivergence::Live => Action::Live,
_ => Action::Refuse(
"replay: the run has diverged from the recording; no recorded result \
exists for this call"
.into(),
),
};
}
let Some(want) = st.calls.get(st.cursor) else {
st.dead = true;
return match self.mode {
OnDivergence::Live => Action::Live,
_ => {
self.cancel.cancel();
Action::Refuse(format!(
"replay: the recording ended after {} calls and has no result for \
this one; stopping",
st.calls.len()
))
}
};
};
if want.name != self.inner.name() {
let msg = format!(
"replay: recorded call #{} was `{}`, not `{}`; stopping",
st.cursor,
want.name,
self.inner.name()
);
st.dead = true;
return match self.mode {
OnDivergence::Live => Action::Live,
_ => {
self.cancel.cancel();
Action::Refuse(msg)
}
};
}
let _ = input;
let out = Action::Recorded(want.output.clone(), want.is_error);
st.cursor += 1;
out
}
}
#[async_trait]
impl Tool for ReplayTool {
fn name(&self) -> &str {
self.spec
.as_ref()
.map(|s| s.name.as_str())
.unwrap_or_else(|| self.inner.name())
}
fn description(&self) -> &str {
self.spec
.as_ref()
.map(|s| s.description.as_str())
.unwrap_or_else(|| self.inner.description())
}
fn input_schema(&self) -> Value {
self.spec
.as_ref()
.map(|s| s.input_schema.clone())
.unwrap_or_else(|| self.inner.input_schema())
}
fn read_only(&self) -> bool {
self.inner.read_only()
}
fn capabilities(&self) -> Capabilities {
let caps = self.inner.capabilities();
match self.mode {
OnDivergence::Live => caps,
OnDivergence::Stop | OnDivergence::Error => Capabilities {
external_send: false,
..caps
},
}
}
async fn call(&self, input: Value, ctx: &ToolCtx) -> Result<ToolOutput> {
match self.decide(&input) {
Action::Recorded(content, is_error) => Ok(ToolOutput {
content,
is_error,
external: false,
refusal: false,
}),
Action::Refuse(msg) => Ok(ToolOutput::err(msg)),
Action::Live => self.inner.call(input, ctx).await,
}
}
}
pub fn replay_registry(
recorded_tools: &[String],
live: &Registry,
surface_only: Option<&Registry>,
recorded_specs: &[crate::message::ToolSpec],
calls: Vec<RecordedCall>,
mode: OnDivergence,
cancel: CancellationToken,
) -> Result<Registry> {
let state = Arc::new(Mutex::new(ReplayState {
calls,
cursor: 0,
dead: false,
}));
let mut registry = Registry::new();
for name in recorded_tools {
let executes = match mode {
OnDivergence::Live => true,
OnDivergence::Stop | OnDivergence::Error => false,
};
let spec = (!executes)
.then(|| recorded_specs.iter().find(|s| s.name == *name))
.flatten();
let stand_in = (!executes)
.then(|| surface_only.and_then(|r| r.get(name)))
.flatten();
let inner: Arc<dyn Tool> = match live.get(name).or(stand_in) {
Some(tool) => Arc::clone(tool),
None => match spec {
Some(s) => Arc::new(SpecTool { spec: s.clone() }),
None => bail!(
"recorded tool `{name}` is not available now, so the replay cannot \
offer the tool surface the model saw. Enable whatever provided it \
(an MCP server? a search backend?) and retry — or, if it no longer \
exists anywhere, only a recording whose surface blob the store \
still holds (`tools_hash`) can be replayed without it"
),
},
};
registry.insert(Arc::new(ReplayTool {
inner,
spec: spec.cloned(),
mode,
state: Arc::clone(&state),
cancel: cancel.clone(),
}));
}
Ok(registry)
}
pub fn replay_surface_specs(
recorded_tools: &[String],
live: &Registry,
surface_only: Option<&Registry>,
recorded_specs: &[crate::message::ToolSpec],
) -> Result<Vec<crate::message::ToolSpec>> {
let registry = replay_registry(
recorded_tools,
live,
surface_only,
recorded_specs,
Vec::new(),
OnDivergence::Stop,
CancellationToken::new(),
)?;
Ok(registry.specs())
}
#[derive(Debug)]
pub struct ReplayReport {
pub divergences: Vec<Divergence>,
pub replayed_calls: Vec<ToolCallTrace>,
pub recorded_calls: usize,
pub turns: usize,
pub stopped_early: bool,
pub final_text: String,
pub stats: crate::session::RunStats,
pub call_base: usize,
}
impl ReplayReport {
pub fn structural(&self) -> impl Iterator<Item = &Divergence> {
self.divergences.iter().filter(|d| d.is_structural())
}
}
pub async fn drive(
agent: &Agent,
cx: &RunContext,
trajectory: &Trajectory,
) -> Result<ReplayReport> {
let mut convo = Conversation::new();
let mut replayed: Vec<ToolCallTrace> = Vec::new();
let mut final_text = String::new();
let mut turns = 0;
let mut stopped_early = false;
let mut stats = crate::session::RunStats {
usage_complete: true,
..Default::default()
};
for turn in &trajectory.turns {
convo.push(Message::user(turn.clone()));
let outcome = agent.run_in(cx, &mut convo, None).await?;
stats.absorb(&outcome);
replayed.extend(outcome.tool_calls);
final_text = outcome.text;
turns += 1;
if cx.cancelled() {
stopped_early = true;
break;
}
}
Ok(ReplayReport {
divergences: diff(&trajectory.calls, &replayed),
replayed_calls: replayed,
recorded_calls: trajectory.calls.len(),
turns,
stopped_early,
final_text,
stats,
call_base: 0,
})
}
pub async fn drive_branch(
agent: &Agent,
cx: &RunContext,
seed: Vec<Message>,
trajectory: &Trajectory,
call_base: usize,
) -> Result<ReplayReport> {
let base = call_base.min(trajectory.calls.len());
let mut convo = Conversation::resumed(seed, Default::default());
let mut stats = crate::session::RunStats {
usage_complete: true,
..Default::default()
};
let outcome = agent.run_in(cx, &mut convo, None).await?;
stats.absorb(&outcome);
Ok(ReplayReport {
divergences: crate::replay::diff_from(base, &trajectory.calls[base..], &outcome.tool_calls),
replayed_calls: outcome.tool_calls,
recorded_calls: trajectory.calls.len(),
turns: 0,
stopped_early: cx.cancelled(),
final_text: outcome.text,
stats,
call_base: base,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::agent::Budget;
use crate::config::{AgentConfig, PermissionMode};
use crate::message::{Block, CompletionRequest, CompletionResponse, StopReason, Usage};
use crate::provider::{Provider, StreamSink};
use crate::tool::ModeApprover;
use serde_json::json;
struct EchoTool;
#[async_trait]
impl Tool for EchoTool {
fn name(&self) -> &str {
"echo"
}
fn description(&self) -> &str {
"Echo the `value` argument back."
}
fn input_schema(&self) -> Value {
json!({"type": "object"})
}
fn read_only(&self) -> bool {
true
}
async fn call(&self, input: Value, _ctx: &ToolCtx) -> Result<ToolOutput> {
Ok(ToolOutput::ok(format!(
"live: {}",
input.get("value").and_then(Value::as_str).unwrap_or("")
)))
}
}
struct NeverRun;
#[async_trait]
impl Tool for NeverRun {
fn name(&self) -> &str {
"never_run"
}
fn description(&self) -> &str {
"Described, never called."
}
fn input_schema(&self) -> Value {
json!({"type": "object"})
}
fn read_only(&self) -> bool {
true
}
async fn call(&self, _input: Value, _ctx: &ToolCtx) -> Result<ToolOutput> {
panic!("a surface-only tool was executed by a replay");
}
}
#[tokio::test]
async fn a_surface_only_tool_is_described_and_never_executed() {
let mut fallback = Registry::new();
fallback.insert(Arc::new(NeverRun));
let registry = replay_registry(
&[NeverRun.name().to_string()],
&Registry::new(),
Some(&fallback),
&[],
vec![RecordedCall {
name: NeverRun.name().to_string(),
input: json!({}),
output: "the recorded answer".into(),
is_error: false,
}],
OnDivergence::Stop,
CancellationToken::new(),
)
.expect("the stand-in supplies the surface");
let tool = registry.get(NeverRun.name()).expect("registered");
assert_eq!(tool.description(), NeverRun.description());
assert_eq!(tool.input_schema(), NeverRun.input_schema());
let out = tool
.call(json!({}), &ToolCtx::default())
.await
.expect("answered from the recording");
assert_eq!(out.content, "the recorded answer");
}
#[test]
fn a_surface_only_tool_fills_a_gap_under_stop_and_never_otherwise() {
let live = Registry::new();
let mut fallback = Registry::new();
fallback.insert(Arc::new(OtherTool));
let recorded = vec![OtherTool.name().to_string()];
let stopped = replay_registry(
&recorded,
&live,
Some(&fallback),
&[],
Vec::new(),
OnDivergence::Stop,
CancellationToken::new(),
)
.expect("a stand-in describes the recorded surface under Stop");
assert!(stopped.get(OtherTool.name()).is_some());
assert!(replay_registry(
&recorded,
&live,
Some(&fallback),
&[],
Vec::new(),
OnDivergence::Error,
CancellationToken::new(),
)
.is_ok());
assert!(replay_registry(
&recorded,
&live,
Some(&fallback),
&[],
Vec::new(),
OnDivergence::Live,
CancellationToken::new(),
)
.is_err());
assert!(replay_registry(
&recorded,
&live,
None,
&[],
Vec::new(),
OnDivergence::Stop,
CancellationToken::new(),
)
.is_err());
}
#[test]
fn fidelity_compares_the_replay_surface_not_the_bare_live_registry() {
let mut live = Registry::new();
live.insert(Arc::new(EchoTool));
let mut surface_only = Registry::new();
surface_only.insert(Arc::new(OtherTool));
let recorded_tools = vec![EchoTool.name().to_string(), OtherTool.name().to_string()];
let mut recorded_registry = Registry::new();
recorded_registry.insert(Arc::new(EchoTool));
recorded_registry.insert(Arc::new(OtherTool));
let recorded_hash = crate::surface::fingerprint(&recorded_registry.specs());
assert_eq!(
crate::surface::Fidelity::of(Some(&recorded_hash), &live.specs()),
crate::surface::Fidelity::Differs,
"live alone lacks `other` by construction — this is the bug being fixed"
);
let surface_specs =
replay_surface_specs(&recorded_tools, &live, Some(&surface_only), &[]).unwrap();
assert_eq!(
crate::surface::Fidelity::of(Some(&recorded_hash), &surface_specs),
crate::surface::Fidelity::Matches,
"identical specs, narrowed the same way the recording was — this must match"
);
}
struct OtherTool;
#[async_trait]
impl Tool for OtherTool {
fn name(&self) -> &str {
"other"
}
fn description(&self) -> &str {
"A second tool."
}
fn input_schema(&self) -> Value {
json!({"type": "object"})
}
fn read_only(&self) -> bool {
true
}
async fn call(&self, _input: Value, _ctx: &ToolCtx) -> Result<ToolOutput> {
Ok(ToolOutput::ok("live: other"))
}
}
fn live_registry() -> Registry {
let mut r = Registry::new();
r.insert(Arc::new(EchoTool));
r.insert(Arc::new(OtherTool));
r
}
fn recorded(name: &str, input: Value, output: &str) -> RecordedCall {
RecordedCall {
name: name.into(),
input,
output: output.into(),
is_error: false,
}
}
fn replay_reg(
calls: Vec<RecordedCall>,
mode: OnDivergence,
cancel: &CancellationToken,
) -> Registry {
replay_registry(
&["echo".to_string(), "other".to_string()],
&live_registry(),
None,
&[],
calls,
mode,
cancel.clone(),
)
.unwrap()
}
#[tokio::test]
async fn matching_calls_replay_the_recorded_outputs_in_order() {
let cancel = CancellationToken::new();
let reg = replay_reg(
vec![
recorded("echo", json!({"value": "a"}), "first"),
recorded("other", json!({}), "second"),
],
OnDivergence::Stop,
&cancel,
);
let ctx = ToolCtx::default();
let out = reg
.get("echo")
.unwrap()
.call(json!({"value": "a"}), &ctx)
.await
.unwrap();
assert_eq!(out.content, "first");
assert!(!out.is_error);
let out = reg
.get("other")
.unwrap()
.call(json!({}), &ctx)
.await
.unwrap();
assert_eq!(out.content, "second");
assert!(!cancel.is_cancelled());
}
#[tokio::test]
async fn a_recorded_error_replays_as_an_error() {
let cancel = CancellationToken::new();
let reg = replay_reg(
vec![RecordedCall {
name: "echo".into(),
input: json!({}),
output: "no such file".into(),
is_error: true,
}],
OnDivergence::Stop,
&cancel,
);
let out = reg
.get("echo")
.unwrap()
.call(json!({}), &ToolCtx::default())
.await
.unwrap();
assert!(
out.is_error,
"the model must see the same failure it saw at record time"
);
assert_eq!(out.content, "no such file");
}
#[tokio::test]
async fn a_different_tool_stops_the_run_and_kills_the_recording() {
let cancel = CancellationToken::new();
let reg = replay_reg(
vec![
recorded("echo", json!({}), "first"),
recorded("echo", json!({}), "second"),
],
OnDivergence::Stop,
&cancel,
);
let ctx = ToolCtx::default();
let out = reg
.get("other")
.unwrap()
.call(json!({}), &ctx)
.await
.unwrap();
assert!(out.is_error);
assert!(
cancel.is_cancelled(),
"a structural divergence must stop the run"
);
let out = reg
.get("echo")
.unwrap()
.call(json!({}), &ctx)
.await
.unwrap();
assert!(out.is_error);
assert!(out.content.contains("diverged"));
}
#[tokio::test]
async fn running_past_the_end_of_the_recording_stops() {
let cancel = CancellationToken::new();
let reg = replay_reg(
vec![recorded("echo", json!({}), "only")],
OnDivergence::Stop,
&cancel,
);
let ctx = ToolCtx::default();
reg.get("echo")
.unwrap()
.call(json!({}), &ctx)
.await
.unwrap();
let out = reg
.get("echo")
.unwrap()
.call(json!({}), &ctx)
.await
.unwrap();
assert!(out.is_error);
assert!(cancel.is_cancelled());
}
#[tokio::test]
async fn different_arguments_still_replay_and_do_not_stop() {
let cancel = CancellationToken::new();
let reg = replay_reg(
vec![recorded("echo", json!({"value": "a.md"}), "contents")],
OnDivergence::Stop,
&cancel,
);
let out = reg
.get("echo")
.unwrap()
.call(json!({"value": "./a.md"}), &ToolCtx::default())
.await
.unwrap();
assert_eq!(out.content, "contents");
assert!(
!cancel.is_cancelled(),
"argument differences are reported by the diff, not fatal"
);
}
#[tokio::test]
async fn live_mode_falls_through_to_the_real_tool_on_divergence() {
let cancel = CancellationToken::new();
let reg = replay_reg(
vec![recorded("echo", json!({}), "recorded")],
OnDivergence::Live,
&cancel,
);
let ctx = ToolCtx::default();
let out = reg
.get("other")
.unwrap()
.call(json!({}), &ctx)
.await
.unwrap();
assert_eq!(out.content, "live: other");
assert!(!cancel.is_cancelled(), "live mode keeps going");
let out = reg
.get("echo")
.unwrap()
.call(json!({"value": "x"}), &ctx)
.await
.unwrap();
assert_eq!(out.content, "live: x");
}
#[tokio::test]
async fn replay_narrows_external_send_in_the_modes_where_nothing_executes() {
struct Sender;
#[async_trait]
impl Tool for Sender {
fn name(&self) -> &str {
"mail_send"
}
fn description(&self) -> &str {
"Sends."
}
fn input_schema(&self) -> Value {
json!({"type": "object"})
}
fn read_only(&self) -> bool {
false
}
fn capabilities(&self) -> Capabilities {
Capabilities {
private_data: true,
external_send: true,
..Default::default()
}
}
async fn call(&self, _input: Value, _ctx: &ToolCtx) -> Result<ToolOutput> {
Ok(ToolOutput::ok("sent"))
}
}
let mut live = Registry::new();
live.insert(Arc::new(Sender));
let caps_under = |mode| {
let reg = replay_registry(
&["mail_send".to_string()],
&live,
None,
&[],
Vec::new(),
mode,
CancellationToken::new(),
)
.unwrap();
reg.get("mail_send").unwrap().capabilities()
};
for mode in [OnDivergence::Stop, OnDivergence::Error] {
let caps = caps_under(mode);
assert!(!caps.external_send, "a replayed send sends nothing");
assert!(caps.private_data, "private data must not narrow");
}
assert!(caps_under(OnDivergence::Live).external_send);
}
#[tokio::test]
async fn a_dead_recorded_tool_is_rebuilt_from_its_recorded_spec_under_stop() {
let recorded_spec = crate::message::ToolSpec {
name: "pkg__kg_entity".into(),
description: "Look up one entity in the knowledge graph.".into(),
input_schema: json!({"type": "object", "properties": {"name": {"type": "string"}}}),
};
let specs = vec![recorded_spec.clone()];
let names = vec![recorded_spec.name.clone()];
let registry = replay_registry(
&names,
&Registry::new(),
None,
&specs,
vec![RecordedCall {
name: recorded_spec.name.clone(),
input: json!({"name": "Yuqi"}),
output: "the recorded entity".into(),
is_error: false,
}],
OnDivergence::Stop,
CancellationToken::new(),
)
.expect("the recorded spec supplies the surface");
let tool = registry.get(&recorded_spec.name).expect("registered");
assert_eq!(tool.description(), recorded_spec.description);
assert_eq!(tool.input_schema(), recorded_spec.input_schema);
assert!(tool.capabilities().private_data, "unknown is never clean");
assert!(
!tool.capabilities().external_send,
"a replayed call sends nothing"
);
let out = tool
.call(json!({"name": "Yuqi"}), &ToolCtx::default())
.await
.unwrap();
assert_eq!(out.content, "the recorded entity");
assert!(replay_registry(
&names,
&Registry::new(),
None,
&specs,
Vec::new(),
OnDivergence::Live,
CancellationToken::new(),
)
.is_err());
}
#[test]
fn a_recorded_spec_overrides_a_live_tools_words_under_stop_only() {
let recorded_spec = crate::message::ToolSpec {
name: EchoTool.name().into(),
description: "The words the recording was sent.".into(),
input_schema: json!({"type": "object"}),
};
let specs = vec![recorded_spec.clone()];
let names = vec![recorded_spec.name.clone()];
let recorded_hash = crate::surface::fingerprint(&specs);
let stopped = replay_registry(
&names,
&live_registry(),
None,
&specs,
Vec::new(),
OnDivergence::Stop,
CancellationToken::new(),
)
.unwrap();
assert_eq!(
stopped.get("echo").unwrap().description(),
recorded_spec.description,
"a probe replays the recorded surface, not today's rewording"
);
let surface = replay_surface_specs(&names, &live_registry(), None, &specs).unwrap();
assert_eq!(
crate::surface::Fidelity::of(Some(&recorded_hash), &surface),
crate::surface::Fidelity::Matches,
"the rebuilt surface is byte-faithful to the recording"
);
let live = replay_registry(
&names,
&live_registry(),
None,
&specs,
Vec::new(),
OnDivergence::Live,
CancellationToken::new(),
)
.unwrap();
assert_eq!(
live.get("echo").unwrap().description(),
EchoTool.description(),
"live mode executes today's tool and must show today's words"
);
}
#[test]
fn a_recorded_tool_missing_today_is_an_error_not_a_shrink() {
let err = replay_registry(
&["echo".to_string(), "gone".to_string()],
&live_registry(),
None,
&[],
Vec::new(),
OnDivergence::Stop,
CancellationToken::new(),
)
.map(|_| ())
.unwrap_err()
.to_string();
assert!(err.contains("gone"), "{err}");
}
struct Scripted(Mutex<Vec<CompletionResponse>>);
#[async_trait]
impl Provider for Scripted {
fn id(&self) -> &str {
"scripted"
}
fn default_model(&self) -> &str {
"scripted-1"
}
async fn complete(
&self,
_req: &CompletionRequest,
_sink: Option<&StreamSink>,
) -> Result<CompletionResponse> {
let mut turns = self.0.lock().unwrap();
anyhow::ensure!(!turns.is_empty(), "provider ran out of scripted turns");
Ok(turns.remove(0))
}
}
fn assistant(blocks: Vec<Block>, stop: StopReason) -> CompletionResponse {
CompletionResponse {
message: Message::assistant(blocks),
stop_reason: stop,
usage: Usage {
input_tokens: 10,
output_tokens: 5,
..Usage::default()
},
refusal: None,
model: "scripted-1".into(),
malformed_tool_args: 0,
}
}
fn tool_use(id: &str, name: &str, input: Value) -> Block {
Block::ToolUse {
id: id.into(),
name: name.into(),
input,
}
}
async fn drive_scripted(
turns: Vec<CompletionResponse>,
calls: Vec<RecordedCall>,
trajectory: &Trajectory,
) -> ReplayReport {
let cancel = CancellationToken::new();
let registry = replay_reg(calls, OnDivergence::Stop, &cancel);
let approver = Arc::new(ModeApprover {
mode: PermissionMode::Allow,
});
let agent = Agent::new(
Box::new(Scripted(Mutex::new(turns))),
registry,
approver.clone(),
ToolCtx::default(),
AgentConfig::default(),
None,
)
.unwrap();
let cx = RunContext::new(ToolCtx::default(), approver)
.with_cancel(cancel)
.with_budget(Budget::turns(8));
drive(&agent, &cx, trajectory).await.unwrap()
}
#[tokio::test]
async fn a_faithful_replay_reports_no_divergence() {
let calls = vec![recorded("echo", json!({"value": "a"}), "first")];
let trajectory = Trajectory {
turns: vec!["do the thing".into()],
calls: calls.clone(),
final_text: "done".into(),
steered: false,
};
let report = drive_scripted(
vec![
assistant(
vec![tool_use("t1", "echo", json!({"value": "a"}))],
StopReason::ToolUse,
),
assistant(vec![Block::text("done")], StopReason::EndTurn),
],
calls,
&trajectory,
)
.await;
assert!(report.divergences.is_empty(), "{:?}", report.divergences);
assert!(!report.stopped_early);
assert_eq!(report.final_text, "done");
assert_eq!(report.turns, 1);
}
#[tokio::test]
async fn a_branch_continues_the_recording_instead_of_regenerating_it() {
let calls = vec![
recorded("echo", json!({"value": "a"}), "first"),
recorded("other", json!({}), "second"),
];
let trajectory = Trajectory {
turns: vec!["do the thing".into()],
calls: calls.clone(),
final_text: "done".into(),
steered: true,
};
let seed = vec![
Message::user("do the thing"),
Message::assistant(vec![tool_use("t1", "echo", json!({"value": "a"}))]),
Message::tool_results(vec![Block::ToolResult {
tool_use_id: "t1".into(),
content: "first".into(),
is_error: false,
}]),
];
let run = |turns: Vec<CompletionResponse>| {
let calls = calls.clone();
let trajectory = trajectory.clone();
let seed = seed.clone();
async move {
let cancel = CancellationToken::new();
let registry = replay_reg(calls[1..].to_vec(), OnDivergence::Stop, &cancel);
let approver = Arc::new(ModeApprover {
mode: PermissionMode::Allow,
});
let agent = Agent::new(
Box::new(Scripted(Mutex::new(turns))),
registry,
approver.clone(),
ToolCtx::default(),
AgentConfig::default(),
None,
)
.unwrap();
let cx = RunContext::new(ToolCtx::default(), approver)
.with_cancel(cancel)
.with_budget(Budget::turns(8));
drive_branch(&agent, &cx, seed, &trajectory, 1)
.await
.unwrap()
}
};
let report = run(vec![
assistant(
vec![tool_use("t2", "other", json!({}))],
StopReason::ToolUse,
),
assistant(vec![Block::text("done")], StopReason::EndTurn),
])
.await;
assert!(report.divergences.is_empty(), "{:?}", report.divergences);
assert_eq!(report.call_base, 1);
assert_eq!(
report.replayed_calls.len(),
1,
"only the continuation is the model's"
);
assert_eq!(report.final_text, "done");
let report = run(vec![
assistant(
vec![tool_use("t2", "echo", json!({"value": "z"}))],
StopReason::ToolUse,
),
assistant(vec![Block::text("gave up")], StopReason::EndTurn),
])
.await;
let structural: Vec<_> = report.structural().collect();
assert!(!structural.is_empty(), "{:?}", report.divergences);
assert_eq!(structural[0].index(), 1, "{:?}", report.divergences);
}
#[tokio::test]
async fn a_divergent_replay_stops_early_and_reports_it() {
let calls = vec![
recorded("echo", json!({"value": "a"}), "first"),
recorded("echo", json!({"value": "b"}), "second"),
];
let trajectory = Trajectory {
turns: vec!["do the thing".into(), "never reached".into()],
calls: calls.clone(),
final_text: "done".into(),
steered: false,
};
let report = drive_scripted(
vec![
assistant(
vec![tool_use("t1", "other", json!({}))],
StopReason::ToolUse,
),
assistant(vec![Block::text("gave up")], StopReason::EndTurn),
],
calls,
&trajectory,
)
.await;
assert!(
report.stopped_early,
"the second recorded turn must never be fed"
);
assert_eq!(report.turns, 1);
let structural: Vec<_> = report.structural().collect();
assert!(!structural.is_empty(), "{:?}", report.divergences);
}
}