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;