Skip to main content

harness_lock_core/
adapters.rs

1//! Harness adapters — a baseline (reference) adapter for the local loop.
2
3use std::fs;
4use std::path::Path;
5
6use serde_json::json;
7
8use crate::error::Result;
9use crate::providers::ReferenceProvider;
10use crate::search::Stack;
11use crate::tasks::Task;
12
13/// The resolved outcome of running one task through a harness.
14pub struct RunOutcome {
15    pub success: bool,
16    pub cost_usd: f64,
17    pub latency_s: f64,
18    pub steps: u64,
19    /// Trace events (a serde array) for content-addressed storage.
20    pub trace: serde_json::Value,
21}
22
23/// A harness adapter runs one `(stack, task, seed)` and produces a `RunOutcome`.
24pub trait HarnessAdapter {
25    fn name(&self) -> &str;
26    fn run(&self, stack: &Stack, task: &Task, workspace: &Path, seed: u64) -> Result<RunOutcome>;
27}
28
29/// Baseline/offline harness that drives the deterministic reference provider.
30pub struct ReferenceAdapter {
31    provider: ReferenceProvider,
32}
33
34impl Default for ReferenceAdapter {
35    fn default() -> Self {
36        ReferenceAdapter {
37            provider: ReferenceProvider::default(),
38        }
39    }
40}
41
42impl HarnessAdapter for ReferenceAdapter {
43    fn name(&self) -> &str {
44        "reference"
45    }
46
47    fn run(&self, stack: &Stack, task: &Task, workspace: &Path, seed: u64) -> Result<RunOutcome> {
48        // copy fixtures into the workspace
49        if let (Some(fixtures), Some(pack_dir)) = (&task.fixtures, &task.pack_dir) {
50            let src = pack_dir.join(fixtures);
51            if src.exists() {
52                copy_dir(&src, workspace);
53            }
54        }
55        let model_spec = stack
56            .llm
57            .get("planner")
58            .cloned()
59            .unwrap_or_else(|| "reference:qwen-7b".to_string());
60        let model = self.provider.resolve_alias(&model_spec).1;
61        let expected = task.expected_artifacts();
62
63        let mut provider = self.provider.clone();
64        provider.seed = seed;
65        let mut cost = 0.0;
66        let mut latency = 0.0;
67        let mut success = false;
68        let mut steps = 0u64;
69        let mut events = Vec::new();
70
71        for step in 0..stack.max_steps {
72            // agent-level retries: attempt this step up to 1+retries times
73            let max_attempts = (stack.retries + 1) as usize;
74            let mut any_ok = false;
75            for _attempt in 0..max_attempts {
76                let r = provider.complete(&model, 1);
77                cost += r.cost_usd;
78                latency += r.latency_ms / 1000.0;
79                events.push(json!({
80                    "type": "llm.call",
81                    "role": "planner",
82                    "model": model.clone(),
83                    "usage": {"input_tokens": r.usage.input_tokens, "output_tokens": r.usage.output_tokens},
84                    "cost_usd": r.cost_usd,
85                    "latency_ms": r.latency_ms,
86                }));
87                if r.content.starts_with("ANSWER_OK") {
88                    any_ok = true;
89                    break;
90                }
91            }
92            if any_ok {
93                for art in &expected {
94                    let out = workspace.join(art);
95                    if let Some(parent) = out.parent() {
96                        let _ = fs::create_dir_all(parent);
97                    }
98                    let _ = fs::write(&out, format!("artifact for {}", task.id));
99                }
100                success = true;
101                steps = step as u64 + 1;
102                break;
103            }
104            if let Some(max_cost) = stack.max_cost_usd_per_task {
105                if cost > max_cost {
106                    steps = step as u64 + 1;
107                    break;
108                }
109            }
110            steps = step as u64 + 1;
111        }
112
113        Ok(RunOutcome {
114            success,
115            cost_usd: (cost * 1e6).round() / 1e6,
116            latency_s: (latency * 1e3).round() / 1e3,
117            steps,
118            trace: json!(events),
119        })
120    }
121}
122
123fn copy_dir(src: &Path, dst: &Path) {
124    let _ = fs::create_dir_all(dst);
125    if let Ok(rd) = fs::read_dir(src) {
126        for entry in rd.flatten() {
127            let from = entry.path();
128            let to = dst.join(entry.file_name());
129            if from.is_dir() {
130                copy_dir(&from, &to);
131            } else if let Ok(data) = fs::read(&from) {
132                let _ = fs::write(&to, data);
133            }
134        }
135    }
136}