use async_trait::async_trait;
use car_eventlog::harness_adapt::HarnessIntervention;
use serde::{Deserialize, Serialize};
use super::ab::{
attribute_round, run_ab_suite, AbArmRunner, AbTask, PairedStats, RoundAttribution,
};
#[derive(Debug, Clone)]
pub struct LoopConfig {
pub quality_bar: f64,
pub require_not_behind: bool,
pub max_rounds: u32,
pub min_evidence: usize,
}
impl Default for LoopConfig {
fn default() -> Self {
Self {
quality_bar: 0.8,
require_not_behind: true,
max_rounds: 5,
min_evidence: 2,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum LoopStop {
QualityMet,
NoLeverLeft,
FixStalled,
MaxRounds,
}
impl LoopStop {
pub fn is_success(self) -> bool {
matches!(self, LoopStop::QualityMet)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FixResult {
pub applied: bool,
pub note: String,
}
#[async_trait]
pub trait AbFixer: Send + Sync {
async fn apply(&self, interventions: &[HarnessIntervention]) -> FixResult;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RoundRecord {
pub round: u32,
pub stats: PairedStats,
pub attribution: RoundAttribution,
pub fix_attempted: bool,
pub fix_applied: bool,
pub fix_note: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImprovementRun {
pub rounds: Vec<RoundRecord>,
pub stop: LoopStop,
pub backbone: Option<String>,
}
impl ImprovementRun {
pub fn final_stats(&self) -> Option<&PairedStats> {
self.rounds.last().map(|r| &r.stats)
}
pub fn pass_rate_gain(&self) -> f64 {
match (self.rounds.first(), self.rounds.last()) {
(Some(first), Some(last)) => last.stats.native_pass_rate - first.stats.native_pass_rate,
_ => 0.0,
}
}
}
fn quality_met(stats: &PairedStats, cfg: &LoopConfig) -> bool {
if stats.paired_tasks == 0 {
return false; }
if stats.native_pass_rate < cfg.quality_bar {
return false;
}
if cfg.require_not_behind {
let behind = stats.mcnemar_significant_05 && stats.external_only > stats.native_only;
if behind {
return false;
}
}
true
}
pub async fn run_improvement_loop(
corpus: &[AbTask],
external_engine: &str,
backbone: Option<String>,
runner: &dyn AbArmRunner,
read_events: impl Fn(&str) -> Option<String>,
fixer: &dyn AbFixer,
cfg: &LoopConfig,
) -> ImprovementRun {
let mut rounds = Vec::new();
let mut stop = LoopStop::MaxRounds;
for round in 0..cfg.max_rounds {
let report = run_ab_suite(corpus, external_engine, backbone.clone(), runner).await;
let attribution = attribute_round(&report, &read_events, cfg.min_evidence);
let met = quality_met(&report.stats, cfg);
if met {
rounds.push(RoundRecord {
round,
stats: report.stats,
attribution,
fix_attempted: false,
fix_applied: false,
fix_note: String::new(),
});
stop = LoopStop::QualityMet;
break;
}
if !attribution.has_lever() {
rounds.push(RoundRecord {
round,
stats: report.stats,
attribution,
fix_attempted: false,
fix_applied: false,
fix_note: "no harness-addressable lever — remaining losses are backbone-bound"
.into(),
});
stop = LoopStop::NoLeverLeft;
break;
}
let fix = fixer.apply(&attribution.interventions).await;
let applied = fix.applied;
rounds.push(RoundRecord {
round,
stats: report.stats,
attribution,
fix_attempted: true,
fix_applied: applied,
fix_note: fix.note,
});
if !applied {
stop = LoopStop::FixStalled;
break;
}
}
ImprovementRun {
rounds,
stop,
backbone,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::coder::ab::{Arm, ArmOutcome};
use crate::coder::contract::OutcomeContract;
use std::collections::HashMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
fn empty_contract() -> OutcomeContract {
OutcomeContract {
description: "tests pass".into(),
checks: vec![],
}
}
fn tasks(n: usize) -> Vec<AbTask> {
(0..n)
.map(|i| AbTask {
id: format!("t{i}"),
intent: format!("task {i}"),
contract: empty_contract(),
repo: None,
})
.collect()
}
struct ImprovingRunner {
fixes: Arc<AtomicUsize>,
native_passes: fn(usize) -> usize,
external_passes: usize,
n: usize,
loss_transcript: &'static str,
}
#[async_trait]
impl AbArmRunner for ImprovingRunner {
async fn run_arm(&self, task: &AbTask, arm: &Arm) -> ArmOutcome {
let idx: usize = task.id.trim_start_matches('t').parse().unwrap();
match arm {
Arm::Native => {
let passes =
(self.native_passes)(self.fixes.load(Ordering::SeqCst)).min(self.n);
if idx < passes {
ArmOutcome {
passed: true,
iterations: 1,
cost_usd: 0.0,
wall_ms: 1,
error: None,
transcript_path: None,
infra_failed: false,
}
} else {
ArmOutcome {
passed: false,
iterations: 3,
cost_usd: 0.0,
wall_ms: 1,
error: None,
transcript_path: Some(self.loss_transcript.into()),
infra_failed: false,
}
}
}
Arm::External(_) => ArmOutcome {
passed: idx < self.external_passes,
iterations: 1,
cost_usd: 0.0,
wall_ms: 1,
error: None,
transcript_path: None,
infra_failed: false,
},
}
}
}
struct LandingFixer {
fixes: Arc<AtomicUsize>,
}
#[async_trait]
impl AbFixer for LandingFixer {
async fn apply(&self, interventions: &[HarnessIntervention]) -> FixResult {
self.fixes.fetch_add(1, Ordering::SeqCst);
FixResult {
applied: true,
note: format!("applied {} intervention(s)", interventions.len()),
}
}
}
struct StalledFixer;
#[async_trait]
impl AbFixer for StalledFixer {
async fn apply(&self, _i: &[HarnessIntervention]) -> FixResult {
FixResult {
applied: false,
note: "all interventions pending human approval".into(),
}
}
}
fn lever_events() -> HashMap<String, String> {
let jsonl = (0..2)
.map(|_| {
r#"{"kind":"action_failed","action_id":"run_command","data":{"error":"runtime boom"}}"#
.to_string()
})
.collect::<Vec<_>>()
.join("\n");
[("loss.jsonl".to_string(), jsonl)].into_iter().collect()
}
#[tokio::test]
async fn loop_converges_to_quality_met() {
let fixes = Arc::new(AtomicUsize::new(0));
let runner = ImprovingRunner {
fixes: fixes.clone(),
native_passes: |f| 2 + 2 * f,
external_passes: 3,
n: 5,
loss_transcript: "loss.jsonl",
};
let fixer = LandingFixer {
fixes: fixes.clone(),
};
let events = lever_events();
let cfg = LoopConfig {
quality_bar: 0.9,
require_not_behind: true,
max_rounds: 5,
min_evidence: 2,
};
let run = run_improvement_loop(
&tasks(5),
"codex",
Some("parslee/reasoning".into()),
&runner,
|p| events.get(p).cloned(),
&fixer,
&cfg,
)
.await;
assert_eq!(run.stop, LoopStop::QualityMet);
assert!(run.stop.is_success());
assert_eq!(run.rounds.len(), 3, "two fix rounds then a met round");
assert!(run.rounds[0].fix_applied && run.rounds[1].fix_applied);
assert!(!run.rounds[2].fix_attempted);
assert!((run.rounds[0].stats.native_pass_rate - 0.4).abs() < 1e-9);
assert!((run.final_stats().unwrap().native_pass_rate - 1.0).abs() < 1e-9);
assert!((run.pass_rate_gain() - 0.6).abs() < 1e-9);
}
#[tokio::test]
async fn loop_stops_when_no_harness_lever_remains() {
let fixes = Arc::new(AtomicUsize::new(0));
let runner = ImprovingRunner {
fixes: fixes.clone(),
native_passes: |_| 2,
external_passes: 3,
n: 5,
loss_transcript: "clean.jsonl", };
let fixer = LandingFixer { fixes };
let events: HashMap<String, String> = HashMap::new(); let run = run_improvement_loop(
&tasks(5),
"codex",
None,
&runner,
|p| events.get(p).cloned(),
&fixer,
&LoopConfig::default(),
)
.await;
assert_eq!(run.stop, LoopStop::NoLeverLeft);
assert!(!run.stop.is_success());
assert_eq!(run.rounds.len(), 1);
assert!(!run.rounds[0].fix_attempted);
}
#[tokio::test]
async fn loop_stops_when_fix_stalls() {
let fixes = Arc::new(AtomicUsize::new(0));
let runner = ImprovingRunner {
fixes,
native_passes: |_| 2,
external_passes: 3,
n: 5,
loss_transcript: "loss.jsonl",
};
let events = lever_events();
let run = run_improvement_loop(
&tasks(5),
"codex",
None,
&runner,
|p| events.get(p).cloned(),
&StalledFixer,
&LoopConfig::default(),
)
.await;
assert_eq!(run.stop, LoopStop::FixStalled);
assert_eq!(run.rounds.len(), 1);
assert!(run.rounds[0].fix_attempted && !run.rounds[0].fix_applied);
}
#[tokio::test]
async fn loop_respects_max_rounds() {
let fixes = Arc::new(AtomicUsize::new(0));
let runner = ImprovingRunner {
fixes: fixes.clone(),
native_passes: |_| 2, external_passes: 1,
n: 5,
loss_transcript: "loss.jsonl",
};
let fixer = LandingFixer { fixes };
let events = lever_events();
let cfg = LoopConfig {
quality_bar: 0.8,
require_not_behind: false,
max_rounds: 3,
min_evidence: 2,
};
let run = run_improvement_loop(
&tasks(5),
"codex",
None,
&runner,
|p| events.get(p).cloned(),
&fixer,
&cfg,
)
.await;
assert_eq!(run.stop, LoopStop::MaxRounds);
assert_eq!(run.rounds.len(), 3);
assert!(run.rounds.iter().all(|r| r.fix_attempted && r.fix_applied));
}
#[tokio::test]
async fn not_behind_gate_blocks_a_high_but_losing_pass_rate() {
let behind = PairedStats {
paired_tasks: 20,
native_passes: 16,
external_passes: 20,
native_pass_rate: 0.8,
external_pass_rate: 1.0,
pass_rate_delta: -0.2,
both_pass: 16,
native_only: 0,
external_only: 4, both_fail: 0,
mcnemar_chi2: 100.0, mcnemar_significant_05: true,
native_infra_failures: 0,
external_infra_failures: 0,
native_mean_cost_usd: 0.0,
external_mean_cost_usd: 0.0,
native_cost_per_pass: None,
external_cost_per_pass: None,
};
let cfg = LoopConfig {
quality_bar: 0.8,
require_not_behind: true,
..LoopConfig::default()
};
assert!(
!quality_met(&behind, &cfg),
"significantly behind → not met"
);
let cfg_off = LoopConfig {
require_not_behind: false,
..cfg.clone()
};
assert!(quality_met(&behind, &cfg_off));
}
}