1use deepstrike_core::context::renderer::RenderedContext;
2use deepstrike_core::types::message::{Message, Role};
3use futures::StreamExt;
4
5use crate::Result;
6use crate::harness::{HarnessEvent, HarnessOutcome, HarnessRequest, QualityGate, Verdict};
7use crate::providers::{LLMProvider, StreamEvent};
8use crate::run_event::RunEvent;
9use crate::runtime::RuntimeRunner;
10
11fn rendered_context_from_messages(messages: Vec<Message>) -> RenderedContext {
12 let mut system_parts = Vec::new();
13 let mut turns = Vec::new();
14
15 for message in messages {
16 if message.role == Role::System {
17 if let Some(text) = message.content.as_text() {
18 system_parts.push(text.to_owned());
19 }
20 } else {
21 turns.push(message);
22 }
23 }
24
25 let system_text = system_parts.join("\n\n");
26 RenderedContext {
27 system_text: system_text.clone(),
28 system_stable: system_text,
29 system_knowledge: String::new(),
30 turns,
31 state_turn: None,
32 frozen_prefix_len: None,
33 }
34}
35
36async fn collect_run_runtime(
37 runner: &RuntimeRunner,
38 req: &HarnessRequest,
39) -> Result<(String, u32, u64, String)> {
40 let criteria_texts: Vec<String> = req.criteria.iter().map(|c| c.text.clone()).collect();
41 let mut text = String::new();
42 let mut iterations = 0u32;
43 let mut total_tokens = 0u64;
44 let mut status = "error".to_string();
45 let mut stream = runner
46 .run_streaming(&req.goal, &criteria_texts, req.extensions.as_ref(), None)
47 .await?;
48 while let Some(evt) = stream.next().await {
49 match evt? {
50 RunEvent::TextDelta(d) => text.push_str(&d),
51 RunEvent::Done {
52 iterations: i,
53 total_tokens: t,
54 status: s,
55 } => {
56 iterations = i;
57 total_tokens = t;
58 status = s;
59 }
60 _ => {}
61 }
62 }
63 Ok((text, iterations, total_tokens, status))
64}
65
66pub struct SinglePassHarness<'a> {
68 runner: &'a RuntimeRunner,
69}
70
71impl<'a> SinglePassHarness<'a> {
72 pub fn new(runner: &'a RuntimeRunner) -> Self {
73 Self { runner }
74 }
75
76 pub async fn run(&self, request: HarnessRequest) -> Result<HarnessOutcome> {
77 let (text, iterations, total_tokens, status) =
78 collect_run_runtime(self.runner, &request).await?;
79 Ok(HarnessOutcome {
80 result: text,
81 passed: true,
82 iterations,
83 total_tokens,
84 status,
85 overall_score: 1.0,
86 feedback: None,
87 details: vec![],
88 })
89 }
90}
91
92#[deprecated(since = "0.2.23", note = "Use HarnessLoop with `with_verdict_fn` for host-defined judgment. EvalLoopHarness.stream() does not honor gate.")]
98pub struct EvalLoopHarness<'a, G: QualityGate> {
99 runner: &'a RuntimeRunner,
100 gate: G,
101 max_attempts: usize,
102}
103
104impl<'a, G: QualityGate> EvalLoopHarness<'a, G> {
105 pub fn new(runner: &'a RuntimeRunner, gate: G, max_attempts: usize) -> Self {
106 Self {
107 runner,
108 gate,
109 max_attempts,
110 }
111 }
112
113 pub async fn run(&self, request: HarnessRequest) -> Result<HarnessOutcome> {
114 let mut outcome = HarnessOutcome {
115 result: String::new(),
116 passed: false,
117 iterations: 0,
118 total_tokens: 0,
119 status: "error".into(),
120 overall_score: 0.0,
121 feedback: None,
122 details: vec![],
123 };
124 for _ in 0..self.max_attempts {
125 let (text, iterations, total_tokens, status) =
126 collect_run_runtime(self.runner, &request).await?;
127 outcome = HarnessOutcome {
128 result: text,
129 passed: false,
130 iterations,
131 total_tokens,
132 status,
133 overall_score: 0.0,
134 feedback: None,
135 details: vec![],
136 };
137 if self.gate.evaluate(&request, &outcome).await? {
138 outcome.passed = true;
139 return Ok(outcome);
140 }
141 }
142 Ok(outcome)
143 }
144}
145
146pub struct VerdictCtx<'a> {
150 pub goal: &'a str,
151 pub criteria: &'a [crate::harness::Criterion],
152 pub attempt: u32,
153 pub result: &'a str,
154}
155pub type VerdictFn = std::sync::Arc<dyn Fn(VerdictCtx<'_>) -> Option<Verdict> + Send + Sync>;
156
157pub struct HarnessLoop<'a> {
159 runner: &'a RuntimeRunner,
160 eval_provider: Box<dyn LLMProvider>,
161 max_attempts: usize,
162 skill_dir: Option<std::path::PathBuf>,
163 verdict_fn: Option<VerdictFn>,
164}
165
166impl<'a> HarnessLoop<'a> {
167 pub fn new(
168 runner: &'a RuntimeRunner,
169 eval_provider: impl LLMProvider + 'static,
170 max_attempts: usize,
171 skill_dir: Option<std::path::PathBuf>,
172 ) -> Self {
173 Self {
174 runner,
175 eval_provider: Box::new(eval_provider),
176 max_attempts,
177 skill_dir,
178 verdict_fn: None,
179 }
180 }
181
182 pub fn with_verdict_fn(mut self, f: VerdictFn) -> Self {
186 self.verdict_fn = Some(f);
187 self
188 }
189
190 pub fn run_streaming<'b>(
191 &'b self,
192 request: HarnessRequest,
193 ) -> impl futures::Stream<Item = Result<HarnessEvent>> + 'b {
194 use deepstrike_core::harness::eval::{build_eval_messages, parse_verdict, Criterion};
195
196 async_stream::stream! {
197 let mut current_goal = request.goal.clone();
198 let criteria_texts: Vec<String> = request.criteria.iter().map(|c| c.text.clone()).collect();
199
200 for attempt in 1..=self.max_attempts as u32 {
201 let mut last_result = String::new();
202 let mut last_iterations = 0u32;
203 let mut last_total_tokens = 0u64;
204 let mut last_status = "error".to_string();
205
206 let goal_for_run = current_goal.clone();
207 let stream_result = self.runner
208 .run_streaming(&goal_for_run, &criteria_texts, request.extensions.as_ref(), None)
209 .await;
210 let mut stream = match stream_result {
211 Ok(s) => s,
212 Err(e) => { yield Err(e); return; }
213 };
214
215 while let Some(evt) = stream.next().await {
216 match evt {
217 Ok(RunEvent::TextDelta(d)) => {
218 last_result.push_str(&d);
219 yield Ok(HarnessEvent::Token(d));
220 }
221 Ok(RunEvent::ToolCall { id, name }) => yield Ok(HarnessEvent::ToolCall { id, name }),
222 Ok(RunEvent::ToolResult { call_id, content, is_error, .. }) => {
223 yield Ok(HarnessEvent::ToolResult { call_id, content, is_error });
224 }
225 Ok(RunEvent::Done { iterations, total_tokens, status }) => {
226 last_iterations = iterations;
227 last_total_tokens = total_tokens;
228 last_status = status;
229 }
230 Ok(_) => {}
231 Err(e) => { yield Err(e); return; }
232 }
233 }
234
235 yield Ok(HarnessEvent::Supervising);
236
237 let mut verdict: Option<Verdict> = None;
239 let mut skill_candidate_from_eval = None;
240 if let Some(f) = self.verdict_fn.clone() {
241 let ctx = VerdictCtx {
242 goal: &request.goal,
243 criteria: &request.criteria,
244 attempt,
245 result: &last_result,
246 };
247 verdict = f(ctx);
248 }
249 let verdict = if let Some(v) = verdict {
250 v
251 } else {
252 let eval_criteria: Vec<Criterion> = request.criteria.iter().map(|c| Criterion {
255 text: c.text.clone(), required: c.required, weight: c.weight,
256 }).collect();
257 let messages = build_eval_messages(&request.goal, &eval_criteria, &last_result, attempt, true);
258
259 let mut eval_text = String::new();
260 let context = rendered_context_from_messages(messages);
261 let eval_state = self.eval_provider.create_run_state();
262 let mut eval_stream = match self.eval_provider.stream(&context, &[], None, eval_state.as_ref()).await {
263 Ok(s) => s,
264 Err(e) => { yield Err(e); return; }
265 };
266 while let Some(evt) = eval_stream.next().await {
267 if let Ok(StreamEvent::TextDelta { delta }) = evt {
268 eval_text.push_str(&delta);
269 }
270 }
271
272 let eval_result = parse_verdict(&eval_text);
273 skill_candidate_from_eval = eval_result.skill_candidate.clone();
274 Verdict {
275 passed: eval_result.passed,
276 overall_score: eval_result.overall_score,
277 feedback: eval_result.feedback.clone(),
278 details: eval_result.details.iter().map(|d| crate::harness::CriterionResult {
279 criterion: d.criterion.clone(), passed: d.passed, score: d.score, feedback: d.feedback.clone(),
280 }).collect(),
281 }
282 };
283
284 if verdict.passed {
285 if let Some(sc) = skill_candidate_from_eval {
286 if let Some(dir) = &self.skill_dir {
287 let mut fm = format!("---\nname: {}\ndescription: {}\n", sc.name, sc.description);
288 if let Some(wtu) = &sc.when_to_use { fm.push_str(&format!("when_to_use: {}\n", wtu)); }
289 fm.push_str("---\n\n");
290 fm.push_str(&sc.content);
291 if let Err(e) = tokio::fs::write(dir.join(format!("{}.md", sc.name)), fm).await {
292 yield Err(e.into()); return;
293 }
294 }
295 }
296 yield Ok(HarnessEvent::Done { verdict, iterations: last_iterations, total_tokens: last_total_tokens, status: last_status });
297 return;
298 }
299
300 yield Ok(HarnessEvent::Revising { verdict: verdict.clone() });
301 current_goal = format!("{}\n\n[Attempt {} feedback: {}]", request.goal, attempt, verdict.feedback);
302 }
303
304 yield Ok(HarnessEvent::MaxAttemptsReached);
305 }
306 }
307}