claude_agent/agent/
config.rs

1//! Agent configuration types.
2//!
3//! Domain-separated configuration for clarity and maintainability.
4
5use std::collections::HashMap;
6use std::path::PathBuf;
7use std::time::Duration;
8
9use crate::client::messages::DEFAULT_MAX_TOKENS;
10use crate::output_style::OutputStyle;
11use crate::permissions::PermissionPolicy;
12use crate::tools::ToolAccess;
13
14/// Model-related configuration.
15#[derive(Debug, Clone)]
16pub struct AgentModelConfig {
17    /// Primary model for main operations
18    pub primary: String,
19    /// Smaller model for quick operations
20    pub small: String,
21    /// Maximum tokens per response
22    pub max_tokens: u32,
23    /// Enable extended context window (1M for supported models)
24    pub extended_context: bool,
25}
26
27impl Default for AgentModelConfig {
28    fn default() -> Self {
29        Self {
30            primary: crate::client::DEFAULT_MODEL.to_string(),
31            small: crate::client::DEFAULT_SMALL_MODEL.to_string(),
32            max_tokens: DEFAULT_MAX_TOKENS,
33            extended_context: false,
34        }
35    }
36}
37
38impl AgentModelConfig {
39    pub fn new(primary: impl Into<String>) -> Self {
40        Self {
41            primary: primary.into(),
42            ..Default::default()
43        }
44    }
45
46    pub fn with_small(mut self, small: impl Into<String>) -> Self {
47        self.small = small.into();
48        self
49    }
50
51    pub fn with_max_tokens(mut self, tokens: u32) -> Self {
52        self.max_tokens = tokens;
53        self
54    }
55
56    pub fn with_extended_context(mut self, enabled: bool) -> Self {
57        self.extended_context = enabled;
58        self
59    }
60}
61
62/// Execution behavior configuration.
63#[derive(Debug, Clone)]
64pub struct ExecutionConfig {
65    /// Maximum agentic loop iterations
66    pub max_iterations: usize,
67    /// Overall execution timeout
68    pub timeout: Option<Duration>,
69    /// Timeout between streaming chunks (detects stalled connections)
70    pub chunk_timeout: Duration,
71    /// Enable automatic context compaction
72    pub auto_compact: bool,
73    /// Context usage threshold for compaction (0.0-1.0)
74    pub compact_threshold: f32,
75    /// Messages to preserve during compaction
76    pub compact_keep_messages: usize,
77}
78
79impl Default for ExecutionConfig {
80    fn default() -> Self {
81        Self {
82            max_iterations: 100,
83            timeout: Some(Duration::from_secs(300)),
84            chunk_timeout: Duration::from_secs(60),
85            auto_compact: true,
86            compact_threshold: crate::types::DEFAULT_COMPACT_THRESHOLD,
87            compact_keep_messages: 4,
88        }
89    }
90}
91
92impl ExecutionConfig {
93    pub fn with_max_iterations(mut self, max: usize) -> Self {
94        self.max_iterations = max;
95        self
96    }
97
98    pub fn with_timeout(mut self, timeout: Duration) -> Self {
99        self.timeout = Some(timeout);
100        self
101    }
102
103    pub fn without_timeout(mut self) -> Self {
104        self.timeout = None;
105        self
106    }
107
108    pub fn with_chunk_timeout(mut self, timeout: Duration) -> Self {
109        self.chunk_timeout = timeout;
110        self
111    }
112
113    pub fn with_auto_compact(mut self, enabled: bool) -> Self {
114        self.auto_compact = enabled;
115        self
116    }
117
118    pub fn with_compact_threshold(mut self, threshold: f32) -> Self {
119        self.compact_threshold = threshold.clamp(0.0, 1.0);
120        self
121    }
122
123    pub fn with_compact_keep_messages(mut self, count: usize) -> Self {
124        self.compact_keep_messages = count;
125        self
126    }
127}
128
129/// Security and permission configuration.
130#[derive(Debug, Clone, Default)]
131pub struct SecurityConfig {
132    /// Tool permission policy
133    pub permission_policy: PermissionPolicy,
134    /// Tool access control
135    pub tool_access: ToolAccess,
136    /// Environment variables for tool execution
137    pub env: HashMap<String, String>,
138}
139
140impl SecurityConfig {
141    pub fn permissive() -> Self {
142        Self {
143            permission_policy: PermissionPolicy::permissive(),
144            tool_access: ToolAccess::All,
145            ..Default::default()
146        }
147    }
148
149    pub fn read_only() -> Self {
150        Self {
151            permission_policy: PermissionPolicy::read_only(),
152            tool_access: ToolAccess::only(["Read", "Glob", "Grep", "Task", "TaskOutput"]),
153            ..Default::default()
154        }
155    }
156
157    pub fn with_permission_policy(mut self, policy: PermissionPolicy) -> Self {
158        self.permission_policy = policy;
159        self
160    }
161
162    pub fn with_tool_access(mut self, access: ToolAccess) -> Self {
163        self.tool_access = access;
164        self
165    }
166
167    pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
168        self.env.insert(key.into(), value.into());
169        self
170    }
171
172    pub fn with_envs(
173        mut self,
174        vars: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
175    ) -> Self {
176        for (k, v) in vars {
177            self.env.insert(k.into(), v.into());
178        }
179        self
180    }
181}
182
183/// Budget and cost control configuration.
184#[derive(Debug, Clone, Default)]
185pub struct BudgetConfig {
186    /// Maximum cost in USD
187    pub max_cost_usd: Option<f64>,
188    /// Tenant identifier for multi-tenant tracking
189    pub tenant_id: Option<String>,
190    /// Model to fall back to when budget exceeded
191    pub fallback_model: Option<String>,
192}
193
194impl BudgetConfig {
195    pub fn unlimited() -> Self {
196        Self::default()
197    }
198
199    pub fn with_max_cost(mut self, usd: f64) -> Self {
200        self.max_cost_usd = Some(usd);
201        self
202    }
203
204    pub fn with_tenant(mut self, tenant_id: impl Into<String>) -> Self {
205        self.tenant_id = Some(tenant_id.into());
206        self
207    }
208
209    pub fn with_fallback(mut self, model: impl Into<String>) -> Self {
210        self.fallback_model = Some(model.into());
211        self
212    }
213}
214
215/// Prompt and output configuration.
216#[derive(Debug, Clone, Default)]
217pub struct PromptConfig {
218    /// Custom system prompt
219    pub system_prompt: Option<String>,
220    /// How to apply system prompt
221    pub system_prompt_mode: SystemPromptMode,
222    /// Output style customization
223    pub output_style: Option<OutputStyle>,
224    /// Structured output schema
225    pub output_schema: Option<serde_json::Value>,
226}
227
228#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
229pub enum SystemPromptMode {
230    /// Replace default system prompt
231    #[default]
232    Replace,
233    /// Append to default system prompt
234    Append,
235}
236
237impl PromptConfig {
238    pub fn with_system_prompt(mut self, prompt: impl Into<String>) -> Self {
239        self.system_prompt = Some(prompt.into());
240        self
241    }
242
243    pub fn with_append_mode(mut self) -> Self {
244        self.system_prompt_mode = SystemPromptMode::Append;
245        self
246    }
247
248    pub fn with_output_style(mut self, style: OutputStyle) -> Self {
249        self.output_style = Some(style);
250        self
251    }
252
253    pub fn with_output_schema(mut self, schema: serde_json::Value) -> Self {
254        self.output_schema = Some(schema);
255        self
256    }
257
258    pub fn with_structured_output<T: schemars::JsonSchema>(mut self) -> Self {
259        let schema = schemars::schema_for!(T);
260        self.output_schema = serde_json::to_value(schema).ok();
261        self
262    }
263}
264
265/// Cache strategy determining which content types to cache.
266///
267/// Anthropic best practices recommend caching static content (system prompts,
268/// tools) with longer TTLs and dynamic content (messages) with shorter TTLs.
269#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
270pub enum CacheStrategy {
271    /// No caching - all content sent without cache_control
272    Disabled,
273    /// Cache system prompt only (static content with long TTL)
274    SystemOnly,
275    /// Cache messages only (last user turn with short TTL)
276    MessagesOnly,
277    /// Cache both system and messages (recommended)
278    #[default]
279    Full,
280}
281
282impl CacheStrategy {
283    /// Returns true if system prompt caching is enabled
284    pub fn cache_system(&self) -> bool {
285        matches!(self, Self::SystemOnly | Self::Full)
286    }
287
288    /// Returns true if message caching is enabled
289    pub fn cache_messages(&self) -> bool {
290        matches!(self, Self::MessagesOnly | Self::Full)
291    }
292
293    /// Returns true if any caching is enabled
294    pub fn is_enabled(&self) -> bool {
295        !matches!(self, Self::Disabled)
296    }
297}
298
299/// Cache configuration for prompt caching.
300///
301/// Implements Anthropic's prompt caching best practices:
302/// - Static content (system prompt, CLAUDE.md) uses longer TTL (1 hour default)
303/// - Dynamic content (messages) uses shorter TTL (5 minutes default)
304/// - Long TTL content must come before short TTL content in requests
305#[derive(Debug, Clone)]
306pub struct CacheConfig {
307    /// Cache strategy determining which content types to cache
308    pub strategy: CacheStrategy,
309    /// TTL for static content (system prompt, tools, CLAUDE.md)
310    pub static_ttl: crate::types::CacheTtl,
311    /// TTL for message content (last user turn)
312    pub message_ttl: crate::types::CacheTtl,
313}
314
315impl Default for CacheConfig {
316    fn default() -> Self {
317        Self {
318            strategy: CacheStrategy::Full,
319            static_ttl: crate::types::CacheTtl::OneHour,
320            message_ttl: crate::types::CacheTtl::FiveMinutes,
321        }
322    }
323}
324
325impl CacheConfig {
326    /// Create a disabled cache configuration
327    pub fn disabled() -> Self {
328        Self {
329            strategy: CacheStrategy::Disabled,
330            ..Default::default()
331        }
332    }
333
334    /// Create a system-only cache configuration
335    pub fn system_only() -> Self {
336        Self {
337            strategy: CacheStrategy::SystemOnly,
338            ..Default::default()
339        }
340    }
341
342    /// Create a messages-only cache configuration
343    pub fn messages_only() -> Self {
344        Self {
345            strategy: CacheStrategy::MessagesOnly,
346            ..Default::default()
347        }
348    }
349
350    /// Set the cache strategy
351    pub fn with_strategy(mut self, strategy: CacheStrategy) -> Self {
352        self.strategy = strategy;
353        self
354    }
355
356    /// Set the TTL for static content
357    pub fn with_static_ttl(mut self, ttl: crate::types::CacheTtl) -> Self {
358        self.static_ttl = ttl;
359        self
360    }
361
362    /// Set the TTL for message content
363    pub fn with_message_ttl(mut self, ttl: crate::types::CacheTtl) -> Self {
364        self.message_ttl = ttl;
365        self
366    }
367
368    /// Get message TTL if message caching is enabled, None otherwise.
369    ///
370    /// This is a convenience method to avoid duplicating the cache_messages() check
371    /// at every call site.
372    pub fn message_ttl_option(&self) -> Option<crate::types::CacheTtl> {
373        if self.strategy.cache_messages() {
374            Some(self.message_ttl)
375        } else {
376            None
377        }
378    }
379}
380
381/// Server-side tools configuration.
382///
383/// Anthropic's built-in server-side tools (Brave Search, web fetch).
384/// These are automatically enabled when "WebSearch" or "WebFetch" are in ToolAccess.
385#[derive(Debug, Clone, Default)]
386pub struct ServerToolsConfig {
387    pub web_search: Option<crate::types::WebSearchTool>,
388    pub web_fetch: Option<crate::types::WebFetchTool>,
389}
390
391impl ServerToolsConfig {
392    pub fn web_search() -> Self {
393        Self {
394            web_search: Some(crate::types::WebSearchTool::default()),
395            web_fetch: None,
396        }
397    }
398
399    pub fn web_fetch() -> Self {
400        Self {
401            web_search: None,
402            web_fetch: Some(crate::types::WebFetchTool::default()),
403        }
404    }
405
406    pub fn all() -> Self {
407        Self {
408            web_search: Some(crate::types::WebSearchTool::default()),
409            web_fetch: Some(crate::types::WebFetchTool::default()),
410        }
411    }
412
413    pub fn with_web_search(mut self, config: crate::types::WebSearchTool) -> Self {
414        self.web_search = Some(config);
415        self
416    }
417
418    pub fn with_web_fetch(mut self, config: crate::types::WebFetchTool) -> Self {
419        self.web_fetch = Some(config);
420        self
421    }
422}
423
424/// Complete agent configuration combining all domain configs.
425#[derive(Debug, Clone, Default)]
426pub struct AgentConfig {
427    pub model: AgentModelConfig,
428    pub execution: ExecutionConfig,
429    pub security: SecurityConfig,
430    pub budget: BudgetConfig,
431    pub prompt: PromptConfig,
432    pub cache: CacheConfig,
433    pub working_dir: Option<PathBuf>,
434    pub server_tools: ServerToolsConfig,
435    pub coding_mode: bool,
436}
437
438impl AgentConfig {
439    pub fn new() -> Self {
440        Self::default()
441    }
442
443    pub fn with_model(mut self, config: AgentModelConfig) -> Self {
444        self.model = config;
445        self
446    }
447
448    pub fn with_execution(mut self, config: ExecutionConfig) -> Self {
449        self.execution = config;
450        self
451    }
452
453    pub fn with_security(mut self, config: SecurityConfig) -> Self {
454        self.security = config;
455        self
456    }
457
458    pub fn with_budget(mut self, config: BudgetConfig) -> Self {
459        self.budget = config;
460        self
461    }
462
463    pub fn with_prompt(mut self, config: PromptConfig) -> Self {
464        self.prompt = config;
465        self
466    }
467
468    pub fn with_cache(mut self, config: CacheConfig) -> Self {
469        self.cache = config;
470        self
471    }
472
473    pub fn with_working_dir(mut self, dir: impl Into<PathBuf>) -> Self {
474        self.working_dir = Some(dir.into());
475        self
476    }
477
478    pub fn with_server_tools(mut self, config: ServerToolsConfig) -> Self {
479        self.server_tools = config;
480        self
481    }
482
483    pub fn with_coding_mode(mut self, enabled: bool) -> Self {
484        self.coding_mode = enabled;
485        self
486    }
487}
488
489#[cfg(test)]
490mod tests {
491    use super::*;
492
493    #[test]
494    fn test_model_config() {
495        let config = AgentModelConfig::new("claude-opus-4")
496            .with_small("claude-haiku")
497            .with_max_tokens(4096);
498
499        assert_eq!(config.primary, "claude-opus-4");
500        assert_eq!(config.small, "claude-haiku");
501        assert_eq!(config.max_tokens, 4096);
502    }
503
504    #[test]
505    fn test_execution_config() {
506        let config = ExecutionConfig::default()
507            .with_max_iterations(50)
508            .with_timeout(Duration::from_secs(600))
509            .with_auto_compact(false);
510
511        assert_eq!(config.max_iterations, 50);
512        assert_eq!(config.timeout, Some(Duration::from_secs(600)));
513        assert!(!config.auto_compact);
514    }
515
516    #[test]
517    fn test_security_config() {
518        let config = SecurityConfig::permissive().with_env("API_KEY", "secret");
519
520        assert_eq!(config.env.get("API_KEY"), Some(&"secret".to_string()));
521    }
522
523    #[test]
524    fn test_budget_config() {
525        let config = BudgetConfig::unlimited()
526            .with_max_cost(10.0)
527            .with_tenant("org-123")
528            .with_fallback("claude-haiku");
529
530        assert_eq!(config.max_cost_usd, Some(10.0));
531        assert_eq!(config.tenant_id, Some("org-123".to_string()));
532        assert_eq!(config.fallback_model, Some("claude-haiku".to_string()));
533    }
534
535    #[test]
536    fn test_agent_config() {
537        let config = AgentConfig::new()
538            .with_model(AgentModelConfig::new("claude-opus-4"))
539            .with_budget(BudgetConfig::unlimited().with_max_cost(5.0))
540            .with_working_dir("/project");
541
542        assert_eq!(config.model.primary, "claude-opus-4");
543        assert_eq!(config.budget.max_cost_usd, Some(5.0));
544        assert_eq!(config.working_dir, Some(PathBuf::from("/project")));
545    }
546
547    #[test]
548    fn test_cache_strategy_default_is_full() {
549        let config = CacheConfig::default();
550        assert_eq!(config.strategy, CacheStrategy::Full);
551        assert_eq!(config.static_ttl, crate::types::CacheTtl::OneHour);
552        assert_eq!(config.message_ttl, crate::types::CacheTtl::FiveMinutes);
553    }
554
555    #[test]
556    fn test_cache_strategy_disabled() {
557        let config = CacheConfig::disabled();
558        assert_eq!(config.strategy, CacheStrategy::Disabled);
559        assert!(!config.strategy.is_enabled());
560        assert!(!config.strategy.cache_system());
561        assert!(!config.strategy.cache_messages());
562    }
563
564    #[test]
565    fn test_cache_strategy_system_only() {
566        let config = CacheConfig::system_only();
567        assert_eq!(config.strategy, CacheStrategy::SystemOnly);
568        assert!(config.strategy.is_enabled());
569        assert!(config.strategy.cache_system());
570        assert!(!config.strategy.cache_messages());
571    }
572
573    #[test]
574    fn test_cache_strategy_messages_only() {
575        let config = CacheConfig::messages_only();
576        assert_eq!(config.strategy, CacheStrategy::MessagesOnly);
577        assert!(config.strategy.is_enabled());
578        assert!(!config.strategy.cache_system());
579        assert!(config.strategy.cache_messages());
580    }
581
582    #[test]
583    fn test_cache_strategy_full() {
584        let config = CacheConfig::default();
585        assert!(config.strategy.is_enabled());
586        assert!(config.strategy.cache_system());
587        assert!(config.strategy.cache_messages());
588    }
589
590    #[test]
591    fn test_cache_config_with_ttl() {
592        let config = CacheConfig::default()
593            .with_static_ttl(crate::types::CacheTtl::FiveMinutes)
594            .with_message_ttl(crate::types::CacheTtl::OneHour);
595
596        assert_eq!(config.static_ttl, crate::types::CacheTtl::FiveMinutes);
597        assert_eq!(config.message_ttl, crate::types::CacheTtl::OneHour);
598    }
599}