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;
15use std::num::NonZeroUsize;
16use std::path::{Path, PathBuf};
17use std::time::Instant;
18
19#[derive(Debug)]
21pub struct EvolutionEngine {
22 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, (Chromosome, Instant)>,
34 pub stats: SearchStats,
36 pub target_health: TargetHealthMonitor,
38 pub checkpoint_path: Option<PathBuf>,
40 pub request_count: usize,
42 pub gene_stats: Vec<(String, String, u32, u32)>,
44 pub fitness_history: Vec<f64>,
46 pub stagnation_counter: u32,
48 pub corpus: BypassCorpus,
50 generation_evals: usize,
52 next_id: u64,
54 pending_single: Option<(usize, Chromosome)>,
56}
57
58impl Clone for EvolutionEngine {
59 fn clone(&self) -> Self {
60 let alg_bytes = self
66 .algorithm
67 .checkpoint()
68 .expect("algorithm checkpoint must succeed for a live engine");
69 let mut restored = Self::with_algorithm(
70 self.algorithm.name(),
71 self.gene_pool.clone(),
72 self.rng.clone(),
73 self.budget,
74 )
75 .expect("re-constructing the same registered algorithm must succeed");
76 restored
77 .algorithm
78 .restore(&alg_bytes)
79 .expect("restoring fresh checkpoint into matching algorithm must succeed");
80 restored.cache = LruCache::new(self.cache.cap());
81 restored.gene_stats = self.gene_stats.clone();
82 restored.fitness_history = self.fitness_history.clone();
83 restored.stagnation_counter = self.stagnation_counter;
84 restored.corpus = self.corpus.clone();
85 restored.request_count = self.request_count;
86 restored.stats = self.stats;
87 restored.next_id = self.next_id;
88 restored.pending_single = None;
89 restored
90 }
91}
92
93impl EvolutionEngine {
94 #[must_use]
96 pub fn new(population_size: usize) -> Self {
97 Self::new_seeded(population_size, 0)
98 }
99
100 #[must_use]
107 pub fn new_seeded(population_size: usize, seed: u64) -> Self {
108 let population_size = population_size.clamp(1, 10_000);
109 let gene_pool = GenePool::default_wafrift();
110 let mut rng = StdRng::seed_from_u64(seed);
111 let mut population: Vec<Chromosome> = (0..population_size)
112 .map(|_| random_chromosome(&gene_pool, &mut rng))
113 .collect();
114 if population_size > 0 {
115 population[0] = baseline_chromosome(&gene_pool);
116 }
117
118 let mut engine = Self::with_algorithm("hill_climbing", gene_pool, rng, Budget::default())
119 .expect("hill_climbing is built-in");
120 engine
121 .algorithm
122 .initialize(population, &engine.gene_pool, &mut engine.rng.clone());
123 let mut population2: Vec<Chromosome> = (0..population_size)
125 .map(|_| random_chromosome(&engine.gene_pool, &mut engine.rng))
126 .collect();
127 if population_size > 0 {
128 population2[0] = baseline_chromosome(&engine.gene_pool);
129 }
130 engine
131 .algorithm
132 .initialize(population2, &engine.gene_pool, &mut engine.rng);
133 engine
134 }
135
136 pub fn with_algorithm(
138 algorithm_name: &str,
139 gene_pool: GenePool,
140 rng: StdRng,
141 budget: Budget,
142 ) -> Result<Self, EvolutionError> {
143 let algorithm: Box<dyn SearchAlgorithm> = match algorithm_name {
144 "hill_climbing" => Box::new(crate::search::HillClimbing::new()),
145 "simulated_annealing" => Box::new(crate::search::SimulatedAnnealing::new()),
146 "tabu_search" => Box::new(crate::search::TabuSearch::new(20)),
147 "novelty_search" => Box::new(crate::search::NoveltySearch::new(15, 0.3)),
148 "map_elites" => Box::new(crate::search::MapElites::new()),
149 _ => {
150 return Err(EvolutionError::AlgorithmError(format!(
151 "unknown algorithm: {algorithm_name}"
152 )));
153 }
154 };
155
156 Ok(Self {
157 algorithm,
158 gene_pool,
159 rng,
160 cache: LruCache::new(NonZeroUsize::new(10_000).unwrap()),
161 budget,
162 in_flight: HashMap::new(),
163 stats: SearchStats::new(),
164 target_health: TargetHealthMonitor::new(),
165 checkpoint_path: None,
166 request_count: 0,
167 gene_stats: Vec::new(),
168 fitness_history: Vec::new(),
169 stagnation_counter: 0,
170 corpus: BypassCorpus::new(),
171 generation_evals: 0,
172 next_id: 0,
173 pending_single: None,
174 })
175 }
176
177 fn cache_key(chromosome: &Chromosome) -> String {
178 let mut parts: Vec<_> = chromosome
179 .genes
180 .iter()
181 .map(|(n, v)| format!("{n}={v}"))
182 .collect();
183 parts.sort();
184 parts.join(";")
185 }
186
187 fn next_eval_id(&mut self) -> u64 {
188 self.next_id += 1;
189 self.next_id
190 }
191
192 #[must_use]
196 pub fn next_candidate(&mut self) -> Option<(usize, &Chromosome)> {
197 if self.should_terminate() {
198 return None;
199 }
200 if self.pending_single.is_none() {
201 let batch = self.batch_candidates(1);
202 if batch.is_empty() {
203 return None;
204 }
205 self.pending_single = Some(batch.into_iter().next().unwrap());
206 }
207 self.pending_single
208 .as_ref()
209 .map(|(idx, chrom)| (*idx, chrom))
210 }
211
212 pub fn batch_candidates(&mut self, n: usize) -> Vec<(usize, Chromosome)> {
221 if self.should_terminate() || n == 0 {
222 return Vec::new();
223 }
224 let remaining = self
225 .budget
226 .max_requests
227 .saturating_sub(self.request_count);
228 if remaining == 0 {
229 return Vec::new();
230 }
231 let n = n.min(remaining);
232
233 let mut result = Vec::with_capacity(n);
234 let mut cached_results = Vec::new();
235 let requested = self.algorithm.request_evaluations(n, &mut self.rng);
236
237 for candidate in requested {
238 let key = Self::cache_key(&candidate.chromosome);
239 if let Some(verdict) = self.cache.get(&key).copied() {
240 cached_results.push((candidate.id, verdict));
241 } else {
242 let eval_id = self.next_eval_id();
243 self.in_flight
244 .insert(eval_id, (candidate.chromosome.clone(), Instant::now()));
245 result.push((eval_id as usize, candidate.chromosome));
246 }
247 }
248
249 if !cached_results.is_empty() {
250 self.algorithm.submit_evaluations(cached_results);
251 }
252
253 self.request_count += result.len();
254 result
255 }
256
257 pub fn submit_batch(
263 &mut self,
264 results: Vec<(usize, OracleVerdict)>,
265 ) -> Result<(), EvolutionError> {
266 let mut to_submit: Vec<(u64, OracleVerdict)> = Vec::with_capacity(results.len());
267 for (id_usize, verdict) in results {
268 let id = id_usize as u64;
269 let (mut chromosome, _sent_at) = self
270 .in_flight
271 .remove(&id)
272 .ok_or(EvolutionError::InvalidChromosomeIndex(id_usize))?;
273
274 chromosome.record_verdict(&verdict);
275 let key = Self::cache_key(&chromosome);
276 self.cache.put(key, verdict);
277
278 update_gene_stats(&mut self.gene_stats, &chromosome.genes, verdict.passed);
279 let adjusted = evolutionary_fitness(&chromosome, &self.gene_stats);
280 chromosome.fitness = adjusted;
281
282 if chromosome.fitness >= 0.85
284 && !self
285 .corpus
286 .entries
287 .iter()
288 .any(|e| e.payload_hash == format!("{:016x}", chromosome.hash()))
289 {
290 self.corpus
291 .add(BypassEntry::from_chromosome(&chromosome, None));
292 }
293
294 to_submit.push((id, verdict));
295 self.generation_evals += 1;
296 self.stats.evaluations += 1;
297
298 if verdict.passed {
299 self.target_health.record_success();
300 } else if verdict.status_delta >= 500 {
301 self.target_health.record_error();
302 }
303 }
304
305 self.algorithm.submit_evaluations(to_submit);
306 Ok(())
307 }
308
309 pub fn record_feedback(
311 &mut self,
312 chromosome_index: usize,
313 passed: bool,
314 ) -> Result<(), EvolutionError> {
315 if let Some((idx, _)) = self.pending_single
317 && idx == chromosome_index
318 {
319 self.pending_single = None;
320 }
321 self.record_verdict(chromosome_index, &OracleVerdict::from_bool(passed))
322 }
323
324 pub fn record_verdict(
326 &mut self,
327 chromosome_index: usize,
328 verdict: &OracleVerdict,
329 ) -> Result<(), EvolutionError> {
330 self.submit_batch(vec![(chromosome_index, *verdict)])
331 }
332
333 pub fn record_target_error(&mut self, error: String) -> Result<(), EvolutionError> {
335 self.target_health.record_error();
336 if !self.target_health.is_healthy() {
337 return Err(EvolutionError::TargetHealthCritical(error));
338 }
339 Ok(())
340 }
341
342 pub fn evolve(&mut self) {
344 if self.algorithm.best().is_none() {
345 return;
346 }
347
348 if let Some(best) = self.algorithm.best() {
350 self.fitness_history.push(best.fitness);
351 }
352 if self.fitness_history.len() > 1000 {
353 self.fitness_history.remove(0);
354 }
355
356 let window = 10_usize;
358 if self.fitness_history.len() >= window {
359 let recent = &self.fitness_history[self.fitness_history.len() - window..];
360 let improved = recent.windows(2).any(|w| w[1] > w[0] + 0.001);
361 if !improved {
362 self.stagnation_counter += 1;
363 } else {
364 self.stagnation_counter = 0;
365 }
366 }
367
368 self.stats.generation += 1;
369 self.generation_evals = 0;
370
371 if let Some(ref path) = self.checkpoint_path {
372 let _ = self.save_checkpoint(path);
373 }
374 }
375
376 #[must_use]
378 pub fn should_terminate(&self) -> bool {
379 if !self.target_health.is_healthy() {
380 return true;
381 }
382 self.algorithm.should_terminate(&self.stats, &self.budget)
383 || self.request_count >= self.budget.max_requests
384 || self.stats.stagnation_counter >= self.budget.stagnation_limit
385 }
386
387 #[must_use]
389 pub fn best(&self) -> Option<&Chromosome> {
390 self.algorithm.best()
391 }
392
393 pub fn save_checkpoint(&self, path: &Path) -> Result<(), EvolutionError> {
395 let state = EngineState {
396 algorithm_name: self.algorithm.name().to_string(),
397 algorithm_state: self.algorithm.checkpoint()?,
398 gene_pool: self.gene_pool.clone(),
399 rng_seed: 0, budget: self.budget,
401 gene_stats: self.gene_stats.clone(),
402 fitness_history: self.fitness_history.clone(),
403 stagnation_counter: self.stagnation_counter,
404 request_count: self.request_count,
405 stats: self.stats,
406 schema_version: 1,
407 };
408 save_checkpoint(path, &state)
409 }
410
411 pub fn load_checkpoint(&mut self, path: &Path) -> Result<(), EvolutionError> {
413 let mut state: EngineState = load_checkpoint(path)?;
414 state.stats.fixup_start_time();
415 self.algorithm.restore(&state.algorithm_state)?;
416 self.gene_pool = state.gene_pool;
417 self.budget = state.budget;
418 self.gene_stats = state.gene_stats;
419 self.fitness_history = state.fitness_history;
420 self.stagnation_counter = state.stagnation_counter;
421 self.request_count = state.request_count;
422 self.stats = state.stats;
423 Ok(())
424 }
425
426 #[must_use]
428 pub fn gene_success_rates(&self) -> Vec<(&str, &str, f64)> {
429 crate::evolution::fitness::gene_success_rates(&self.gene_stats)
430 }
431
432 #[must_use]
434 pub fn learned_summary(&self) -> String {
435 crate::evolution::fitness::learned_summary(
436 self.stats.generation,
437 self.algorithm.best(),
438 &self.gene_stats,
439 self.request_count,
440 )
441 }
442
443 #[must_use]
445 pub fn diversity_score(&self) -> f64 {
446 0.5
448 }
449}
450
451#[derive(Debug, Clone, Serialize, Deserialize)]
453pub struct EngineState {
454 pub algorithm_name: String,
455 pub algorithm_state: Vec<u8>,
456 pub gene_pool: GenePool,
457 pub rng_seed: u64,
458 pub budget: Budget,
459 pub gene_stats: Vec<(String, String, u32, u32)>,
460 pub fitness_history: Vec<f64>,
461 pub stagnation_counter: u32,
462 pub request_count: usize,
463 pub stats: SearchStats,
464 pub schema_version: u32,
465}
466
467use serde::{Deserialize, Serialize};