1use crate::constants::{defaults, llm_generation, prompt_budget};
2use crate::types::{
3 EditingMode, ReasoningEffortLevel, SystemPromptMode, ToolDocumentationMode,
4 UiSurfacePreference, 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
13#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
15#[derive(Debug, Clone, Deserialize, Serialize)]
16pub struct AgentConfig {
17 #[serde(default = "default_provider")]
19 pub provider: String,
20
21 #[serde(default = "default_api_key_env")]
23 pub api_key_env: String,
24
25 #[serde(default = "default_model")]
27 pub default_model: String,
28
29 #[serde(default = "default_theme")]
31 pub theme: String,
32
33 #[serde(default)]
37 pub system_prompt_mode: SystemPromptMode,
38
39 #[serde(default)]
45 pub tool_documentation_mode: ToolDocumentationMode,
46
47 #[serde(default = "default_enable_split_tool_results")]
54 pub enable_split_tool_results: bool,
55
56 #[serde(default = "default_todo_planning_mode")]
58 pub todo_planning_mode: bool,
59
60 #[serde(default)]
62 pub ui_surface: UiSurfacePreference,
63
64 #[serde(default = "default_max_conversation_turns")]
66 pub max_conversation_turns: usize,
67
68 #[serde(default = "default_reasoning_effort")]
71 pub reasoning_effort: ReasoningEffortLevel,
72
73 #[serde(default = "default_verbosity")]
76 pub verbosity: VerbosityLevel,
77
78 #[serde(default = "default_temperature")]
83 pub temperature: f32,
84
85 #[serde(default = "default_refine_temperature")]
89 pub refine_temperature: f32,
90
91 #[serde(default = "default_enable_self_review")]
93 pub enable_self_review: bool,
94
95 #[serde(default = "default_max_review_passes")]
97 pub max_review_passes: usize,
98
99 #[serde(default = "default_refine_prompts_enabled")]
101 pub refine_prompts_enabled: bool,
102
103 #[serde(default = "default_refine_max_passes")]
105 pub refine_prompts_max_passes: usize,
106
107 #[serde(default)]
109 pub refine_prompts_model: String,
110
111 #[serde(default)]
115 pub small_model: AgentSmallModelConfig,
116
117 #[serde(default)]
119 pub prompt_suggestions: AgentPromptSuggestionsConfig,
120
121 #[serde(default)]
123 pub onboarding: AgentOnboardingConfig,
124
125 #[serde(default = "default_project_doc_max_bytes")]
127 pub project_doc_max_bytes: usize,
128
129 #[serde(default)]
131 pub project_doc_fallback_filenames: Vec<String>,
132
133 #[serde(
135 default = "default_instruction_max_bytes",
136 alias = "rule_doc_max_bytes"
137 )]
138 pub instruction_max_bytes: usize,
139
140 #[serde(default, alias = "instruction_paths", alias = "instructions")]
142 pub instruction_files: Vec<String>,
143
144 #[serde(default)]
146 pub instruction_excludes: Vec<String>,
147
148 #[serde(default = "default_instruction_import_max_depth")]
150 pub instruction_import_max_depth: usize,
151
152 #[serde(default)]
154 pub persistent_memory: PersistentMemoryConfig,
155
156 #[serde(default, skip_serializing)]
162 pub custom_api_keys: BTreeMap<String, String>,
163
164 #[serde(default)]
171 pub credential_storage_mode: crate::auth::AuthCredentialsStoreMode,
172
173 #[serde(default)]
175 pub checkpointing: AgentCheckpointingConfig,
176
177 #[serde(default)]
179 pub vibe_coding: AgentVibeCodingConfig,
180
181 #[serde(default = "default_max_task_retries")]
185 pub max_task_retries: u32,
186
187 #[serde(default)]
189 pub harness: AgentHarnessConfig,
190
191 #[serde(default)]
193 pub codex_app_server: AgentCodexAppServerConfig,
194
195 #[serde(default = "default_include_temporal_context")]
198 pub include_temporal_context: bool,
199
200 #[serde(default)]
202 pub temporal_context_use_utc: bool,
203
204 #[serde(default = "default_include_working_directory")]
206 pub include_working_directory: bool,
207
208 #[serde(default)]
218 pub include_structured_reasoning_tags: Option<bool>,
219
220 #[serde(default)]
222 pub user_instructions: Option<String>,
223
224 #[serde(default)]
227 pub default_editing_mode: EditingMode,
228
229 #[serde(default = "default_require_plan_confirmation")]
233 pub require_plan_confirmation: bool,
234
235 #[serde(default = "default_autonomous_mode")]
239 pub autonomous_mode: bool,
240
241 #[serde(default)]
244 pub circuit_breaker: CircuitBreakerConfig,
245
246 #[serde(default)]
249 pub open_responses: OpenResponsesConfig,
250}
251
252#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
253#[cfg_attr(feature = "schema", schemars(rename_all = "snake_case"))]
254#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
255#[serde(rename_all = "snake_case")]
256pub enum ContinuationPolicy {
257 Off,
258 ExecOnly,
259 #[default]
260 All,
261}
262
263impl ContinuationPolicy {
264 pub fn as_str(&self) -> &'static str {
265 match self {
266 Self::Off => "off",
267 Self::ExecOnly => "exec_only",
268 Self::All => "all",
269 }
270 }
271
272 pub fn parse(value: &str) -> Option<Self> {
273 let normalized = value.trim();
274 if normalized.eq_ignore_ascii_case("off") {
275 Some(Self::Off)
276 } else if normalized.eq_ignore_ascii_case("exec_only")
277 || normalized.eq_ignore_ascii_case("exec-only")
278 {
279 Some(Self::ExecOnly)
280 } else if normalized.eq_ignore_ascii_case("all") {
281 Some(Self::All)
282 } else {
283 None
284 }
285 }
286}
287
288impl<'de> Deserialize<'de> for ContinuationPolicy {
289 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
290 where
291 D: serde::Deserializer<'de>,
292 {
293 let raw = String::deserialize(deserializer)?;
294 Ok(Self::parse(&raw).unwrap_or_default())
295 }
296}
297
298#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
299#[cfg_attr(feature = "schema", schemars(rename_all = "snake_case"))]
300#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
301#[serde(rename_all = "snake_case")]
302pub enum HarnessOrchestrationMode {
303 #[default]
304 Single,
305 PlanBuildEvaluate,
306}
307
308impl HarnessOrchestrationMode {
309 pub fn as_str(&self) -> &'static str {
310 match self {
311 Self::Single => "single",
312 Self::PlanBuildEvaluate => "plan_build_evaluate",
313 }
314 }
315
316 pub fn parse(value: &str) -> Option<Self> {
317 let normalized = value.trim();
318 if normalized.eq_ignore_ascii_case("single") {
319 Some(Self::Single)
320 } else if normalized.eq_ignore_ascii_case("plan_build_evaluate")
321 || normalized.eq_ignore_ascii_case("plan-build-evaluate")
322 || normalized.eq_ignore_ascii_case("planner_generator_evaluator")
323 || normalized.eq_ignore_ascii_case("planner-generator-evaluator")
324 {
325 Some(Self::PlanBuildEvaluate)
326 } else {
327 None
328 }
329 }
330}
331
332impl<'de> Deserialize<'de> for HarnessOrchestrationMode {
333 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
334 where
335 D: serde::Deserializer<'de>,
336 {
337 let raw = String::deserialize(deserializer)?;
338 Ok(Self::parse(&raw).unwrap_or_default())
339 }
340}
341
342#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
343#[derive(Debug, Clone, Deserialize, Serialize)]
344pub struct AgentHarnessConfig {
345 #[serde(default = "default_harness_max_tool_calls_per_turn")]
347 pub max_tool_calls_per_turn: usize,
348 #[serde(default = "default_harness_max_tool_wall_clock_secs")]
350 pub max_tool_wall_clock_secs: u64,
351 #[serde(default = "default_harness_max_tool_retries")]
353 pub max_tool_retries: u32,
354 #[serde(default = "default_harness_auto_compaction_enabled")]
358 pub auto_compaction_enabled: bool,
359 #[serde(default)]
363 pub auto_compaction_threshold_tokens: Option<u64>,
364 #[serde(default)]
366 pub tool_result_clearing: ToolResultClearingConfig,
367 #[serde(default)]
369 pub max_budget_usd: Option<f64>,
370 #[serde(default)]
372 pub continuation_policy: ContinuationPolicy,
373 #[serde(default)]
376 pub event_log_path: Option<String>,
377 #[serde(default)]
379 pub orchestration_mode: HarnessOrchestrationMode,
380 #[serde(default = "default_harness_max_revision_rounds")]
382 pub max_revision_rounds: usize,
383}
384
385impl Default for AgentHarnessConfig {
386 fn default() -> Self {
387 Self {
388 max_tool_calls_per_turn: default_harness_max_tool_calls_per_turn(),
389 max_tool_wall_clock_secs: default_harness_max_tool_wall_clock_secs(),
390 max_tool_retries: default_harness_max_tool_retries(),
391 auto_compaction_enabled: default_harness_auto_compaction_enabled(),
392 auto_compaction_threshold_tokens: None,
393 tool_result_clearing: ToolResultClearingConfig::default(),
394 max_budget_usd: None,
395 continuation_policy: ContinuationPolicy::default(),
396 event_log_path: None,
397 orchestration_mode: HarnessOrchestrationMode::default(),
398 max_revision_rounds: default_harness_max_revision_rounds(),
399 }
400 }
401}
402
403#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
404#[derive(Debug, Clone, Deserialize, Serialize)]
405pub struct ToolResultClearingConfig {
406 #[serde(default = "default_tool_result_clearing_enabled")]
407 pub enabled: bool,
408 #[serde(default = "default_tool_result_clearing_trigger_tokens")]
409 pub trigger_tokens: u64,
410 #[serde(default = "default_tool_result_clearing_keep_tool_uses")]
411 pub keep_tool_uses: u32,
412 #[serde(default = "default_tool_result_clearing_clear_at_least_tokens")]
413 pub clear_at_least_tokens: u64,
414 #[serde(default)]
415 pub clear_tool_inputs: bool,
416}
417
418impl Default for ToolResultClearingConfig {
419 fn default() -> Self {
420 Self {
421 enabled: default_tool_result_clearing_enabled(),
422 trigger_tokens: default_tool_result_clearing_trigger_tokens(),
423 keep_tool_uses: default_tool_result_clearing_keep_tool_uses(),
424 clear_at_least_tokens: default_tool_result_clearing_clear_at_least_tokens(),
425 clear_tool_inputs: false,
426 }
427 }
428}
429
430impl ToolResultClearingConfig {
431 pub fn validate(&self) -> Result<(), String> {
432 if self.trigger_tokens == 0 {
433 return Err("tool_result_clearing.trigger_tokens must be greater than 0".to_string());
434 }
435 if self.keep_tool_uses == 0 {
436 return Err("tool_result_clearing.keep_tool_uses must be greater than 0".to_string());
437 }
438 if self.clear_at_least_tokens == 0 {
439 return Err(
440 "tool_result_clearing.clear_at_least_tokens must be greater than 0".to_string(),
441 );
442 }
443 Ok(())
444 }
445}
446
447#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
448#[derive(Debug, Clone, Deserialize, Serialize)]
449pub struct AgentCodexAppServerConfig {
450 #[serde(default = "default_codex_app_server_command")]
452 pub command: String,
453 #[serde(default = "default_codex_app_server_args")]
455 pub args: Vec<String>,
456 #[serde(default = "default_codex_app_server_startup_timeout_secs")]
458 pub startup_timeout_secs: u64,
459 #[serde(default = "default_codex_app_server_experimental_features")]
462 pub experimental_features: bool,
463}
464
465impl Default for AgentCodexAppServerConfig {
466 fn default() -> Self {
467 Self {
468 command: default_codex_app_server_command(),
469 args: default_codex_app_server_args(),
470 startup_timeout_secs: default_codex_app_server_startup_timeout_secs(),
471 experimental_features: default_codex_app_server_experimental_features(),
472 }
473 }
474}
475
476#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
477#[derive(Debug, Clone, Deserialize, Serialize)]
478pub struct CircuitBreakerConfig {
479 #[serde(default = "default_circuit_breaker_enabled")]
481 pub enabled: bool,
482
483 #[serde(default = "default_failure_threshold")]
485 pub failure_threshold: u32,
486
487 #[serde(default = "default_pause_on_open")]
489 pub pause_on_open: bool,
490
491 #[serde(default = "default_max_open_circuits")]
493 pub max_open_circuits: usize,
494
495 #[serde(default = "default_recovery_cooldown")]
497 pub recovery_cooldown: u64,
498}
499
500impl Default for CircuitBreakerConfig {
501 fn default() -> Self {
502 Self {
503 enabled: default_circuit_breaker_enabled(),
504 failure_threshold: default_failure_threshold(),
505 pause_on_open: default_pause_on_open(),
506 max_open_circuits: default_max_open_circuits(),
507 recovery_cooldown: default_recovery_cooldown(),
508 }
509 }
510}
511
512#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
518#[derive(Debug, Clone, Deserialize, Serialize)]
519pub struct OpenResponsesConfig {
520 #[serde(default)]
524 pub enabled: bool,
525
526 #[serde(default = "default_open_responses_emit_events")]
530 pub emit_events: bool,
531
532 #[serde(default = "default_open_responses_include_extensions")]
535 pub include_extensions: bool,
536
537 #[serde(default = "default_open_responses_map_tool_calls")]
540 pub map_tool_calls: bool,
541
542 #[serde(default = "default_open_responses_include_reasoning")]
545 pub include_reasoning: bool,
546}
547
548impl Default for OpenResponsesConfig {
549 fn default() -> Self {
550 Self {
551 enabled: false, emit_events: default_open_responses_emit_events(),
553 include_extensions: default_open_responses_include_extensions(),
554 map_tool_calls: default_open_responses_map_tool_calls(),
555 include_reasoning: default_open_responses_include_reasoning(),
556 }
557 }
558}
559
560#[inline]
561const fn default_open_responses_emit_events() -> bool {
562 true }
564
565#[inline]
566const fn default_open_responses_include_extensions() -> bool {
567 true }
569
570#[inline]
571const fn default_open_responses_map_tool_calls() -> bool {
572 true }
574
575#[inline]
576const fn default_open_responses_include_reasoning() -> bool {
577 true }
579
580#[inline]
581fn default_codex_app_server_command() -> String {
582 "codex".to_string()
583}
584
585#[inline]
586fn default_codex_app_server_args() -> Vec<String> {
587 vec!["app-server".to_string()]
588}
589
590#[inline]
591const fn default_codex_app_server_startup_timeout_secs() -> u64 {
592 10
593}
594
595#[inline]
596const fn default_codex_app_server_experimental_features() -> bool {
597 false
598}
599
600impl Default for AgentConfig {
601 fn default() -> Self {
602 Self {
603 provider: default_provider(),
604 api_key_env: default_api_key_env(),
605 default_model: default_model(),
606 theme: default_theme(),
607 system_prompt_mode: SystemPromptMode::default(),
608 tool_documentation_mode: ToolDocumentationMode::default(),
609 enable_split_tool_results: default_enable_split_tool_results(),
610 todo_planning_mode: default_todo_planning_mode(),
611 ui_surface: UiSurfacePreference::default(),
612 max_conversation_turns: default_max_conversation_turns(),
613 reasoning_effort: default_reasoning_effort(),
614 verbosity: default_verbosity(),
615 temperature: default_temperature(),
616 refine_temperature: default_refine_temperature(),
617 enable_self_review: default_enable_self_review(),
618 max_review_passes: default_max_review_passes(),
619 refine_prompts_enabled: default_refine_prompts_enabled(),
620 refine_prompts_max_passes: default_refine_max_passes(),
621 refine_prompts_model: String::new(),
622 small_model: AgentSmallModelConfig::default(),
623 prompt_suggestions: AgentPromptSuggestionsConfig::default(),
624 onboarding: AgentOnboardingConfig::default(),
625 project_doc_max_bytes: default_project_doc_max_bytes(),
626 project_doc_fallback_filenames: Vec::new(),
627 instruction_max_bytes: default_instruction_max_bytes(),
628 instruction_files: Vec::new(),
629 instruction_excludes: Vec::new(),
630 instruction_import_max_depth: default_instruction_import_max_depth(),
631 persistent_memory: PersistentMemoryConfig::default(),
632 custom_api_keys: BTreeMap::new(),
633 credential_storage_mode: crate::auth::AuthCredentialsStoreMode::default(),
634 checkpointing: AgentCheckpointingConfig::default(),
635 vibe_coding: AgentVibeCodingConfig::default(),
636 max_task_retries: default_max_task_retries(),
637 harness: AgentHarnessConfig::default(),
638 codex_app_server: AgentCodexAppServerConfig::default(),
639 include_temporal_context: default_include_temporal_context(),
640 temporal_context_use_utc: false, include_working_directory: default_include_working_directory(),
642 include_structured_reasoning_tags: None,
643 user_instructions: None,
644 default_editing_mode: EditingMode::default(),
645 require_plan_confirmation: default_require_plan_confirmation(),
646 autonomous_mode: default_autonomous_mode(),
647 circuit_breaker: CircuitBreakerConfig::default(),
648 open_responses: OpenResponsesConfig::default(),
649 }
650 }
651}
652
653impl AgentConfig {
654 pub fn should_include_structured_reasoning_tags(&self) -> bool {
656 self.include_structured_reasoning_tags.unwrap_or(matches!(
657 self.system_prompt_mode,
658 SystemPromptMode::Specialized
659 ))
660 }
661
662 pub fn validate_llm_params(&self) -> Result<(), String> {
664 if !(0.0..=1.0).contains(&self.temperature) {
666 return Err(format!(
667 "temperature must be between 0.0 and 1.0, got {}",
668 self.temperature
669 ));
670 }
671
672 if !(0.0..=1.0).contains(&self.refine_temperature) {
673 return Err(format!(
674 "refine_temperature must be between 0.0 and 1.0, got {}",
675 self.refine_temperature
676 ));
677 }
678
679 if self.instruction_import_max_depth == 0 {
680 return Err("instruction_import_max_depth must be greater than 0".to_string());
681 }
682
683 self.persistent_memory.validate()?;
684 self.harness.tool_result_clearing.validate()?;
685
686 Ok(())
687 }
688}
689
690#[inline]
692fn default_provider() -> String {
693 defaults::DEFAULT_PROVIDER.into()
694}
695
696#[inline]
697fn default_api_key_env() -> String {
698 defaults::DEFAULT_API_KEY_ENV.into()
699}
700
701#[inline]
702fn default_model() -> String {
703 defaults::DEFAULT_MODEL.into()
704}
705
706#[inline]
707fn default_theme() -> String {
708 defaults::DEFAULT_THEME.into()
709}
710
711#[inline]
712const fn default_todo_planning_mode() -> bool {
713 true
714}
715
716#[inline]
717const fn default_enable_split_tool_results() -> bool {
718 true }
720
721#[inline]
722const fn default_max_conversation_turns() -> usize {
723 defaults::DEFAULT_MAX_CONVERSATION_TURNS
724}
725
726#[inline]
727fn default_reasoning_effort() -> ReasoningEffortLevel {
728 ReasoningEffortLevel::None
729}
730
731#[inline]
732fn default_verbosity() -> VerbosityLevel {
733 VerbosityLevel::default()
734}
735
736#[inline]
737const fn default_temperature() -> f32 {
738 llm_generation::DEFAULT_TEMPERATURE
739}
740
741#[inline]
742const fn default_refine_temperature() -> f32 {
743 llm_generation::DEFAULT_REFINE_TEMPERATURE
744}
745
746#[inline]
747const fn default_enable_self_review() -> bool {
748 false
749}
750
751#[inline]
752const fn default_max_review_passes() -> usize {
753 1
754}
755
756#[inline]
757const fn default_refine_prompts_enabled() -> bool {
758 false
759}
760
761#[inline]
762const fn default_refine_max_passes() -> usize {
763 1
764}
765
766#[inline]
767const fn default_project_doc_max_bytes() -> usize {
768 prompt_budget::DEFAULT_MAX_BYTES
769}
770
771#[inline]
772const fn default_instruction_max_bytes() -> usize {
773 prompt_budget::DEFAULT_MAX_BYTES
774}
775
776#[inline]
777const fn default_instruction_import_max_depth() -> usize {
778 5
779}
780
781#[inline]
782const fn default_max_task_retries() -> u32 {
783 2 }
785
786#[inline]
787const fn default_harness_max_tool_calls_per_turn() -> usize {
788 defaults::DEFAULT_MAX_TOOL_CALLS_PER_TURN
789}
790
791#[inline]
792const fn default_harness_max_tool_wall_clock_secs() -> u64 {
793 defaults::DEFAULT_MAX_TOOL_WALL_CLOCK_SECS
794}
795
796#[inline]
797const fn default_harness_max_tool_retries() -> u32 {
798 defaults::DEFAULT_MAX_TOOL_RETRIES
799}
800
801#[inline]
802const fn default_harness_auto_compaction_enabled() -> bool {
803 false
804}
805
806#[inline]
807const fn default_tool_result_clearing_enabled() -> bool {
808 false
809}
810
811#[inline]
812const fn default_tool_result_clearing_trigger_tokens() -> u64 {
813 100_000
814}
815
816#[inline]
817const fn default_tool_result_clearing_keep_tool_uses() -> u32 {
818 3
819}
820
821#[inline]
822const fn default_tool_result_clearing_clear_at_least_tokens() -> u64 {
823 30_000
824}
825
826#[inline]
827const fn default_harness_max_revision_rounds() -> usize {
828 2
829}
830
831#[inline]
832const fn default_include_temporal_context() -> bool {
833 true }
835
836#[inline]
837const fn default_include_working_directory() -> bool {
838 true }
840
841#[inline]
842const fn default_require_plan_confirmation() -> bool {
843 true }
845
846#[inline]
847const fn default_autonomous_mode() -> bool {
848 false }
850
851#[inline]
852const fn default_circuit_breaker_enabled() -> bool {
853 true }
855
856#[inline]
857const fn default_failure_threshold() -> u32 {
858 7 }
860
861#[inline]
862const fn default_pause_on_open() -> bool {
863 true }
865
866#[inline]
867const fn default_max_open_circuits() -> usize {
868 3 }
870
871#[inline]
872const fn default_recovery_cooldown() -> u64 {
873 60 }
875
876#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
877#[derive(Debug, Clone, Deserialize, Serialize)]
878pub struct AgentCheckpointingConfig {
879 #[serde(default = "default_checkpointing_enabled")]
881 pub enabled: bool,
882
883 #[serde(default)]
885 pub storage_dir: Option<String>,
886
887 #[serde(default = "default_checkpointing_max_snapshots")]
889 pub max_snapshots: usize,
890
891 #[serde(default = "default_checkpointing_max_age_days")]
893 pub max_age_days: Option<u64>,
894}
895
896impl Default for AgentCheckpointingConfig {
897 fn default() -> Self {
898 Self {
899 enabled: default_checkpointing_enabled(),
900 storage_dir: None,
901 max_snapshots: default_checkpointing_max_snapshots(),
902 max_age_days: default_checkpointing_max_age_days(),
903 }
904 }
905}
906
907#[inline]
908const fn default_checkpointing_enabled() -> bool {
909 DEFAULT_CHECKPOINTS_ENABLED
910}
911
912#[inline]
913const fn default_checkpointing_max_snapshots() -> usize {
914 DEFAULT_MAX_SNAPSHOTS
915}
916
917#[inline]
918const fn default_checkpointing_max_age_days() -> Option<u64> {
919 Some(DEFAULT_MAX_AGE_DAYS)
920}
921
922#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
923#[derive(Debug, Clone, Deserialize, Serialize)]
924pub struct PersistentMemoryConfig {
925 #[serde(default = "default_persistent_memory_enabled")]
927 pub enabled: bool,
928
929 #[serde(default = "default_persistent_memory_auto_write")]
931 pub auto_write: bool,
932
933 #[serde(default)]
935 pub directory_override: Option<String>,
936
937 #[serde(default = "default_persistent_memory_startup_line_limit")]
939 pub startup_line_limit: usize,
940
941 #[serde(default = "default_persistent_memory_startup_byte_limit")]
943 pub startup_byte_limit: usize,
944}
945
946impl Default for PersistentMemoryConfig {
947 fn default() -> Self {
948 Self {
949 enabled: default_persistent_memory_enabled(),
950 auto_write: default_persistent_memory_auto_write(),
951 directory_override: None,
952 startup_line_limit: default_persistent_memory_startup_line_limit(),
953 startup_byte_limit: default_persistent_memory_startup_byte_limit(),
954 }
955 }
956}
957
958impl PersistentMemoryConfig {
959 pub fn validate(&self) -> Result<(), String> {
960 if self.startup_line_limit == 0 {
961 return Err("persistent_memory.startup_line_limit must be greater than 0".to_string());
962 }
963
964 if self.startup_byte_limit == 0 {
965 return Err("persistent_memory.startup_byte_limit must be greater than 0".to_string());
966 }
967
968 Ok(())
969 }
970}
971
972#[inline]
973const fn default_persistent_memory_enabled() -> bool {
974 false
975}
976
977#[inline]
978const fn default_persistent_memory_auto_write() -> bool {
979 true
980}
981
982#[inline]
983const fn default_persistent_memory_startup_line_limit() -> usize {
984 200
985}
986
987#[inline]
988const fn default_persistent_memory_startup_byte_limit() -> usize {
989 25 * 1024
990}
991
992#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
993#[derive(Debug, Clone, Deserialize, Serialize)]
994pub struct AgentOnboardingConfig {
995 #[serde(default = "default_onboarding_enabled")]
997 pub enabled: bool,
998
999 #[serde(default = "default_intro_text")]
1001 pub intro_text: String,
1002
1003 #[serde(default = "default_show_project_overview")]
1005 pub include_project_overview: bool,
1006
1007 #[serde(default = "default_show_language_summary")]
1009 pub include_language_summary: bool,
1010
1011 #[serde(default = "default_show_guideline_highlights")]
1013 pub include_guideline_highlights: bool,
1014
1015 #[serde(default = "default_show_usage_tips_in_welcome")]
1017 pub include_usage_tips_in_welcome: bool,
1018
1019 #[serde(default = "default_show_recommended_actions_in_welcome")]
1021 pub include_recommended_actions_in_welcome: bool,
1022
1023 #[serde(default = "default_guideline_highlight_limit")]
1025 pub guideline_highlight_limit: usize,
1026
1027 #[serde(default = "default_usage_tips")]
1029 pub usage_tips: Vec<String>,
1030
1031 #[serde(default = "default_recommended_actions")]
1033 pub recommended_actions: Vec<String>,
1034
1035 #[serde(default)]
1037 pub chat_placeholder: Option<String>,
1038}
1039
1040impl Default for AgentOnboardingConfig {
1041 fn default() -> Self {
1042 Self {
1043 enabled: default_onboarding_enabled(),
1044 intro_text: default_intro_text(),
1045 include_project_overview: default_show_project_overview(),
1046 include_language_summary: default_show_language_summary(),
1047 include_guideline_highlights: default_show_guideline_highlights(),
1048 include_usage_tips_in_welcome: default_show_usage_tips_in_welcome(),
1049 include_recommended_actions_in_welcome: default_show_recommended_actions_in_welcome(),
1050 guideline_highlight_limit: default_guideline_highlight_limit(),
1051 usage_tips: default_usage_tips(),
1052 recommended_actions: default_recommended_actions(),
1053 chat_placeholder: None,
1054 }
1055 }
1056}
1057
1058#[inline]
1059const fn default_onboarding_enabled() -> bool {
1060 true
1061}
1062
1063const DEFAULT_INTRO_TEXT: &str =
1064 "Let's get oriented. I preloaded workspace context so we can move fast.";
1065
1066#[inline]
1067fn default_intro_text() -> String {
1068 DEFAULT_INTRO_TEXT.into()
1069}
1070
1071#[inline]
1072const fn default_show_project_overview() -> bool {
1073 true
1074}
1075
1076#[inline]
1077const fn default_show_language_summary() -> bool {
1078 false
1079}
1080
1081#[inline]
1082const fn default_show_guideline_highlights() -> bool {
1083 true
1084}
1085
1086#[inline]
1087const fn default_show_usage_tips_in_welcome() -> bool {
1088 false
1089}
1090
1091#[inline]
1092const fn default_show_recommended_actions_in_welcome() -> bool {
1093 false
1094}
1095
1096#[inline]
1097const fn default_guideline_highlight_limit() -> usize {
1098 3
1099}
1100
1101const DEFAULT_USAGE_TIPS: &[&str] = &[
1102 "Describe your current coding goal or ask for a quick status overview.",
1103 "Reference AGENTS.md guidelines when proposing changes.",
1104 "Prefer asking for targeted file reads or diffs before editing.",
1105];
1106
1107const DEFAULT_RECOMMENDED_ACTIONS: &[&str] = &[
1108 "Review the highlighted guidelines and share the task you want to tackle.",
1109 "Ask for a workspace tour if you need more context.",
1110];
1111
1112fn default_usage_tips() -> Vec<String> {
1113 DEFAULT_USAGE_TIPS.iter().map(|s| (*s).into()).collect()
1114}
1115
1116fn default_recommended_actions() -> Vec<String> {
1117 DEFAULT_RECOMMENDED_ACTIONS
1118 .iter()
1119 .map(|s| (*s).into())
1120 .collect()
1121}
1122
1123#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1133#[derive(Debug, Clone, Deserialize, Serialize)]
1134pub struct AgentSmallModelConfig {
1135 #[serde(default = "default_small_model_enabled")]
1137 pub enabled: bool,
1138
1139 #[serde(default)]
1142 pub model: String,
1143
1144 #[serde(default = "default_small_model_temperature")]
1146 pub temperature: f32,
1147
1148 #[serde(default = "default_small_model_for_large_reads")]
1150 pub use_for_large_reads: bool,
1151
1152 #[serde(default = "default_small_model_for_web_summary")]
1154 pub use_for_web_summary: bool,
1155
1156 #[serde(default = "default_small_model_for_git_history")]
1158 pub use_for_git_history: bool,
1159
1160 #[serde(default = "default_small_model_for_memory")]
1162 pub use_for_memory: bool,
1163}
1164
1165impl Default for AgentSmallModelConfig {
1166 fn default() -> Self {
1167 Self {
1168 enabled: default_small_model_enabled(),
1169 model: String::new(),
1170 temperature: default_small_model_temperature(),
1171 use_for_large_reads: default_small_model_for_large_reads(),
1172 use_for_web_summary: default_small_model_for_web_summary(),
1173 use_for_git_history: default_small_model_for_git_history(),
1174 use_for_memory: default_small_model_for_memory(),
1175 }
1176 }
1177}
1178
1179#[inline]
1180const fn default_small_model_enabled() -> bool {
1181 true }
1183
1184#[inline]
1185const fn default_small_model_temperature() -> f32 {
1186 0.3 }
1188
1189#[inline]
1190const fn default_small_model_for_large_reads() -> bool {
1191 true
1192}
1193
1194#[inline]
1195const fn default_small_model_for_web_summary() -> bool {
1196 true
1197}
1198
1199#[inline]
1200const fn default_small_model_for_git_history() -> bool {
1201 true
1202}
1203
1204#[inline]
1205const fn default_small_model_for_memory() -> bool {
1206 true
1207}
1208
1209#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1211#[derive(Debug, Clone, Deserialize, Serialize)]
1212pub struct AgentPromptSuggestionsConfig {
1213 #[serde(default = "default_prompt_suggestions_enabled")]
1215 pub enabled: bool,
1216
1217 #[serde(default)]
1220 pub model: String,
1221
1222 #[serde(default = "default_prompt_suggestions_temperature")]
1224 pub temperature: f32,
1225
1226 #[serde(default = "default_prompt_suggestions_show_cost_notice")]
1228 pub show_cost_notice: bool,
1229}
1230
1231impl Default for AgentPromptSuggestionsConfig {
1232 fn default() -> Self {
1233 Self {
1234 enabled: default_prompt_suggestions_enabled(),
1235 model: String::new(),
1236 temperature: default_prompt_suggestions_temperature(),
1237 show_cost_notice: default_prompt_suggestions_show_cost_notice(),
1238 }
1239 }
1240}
1241
1242#[inline]
1243const fn default_prompt_suggestions_enabled() -> bool {
1244 true
1245}
1246
1247#[inline]
1248const fn default_prompt_suggestions_temperature() -> f32 {
1249 0.3
1250}
1251
1252#[inline]
1253const fn default_prompt_suggestions_show_cost_notice() -> bool {
1254 true
1255}
1256
1257#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1262#[derive(Debug, Clone, Deserialize, Serialize)]
1263pub struct AgentVibeCodingConfig {
1264 #[serde(default = "default_vibe_coding_enabled")]
1266 pub enabled: bool,
1267
1268 #[serde(default = "default_vibe_min_prompt_length")]
1270 pub min_prompt_length: usize,
1271
1272 #[serde(default = "default_vibe_min_prompt_words")]
1274 pub min_prompt_words: usize,
1275
1276 #[serde(default = "default_vibe_entity_resolution")]
1278 pub enable_entity_resolution: bool,
1279
1280 #[serde(default = "default_vibe_entity_cache")]
1282 pub entity_index_cache: String,
1283
1284 #[serde(default = "default_vibe_max_entity_matches")]
1286 pub max_entity_matches: usize,
1287
1288 #[serde(default = "default_vibe_track_workspace")]
1290 pub track_workspace_state: bool,
1291
1292 #[serde(default = "default_vibe_max_recent_files")]
1294 pub max_recent_files: usize,
1295
1296 #[serde(default = "default_vibe_track_values")]
1298 pub track_value_history: bool,
1299
1300 #[serde(default = "default_vibe_conversation_memory")]
1302 pub enable_conversation_memory: bool,
1303
1304 #[serde(default = "default_vibe_max_memory_turns")]
1306 pub max_memory_turns: usize,
1307
1308 #[serde(default = "default_vibe_pronoun_resolution")]
1310 pub enable_pronoun_resolution: bool,
1311
1312 #[serde(default = "default_vibe_proactive_context")]
1314 pub enable_proactive_context: bool,
1315
1316 #[serde(default = "default_vibe_max_context_files")]
1318 pub max_context_files: usize,
1319
1320 #[serde(default = "default_vibe_max_snippets_per_file")]
1322 pub max_context_snippets_per_file: usize,
1323
1324 #[serde(default = "default_vibe_max_search_results")]
1326 pub max_search_results: usize,
1327
1328 #[serde(default = "default_vibe_value_inference")]
1330 pub enable_relative_value_inference: bool,
1331}
1332
1333impl Default for AgentVibeCodingConfig {
1334 fn default() -> Self {
1335 Self {
1336 enabled: default_vibe_coding_enabled(),
1337 min_prompt_length: default_vibe_min_prompt_length(),
1338 min_prompt_words: default_vibe_min_prompt_words(),
1339 enable_entity_resolution: default_vibe_entity_resolution(),
1340 entity_index_cache: default_vibe_entity_cache(),
1341 max_entity_matches: default_vibe_max_entity_matches(),
1342 track_workspace_state: default_vibe_track_workspace(),
1343 max_recent_files: default_vibe_max_recent_files(),
1344 track_value_history: default_vibe_track_values(),
1345 enable_conversation_memory: default_vibe_conversation_memory(),
1346 max_memory_turns: default_vibe_max_memory_turns(),
1347 enable_pronoun_resolution: default_vibe_pronoun_resolution(),
1348 enable_proactive_context: default_vibe_proactive_context(),
1349 max_context_files: default_vibe_max_context_files(),
1350 max_context_snippets_per_file: default_vibe_max_snippets_per_file(),
1351 max_search_results: default_vibe_max_search_results(),
1352 enable_relative_value_inference: default_vibe_value_inference(),
1353 }
1354 }
1355}
1356
1357#[inline]
1359const fn default_vibe_coding_enabled() -> bool {
1360 false }
1362
1363#[inline]
1364const fn default_vibe_min_prompt_length() -> usize {
1365 5
1366}
1367
1368#[inline]
1369const fn default_vibe_min_prompt_words() -> usize {
1370 2
1371}
1372
1373#[inline]
1374const fn default_vibe_entity_resolution() -> bool {
1375 true
1376}
1377
1378#[inline]
1379fn default_vibe_entity_cache() -> String {
1380 ".vtcode/entity_index.json".into()
1381}
1382
1383#[inline]
1384const fn default_vibe_max_entity_matches() -> usize {
1385 5
1386}
1387
1388#[inline]
1389const fn default_vibe_track_workspace() -> bool {
1390 true
1391}
1392
1393#[inline]
1394const fn default_vibe_max_recent_files() -> usize {
1395 20
1396}
1397
1398#[inline]
1399const fn default_vibe_track_values() -> bool {
1400 true
1401}
1402
1403#[inline]
1404const fn default_vibe_conversation_memory() -> bool {
1405 true
1406}
1407
1408#[inline]
1409const fn default_vibe_max_memory_turns() -> usize {
1410 50
1411}
1412
1413#[inline]
1414const fn default_vibe_pronoun_resolution() -> bool {
1415 true
1416}
1417
1418#[inline]
1419const fn default_vibe_proactive_context() -> bool {
1420 true
1421}
1422
1423#[inline]
1424const fn default_vibe_max_context_files() -> usize {
1425 3
1426}
1427
1428#[inline]
1429const fn default_vibe_max_snippets_per_file() -> usize {
1430 20
1431}
1432
1433#[inline]
1434const fn default_vibe_max_search_results() -> usize {
1435 5
1436}
1437
1438#[inline]
1439const fn default_vibe_value_inference() -> bool {
1440 true
1441}
1442
1443#[cfg(test)]
1444mod tests {
1445 use super::*;
1446
1447 #[test]
1448 fn test_continuation_policy_defaults_and_parses() {
1449 assert_eq!(ContinuationPolicy::default(), ContinuationPolicy::All);
1450 assert_eq!(
1451 ContinuationPolicy::parse("off"),
1452 Some(ContinuationPolicy::Off)
1453 );
1454 assert_eq!(
1455 ContinuationPolicy::parse("exec-only"),
1456 Some(ContinuationPolicy::ExecOnly)
1457 );
1458 assert_eq!(
1459 ContinuationPolicy::parse("all"),
1460 Some(ContinuationPolicy::All)
1461 );
1462 assert_eq!(ContinuationPolicy::parse("invalid"), None);
1463 }
1464
1465 #[test]
1466 fn test_harness_config_continuation_policy_deserializes_with_fallback() {
1467 let parsed: AgentHarnessConfig =
1468 toml::from_str("continuation_policy = \"all\"").expect("valid harness config");
1469 assert_eq!(parsed.continuation_policy, ContinuationPolicy::All);
1470
1471 let fallback: AgentHarnessConfig =
1472 toml::from_str("continuation_policy = \"unexpected\"").expect("fallback config");
1473 assert_eq!(fallback.continuation_policy, ContinuationPolicy::All);
1474 }
1475
1476 #[test]
1477 fn test_harness_orchestration_mode_defaults_and_parses() {
1478 assert_eq!(
1479 HarnessOrchestrationMode::default(),
1480 HarnessOrchestrationMode::Single
1481 );
1482 assert_eq!(
1483 HarnessOrchestrationMode::parse("single"),
1484 Some(HarnessOrchestrationMode::Single)
1485 );
1486 assert_eq!(
1487 HarnessOrchestrationMode::parse("plan_build_evaluate"),
1488 Some(HarnessOrchestrationMode::PlanBuildEvaluate)
1489 );
1490 assert_eq!(
1491 HarnessOrchestrationMode::parse("planner-generator-evaluator"),
1492 Some(HarnessOrchestrationMode::PlanBuildEvaluate)
1493 );
1494 assert_eq!(HarnessOrchestrationMode::parse("unexpected"), None);
1495 }
1496
1497 #[test]
1498 fn test_harness_config_orchestration_deserializes_with_fallback() {
1499 let parsed: AgentHarnessConfig =
1500 toml::from_str("orchestration_mode = \"plan_build_evaluate\"")
1501 .expect("valid harness config");
1502 assert_eq!(
1503 parsed.orchestration_mode,
1504 HarnessOrchestrationMode::PlanBuildEvaluate
1505 );
1506 assert_eq!(parsed.max_revision_rounds, 2);
1507
1508 let fallback: AgentHarnessConfig =
1509 toml::from_str("orchestration_mode = \"unexpected\"").expect("fallback config");
1510 assert_eq!(
1511 fallback.orchestration_mode,
1512 HarnessOrchestrationMode::Single
1513 );
1514 }
1515
1516 #[test]
1517 fn test_editing_mode_config_default() {
1518 let config = AgentConfig::default();
1519 assert_eq!(config.default_editing_mode, EditingMode::Edit);
1520 assert!(config.require_plan_confirmation);
1521 assert!(!config.autonomous_mode);
1522 }
1523
1524 #[test]
1525 fn test_persistent_memory_is_disabled_by_default() {
1526 let config = AgentConfig::default();
1527 assert!(!config.persistent_memory.enabled);
1528 assert!(config.persistent_memory.auto_write);
1529 }
1530
1531 #[test]
1532 fn test_tool_result_clearing_defaults() {
1533 let config = AgentConfig::default();
1534 let clearing = config.harness.tool_result_clearing;
1535
1536 assert!(!clearing.enabled);
1537 assert_eq!(clearing.trigger_tokens, 100_000);
1538 assert_eq!(clearing.keep_tool_uses, 3);
1539 assert_eq!(clearing.clear_at_least_tokens, 30_000);
1540 assert!(!clearing.clear_tool_inputs);
1541 }
1542
1543 #[test]
1544 fn test_codex_app_server_experimental_features_default_to_disabled() {
1545 let config = AgentConfig::default();
1546
1547 assert!(!config.codex_app_server.experimental_features);
1548 }
1549
1550 #[test]
1551 fn test_codex_app_server_experimental_features_parse_from_toml() {
1552 let parsed: AgentCodexAppServerConfig = toml::from_str(
1553 r#"
1554 command = "codex"
1555 args = ["app-server"]
1556 startup_timeout_secs = 15
1557 experimental_features = true
1558 "#,
1559 )
1560 .expect("valid codex app-server config");
1561
1562 assert!(parsed.experimental_features);
1563 assert_eq!(parsed.startup_timeout_secs, 15);
1564 }
1565
1566 #[test]
1567 fn test_tool_result_clearing_parses_and_validates() {
1568 let parsed: AgentHarnessConfig = toml::from_str(
1569 r#"
1570 [tool_result_clearing]
1571 enabled = true
1572 trigger_tokens = 123456
1573 keep_tool_uses = 6
1574 clear_at_least_tokens = 4096
1575 clear_tool_inputs = true
1576 "#,
1577 )
1578 .expect("valid harness config");
1579
1580 assert!(parsed.tool_result_clearing.enabled);
1581 assert_eq!(parsed.tool_result_clearing.trigger_tokens, 123_456);
1582 assert_eq!(parsed.tool_result_clearing.keep_tool_uses, 6);
1583 assert_eq!(parsed.tool_result_clearing.clear_at_least_tokens, 4_096);
1584 assert!(parsed.tool_result_clearing.clear_tool_inputs);
1585 assert!(parsed.tool_result_clearing.validate().is_ok());
1586 }
1587
1588 #[test]
1589 fn test_tool_result_clearing_rejects_zero_values() {
1590 let clearing = ToolResultClearingConfig {
1591 trigger_tokens: 0,
1592 ..ToolResultClearingConfig::default()
1593 };
1594 assert!(clearing.validate().is_err());
1595
1596 let clearing = ToolResultClearingConfig {
1597 keep_tool_uses: 0,
1598 ..ToolResultClearingConfig::default()
1599 };
1600 assert!(clearing.validate().is_err());
1601
1602 let clearing = ToolResultClearingConfig {
1603 clear_at_least_tokens: 0,
1604 ..ToolResultClearingConfig::default()
1605 };
1606 assert!(clearing.validate().is_err());
1607 }
1608
1609 #[test]
1610 fn test_structured_reasoning_defaults_follow_prompt_mode() {
1611 let default_mode = AgentConfig {
1612 system_prompt_mode: SystemPromptMode::Default,
1613 ..Default::default()
1614 };
1615 assert!(!default_mode.should_include_structured_reasoning_tags());
1616
1617 let specialized_mode = AgentConfig {
1618 system_prompt_mode: SystemPromptMode::Specialized,
1619 ..Default::default()
1620 };
1621 assert!(specialized_mode.should_include_structured_reasoning_tags());
1622
1623 let minimal_mode = AgentConfig {
1624 system_prompt_mode: SystemPromptMode::Minimal,
1625 ..Default::default()
1626 };
1627 assert!(!minimal_mode.should_include_structured_reasoning_tags());
1628
1629 let lightweight_mode = AgentConfig {
1630 system_prompt_mode: SystemPromptMode::Lightweight,
1631 ..Default::default()
1632 };
1633 assert!(!lightweight_mode.should_include_structured_reasoning_tags());
1634 }
1635
1636 #[test]
1637 fn test_structured_reasoning_explicit_override() {
1638 let mut config = AgentConfig {
1639 system_prompt_mode: SystemPromptMode::Minimal,
1640 include_structured_reasoning_tags: Some(true),
1641 ..AgentConfig::default()
1642 };
1643 assert!(config.should_include_structured_reasoning_tags());
1644
1645 config.include_structured_reasoning_tags = Some(false);
1646 assert!(!config.should_include_structured_reasoning_tags());
1647 }
1648}