collet 0.1.1

Relentless agentic coding orchestrator with zero-drop agent loops
Documentation
mod arch;
mod core;
mod iter;
mod perf;
mod worker;

pub use arch::{execute_plan, run_architect_code_loop};
pub use core::run_loop_with_mcp;

use crate::agent::approval::ApprovalGate;
use crate::agent::context::ConversationContext;
use crate::api::provider::OpenAiCompatibleProvider;
use crate::config::Config;
use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;

// Re-export the main API entry points
pub use core::run_loop;

/// Events sent from the agent loop to the TUI.
pub enum AgentEvent {
    /// Streaming text token.
    Token(String),
    /// Agent produced a complete text response.
    Response(String),
    /// Tool is being called.
    ToolCall {
        name: String,
        args: String,
        call_id: Option<String>,
    },
    /// Tool returned a result.
    ToolResult {
        name: String,
        result: String,
        success: bool,
        call_id: Option<String>,
    },
    /// File was modified by a tool (triggers repo map invalidation).
    FileModified { path: String },
    /// LSP server was successfully installed.
    LspInstalled { language: String, server: String },
    /// Agent loop finished — returns the conversation context for persistence.
    /// `stop_reason` is `None` on normal completion, `Some` when stopped by guard/cancel.
    Done {
        context: ConversationContext,
        stop_reason: Option<crate::agent::guard::StopReason>,
    },
    /// Error occurred.
    Error(String),
    /// Guard stopped the agent.
    GuardStop(String),
    /// Stream retry attempt due to idle timeout or rate limit.
    StreamRetry {
        attempt: u32,
        max: u32,
        message: String,
    },
    /// Status update (iteration, elapsed, cumulative token usage for this turn).
    Status {
        iteration: u32,
        elapsed_secs: u64,
        prompt_tokens: u32,
        completion_tokens: u32,
        /// Cached prompt tokens returned by the API for this turn (0 if not supported).
        cached_tokens: u32,
        /// Current context window usage in tokens (updated after each API turn).
        context_tokens: usize,
    },
    /// Image processing notice (how attached images were handled).
    /// `install_hint` is set when tesseract is not installed and could help.
    ImageNotice {
        notice: String,
        install_hint: Option<String>,
    },
    /// Phase transition in a multi-phase pipeline (e.g., architect → code).
    PhaseChange { label: String },
    /// Architect phase complete — plan ready for user review before code phase.
    PlanReady {
        plan: String,
        context: ConversationContext,
        user_msg: String,
    },

    // ── Swarm mode events ──────────────────────────────────────────────────
    /// Swarm mode: an agent started working on a subtask.
    SwarmAgentStarted {
        agent_id: String,
        agent_name: String,
        task_preview: String,
    },
    /// Swarm mode: an agent produced a progress update.
    SwarmAgentProgress {
        agent_id: String,
        agent_name: String,
        iteration: u32,
        status: String,
    },
    /// Swarm mode: per-agent tool call forwarded from sub-agent loop.
    SwarmAgentToolCall {
        agent_id: String,
        name: String,
        args: String,
    },
    /// Swarm mode: per-agent tool result forwarded from sub-agent loop.
    SwarmAgentToolResult {
        agent_id: String,
        name: String,
        result: String,
        success: bool,
    },
    /// Swarm mode: per-agent streaming token forwarded from sub-agent loop.
    SwarmAgentToken { agent_id: String, text: String },
    /// Swarm mode: per-agent response forwarded from sub-agent loop.
    SwarmAgentResponse { agent_id: String, text: String },
    /// Swarm mode: an agent finished.
    SwarmAgentDone {
        agent_id: String,
        agent_name: String,
        success: bool,
        modified_files: Vec<String>,
        tool_calls: u32,
        /// Cumulative input (prompt) tokens consumed by this agent.
        input_tokens: u64,
        /// Cumulative output (completion) tokens produced by this agent.
        output_tokens: u64,
        /// Full agent response text (for popup display).
        response: String,
    },
    /// Swarm mode: active execution tier changed (e.g. flock config using fork strategy).
    SwarmModeSwitch { label: String },
    /// Swarm mode: coordinator decided to handle the task as a single agent.
    /// Clears the swarm UI and shows the delegated agent name instead.
    SwarmResolvedToSingle { agent_label: String },
    /// Swarm mode: conflicts detected between agents.
    SwarmConflict {
        conflicts: Vec<(String, Vec<String>)>,
    },
    /// Swarm worker is approaching its iteration limit (non-blocking TUI warning).
    SwarmWorkerApproaching {
        agent_id: String,
        task_preview: String,
        remaining: u32,
    },
    /// Performance metrics update (sent after each iteration when debug mode is active).
    PerformanceUpdate {
        tool_latency_avg_ms: f64,
        tool_latency_max_ms: u64,
        api_latency_avg_ms: f64,
        api_latency_max_ms: u64,
        tool_success_count: u32,
        tool_failure_count: u32,
        total_iterations: u32,
        total_tokens_used: u64,
        total_tool_calls_made: u32,
        top_tools: Vec<(String, u32)>,
    },

    /// Tool approval requested — frontend should respond via the approval gate.
    ApprovalRequired {
        tool_name: String,
        /// Full argument JSON for approval UIs that display args inline.
        /// The TUI approval popup also reads args from a separate channel.
        tool_args: String,
    },
    /// Tool was denied by the approval gate.
    ApprovalDenied { tool_name: String },

    /// MCP server child process PIDs (sent once after connection).
    McpPids { pids: Vec<u32> },

    /// Shell passthrough result (`!cmd`).
    ShellOutput {
        cmd: String,
        stdout: String,
        stderr: String,
        exit_code: i32,
    },

    /// Soul reflection is in progress (sent just before the LLM reflection call).
    SoulReflecting { agent_name: String },

    /// Swarm mode: entire hive execution finished.
    SwarmDone {
        context: ConversationContext,
        merged_response: String,
        agent_count: usize,
        total_tool_calls: u32,
        conflicts_resolved: usize,
    },

    /// Swarm mode: all workers have been dispatched (Hive/Flock only).
    /// The coordinator continues collecting results in the background.
    /// Receivers should clear `agent_busy` so the user can interact immediately.
    SwarmWorkersDispatched,

    /// Swarm mode: a worker has been paused via instruction.
    SwarmWorkerPaused { agent_id: String },
    /// Swarm mode: a paused worker has been resumed.
    SwarmWorkerResumed { agent_id: String },

    /// Evolution loop progress event (forwarded from `evolution::EvolutionEvent`).
    Evolution(crate::evolution::EvolutionEvent),

    /// Tool batch execution in progress — sent when parallel tools start executing.
    ToolBatchProgress {
        /// Number of tools currently running.
        running: usize,
        /// Total tools in this batch.
        total: usize,
    },

    /// Stream is alive but idle — sent every 2 seconds during idle timeout wait.
    StreamWaiting {
        /// Seconds elapsed since the stream went idle.
        elapsed_secs: u64,
    },

    /// Context compaction started (reserved for future use when compaction is
    /// triggered outside the tool-result path).
    #[allow(dead_code)]
    CompactionStarted {
        /// Token count before compaction.
        before_tokens: usize,
        /// Target token count after compaction.
        target_tokens: usize,
    },

    /// Context compaction finished.
    CompactionDone {
        /// Token count before compaction.
        before_tokens: usize,
        /// Token count after compaction.
        after_tokens: usize,
    },

    /// Tool result was truncated to fit context limits.
    ToolResultTruncated {
        /// Name of the tool whose output was truncated.
        tool_name: String,
        /// Original output size in bytes.
        original_bytes: usize,
        /// Size after truncation in bytes.
        truncated_bytes: usize,
    },
}

/// Parameters for the core agent run loop.
pub struct AgentParams {
    pub client: OpenAiCompatibleProvider,
    pub config: Config,
    pub context: ConversationContext,
    pub user_msg: String,
    pub working_dir: String,
    pub event_tx: mpsc::UnboundedSender<AgentEvent>,
    pub cancel: CancellationToken,
    pub lsp_manager: crate::lsp::manager::LspManager,
    pub trust_level: crate::trust::TrustLevel,
    pub approval_gate: ApprovalGate,
    pub images: Vec<crate::api::ImageData>,
}

/// Optional swarm-specific parameters for shared MCP execution.
pub struct SwarmParams {
    pub mcp_manager: std::sync::Arc<crate::mcp::manager::McpManager>,
    pub shared_knowledge: Option<crate::agent::swarm::knowledge::SharedKnowledge>,
    pub shared_tool_index: Option<std::sync::Arc<crate::tools::tool_index::ToolIndex>>,
    pub shared_skill_registry: Option<std::sync::Arc<crate::skills::SkillRegistry>>,
    pub instruction_rx: Option<
        tokio::sync::mpsc::UnboundedReceiver<crate::agent::swarm::knowledge::WorkerInstruction>,
    >,
}

/// Parameters for the architect→code execute_plan phase.
pub struct ExecutePlanParams {
    pub client: OpenAiCompatibleProvider,
    pub config: Config,
    pub system_prompt: String,
    pub plan: String,
    pub user_msg: String,
    pub working_dir: String,
    pub event_tx: mpsc::UnboundedSender<AgentEvent>,
    pub cancel: CancellationToken,
    pub lsp_manager: crate::lsp::manager::LspManager,
    pub arch_context: Option<ConversationContext>,
    pub approval_gate: ApprovalGate,
}

/// Run the agent loop. Accepts a cancellation token for Ctrl+C abort.
pub async fn run(params: AgentParams) {
    run_loop(params).await;
}

/// Run the agent loop with a specific trust level and approval gate.
pub async fn run_with_mode(params: AgentParams) {
    run_loop(params).await;
}

/// Run the agent loop reusing a shared MCP manager from the parent agent.
///
/// This avoids re-spawning MCP server processes, saving ~30-45s per subagent.
/// The shared `McpManager` is thread-safe (`Arc<Mutex<McpClient>>` per server).
///
/// Optionally accepts pre-built `ToolIndex` and `SkillRegistry` to skip
/// redundant initialization when spawning multiple swarm workers.
pub async fn run_with_shared_mcp(params: AgentParams, swarm: SwarmParams) {
    run_loop_with_mcp(params, Some(swarm)).await;
}