collet 0.1.1

Relentless agentic coding orchestrator with zero-drop agent loops
Documentation
use super::super::App;
use tokio::sync::mpsc;

use crate::agent::r#loop::AgentEvent;

impl App {
    pub(crate) fn start_evolution_loop(
        &mut self,
        event_tx: mpsc::UnboundedSender<AgentEvent>,
        extra_args: &[String],
    ) {
        use crate::evolution::{
            adapter::ColletEvolvable,
            benchmarks::{FileBenchmark, NullBenchmark, SingleTaskBenchmark, SweBenchAdapter},
            config::EvolveConfig,
            engines::SkillforgeEngine,
            r#loop::EvolutionLoop,
            trial::{BenchmarkAdapter, TrialRunner},
        };
        use std::path::PathBuf;
        use tokio::sync::mpsc as tpsc;
        use tokio_util::sync::CancellationToken;

        // Parse args: [task text words] --cycles N --benchmark <name> --tasks <path> ...
        let mut cycles: Option<u32> = None;
        let mut benchmark_name: Option<String> = None;
        let mut tasks_path: Option<PathBuf> = None;
        let mut batch = 5usize;
        let mut docker = false;
        let mut score_cmd: Option<String> = None;
        let mut task_words: Vec<String> = Vec::new();
        let mut i = 0;
        while i < extra_args.len() {
            match extra_args[i].as_str() {
                "--cycles" => {
                    i += 1;
                    cycles = extra_args.get(i).and_then(|s| s.parse().ok());
                }
                "--benchmark" => {
                    i += 1;
                    benchmark_name = extra_args.get(i).cloned();
                }
                "--tasks" => {
                    i += 1;
                    tasks_path = extra_args.get(i).map(PathBuf::from);
                }
                "--batch" => {
                    i += 1;
                    batch = extra_args.get(i).and_then(|s| s.parse().ok()).unwrap_or(5);
                }
                "--docker" => docker = true,
                "--score-cmd" => {
                    i += 1;
                    score_cmd = extra_args.get(i).cloned();
                }
                other if other.starts_with("--") => {} // unknown flag
                word => task_words.push(word.to_string()), // positional → task text
            }
            i += 1;
        }
        let task_text: Option<String> = if task_words.is_empty() {
            None
        } else {
            Some(task_words.join(" "))
        };

        let workspace_root = PathBuf::from(&self.working_dir)
            .join(".collet")
            .join("workspace");
        let eval_dir = PathBuf::from(&self.working_dir)
            .join(".collet")
            .join("eval");
        let _ = std::fs::create_dir_all(&workspace_root);

        // Determine benchmark + default cycle count
        let (benchmark, benchmark_label, cycles_default): (Box<dyn BenchmarkAdapter>, String, u32) =
            if let Some(ref text) = task_text {
                (
                    Box::new(SingleTaskBenchmark::from_text(text.clone())),
                    "inline".to_string(),
                    1,
                )
            } else {
                match benchmark_name.as_deref().unwrap_or("null") {
                    "file" => {
                        let path = tasks_path
                            .clone()
                            .unwrap_or_else(|| PathBuf::from("tasks.jsonl"));
                        let mut fb = FileBenchmark::new(path);
                        if let Some(cmd) = score_cmd {
                            fb = fb.with_score_cmd(cmd);
                        }
                        (Box::new(fb), "file".to_string(), 10)
                    }
                    "swebench" | "swe-bench" => {
                        let path = tasks_path
                            .clone()
                            .unwrap_or_else(|| PathBuf::from("swe-bench.jsonl"));
                        let _ = std::fs::create_dir_all(&eval_dir);
                        (
                            Box::new(SweBenchAdapter::new(path, eval_dir).with_docker(docker)),
                            "swebench".to_string(),
                            10,
                        )
                    }
                    _ => (Box::new(NullBenchmark), "null".to_string(), 10),
                }
            };

        let max_cycles = cycles.unwrap_or(cycles_default);

        let evolve_config = EvolveConfig {
            max_cycles,
            batch_size: batch,
            evolver_model: self.config.model.clone(),
            ..Default::default()
        };

        let evolvable = ColletEvolvable::new(
            self.client.clone(),
            self.config.clone(),
            workspace_root.clone(),
            self.working_dir.clone(),
        );
        let trial = TrialRunner::new(Box::new(evolvable), benchmark);

        // Wrap EvolutionEvent sender → AgentEvent::Evolution
        let (evo_tx, mut evo_rx) = tpsc::unbounded_channel::<crate::evolution::EvolutionEvent>();
        let agent_tx_clone = event_tx.clone();
        tokio::spawn(async move {
            while let Some(evt) = evo_rx.recv().await {
                let _ = agent_tx_clone.send(AgentEvent::Evolution(evt));
            }
        });

        let client = self.client.clone();
        let cancel = CancellationToken::new();
        if let Some(ref old_cancel) = self.cancel_token {
            old_cancel.cancel();
        }
        self.cancel_token = Some(cancel.clone());
        self.state.agent_busy = true;
        self.agent_busy_since = Some(std::time::Instant::now());

        let start_msg = if let Some(ref text) = task_text {
            format!("🧬 Evolving: \"{text}\" ({max_cycles} cycle(s))...")
        } else {
            format!(
                "🧬 Starting evolution loop ({max_cycles} cycles, benchmark: {benchmark_label})..."
            )
        };
        self.state
            .messages
            .push(crate::tui::state::ChatMessage::text(
                crate::tui::state::MessageRole::System,
                start_msg,
            ));

        tokio::spawn(async move {
            let Ok(mut evo_loop) =
                EvolutionLoop::new(workspace_root, trial, evolve_config.clone(), evo_tx)
            else {
                return;
            };
            let mut engine = SkillforgeEngine::new(evolve_config, client);
            let _ = evo_loop.run(&mut engine, cancel).await;
        });
    }
}