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