use crate::common::Result;
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct SubagentInput {
pub task: String,
#[serde(default = "default_mode")]
pub mode: String,
#[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"]
}
}
})
}
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};
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(), 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",
);
let result = spawn(
task,
client,
(*config).clone(),
system_prompt,
working_dir,
lsp_manager,
mcp_manager,
)
.await;
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));
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");
}
}