1use serde::Deserialize;
22
23use crate::ast::completion::CompletionConfig;
24use crate::error::NikaError;
25
26#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
41#[serde(rename_all = "lowercase")]
42pub enum ToolChoice {
43 #[default]
45 Auto,
46 Required,
48 None,
50}
51
52impl ToolChoice {
53 pub fn as_str(&self) -> &'static str {
55 match self {
56 Self::Auto => "auto",
57 Self::Required => "required",
58 Self::None => "none",
59 }
60 }
61}
62
63const DEFAULT_MAX_TURNS: u32 = 10;
69
70const MAX_ALLOWED_TURNS: u32 = 100;
72
73const DEFAULT_THINKING_BUDGET: u64 = 4096;
75
76const DEFAULT_DEPTH_LIMIT: u32 = 3;
78
79const MAX_DEPTH_LIMIT: u32 = 10;
81
82#[derive(Debug, Clone, Default, Deserialize)]
88pub struct AgentParams {
89 pub prompt: String,
91
92 #[serde(default)]
94 pub system: Option<String>,
95
96 #[serde(default)]
98 pub provider: Option<String>,
99
100 #[serde(default)]
102 pub model: Option<String>,
103
104 #[serde(default)]
106 pub mcp: Vec<String>,
107
108 #[serde(default)]
110 pub tools: Vec<String>,
111
112 #[serde(default)]
114 pub max_turns: Option<u32>,
115
116 #[serde(default)]
119 pub token_budget: Option<u32>,
120
121 #[serde(default)]
123 pub stop_sequences: Vec<String>,
124
125 #[serde(default)]
127 pub scope: Option<String>,
128
129 #[serde(default)]
135 pub extended_thinking: Option<bool>,
136
137 #[serde(default)]
143 pub thinking_budget: Option<u64>,
144
145 #[serde(default)]
151 pub depth_limit: Option<u32>,
152
153 #[serde(default)]
160 pub tool_choice: Option<ToolChoice>,
161
162 #[serde(default)]
171 pub temperature: Option<f32>,
172
173 #[serde(default)]
179 pub max_tokens: Option<u32>,
180
181 #[serde(default)]
187 pub skills: Option<Vec<String>>,
188
189 #[serde(default)]
198 pub completion: Option<CompletionConfig>,
199
200 #[serde(default)]
205 pub guardrails: Vec<crate::ast::guardrails::GuardrailConfig>,
206
207 #[serde(default)]
218 pub limits: Option<crate::ast::limits::LimitsConfig>,
219}
220
221impl AgentParams {
222 #[inline]
227 pub fn effective_max_turns(&self) -> u32 {
228 self.max_turns.unwrap_or(DEFAULT_MAX_TURNS)
229 }
230
231 #[inline]
236 pub fn effective_token_budget(&self) -> u32 {
237 self.token_budget.unwrap_or(u32::MAX)
238 }
239
240 #[inline]
245 pub fn effective_thinking_budget(&self) -> u64 {
246 self.thinking_budget.unwrap_or(DEFAULT_THINKING_BUDGET)
247 }
248
249 #[inline]
254 pub fn effective_depth_limit(&self) -> u32 {
255 self.depth_limit.unwrap_or(DEFAULT_DEPTH_LIMIT)
256 }
257
258 #[inline]
266 pub fn effective_max_tokens(&self) -> Option<u32> {
267 if let Some(max_tokens) = self.max_tokens {
268 Some(max_tokens)
269 } else if self.extended_thinking.unwrap_or(false) {
270 let thinking_budget = self.effective_thinking_budget() as u32;
272 Some(thinking_budget + 8192)
273 } else {
274 None
275 }
276 }
277
278 #[inline]
283 pub fn effective_tool_choice(&self) -> ToolChoice {
284 self.tool_choice.clone().unwrap_or_default()
285 }
286
287 #[inline]
303 pub fn has_explicit_tool_choice(&self) -> bool {
304 self.tool_choice.is_some()
305 }
306
307 #[inline]
312 pub fn effective_temperature(&self) -> Option<f32> {
313 self.temperature
314 }
315
316 pub fn effective_completion(&self) -> Option<CompletionConfig> {
320 self.completion.clone()
321 }
322
323 pub fn completion_system_instruction(&self) -> String {
328 self.completion
329 .as_ref()
330 .map(|c| c.generate_system_instruction())
331 .unwrap_or_default()
332 }
333
334 pub fn validate(&self) -> Result<(), NikaError> {
343 if self.prompt.is_empty() {
344 return Err(NikaError::ValidationError {
345 reason: "Agent prompt cannot be empty".into(),
346 });
347 }
348
349 if let Some(max) = self.max_turns {
350 if max == 0 {
351 return Err(NikaError::ValidationError {
352 reason: "max_turns must be > 0".into(),
353 });
354 }
355 if max > MAX_ALLOWED_TURNS {
356 return Err(NikaError::ValidationError {
357 reason: format!("max_turns cannot exceed {}", MAX_ALLOWED_TURNS),
358 });
359 }
360 }
361
362 if let Some(budget) = self.token_budget {
363 if budget == 0 {
364 return Err(NikaError::ValidationError {
365 reason: "token_budget must be > 0".into(),
366 });
367 }
368 }
369
370 if self.extended_thinking == Some(true) {
372 if let Some(ref provider) = self.provider {
373 if provider != "claude" {
374 return Err(NikaError::ValidationError {
375 reason: format!(
376 "extended_thinking only supported for claude provider, got '{}'",
377 provider
378 ),
379 });
380 }
381 }
382 }
383
384 if let Some(depth) = self.depth_limit {
386 if depth == 0 {
387 return Err(NikaError::ValidationError {
388 reason: "depth_limit must be > 0".into(),
389 });
390 }
391 if depth > MAX_DEPTH_LIMIT {
392 return Err(NikaError::ValidationError {
393 reason: format!("depth_limit cannot exceed {}", MAX_DEPTH_LIMIT),
394 });
395 }
396 }
397
398 if let Some(temp) = self.temperature {
400 if !(0.0..=2.0).contains(&temp) {
401 return Err(NikaError::ValidationError {
402 reason: format!("temperature must be between 0.0 and 2.0, got {}", temp),
403 });
404 }
405 }
406
407 if let Some(ref completion) = self.completion {
409 completion.validate()?;
410 }
411
412 if let Some(ref limits) = self.limits {
414 limits.validate()?;
415 }
416
417 Ok(())
418 }
419
420 pub fn effective_limits(&self) -> crate::ast::limits::LimitsConfig {
424 self.limits.clone().unwrap_or_default()
425 }
426
427 pub fn has_limits(&self) -> bool {
429 self.limits
430 .as_ref()
431 .map(|l| l.has_limits())
432 .unwrap_or(false)
433 }
434}
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439 use crate::serde_yaml;
440
441 #[test]
442 fn parse_agent_params_basic() {
443 let yaml = r#"
444prompt: "Test prompt"
445provider: claude
446model: claude-sonnet-4-6
447"#;
448 let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
449 assert_eq!(params.prompt, "Test prompt");
450 assert_eq!(params.provider, Some("claude".to_string()));
451 assert_eq!(params.model, Some("claude-sonnet-4-6".to_string()));
452 }
453
454 #[test]
455 fn parse_agent_params_mcp_list() {
456 let yaml = r#"
457prompt: "Test"
458mcp:
459 - novanet
460 - filesystem
461"#;
462 let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
463 assert_eq!(params.mcp, vec!["novanet", "filesystem"]);
464 }
465
466 #[test]
467 fn effective_max_turns_default() {
468 let params = AgentParams::default();
469 assert_eq!(params.effective_max_turns(), DEFAULT_MAX_TURNS);
470 }
471
472 #[test]
473 fn effective_max_turns_custom() {
474 let params = AgentParams {
475 max_turns: Some(20),
476 ..Default::default()
477 };
478 assert_eq!(params.effective_max_turns(), 20);
479 }
480
481 #[test]
482 fn validate_empty_prompt() {
483 let params = AgentParams::default();
484 assert!(params.validate().is_err());
485 }
486
487 #[test]
488 fn validate_zero_max_turns() {
489 let params = AgentParams {
490 prompt: "test".to_string(),
491 max_turns: Some(0),
492 ..Default::default()
493 };
494 assert!(params.validate().is_err());
495 }
496
497 #[test]
498 fn validate_excessive_max_turns() {
499 let params = AgentParams {
500 prompt: "test".to_string(),
501 max_turns: Some(101),
502 ..Default::default()
503 };
504 assert!(params.validate().is_err());
505 }
506
507 #[test]
508 fn validate_ok() {
509 let params = AgentParams {
510 prompt: "test".to_string(),
511 max_turns: Some(50),
512 ..Default::default()
513 };
514 assert!(params.validate().is_ok());
515 }
516
517 #[test]
522 fn parse_token_budget() {
523 let yaml = r#"
524prompt: "Test"
525token_budget: 100000
526"#;
527 let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
528 assert_eq!(params.token_budget, Some(100000));
529 }
530
531 #[test]
532 fn effective_token_budget_default() {
533 let params = AgentParams {
534 prompt: "test".to_string(),
535 ..Default::default()
536 };
537 assert_eq!(params.effective_token_budget(), u32::MAX);
539 }
540
541 #[test]
542 fn effective_token_budget_custom() {
543 let params = AgentParams {
544 prompt: "test".to_string(),
545 token_budget: Some(50000),
546 ..Default::default()
547 };
548 assert_eq!(params.effective_token_budget(), 50000);
549 }
550
551 #[test]
552 fn validate_zero_token_budget() {
553 let params = AgentParams {
554 prompt: "test".to_string(),
555 token_budget: Some(0),
556 ..Default::default()
557 };
558 assert!(params.validate().is_err());
559 }
560
561 #[test]
566 fn parse_system_prompt() {
567 let yaml = r#"
568prompt: "User prompt"
569system: "You are a helpful assistant."
570"#;
571 let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
572 assert_eq!(
573 params.system,
574 Some("You are a helpful assistant.".to_string())
575 );
576 }
577
578 #[test]
579 fn system_prompt_defaults_to_none() {
580 let params = AgentParams::default();
581 assert!(params.system.is_none());
582 }
583
584 #[test]
589 fn parse_extended_thinking_true() {
590 let yaml = r#"
591prompt: "Test"
592extended_thinking: true
593"#;
594 let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
595 assert_eq!(params.extended_thinking, Some(true));
596 }
597
598 #[test]
599 fn parse_extended_thinking_false() {
600 let yaml = r#"
601prompt: "Test"
602extended_thinking: false
603"#;
604 let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
605 assert_eq!(params.extended_thinking, Some(false));
606 }
607
608 #[test]
609 fn extended_thinking_defaults_to_none() {
610 let params = AgentParams::default();
611 assert!(params.extended_thinking.is_none());
612 }
613
614 #[test]
615 fn validate_extended_thinking_with_openai_fails() {
616 let params = AgentParams {
617 prompt: "test".to_string(),
618 extended_thinking: Some(true),
619 provider: Some("openai".to_string()),
620 ..Default::default()
621 };
622 let err = params.validate().unwrap_err();
623 assert!(err
624 .to_string()
625 .contains("extended_thinking only supported for claude"));
626 }
627
628 #[test]
629 fn validate_extended_thinking_with_claude_ok() {
630 let params = AgentParams {
631 prompt: "test".to_string(),
632 extended_thinking: Some(true),
633 provider: Some("claude".to_string()),
634 ..Default::default()
635 };
636 assert!(params.validate().is_ok());
637 }
638
639 #[test]
640 fn validate_extended_thinking_without_provider_ok() {
641 let params = AgentParams {
644 prompt: "test".to_string(),
645 extended_thinking: Some(true),
646 provider: None,
647 ..Default::default()
648 };
649 assert!(params.validate().is_ok());
650 }
651
652 #[test]
657 fn parse_thinking_budget() {
658 let yaml = r#"
659prompt: "Test"
660extended_thinking: true
661thinking_budget: 8192
662"#;
663 let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
664 assert_eq!(params.thinking_budget, Some(8192));
665 }
666
667 #[test]
668 fn effective_thinking_budget_default() {
669 let params = AgentParams {
670 prompt: "test".to_string(),
671 ..Default::default()
672 };
673 assert_eq!(params.effective_thinking_budget(), DEFAULT_THINKING_BUDGET);
675 assert_eq!(params.effective_thinking_budget(), 4096);
676 }
677
678 #[test]
679 fn effective_thinking_budget_custom() {
680 let params = AgentParams {
681 prompt: "test".to_string(),
682 thinking_budget: Some(16384),
683 ..Default::default()
684 };
685 assert_eq!(params.effective_thinking_budget(), 16384);
686 }
687
688 #[test]
689 fn thinking_budget_defaults_to_none() {
690 let params = AgentParams::default();
691 assert!(params.thinking_budget.is_none());
692 }
693
694 #[test]
699 fn effective_max_tokens_explicit() {
700 let params = AgentParams {
701 prompt: "test".to_string(),
702 max_tokens: Some(16384),
703 ..Default::default()
704 };
705 assert_eq!(params.effective_max_tokens(), Some(16384));
706 }
707
708 #[test]
709 fn effective_max_tokens_with_extended_thinking() {
710 let params = AgentParams {
711 prompt: "test".to_string(),
712 extended_thinking: Some(true),
713 thinking_budget: Some(8192),
714 max_tokens: None, ..Default::default()
716 };
717 assert_eq!(params.effective_max_tokens(), Some(8192 + 8192));
719 }
720
721 #[test]
722 fn effective_max_tokens_explicit_overrides_auto() {
723 let params = AgentParams {
724 prompt: "test".to_string(),
725 extended_thinking: Some(true),
726 thinking_budget: Some(8192),
727 max_tokens: Some(32768), ..Default::default()
729 };
730 assert_eq!(params.effective_max_tokens(), Some(32768));
732 }
733
734 #[test]
735 fn effective_max_tokens_none_without_thinking() {
736 let params = AgentParams {
737 prompt: "test".to_string(),
738 extended_thinking: None,
739 max_tokens: None,
740 ..Default::default()
741 };
742 assert_eq!(params.effective_max_tokens(), None);
744 }
745
746 #[test]
751 fn test_parse_tool_choice_auto() {
752 let yaml = r#"
753prompt: "Test"
754tool_choice: auto
755"#;
756 let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
757 assert_eq!(params.tool_choice, Some(ToolChoice::Auto));
758 }
759
760 #[test]
761 fn test_parse_tool_choice_required() {
762 let yaml = r#"
763prompt: "Test"
764tool_choice: required
765"#;
766 let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
767 assert_eq!(params.tool_choice, Some(ToolChoice::Required));
768 }
769
770 #[test]
771 fn test_parse_tool_choice_none() {
772 let yaml = r#"
773prompt: "Test"
774tool_choice: none
775"#;
776 let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
777 assert_eq!(params.tool_choice, Some(ToolChoice::None));
778 }
779
780 #[test]
781 fn test_tool_choice_default() {
782 let params = AgentParams::default();
783 assert!(params.tool_choice.is_none());
784 assert_eq!(params.effective_tool_choice(), ToolChoice::Auto);
785 }
786
787 #[test]
788 fn test_tool_choice_as_str() {
789 assert_eq!(ToolChoice::Auto.as_str(), "auto");
790 assert_eq!(ToolChoice::Required.as_str(), "required");
791 assert_eq!(ToolChoice::None.as_str(), "none");
792 }
793
794 #[test]
799 fn test_parse_temperature() {
800 let yaml = r#"
801prompt: "Test"
802temperature: 0.7
803"#;
804 let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
805 assert_eq!(params.temperature, Some(0.7));
806 }
807
808 #[test]
809 fn test_temperature_default() {
810 let params = AgentParams::default();
811 assert!(params.temperature.is_none());
812 assert_eq!(params.effective_temperature(), None);
813 }
814
815 #[test]
816 fn test_temperature_validation_valid_range() {
817 for temp in [0.0, 0.5, 1.0, 1.5, 2.0] {
819 let params = AgentParams {
820 prompt: "test".to_string(),
821 temperature: Some(temp),
822 ..Default::default()
823 };
824 assert!(
825 params.validate().is_ok(),
826 "temperature {} should be valid",
827 temp
828 );
829 }
830 }
831
832 #[test]
833 fn test_temperature_validation_too_low() {
834 let params = AgentParams {
835 prompt: "test".to_string(),
836 temperature: Some(-0.1),
837 ..Default::default()
838 };
839 let err = params.validate().unwrap_err();
840 assert!(err
841 .to_string()
842 .contains("temperature must be between 0.0 and 2.0"));
843 }
844
845 #[test]
846 fn test_temperature_validation_too_high() {
847 let params = AgentParams {
848 prompt: "test".to_string(),
849 temperature: Some(2.1),
850 ..Default::default()
851 };
852 let err = params.validate().unwrap_err();
853 assert!(err
854 .to_string()
855 .contains("temperature must be between 0.0 and 2.0"));
856 }
857
858 #[test]
859 fn test_effective_temperature_custom() {
860 let params = AgentParams {
861 prompt: "test".to_string(),
862 temperature: Some(0.3),
863 ..Default::default()
864 };
865 assert_eq!(params.effective_temperature(), Some(0.3));
866 }
867
868 #[test]
869 fn test_combined_tool_choice_and_temperature() {
870 let yaml = r#"
871prompt: "Generate creative content"
872tool_choice: required
873temperature: 1.5
874"#;
875 let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
876 assert_eq!(params.tool_choice, Some(ToolChoice::Required));
877 assert_eq!(params.temperature, Some(1.5));
878 assert!(params.validate().is_ok());
879 }
880
881 #[test]
886 fn test_has_explicit_tool_choice_when_not_set() {
887 let params = AgentParams {
888 prompt: "test".to_string(),
889 ..Default::default()
890 };
891 assert!(!params.has_explicit_tool_choice());
892 assert_eq!(params.effective_tool_choice(), ToolChoice::Auto);
894 }
895
896 #[test]
897 fn test_has_explicit_tool_choice_when_auto() {
898 let params = AgentParams {
899 prompt: "test".to_string(),
900 tool_choice: Some(ToolChoice::Auto),
901 ..Default::default()
902 };
903 assert!(params.has_explicit_tool_choice());
905 assert_eq!(params.effective_tool_choice(), ToolChoice::Auto);
906 }
907
908 #[test]
909 fn test_has_explicit_tool_choice_when_required() {
910 let params = AgentParams {
911 prompt: "test".to_string(),
912 tool_choice: Some(ToolChoice::Required),
913 ..Default::default()
914 };
915 assert!(params.has_explicit_tool_choice());
916 assert_eq!(params.effective_tool_choice(), ToolChoice::Required);
917 }
918
919 #[test]
920 fn test_has_explicit_tool_choice_when_none() {
921 let params = AgentParams {
922 prompt: "test".to_string(),
923 tool_choice: Some(ToolChoice::None),
924 ..Default::default()
925 };
926 assert!(params.has_explicit_tool_choice());
927 assert_eq!(params.effective_tool_choice(), ToolChoice::None);
928 }
929
930 #[test]
931 fn test_has_explicit_tool_choice_from_yaml_absent() {
932 let yaml = r#"
933prompt: "Test prompt"
934"#;
935 let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
936 assert!(!params.has_explicit_tool_choice());
937 }
938
939 #[test]
940 fn test_has_explicit_tool_choice_from_yaml_present() {
941 let yaml = r#"
942prompt: "Test prompt"
943tool_choice: none
944"#;
945 let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
946 assert!(params.has_explicit_tool_choice());
947 assert_eq!(params.effective_tool_choice(), ToolChoice::None);
948 }
949
950 #[test]
955 fn test_parse_completion_explicit_mode() {
956 let yaml = r#"
957prompt: "Test"
958completion:
959 mode: explicit
960 signal:
961 fields:
962 required: [result]
963 optional: [confidence]
964"#;
965 let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
966 assert!(params.completion.is_some());
967
968 let completion = params.completion.clone().unwrap();
969 assert_eq!(completion.mode, crate::ast::CompletionMode::Explicit);
970 assert!(completion.signal.is_some());
971 }
972
973 #[test]
974 fn test_parse_completion_natural_mode() {
975 let yaml = r#"
976prompt: "Test"
977completion:
978 mode: natural
979"#;
980 let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
981 let completion = params.completion.clone().unwrap();
982 assert_eq!(completion.mode, crate::ast::CompletionMode::Natural);
983 }
984
985 #[test]
986 fn test_parse_completion_pattern_mode() {
987 let yaml = r#"
988prompt: "Test"
989completion:
990 mode: pattern
991 patterns:
992 - value: "DONE"
993 type: contains
994 - value: "^COMPLETE:"
995 type: regex
996"#;
997 let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
998 let completion = params.completion.clone().unwrap();
999 assert_eq!(completion.mode, crate::ast::CompletionMode::Pattern);
1000 assert_eq!(completion.patterns.len(), 2);
1001 }
1002
1003 #[test]
1004 fn test_effective_completion_uses_completion_field() {
1005 let yaml = r#"
1006prompt: "Test"
1007completion:
1008 mode: explicit
1009"#;
1010 let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
1011
1012 let effective = params.effective_completion().unwrap();
1013 assert_eq!(effective.mode, crate::ast::CompletionMode::Explicit);
1014 }
1015
1016 #[test]
1017 fn test_effective_completion_returns_none_when_empty() {
1018 let params = AgentParams {
1019 prompt: "test".to_string(),
1020 ..Default::default()
1021 };
1022
1023 assert!(params.effective_completion().is_none());
1024 }
1025
1026 #[test]
1027 fn test_completion_system_instruction_explicit_mode() {
1028 let yaml = r#"
1029prompt: "Test"
1030completion:
1031 mode: explicit
1032 signal:
1033 fields:
1034 required: [result, confidence]
1035"#;
1036 let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
1037 let instruction = params.completion_system_instruction();
1038
1039 assert!(instruction.contains("nika:complete"));
1041 assert!(instruction.contains("result"));
1042 assert!(instruction.contains("confidence"));
1043 }
1044
1045 #[test]
1046 fn test_completion_system_instruction_natural_mode() {
1047 let yaml = r#"
1048prompt: "Test"
1049completion:
1050 mode: natural
1051"#;
1052 let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
1053 let instruction = params.completion_system_instruction();
1054
1055 assert!(instruction.is_empty());
1057 }
1058
1059 #[test]
1060 fn test_completion_system_instruction_pattern_mode() {
1061 let yaml = r#"
1062prompt: "Test"
1063completion:
1064 mode: pattern
1065 patterns:
1066 - value: "TASK_DONE"
1067 type: exact
1068"#;
1069 let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
1070 let instruction = params.completion_system_instruction();
1071
1072 assert!(instruction.contains("TASK_DONE"));
1074 }
1075
1076 #[test]
1077 fn test_completion_system_instruction_empty_when_none() {
1078 let params = AgentParams {
1079 prompt: "test".to_string(),
1080 ..Default::default()
1081 };
1082
1083 let instruction = params.completion_system_instruction();
1084 assert!(instruction.is_empty());
1085 }
1086
1087 #[test]
1088 fn test_validate_completion_config_valid() {
1089 let yaml = r#"
1090prompt: "Test"
1091completion:
1092 mode: pattern
1093 patterns:
1094 - value: "^DONE$"
1095 type: regex
1096"#;
1097 let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
1098 assert!(params.validate().is_ok());
1099 }
1100
1101 #[test]
1102 fn test_validate_completion_config_invalid_regex() {
1103 let yaml = r#"
1104prompt: "Test"
1105completion:
1106 mode: pattern
1107 patterns:
1108 - value: "[invalid"
1109 type: regex
1110"#;
1111 let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
1112 let err = params.validate().unwrap_err();
1113 assert!(err.to_string().contains("Invalid regex pattern"));
1114 }
1115
1116 #[test]
1117 fn test_completion_with_confidence_config() {
1118 let yaml = r#"
1119prompt: "Test"
1120completion:
1121 mode: explicit
1122 confidence:
1123 threshold: 0.8
1124 on_low:
1125 action: escalate
1126"#;
1127 let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
1128 let completion = params.completion.clone().unwrap();
1129
1130 let confidence = completion.confidence.clone().unwrap();
1131 assert!((confidence.threshold - 0.8).abs() < f64::EPSILON);
1132 assert_eq!(
1133 confidence.on_low.action,
1134 crate::ast::LowConfidenceAction::Escalate
1135 );
1136 }
1137
1138 #[test]
1139 fn test_completion_with_instruction_config() {
1140 let yaml = r#"
1141prompt: "Test"
1142completion:
1143 mode: explicit
1144 instruction:
1145 tone: detailed
1146 lang: fr
1147"#;
1148 let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
1149 let completion = params.completion.clone().unwrap();
1150
1151 let instruction_config = completion.instruction.clone().unwrap();
1152 assert_eq!(
1153 instruction_config.tone,
1154 crate::ast::completion::InstructionTone::Detailed
1155 );
1156 assert_eq!(instruction_config.lang, Some("fr".to_string()));
1157 }
1158
1159 #[test]
1160 fn test_full_completion_config_yaml() {
1161 let yaml = r#"
1162prompt: "Generate content for QR Code AI"
1163provider: claude
1164model: claude-sonnet-4-6
1165mcp:
1166 - novanet
1167max_turns: 10
1168completion:
1169 mode: explicit
1170 signal:
1171 fields:
1172 required: [result]
1173 optional: [confidence, reasoning]
1174 confidence:
1175 threshold: 0.75
1176 on_low:
1177 action: retry
1178 instruction:
1179 tone: concise
1180 lang: en
1181"#;
1182 let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
1183
1184 assert!(params.validate().is_ok());
1186
1187 let completion = params.completion.clone().unwrap();
1189 assert_eq!(completion.mode, crate::ast::CompletionMode::Explicit);
1190
1191 let signal = completion.signal.clone().unwrap();
1193 assert_eq!(signal.fields.required, vec!["result"]);
1194 assert_eq!(signal.fields.optional, vec!["confidence", "reasoning"]);
1195
1196 let confidence = completion.confidence.clone().unwrap();
1198 assert!((confidence.threshold - 0.75).abs() < f64::EPSILON);
1199
1200 let instruction = completion.instruction.clone().unwrap();
1202 assert_eq!(instruction.lang, Some("en".to_string()));
1203 }
1204
1205 #[test]
1210 fn test_parse_limits_full() {
1211 let yaml = r#"
1212prompt: "Test"
1213limits:
1214 max_turns: 20
1215 max_tokens: 50000
1216 max_cost_usd: 2.00
1217 max_duration_secs: 300
1218 on_limit_reached:
1219 action: complete_partial
1220 save_progress: true
1221"#;
1222 let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
1223 assert!(params.limits.is_some());
1224
1225 let limits = params.limits.clone().unwrap();
1226 assert_eq!(limits.max_turns, 20);
1227 assert_eq!(limits.max_tokens, 50000);
1228 assert!((limits.max_cost_usd - 2.00).abs() < f64::EPSILON);
1229 assert_eq!(limits.max_duration_secs, 300);
1230 assert_eq!(
1231 limits.on_limit_reached.action,
1232 crate::ast::limits::LimitAction::CompletePartial
1233 );
1234 assert!(limits.on_limit_reached.save_progress);
1235 }
1236
1237 #[test]
1238 fn test_parse_limits_partial() {
1239 let yaml = r#"
1240prompt: "Test"
1241limits:
1242 max_turns: 10
1243 max_cost_usd: 1.50
1244"#;
1245 let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
1246 let limits = params.limits.clone().unwrap();
1247 assert_eq!(limits.max_turns, 10);
1248 assert!((limits.max_cost_usd - 1.50).abs() < f64::EPSILON);
1249 assert_eq!(limits.max_tokens, 0); assert_eq!(limits.max_duration_secs, 0); }
1252
1253 #[test]
1254 fn test_parse_limits_action_fail() {
1255 let yaml = r#"
1256prompt: "Test"
1257limits:
1258 max_turns: 5
1259 on_limit_reached:
1260 action: fail
1261"#;
1262 let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
1263 let limits = params.limits.clone().unwrap();
1264 assert_eq!(
1265 limits.on_limit_reached.action,
1266 crate::ast::limits::LimitAction::Fail
1267 );
1268 }
1269
1270 #[test]
1271 fn test_parse_limits_action_escalate() {
1272 let yaml = r#"
1273prompt: "Test"
1274limits:
1275 max_cost_usd: 0.50
1276 on_limit_reached:
1277 action: escalate
1278 message: "Budget exceeded, needs approval"
1279"#;
1280 let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
1281 let limits = params.limits.clone().unwrap();
1282 assert_eq!(
1283 limits.on_limit_reached.action,
1284 crate::ast::limits::LimitAction::Escalate
1285 );
1286 assert_eq!(
1287 limits.on_limit_reached.message,
1288 Some("Budget exceeded, needs approval".to_string())
1289 );
1290 }
1291
1292 #[test]
1293 fn test_limits_defaults_to_none() {
1294 let params = AgentParams::default();
1295 assert!(params.limits.is_none());
1296 assert!(!params.has_limits());
1297 }
1298
1299 #[test]
1300 fn test_effective_limits_default() {
1301 let params = AgentParams {
1302 prompt: "test".to_string(),
1303 ..Default::default()
1304 };
1305 let limits = params.effective_limits();
1306 assert_eq!(limits.max_turns, 0);
1307 assert_eq!(limits.max_tokens, 0);
1308 assert!((limits.max_cost_usd - 0.0).abs() < f64::EPSILON);
1309 assert!(!limits.has_limits());
1310 }
1311
1312 #[test]
1313 fn test_has_limits_true_when_configured() {
1314 let yaml = r#"
1315prompt: "Test"
1316limits:
1317 max_turns: 10
1318"#;
1319 let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
1320 assert!(params.has_limits());
1321 }
1322
1323 #[test]
1324 fn test_has_limits_false_when_all_zero() {
1325 let yaml = r#"
1326prompt: "Test"
1327limits:
1328 max_turns: 0
1329 max_tokens: 0
1330"#;
1331 let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
1332 assert!(!params.has_limits());
1333 }
1334
1335 #[test]
1336 fn test_validate_limits_negative_cost() {
1337 let yaml = r#"
1338prompt: "Test"
1339limits:
1340 max_cost_usd: -1.00
1341"#;
1342 let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
1343 let err = params.validate().unwrap_err();
1344 assert!(err.to_string().contains("max_cost_usd"));
1345 assert!(err.to_string().contains("non-negative"));
1346 }
1347
1348 #[test]
1349 fn test_validate_limits_valid() {
1350 let yaml = r#"
1351prompt: "Test"
1352limits:
1353 max_turns: 20
1354 max_tokens: 50000
1355 max_cost_usd: 5.00
1356 max_duration_secs: 600
1357"#;
1358 let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
1359 assert!(params.validate().is_ok());
1360 }
1361
1362 #[test]
1363 fn test_full_agent_config_with_limits() {
1364 let yaml = r#"
1365prompt: "Generate comprehensive research report"
1366provider: claude
1367model: claude-sonnet-4-6
1368mcp:
1369 - novanet
1370 - perplexity
1371max_turns: 20
1372completion:
1373 mode: explicit
1374 confidence:
1375 threshold: 0.8
1376guardrails:
1377 - type: length
1378 min_words: 500
1379limits:
1380 max_turns: 15
1381 max_tokens: 100000
1382 max_cost_usd: 3.00
1383 max_duration_secs: 600
1384 on_limit_reached:
1385 action: complete_partial
1386 save_progress: true
1387 message: "Research incomplete due to limits"
1388"#;
1389 let params: AgentParams = serde_yaml::from_str(yaml).unwrap();
1390 assert!(params.validate().is_ok());
1391 assert!(params.has_limits());
1392
1393 let limits = params.effective_limits();
1394 assert_eq!(limits.max_turns, 15);
1395 assert_eq!(limits.max_tokens, 100000);
1396 assert!((limits.max_cost_usd - 3.00).abs() < f64::EPSILON);
1397 }
1398}