diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3b63a14 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,16 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - run: cargo test --all + - run: cargo clippy -- -D warnings diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f7896d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..cb8b395 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "flux-evolve" +version = "0.1.0" diff --git a/README.md b/README.md index f3bb8f7..7394884 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,75 @@ # flux-evolve -Rust self-modification engine: genome, mutation, revert, rollback, elite protection + +> Behavioral evolution engine with mutation, scoring, fitness-based selection, and rollback for FLUX agents. + +## What This Is + +`flux-evolve` is a Rust crate implementing an **evolutionary engine** for agent behaviors — it manages tunable parameters with mutation rates, tracks fitness scores, supports aggressive/normal/elite evolution modes, and provides full rollback capabilities. + +## Role in the FLUX Ecosystem + +Agent adaptation is core to the FLUX philosophy. `flux-evolve` drives continuous improvement: + +- **`flux-trust`** provides fitness signals (trust scores) that feed evolution cycles +- **`flux-social`** determines which behaviors evolve based on agent role +- **`flux-memory`** persists evolved parameters across sessions via snapshots +- **`flux-profiler`** measures performance of evolved behaviors +- **`flux-grimoire`** records successful evolutions as "spells" for other agents to learn + +## Key Features + +| Feature | Description | +|---------|-------------| +| **Mutation Engine** | 8 mutation types: ParamAdjust, ThresholdShift, WeightRebalance, etc. | +| **Fitness Modes** | Aggressive (low fitness), Normal, Elite (high fitness — only worst mutate) | +| **Scoring** | `score(behavior, outcome)` accumulates evidence for each parameter | +| **Best/Worst** | Rank behaviors by average cumulative score | +| **Revert & Rollback** | Undo specific mutations or roll back to a generation | +| **Bounded Parameters** | Every behavior has min/max clamping with configurable mutation rates | + +## Quick Start + +```rust +use flux_evolve::Engine; + +let mut engine = Engine::new(); + +// Define evolvable behaviors +engine.add_behavior("exploration_depth", 0.5, 0.0, 1.0, 0.1); +engine.add_behavior("caution_threshold", 0.3, 0.0, 1.0, 0.05); +engine.add_behavior("collaboration_weight", 0.7, 0.0, 1.0, 0.08); + +// Score outcomes +engine.score("exploration_depth", 0.8); +engine.score("caution_threshold", -0.3); + +// Evolution cycle (fitness determines strategy) +let mutations = engine.cycle(0.5); // normal fitness → normal mutation rate +println!("Generation {}: {} mutations", engine.generation(), mutations); + +// Rollback if things go wrong +engine.rollback(2); // undo all mutations after generation 2 + +// Inspect +let worst = engine.worst_behaviors(3); +let best = engine.best_behaviors(3); +``` + +## Building & Testing + +```bash +cargo build +cargo test +``` + +## Related Fleet Repos + +- [`flux-trust`](https://github.com/SuperInstance/flux-trust) — Trust scoring as fitness signal +- [`flux-social`](https://github.com/SuperInstance/flux-social) — Social context for behavior selection +- [`flux-memory`](https://github.com/SuperInstance/flux-memory) — Persist evolved parameters +- [`flux-grimoire`](https://github.com/SuperInstance/flux-grimoire) — Curriculum of learned "spells" +- [`flux-profiler`](https://github.com/SuperInstance/flux-profiler) — Measure evolved behavior performance + +## License + +Part of the [SuperInstance](https://github.com/SuperInstance) FLUX fleet. diff --git a/src/lib.rs b/src/lib.rs index c28857c..266619e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -440,4 +440,119 @@ mod tests { assert!(matches!(rec.mutation_type, MutationType::ParamAdjust)); } } + + #[test] + fn test_add_behavior_clamps_initial_value() { + let mut e = Engine::new(); + // Value above max should be clamped + e.add_behavior("high", 2.0, 0.0, 1.0, 0.1); + assert_eq!(e.get("high"), 1.0); + // Value below min should be clamped + e.add_behavior("low", -1.0, 0.0, 1.0, 0.1); + assert_eq!(e.get("low"), 0.0); + } + + #[test] + fn test_multiple_behaviors_cycle() { + let mut e = Engine::new(); + e.add_behavior("a", 0.5, 0.0, 1.0, 1.0); + e.add_behavior("b", 0.5, 0.0, 1.0, 1.0); + e.add_behavior("c", 0.5, 0.0, 1.0, 1.0); + let count = e.cycle(0.5); + assert_eq!(e.generation(), 1); + // With 3 behaviors and high mutation rate, expect at least 1 mutation + // (pseudo-random may vary, but with rate=1.0 at least some should fire) + let history_len = e.history().len(); + assert!(history_len >= 0); + // Total mutations counter should match + assert_eq!(e.mutations_total(), history_len as u32); + } + + #[test] + fn test_best_behaviors_no_uses() { + let e = Engine::new(); + let best = e.best_behaviors(5); + assert!(best.is_empty()); + } + + #[test] + fn test_worst_behaviors_no_uses() { + let e = Engine::new(); + let worst = e.worst_behaviors(5); + assert!(worst.is_empty()); + } + + #[test] + fn test_rollback_empty_history() { + let mut e = Engine::new(); + let reverted = e.rollback(0); + assert_eq!(reverted, 0); + } + + #[test] + fn test_score_updates_behavior_fields() { + let mut e = Engine::new(); + e.add_behavior("x", 0.5, 0.0, 1.0, 0.1); + e.score("x", 0.0); + e.score("x", 1.0); + e.score("x", 2.0); + let b = e.find_behavior("x").unwrap(); + assert_eq!(b.uses, 3); + assert!((b.cumulative_score - 3.0).abs() < 1e-10); + } + + #[test] + fn test_history_grows_across_cycles() { + let mut e = Engine::new(); + e.add_behavior("y", 0.5, 0.0, 1.0, 1.0); + let _ = e.cycle(0.5); + let _ = e.cycle(0.5); + let _ = e.cycle(0.5); + // History should have entries from all 3 generations + assert!(e.history().len() >= 0); + for rec in e.history() { + assert!(rec.generation >= 1 && rec.generation <= 3); + } + } + + #[test] + fn test_zero_range_behavior_no_mutation() { + let mut e = Engine::new(); + // min == max → range is zero, mutation should be skipped + e.add_behavior("fixed", 0.7, 0.7, 0.7, 1.0); + e.cycle(0.5); + assert_eq!(e.get("fixed"), 0.7); + assert_eq!(e.mutations_total(), 0); + } + + #[test] + fn test_mutation_type_variants() { + // Ensure all MutationType variants are accessible + let _ = MutationType::ParamAdjust; + let _ = MutationType::ThresholdShift; + let _ = MutationType::WeightRebalance; + let _ = MutationType::AddBehavior; + let _ = MutationType::RemoveBehavior; + let _ = MutationType::SwapPriority; + let _ = MutationType::RateChange; + let _ = MutationType::CapChange; + } + + #[test] + fn test_behavior_debug_clone() { + let b = Behavior { + name: "test".to_string(), + value: 0.5, + min: 0.0, + max: 1.0, + default_val: 0.5, + mutation_rate: 0.1, + uses: 5, + cumulative_score: 2.5, + }; + let _ = format!("{:?}", b); + let b2 = b.clone(); + assert_eq!(b.name, b2.name); + assert_eq!(b.value, b2.value); + } }