Skip to main content

vtcode_config/core/agent/
mod.rs

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/// Agent-wide configuration
39#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
40#[derive(Debug, Clone, Deserialize, Serialize)]
41pub struct AgentConfig {
42    /// Active provider for single-agent runs.
43    #[serde(default = "default_provider")]
44    pub provider: String,
45
46    /// Environment variable that stores the API key for the active provider
47    #[serde(default = "default_api_key_env")]
48    pub api_key_env: String,
49
50    /// Default model for new conversations.
51    #[serde(default = "default_model")]
52    pub default_model: String,
53
54    /// UI theme identifier controlling ANSI styling
55    #[serde(default = "default_theme")]
56    pub theme: String,
57
58    /// System prompt mode controlling prompt verbosity and token overhead.
59    /// Options target lean base prompts: minimal (~500 tokens), lightweight (~750 tokens),
60    /// default and specialized (~900 tokens) before dynamic runtime addenda.
61    #[serde(default)]
62    pub system_prompt_mode: SystemPromptMode,
63
64    /// Soft token budget for the fully composed system prompt (character-based
65    /// estimate, ~4 chars/token). Includes workspace instructions, guidelines,
66    /// and runtime addenda on top of the base prompt.
67    #[serde(default = "default_max_system_prompt_tokens")]
68    pub max_system_prompt_tokens: u64,
69
70    /// Warn when the composed system prompt exceeds `max_system_prompt_tokens`.
71    #[serde(default = "default_system_prompt_budget_warning")]
72    pub system_prompt_budget_warning: bool,
73
74    /// Trim low-priority advisory system prompt sections when over budget.
75    /// Base, shell-safety, and active-tool contracts are never trimmed.
76    #[serde(default = "default_trim_system_prompt")]
77    pub trim_system_prompt: bool,
78
79    /// Tool documentation mode controlling token overhead for tool definitions
80    /// Options: minimal, progressive (default, ~1.8k tokens for the builtin catalog), full
81    /// Progressive: complete tool and parameter descriptions; only unusually long tails are trimmed at a sentence boundary (recommended)
82    /// Minimal: first sentence of each tool description and no parameter descriptions (power users)
83    /// Full: every tool and parameter description sent unmodified
84    #[serde(default)]
85    pub tool_documentation_mode: ToolDocumentationMode,
86
87    /// Shell syntax profile used in model-facing command examples.
88    /// This controls prompt wording only; command policy remains in the runtime.
89    /// Values: auto, unix_like, powershell.
90    #[serde(default)]
91    pub shell_prompt_profile: ShellPromptProfile,
92
93    /// Enable split tool results for massive token savings (Phase 4)
94    /// When enabled, tools return dual-channel output:
95    /// - llm_content: Concise summary sent to LLM (token-optimized, 53-95% reduction)
96    /// - ui_content: Rich output displayed to user (full details preserved)
97    ///   Applies to: exec_command, code_search, apply_patch, and retained internal helpers
98    ///   Default: true (opt-out for compatibility), recommended for production use
99    #[serde(default = "default_enable_split_tool_results")]
100    pub enable_split_tool_results: bool,
101
102    /// Enable TODO planning helper mode for structured task management
103    #[serde(default = "default_todo_planning_mode")]
104    pub todo_planning_mode: bool,
105
106    /// Preferred rendering surface for the interactive chat UI (inline by default; auto, alternate, inline)
107    #[serde(default)]
108    pub ui_surface: UiSurfacePreference,
109
110    /// Maximum number of conversation turns before auto-termination
111    #[serde(default = "default_max_conversation_turns")]
112    pub max_conversation_turns: usize,
113
114    /// Maximum consecutive idle turns (no tool calls, no meaningful response) before
115    /// the agent runner treats the session as stalled and aborts the loop.
116    #[serde(default = "default_idle_turn_limit")]
117    pub idle_turn_limit: usize,
118
119    /// Reasoning depth for capable models (none to max).
120    #[serde(default = "default_reasoning_effort")]
121    pub reasoning_effort: ReasoningEffortLevel,
122    /// Permit a lower supported effort with an explicit harness diagnostic.
123    #[serde(default)]
124    pub allow_reasoning_effort_downgrade: bool,
125
126    /// Output verbosity for supported models (low to high).
127    #[serde(default = "default_verbosity")]
128    pub verbosity: VerbosityLevel,
129
130    /// Sampling temperature (0.0 precise to 1.0 creative).
131    #[serde(default = "default_temperature")]
132    pub temperature: f32,
133
134    /// Temperature for prompt refinement (0.0-1.0).
135    #[serde(default = "default_refine_temperature")]
136    pub refine_temperature: f32,
137
138    /// Enable an extra self-review pass to refine final responses
139    #[serde(default = "default_enable_self_review")]
140    pub enable_self_review: bool,
141
142    /// Maximum number of self-review passes
143    #[serde(default = "default_max_review_passes")]
144    pub max_review_passes: usize,
145
146    /// Enable prompt refinement pass before sending to LLM
147    #[serde(default = "default_refine_prompts_enabled")]
148    pub refine_prompts_enabled: bool,
149
150    /// Max refinement passes for prompt writing
151    #[serde(default = "default_refine_max_passes")]
152    pub refine_prompts_max_passes: usize,
153
154    /// Optional model override for the refiner (empty = auto pick efficient sibling)
155    #[serde(default)]
156    pub refine_prompts_model: String,
157
158    /// Small/lightweight model configuration for efficient operations
159    /// Used for tasks like large file reads, parsing, git history, conversation summarization
160    /// Typically 70-80% cheaper than main model; ~50% of VT Code's calls use this tier
161    #[serde(default)]
162    pub small_model: AgentSmallModelConfig,
163
164    /// Inline prompt suggestion configuration for the chat composer
165    #[serde(default)]
166    pub prompt_suggestions: AgentPromptSuggestionsConfig,
167
168    /// Session onboarding and welcome message configuration
169    #[serde(default)]
170    pub onboarding: AgentOnboardingConfig,
171
172    /// Maximum bytes of AGENTS.md/CLAUDE.md content to load from project hierarchy
173    #[serde(default = "default_project_doc_max_bytes")]
174    pub project_doc_max_bytes: usize,
175
176    /// Additional filenames to check when AGENTS.md is absent at a directory level.
177    #[serde(default)]
178    pub project_doc_fallback_filenames: Vec<String>,
179
180    /// Maximum bytes of instruction content to load from AGENTS.md/CLAUDE.md hierarchy
181    #[serde(default = "default_instruction_max_bytes", alias = "rule_doc_max_bytes")]
182    pub instruction_max_bytes: usize,
183
184    /// Additional instruction files or globs to merge into the hierarchy
185    #[serde(default, alias = "instruction_paths", alias = "instructions")]
186    pub instruction_files: Vec<String>,
187
188    /// Instruction files or globs to exclude from AGENTS.md and rules discovery
189    #[serde(default)]
190    pub instruction_excludes: Vec<String>,
191
192    /// Maximum recursive `@path` import depth for instruction and rule files
193    #[serde(default = "default_instruction_import_max_depth")]
194    pub instruction_import_max_depth: usize,
195
196    /// Durable per-repository memory for main sessions
197    #[serde(default)]
198    pub persistent_memory: PersistentMemoryConfig,
199
200    /// Provider/key identities captured from interactive configuration flows
201    ///
202    /// Note: Actual API keys are stored securely in the configured credential
203    /// backend (OS keyring when available, otherwise encrypted file storage).
204    /// Keys use `<provider>/<environment-variable>` identity keys and this
205    /// field only tracks which identities have keys stored (for UI/migration purposes).
206    /// The keys themselves are NOT serialized to the config file for security.
207    #[serde(default, skip_serializing)]
208    pub custom_api_keys: BTreeMap<String, String>,
209
210    /// Preferred storage backend for credentials (OAuth tokens, API keys, etc.)
211    ///
212    /// - `keyring`: Use OS-specific secure storage (macOS Keychain, Windows Credential
213    ///   Manager, Linux Secret Service), with encrypted-file fallback when unavailable.
214    /// - `file`: Use AES-256-GCM encrypted file with machine-derived key
215    /// - `auto`: Try keyring first, fall back to file if unavailable
216    #[serde(default)]
217    pub credential_storage_mode: crate::auth::AuthCredentialsStoreMode,
218
219    /// Checkpointing configuration for automatic turn snapshots
220    #[serde(default)]
221    pub checkpointing: AgentCheckpointingConfig,
222
223    /// Vibe coding configuration for lazy or vague request support
224    #[serde(default)]
225    pub vibe_coding: AgentVibeCodingConfig,
226
227    /// Maximum number of retries for agent task execution (default: 2)
228    /// When an agent task fails due to retryable errors (timeout, network, 503, etc.),
229    /// it will be retried up to this many times with exponential backoff
230    #[serde(default = "default_max_task_retries")]
231    pub max_task_retries: u32,
232
233    /// Harness configuration for turn-level budgets, telemetry, and execution limits
234    #[serde(default)]
235    pub harness: AgentHarnessConfig,
236
237    /// Experimental Codex app-server sidecar configuration.
238    #[serde(default)]
239    pub codex_app_server: AgentCodexAppServerConfig,
240
241    /// Include current date/time in system prompt for temporal awareness
242    /// Helps LLM understand context for time-sensitive tasks (default: true)
243    #[serde(default = "default_include_temporal_context")]
244    pub include_temporal_context: bool,
245
246    /// Use UTC instead of local time for temporal context in system prompts
247    #[serde(default)]
248    pub temporal_context_use_utc: bool,
249
250    /// Include current working directory in system prompt (default: true)
251    #[serde(default = "default_include_working_directory")]
252    pub include_working_directory: bool,
253
254    /// Controls inclusion of the structured reasoning tag instructions block.
255    ///
256    /// Behavior:
257    /// - `Some(true)`: always include structured reasoning instructions.
258    /// - `Some(false)`: never include structured reasoning instructions.
259    /// - `None` (default): omit structured reasoning instructions in every prompt mode.
260    ///
261    /// Models with native reasoning do not need visible reasoning tags, so the
262    /// block is opt-in for users who want tag-based reasoning guidance.
263    #[serde(default)]
264    pub include_structured_reasoning_tags: Option<bool>,
265
266    /// Custom instructions provided by the user via configuration to guide agent behavior
267    #[serde(default)]
268    pub user_instructions: Option<String>,
269
270    /// Require user confirmation before executing a plan generated in planning workflow
271    /// When true, exiting planning workflow shows the implementation blueprint and
272    /// requires explicit user approval before enabling edit tools.
273    #[serde(default = "default_require_plan_confirmation")]
274    pub require_plan_confirmation: bool,
275
276    /// Circuit breaker configuration for resilient tool execution
277    /// Controls when the agent should pause and ask for user guidance due to repeated failures
278    #[serde(default)]
279    pub circuit_breaker: CircuitBreakerConfig,
280
281    /// Open Responses specification compliance configuration
282    /// Enables vendor-neutral LLM API format for interoperable workflows
283    #[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/// When to trigger a context reset — starting a clean session from external
332/// artifacts only, discarding conversation history to clear noise and bad
333/// assumptions. This is distinct from compaction, which preserves
334/// conversational continuity within the same task/agent loop.
335///
336/// Following the context engineering pattern: "Context reset uses external
337/// artifacts (files from note-taking, git logs, test results, task lists) as
338/// startup material to open a clean new context/session. It does not preserve
339/// the full conversation history, and can clear noise and bad assumptions so
340/// that a new agent can reorient itself."
341#[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    /// Never reset — always carry forward conversation history (current behavior).
347    #[default]
348    Off,
349    /// Reset when the progress monitor detects a stall (no forward progress for
350    /// `context_reset_stall_threshold` consecutive turns).
351    OnStall,
352    /// Reset after every automatic compaction, so the post-compaction session
353    /// starts from artifacts only rather than the compacted summary.
354    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, // Default to local time
480            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    /// Determine whether structured reasoning tag instructions should be included.
492    pub fn should_include_structured_reasoning_tags(&self) -> bool {
493        self.include_structured_reasoning_tags.unwrap_or(false)
494    }
495
496    /// Validate LLM generation parameters
497    pub(crate) fn validate_llm_params(&self) -> Result<(), String> {
498        // Validate temperature range
499        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// Optimized: Use inline defaults with constants to reduce function call overhead
526#[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 // Default: enabled for production use (84% token savings)
554}
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 // Retry twice on transient failures
639}
640
641#[inline]
642const fn default_include_temporal_context() -> bool {
643    true // Enable by default - minimal overhead (~20 tokens)
644}
645
646#[inline]
647const fn default_include_working_directory() -> bool {
648    true // Enable by default - minimal overhead (~10 tokens)
649}
650
651#[inline]
652const fn default_require_plan_confirmation() -> bool {
653    true // Default: require confirmation (HITL pattern)
654}
655
656#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
657#[derive(Debug, Clone, Deserialize, Serialize)]
658pub struct AgentCheckpointingConfig {
659    /// Enable automatic checkpoints after each successful turn
660    #[serde(default = "default_checkpointing_enabled")]
661    pub enabled: bool,
662
663    /// Optional custom directory for storing checkpoints (relative to workspace or absolute)
664    #[serde(default)]
665    pub storage_dir: Option<String>,
666
667    /// Maximum number of checkpoints to retain on disk
668    #[serde(default = "default_checkpointing_max_snapshots")]
669    pub max_snapshots: usize,
670
671    /// Maximum age in days before checkpoints are removed automatically (None disables)
672    #[serde(default = "default_checkpointing_max_age_days")]
673    pub max_age_days: Option<u64>,
674}
675
676impl Default for AgentCheckpointingConfig {
677    fn default() -> Self {
678        Self {
679            enabled: default_checkpointing_enabled(),
680            storage_dir: None,
681            max_snapshots: default_checkpointing_max_snapshots(),
682            max_age_days: default_checkpointing_max_age_days(),
683        }
684    }
685}
686
687#[inline]
688const fn default_checkpointing_enabled() -> bool {
689    DEFAULT_CHECKPOINTS_ENABLED
690}
691
692#[inline]
693const fn default_checkpointing_max_snapshots() -> usize {
694    DEFAULT_MAX_SNAPSHOTS
695}
696
697#[inline]
698const fn default_checkpointing_max_age_days() -> Option<u64> {
699    Some(DEFAULT_MAX_AGE_DAYS)
700}
701
702#[cfg(test)]
703mod tests {
704    use super::*;
705
706    #[test]
707    fn test_continuation_policy_defaults_and_parses() {
708        assert_eq!(ContinuationPolicy::default(), ContinuationPolicy::All);
709        assert_eq!(ContinuationPolicy::parse("off"), Some(ContinuationPolicy::Off));
710        assert_eq!(ContinuationPolicy::parse("exec-only"), Some(ContinuationPolicy::ExecOnly));
711        assert_eq!(ContinuationPolicy::parse("all"), Some(ContinuationPolicy::All));
712        assert_eq!(ContinuationPolicy::parse("invalid"), None);
713    }
714
715    #[test]
716    fn test_tracker_continuation_defaults_and_deserializes() {
717        assert!(TrackerContinuationConfig::default().auto_continue_tracker);
718        assert_eq!(TrackerContinuationConfig::default().cross_turn_turns, 32);
719        let parsed: AgentHarnessConfig =
720            toml::from_str("[continuation]\nauto_continue_tracker = false\ncross_turn_turns = 3")
721                .expect("valid harness config");
722        assert!(!parsed.continuation.auto_continue_tracker);
723        assert_eq!(parsed.continuation.cross_turn_turns, 3);
724        let fallback: AgentHarnessConfig = toml::from_str("").expect("empty harness config");
725        assert!(fallback.continuation.auto_continue_tracker);
726        assert_eq!(fallback.continuation.cross_turn_turns, 32);
727    }
728
729    #[test]
730    fn test_harness_config_continuation_policy_deserializes_with_fallback() {
731        let parsed: AgentHarnessConfig = toml::from_str("continuation_policy = \"all\"").expect("valid harness config");
732        assert_eq!(parsed.continuation_policy, ContinuationPolicy::All);
733
734        let fallback: AgentHarnessConfig =
735            toml::from_str("continuation_policy = \"unexpected\"").expect("fallback config");
736        assert_eq!(fallback.continuation_policy, ContinuationPolicy::All);
737    }
738
739    #[test]
740    fn test_harness_config_tool_call_budget_defaults_to_120() {
741        assert_eq!(AgentHarnessConfig::default().max_tool_calls_per_turn, tool_limits::DEFAULT_MAX_TOOL_CALLS_PER_TURN);
742
743        let parsed: AgentHarnessConfig = toml::from_str("").expect("default harness config");
744        assert_eq!(parsed.max_tool_calls_per_turn, tool_limits::DEFAULT_MAX_TOOL_CALLS_PER_TURN);
745    }
746
747    #[test]
748    fn test_harness_orchestration_mode_defaults_and_parses() {
749        assert_eq!(HarnessOrchestrationMode::default(), HarnessOrchestrationMode::PlanBuildEvaluate);
750        assert_eq!(HarnessOrchestrationMode::parse("single"), Some(HarnessOrchestrationMode::Single));
751        assert_eq!(
752            HarnessOrchestrationMode::parse("plan_build_evaluate"),
753            Some(HarnessOrchestrationMode::PlanBuildEvaluate)
754        );
755        assert_eq!(
756            HarnessOrchestrationMode::parse("planner-generator-evaluator"),
757            Some(HarnessOrchestrationMode::PlanBuildEvaluate)
758        );
759        assert_eq!(HarnessOrchestrationMode::parse("unexpected"), None);
760    }
761
762    #[test]
763    fn test_harness_config_orchestration_deserializes_with_fallback() {
764        let parsed: AgentHarnessConfig =
765            toml::from_str("orchestration_mode = \"plan_build_evaluate\"").expect("valid harness config");
766        assert_eq!(parsed.orchestration_mode, HarnessOrchestrationMode::PlanBuildEvaluate);
767        assert_eq!(parsed.max_revision_rounds, 2);
768
769        let fallback: AgentHarnessConfig =
770            toml::from_str("orchestration_mode = \"unexpected\"").expect("fallback config");
771        assert_eq!(fallback.orchestration_mode, HarnessOrchestrationMode::PlanBuildEvaluate);
772    }
773
774    /// Drift guard for hand-written enum string mappings. Each of these enums
775    /// exposes the same value through serde, `as_str()`, and `parse()`; see
776    /// [`crate::test_support::assert_string_enum_lockstep`].
777    #[test]
778    fn string_config_enums_keep_as_str_and_serde_in_lockstep() {
779        use crate::test_support::assert_string_enum_lockstep;
780
781        assert_string_enum_lockstep!(
782            ContinuationPolicy,
783            [
784                ContinuationPolicy::Off,
785                ContinuationPolicy::ExecOnly,
786                ContinuationPolicy::All
787            ]
788        );
789        assert_string_enum_lockstep!(
790            ContextResetMode,
791            [
792                ContextResetMode::Off,
793                ContextResetMode::OnStall,
794                ContextResetMode::OnCompaction
795            ]
796        );
797        assert_string_enum_lockstep!(
798            HarnessOrchestrationMode,
799            [
800                HarnessOrchestrationMode::Single,
801                HarnessOrchestrationMode::PlanBuildEvaluate
802            ]
803        );
804    }
805
806    #[test]
807    fn test_verification_auto_recovery_defaults_to_bounded_auto_execute() {
808        let config = VerificationAutoRecoveryConfig::default();
809        assert!(config.auto_execute);
810        assert_eq!(config.in_turn_attempts, 2);
811        assert_eq!(config.cross_turn_turns, 2);
812        assert_eq!(config.default_verifier_override, None);
813        assert_eq!(config.max_consecutive_failures, 3);
814    }
815
816    #[test]
817    fn test_verification_auto_recovery_survives_missing_field_for_backward_compatibility() {
818        // Older vtcode.toml files predate [agent.harness.verification]: the
819        // whole table and each field must fall back to defaults.
820        let without_table: AgentHarnessConfig = toml::from_str("").expect("default harness config");
821        assert!(without_table.verification.auto_execute);
822        assert_eq!(without_table.verification.in_turn_attempts, 2);
823
824        let partial: VerificationAutoRecoveryConfig =
825            toml::from_str("auto_execute = false").expect("minimal verification config parses");
826        assert!(!partial.auto_execute);
827        assert_eq!(partial.in_turn_attempts, 2);
828        assert_eq!(partial.max_consecutive_failures, 3);
829
830        let full: VerificationAutoRecoveryConfig = toml::from_str(
831            "auto_execute = true\nin_turn_attempts = 1\ncross_turn_turns = 0\ndefault_verifier_override = \"cargo nextest run -p mycrate\"\nmax_consecutive_failures = 5",
832        )
833        .expect("full verification config parses");
834        assert_eq!(full.in_turn_attempts, 1);
835        assert_eq!(full.cross_turn_turns, 0);
836        assert_eq!(full.default_verifier_override.as_deref(), Some("cargo nextest run -p mycrate"));
837        assert_eq!(full.max_consecutive_failures, 5);
838    }
839
840    #[test]
841    fn test_plan_confirmation_config_default() {
842        let config = AgentConfig::default();
843        assert!(config.require_plan_confirmation);
844    }
845
846    #[test]
847    fn test_system_prompt_budget_defaults() {
848        let config = AgentConfig::default();
849        assert_eq!(config.max_system_prompt_tokens, prompt_budget::DEFAULT_MAX_SYSTEM_PROMPT_TOKENS);
850        assert!(config.system_prompt_budget_warning);
851        assert!(config.trim_system_prompt);
852
853        let parsed: AgentConfig = toml::from_str(
854            r#"
855max_system_prompt_tokens = 4000
856system_prompt_budget_warning = false
857trim_system_prompt = true
858"#,
859        )
860        .expect("agent config should parse");
861        assert_eq!(parsed.max_system_prompt_tokens, 4000);
862        assert!(!parsed.system_prompt_budget_warning);
863        assert!(parsed.trim_system_prompt);
864    }
865
866    #[test]
867    fn test_budget_warning_threshold_default_and_validation() {
868        let config = AgentConfig::default();
869        assert!((config.harness.budget_warning_threshold - 0.75).abs() < f64::EPSILON);
870        assert!(config.validate_llm_params().is_ok());
871
872        let parsed: AgentHarnessConfig = toml::from_str(
873            r#"
874max_budget_usd = 5.0
875budget_warning_threshold = 0.5
876"#,
877        )
878        .expect("harness config should parse");
879        assert_eq!(parsed.max_budget_usd, Some(5.0));
880        assert!((parsed.budget_warning_threshold - 0.5).abs() < f64::EPSILON);
881
882        let mut invalid = AgentConfig::default();
883        invalid.harness.budget_warning_threshold = 1.5;
884        assert!(invalid.validate_llm_params().is_err());
885    }
886
887    #[test]
888    fn test_persistent_memory_is_disabled_by_default() {
889        let config = AgentConfig::default();
890        assert!(!config.persistent_memory.enabled);
891        assert!(config.persistent_memory.auto_write);
892    }
893
894    #[test]
895    fn test_tool_result_clearing_defaults() {
896        let config = AgentConfig::default();
897        let clearing = config.harness.tool_result_clearing;
898
899        assert!(clearing.enabled);
900        assert_eq!(clearing.trigger_tokens, 40_000);
901        assert_eq!(clearing.keep_tool_uses, 2);
902        assert_eq!(clearing.keep_recent_tool_batches, 2);
903        assert_eq!(clearing.clear_at_least_tokens, 30_000);
904        assert!(clearing.clear_tool_inputs);
905    }
906
907    #[test]
908    fn test_tool_result_clearing_missing_key_defaults_clear_tool_inputs_true() {
909        let parsed: AgentHarnessConfig = toml::from_str(
910            r#"
911                [tool_result_clearing]
912                enabled = true
913                trigger_tokens = 40000
914                keep_tool_uses = 2
915                clear_at_least_tokens = 30000
916            "#,
917        )
918        .expect("valid harness config");
919
920        assert!(parsed.tool_result_clearing.clear_tool_inputs);
921        assert_eq!(parsed.tool_result_clearing.keep_recent_tool_batches, 2);
922    }
923
924    #[test]
925    fn tool_result_clearing_recent_batches_round_trip_including_compatibility() {
926        for batches in [0, 1, 2, 9] {
927            let parsed: AgentHarnessConfig =
928                toml::from_str(&format!("[tool_result_clearing]\nkeep_recent_tool_batches = {batches}")).unwrap();
929            assert!(parsed.tool_result_clearing.validate().is_ok());
930            let restored: AgentHarnessConfig = toml::from_str(&toml::to_string(&parsed).unwrap()).unwrap();
931            assert_eq!(restored.tool_result_clearing.keep_recent_tool_batches, batches);
932        }
933    }
934
935    #[test]
936    fn test_tool_result_clearing_explicit_false_opts_out_of_input_clearing() {
937        let parsed: AgentHarnessConfig = toml::from_str(
938            r#"
939                [tool_result_clearing]
940                clear_tool_inputs = false
941            "#,
942        )
943        .expect("valid harness config");
944
945        assert!(!parsed.tool_result_clearing.clear_tool_inputs);
946    }
947
948    #[test]
949    fn test_codex_app_server_experimental_features_default_to_disabled() {
950        let config = AgentConfig::default();
951
952        assert!(!config.codex_app_server.experimental_features);
953    }
954
955    #[test]
956    fn test_codex_app_server_experimental_features_parse_from_toml() {
957        let parsed: AgentCodexAppServerConfig = toml::from_str(
958            r#"
959                command = "codex"
960                args = ["app-server"]
961                startup_timeout_secs = 15
962                experimental_features = true
963            "#,
964        )
965        .expect("valid codex app-server config");
966
967        assert!(parsed.experimental_features);
968        assert_eq!(parsed.startup_timeout_secs, 15);
969    }
970
971    #[test]
972    fn test_tool_result_clearing_parses_and_validates() {
973        let parsed: AgentHarnessConfig = toml::from_str(
974            r#"
975                [tool_result_clearing]
976                enabled = true
977                trigger_tokens = 123456
978                keep_tool_uses = 6
979                clear_at_least_tokens = 4096
980                clear_tool_inputs = true
981            "#,
982        )
983        .expect("valid harness config");
984
985        assert!(parsed.tool_result_clearing.enabled);
986        assert_eq!(parsed.tool_result_clearing.trigger_tokens, 123_456);
987        assert_eq!(parsed.tool_result_clearing.keep_tool_uses, 6);
988        assert_eq!(parsed.tool_result_clearing.clear_at_least_tokens, 4_096);
989        assert!(parsed.tool_result_clearing.clear_tool_inputs);
990        assert!(parsed.tool_result_clearing.validate().is_ok());
991    }
992
993    #[test]
994    fn test_tool_result_clearing_rejects_zero_values() {
995        let clearing = ToolResultClearingConfig {
996            trigger_tokens: 0,
997            ..ToolResultClearingConfig::default()
998        };
999        assert!(clearing.validate().is_err());
1000
1001        let clearing = ToolResultClearingConfig {
1002            keep_tool_uses: 0,
1003            ..ToolResultClearingConfig::default()
1004        };
1005        assert!(clearing.validate().is_err());
1006
1007        let clearing = ToolResultClearingConfig {
1008            clear_at_least_tokens: 0,
1009            ..ToolResultClearingConfig::default()
1010        };
1011        assert!(clearing.validate().is_err());
1012    }
1013
1014    #[test]
1015    fn test_structured_reasoning_is_opt_in_for_every_prompt_mode() {
1016        let default_prompt_config = AgentConfig {
1017            system_prompt_mode: SystemPromptMode::Default,
1018            ..Default::default()
1019        };
1020        assert!(!default_prompt_config.should_include_structured_reasoning_tags());
1021
1022        let specialized_mode = AgentConfig {
1023            system_prompt_mode: SystemPromptMode::Specialized,
1024            ..Default::default()
1025        };
1026        assert!(!specialized_mode.should_include_structured_reasoning_tags());
1027
1028        let minimal_mode = AgentConfig {
1029            system_prompt_mode: SystemPromptMode::Minimal,
1030            ..Default::default()
1031        };
1032        assert!(!minimal_mode.should_include_structured_reasoning_tags());
1033
1034        let lightweight_mode = AgentConfig {
1035            system_prompt_mode: SystemPromptMode::Lightweight,
1036            ..Default::default()
1037        };
1038        assert!(!lightweight_mode.should_include_structured_reasoning_tags());
1039    }
1040
1041    #[test]
1042    fn test_structured_reasoning_explicit_override() {
1043        let mut config = AgentConfig {
1044            system_prompt_mode: SystemPromptMode::Minimal,
1045            include_structured_reasoning_tags: Some(true),
1046            ..AgentConfig::default()
1047        };
1048        assert!(config.should_include_structured_reasoning_tags());
1049
1050        config.include_structured_reasoning_tags = Some(false);
1051        assert!(!config.should_include_structured_reasoning_tags());
1052    }
1053}