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). Options: "low", "medium", "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    /// Measure quality, token cost, and latency before choosing a different route.
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    /// Deprecated and ignored: kept so existing configs still parse. Instruction loading,
173    /// including welcome-screen highlights, uses `instruction_max_bytes`.
174    #[serde(default = "default_project_doc_max_bytes")]
175    pub project_doc_max_bytes: usize,
176
177    /// Additional filenames to check when AGENTS.md is absent at a directory level.
178    #[serde(default)]
179    pub project_doc_fallback_filenames: Vec<String>,
180
181    /// Maximum bytes of instruction content to load from AGENTS.md/CLAUDE.md hierarchy
182    #[serde(default = "default_instruction_max_bytes", alias = "rule_doc_max_bytes")]
183    pub instruction_max_bytes: usize,
184
185    /// Additional instruction files or globs to merge into the hierarchy
186    #[serde(default, alias = "instruction_paths", alias = "instructions")]
187    pub instruction_files: Vec<String>,
188
189    /// Instruction files or globs to exclude from AGENTS.md and rules discovery
190    #[serde(default)]
191    pub instruction_excludes: Vec<String>,
192
193    /// Maximum recursive `@path` import depth for instruction and rule files
194    #[serde(default = "default_instruction_import_max_depth")]
195    pub instruction_import_max_depth: usize,
196
197    /// Durable per-repository memory for main sessions
198    #[serde(default)]
199    pub persistent_memory: PersistentMemoryConfig,
200
201    /// Provider/key identities captured from interactive configuration flows
202    ///
203    /// Note: Actual API keys are stored securely in the configured credential
204    /// backend (OS keyring when available, otherwise encrypted file storage).
205    /// Keys use `<provider>/<environment-variable>` identity keys and this
206    /// field only tracks which identities have keys stored (for UI/migration purposes).
207    /// The keys themselves are NOT serialized to the config file for security.
208    #[serde(default, skip_serializing)]
209    pub custom_api_keys: BTreeMap<String, String>,
210
211    /// Preferred storage backend for credentials (OAuth tokens, API keys, etc.)
212    ///
213    /// - `keyring`: Use OS-specific secure storage (macOS Keychain, Windows Credential
214    ///   Manager, Linux Secret Service), with encrypted-file fallback when unavailable.
215    /// - `file`: Use AES-256-GCM encrypted file with machine-derived key
216    /// - `auto`: Try keyring first, fall back to file if unavailable
217    #[serde(default)]
218    pub credential_storage_mode: crate::auth::AuthCredentialsStoreMode,
219
220    /// Checkpointing configuration for automatic turn snapshots
221    #[serde(default)]
222    pub checkpointing: AgentCheckpointingConfig,
223
224    /// Vibe coding configuration for lazy or vague request support
225    #[serde(default)]
226    pub vibe_coding: AgentVibeCodingConfig,
227
228    /// Maximum number of retries for agent task execution (default: 2)
229    /// When an agent task fails due to retryable errors (timeout, network, 503, etc.),
230    /// it will be retried up to this many times with exponential backoff
231    #[serde(default = "default_max_task_retries")]
232    pub max_task_retries: u32,
233
234    /// Harness configuration for turn-level budgets, telemetry, and execution limits
235    #[serde(default)]
236    pub harness: AgentHarnessConfig,
237
238    /// Experimental Codex app-server sidecar configuration.
239    #[serde(default)]
240    pub codex_app_server: AgentCodexAppServerConfig,
241
242    /// Include current date/time in system prompt for temporal awareness
243    /// Helps LLM understand context for time-sensitive tasks (default: true)
244    #[serde(default = "default_include_temporal_context")]
245    pub include_temporal_context: bool,
246
247    /// Use UTC instead of local time for temporal context in system prompts
248    #[serde(default)]
249    pub temporal_context_use_utc: bool,
250
251    /// Include current working directory in system prompt (default: true)
252    #[serde(default = "default_include_working_directory")]
253    pub include_working_directory: bool,
254
255    /// Controls inclusion of the structured reasoning tag instructions block.
256    ///
257    /// Behavior:
258    /// - `Some(true)`: always include structured reasoning instructions.
259    /// - `Some(false)`: never include structured reasoning instructions.
260    /// - `None` (default): omit structured reasoning instructions in every prompt mode.
261    ///
262    /// Models with native reasoning do not need visible reasoning tags, so the
263    /// block is opt-in for users who want tag-based reasoning guidance.
264    #[serde(default)]
265    pub include_structured_reasoning_tags: Option<bool>,
266
267    /// Custom instructions provided by the user via configuration to guide agent behavior
268    #[serde(default)]
269    pub user_instructions: Option<String>,
270
271    /// Require user confirmation before executing a plan generated in planning workflow
272    /// When true, exiting planning workflow shows the implementation blueprint and
273    /// requires explicit user approval before enabling edit tools.
274    #[serde(default = "default_require_plan_confirmation")]
275    pub require_plan_confirmation: bool,
276
277    /// Circuit breaker configuration for resilient tool execution
278    /// Controls when the agent should pause and ask for user guidance due to repeated failures
279    #[serde(default)]
280    pub circuit_breaker: CircuitBreakerConfig,
281
282    /// Open Responses specification compliance configuration
283    /// Enables vendor-neutral LLM API format for interoperable workflows
284    #[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/// When to trigger a context reset — starting a clean session from external
333/// artifacts only, discarding conversation history to clear noise and bad
334/// assumptions. This is distinct from compaction, which preserves
335/// conversational continuity within the same task/agent loop.
336///
337/// Following the context engineering pattern: "Context reset uses external
338/// artifacts (files from note-taking, git logs, test results, task lists) as
339/// startup material to open a clean new context/session. It does not preserve
340/// the full conversation history, and can clear noise and bad assumptions so
341/// that a new agent can reorient itself."
342#[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    /// Never reset — always carry forward conversation history (current behavior).
348    #[default]
349    Off,
350    /// Reset when the progress monitor detects a stall (no forward progress for
351    /// `context_reset_stall_threshold` consecutive turns).
352    OnStall,
353    /// Reset after every automatic compaction, so the post-compaction session
354    /// starts from artifacts only rather than the compacted summary.
355    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, // Default to local time
481            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    /// Determine whether structured reasoning tag instructions should be included.
493    pub fn should_include_structured_reasoning_tags(&self) -> bool {
494        self.include_structured_reasoning_tags.unwrap_or(false)
495    }
496
497    /// Validate LLM generation parameters
498    pub(crate) fn validate_llm_params(&self) -> Result<(), String> {
499        // Validate temperature range
500        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// Optimized: Use inline defaults with constants to reduce function call overhead
527#[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 // Default: enabled for production use (84% token savings)
555}
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 // Retry twice on transient failures
640}
641
642#[inline]
643const fn default_include_temporal_context() -> bool {
644    true // Enable by default - minimal overhead (~20 tokens)
645}
646
647#[inline]
648const fn default_include_working_directory() -> bool {
649    true // Enable by default - minimal overhead (~10 tokens)
650}
651
652#[inline]
653const fn default_require_plan_confirmation() -> bool {
654    true // Default: require confirmation (HITL pattern)
655}
656
657#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
658#[derive(Debug, Clone, Deserialize, Serialize)]
659pub struct AgentCheckpointingConfig {
660    /// Enable automatic checkpoints after each successful turn
661    #[serde(default = "default_checkpointing_enabled")]
662    pub enabled: bool,
663
664    /// Optional custom directory for storing checkpoints (relative to workspace or absolute)
665    #[serde(default)]
666    pub storage_dir: Option<String>,
667
668    /// Maximum number of checkpoints to retain on disk
669    #[serde(default = "default_checkpointing_max_snapshots")]
670    pub max_snapshots: usize,
671
672    /// Maximum age in days before checkpoints are removed automatically (None disables)
673    #[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;