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