harness_lock_core/
adapters.rs1use 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
13pub struct RunOutcome {
15 pub success: bool,
16 pub cost_usd: f64,
17 pub latency_s: f64,
18 pub steps: u64,
19 pub trace: serde_json::Value,
21}
22
23pub trait HarnessAdapter {
25 fn name(&self) -> &str;
26 fn run(&self, stack: &Stack, task: &Task, workspace: &Path, seed: u64) -> Result<RunOutcome>;
27}
28
29pub 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 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 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}