use serde::Deserialize;
use crate::ast::completion::CompletionConfig;
use crate::error::NikaError;
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ToolChoice {
#[default]
Auto,
Required,
None,
}
impl ToolChoice {
pub fn as_str(&self) -> &'static str {
match self {
Self::Auto => "auto",
Self::Required => "required",
Self::None => "none",
}
}
}
const DEFAULT_MAX_TURNS: u32 = 10;
const MAX_ALLOWED_TURNS: u32 = 100;
const DEFAULT_THINKING_BUDGET: u64 = 4096;
const DEFAULT_DEPTH_LIMIT: u32 = 3;
const MAX_DEPTH_LIMIT: u32 = 10;
#[derive(Debug, Clone, Default, Deserialize)]
pub struct AgentParams {
pub prompt: String,
#[serde(default)]
pub system: Option<String>,
#[serde(default)]
pub provider: Option<String>,
#[serde(default)]
pub model: Option<String>,
#[serde(default)]
pub mcp: Vec<String>,
#[serde(default)]
pub tools: Vec<String>,
#[serde(default)]
pub max_turns: Option<u32>,
#[serde(default)]
pub token_budget: Option<u32>,
#[serde(default)]
pub stop_sequences: Vec<String>,
#[serde(default)]
pub scope: Option<String>,
#[serde(default)]
pub extended_thinking: Option<bool>,
#[serde(default)]
pub thinking_budget: Option<u64>,
#[serde(default)]
pub depth_limit: Option<u32>,
#[serde(default)]
pub tool_choice: Option<ToolChoice>,
#[serde(default)]
pub temperature: Option<f32>,
#[serde(default)]
pub max_tokens: Option<u32>,
#[serde(default)]
pub skills: Option<Vec<String>>,
#[serde(default)]
pub completion: Option<CompletionConfig>,
#[serde(default)]
pub guardrails: Vec<crate::ast::guardrails::GuardrailConfig>,
#[serde(default)]
pub limits: Option<crate::ast::limits::LimitsConfig>,
}
impl AgentParams {
#[inline]
pub fn effective_max_turns(&self) -> u32 {
self.max_turns.unwrap_or(DEFAULT_MAX_TURNS)
}
#[inline]
pub fn effective_token_budget(&self) -> u32 {
self.token_budget.unwrap_or(u32::MAX)
}
#[inline]
pub fn effective_thinking_budget(&self) -> u64 {
self.thinking_budget.unwrap_or(DEFAULT_THINKING_BUDGET)
}
#[inline]
pub fn effective_depth_limit(&self) -> u32 {
self.depth_limit.unwrap_or(DEFAULT_DEPTH_LIMIT)
}
#[inline]
pub fn effective_max_tokens(&self) -> Option<u32> {
if let Some(max_tokens) = self.max_tokens {
Some(max_tokens)
} else if self.extended_thinking.unwrap_or(false) {
let thinking_budget = self.effective_thinking_budget() as u32;
Some(thinking_budget + 8192)
} else {
None
}
}
#[inline]
pub fn effective_tool_choice(&self) -> ToolChoice {
self.tool_choice.clone().unwrap_or_default()
}
#[inline]
pub fn has_explicit_tool_choice(&self) -> bool {
self.tool_choice.is_some()
}
#[inline]
pub fn effective_temperature(&self) -> Option<f32> {
self.temperature
}
pub fn effective_completion(&self) -> Option<CompletionConfig> {
self.completion.clone()
}
pub fn completion_system_instruction(&self) -> String {
self.completion
.as_ref()
.map(|c| c.generate_system_instruction())
.unwrap_or_default()
}
pub fn validate(&self) -> Result<(), NikaError> {
if self.prompt.is_empty() {
return Err(NikaError::ValidationError {
reason: "Agent prompt cannot be empty".into(),
});
}
if let Some(max) = self.max_turns {
if max == 0 {
return Err(NikaError::ValidationError {
reason: "max_turns must be > 0".into(),
});
}
if max > MAX_ALLOWED_TURNS {
return Err(NikaError::ValidationError {
reason: format!("max_turns cannot exceed {}", MAX_ALLOWED_TURNS),
});
}
}
if let Some(budget) = self.token_budget {
if budget == 0 {
return Err(NikaError::ValidationError {
reason: "token_budget must be > 0".into(),
});
}
}
if self.extended_thinking == Some(true) {
if let Some(ref provider) = self.provider {
if provider != "claude" {
return Err(NikaError::ValidationError {
reason: format!(
"extended_thinking only supported for claude provider, got '{}'",
provider
),
});
}
}
}
if let Some(depth) = self.depth_limit {
if depth == 0 {
return Err(NikaError::ValidationError {
reason: "depth_limit must be > 0".into(),
});
}
if depth > MAX_DEPTH_LIMIT {
return Err(NikaError::ValidationError {
reason: format!("depth_limit cannot exceed {}", MAX_DEPTH_LIMIT),
});
}
}
if let Some(temp) = self.temperature {
if !(0.0..=2.0).contains(&temp) {
return Err(NikaError::ValidationError {
reason: format!("temperature must be between 0.0 and 2.0, got {}", temp),
});
}
}
if let Some(ref completion) = self.completion {
completion.validate()?;
}
if let Some(ref limits) = self.limits {
limits.validate()?;
}
Ok(())
}
pub fn effective_limits(&self) -> crate::ast::limits::LimitsConfig {
self.limits.clone().unwrap_or_default()
}
pub fn has_limits(&self) -> bool {
self.limits
.as_ref()
.map(|l| l.has_limits())
.unwrap_or(false)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::serde_yaml;
#[test]
fn parse_agent_params_basic() {
let yaml = r#"
prompt: "Test prompt"
provider: claude
model: claude-sonnet-4-6
"#;
let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
assert_eq!(params.prompt, "Test prompt");
assert_eq!(params.provider, Some("claude".to_string()));
assert_eq!(params.model, Some("claude-sonnet-4-6".to_string()));
}
#[test]
fn parse_agent_params_mcp_list() {
let yaml = r#"
prompt: "Test"
mcp:
- novanet
- filesystem
"#;
let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
assert_eq!(params.mcp, vec!["novanet", "filesystem"]);
}
#[test]
fn effective_max_turns_default() {
let params = AgentParams::default();
assert_eq!(params.effective_max_turns(), DEFAULT_MAX_TURNS);
}
#[test]
fn effective_max_turns_custom() {
let params = AgentParams {
max_turns: Some(20),
..Default::default()
};
assert_eq!(params.effective_max_turns(), 20);
}
#[test]
fn validate_empty_prompt() {
let params = AgentParams::default();
assert!(params.validate().is_err());
}
#[test]
fn validate_zero_max_turns() {
let params = AgentParams {
prompt: "test".to_string(),
max_turns: Some(0),
..Default::default()
};
assert!(params.validate().is_err());
}
#[test]
fn validate_excessive_max_turns() {
let params = AgentParams {
prompt: "test".to_string(),
max_turns: Some(101),
..Default::default()
};
assert!(params.validate().is_err());
}
#[test]
fn validate_ok() {
let params = AgentParams {
prompt: "test".to_string(),
max_turns: Some(50),
..Default::default()
};
assert!(params.validate().is_ok());
}
#[test]
fn parse_token_budget() {
let yaml = r#"
prompt: "Test"
token_budget: 100000
"#;
let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
assert_eq!(params.token_budget, Some(100000));
}
#[test]
fn effective_token_budget_default() {
let params = AgentParams {
prompt: "test".to_string(),
..Default::default()
};
assert_eq!(params.effective_token_budget(), u32::MAX);
}
#[test]
fn effective_token_budget_custom() {
let params = AgentParams {
prompt: "test".to_string(),
token_budget: Some(50000),
..Default::default()
};
assert_eq!(params.effective_token_budget(), 50000);
}
#[test]
fn validate_zero_token_budget() {
let params = AgentParams {
prompt: "test".to_string(),
token_budget: Some(0),
..Default::default()
};
assert!(params.validate().is_err());
}
#[test]
fn parse_system_prompt() {
let yaml = r#"
prompt: "User prompt"
system: "You are a helpful assistant."
"#;
let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
assert_eq!(
params.system,
Some("You are a helpful assistant.".to_string())
);
}
#[test]
fn system_prompt_defaults_to_none() {
let params = AgentParams::default();
assert!(params.system.is_none());
}
#[test]
fn parse_extended_thinking_true() {
let yaml = r#"
prompt: "Test"
extended_thinking: true
"#;
let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
assert_eq!(params.extended_thinking, Some(true));
}
#[test]
fn parse_extended_thinking_false() {
let yaml = r#"
prompt: "Test"
extended_thinking: false
"#;
let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
assert_eq!(params.extended_thinking, Some(false));
}
#[test]
fn extended_thinking_defaults_to_none() {
let params = AgentParams::default();
assert!(params.extended_thinking.is_none());
}
#[test]
fn validate_extended_thinking_with_openai_fails() {
let params = AgentParams {
prompt: "test".to_string(),
extended_thinking: Some(true),
provider: Some("openai".to_string()),
..Default::default()
};
let err = params.validate().unwrap_err();
assert!(err
.to_string()
.contains("extended_thinking only supported for claude"));
}
#[test]
fn validate_extended_thinking_with_claude_ok() {
let params = AgentParams {
prompt: "test".to_string(),
extended_thinking: Some(true),
provider: Some("claude".to_string()),
..Default::default()
};
assert!(params.validate().is_ok());
}
#[test]
fn validate_extended_thinking_without_provider_ok() {
let params = AgentParams {
prompt: "test".to_string(),
extended_thinking: Some(true),
provider: None,
..Default::default()
};
assert!(params.validate().is_ok());
}
#[test]
fn parse_thinking_budget() {
let yaml = r#"
prompt: "Test"
extended_thinking: true
thinking_budget: 8192
"#;
let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
assert_eq!(params.thinking_budget, Some(8192));
}
#[test]
fn effective_thinking_budget_default() {
let params = AgentParams {
prompt: "test".to_string(),
..Default::default()
};
assert_eq!(params.effective_thinking_budget(), DEFAULT_THINKING_BUDGET);
assert_eq!(params.effective_thinking_budget(), 4096);
}
#[test]
fn effective_thinking_budget_custom() {
let params = AgentParams {
prompt: "test".to_string(),
thinking_budget: Some(16384),
..Default::default()
};
assert_eq!(params.effective_thinking_budget(), 16384);
}
#[test]
fn thinking_budget_defaults_to_none() {
let params = AgentParams::default();
assert!(params.thinking_budget.is_none());
}
#[test]
fn effective_max_tokens_explicit() {
let params = AgentParams {
prompt: "test".to_string(),
max_tokens: Some(16384),
..Default::default()
};
assert_eq!(params.effective_max_tokens(), Some(16384));
}
#[test]
fn effective_max_tokens_with_extended_thinking() {
let params = AgentParams {
prompt: "test".to_string(),
extended_thinking: Some(true),
thinking_budget: Some(8192),
max_tokens: None, ..Default::default()
};
assert_eq!(params.effective_max_tokens(), Some(8192 + 8192));
}
#[test]
fn effective_max_tokens_explicit_overrides_auto() {
let params = AgentParams {
prompt: "test".to_string(),
extended_thinking: Some(true),
thinking_budget: Some(8192),
max_tokens: Some(32768), ..Default::default()
};
assert_eq!(params.effective_max_tokens(), Some(32768));
}
#[test]
fn effective_max_tokens_none_without_thinking() {
let params = AgentParams {
prompt: "test".to_string(),
extended_thinking: None,
max_tokens: None,
..Default::default()
};
assert_eq!(params.effective_max_tokens(), None);
}
#[test]
fn test_parse_tool_choice_auto() {
let yaml = r#"
prompt: "Test"
tool_choice: auto
"#;
let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
assert_eq!(params.tool_choice, Some(ToolChoice::Auto));
}
#[test]
fn test_parse_tool_choice_required() {
let yaml = r#"
prompt: "Test"
tool_choice: required
"#;
let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
assert_eq!(params.tool_choice, Some(ToolChoice::Required));
}
#[test]
fn test_parse_tool_choice_none() {
let yaml = r#"
prompt: "Test"
tool_choice: none
"#;
let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
assert_eq!(params.tool_choice, Some(ToolChoice::None));
}
#[test]
fn test_tool_choice_default() {
let params = AgentParams::default();
assert!(params.tool_choice.is_none());
assert_eq!(params.effective_tool_choice(), ToolChoice::Auto);
}
#[test]
fn test_tool_choice_as_str() {
assert_eq!(ToolChoice::Auto.as_str(), "auto");
assert_eq!(ToolChoice::Required.as_str(), "required");
assert_eq!(ToolChoice::None.as_str(), "none");
}
#[test]
fn test_parse_temperature() {
let yaml = r#"
prompt: "Test"
temperature: 0.7
"#;
let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
assert_eq!(params.temperature, Some(0.7));
}
#[test]
fn test_temperature_default() {
let params = AgentParams::default();
assert!(params.temperature.is_none());
assert_eq!(params.effective_temperature(), None);
}
#[test]
fn test_temperature_validation_valid_range() {
for temp in [0.0, 0.5, 1.0, 1.5, 2.0] {
let params = AgentParams {
prompt: "test".to_string(),
temperature: Some(temp),
..Default::default()
};
assert!(
params.validate().is_ok(),
"temperature {} should be valid",
temp
);
}
}
#[test]
fn test_temperature_validation_too_low() {
let params = AgentParams {
prompt: "test".to_string(),
temperature: Some(-0.1),
..Default::default()
};
let err = params.validate().unwrap_err();
assert!(err
.to_string()
.contains("temperature must be between 0.0 and 2.0"));
}
#[test]
fn test_temperature_validation_too_high() {
let params = AgentParams {
prompt: "test".to_string(),
temperature: Some(2.1),
..Default::default()
};
let err = params.validate().unwrap_err();
assert!(err
.to_string()
.contains("temperature must be between 0.0 and 2.0"));
}
#[test]
fn test_effective_temperature_custom() {
let params = AgentParams {
prompt: "test".to_string(),
temperature: Some(0.3),
..Default::default()
};
assert_eq!(params.effective_temperature(), Some(0.3));
}
#[test]
fn test_combined_tool_choice_and_temperature() {
let yaml = r#"
prompt: "Generate creative content"
tool_choice: required
temperature: 1.5
"#;
let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
assert_eq!(params.tool_choice, Some(ToolChoice::Required));
assert_eq!(params.temperature, Some(1.5));
assert!(params.validate().is_ok());
}
#[test]
fn test_has_explicit_tool_choice_when_not_set() {
let params = AgentParams {
prompt: "test".to_string(),
..Default::default()
};
assert!(!params.has_explicit_tool_choice());
assert_eq!(params.effective_tool_choice(), ToolChoice::Auto);
}
#[test]
fn test_has_explicit_tool_choice_when_auto() {
let params = AgentParams {
prompt: "test".to_string(),
tool_choice: Some(ToolChoice::Auto),
..Default::default()
};
assert!(params.has_explicit_tool_choice());
assert_eq!(params.effective_tool_choice(), ToolChoice::Auto);
}
#[test]
fn test_has_explicit_tool_choice_when_required() {
let params = AgentParams {
prompt: "test".to_string(),
tool_choice: Some(ToolChoice::Required),
..Default::default()
};
assert!(params.has_explicit_tool_choice());
assert_eq!(params.effective_tool_choice(), ToolChoice::Required);
}
#[test]
fn test_has_explicit_tool_choice_when_none() {
let params = AgentParams {
prompt: "test".to_string(),
tool_choice: Some(ToolChoice::None),
..Default::default()
};
assert!(params.has_explicit_tool_choice());
assert_eq!(params.effective_tool_choice(), ToolChoice::None);
}
#[test]
fn test_has_explicit_tool_choice_from_yaml_absent() {
let yaml = r#"
prompt: "Test prompt"
"#;
let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
assert!(!params.has_explicit_tool_choice());
}
#[test]
fn test_has_explicit_tool_choice_from_yaml_present() {
let yaml = r#"
prompt: "Test prompt"
tool_choice: none
"#;
let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
assert!(params.has_explicit_tool_choice());
assert_eq!(params.effective_tool_choice(), ToolChoice::None);
}
#[test]
fn test_parse_completion_explicit_mode() {
let yaml = r#"
prompt: "Test"
completion:
mode: explicit
signal:
fields:
required: [result]
optional: [confidence]
"#;
let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
assert!(params.completion.is_some());
let completion = params.completion.clone().unwrap();
assert_eq!(completion.mode, crate::ast::CompletionMode::Explicit);
assert!(completion.signal.is_some());
}
#[test]
fn test_parse_completion_natural_mode() {
let yaml = r#"
prompt: "Test"
completion:
mode: natural
"#;
let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
let completion = params.completion.clone().unwrap();
assert_eq!(completion.mode, crate::ast::CompletionMode::Natural);
}
#[test]
fn test_parse_completion_pattern_mode() {
let yaml = r#"
prompt: "Test"
completion:
mode: pattern
patterns:
- value: "DONE"
type: contains
- value: "^COMPLETE:"
type: regex
"#;
let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
let completion = params.completion.clone().unwrap();
assert_eq!(completion.mode, crate::ast::CompletionMode::Pattern);
assert_eq!(completion.patterns.len(), 2);
}
#[test]
fn test_effective_completion_uses_completion_field() {
let yaml = r#"
prompt: "Test"
completion:
mode: explicit
"#;
let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
let effective = params.effective_completion().unwrap();
assert_eq!(effective.mode, crate::ast::CompletionMode::Explicit);
}
#[test]
fn test_effective_completion_returns_none_when_empty() {
let params = AgentParams {
prompt: "test".to_string(),
..Default::default()
};
assert!(params.effective_completion().is_none());
}
#[test]
fn test_completion_system_instruction_explicit_mode() {
let yaml = r#"
prompt: "Test"
completion:
mode: explicit
signal:
fields:
required: [result, confidence]
"#;
let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
let instruction = params.completion_system_instruction();
assert!(instruction.contains("nika:complete"));
assert!(instruction.contains("result"));
assert!(instruction.contains("confidence"));
}
#[test]
fn test_completion_system_instruction_natural_mode() {
let yaml = r#"
prompt: "Test"
completion:
mode: natural
"#;
let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
let instruction = params.completion_system_instruction();
assert!(instruction.is_empty());
}
#[test]
fn test_completion_system_instruction_pattern_mode() {
let yaml = r#"
prompt: "Test"
completion:
mode: pattern
patterns:
- value: "TASK_DONE"
type: exact
"#;
let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
let instruction = params.completion_system_instruction();
assert!(instruction.contains("TASK_DONE"));
}
#[test]
fn test_completion_system_instruction_empty_when_none() {
let params = AgentParams {
prompt: "test".to_string(),
..Default::default()
};
let instruction = params.completion_system_instruction();
assert!(instruction.is_empty());
}
#[test]
fn test_validate_completion_config_valid() {
let yaml = r#"
prompt: "Test"
completion:
mode: pattern
patterns:
- value: "^DONE$"
type: regex
"#;
let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
assert!(params.validate().is_ok());
}
#[test]
fn test_validate_completion_config_invalid_regex() {
let yaml = r#"
prompt: "Test"
completion:
mode: pattern
patterns:
- value: "[invalid"
type: regex
"#;
let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
let err = params.validate().unwrap_err();
assert!(err.to_string().contains("Invalid regex pattern"));
}
#[test]
fn test_completion_with_confidence_config() {
let yaml = r#"
prompt: "Test"
completion:
mode: explicit
confidence:
threshold: 0.8
on_low:
action: escalate
"#;
let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
let completion = params.completion.clone().unwrap();
let confidence = completion.confidence.clone().unwrap();
assert!((confidence.threshold - 0.8).abs() < f64::EPSILON);
assert_eq!(
confidence.on_low.action,
crate::ast::LowConfidenceAction::Escalate
);
}
#[test]
fn test_completion_with_instruction_config() {
let yaml = r#"
prompt: "Test"
completion:
mode: explicit
instruction:
tone: detailed
lang: fr
"#;
let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
let completion = params.completion.clone().unwrap();
let instruction_config = completion.instruction.clone().unwrap();
assert_eq!(
instruction_config.tone,
crate::ast::completion::InstructionTone::Detailed
);
assert_eq!(instruction_config.lang, Some("fr".to_string()));
}
#[test]
fn test_full_completion_config_yaml() {
let yaml = r#"
prompt: "Generate content for QR Code AI"
provider: claude
model: claude-sonnet-4-6
mcp:
- novanet
max_turns: 10
completion:
mode: explicit
signal:
fields:
required: [result]
optional: [confidence, reasoning]
confidence:
threshold: 0.75
on_low:
action: retry
instruction:
tone: concise
lang: en
"#;
let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
assert!(params.validate().is_ok());
let completion = params.completion.clone().unwrap();
assert_eq!(completion.mode, crate::ast::CompletionMode::Explicit);
let signal = completion.signal.clone().unwrap();
assert_eq!(signal.fields.required, vec!["result"]);
assert_eq!(signal.fields.optional, vec!["confidence", "reasoning"]);
let confidence = completion.confidence.clone().unwrap();
assert!((confidence.threshold - 0.75).abs() < f64::EPSILON);
let instruction = completion.instruction.clone().unwrap();
assert_eq!(instruction.lang, Some("en".to_string()));
}
#[test]
fn test_parse_limits_full() {
let yaml = r#"
prompt: "Test"
limits:
max_turns: 20
max_tokens: 50000
max_cost_usd: 2.00
max_duration_secs: 300
on_limit_reached:
action: complete_partial
save_progress: true
"#;
let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
assert!(params.limits.is_some());
let limits = params.limits.clone().unwrap();
assert_eq!(limits.max_turns, 20);
assert_eq!(limits.max_tokens, 50000);
assert!((limits.max_cost_usd - 2.00).abs() < f64::EPSILON);
assert_eq!(limits.max_duration_secs, 300);
assert_eq!(
limits.on_limit_reached.action,
crate::ast::limits::LimitAction::CompletePartial
);
assert!(limits.on_limit_reached.save_progress);
}
#[test]
fn test_parse_limits_partial() {
let yaml = r#"
prompt: "Test"
limits:
max_turns: 10
max_cost_usd: 1.50
"#;
let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
let limits = params.limits.clone().unwrap();
assert_eq!(limits.max_turns, 10);
assert!((limits.max_cost_usd - 1.50).abs() < f64::EPSILON);
assert_eq!(limits.max_tokens, 0); assert_eq!(limits.max_duration_secs, 0); }
#[test]
fn test_parse_limits_action_fail() {
let yaml = r#"
prompt: "Test"
limits:
max_turns: 5
on_limit_reached:
action: fail
"#;
let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
let limits = params.limits.clone().unwrap();
assert_eq!(
limits.on_limit_reached.action,
crate::ast::limits::LimitAction::Fail
);
}
#[test]
fn test_parse_limits_action_escalate() {
let yaml = r#"
prompt: "Test"
limits:
max_cost_usd: 0.50
on_limit_reached:
action: escalate
message: "Budget exceeded, needs approval"
"#;
let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
let limits = params.limits.clone().unwrap();
assert_eq!(
limits.on_limit_reached.action,
crate::ast::limits::LimitAction::Escalate
);
assert_eq!(
limits.on_limit_reached.message,
Some("Budget exceeded, needs approval".to_string())
);
}
#[test]
fn test_limits_defaults_to_none() {
let params = AgentParams::default();
assert!(params.limits.is_none());
assert!(!params.has_limits());
}
#[test]
fn test_effective_limits_default() {
let params = AgentParams {
prompt: "test".to_string(),
..Default::default()
};
let limits = params.effective_limits();
assert_eq!(limits.max_turns, 0);
assert_eq!(limits.max_tokens, 0);
assert!((limits.max_cost_usd - 0.0).abs() < f64::EPSILON);
assert!(!limits.has_limits());
}
#[test]
fn test_has_limits_true_when_configured() {
let yaml = r#"
prompt: "Test"
limits:
max_turns: 10
"#;
let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
assert!(params.has_limits());
}
#[test]
fn test_has_limits_false_when_all_zero() {
let yaml = r#"
prompt: "Test"
limits:
max_turns: 0
max_tokens: 0
"#;
let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
assert!(!params.has_limits());
}
#[test]
fn test_validate_limits_negative_cost() {
let yaml = r#"
prompt: "Test"
limits:
max_cost_usd: -1.00
"#;
let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
let err = params.validate().unwrap_err();
assert!(err.to_string().contains("max_cost_usd"));
assert!(err.to_string().contains("non-negative"));
}
#[test]
fn test_validate_limits_valid() {
let yaml = r#"
prompt: "Test"
limits:
max_turns: 20
max_tokens: 50000
max_cost_usd: 5.00
max_duration_secs: 600
"#;
let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
assert!(params.validate().is_ok());
}
#[test]
fn test_full_agent_config_with_limits() {
let yaml = r#"
prompt: "Generate comprehensive research report"
provider: claude
model: claude-sonnet-4-6
mcp:
- novanet
- perplexity
max_turns: 20
completion:
mode: explicit
confidence:
threshold: 0.8
guardrails:
- type: length
min_words: 500
limits:
max_turns: 15
max_tokens: 100000
max_cost_usd: 3.00
max_duration_secs: 600
on_limit_reached:
action: complete_partial
save_progress: true
message: "Research incomplete due to limits"
"#;
let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
assert!(params.validate().is_ok());
assert!(params.has_limits());
let limits = params.effective_limits();
assert_eq!(limits.max_turns, 15);
assert_eq!(limits.max_tokens, 100000);
assert!((limits.max_cost_usd - 3.00).abs() < f64::EPSILON);
}
}