collet 0.1.0

Relentless agentic coding orchestrator with zero-drop agent loops
Documentation
//! Subagent tool — spawn isolated agents from LLM tool calls.
//!
//! Enables the orchestrator agent to delegate tasks to subagents with:
//! - Isolated conversation contexts (no context pollution)
//! - Optional model override (inherits parent model when omitted)
//! - Parallel execution support

use crate::common::Result;
use serde::Deserialize;

#[derive(Debug, Deserialize)]
pub struct SubagentInput {
    /// The task prompt for the subagent.
    pub task: String,
    /// Agent name or operating mode.
    /// Pass a registered agent name (e.g. "security", "devops", "writer") to use that
    /// agent's full persona and system prompt. Or use "architect" for the base mode.
    #[serde(default = "default_mode")]
    pub mode: String,
    /// Optional model override. When set, overrides the current model for this subagent.
    #[serde(default)]
    pub model: Option<String>,
}

fn default_mode() -> String {
    "architect".to_string()
}

pub fn definition() -> serde_json::Value {
    serde_json::json!({
        "type": "function",
        "function": {
            "name": "subagent",
            "description": "Spawn a subagent with an isolated context to handle a specific subtask. The subagent gets its own conversation window, tools, and optionally a different model. Use this to parallelize work or delegate focused tasks without polluting the main context.",
            "parameters": {
                "type": "object",
                "properties": {
                    "task": {
                        "type": "string",
                        "description": "The task prompt for the subagent. Be specific and include all necessary context."
                    },
                    "mode": {
                        "type": "string",
                        "description": "Agent name or base mode. Use a registered agent name (e.g. 'security', 'devops', 'writer') to run with that agent's full persona. Or use 'architect' for full tool access. Default: 'architect'."
                    },
                    "model": {
                        "type": "string",
                        "description": "Optional model override. If omitted, the subagent inherits the current model. Only set this if a specific model is explicitly required."
                    }
                },
                "required": ["task"]
            }
        }
    })
}

/// Execute the subagent tool — spawns an isolated agent.
///
/// When `mcp_manager` is provided, the subagent reuses the parent's MCP
/// connections instead of spawning new server processes.
pub async fn execute(
    input: SubagentInput,
    client: crate::api::provider::OpenAiCompatibleProvider,
    config: std::sync::Arc<crate::config::Config>,
    system_prompt: String,
    working_dir: String,
    lsp_manager: crate::lsp::manager::LspManager,
    mcp_manager: Option<std::sync::Arc<crate::mcp::manager::McpManager>>,
) -> Result<String> {
    use crate::agent::subagent::{SubagentTask, spawn};

    // Resolve agent by name first; fall back to base mode string.
    let named_agent = config
        .agents
        .iter()
        .find(|a| a.name.eq_ignore_ascii_case(&input.mode));

    let task = SubagentTask {
        id: uuid::Uuid::new_v4().to_string(),
        prompt: input.task.clone(),
        agent_name: named_agent.map(|a| a.name.clone()),
        available_agents: Vec::new(), // tool-level dispatch is always explicit
        model_override: input.model.clone(),
        working_dir_override: None,
        outer_event_tx: None,
        cancel_token: None,
        iteration_budget: None,
        instruction_rx: None,
    };

    tracing::info!(
        task_id = %task.id,
        mode = %input.mode,
        model = ?input.model,
        prompt_len = input.task.len(),
        shared_mcp = mcp_manager.is_some(),
        "Spawning subagent",
    );

    // Deref the Arc to clone the inner Config for the isolated subagent.
    let result = spawn(
        task,
        client,
        (*config).clone(),
        system_prompt,
        working_dir,
        lsp_manager,
        mcp_manager,
    )
    .await;

    // Format result for the orchestrator
    let mut output = String::new();
    output.push_str(&format!(
        "## Subagent Result ({})\n\n",
        if result.success {
            "✅ success"
        } else {
            "❌ failed"
        }
    ));

    if !result.modified_files.is_empty() {
        output.push_str(&format!(
            "**Modified files:** {}\n\n",
            result.modified_files.join(", ")
        ));
    }

    output.push_str(&format!("**Tool calls:** {}\n\n", result.tool_calls));

    // Truncate very long responses
    if result.response.len() > 10_000 {
        output.push_str(crate::util::truncate_bytes(&result.response, 10_000));
        output.push_str(&format!(
            "\n\n... (truncated, {} total chars)",
            result.response.len()
        ));
    } else {
        output.push_str(&result.response);
    }

    Ok(output)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_subagent_definition() {
        let def = definition();
        assert_eq!(def["function"]["name"], "subagent");
        assert!(
            def["function"]["description"]
                .as_str()
                .unwrap()
                .contains("subagent")
        );
    }

    #[test]
    fn test_subagent_input_deserialization() {
        let json = r#"{"task": "Fix the bug in main.rs"}"#;
        let input: SubagentInput = serde_json::from_str(json).unwrap();
        assert_eq!(input.task, "Fix the bug in main.rs");
        assert_eq!(input.mode, "architect");
        assert!(input.model.is_none());
    }

    #[test]
    fn test_subagent_input_with_options() {
        let json =
            r#"{"task": "Explain this code", "mode": "architect", "model": "glm-4.7-flash"}"#;
        let input: SubagentInput = serde_json::from_str(json).unwrap();
        assert_eq!(input.mode, "architect");
        assert_eq!(input.model.unwrap(), "glm-4.7-flash");
    }
}