wafrift_evolution/evolution/
engine.rs1use 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.algorithm.checkpoint().unwrap_or_default();
62 let mut restored = Self::with_algorithm(
63 self.algorithm.name(),
64 self.gene_pool.clone(),
65 self.rng.clone(),
66 self.budget,
67 )
68 .unwrap_or_else(|_| Self::new(20));
69 let _ = restored.algorithm.restore(&alg_bytes);
70 restored.cache = LruCache::new(self.cache.cap());
71 restored.gene_stats = self.gene_stats.clone();
73 restored.fitness_history = self.fitness_history.clone();
74 restored.stagnation_counter = self.stagnation_counter;
75 restored.corpus = self.corpus.clone();
76 restored.request_count = self.request_count;
77 restored.stats = self.stats;
78 restored.next_id = self.next_id;
79 restored.pending_single = None;
80 restored
81 }
82}
83
84impl EvolutionEngine {
85 #[must_use]
87 pub fn new(population_size: usize) -> Self {
88 Self::new_seeded(population_size, 0)
89 }
90
91 #[must_use]
93 pub fn new_seeded(population_size: usize, seed: u64) -> Self {
94 let gene_pool = GenePool::default_wafrift();
95 let mut rng = StdRng::seed_from_u64(seed);
96 let mut population: Vec<Chromosome> = (0..population_size)
97 .map(|_| random_chromosome(&gene_pool, &mut rng))
98 .collect();
99 if population_size > 0 {
100 population[0] = baseline_chromosome(&gene_pool);
101 }
102
103 let mut engine = Self::with_algorithm("hill_climbing", gene_pool, rng, Budget::default())
104 .expect("hill_climbing is built-in");
105 engine
106 .algorithm
107 .initialize(population, &engine.gene_pool, &mut engine.rng.clone());
108 let mut population2: Vec<Chromosome> = (0..population_size)
110 .map(|_| random_chromosome(&engine.gene_pool, &mut engine.rng))
111 .collect();
112 if population_size > 0 {
113 population2[0] = baseline_chromosome(&engine.gene_pool);
114 }
115 engine
116 .algorithm
117 .initialize(population2, &engine.gene_pool, &mut engine.rng);
118 engine
119 }
120
121 pub fn with_algorithm(
123 algorithm_name: &str,
124 gene_pool: GenePool,
125 rng: StdRng,
126 budget: Budget,
127 ) -> Result<Self, EvolutionError> {
128 let algorithm: Box<dyn SearchAlgorithm> = match algorithm_name {
129 "hill_climbing" => Box::new(crate::search::HillClimbing::new()),
130 "simulated_annealing" => Box::new(crate::search::SimulatedAnnealing::new()),
131 "tabu_search" => Box::new(crate::search::TabuSearch::new(20)),
132 "novelty_search" => Box::new(crate::search::NoveltySearch::new(15, 0.3)),
133 "map_elites" => Box::new(crate::search::MapElites::new()),
134 _ => {
135 return Err(EvolutionError::AlgorithmError(format!(
136 "unknown algorithm: {algorithm_name}"
137 )));
138 }
139 };
140
141 Ok(Self {
142 algorithm,
143 gene_pool,
144 rng,
145 cache: LruCache::new(NonZeroUsize::new(10_000).unwrap()),
146 budget,
147 in_flight: HashMap::new(),
148 stats: SearchStats::new(),
149 target_health: TargetHealthMonitor::new(),
150 checkpoint_path: None,
151 request_count: 0,
152 gene_stats: Vec::new(),
153 fitness_history: Vec::new(),
154 stagnation_counter: 0,
155 corpus: BypassCorpus::new(),
156 generation_evals: 0,
157 next_id: 0,
158 pending_single: None,
159 })
160 }
161
162 fn cache_key(chromosome: &Chromosome) -> String {
163 let mut parts: Vec<_> = chromosome
164 .genes
165 .iter()
166 .map(|(n, v)| format!("{n}={v}"))
167 .collect();
168 parts.sort();
169 parts.join(";")
170 }
171
172 fn next_eval_id(&mut self) -> u64 {
173 self.next_id += 1;
174 self.next_id
175 }
176
177 #[must_use]
181 pub fn next_candidate(&mut self) -> Option<(usize, &Chromosome)> {
182 if self.should_terminate() {
183 return None;
184 }
185 if self.pending_single.is_none() {
186 let batch = self.batch_candidates(1);
187 if batch.is_empty() {
188 return None;
189 }
190 self.pending_single = Some(batch.into_iter().next().unwrap());
191 }
192 self.pending_single
193 .as_ref()
194 .map(|(idx, chrom)| (*idx, chrom))
195 }
196
197 pub fn batch_candidates(&mut self, n: usize) -> Vec<(usize, Chromosome)> {
201 if self.should_terminate() || n == 0 {
202 return Vec::new();
203 }
204
205 let mut result = Vec::with_capacity(n);
206 let mut cached_results = Vec::new();
207 let requested = self.algorithm.request_evaluations(n, &mut self.rng);
208
209 for candidate in requested {
210 let key = Self::cache_key(&candidate.chromosome);
211 if let Some(verdict) = self.cache.get(&key).copied() {
212 cached_results.push((candidate.id, verdict));
213 } else {
214 let eval_id = self.next_eval_id();
215 self.in_flight
216 .insert(eval_id, (candidate.chromosome.clone(), Instant::now()));
217 result.push((eval_id as usize, candidate.chromosome));
218 }
219 }
220
221 if !cached_results.is_empty() {
222 self.algorithm.submit_evaluations(cached_results);
223 }
224
225 self.request_count += result.len();
226 result
227 }
228
229 pub fn submit_batch(
235 &mut self,
236 results: Vec<(usize, OracleVerdict)>,
237 ) -> Result<(), EvolutionError> {
238 let mut to_submit: Vec<(u64, OracleVerdict)> = Vec::with_capacity(results.len());
239 for (id_usize, verdict) in results {
240 let id = id_usize as u64;
241 let (mut chromosome, _sent_at) = self
242 .in_flight
243 .remove(&id)
244 .ok_or(EvolutionError::InvalidChromosomeIndex(id_usize))?;
245
246 chromosome.record_verdict(&verdict);
247 let key = Self::cache_key(&chromosome);
248 self.cache.put(key, verdict);
249
250 update_gene_stats(&mut self.gene_stats, &chromosome.genes, verdict.passed);
251 let adjusted = evolutionary_fitness(&chromosome, &self.gene_stats);
252 chromosome.fitness = adjusted;
253
254 if chromosome.fitness >= 0.85
256 && !self
257 .corpus
258 .entries
259 .iter()
260 .any(|e| e.payload_hash == format!("{:016x}", chromosome.hash()))
261 {
262 self.corpus
263 .add(BypassEntry::from_chromosome(&chromosome, None));
264 }
265
266 to_submit.push((id, verdict));
267 self.generation_evals += 1;
268 self.stats.evaluations += 1;
269
270 if verdict.passed {
271 self.target_health.record_success();
272 } else if verdict.status_delta >= 500 {
273 self.target_health.record_error();
274 }
275 }
276
277 self.algorithm.submit_evaluations(to_submit);
278 Ok(())
279 }
280
281 pub fn record_feedback(
283 &mut self,
284 chromosome_index: usize,
285 passed: bool,
286 ) -> Result<(), EvolutionError> {
287 if let Some((idx, _)) = self.pending_single
289 && idx == chromosome_index
290 {
291 self.pending_single = None;
292 }
293 self.record_verdict(chromosome_index, &OracleVerdict::from_bool(passed))
294 }
295
296 pub fn record_verdict(
298 &mut self,
299 chromosome_index: usize,
300 verdict: &OracleVerdict,
301 ) -> Result<(), EvolutionError> {
302 self.submit_batch(vec![(chromosome_index, *verdict)])
303 }
304
305 pub fn record_target_error(&mut self, error: String) -> Result<(), EvolutionError> {
307 self.target_health.record_error();
308 if !self.target_health.is_healthy() {
309 return Err(EvolutionError::TargetHealthCritical(error));
310 }
311 Ok(())
312 }
313
314 pub fn evolve(&mut self) {
316 if self.algorithm.best().is_none() {
317 return;
318 }
319
320 if let Some(best) = self.algorithm.best() {
322 self.fitness_history.push(best.fitness);
323 }
324 if self.fitness_history.len() > 1000 {
325 self.fitness_history.remove(0);
326 }
327
328 let window = 10_usize;
330 if self.fitness_history.len() >= window {
331 let recent = &self.fitness_history[self.fitness_history.len() - window..];
332 let improved = recent.windows(2).any(|w| w[1] > w[0] + 0.001);
333 if !improved {
334 self.stagnation_counter += 1;
335 } else {
336 self.stagnation_counter = 0;
337 }
338 }
339
340 self.stats.generation += 1;
341 self.generation_evals = 0;
342
343 if let Some(ref path) = self.checkpoint_path {
344 let _ = self.save_checkpoint(path);
345 }
346 }
347
348 #[must_use]
350 pub fn should_terminate(&self) -> bool {
351 if !self.target_health.is_healthy() {
352 return true;
353 }
354 self.algorithm.should_terminate(&self.stats, &self.budget)
355 || self.request_count >= self.budget.max_requests
356 || self.stats.stagnation_counter >= self.budget.stagnation_limit
357 }
358
359 #[must_use]
361 pub fn best(&self) -> Option<&Chromosome> {
362 self.algorithm.best()
363 }
364
365 pub fn save_checkpoint(&self, path: &Path) -> Result<(), EvolutionError> {
367 let state = EngineState {
368 algorithm_name: self.algorithm.name().to_string(),
369 algorithm_state: self.algorithm.checkpoint()?,
370 gene_pool: self.gene_pool.clone(),
371 rng_seed: 0, budget: self.budget,
373 gene_stats: self.gene_stats.clone(),
374 fitness_history: self.fitness_history.clone(),
375 stagnation_counter: self.stagnation_counter,
376 request_count: self.request_count,
377 stats: self.stats,
378 schema_version: 1,
379 };
380 save_checkpoint(path, &state)
381 }
382
383 pub fn load_checkpoint(&mut self, path: &Path) -> Result<(), EvolutionError> {
385 let mut state: EngineState = load_checkpoint(path)?;
386 state.stats.fixup_start_time();
387 self.algorithm.restore(&state.algorithm_state)?;
388 self.gene_pool = state.gene_pool;
389 self.budget = state.budget;
390 self.gene_stats = state.gene_stats;
391 self.fitness_history = state.fitness_history;
392 self.stagnation_counter = state.stagnation_counter;
393 self.request_count = state.request_count;
394 self.stats = state.stats;
395 Ok(())
396 }
397
398 #[must_use]
400 pub fn gene_success_rates(&self) -> Vec<(&str, &str, f64)> {
401 crate::evolution::fitness::gene_success_rates(&self.gene_stats)
402 }
403
404 #[must_use]
406 pub fn learned_summary(&self) -> String {
407 crate::evolution::fitness::learned_summary(
408 self.stats.generation,
409 self.algorithm.best(),
410 &self.gene_stats,
411 self.request_count,
412 )
413 }
414
415 #[must_use]
417 pub fn diversity_score(&self) -> f64 {
418 0.5
420 }
421}
422
423#[derive(Debug, Clone, Serialize, Deserialize)]
425pub struct EngineState {
426 pub algorithm_name: String,
427 pub algorithm_state: Vec<u8>,
428 pub gene_pool: GenePool,
429 pub rng_seed: u64,
430 pub budget: Budget,
431 pub gene_stats: Vec<(String, String, u32, u32)>,
432 pub fitness_history: Vec<f64>,
433 pub stagnation_counter: u32,
434 pub request_count: usize,
435 pub stats: SearchStats,
436 pub schema_version: u32,
437}
438
439use serde::{Deserialize, Serialize};