1use crate::constants::{defaults, execution, llm_generation, prompt_budget, tool_limits};
2use crate::types::{
3 ReasoningEffortLevel, ShellPromptProfile, SystemPromptMode, ToolDocumentationMode, UiSurfacePreference,
4 VerbosityLevel,
5};
6use serde::{Deserialize, Serialize};
7use std::collections::BTreeMap;
8
9const DEFAULT_CHECKPOINTS_ENABLED: bool = true;
10const DEFAULT_MAX_SNAPSHOTS: usize = 50;
11const DEFAULT_MAX_AGE_DAYS: u64 = 30;
12
13mod approval;
14mod circuit_breaker;
15mod codex_app_server;
16mod harness;
17mod memory;
18mod onboarding;
19mod open_responses;
20mod prompt_suggestions;
21mod small_model;
22mod vibe_coding;
23
24pub use approval::{AsyncApprovalConfig, ConfidenceEscalationConfig, SkepticPanelConfig};
25pub use circuit_breaker::CircuitBreakerConfig;
26pub use codex_app_server::AgentCodexAppServerConfig;
27pub use harness::{
28 AgentHarnessConfig, ToolResultClearingConfig, TrackerContinuationConfig, VerificationAutoRecoveryConfig,
29};
30pub use memory::{MemoriesConfig, PersistentMemoryConfig};
31pub use onboarding::AgentOnboardingConfig;
32pub use open_responses::OpenResponsesConfig;
33pub use prompt_suggestions::AgentPromptSuggestionsConfig;
34pub use small_model::AgentSmallModelConfig;
35pub use vibe_coding::AgentVibeCodingConfig;
36
37#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
39#[derive(Debug, Clone, Deserialize, Serialize)]
40pub struct AgentConfig {
41 #[serde(default = "default_provider")]
43 pub provider: String,
44
45 #[serde(default = "default_api_key_env")]
47 pub api_key_env: String,
48
49 #[serde(default = "default_model")]
51 pub default_model: String,
52
53 #[serde(default = "default_theme")]
55 pub theme: String,
56
57 #[serde(default)]
61 pub system_prompt_mode: SystemPromptMode,
62
63 #[serde(default = "default_max_system_prompt_tokens")]
67 pub max_system_prompt_tokens: u64,
68
69 #[serde(default = "default_system_prompt_budget_warning")]
71 pub system_prompt_budget_warning: bool,
72
73 #[serde(default = "default_trim_system_prompt")]
76 pub trim_system_prompt: bool,
77
78 #[serde(default)]
84 pub tool_documentation_mode: ToolDocumentationMode,
85
86 #[serde(default)]
90 pub shell_prompt_profile: ShellPromptProfile,
91
92 #[serde(default = "default_enable_split_tool_results")]
99 pub enable_split_tool_results: bool,
100
101 #[serde(default = "default_todo_planning_mode")]
103 pub todo_planning_mode: bool,
104
105 #[serde(default)]
107 pub ui_surface: UiSurfacePreference,
108
109 #[serde(default = "default_max_conversation_turns")]
111 pub max_conversation_turns: usize,
112
113 #[serde(default = "default_idle_turn_limit")]
116 pub idle_turn_limit: usize,
117
118 #[serde(default = "default_reasoning_effort")]
120 pub reasoning_effort: ReasoningEffortLevel,
121 #[serde(default)]
123 pub allow_reasoning_effort_downgrade: bool,
124
125 #[serde(default = "default_verbosity")]
127 pub verbosity: VerbosityLevel,
128
129 #[serde(default = "default_temperature")]
131 pub temperature: f32,
132
133 #[serde(default = "default_refine_temperature")]
135 pub refine_temperature: f32,
136
137 #[serde(default = "default_enable_self_review")]
139 pub enable_self_review: bool,
140
141 #[serde(default = "default_max_review_passes")]
143 pub max_review_passes: usize,
144
145 #[serde(default = "default_refine_prompts_enabled")]
147 pub refine_prompts_enabled: bool,
148
149 #[serde(default = "default_refine_max_passes")]
151 pub refine_prompts_max_passes: usize,
152
153 #[serde(default)]
155 pub refine_prompts_model: String,
156
157 #[serde(default)]
161 pub small_model: AgentSmallModelConfig,
162
163 #[serde(default)]
165 pub prompt_suggestions: AgentPromptSuggestionsConfig,
166
167 #[serde(default)]
169 pub onboarding: AgentOnboardingConfig,
170
171 #[serde(default = "default_project_doc_max_bytes")]
173 pub project_doc_max_bytes: usize,
174
175 #[serde(default)]
177 pub project_doc_fallback_filenames: Vec<String>,
178
179 #[serde(default = "default_instruction_max_bytes", alias = "rule_doc_max_bytes")]
181 pub instruction_max_bytes: usize,
182
183 #[serde(default, alias = "instruction_paths", alias = "instructions")]
185 pub instruction_files: Vec<String>,
186
187 #[serde(default)]
189 pub instruction_excludes: Vec<String>,
190
191 #[serde(default = "default_instruction_import_max_depth")]
193 pub instruction_import_max_depth: usize,
194
195 #[serde(default)]
197 pub persistent_memory: PersistentMemoryConfig,
198
199 #[serde(default, skip_serializing)]
207 pub custom_api_keys: BTreeMap<String, String>,
208
209 #[serde(default)]
216 pub credential_storage_mode: crate::auth::AuthCredentialsStoreMode,
217
218 #[serde(default)]
220 pub checkpointing: AgentCheckpointingConfig,
221
222 #[serde(default)]
224 pub vibe_coding: AgentVibeCodingConfig,
225
226 #[serde(default = "default_max_task_retries")]
230 pub max_task_retries: u32,
231
232 #[serde(default)]
234 pub harness: AgentHarnessConfig,
235
236 #[serde(default)]
238 pub codex_app_server: AgentCodexAppServerConfig,
239
240 #[serde(default = "default_include_temporal_context")]
243 pub include_temporal_context: bool,
244
245 #[serde(default)]
247 pub temporal_context_use_utc: bool,
248
249 #[serde(default = "default_include_working_directory")]
251 pub include_working_directory: bool,
252
253 #[serde(default)]
263 pub include_structured_reasoning_tags: Option<bool>,
264
265 #[serde(default)]
267 pub user_instructions: Option<String>,
268
269 #[serde(default = "default_require_plan_confirmation")]
273 pub require_plan_confirmation: bool,
274
275 #[serde(default)]
278 pub circuit_breaker: CircuitBreakerConfig,
279
280 #[serde(default)]
283 pub open_responses: OpenResponsesConfig,
284}
285
286#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
287#[cfg_attr(feature = "schema", schemars(rename_all = "snake_case"))]
288#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
289#[serde(rename_all = "snake_case")]
290pub enum ContinuationPolicy {
291 Off,
292 ExecOnly,
293 #[default]
294 All,
295}
296
297impl ContinuationPolicy {
298 pub fn as_str(&self) -> &'static str {
299 match self {
300 Self::Off => "off",
301 Self::ExecOnly => "exec_only",
302 Self::All => "all",
303 }
304 }
305
306 fn parse(value: &str) -> Option<Self> {
307 let normalized = value.trim();
308 if normalized.eq_ignore_ascii_case("off") {
309 Some(Self::Off)
310 } else if normalized.eq_ignore_ascii_case("exec_only") || normalized.eq_ignore_ascii_case("exec-only") {
311 Some(Self::ExecOnly)
312 } else if normalized.eq_ignore_ascii_case("all") {
313 Some(Self::All)
314 } else {
315 None
316 }
317 }
318}
319
320impl<'de> Deserialize<'de> for ContinuationPolicy {
321 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
322 where
323 D: serde::Deserializer<'de>,
324 {
325 let raw = String::deserialize(deserializer)?;
326 Ok(Self::parse(&raw).unwrap_or_default())
327 }
328}
329
330#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
341#[cfg_attr(feature = "schema", schemars(rename_all = "snake_case"))]
342#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
343#[serde(rename_all = "snake_case")]
344pub enum ContextResetMode {
345 #[default]
347 Off,
348 OnStall,
351 OnCompaction,
354}
355
356impl ContextResetMode {
357 pub fn as_str(&self) -> &'static str {
358 match self {
359 Self::Off => "off",
360 Self::OnStall => "on_stall",
361 Self::OnCompaction => "on_compaction",
362 }
363 }
364
365 fn parse(value: &str) -> Option<Self> {
366 let normalized = value.trim();
367 if normalized.eq_ignore_ascii_case("off") {
368 Some(Self::Off)
369 } else if normalized.eq_ignore_ascii_case("on_stall") || normalized.eq_ignore_ascii_case("on-stall") {
370 Some(Self::OnStall)
371 } else if normalized.eq_ignore_ascii_case("on_compaction") || normalized.eq_ignore_ascii_case("on-compaction") {
372 Some(Self::OnCompaction)
373 } else {
374 None
375 }
376 }
377}
378
379impl<'de> Deserialize<'de> for ContextResetMode {
380 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
381 where
382 D: serde::Deserializer<'de>,
383 {
384 let raw = String::deserialize(deserializer)?;
385 Ok(Self::parse(&raw).unwrap_or_default())
386 }
387}
388
389#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
390#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
391#[serde(rename_all = "snake_case")]
392pub enum HarnessOrchestrationMode {
393 #[default]
394 PlanBuildEvaluate,
395 Single,
396}
397
398impl HarnessOrchestrationMode {
399 pub fn as_str(&self) -> &'static str {
400 match self {
401 Self::Single => "single",
402 Self::PlanBuildEvaluate => "plan_build_evaluate",
403 }
404 }
405
406 fn parse(value: &str) -> Option<Self> {
407 let normalized = value.trim();
408 if normalized.eq_ignore_ascii_case("single") {
409 Some(Self::Single)
410 } else if normalized.eq_ignore_ascii_case("plan_build_evaluate")
411 || normalized.eq_ignore_ascii_case("plan-build-evaluate")
412 || normalized.eq_ignore_ascii_case("planner_generator_evaluator")
413 || normalized.eq_ignore_ascii_case("planner-generator-evaluator")
414 {
415 Some(Self::PlanBuildEvaluate)
416 } else {
417 None
418 }
419 }
420}
421
422impl<'de> Deserialize<'de> for HarnessOrchestrationMode {
423 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
424 where
425 D: serde::Deserializer<'de>,
426 {
427 let raw = String::deserialize(deserializer)?;
428 Ok(Self::parse(&raw).unwrap_or_default())
429 }
430}
431
432impl Default for AgentConfig {
433 fn default() -> Self {
434 Self {
435 provider: default_provider(),
436 api_key_env: default_api_key_env(),
437 default_model: default_model(),
438 theme: default_theme(),
439 system_prompt_mode: SystemPromptMode::default(),
440 max_system_prompt_tokens: default_max_system_prompt_tokens(),
441 system_prompt_budget_warning: default_system_prompt_budget_warning(),
442 trim_system_prompt: default_trim_system_prompt(),
443 tool_documentation_mode: ToolDocumentationMode::default(),
444 shell_prompt_profile: ShellPromptProfile::default(),
445 enable_split_tool_results: default_enable_split_tool_results(),
446 todo_planning_mode: default_todo_planning_mode(),
447 ui_surface: UiSurfacePreference::default(),
448 max_conversation_turns: default_max_conversation_turns(),
449 idle_turn_limit: default_idle_turn_limit(),
450 reasoning_effort: default_reasoning_effort(),
451 allow_reasoning_effort_downgrade: false,
452 verbosity: default_verbosity(),
453 temperature: default_temperature(),
454 refine_temperature: default_refine_temperature(),
455 enable_self_review: default_enable_self_review(),
456 max_review_passes: default_max_review_passes(),
457 refine_prompts_enabled: default_refine_prompts_enabled(),
458 refine_prompts_max_passes: default_refine_max_passes(),
459 refine_prompts_model: String::new(),
460 small_model: AgentSmallModelConfig::default(),
461 prompt_suggestions: AgentPromptSuggestionsConfig::default(),
462 onboarding: AgentOnboardingConfig::default(),
463 project_doc_max_bytes: default_project_doc_max_bytes(),
464 project_doc_fallback_filenames: Vec::new(),
465 instruction_max_bytes: default_instruction_max_bytes(),
466 instruction_files: Vec::new(),
467 instruction_excludes: Vec::new(),
468 instruction_import_max_depth: default_instruction_import_max_depth(),
469 persistent_memory: PersistentMemoryConfig::default(),
470 custom_api_keys: BTreeMap::new(),
471 credential_storage_mode: crate::auth::AuthCredentialsStoreMode::default(),
472 checkpointing: AgentCheckpointingConfig::default(),
473 vibe_coding: AgentVibeCodingConfig::default(),
474 max_task_retries: default_max_task_retries(),
475 harness: AgentHarnessConfig::default(),
476 codex_app_server: AgentCodexAppServerConfig::default(),
477 include_temporal_context: default_include_temporal_context(),
478 temporal_context_use_utc: false, include_working_directory: default_include_working_directory(),
480 include_structured_reasoning_tags: None,
481 user_instructions: None,
482 require_plan_confirmation: default_require_plan_confirmation(),
483 circuit_breaker: CircuitBreakerConfig::default(),
484 open_responses: OpenResponsesConfig::default(),
485 }
486 }
487}
488
489impl AgentConfig {
490 pub fn should_include_structured_reasoning_tags(&self) -> bool {
492 self.include_structured_reasoning_tags.unwrap_or(false)
493 }
494
495 pub(crate) fn validate_llm_params(&self) -> Result<(), String> {
497 if !(0.0..=1.0).contains(&self.temperature) {
499 return Err(format!("temperature must be between 0.0 and 1.0, got {}", self.temperature));
500 }
501
502 if !(0.0..=1.0).contains(&self.refine_temperature) {
503 return Err(format!("refine_temperature must be between 0.0 and 1.0, got {}", self.refine_temperature));
504 }
505
506 if self.instruction_import_max_depth == 0 {
507 return Err("instruction_import_max_depth must be greater than 0".to_string());
508 }
509
510 if !(0.0..=1.0).contains(&self.harness.budget_warning_threshold) {
511 return Err(format!(
512 "harness.budget_warning_threshold must be between 0.0 and 1.0, got {}",
513 self.harness.budget_warning_threshold
514 ));
515 }
516
517 self.persistent_memory.validate()?;
518 self.harness.tool_result_clearing.validate()?;
519
520 Ok(())
521 }
522}
523
524#[inline]
526fn default_provider() -> String {
527 defaults::DEFAULT_PROVIDER.into()
528}
529
530#[inline]
531fn default_api_key_env() -> String {
532 defaults::DEFAULT_API_KEY_ENV.into()
533}
534
535#[inline]
536fn default_model() -> String {
537 defaults::DEFAULT_MODEL.into()
538}
539
540#[inline]
541fn default_theme() -> String {
542 defaults::DEFAULT_THEME.into()
543}
544
545#[inline]
546const fn default_todo_planning_mode() -> bool {
547 true
548}
549
550#[inline]
551const fn default_enable_split_tool_results() -> bool {
552 true }
554
555#[inline]
556const fn default_max_conversation_turns() -> usize {
557 tool_limits::DEFAULT_MAX_CONVERSATION_TURNS
558}
559
560#[inline]
561const fn default_idle_turn_limit() -> usize {
562 execution::IDLE_TURN_LIMIT
563}
564
565#[inline]
566fn default_reasoning_effort() -> ReasoningEffortLevel {
567 ReasoningEffortLevel::None
568}
569
570#[inline]
571fn default_verbosity() -> VerbosityLevel {
572 VerbosityLevel::default()
573}
574
575#[inline]
576const fn default_temperature() -> f32 {
577 llm_generation::DEFAULT_TEMPERATURE
578}
579
580#[inline]
581const fn default_refine_temperature() -> f32 {
582 llm_generation::DEFAULT_REFINE_TEMPERATURE
583}
584
585#[inline]
586const fn default_enable_self_review() -> bool {
587 false
588}
589
590#[inline]
591const fn default_max_review_passes() -> usize {
592 1
593}
594
595#[inline]
596const fn default_refine_prompts_enabled() -> bool {
597 false
598}
599
600#[inline]
601const fn default_refine_max_passes() -> usize {
602 1
603}
604
605#[inline]
606const fn default_max_system_prompt_tokens() -> u64 {
607 prompt_budget::DEFAULT_MAX_SYSTEM_PROMPT_TOKENS
608}
609
610#[inline]
611const fn default_system_prompt_budget_warning() -> bool {
612 true
613}
614
615#[inline]
616const fn default_trim_system_prompt() -> bool {
617 true
618}
619
620#[inline]
621const fn default_project_doc_max_bytes() -> usize {
622 prompt_budget::DEFAULT_MAX_BYTES
623}
624
625#[inline]
626const fn default_instruction_max_bytes() -> usize {
627 prompt_budget::DEFAULT_MAX_BYTES
628}
629
630#[inline]
631const fn default_instruction_import_max_depth() -> usize {
632 5
633}
634
635#[inline]
636const fn default_max_task_retries() -> u32 {
637 2 }
639
640#[inline]
641const fn default_include_temporal_context() -> bool {
642 true }
644
645#[inline]
646const fn default_include_working_directory() -> bool {
647 true }
649
650#[inline]
651const fn default_require_plan_confirmation() -> bool {
652 true }
654
655#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
656#[derive(Debug, Clone, Deserialize, Serialize)]
657pub struct AgentCheckpointingConfig {
658 #[serde(default = "default_checkpointing_enabled")]
660 pub enabled: bool,
661
662 #[serde(default)]
664 pub storage_dir: Option<String>,
665
666 #[serde(default = "default_checkpointing_max_snapshots")]
668 pub max_snapshots: usize,
669
670 #[serde(default = "default_checkpointing_max_age_days")]
672 pub max_age_days: Option<u64>,
673}
674
675impl Default for AgentCheckpointingConfig {
676 fn default() -> Self {
677 Self {
678 enabled: default_checkpointing_enabled(),
679 storage_dir: None,
680 max_snapshots: default_checkpointing_max_snapshots(),
681 max_age_days: default_checkpointing_max_age_days(),
682 }
683 }
684}
685
686#[inline]
687const fn default_checkpointing_enabled() -> bool {
688 DEFAULT_CHECKPOINTS_ENABLED
689}
690
691#[inline]
692const fn default_checkpointing_max_snapshots() -> usize {
693 DEFAULT_MAX_SNAPSHOTS
694}
695
696#[inline]
697const fn default_checkpointing_max_age_days() -> Option<u64> {
698 Some(DEFAULT_MAX_AGE_DAYS)
699}
700
701#[cfg(test)]
702mod tests {
703 use super::*;
704
705 #[test]
706 fn test_continuation_policy_defaults_and_parses() {
707 assert_eq!(ContinuationPolicy::default(), ContinuationPolicy::All);
708 assert_eq!(ContinuationPolicy::parse("off"), Some(ContinuationPolicy::Off));
709 assert_eq!(ContinuationPolicy::parse("exec-only"), Some(ContinuationPolicy::ExecOnly));
710 assert_eq!(ContinuationPolicy::parse("all"), Some(ContinuationPolicy::All));
711 assert_eq!(ContinuationPolicy::parse("invalid"), None);
712 }
713
714 #[test]
715 fn test_tracker_continuation_defaults_and_deserializes() {
716 assert!(TrackerContinuationConfig::default().auto_continue_tracker);
717 assert_eq!(TrackerContinuationConfig::default().cross_turn_turns, 32);
718 let parsed: AgentHarnessConfig =
719 toml::from_str("[continuation]\nauto_continue_tracker = false\ncross_turn_turns = 3")
720 .expect("valid harness config");
721 assert!(!parsed.continuation.auto_continue_tracker);
722 assert_eq!(parsed.continuation.cross_turn_turns, 3);
723 let fallback: AgentHarnessConfig = toml::from_str("").expect("empty harness config");
724 assert!(fallback.continuation.auto_continue_tracker);
725 assert_eq!(fallback.continuation.cross_turn_turns, 32);
726 }
727
728 #[test]
729 fn test_harness_config_continuation_policy_deserializes_with_fallback() {
730 let parsed: AgentHarnessConfig = toml::from_str("continuation_policy = \"all\"").expect("valid harness config");
731 assert_eq!(parsed.continuation_policy, ContinuationPolicy::All);
732
733 let fallback: AgentHarnessConfig =
734 toml::from_str("continuation_policy = \"unexpected\"").expect("fallback config");
735 assert_eq!(fallback.continuation_policy, ContinuationPolicy::All);
736 }
737
738 #[test]
739 fn test_harness_config_tool_call_budget_defaults_to_120() {
740 assert_eq!(AgentHarnessConfig::default().max_tool_calls_per_turn, tool_limits::DEFAULT_MAX_TOOL_CALLS_PER_TURN);
741
742 let parsed: AgentHarnessConfig = toml::from_str("").expect("default harness config");
743 assert_eq!(parsed.max_tool_calls_per_turn, tool_limits::DEFAULT_MAX_TOOL_CALLS_PER_TURN);
744 }
745
746 #[test]
747 fn test_harness_orchestration_mode_defaults_and_parses() {
748 assert_eq!(HarnessOrchestrationMode::default(), HarnessOrchestrationMode::PlanBuildEvaluate);
749 assert_eq!(HarnessOrchestrationMode::parse("single"), Some(HarnessOrchestrationMode::Single));
750 assert_eq!(
751 HarnessOrchestrationMode::parse("plan_build_evaluate"),
752 Some(HarnessOrchestrationMode::PlanBuildEvaluate)
753 );
754 assert_eq!(
755 HarnessOrchestrationMode::parse("planner-generator-evaluator"),
756 Some(HarnessOrchestrationMode::PlanBuildEvaluate)
757 );
758 assert_eq!(HarnessOrchestrationMode::parse("unexpected"), None);
759 }
760
761 #[test]
762 fn test_harness_config_orchestration_deserializes_with_fallback() {
763 let parsed: AgentHarnessConfig =
764 toml::from_str("orchestration_mode = \"plan_build_evaluate\"").expect("valid harness config");
765 assert_eq!(parsed.orchestration_mode, HarnessOrchestrationMode::PlanBuildEvaluate);
766 assert_eq!(parsed.max_revision_rounds, 2);
767
768 let fallback: AgentHarnessConfig =
769 toml::from_str("orchestration_mode = \"unexpected\"").expect("fallback config");
770 assert_eq!(fallback.orchestration_mode, HarnessOrchestrationMode::PlanBuildEvaluate);
771 }
772
773 #[test]
777 fn string_config_enums_keep_as_str_and_serde_in_lockstep() {
778 use crate::test_support::assert_string_enum_lockstep;
779
780 assert_string_enum_lockstep!(
781 ContinuationPolicy,
782 [
783 ContinuationPolicy::Off,
784 ContinuationPolicy::ExecOnly,
785 ContinuationPolicy::All
786 ]
787 );
788 assert_string_enum_lockstep!(
789 ContextResetMode,
790 [
791 ContextResetMode::Off,
792 ContextResetMode::OnStall,
793 ContextResetMode::OnCompaction
794 ]
795 );
796 assert_string_enum_lockstep!(
797 HarnessOrchestrationMode,
798 [
799 HarnessOrchestrationMode::Single,
800 HarnessOrchestrationMode::PlanBuildEvaluate
801 ]
802 );
803 }
804
805 #[test]
806 fn test_verification_auto_recovery_defaults_to_bounded_auto_execute() {
807 let config = VerificationAutoRecoveryConfig::default();
808 assert!(config.auto_execute);
809 assert_eq!(config.in_turn_attempts, 2);
810 assert_eq!(config.cross_turn_turns, 2);
811 assert_eq!(config.default_verifier_override, None);
812 assert_eq!(config.max_consecutive_failures, 3);
813 }
814
815 #[test]
816 fn test_verification_auto_recovery_survives_missing_field_for_backward_compatibility() {
817 let without_table: AgentHarnessConfig = toml::from_str("").expect("default harness config");
820 assert!(without_table.verification.auto_execute);
821 assert_eq!(without_table.verification.in_turn_attempts, 2);
822
823 let partial: VerificationAutoRecoveryConfig =
824 toml::from_str("auto_execute = false").expect("minimal verification config parses");
825 assert!(!partial.auto_execute);
826 assert_eq!(partial.in_turn_attempts, 2);
827 assert_eq!(partial.max_consecutive_failures, 3);
828
829 let full: VerificationAutoRecoveryConfig = toml::from_str(
830 "auto_execute = true\nin_turn_attempts = 1\ncross_turn_turns = 0\ndefault_verifier_override = \"cargo nextest run -p mycrate\"\nmax_consecutive_failures = 5",
831 )
832 .expect("full verification config parses");
833 assert_eq!(full.in_turn_attempts, 1);
834 assert_eq!(full.cross_turn_turns, 0);
835 assert_eq!(full.default_verifier_override.as_deref(), Some("cargo nextest run -p mycrate"));
836 assert_eq!(full.max_consecutive_failures, 5);
837 }
838
839 #[test]
840 fn test_plan_confirmation_config_default() {
841 let config = AgentConfig::default();
842 assert!(config.require_plan_confirmation);
843 }
844
845 #[test]
846 fn test_system_prompt_budget_defaults() {
847 let config = AgentConfig::default();
848 assert_eq!(config.max_system_prompt_tokens, prompt_budget::DEFAULT_MAX_SYSTEM_PROMPT_TOKENS);
849 assert!(config.system_prompt_budget_warning);
850 assert!(config.trim_system_prompt);
851
852 let parsed: AgentConfig = toml::from_str(
853 r#"
854max_system_prompt_tokens = 4000
855system_prompt_budget_warning = false
856trim_system_prompt = true
857"#,
858 )
859 .expect("agent config should parse");
860 assert_eq!(parsed.max_system_prompt_tokens, 4000);
861 assert!(!parsed.system_prompt_budget_warning);
862 assert!(parsed.trim_system_prompt);
863 }
864
865 #[test]
866 fn test_budget_warning_threshold_default_and_validation() {
867 let config = AgentConfig::default();
868 assert!((config.harness.budget_warning_threshold - 0.75).abs() < f64::EPSILON);
869 assert!(config.validate_llm_params().is_ok());
870
871 let parsed: AgentHarnessConfig = toml::from_str(
872 r#"
873max_budget_usd = 5.0
874budget_warning_threshold = 0.5
875"#,
876 )
877 .expect("harness config should parse");
878 assert_eq!(parsed.max_budget_usd, Some(5.0));
879 assert!((parsed.budget_warning_threshold - 0.5).abs() < f64::EPSILON);
880
881 let mut invalid = AgentConfig::default();
882 invalid.harness.budget_warning_threshold = 1.5;
883 assert!(invalid.validate_llm_params().is_err());
884 }
885
886 #[test]
887 fn test_persistent_memory_is_disabled_by_default() {
888 let config = AgentConfig::default();
889 assert!(!config.persistent_memory.enabled);
890 assert!(config.persistent_memory.auto_write);
891 }
892
893 #[test]
894 fn test_tool_result_clearing_defaults() {
895 let config = AgentConfig::default();
896 let clearing = config.harness.tool_result_clearing;
897
898 assert!(clearing.enabled);
899 assert_eq!(clearing.trigger_tokens, 40_000);
900 assert_eq!(clearing.keep_tool_uses, 2);
901 assert_eq!(clearing.keep_recent_tool_batches, 2);
902 assert_eq!(clearing.clear_at_least_tokens, 30_000);
903 assert!(clearing.clear_tool_inputs);
904 }
905
906 #[test]
907 fn test_tool_result_clearing_missing_key_defaults_clear_tool_inputs_true() {
908 let parsed: AgentHarnessConfig = toml::from_str(
909 r#"
910 [tool_result_clearing]
911 enabled = true
912 trigger_tokens = 40000
913 keep_tool_uses = 2
914 clear_at_least_tokens = 30000
915 "#,
916 )
917 .expect("valid harness config");
918
919 assert!(parsed.tool_result_clearing.clear_tool_inputs);
920 assert_eq!(parsed.tool_result_clearing.keep_recent_tool_batches, 2);
921 }
922
923 #[test]
924 fn tool_result_clearing_recent_batches_round_trip_including_compatibility() {
925 for batches in [0, 1, 2, 9] {
926 let parsed: AgentHarnessConfig =
927 toml::from_str(&format!("[tool_result_clearing]\nkeep_recent_tool_batches = {batches}")).unwrap();
928 assert!(parsed.tool_result_clearing.validate().is_ok());
929 let restored: AgentHarnessConfig = toml::from_str(&toml::to_string(&parsed).unwrap()).unwrap();
930 assert_eq!(restored.tool_result_clearing.keep_recent_tool_batches, batches);
931 }
932 }
933
934 #[test]
935 fn test_tool_result_clearing_explicit_false_opts_out_of_input_clearing() {
936 let parsed: AgentHarnessConfig = toml::from_str(
937 r#"
938 [tool_result_clearing]
939 clear_tool_inputs = false
940 "#,
941 )
942 .expect("valid harness config");
943
944 assert!(!parsed.tool_result_clearing.clear_tool_inputs);
945 }
946
947 #[test]
948 fn test_codex_app_server_experimental_features_default_to_disabled() {
949 let config = AgentConfig::default();
950
951 assert!(!config.codex_app_server.experimental_features);
952 }
953
954 #[test]
955 fn test_codex_app_server_experimental_features_parse_from_toml() {
956 let parsed: AgentCodexAppServerConfig = toml::from_str(
957 r#"
958 command = "codex"
959 args = ["app-server"]
960 startup_timeout_secs = 15
961 experimental_features = true
962 "#,
963 )
964 .expect("valid codex app-server config");
965
966 assert!(parsed.experimental_features);
967 assert_eq!(parsed.startup_timeout_secs, 15);
968 }
969
970 #[test]
971 fn test_tool_result_clearing_parses_and_validates() {
972 let parsed: AgentHarnessConfig = toml::from_str(
973 r#"
974 [tool_result_clearing]
975 enabled = true
976 trigger_tokens = 123456
977 keep_tool_uses = 6
978 clear_at_least_tokens = 4096
979 clear_tool_inputs = true
980 "#,
981 )
982 .expect("valid harness config");
983
984 assert!(parsed.tool_result_clearing.enabled);
985 assert_eq!(parsed.tool_result_clearing.trigger_tokens, 123_456);
986 assert_eq!(parsed.tool_result_clearing.keep_tool_uses, 6);
987 assert_eq!(parsed.tool_result_clearing.clear_at_least_tokens, 4_096);
988 assert!(parsed.tool_result_clearing.clear_tool_inputs);
989 assert!(parsed.tool_result_clearing.validate().is_ok());
990 }
991
992 #[test]
993 fn test_tool_result_clearing_rejects_zero_values() {
994 let clearing = ToolResultClearingConfig {
995 trigger_tokens: 0,
996 ..ToolResultClearingConfig::default()
997 };
998 assert!(clearing.validate().is_err());
999
1000 let clearing = ToolResultClearingConfig {
1001 keep_tool_uses: 0,
1002 ..ToolResultClearingConfig::default()
1003 };
1004 assert!(clearing.validate().is_err());
1005
1006 let clearing = ToolResultClearingConfig {
1007 clear_at_least_tokens: 0,
1008 ..ToolResultClearingConfig::default()
1009 };
1010 assert!(clearing.validate().is_err());
1011 }
1012
1013 #[test]
1014 fn test_structured_reasoning_is_opt_in_for_every_prompt_mode() {
1015 let default_prompt_config = AgentConfig {
1016 system_prompt_mode: SystemPromptMode::Default,
1017 ..Default::default()
1018 };
1019 assert!(!default_prompt_config.should_include_structured_reasoning_tags());
1020
1021 let specialized_mode = AgentConfig {
1022 system_prompt_mode: SystemPromptMode::Specialized,
1023 ..Default::default()
1024 };
1025 assert!(!specialized_mode.should_include_structured_reasoning_tags());
1026
1027 let minimal_mode = AgentConfig {
1028 system_prompt_mode: SystemPromptMode::Minimal,
1029 ..Default::default()
1030 };
1031 assert!(!minimal_mode.should_include_structured_reasoning_tags());
1032
1033 let lightweight_mode = AgentConfig {
1034 system_prompt_mode: SystemPromptMode::Lightweight,
1035 ..Default::default()
1036 };
1037 assert!(!lightweight_mode.should_include_structured_reasoning_tags());
1038 }
1039
1040 #[test]
1041 fn test_structured_reasoning_explicit_override() {
1042 let mut config = AgentConfig {
1043 system_prompt_mode: SystemPromptMode::Minimal,
1044 include_structured_reasoning_tags: Some(true),
1045 ..AgentConfig::default()
1046 };
1047 assert!(config.should_include_structured_reasoning_tags());
1048
1049 config.include_structured_reasoning_tags = Some(false);
1050 assert!(!config.should_include_structured_reasoning_tags());
1051 }
1052}