use crate::modes::OperatingMode;
use serde::Deserialize;
use serde::Serialize;
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum IntelligenceLevel {
Light,
Medium,
Hard,
}
impl Default for IntelligenceLevel {
fn default() -> Self {
Self::Medium
}
}
impl IntelligenceLevel {
pub const fn compression_percentage(self) -> u8 {
match self {
Self::Light => 70,
Self::Medium => 85,
Self::Hard => 95,
}
}
pub const fn chunk_size(self) -> usize {
match self {
Self::Light => 256,
Self::Medium => 512,
Self::Hard => 1024,
}
}
pub const fn max_chunks(self) -> usize {
match self {
Self::Light => 1_000,
Self::Medium => 10_000,
Self::Hard => 100_000,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ToolPermission {
Allow,
Deny,
Restricted(HashMap<String, String>),
}
impl Default for ToolPermission {
fn default() -> Self {
Self::Allow
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ParameterDefinition {
pub name: String,
pub description: String,
#[serde(default)]
pub required: bool,
pub default: Option<String>,
pub valid_values: Option<Vec<String>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SubagentConfig {
pub name: String,
pub description: String,
pub mode_override: Option<OperatingMode>,
#[serde(default)]
pub intelligence: IntelligenceLevel,
#[serde(default)]
pub tools: HashMap<String, ToolPermission>,
pub prompt: String,
#[serde(default)]
pub parameters: Vec<ParameterDefinition>,
pub template: Option<String>,
#[serde(default = "default_timeout")]
pub timeout_seconds: u64,
#[serde(default = "default_true")]
pub chainable: bool,
#[serde(default = "default_true")]
pub parallelizable: bool,
#[serde(default)]
pub metadata: HashMap<String, serde_json::Value>,
#[serde(default)]
pub file_patterns: Vec<String>,
#[serde(default)]
pub tags: Vec<String>,
}
const fn default_timeout() -> u64 {
300 }
const fn default_true() -> bool {
true
}
impl SubagentConfig {
pub fn from_file(path: &PathBuf) -> Result<Self, super::SubagentError> {
let content = std::fs::read_to_string(path)?;
let config: Self = toml::from_str(&content)?;
config.validate()?;
Ok(config)
}
pub fn to_file(&self, path: &PathBuf) -> Result<(), super::SubagentError> {
let content = toml::to_string_pretty(self)
.map_err(|e| super::SubagentError::InvalidConfig(e.to_string()))?;
std::fs::write(path, content)?;
Ok(())
}
pub fn validate(&self) -> Result<(), super::SubagentError> {
if self.name.is_empty() {
return Err(super::SubagentError::InvalidConfig(
"agent name cannot be empty".to_string(),
));
}
if self.template.is_none() {
if self.description.is_empty() {
return Err(super::SubagentError::InvalidConfig(
"agent description cannot be empty (unless using a template)".to_string(),
));
}
if self.prompt.is_empty() {
return Err(super::SubagentError::InvalidConfig(
"agent prompt cannot be empty (unless using a template)".to_string(),
));
}
}
if self.timeout_seconds == 0 {
return Err(super::SubagentError::InvalidConfig(
"timeout must be greater than 0".to_string(),
));
}
let mut param_names = std::collections::HashSet::new();
for param in &self.parameters {
if !param_names.insert(¶m.name) {
return Err(super::SubagentError::InvalidConfig(format!(
"duplicate parameter name: {}",
param.name
)));
}
}
Ok(())
}
pub fn is_tool_allowed(&self, tool_name: &str) -> bool {
match self.tools.get(tool_name) {
Some(ToolPermission::Allow) => true,
Some(ToolPermission::Deny) => false,
Some(ToolPermission::Restricted(_)) => true, None => true, }
}
pub fn get_tool_restrictions(&self, tool_name: &str) -> Option<&HashMap<String, String>> {
match self.tools.get(tool_name) {
Some(ToolPermission::Restricted(restrictions)) => Some(restrictions),
_ => None,
}
}
pub fn effective_mode(&self, current_mode: OperatingMode) -> OperatingMode {
self.mode_override.unwrap_or(current_mode)
}
pub fn matches_file(&self, file_path: &std::path::Path) -> bool {
if self.file_patterns.is_empty() {
return true; }
let path_str = file_path.to_string_lossy();
self.file_patterns.iter().any(|pattern| {
if pattern.contains('*') {
let pattern = pattern.replace('*', ".*");
regex_lite::Regex::new(&pattern)
.map(|re| re.is_match(&path_str))
.unwrap_or(false)
} else {
path_str.contains(pattern)
}
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SubagentTemplate {
pub name: String,
pub description: String,
pub config: SubagentConfig,
#[serde(default)]
pub placeholders: Vec<String>,
}
impl SubagentTemplate {
pub fn from_file(path: &PathBuf) -> Result<Self, super::SubagentError> {
let content = std::fs::read_to_string(path)?;
let template: Self = toml::from_str(&content)?;
Ok(template)
}
pub fn instantiate(
&self,
name: String,
substitutions: HashMap<String, String>,
) -> Result<SubagentConfig, super::SubagentError> {
let mut config = self.config.clone();
config.name = name;
let mut prompt = config.prompt.clone();
for (placeholder, value) in substitutions {
let placeholder_pattern = format!("{{{{{}}}}}", placeholder);
prompt = prompt.replace(&placeholder_pattern, &value);
}
config.prompt = prompt;
config.validate()?;
Ok(config)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_intelligence_level_properties() {
assert_eq!(IntelligenceLevel::Light.compression_percentage(), 70);
assert_eq!(IntelligenceLevel::Medium.compression_percentage(), 85);
assert_eq!(IntelligenceLevel::Hard.compression_percentage(), 95);
assert_eq!(IntelligenceLevel::Light.chunk_size(), 256);
assert_eq!(IntelligenceLevel::Medium.chunk_size(), 512);
assert_eq!(IntelligenceLevel::Hard.chunk_size(), 1024);
}
#[test]
fn test_subagent_config_validation() {
let mut config = SubagentConfig {
name: "test-agent".to_string(),
description: "Test agent".to_string(),
mode_override: None,
intelligence: IntelligenceLevel::Medium,
tools: HashMap::new(),
prompt: "You are a test agent.".to_string(),
parameters: vec![],
template: None,
timeout_seconds: 300,
chainable: true,
parallelizable: true,
metadata: HashMap::new(),
file_patterns: vec![],
tags: vec![],
};
assert!(config.validate().is_ok());
config.name = String::new();
assert!(config.validate().is_err());
}
#[test]
fn test_tool_permissions() {
let mut tools = HashMap::new();
tools.insert("allowed_tool".to_string(), ToolPermission::Allow);
tools.insert("denied_tool".to_string(), ToolPermission::Deny);
tools.insert(
"restricted_tool".to_string(),
ToolPermission::Restricted(HashMap::from([(
"max_files".to_string(),
"10".to_string(),
)])),
);
let config = SubagentConfig {
name: "test-agent".to_string(),
description: "Test agent".to_string(),
mode_override: None,
intelligence: IntelligenceLevel::Medium,
tools,
prompt: "You are a test agent.".to_string(),
parameters: vec![],
template: None,
timeout_seconds: 300,
chainable: true,
parallelizable: true,
metadata: HashMap::new(),
file_patterns: vec![],
tags: vec![],
};
assert!(config.is_tool_allowed("allowed_tool"));
assert!(!config.is_tool_allowed("denied_tool"));
assert!(config.is_tool_allowed("restricted_tool"));
assert!(config.is_tool_allowed("unknown_tool"));
assert!(config.get_tool_restrictions("restricted_tool").is_some());
assert!(config.get_tool_restrictions("allowed_tool").is_none());
}
#[test]
fn test_config_serialization() {
let config = SubagentConfig {
name: "test-agent".to_string(),
description: "Test agent".to_string(),
mode_override: Some(OperatingMode::Review),
intelligence: IntelligenceLevel::Hard,
tools: HashMap::new(),
prompt: "You are a test agent.".to_string(),
parameters: vec![ParameterDefinition {
name: "target".to_string(),
description: "Target file".to_string(),
required: true,
default: None,
valid_values: None,
}],
template: None,
timeout_seconds: 600,
chainable: false,
parallelizable: true,
metadata: HashMap::new(),
file_patterns: vec!["*.rs".to_string()],
tags: vec!["rust".to_string(), "review".to_string()],
};
let toml_str = toml::to_string(&config).unwrap();
let deserialized: SubagentConfig = toml::from_str(&toml_str).unwrap();
assert_eq!(config, deserialized);
}
#[test]
fn test_file_pattern_matching() {
let config = SubagentConfig {
name: "rust-agent".to_string(),
description: "Rust-specific agent".to_string(),
mode_override: None,
intelligence: IntelligenceLevel::Medium,
tools: HashMap::new(),
prompt: "You are a Rust agent.".to_string(),
parameters: vec![],
template: None,
timeout_seconds: 300,
chainable: true,
parallelizable: true,
metadata: HashMap::new(),
file_patterns: vec!["*.rs".to_string(), "Cargo.toml".to_string()],
tags: vec![],
};
assert!(config.matches_file(&PathBuf::from("src/main.rs")));
assert!(config.matches_file(&PathBuf::from("Cargo.toml")));
assert!(!config.matches_file(&PathBuf::from("src/main.py")));
}
}