pub mod parser;
pub mod service;
pub mod visualizer;
use paladin_core::platform::container::paladin::Paladin;
use parser::FlowExpression;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum ErrorStrategy {
#[default]
FailFast,
ContinueParallel,
IgnoreErrors,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum OutputFormat {
#[default]
Concatenate,
JsonArray,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ManeuverConfig {
pub error_strategy: ErrorStrategy,
pub output_format: OutputFormat,
pub pass_output_as_input: bool,
pub timeout: Option<Duration>,
pub collect_timing_metrics: bool,
pub detailed_observability: bool,
}
impl Default for ManeuverConfig {
fn default() -> Self {
ManeuverConfig {
error_strategy: ErrorStrategy::FailFast,
output_format: OutputFormat::Concatenate,
pass_output_as_input: true,
timeout: Some(Duration::from_secs(300)), collect_timing_metrics: true,
detailed_observability: false,
}
}
}
impl ManeuverConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_error_strategy(mut self, strategy: ErrorStrategy) -> Self {
self.error_strategy = strategy;
self
}
pub fn with_output_format(mut self, format: OutputFormat) -> Self {
self.output_format = format;
self
}
pub fn with_pass_output_as_input(mut self, pass: bool) -> Self {
self.pass_output_as_input = pass;
self
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub fn without_timeout(mut self) -> Self {
self.timeout = None;
self
}
pub fn with_timing_metrics(mut self, enabled: bool) -> Self {
self.collect_timing_metrics = enabled;
self
}
pub fn with_detailed_observability(mut self, enabled: bool) -> Self {
self.detailed_observability = enabled;
self
}
pub fn validate(&self) -> Result<(), String> {
if matches!(self.timeout, Some(timeout) if timeout.as_secs() == 0) {
return Err("Timeout must be greater than zero".to_string());
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct Maneuver {
pub name: String,
pub agents: HashMap<String, Paladin>,
pub flow: FlowExpression,
pub config: ManeuverConfig,
}
impl Maneuver {
pub fn new(
name: impl Into<String>,
agents: HashMap<String, Paladin>,
flow: FlowExpression,
config: ManeuverConfig,
) -> Result<Self, ManeuverError> {
let name = name.into();
config.validate().map_err(ManeuverError::ValidationError)?;
let maneuver = Maneuver {
name,
agents,
flow,
config,
};
maneuver.validate()?;
Ok(maneuver)
}
pub fn validate(&self) -> Result<(), ManeuverError> {
let flow_agents = self.flow.agent_names();
for agent_name in &flow_agents {
if !self.agents.contains_key(agent_name) {
return Err(ManeuverError::AgentNotFound {
agent_name: agent_name.clone(),
available_agents: self.agents.keys().cloned().collect(),
});
}
}
let depth = self.flow.depth();
if depth > 5 {
return Err(ManeuverError::ValidationError(format!(
"Flow depth {} exceeds maximum of 5 levels",
depth
)));
}
let agent_count = self.flow.agent_count();
if agent_count > 30 {
return Err(ManeuverError::ValidationError(format!(
"Flow contains {} agents, exceeds maximum of 30",
agent_count
)));
}
Ok(())
}
pub fn depth(&self) -> usize {
self.flow.depth()
}
pub fn agent_count(&self) -> usize {
self.flow.agent_count()
}
pub fn width(&self) -> usize {
self.flow.width()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ManeuverResult {
pub final_output: String,
pub step_outputs: HashMap<String, String>,
pub execution_order: Vec<String>,
pub timing_metrics: Option<HashMap<String, Duration>>,
pub status: ExecutionStatus,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ExecutionStatus {
Success,
PartialSuccess,
Failed,
}
impl ManeuverResult {
pub fn new(
final_output: String,
step_outputs: HashMap<String, String>,
execution_order: Vec<String>,
) -> Self {
ManeuverResult {
final_output,
step_outputs,
execution_order,
timing_metrics: None,
status: ExecutionStatus::Success,
}
}
pub fn with_timing(
final_output: String,
step_outputs: HashMap<String, String>,
execution_order: Vec<String>,
timing_metrics: HashMap<String, Duration>,
) -> Self {
ManeuverResult {
final_output,
step_outputs,
execution_order,
timing_metrics: Some(timing_metrics),
status: ExecutionStatus::Success,
}
}
pub fn with_status(mut self, status: ExecutionStatus) -> Self {
self.status = status;
self
}
pub fn get_agent_output(&self, agent_name: &str) -> Option<&String> {
self.step_outputs.get(agent_name)
}
pub fn total_duration(&self) -> Option<Duration> {
self.timing_metrics
.as_ref()
.map(|metrics| metrics.values().sum())
}
}
#[derive(Debug, thiserror::Error)]
pub enum ManeuverError {
#[error("Parse error: {0}")]
ParseError(#[from] parser::FlowParseError),
#[error("Validation error: {0}")]
ValidationError(String),
#[error("Execution error: {0}")]
ExecutionError(String),
#[error("Agent '{agent_name}' not found. Available agents: {}", available_agents.join(", "))]
AgentNotFound {
agent_name: String,
available_agents: Vec<String>,
},
#[error("Timeout after {duration:?}")]
TimeoutError {
duration: Duration,
},
#[error("Paladin error: {0}")]
PaladinError(String),
}
impl From<String> for ManeuverError {
fn from(err: String) -> Self {
ManeuverError::PaladinError(err)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_strategy_default() {
assert_eq!(ErrorStrategy::default(), ErrorStrategy::FailFast);
}
#[test]
fn test_output_format_default() {
assert_eq!(OutputFormat::default(), OutputFormat::Concatenate);
}
#[test]
fn test_maneuver_config_default() {
let config = ManeuverConfig::default();
assert_eq!(config.error_strategy, ErrorStrategy::FailFast);
assert_eq!(config.output_format, OutputFormat::Concatenate);
assert!(config.pass_output_as_input);
assert!(config.timeout.is_some());
assert!(config.collect_timing_metrics);
assert!(!config.detailed_observability);
}
#[test]
fn test_maneuver_config_builder() {
let config = ManeuverConfig::new()
.with_error_strategy(ErrorStrategy::ContinueParallel)
.with_output_format(OutputFormat::JsonArray)
.with_pass_output_as_input(false)
.with_timeout(Duration::from_secs(60))
.with_timing_metrics(false)
.with_detailed_observability(true);
assert_eq!(config.error_strategy, ErrorStrategy::ContinueParallel);
assert_eq!(config.output_format, OutputFormat::JsonArray);
assert!(!config.pass_output_as_input);
assert_eq!(config.timeout, Some(Duration::from_secs(60)));
assert!(!config.collect_timing_metrics);
assert!(config.detailed_observability);
}
#[test]
fn test_maneuver_config_validation() {
let config = ManeuverConfig::default();
assert!(config.validate().is_ok());
let invalid_config = ManeuverConfig::default().with_timeout(Duration::from_secs(0));
assert!(invalid_config.validate().is_err());
}
#[test]
fn test_execution_status() {
let status = ExecutionStatus::Success;
assert_eq!(status, ExecutionStatus::Success);
}
#[test]
fn test_maneuver_result_new() {
let mut step_outputs = HashMap::new();
step_outputs.insert("agent1".to_string(), "output1".to_string());
let result = ManeuverResult::new(
"final output".to_string(),
step_outputs.clone(),
vec!["agent1".to_string()],
);
assert_eq!(result.final_output, "final output");
assert_eq!(result.status, ExecutionStatus::Success);
assert!(result.timing_metrics.is_none());
}
#[test]
fn test_maneuver_result_with_timing() {
let mut step_outputs = HashMap::new();
step_outputs.insert("agent1".to_string(), "output1".to_string());
let mut timing = HashMap::new();
timing.insert("agent1".to_string(), Duration::from_millis(100));
let result = ManeuverResult::with_timing(
"final output".to_string(),
step_outputs.clone(),
vec!["agent1".to_string()],
timing,
);
assert!(result.timing_metrics.is_some());
assert_eq!(result.total_duration(), Some(Duration::from_millis(100)));
}
#[test]
fn test_maneuver_result_get_agent_output() {
let mut step_outputs = HashMap::new();
step_outputs.insert("agent1".to_string(), "output1".to_string());
let result = ManeuverResult::new(
"final".to_string(),
step_outputs,
vec!["agent1".to_string()],
);
assert_eq!(
result.get_agent_output("agent1"),
Some(&"output1".to_string())
);
assert_eq!(result.get_agent_output("agent2"), None);
}
}