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
491impl AgentConfig {
492 pub fn should_include_structured_reasoning_tags(&self) -> bool {
494 self.include_structured_reasoning_tags.unwrap_or(false)
495 }
496
497 pub(crate) fn validate_llm_params(&self) -> Result<(), String> {
499 if !(0.0..=1.0).contains(&self.temperature) {
501 return Err(format!("temperature must be between 0.0 and 1.0, got {}", self.temperature));
502 }
503
504 if !(0.0..=1.0).contains(&self.refine_temperature) {
505 return Err(format!("refine_temperature must be between 0.0 and 1.0, got {}", self.refine_temperature));
506 }
507
508 if self.instruction_import_max_depth == 0 {
509 return Err("instruction_import_max_depth must be greater than 0".to_string());
510 }
511
512 if !(0.0..=1.0).contains(&self.harness.budget_warning_threshold) {
513 return Err(format!(
514 "harness.budget_warning_threshold must be between 0.0 and 1.0, got {}",
515 self.harness.budget_warning_threshold
516 ));
517 }
518
519 self.persistent_memory.validate()?;
520 self.harness.tool_result_clearing.validate()?;
521
522 Ok(())
523 }
524}
525
526#[inline]
528fn default_provider() -> String {
529 defaults::DEFAULT_PROVIDER.into()
530}
531
532#[inline]
533fn default_api_key_env() -> String {
534 defaults::DEFAULT_API_KEY_ENV.into()
535}
536
537#[inline]
538fn default_model() -> String {
539 defaults::DEFAULT_MODEL.into()
540}
541
542#[inline]
543fn default_theme() -> String {
544 defaults::DEFAULT_THEME.into()
545}
546
547#[inline]
548const fn default_todo_planning_mode() -> bool {
549 true
550}
551
552#[inline]
553const fn default_enable_split_tool_results() -> bool {
554 true }
556
557#[inline]
558const fn default_max_conversation_turns() -> usize {
559 tool_limits::DEFAULT_MAX_CONVERSATION_TURNS
560}
561
562#[inline]
563const fn default_idle_turn_limit() -> usize {
564 execution::IDLE_TURN_LIMIT
565}
566
567#[inline]
568fn default_reasoning_effort() -> ReasoningEffortLevel {
569 ReasoningEffortLevel::None
570}
571
572#[inline]
573fn default_verbosity() -> VerbosityLevel {
574 VerbosityLevel::default()
575}
576
577#[inline]
578const fn default_temperature() -> f32 {
579 llm_generation::DEFAULT_TEMPERATURE
580}
581
582#[inline]
583const fn default_refine_temperature() -> f32 {
584 llm_generation::DEFAULT_REFINE_TEMPERATURE
585}
586
587#[inline]
588const fn default_enable_self_review() -> bool {
589 false
590}
591
592#[inline]
593const fn default_max_review_passes() -> usize {
594 1
595}
596
597#[inline]
598const fn default_refine_prompts_enabled() -> bool {
599 false
600}
601
602#[inline]
603const fn default_refine_max_passes() -> usize {
604 1
605}
606
607#[inline]
608const fn default_max_system_prompt_tokens() -> u64 {
609 prompt_budget::DEFAULT_MAX_SYSTEM_PROMPT_TOKENS
610}
611
612#[inline]
613const fn default_system_prompt_budget_warning() -> bool {
614 true
615}
616
617#[inline]
618const fn default_trim_system_prompt() -> bool {
619 true
620}
621
622#[inline]
623const fn default_project_doc_max_bytes() -> usize {
624 prompt_budget::DEFAULT_MAX_BYTES
625}
626
627#[inline]
628const fn default_instruction_max_bytes() -> usize {
629 prompt_budget::DEFAULT_MAX_BYTES
630}
631
632#[inline]
633const fn default_instruction_import_max_depth() -> usize {
634 5
635}
636
637#[inline]
638const fn default_max_task_retries() -> u32 {
639 2 }
641
642#[inline]
643const fn default_include_temporal_context() -> bool {
644 true }
646
647#[inline]
648const fn default_include_working_directory() -> bool {
649 true }
651
652#[inline]
653const fn default_require_plan_confirmation() -> bool {
654 true }
656
657#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
658#[derive(Debug, Clone, Deserialize, Serialize)]
659pub struct AgentCheckpointingConfig {
660 #[serde(default = "default_checkpointing_enabled")]
662 pub enabled: bool,
663
664 #[serde(default)]
666 pub storage_dir: Option<String>,
667
668 #[serde(default = "default_checkpointing_max_snapshots")]
670 pub max_snapshots: usize,
671
672 #[serde(default = "default_checkpointing_max_age_days")]
674 pub max_age_days: Option<u64>,
675}
676
677impl Default for AgentCheckpointingConfig {
678 fn default() -> Self {
679 Self {
680 enabled: default_checkpointing_enabled(),
681 storage_dir: None,
682 max_snapshots: default_checkpointing_max_snapshots(),
683 max_age_days: default_checkpointing_max_age_days(),
684 }
685 }
686}
687
688#[inline]
689const fn default_checkpointing_enabled() -> bool {
690 DEFAULT_CHECKPOINTS_ENABLED
691}
692
693#[inline]
694const fn default_checkpointing_max_snapshots() -> usize {
695 DEFAULT_MAX_SNAPSHOTS
696}
697
698#[inline]
699const fn default_checkpointing_max_age_days() -> Option<u64> {
700 Some(DEFAULT_MAX_AGE_DAYS)
701}
702
703#[cfg(test)]
704mod tests;