collet 0.1.1

Relentless agentic coding orchestrator with zero-drop agent loops
Documentation
//! Phase 1: analyze and split the user's request into parallel SwarmTasks.

use super::*;
use crate::api::models::{ChatRequest, Message};
use crate::api::provider::OpenAiCompatibleProvider;

impl SwarmCoordinator {
    /// Use the coordinator LLM to analyze and split the task.
    pub(super) async fn analyze_and_split(
        &self,
        user_msg: &str,
        system_prompt: &str,
    ) -> crate::common::Result<Vec<SwarmTask>> {
        let coord_client = self.hive_config.coordinator_client(&self.client);
        self.analyze_and_split_with_client(user_msg, system_prompt, &coord_client)
            .await
    }

    /// Retry `analyze_and_split` with a fallback model (worker model or default).
    ///
    /// If the coordinator model fails (rate limit, timeout, malformed response),
    /// this attempts the same split prompt with a different model.
    pub(super) async fn analyze_and_split_fallback(
        &self,
        user_msg: &str,
        system_prompt: &str,
    ) -> crate::common::Result<Vec<SwarmTask>> {
        // Use worker model/provider as fallback, or keep default
        let fallback_client = if self.hive_config.worker_model.is_some() {
            let c = self.hive_config.worker_client(&self.client);
            tracing::info!(
                "Retrying analyze_and_split with fallback model: {}",
                c.model
            );
            c
        } else {
            tracing::info!("Retrying analyze_and_split with default model");
            self.client.clone()
        };

        // Re-run the same split prompt
        self.analyze_and_split_with_client(user_msg, system_prompt, &fallback_client)
            .await
    }

    /// Inner analysis method that accepts an explicit client (for fallback retry).
    pub(super) async fn analyze_and_split_with_client(
        &self,
        user_msg: &str,
        _system_prompt: &str,
        coord_client: &OpenAiCompatibleProvider,
    ) -> crate::common::Result<Vec<SwarmTask>> {
        let split_prompt = format!(
            "You are a task coordinator. Analyze the following task and decide if it \
             should be split into parallel subtasks. Default to a single agent unless \
             splitting provides clear, measurable benefit.\n\n\
             Rules:\n\
             - DEFAULT to 1 subtask. Splitting is the exception, not the rule.\n\
             - Return EXACTLY 1 subtask if ANY of the following apply:\n\
               * The work touches ≤3 files\n\
               * The work is naturally sequential (A must finish before B starts)\n\
               * The subtasks would share or conflict on the same files\n\
               * The task is adding/modifying a single feature or module\n\
               * The task is a bug fix, rename, or simple refactor\n\
             - Only split when ALL of the following are true:\n\
               * There are 2+ clearly independent workstreams with NO file overlap\n\
               * Each subtask is large enough to justify its own agent (>10 tool calls expected)\n\
               * Parallel execution would meaningfully reduce total time\n\
             - Maximum {} subtasks\n\
             - Each subtask must be independently executable with zero file overlap\n\
             - Include dependency information if subtasks depend on each other\n\
             - The task language does not affect the split decision — evaluate structure, not keywords\n\
             Respond with a JSON array (1 element if no split needed):\n\
             ```json\n\
             [{{\n\
               \"id\": \"t1\",\n\
               \"prompt\": \"detailed instruction for this subtask\",\n\
               \"role\": \"worker\",\n\
               \"dependencies\": [],\n\
               \"target_files\": [\"src/foo.rs\"]\n\
             }}]\n\
             ```\n\n\
             Task: {user_msg}",
            self.hive_config.max_agents
        );

        let messages = vec![Message {
            role: "user".to_string(),
            content: Some(crate::api::Content::text(split_prompt)),
            reasoning_content: None,
            tool_calls: None,
            tool_call_id: None,
        }];

        let request = ChatRequest {
            model: coord_client.model.clone(),
            messages,
            max_tokens: 4096,
            stream: false,
            tools: None,
            tool_choice: None,
            temperature: None,
            thinking_budget_tokens: None,
            reasoning_effort: None,
        };

        let response = coord_client
            .chat(&request)
            .await
            .map_err(|e| crate::common::AgentError::Transport(e.to_string()))?;
        let text = response
            .choices
            .first()
            .and_then(|c| c.message.content.as_ref().map(|c| c.text_content()))
            .unwrap_or_default();

        let mut tasks = parse_task_json(&text)?;

        // Clear any agent_name the LLM may have produced — agent selection is disabled.
        for task in &mut tasks {
            task.agent_name = None;
        }

        enforce_file_disjoint(&mut tasks);

        Ok(tasks)
    }
}