use crate::setup::Prepared;
use anyhow::Result;
use mecha_core::agent::{Agent, RunContext};
use mecha_core::config::{PermissionMode, ProviderConfig};
use mecha_core::harness::ConfigChange;
use mecha_core::replay::{extract, Trajectory};
use mecha_core::replay_run::{drive, replay_registry, OnDivergence};
use mecha_core::session::{RunConfig, RunStats, Session, SessionMeta};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio_util::sync::CancellationToken;
pub struct EpisodePrep {
pub id: String,
trajectory: Trajectory,
recorded: RunConfig,
}
pub fn prepare_episode(path: &Path, id: &str) -> Result<Result<EpisodePrep, String>> {
let (_, convo) = match Session::load(path) {
Ok(loaded) => loaded,
Err(e) => return Ok(Err(format!("session unreadable: {e:#}"))),
};
let trajectory = extract(&convo.messages);
if trajectory.turns.is_empty() {
return Ok(Err("no user turns".into()));
}
if trajectory.calls.is_empty() {
return Ok(Err("no recorded tool calls".into()));
}
let Some(recorded) = Session::run_configs(path)?.first().cloned() else {
return Ok(Err("no RunConfig recorded".into()));
};
Ok(Ok(EpisodePrep {
id: id.to_string(),
trajectory,
recorded,
}))
}
pub fn sample_episodes(
sessions_dir: &Path,
model: &str,
want: usize,
) -> Result<(Vec<EpisodePrep>, usize)> {
let mut listed: Vec<(SessionMeta, PathBuf)> = Session::list(sessions_dir)?;
listed.sort_by_key(|entry| std::cmp::Reverse(entry.0.created_at));
let mut preps = Vec::new();
let mut skipped = 0usize;
for (meta, path) in listed {
if preps.len() >= want {
break;
}
if meta.model != model {
continue;
}
match prepare_episode(&path, &meta.id)? {
Ok(prep) => preps.push(prep),
Err(_) => skipped += 1,
}
}
Ok((preps, skipped))
}
pub struct ArmOutcome {
pub stats: RunStats,
pub diverged: bool,
}
pub async fn drive_episode(
prepared: &Prepared,
provider_cfg: &ProviderConfig,
model: &str,
prep: &EpisodePrep,
change: Option<&ConfigChange>,
) -> Result<Result<ArmOutcome, String>> {
let recorded = &prep.recorded;
let cancel = CancellationToken::new();
let registry = match replay_registry(
&recorded.tools,
prepared.agent.registry(),
prep.trajectory.calls.clone(),
OnDivergence::Stop,
cancel.clone(),
) {
Ok(reg) => reg,
Err(e) => return Ok(Err(format!("{e:#}"))),
};
let approver: Arc<dyn mecha_core::tool::Approver> = Arc::new(mecha_core::tool::ModeApprover {
mode: PermissionMode::Allow,
});
let mut agent_cfg = prepared.config.agent.clone();
agent_cfg.system_prompt = recorded.system_prompt.clone();
agent_cfg.system_prompt_file = None;
agent_cfg.effort = recorded.effort;
agent_cfg.thinking = recorded.thinking;
agent_cfg.cache_prompt = recorded.cache_prompt;
agent_cfg.max_tokens = recorded.max_tokens;
agent_cfg.max_turns = recorded.max_turns;
agent_cfg.max_output_tokens = recorded.max_output_tokens;
agent_cfg.max_cost_usd = recorded.max_cost_usd;
agent_cfg.compact_at_tokens = recorded.compact_at_tokens;
agent_cfg.compact_keep_recent = recorded.compact_keep_recent;
if let Some(change) = change {
if let Err(e) = change.apply_to_agent(&mut agent_cfg) {
return Ok(Err(format!("candidate change failed to apply: {e:#}")));
}
}
let mut tool_ctx = mecha_core::tool::ToolCtx {
workspace: recorded.workspace.clone(),
..Default::default()
};
if !recorded.workspace.exists() {
tool_ctx.workspace = std::env::temp_dir();
}
let agent = Agent::new(
mecha_core::provider::build(provider_cfg)?,
registry,
Arc::clone(&approver),
tool_ctx.clone(),
agent_cfg.clone(),
Some(model.to_string()),
)?;
let cx = RunContext::new(tool_ctx, approver)
.with_cancel(cancel)
.with_compact_at(agent_cfg.compact_at_tokens);
match drive(&agent, &cx, &prep.trajectory).await {
Ok(report) => Ok(Ok(ArmOutcome {
diverged: report.stopped_early,
stats: report.stats,
})),
Err(e) => Ok(Err(format!("replay failed: {e:#}"))),
}
}