1use crate::evolution::fitness::{evolutionary_fitness, update_gene_stats};
2use crate::evolution::{
3 Chromosome, GenePool,
4 population::{baseline_chromosome, random_chromosome},
5};
6use crate::lineage::{BypassCorpus, BypassEntry};
7use crate::search::SearchAlgorithm;
8use crate::types::{
9 Budget, EvolutionError, OracleVerdict, SearchStats, TargetHealthMonitor, load_checkpoint,
10 save_checkpoint,
11};
12use lru::LruCache;
13use rand::{SeedableRng, rngs::StdRng};
14use std::collections::{HashMap, VecDeque};
15use std::num::NonZeroUsize;
16use std::path::{Path, PathBuf};
17use std::time::Instant;
18
19#[derive(Debug)]
21pub struct EvolutionEngine {
22 pub(crate) algorithm: Box<dyn SearchAlgorithm>,
24 pub gene_pool: GenePool,
26 pub rng: StdRng,
28 pub cache: LruCache<String, OracleVerdict>,
30 pub budget: Budget,
32 pub in_flight: HashMap<u64, (u64, Chromosome, Instant)>,
43 pub stats: SearchStats,
45 pub target_health: TargetHealthMonitor,
47 pub checkpoint_path: Option<PathBuf>,
49 pub request_count: usize,
51 pub gene_stats: Vec<(String, String, u32, u32)>,
53 pub fitness_history: VecDeque<f64>,
55 pub stagnation_counter: u32,
57 pub corpus: BypassCorpus,
59 generation_evals: usize,
61 next_id: u64,
63 pending_single: Option<(usize, Chromosome)>,
65}
66
67impl Clone for EvolutionEngine {
68 fn clone(&self) -> Self {
69 Self {
76 algorithm: self.algorithm.clone_box(),
77 gene_pool: self.gene_pool.clone(),
78 rng: self.rng.clone(),
79 cache: LruCache::new(self.cache.cap()),
85 budget: self.budget,
86 in_flight: HashMap::new(),
89 stats: self.stats,
90 target_health: self.target_health.clone(),
91 checkpoint_path: self.checkpoint_path.clone(),
92 request_count: self.request_count,
93 gene_stats: self.gene_stats.clone(),
94 fitness_history: self.fitness_history.clone(),
95 stagnation_counter: self.stagnation_counter,
96 corpus: self.corpus.clone(),
97 generation_evals: self.generation_evals,
98 next_id: self.next_id,
99 pending_single: None,
100 }
101 }
102}
103
104pub type SharedEngine = std::sync::Arc<tokio::sync::RwLock<EvolutionEngine>>;
120
121impl EvolutionEngine {
122 #[must_use]
128 pub fn into_shared(self) -> SharedEngine {
129 std::sync::Arc::new(tokio::sync::RwLock::new(self))
130 }
131}
132
133impl EvolutionEngine {
134 #[must_use]
136 pub fn new(population_size: usize) -> Self {
137 Self::new_seeded(population_size, 0)
138 }
139
140 #[must_use]
147 pub fn new_seeded(population_size: usize, seed: u64) -> Self {
148 let population_size = population_size.clamp(1, 10_000);
149 let gene_pool = GenePool::default_wafrift();
150 let mut rng = StdRng::seed_from_u64(seed);
151 let mut population: Vec<Chromosome> = (0..population_size)
152 .map(|_| random_chromosome(&gene_pool, &mut rng))
153 .collect();
154 if population_size > 0 {
155 population[0] = baseline_chromosome(&gene_pool);
156 }
157
158 let mut engine = Self::with_algorithm("hill_climbing", gene_pool, rng, Budget::default())
159 .expect("hill_climbing is built-in");
160 engine
161 .algorithm
162 .initialize(population, &engine.gene_pool, &mut engine.rng.clone());
163 let mut population2: Vec<Chromosome> = (0..population_size)
165 .map(|_| random_chromosome(&engine.gene_pool, &mut engine.rng))
166 .collect();
167 if population_size > 0 {
168 population2[0] = baseline_chromosome(&engine.gene_pool);
169 }
170 engine
171 .algorithm
172 .initialize(population2, &engine.gene_pool, &mut engine.rng);
173 engine
174 }
175
176 pub fn with_algorithm(
178 algorithm_name: &str,
179 gene_pool: GenePool,
180 rng: StdRng,
181 budget: Budget,
182 ) -> Result<Self, EvolutionError> {
183 let algorithm: Box<dyn SearchAlgorithm> = match algorithm_name {
184 "hill_climbing" => Box::new(crate::search::HillClimbing::new()),
185 "simulated_annealing" => Box::new(crate::search::SimulatedAnnealing::new()),
186 "tabu_search" => Box::new(crate::search::TabuSearch::new(20)),
187 "novelty_search" => Box::new(crate::search::NoveltySearch::new(15, 0.3)),
188 "map_elites" => Box::new(crate::search::MapElites::new()),
189 _ => {
190 return Err(EvolutionError::AlgorithmError(format!(
191 "unknown algorithm: {algorithm_name}"
192 )));
193 }
194 };
195
196 Ok(Self {
197 algorithm,
198 gene_pool,
199 rng,
200 cache: LruCache::new(NonZeroUsize::new(10_000).expect("10_000 is non-zero")),
201 budget,
202 in_flight: HashMap::new(),
203 stats: SearchStats::new(),
204 target_health: TargetHealthMonitor::new(),
205 checkpoint_path: None,
206 request_count: 0,
207 gene_stats: Vec::new(),
208 fitness_history: VecDeque::new(),
209 stagnation_counter: 0,
210 corpus: BypassCorpus::new(),
211 generation_evals: 0,
212 next_id: 0,
213 pending_single: None,
214 })
215 }
216
217 fn cache_key(chromosome: &Chromosome) -> String {
218 let mut parts: Vec<_> = chromosome
219 .genes
220 .iter()
221 .map(|(n, v)| format!("{n}={v}"))
222 .collect();
223 parts.sort();
224 parts.join(";")
225 }
226
227 #[must_use]
232 pub fn next_id(&self) -> u64 {
233 self.next_id
234 }
235
236 fn next_eval_id(&mut self) -> u64 {
237 self.next_id += 1;
238 self.next_id
239 }
240
241 #[must_use]
245 pub fn next_candidate(&mut self) -> Option<(usize, &Chromosome)> {
246 if self.should_terminate() {
247 return None;
248 }
249 if self.pending_single.is_none() {
250 self.pending_single = self.batch_candidates(1).into_iter().next();
251 }
252 self.pending_single
253 .as_ref()
254 .map(|(idx, chrom)| (*idx, chrom))
255 }
256
257 pub fn batch_candidates(&mut self, n: usize) -> Vec<(usize, Chromosome)> {
266 if self.should_terminate() || n == 0 {
267 return Vec::new();
268 }
269 let remaining = self.budget.max_requests.saturating_sub(self.request_count);
270 if remaining == 0 {
271 return Vec::new();
272 }
273 let n = n.min(remaining);
274
275 let mut result = Vec::with_capacity(n);
276 let mut cached_results = Vec::new();
277 let requested = self.algorithm.request_evaluations(n, &mut self.rng);
278
279 for candidate in requested {
280 let key = Self::cache_key(&candidate.chromosome);
281 if let Some(verdict) = self.cache.get(&key).copied() {
282 cached_results.push((candidate.id, verdict));
283 } else {
284 let eval_id = self.next_eval_id();
285 self.in_flight.insert(
290 eval_id,
291 (candidate.id, candidate.chromosome.clone(), Instant::now()),
292 );
293 result.push((eval_id as usize, candidate.chromosome));
294 }
295 }
296
297 if !cached_results.is_empty() {
298 self.algorithm.submit_evaluations(cached_results);
299 }
300
301 self.request_count = self.request_count.saturating_add(result.len());
302 result
303 }
304
305 pub fn submit_batch(
311 &mut self,
312 results: Vec<(usize, OracleVerdict)>,
313 ) -> Result<(), EvolutionError> {
314 let mut to_submit: Vec<(u64, OracleVerdict)> = Vec::with_capacity(results.len());
315 for (id_usize, verdict) in results {
316 let id = id_usize as u64;
317 let (algorithm_candidate_id, mut chromosome, _sent_at) = self
318 .in_flight
319 .remove(&id)
320 .ok_or(EvolutionError::InvalidChromosomeIndex(id_usize))?;
321
322 chromosome.record_verdict(&verdict);
323 let key = Self::cache_key(&chromosome);
324 self.cache.put(key, verdict);
325
326 update_gene_stats(&mut self.gene_stats, &chromosome.genes, verdict.passed);
327 let adjusted = evolutionary_fitness(&chromosome, &self.gene_stats);
328 chromosome.fitness = adjusted;
329
330 let hash_str = format!("{:016x}", chromosome.hash());
332 if chromosome.fitness >= 0.85
333 && !self
334 .corpus
335 .entries
336 .iter()
337 .any(|e| e.payload_hash == hash_str)
338 {
339 self.corpus
340 .add(BypassEntry::from_chromosome(&chromosome, None));
341 }
342
343 to_submit.push((algorithm_candidate_id, verdict));
347 self.generation_evals += 1;
348 self.stats.evaluations += 1;
349
350 if verdict.passed {
351 self.target_health.record_success();
352 } else if verdict.status_delta >= 500 {
353 self.target_health.record_error();
354 }
355 }
356
357 self.algorithm.submit_evaluations(to_submit);
358 Ok(())
359 }
360
361 pub fn record_feedback(
363 &mut self,
364 chromosome_index: usize,
365 passed: bool,
366 ) -> Result<(), EvolutionError> {
367 if let Some((idx, _)) = self.pending_single
369 && idx == chromosome_index
370 {
371 self.pending_single = None;
372 }
373 self.record_verdict(chromosome_index, &OracleVerdict::from_bool(passed))
374 }
375
376 pub fn record_verdict(
378 &mut self,
379 chromosome_index: usize,
380 verdict: &OracleVerdict,
381 ) -> Result<(), EvolutionError> {
382 self.submit_batch(vec![(chromosome_index, *verdict)])
383 }
384
385 pub fn record_target_error(&mut self, error: String) -> Result<(), EvolutionError> {
387 self.target_health.record_error();
388 if !self.target_health.is_healthy() {
389 return Err(EvolutionError::TargetHealthCritical(error));
390 }
391 Ok(())
392 }
393
394 pub fn evolve(&mut self) {
396 if self.algorithm.best().is_none() {
397 return;
398 }
399
400 if let Some(best) = self.algorithm.best() {
402 self.fitness_history.push_back(best.fitness);
403 }
404 if self.fitness_history.len() > 1000 {
405 self.fitness_history.pop_front();
406 }
407
408 let window = 10_usize;
410 if self.fitness_history.len() >= window {
411 let skip = self.fitness_history.len().saturating_sub(window);
412 let recent: Vec<f64> = self.fitness_history.iter().skip(skip).copied().collect();
413 let improved = recent.windows(2).any(|w| w[1] > w[0] + 0.001);
414 if !improved {
415 self.stagnation_counter += 1;
416 } else {
417 self.stagnation_counter = 0;
418 }
419 }
420 self.stats.stagnation_counter = self.stagnation_counter;
426
427 self.stats.generation += 1;
428 self.generation_evals = 0;
429
430 if let Some(ref path) = self.checkpoint_path
431 && let Err(e) = self.save_checkpoint(path)
432 {
433 tracing::warn!(error = %e, path = %path.display(), "checkpoint save failed");
434 }
435 }
436
437 #[must_use]
439 pub fn should_terminate(&self) -> bool {
440 if !self.target_health.is_healthy() {
441 return true;
442 }
443 self.algorithm.should_terminate(&self.stats, &self.budget)
444 || self.request_count >= self.budget.max_requests
445 || self.stats.stagnation_counter >= self.budget.stagnation_limit
446 }
447
448 #[must_use]
450 pub fn best(&self) -> Option<&Chromosome> {
451 self.algorithm.best()
452 }
453
454 pub fn save_checkpoint(&self, path: &Path) -> Result<(), EvolutionError> {
456 let state = EngineState {
457 algorithm_name: self.algorithm.name().to_string(),
458 algorithm_state: self.algorithm.checkpoint()?,
459 gene_pool: self.gene_pool.clone(),
460 rng_seed: 0,
466 budget: self.budget,
467 gene_stats: self.gene_stats.clone(),
468 fitness_history: self.fitness_history.clone(),
469 stagnation_counter: self.stagnation_counter,
470 request_count: self.request_count,
471 stats: self.stats,
472 schema_version: 2,
473 corpus: self.corpus.clone(),
474 next_id: self.next_id,
475 generation_evals: self.generation_evals,
476 };
477 save_checkpoint(path, &state)
478 }
479
480 pub fn load_checkpoint(&mut self, path: &Path) -> Result<(), EvolutionError> {
482 let mut state: EngineState = load_checkpoint(path)?;
483 state.stats.fixup_start_time();
484 self.algorithm.restore(&state.algorithm_state)?;
485 self.gene_pool = state.gene_pool;
486 self.budget = state.budget;
487 self.gene_stats = state.gene_stats;
488 self.fitness_history = state.fitness_history;
489 self.stagnation_counter = state.stagnation_counter;
490 self.request_count = state.request_count;
491 self.stats = state.stats;
492 self.corpus = state.corpus;
495 self.next_id = state.next_id;
496 self.generation_evals = state.generation_evals;
497 Ok(())
498 }
499
500 #[must_use]
502 pub fn gene_success_rates(&self) -> Vec<(&str, &str, f64)> {
503 crate::evolution::fitness::gene_success_rates(&self.gene_stats)
504 }
505
506 #[must_use]
508 pub fn learned_summary(&self) -> String {
509 crate::evolution::fitness::learned_summary(
510 self.stats.generation,
511 self.algorithm.best(),
512 &self.gene_stats,
513 self.request_count,
514 )
515 }
516
517 pub fn seed_population(&mut self, population: Vec<Chromosome>) {
521 let mut rng = self.rng.clone();
522 self.algorithm
523 .initialize(population, &self.gene_pool, &mut rng);
524 }
525
526 #[must_use]
530 pub fn population_snapshot(&self) -> Vec<Chromosome> {
531 self.algorithm.population_snapshot()
532 }
533
534 #[must_use]
550 pub fn diversity_score(&self) -> f64 {
551 let mut population = self.algorithm.population_snapshot();
552 for (_, chromosome, _) in self.in_flight.values() {
553 population.push(chromosome.clone());
554 }
555 if population.len() >= 2 {
556 return crate::evolution::crossover::diversity::diversity_score(&population);
557 }
558 let gene_div = self.gene_stats_diversity();
559 if gene_div > 0.0 { gene_div } else { 1.0 }
560 }
561
562 #[must_use]
576 pub fn gene_stats_diversity(&self) -> f64 {
577 if self.gene_stats.is_empty() {
578 return 0.0;
579 }
580 let mut by_gene: HashMap<&str, Vec<u32>> = HashMap::new();
582 for (name, _value, _successes, attempts) in &self.gene_stats {
583 if *attempts == 0 {
584 continue;
585 }
586 by_gene.entry(name.as_str()).or_default().push(*attempts);
587 }
588 if by_gene.is_empty() {
589 return 0.0;
590 }
591 let mut entropy_sum = 0.0_f64;
592 let mut counted = 0_usize;
593 for attempts in by_gene.values() {
594 let total: u64 = attempts.iter().map(|a| u64::from(*a)).sum();
595 if total == 0 || attempts.len() < 2 {
596 counted += 1;
599 continue;
600 }
601 #[allow(clippy::cast_precision_loss)]
602 let total_f = total as f64;
603 let mut h = 0.0_f64;
604 for a in attempts {
605 #[allow(clippy::cast_precision_loss)]
606 let p = f64::from(*a) / total_f;
607 if p > 0.0 {
608 h -= p * p.log2();
609 }
610 }
611 #[allow(clippy::cast_precision_loss)]
614 let h_max = (attempts.len() as f64).log2();
615 let normalised = if h_max > 0.0 { h / h_max } else { 0.0 };
616 entropy_sum += normalised;
617 counted += 1;
618 }
619 if counted == 0 {
620 0.0
621 } else {
622 #[allow(clippy::cast_precision_loss)]
623 let avg = entropy_sum / counted as f64;
624 avg.clamp(0.0, 1.0)
625 }
626 }
627}
628
629#[derive(Debug, Clone, Serialize, Deserialize)]
650pub struct EngineState {
651 pub algorithm_name: String,
652 pub algorithm_state: Vec<u8>,
653 pub gene_pool: GenePool,
654 pub rng_seed: u64,
655 pub budget: Budget,
656 pub gene_stats: Vec<(String, String, u32, u32)>,
657 pub fitness_history: VecDeque<f64>,
658 pub stagnation_counter: u32,
659 pub request_count: usize,
660 pub stats: SearchStats,
661 pub schema_version: u32,
662 #[serde(default)]
665 pub corpus: BypassCorpus,
666 #[serde(default)]
670 pub next_id: u64,
671 #[serde(default)]
673 pub generation_evals: usize,
674}
675
676use serde::{Deserialize, Serialize};