magi-code 0.105.1

Repository-aware CLI coding agent for terminal work
Documentation
use super::profiles;
pub(crate) use crate::output::{
    MAX_SUBAGENT_TASKS, SubagentStatus, SubagentTaskResult, SubagentsOutput, SubagentsSummary,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::path::PathBuf;

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, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum SubagentMode {
    Inspect,
    #[default]
    Standard,
}

#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct SubagentTask {
    pub intent: String,
    #[serde(default)]
    pub mode: SubagentMode,
    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)
    }

    /// Parse saved call arguments, ignoring harness-owned call metadata such as `intent`.
    pub(crate) fn from_recorded_call(mut arguments: Value) -> Option<Self> {
        arguments
            .as_object_mut()?
            .remove(crate::tool_contract::arg_key::INTENT);
        let args: Self = serde_json::from_value(arguments).ok()?;
        args.validated_concurrency().ok()?;
        Some(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"
                );
            }
            for text in [
                task.agent.as_deref(),
                task.cwd.as_ref().and_then(|p| p.to_str()),
            ]
            .into_iter()
            .flatten()
            {
                if text.len() > MAX_SUBAGENT_TASK_INTENT_BYTES {
                    anyhow::bail!("task {index} label or cwd exceeds size limit");
                }
            }
            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)
    }
}