collet 0.1.1

Relentless agentic coding orchestrator with zero-drop agent loops
Documentation
//! ColletEvolvable — bridges Collet's agent loop to the evolution system.
//!
//! Implements the [`Evolvable`] trait by wrapping Collet's event-driven
//! `agent::loop::run()`.  The adapter:
//!   1. Reads the current workspace prompt and loads it into a fresh context
//!   2. Spawns the agent loop with the task as the user message
//!   3. Collects events until `AgentEvent::Done`
//!   4. Packages the result as a [`Trajectory`]
//!
//! `export_to_fs()` writes the current system prompt to `workspace/prompts/system.md`
//! so the evolution engine can analyze and mutate it.
//!
//! `reload_from_fs()` re-reads the (possibly mutated) prompt from disk and
//! updates the agent's system prompt for the next solve cycle.

use std::path::PathBuf;
use std::sync::Arc;

use anyhow::Result;
use async_trait::async_trait;
use tokio::sync::{Mutex, mpsc};
use tokio_util::sync::CancellationToken;

use crate::agent::context::ConversationContext;
use crate::agent::r#loop::AgentEvent;
use crate::api::provider::OpenAiCompatibleProvider;
use crate::config::Config;
use crate::evolution::trial::Evolvable;
use crate::evolution::types::{Task, Trajectory};
use crate::evolution::workspace::AgentWorkspace;

/// Collet agent adapter for the evolution system.
///
/// Each solve call runs a fresh agent loop against the current workspace prompt,
/// then packages the result into a `Trajectory`.
pub struct ColletEvolvable {
    client: OpenAiCompatibleProvider,
    config: Config,
    workspace: AgentWorkspace,
    working_dir: String,
    /// Current system prompt (updated by reload_from_fs).
    system_prompt: Arc<Mutex<String>>,
}

impl ColletEvolvable {
    /// Create a new evolvable agent adapter.
    ///
    /// - `workspace_root`: path to the evolution workspace (contains `prompts/system.md`)
    /// - `working_dir`: the repository/project directory for the agent to work in
    pub fn new(
        client: OpenAiCompatibleProvider,
        config: Config,
        workspace_root: PathBuf,
        working_dir: String,
    ) -> Self {
        let workspace = AgentWorkspace::new(&workspace_root);
        let initial_prompt = workspace.read_prompt().unwrap_or_default();
        Self {
            client,
            config,
            workspace,
            working_dir,
            system_prompt: Arc::new(Mutex::new(initial_prompt)),
        }
    }
}

#[async_trait]
impl Evolvable for ColletEvolvable {
    /// Solve a single task using Collet's agent loop.
    async fn solve(&self, task: &Task) -> Result<Trajectory> {
        let system_prompt = self.system_prompt.lock().await.clone();
        let context = ConversationContext::new(system_prompt);

        let (event_tx, mut event_rx) = mpsc::unbounded_channel::<AgentEvent>();
        let cancel = CancellationToken::new();
        let lsp = crate::lsp::manager::LspManager::new(self.working_dir.clone());

        let client = self.client.clone();
        let config = self.config.clone();
        let working_dir = self.working_dir.clone();
        let task_input = task.input.clone();

        tokio::spawn(async move {
            crate::agent::r#loop::run(crate::agent::r#loop::AgentParams {
                client,
                config,
                context,
                user_msg: task_input,
                working_dir,
                event_tx,
                cancel,
                lsp_manager: lsp,
                trust_level: crate::trust::TrustLevel::Full,
                approval_gate: crate::agent::approval::ApprovalGate::yolo(),
                images: Vec::new(),
            })
            .await;
        });

        // Collect events until Done
        let mut output = String::new();
        let mut steps: Vec<serde_json::Value> = Vec::new();

        while let Some(event) = event_rx.recv().await {
            match event {
                AgentEvent::Token(t) => output.push_str(&t),
                AgentEvent::Response(r) => output = r,
                AgentEvent::ToolCall {
                    name,
                    args,
                    call_id,
                } => {
                    steps.push(serde_json::json!({
                        "type": "tool_call",
                        "name": name,
                        "args": args,
                        "call_id": call_id,
                    }));
                }
                AgentEvent::ToolResult {
                    name,
                    result,
                    success,
                    call_id,
                } => {
                    steps.push(serde_json::json!({
                        "type": "tool_result",
                        "name": name,
                        "result": result,
                        "success": success,
                        "call_id": call_id,
                    }));
                }
                AgentEvent::Done { .. } | AgentEvent::GuardStop(_) | AgentEvent::Error(_) => break,
                _ => {}
            }
        }

        Ok(Trajectory {
            task_id: task.id.clone(),
            output,
            steps,
            conversation: Vec::new(),
        })
    }

    /// Export the current system prompt to the workspace filesystem.
    async fn export_to_fs(&self) -> Result<()> {
        let prompt = self.system_prompt.lock().await.clone();
        if !prompt.is_empty() {
            self.workspace.write_prompt(&prompt)?;
        }
        Ok(())
    }

    /// Reload agent state from the (possibly mutated) workspace filesystem.
    async fn reload_from_fs(&self) -> Result<()> {
        let new_prompt = self.workspace.read_prompt().unwrap_or_default();
        if !new_prompt.is_empty() {
            let mut prompt = self.system_prompt.lock().await;
            *prompt = new_prompt;
            tracing::info!("Agent reloaded system prompt from workspace");
        }
        Ok(())
    }
}