use std::fs;
use std::path::Path;
use serde_json::json;
use crate::error::Result;
use crate::providers::ReferenceProvider;
use crate::search::Stack;
use crate::tasks::Task;
pub struct RunOutcome {
pub success: bool,
pub cost_usd: f64,
pub latency_s: f64,
pub steps: u64,
pub trace: serde_json::Value,
}
pub trait HarnessAdapter {
fn name(&self) -> &str;
fn run(&self, stack: &Stack, task: &Task, workspace: &Path, seed: u64) -> Result<RunOutcome>;
}
pub struct ReferenceAdapter {
provider: ReferenceProvider,
}
impl Default for ReferenceAdapter {
fn default() -> Self {
ReferenceAdapter {
provider: ReferenceProvider::default(),
}
}
}
impl HarnessAdapter for ReferenceAdapter {
fn name(&self) -> &str {
"reference"
}
fn run(&self, stack: &Stack, task: &Task, workspace: &Path, seed: u64) -> Result<RunOutcome> {
if let (Some(fixtures), Some(pack_dir)) = (&task.fixtures, &task.pack_dir) {
let src = pack_dir.join(fixtures);
if src.exists() {
copy_dir(&src, workspace);
}
}
let model_spec = stack
.llm
.get("planner")
.cloned()
.unwrap_or_else(|| "reference:qwen-7b".to_string());
let model = self.provider.resolve_alias(&model_spec).1;
let expected = task.expected_artifacts();
let mut provider = self.provider.clone();
provider.seed = seed;
let mut cost = 0.0;
let mut latency = 0.0;
let mut success = false;
let mut steps = 0u64;
let mut events = Vec::new();
for step in 0..stack.max_steps {
let max_attempts = (stack.retries + 1) as usize;
let mut any_ok = false;
for _attempt in 0..max_attempts {
let r = provider.complete(&model, 1);
cost += r.cost_usd;
latency += r.latency_ms / 1000.0;
events.push(json!({
"type": "llm.call",
"role": "planner",
"model": model.clone(),
"usage": {"input_tokens": r.usage.input_tokens, "output_tokens": r.usage.output_tokens},
"cost_usd": r.cost_usd,
"latency_ms": r.latency_ms,
}));
if r.content.starts_with("ANSWER_OK") {
any_ok = true;
break;
}
}
if any_ok {
for art in &expected {
let out = workspace.join(art);
if let Some(parent) = out.parent() {
let _ = fs::create_dir_all(parent);
}
let _ = fs::write(&out, format!("artifact for {}", task.id));
}
success = true;
steps = step as u64 + 1;
break;
}
if let Some(max_cost) = stack.max_cost_usd_per_task {
if cost > max_cost {
steps = step as u64 + 1;
break;
}
}
steps = step as u64 + 1;
}
Ok(RunOutcome {
success,
cost_usd: (cost * 1e6).round() / 1e6,
latency_s: (latency * 1e3).round() / 1e3,
steps,
trace: json!(events),
})
}
}
fn copy_dir(src: &Path, dst: &Path) {
let _ = fs::create_dir_all(dst);
if let Ok(rd) = fs::read_dir(src) {
for entry in rd.flatten() {
let from = entry.path();
let to = dst.join(entry.file_name());
if from.is_dir() {
copy_dir(&from, &to);
} else if let Ok(data) = fs::read(&from) {
let _ = fs::write(&to, data);
}
}
}
}