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;
pub const MAX_SUBAGENT_TASK_INTENT_BYTES: usize = 32 * 1024;
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>,
}