use chrono::Utc;
use tracing::{info, warn};
use crate::budget::BudgetController;
use crate::error::{EgriError, Result};
use crate::evaluator::Evaluator;
use crate::executor::Executor;
use crate::ledger::Ledger;
use crate::promotion::PromotionController;
use crate::proposer::Proposer;
use crate::selector::Selector;
use crate::types::*;
pub struct EgriLoop<A, P, X, E, S>
where
A: Clone,
P: Proposer<Artifact = A>,
X: Executor<Artifact = A>,
E: Evaluator<Artifact = A>,
S: Selector,
{
proposer: P,
executor: X,
evaluator: E,
selector: S,
budget: BudgetController,
promotion: PromotionController<A>,
ledger: Ledger,
best_outcome: Option<Outcome>,
}
#[derive(Debug)]
pub struct LoopSummary {
pub total_trials: usize,
pub promoted_count: usize,
pub discarded_count: usize,
pub escalated_count: usize,
pub baseline_score: Option<Score>,
pub final_score: Option<Score>,
}
impl<A, P, X, E, S> EgriLoop<A, P, X, E, S>
where
A: Clone,
P: Proposer<Artifact = A>,
X: Executor<Artifact = A>,
E: Evaluator<Artifact = A>,
S: Selector,
{
pub fn new(
proposer: P,
executor: X,
evaluator: E,
selector: S,
budget: BudgetController,
ledger: Ledger,
) -> Self {
Self {
proposer,
executor,
evaluator,
selector,
budget,
promotion: PromotionController::new(),
ledger,
best_outcome: None,
}
}
pub fn baseline(&mut self, artifact: A) -> Result<Outcome> {
info!("establishing baseline");
let exec_result = self.executor.execute(&artifact)?;
let outcome = self.evaluator.evaluate(&artifact, &exec_result)?;
self.promotion.set_baseline(artifact);
self.best_outcome = Some(outcome.clone());
let record = TrialRecord {
trial_id: TrialId::baseline(),
timestamp: Utc::now(),
parent_state: StateId::baseline(),
mutation: Mutation {
operator: "none".into(),
description: "baseline measurement".into(),
diff: None,
hypothesis: None,
},
execution: Some(exec_result),
outcome: outcome.clone(),
decision: Decision {
action: Action::Promoted,
reason: "baseline establishment".into(),
new_state_id: Some(StateId::baseline()),
},
strategy_notes: None,
};
self.ledger.append(record)?;
info!(score = ?outcome.score, "baseline established");
Ok(outcome)
}
pub fn step(&mut self) -> Result<TrialRecord> {
self.budget.check()?;
let best_outcome = self.best_outcome.as_ref().ok_or(EgriError::NoBaseline)?;
let current = self
.promotion
.current()
.ok_or(EgriError::NoBaseline)?
.clone();
let parent_state = self
.promotion
.current_state_id()
.cloned()
.unwrap_or_else(StateId::baseline);
let (mutation, candidate) = self.proposer.propose(¤t, &self.ledger)?;
info!(operator = %mutation.operator, desc = %mutation.description, "proposed mutation");
let exec_result = self.executor.execute(&candidate);
let exec_result = match exec_result {
Ok(r) => r,
Err(e) => {
warn!(error = %e, "execution failed");
self.budget.consume();
let record = TrialRecord {
trial_id: TrialId::new(self.budget.used()),
timestamp: Utc::now(),
parent_state,
mutation,
execution: None,
outcome: Outcome {
score: Score::Scalar(0.0),
constraints_passed: false,
constraint_violations: vec![format!("execution failed: {e}")],
evaluator_metadata: None,
},
decision: Decision {
action: Action::Discarded,
reason: format!("execution failed: {e}"),
new_state_id: None,
},
strategy_notes: None,
};
self.ledger.append(record.clone())?;
return Ok(record);
}
};
let outcome = self.evaluator.evaluate(&candidate, &exec_result)?;
let decision = self.selector.select(&outcome, best_outcome)?;
info!(
score = ?outcome.score,
action = %decision.action,
reason = %decision.reason,
"trial complete"
);
if decision.action == Action::Promoted {
self.best_outcome = Some(outcome.clone());
}
self.promotion.apply_decision(&decision, candidate);
self.budget.consume();
let record = TrialRecord {
trial_id: TrialId::new(self.budget.used()),
timestamp: Utc::now(),
parent_state,
mutation,
execution: Some(exec_result),
outcome,
decision,
strategy_notes: None,
};
self.ledger.append(record.clone())?;
Ok(record)
}
pub fn run(&mut self) -> Result<LoopSummary> {
self.budget.start();
loop {
match self.step() {
Ok(record) => {
if record.decision.action == Action::Escalated {
info!(reason = %record.decision.reason, "escalation — halting loop");
break;
}
}
Err(EgriError::BudgetExhausted(msg)) => {
info!(reason = %msg, "budget exhausted — loop complete");
break;
}
Err(e) => return Err(e),
}
}
Ok(self.summary())
}
pub fn summary(&self) -> LoopSummary {
let records = self.ledger.records();
let baseline_record = records.first();
let last_promoted = self.ledger.last_promoted();
LoopSummary {
total_trials: self.ledger.trial_count(),
promoted_count: self.ledger.by_action(Action::Promoted).len(),
discarded_count: self.ledger.by_action(Action::Discarded).len(),
escalated_count: self.ledger.by_action(Action::Escalated).len(),
baseline_score: baseline_record.map(|r| r.outcome.score.clone()),
final_score: last_promoted.map(|r| r.outcome.score.clone()),
}
}
pub fn ledger(&self) -> &Ledger {
&self.ledger
}
pub fn best(&self) -> Option<&A> {
self.promotion.best()
}
pub fn rollback(&mut self) -> Result<&A> {
self.promotion.rollback()
}
}