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")]
175 pub project_doc_max_bytes: usize,
176
177 #[serde(default)]
179 pub project_doc_fallback_filenames: Vec<String>,
180
181 #[serde(default = "default_instruction_max_bytes", alias = "rule_doc_max_bytes")]
183 pub instruction_max_bytes: usize,
184
185 #[serde(default, alias = "instruction_paths", alias = "instructions")]
187 pub instruction_files: Vec<String>,
188
189 #[serde(default)]
191 pub instruction_excludes: Vec<String>,
192
193 #[serde(default = "default_instruction_import_max_depth")]
195 pub instruction_import_max_depth: usize,
196
197 #[serde(default)]
199 pub persistent_memory: PersistentMemoryConfig,
200
201 #[serde(default, skip_serializing)]
209 pub custom_api_keys: BTreeMap<String, String>,
210
211 #[serde(default)]
218 pub credential_storage_mode: crate::auth::AuthCredentialsStoreMode,
219
220 #[serde(default)]
222 pub checkpointing: AgentCheckpointingConfig,
223
224 #[serde(default)]
226 pub vibe_coding: AgentVibeCodingConfig,
227
228 #[serde(default = "default_max_task_retries")]
232 pub max_task_retries: u32,
233
234 #[serde(default)]
236 pub harness: AgentHarnessConfig,
237
238 #[serde(default)]
240 pub codex_app_server: AgentCodexAppServerConfig,
241
242 #[serde(default = "default_include_temporal_context")]
245 pub include_temporal_context: bool,
246
247 #[serde(default)]
249 pub temporal_context_use_utc: bool,
250
251 #[serde(default = "default_include_working_directory")]
253 pub include_working_directory: bool,
254
255 #[serde(default)]
265 pub include_structured_reasoning_tags: Option<bool>,
266
267 #[serde(default)]
269 pub user_instructions: Option<String>,
270
271 #[serde(default = "default_require_plan_confirmation")]
275 pub require_plan_confirmation: bool,
276
277 #[serde(default)]
280 pub circuit_breaker: CircuitBreakerConfig,
281
282 #[serde(default)]
285 pub open_responses: OpenResponsesConfig,
286}
287
288#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
289#[cfg_attr(feature = "schema", schemars(rename_all = "snake_case"))]
290#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
291#[serde(rename_all = "snake_case")]
292pub enum ContinuationPolicy {
293 Off,
294 ExecOnly,
295 #[default]
296 All,
297}
298
299impl ContinuationPolicy {
300 pub fn as_str(&self) -> &'static str {
301 match self {
302 Self::Off => "off",
303 Self::ExecOnly => "exec_only",
304 Self::All => "all",
305 }
306 }
307
308 fn parse(value: &str) -> Option<Self> {
309 let normalized = value.trim();
310 if normalized.eq_ignore_ascii_case("off") {
311 Some(Self::Off)
312 } else if normalized.eq_ignore_ascii_case("exec_only") || normalized.eq_ignore_ascii_case("exec-only") {
313 Some(Self::ExecOnly)
314 } else if normalized.eq_ignore_ascii_case("all") {
315 Some(Self::All)
316 } else {
317 None
318 }
319 }
320}
321
322impl<'de> Deserialize<'de> for ContinuationPolicy {
323 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
324 where
325 D: serde::Deserializer<'de>,
326 {
327 let raw = String::deserialize(deserializer)?;
328 Ok(Self::parse(&raw).unwrap_or_default())
329 }
330}
331
332#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
343#[cfg_attr(feature = "schema", schemars(rename_all = "snake_case"))]
344#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
345#[serde(rename_all = "snake_case")]
346pub enum ContextResetMode {
347 #[default]
349 Off,
350 OnStall,
353 OnCompaction,
356}
357
358impl ContextResetMode {
359 pub fn as_str(&self) -> &'static str {
360 match self {
361 Self::Off => "off",
362 Self::OnStall => "on_stall",
363 Self::OnCompaction => "on_compaction",
364 }
365 }
366
367 fn parse(value: &str) -> Option<Self> {
368 let normalized = value.trim();
369 if normalized.eq_ignore_ascii_case("off") {
370 Some(Self::Off)
371 } else if normalized.eq_ignore_ascii_case("on_stall") || normalized.eq_ignore_ascii_case("on-stall") {
372 Some(Self::OnStall)
373 } else if normalized.eq_ignore_ascii_case("on_compaction") || normalized.eq_ignore_ascii_case("on-compaction") {
374 Some(Self::OnCompaction)
375 } else {
376 None
377 }
378 }
379}
380
381impl<'de> Deserialize<'de> for ContextResetMode {
382 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
383 where
384 D: serde::Deserializer<'de>,
385 {
386 let raw = String::deserialize(deserializer)?;
387 Ok(Self::parse(&raw).unwrap_or_default())
388 }
389}
390
391#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
392#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
393#[serde(rename_all = "snake_case")]
394pub enum HarnessOrchestrationMode {
395 #[default]
396 PlanBuildEvaluate,
397 Single,
398}
399
400impl HarnessOrchestrationMode {
401 pub fn as_str(&self) -> &'static str {
402 match self {
403 Self::Single => "single",
404 Self::PlanBuildEvaluate => "plan_build_evaluate",
405 }
406 }
407
408 fn parse(value: &str) -> Option<Self> {
409 let normalized = value.trim();
410 if normalized.eq_ignore_ascii_case("single") {
411 Some(Self::Single)
412 } else if normalized.eq_ignore_ascii_case("plan_build_evaluate")
413 || normalized.eq_ignore_ascii_case("plan-build-evaluate")
414 || normalized.eq_ignore_ascii_case("planner_generator_evaluator")
415 || normalized.eq_ignore_ascii_case("planner-generator-evaluator")
416 {
417 Some(Self::PlanBuildEvaluate)
418 } else {
419 None
420 }
421 }
422}
423
424impl<'de> Deserialize<'de> for HarnessOrchestrationMode {
425 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
426 where
427 D: serde::Deserializer<'de>,
428 {
429 let raw = String::deserialize(deserializer)?;
430 Ok(Self::parse(&raw).unwrap_or_default())
431 }
432}
433
434impl Default for AgentConfig {
435 fn default() -> Self {
436 Self {
437 provider: default_provider(),
438 api_key_env: default_api_key_env(),
439 default_model: default_model(),
440 theme: default_theme(),
441 system_prompt_mode: SystemPromptMode::default(),
442 max_system_prompt_tokens: default_max_system_prompt_tokens(),
443 system_prompt_budget_warning: default_system_prompt_budget_warning(),
444 trim_system_prompt: default_trim_system_prompt(),
445 tool_documentation_mode: ToolDocumentationMode::default(),
446 shell_prompt_profile: ShellPromptProfile::default(),
447 enable_split_tool_results: default_enable_split_tool_results(),
448 todo_planning_mode: default_todo_planning_mode(),
449 ui_surface: UiSurfacePreference::default(),
450 max_conversation_turns: default_max_conversation_turns(),
451 idle_turn_limit: default_idle_turn_limit(),
452 reasoning_effort: default_reasoning_effort(),
453 allow_reasoning_effort_downgrade: false,
454 verbosity: default_verbosity(),
455 temperature: default_temperature(),
456 refine_temperature: default_refine_temperature(),
457 enable_self_review: default_enable_self_review(),
458 max_review_passes: default_max_review_passes(),
459 refine_prompts_enabled: default_refine_prompts_enabled(),
460 refine_prompts_max_passes: default_refine_max_passes(),
461 refine_prompts_model: String::new(),
462 small_model: AgentSmallModelConfig::default(),
463 prompt_suggestions: AgentPromptSuggestionsConfig::default(),
464 onboarding: AgentOnboardingConfig::default(),
465 project_doc_max_bytes: default_project_doc_max_bytes(),
466 project_doc_fallback_filenames: Vec::new(),
467 instruction_max_bytes: default_instruction_max_bytes(),
468 instruction_files: Vec::new(),
469 instruction_excludes: Vec::new(),
470 instruction_import_max_depth: default_instruction_import_max_depth(),
471 persistent_memory: PersistentMemoryConfig::default(),
472 custom_api_keys: BTreeMap::new(),
473 credential_storage_mode: crate::auth::AuthCredentialsStoreMode::default(),
474 checkpointing: AgentCheckpointingConfig::default(),
475 vibe_coding: AgentVibeCodingConfig::default(),
476 max_task_retries: default_max_task_retries(),
477 harness: AgentHarnessConfig::default(),
478 codex_app_server: AgentCodexAppServerConfig::default(),
479 include_temporal_context: default_include_temporal_context(),
480 temporal_context_use_utc: false, include_working_directory: default_include_working_directory(),
482 include_structured_reasoning_tags: None,
483 user_instructions: None,
484 require_plan_confirmation: default_require_plan_confirmation(),
485 circuit_breaker: CircuitBreakerConfig::default(),
486 open_responses: OpenResponsesConfig::default(),
487 }
488 }
489}
490
491fn validate_unit_temperature(name: &str, value: f32) -> Result<(), String> {
494 if !(0.0..=1.0).contains(&value) {
495 return Err(format!("{name} must be between 0.0 and 1.0, got {value}"));
496 }
497 Ok(())
498}
499
500impl AgentConfig {
501 pub fn should_include_structured_reasoning_tags(&self) -> bool {
503 self.include_structured_reasoning_tags.unwrap_or(false)
504 }
505
506 pub(crate) fn validate_llm_params(&self) -> Result<(), String> {
508 validate_unit_temperature("temperature", self.temperature)?;
511 validate_unit_temperature("refine_temperature", self.refine_temperature)?;
512 validate_unit_temperature("small_model.temperature", self.small_model.temperature)?;
513 validate_unit_temperature("prompt_suggestions.temperature", self.prompt_suggestions.temperature)?;
514
515 if self.instruction_import_max_depth == 0 {
516 return Err("instruction_import_max_depth must be greater than 0".to_string());
517 }
518
519 if !(0.0..=1.0).contains(&self.harness.budget_warning_threshold) {
520 return Err(format!(
521 "harness.budget_warning_threshold must be between 0.0 and 1.0, got {}",
522 self.harness.budget_warning_threshold
523 ));
524 }
525
526 self.persistent_memory.validate()?;
527 self.harness.tool_result_clearing.validate()?;
528
529 Ok(())
530 }
531}
532
533#[inline]
535fn default_provider() -> String {
536 defaults::DEFAULT_PROVIDER.into()
537}
538
539#[inline]
540fn default_api_key_env() -> String {
541 defaults::DEFAULT_API_KEY_ENV.into()
542}
543
544#[inline]
545fn default_model() -> String {
546 defaults::DEFAULT_MODEL.into()
547}
548
549#[inline]
550fn default_theme() -> String {
551 defaults::DEFAULT_THEME.into()
552}
553
554#[inline]
555const fn default_todo_planning_mode() -> bool {
556 true
557}
558
559#[inline]
560const fn default_enable_split_tool_results() -> bool {
561 true }
563
564#[inline]
565const fn default_max_conversation_turns() -> usize {
566 tool_limits::DEFAULT_MAX_CONVERSATION_TURNS
567}
568
569#[inline]
570const fn default_idle_turn_limit() -> usize {
571 execution::IDLE_TURN_LIMIT
572}
573
574#[inline]
575fn default_reasoning_effort() -> ReasoningEffortLevel {
576 ReasoningEffortLevel::None
577}
578
579#[inline]
580fn default_verbosity() -> VerbosityLevel {
581 VerbosityLevel::default()
582}
583
584#[inline]
585const fn default_temperature() -> f32 {
586 llm_generation::DEFAULT_TEMPERATURE
587}
588
589#[inline]
590const fn default_refine_temperature() -> f32 {
591 llm_generation::DEFAULT_REFINE_TEMPERATURE
592}
593
594#[inline]
595const fn default_enable_self_review() -> bool {
596 false
597}
598
599#[inline]
600const fn default_max_review_passes() -> usize {
601 1
602}
603
604#[inline]
605const fn default_refine_prompts_enabled() -> bool {
606 false
607}
608
609#[inline]
610const fn default_refine_max_passes() -> usize {
611 1
612}
613
614#[inline]
615const fn default_max_system_prompt_tokens() -> u64 {
616 prompt_budget::DEFAULT_MAX_SYSTEM_PROMPT_TOKENS
617}
618
619#[inline]
620const fn default_system_prompt_budget_warning() -> bool {
621 true
622}
623
624#[inline]
625const fn default_trim_system_prompt() -> bool {
626 true
627}
628
629#[inline]
630const fn default_project_doc_max_bytes() -> usize {
631 prompt_budget::DEFAULT_MAX_BYTES
632}
633
634#[inline]
635const fn default_instruction_max_bytes() -> usize {
636 prompt_budget::DEFAULT_MAX_BYTES
637}
638
639#[inline]
640const fn default_instruction_import_max_depth() -> usize {
641 5
642}
643
644#[inline]
645const fn default_max_task_retries() -> u32 {
646 2 }
648
649#[inline]
650const fn default_include_temporal_context() -> bool {
651 true }
653
654#[inline]
655const fn default_include_working_directory() -> bool {
656 true }
658
659#[inline]
660const fn default_require_plan_confirmation() -> bool {
661 true }
663
664#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
665#[derive(Debug, Clone, Deserialize, Serialize)]
666pub struct AgentCheckpointingConfig {
667 #[serde(default = "default_checkpointing_enabled")]
669 pub enabled: bool,
670
671 #[serde(default)]
673 pub storage_dir: Option<String>,
674
675 #[serde(default = "default_checkpointing_max_snapshots")]
677 pub max_snapshots: usize,
678
679 #[serde(default = "default_checkpointing_max_age_days")]
681 pub max_age_days: Option<u64>,
682}
683
684impl Default for AgentCheckpointingConfig {
685 fn default() -> Self {
686 Self {
687 enabled: default_checkpointing_enabled(),
688 storage_dir: None,
689 max_snapshots: default_checkpointing_max_snapshots(),
690 max_age_days: default_checkpointing_max_age_days(),
691 }
692 }
693}
694
695#[inline]
696const fn default_checkpointing_enabled() -> bool {
697 DEFAULT_CHECKPOINTS_ENABLED
698}
699
700#[inline]
701const fn default_checkpointing_max_snapshots() -> usize {
702 DEFAULT_MAX_SNAPSHOTS
703}
704
705#[inline]
706const fn default_checkpointing_max_age_days() -> Option<u64> {
707 Some(DEFAULT_MAX_AGE_DAYS)
708}
709
710#[cfg(test)]
711mod tests;