magi-code 0.61.0

Repository-aware CLI coding agent for terminal work
Documentation
use super::profiles;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::path::PathBuf;

pub const MAX_SUBAGENT_TASKS: usize = 10;
pub const DEFAULT_SUBAGENT_CONCURRENCY: usize = 4;
pub const MAX_SUBAGENT_CONCURRENCY: usize = 4;
pub const DEFAULT_SUBAGENT_MAX_DEPTH: usize = 2;
pub const MAX_SUBAGENT_MAX_DEPTH: usize = 4;
/// Maximum accepted size for a task `intent` field (provider-bound text).
pub const MAX_SUBAGENT_TASK_INTENT_BYTES: usize = 32 * 1024;
/// Maximum accepted size for a task `context` field (provider-bound text).
pub const MAX_SUBAGENT_TASK_CONTEXT_BYTES: usize = 256 * 1024;

#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct SubagentTask {
    pub intent: String,
    pub agent: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub identity: Option<String>,
    pub context: Option<String>,
    pub cwd: Option<PathBuf>,
}

#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct SubagentsArgs {
    pub tasks: Vec<SubagentTask>,
    pub concurrency: Option<usize>,
}

impl SubagentsArgs {
    pub(crate) fn from_validated_parts(
        tasks: Vec<SubagentTask>,
        concurrency: Option<usize>,
    ) -> anyhow::Result<Self> {
        let args = Self { tasks, concurrency };
        args.validated_concurrency()?;
        Ok(args)
    }

    pub(crate) fn validated_concurrency(&self) -> anyhow::Result<usize> {
        if self.tasks.is_empty() {
            anyhow::bail!("subagents requires at least one task");
        }
        if self.tasks.len() > MAX_SUBAGENT_TASKS {
            anyhow::bail!("subagents supports at most {MAX_SUBAGENT_TASKS} tasks per batch");
        }
        for (index, task) in self.tasks.iter().enumerate() {
            if task.intent.trim().is_empty() {
                anyhow::bail!("task {index} intent must not be empty");
            }
            if task.intent.len() > MAX_SUBAGENT_TASK_INTENT_BYTES {
                anyhow::bail!("task {index} intent exceeds {MAX_SUBAGENT_TASK_INTENT_BYTES} bytes");
            }
            if let Some(context) = &task.context
                && context.len() > MAX_SUBAGENT_TASK_CONTEXT_BYTES
            {
                anyhow::bail!(
                    "task {index} context exceeds {MAX_SUBAGENT_TASK_CONTEXT_BYTES} bytes"
                );
            }
            if let Some(identity) = &task.identity {
                profiles::validate_subagent_identity_id(identity).map_err(|error| {
                    anyhow::anyhow!("task {index} identity is invalid: {error}")
                })?;
            }
        }
        let concurrency = self.concurrency.unwrap_or(DEFAULT_SUBAGENT_CONCURRENCY);
        if !(1..=MAX_SUBAGENT_CONCURRENCY).contains(&concurrency) {
            anyhow::bail!("concurrency must be between 1 and {MAX_SUBAGENT_CONCURRENCY}");
        }
        Ok(concurrency)
    }
}

#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub struct SubagentTaskResult {
    pub id: String,
    pub status: SubagentStatus,
    pub intent: String,
    pub agent: Option<String>,
    pub identity: Option<String>,
    pub cwd: PathBuf,
    pub session_id: Option<String>,
    pub session_path: Option<PathBuf>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub total_tokens: Option<u64>,
    pub changed_files: Vec<PathBuf>,
    pub output: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub structured_output: Option<Value>,
    pub output_truncated: bool,
    pub error: Option<String>,
}

#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum SubagentStatus {
    Completed,
    Failed,
}

#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub struct SubagentsSummary {
    pub total: usize,
    pub completed: usize,
    pub failed: usize,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub total_tokens: Option<u64>,
}

#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub struct SubagentsOutput {
    pub summary: SubagentsSummary,
    pub results: Vec<SubagentTaskResult>,
}