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::output_style::OutputStyle;
10use crate::permissions::PermissionPolicy;
11use crate::tools::ToolAccess;
12
13/// Model-related configuration.
14#[derive(Debug, Clone)]
15pub struct AgentModelConfig {
16    /// Primary model for main operations
17    pub primary: String,
18    /// Smaller model for quick operations
19    pub small: String,
20    /// Maximum tokens per response
21    pub max_tokens: u32,
22}
23
24impl Default for AgentModelConfig {
25    fn default() -> Self {
26        Self {
27            primary: crate::client::DEFAULT_MODEL.to_string(),
28            small: crate::client::DEFAULT_SMALL_MODEL.to_string(),
29            max_tokens: 8192,
30        }
31    }
32}
33
34impl AgentModelConfig {
35    pub fn new(primary: impl Into<String>) -> Self {
36        Self {
37            primary: primary.into(),
38            ..Default::default()
39        }
40    }
41
42    pub fn with_small(mut self, small: impl Into<String>) -> Self {
43        self.small = small.into();
44        self
45    }
46
47    pub fn with_max_tokens(mut self, tokens: u32) -> Self {
48        self.max_tokens = tokens;
49        self
50    }
51}
52
53/// Execution behavior configuration.
54#[derive(Debug, Clone)]
55pub struct ExecutionConfig {
56    /// Maximum agentic loop iterations
57    pub max_iterations: usize,
58    /// Overall execution timeout
59    pub timeout: Option<Duration>,
60    /// Timeout between streaming chunks (detects stalled connections)
61    pub chunk_timeout: Duration,
62    /// Enable automatic context compaction
63    pub auto_compact: bool,
64    /// Context usage threshold for compaction (0.0-1.0)
65    pub compact_threshold: f32,
66    /// Messages to preserve during compaction
67    pub compact_keep_messages: usize,
68}
69
70impl Default for ExecutionConfig {
71    fn default() -> Self {
72        Self {
73            max_iterations: 100,
74            timeout: Some(Duration::from_secs(300)),
75            chunk_timeout: Duration::from_secs(60),
76            auto_compact: true,
77            compact_threshold: crate::types::DEFAULT_COMPACT_THRESHOLD,
78            compact_keep_messages: 4,
79        }
80    }
81}
82
83impl ExecutionConfig {
84    pub fn with_max_iterations(mut self, max: usize) -> Self {
85        self.max_iterations = max;
86        self
87    }
88
89    pub fn with_timeout(mut self, timeout: Duration) -> Self {
90        self.timeout = Some(timeout);
91        self
92    }
93
94    pub fn without_timeout(mut self) -> Self {
95        self.timeout = None;
96        self
97    }
98
99    pub fn with_chunk_timeout(mut self, timeout: Duration) -> Self {
100        self.chunk_timeout = timeout;
101        self
102    }
103
104    pub fn with_auto_compact(mut self, enabled: bool) -> Self {
105        self.auto_compact = enabled;
106        self
107    }
108
109    pub fn with_compact_threshold(mut self, threshold: f32) -> Self {
110        self.compact_threshold = threshold.clamp(0.0, 1.0);
111        self
112    }
113
114    pub fn with_compact_keep_messages(mut self, count: usize) -> Self {
115        self.compact_keep_messages = count;
116        self
117    }
118}
119
120/// Security and permission configuration.
121#[derive(Debug, Clone, Default)]
122pub struct SecurityConfig {
123    /// Tool permission policy
124    pub permission_policy: PermissionPolicy,
125    /// Tool access control
126    pub tool_access: ToolAccess,
127    /// Environment variables for tool execution
128    pub env: HashMap<String, String>,
129}
130
131impl SecurityConfig {
132    pub fn permissive() -> Self {
133        Self {
134            permission_policy: PermissionPolicy::permissive(),
135            tool_access: ToolAccess::All,
136            ..Default::default()
137        }
138    }
139
140    pub fn read_only() -> Self {
141        Self {
142            permission_policy: PermissionPolicy::read_only(),
143            tool_access: ToolAccess::only(["Read", "Glob", "Grep", "Task", "TaskOutput"]),
144            ..Default::default()
145        }
146    }
147
148    pub fn with_permission_policy(mut self, policy: PermissionPolicy) -> Self {
149        self.permission_policy = policy;
150        self
151    }
152
153    pub fn with_tool_access(mut self, access: ToolAccess) -> Self {
154        self.tool_access = access;
155        self
156    }
157
158    pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
159        self.env.insert(key.into(), value.into());
160        self
161    }
162
163    pub fn with_envs(
164        mut self,
165        vars: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
166    ) -> Self {
167        for (k, v) in vars {
168            self.env.insert(k.into(), v.into());
169        }
170        self
171    }
172}
173
174/// Budget and cost control configuration.
175#[derive(Debug, Clone, Default)]
176pub struct BudgetConfig {
177    /// Maximum cost in USD
178    pub max_cost_usd: Option<f64>,
179    /// Tenant identifier for multi-tenant tracking
180    pub tenant_id: Option<String>,
181    /// Model to fall back to when budget exceeded
182    pub fallback_model: Option<String>,
183}
184
185impl BudgetConfig {
186    pub fn unlimited() -> Self {
187        Self::default()
188    }
189
190    pub fn with_max_cost(mut self, usd: f64) -> Self {
191        self.max_cost_usd = Some(usd);
192        self
193    }
194
195    pub fn with_tenant(mut self, tenant_id: impl Into<String>) -> Self {
196        self.tenant_id = Some(tenant_id.into());
197        self
198    }
199
200    pub fn with_fallback(mut self, model: impl Into<String>) -> Self {
201        self.fallback_model = Some(model.into());
202        self
203    }
204}
205
206/// Prompt and output configuration.
207#[derive(Debug, Clone, Default)]
208pub struct PromptConfig {
209    /// Custom system prompt
210    pub system_prompt: Option<String>,
211    /// How to apply system prompt
212    pub system_prompt_mode: SystemPromptMode,
213    /// Output style customization
214    pub output_style: Option<OutputStyle>,
215    /// Structured output schema
216    pub output_schema: Option<serde_json::Value>,
217}
218
219#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
220pub enum SystemPromptMode {
221    /// Replace default system prompt
222    #[default]
223    Replace,
224    /// Append to default system prompt
225    Append,
226}
227
228impl PromptConfig {
229    pub fn with_system_prompt(mut self, prompt: impl Into<String>) -> Self {
230        self.system_prompt = Some(prompt.into());
231        self
232    }
233
234    pub fn with_append_mode(mut self) -> Self {
235        self.system_prompt_mode = SystemPromptMode::Append;
236        self
237    }
238
239    pub fn with_output_style(mut self, style: OutputStyle) -> Self {
240        self.output_style = Some(style);
241        self
242    }
243
244    pub fn with_output_schema(mut self, schema: serde_json::Value) -> Self {
245        self.output_schema = Some(schema);
246        self
247    }
248
249    pub fn with_structured_output<T: schemars::JsonSchema>(mut self) -> Self {
250        let schema = schemars::schema_for!(T);
251        self.output_schema = serde_json::to_value(schema).ok();
252        self
253    }
254}
255
256/// Cache configuration for prompt caching.
257#[derive(Debug, Clone)]
258pub struct CacheConfig {
259    pub enabled: bool,
260    pub system_prompt_cache: bool,
261    pub message_cache: bool,
262}
263
264impl Default for CacheConfig {
265    fn default() -> Self {
266        Self {
267            enabled: true,
268            system_prompt_cache: true,
269            message_cache: true,
270        }
271    }
272}
273
274impl CacheConfig {
275    pub fn disabled() -> Self {
276        Self {
277            enabled: false,
278            system_prompt_cache: false,
279            message_cache: false,
280        }
281    }
282
283    pub fn system_only() -> Self {
284        Self {
285            enabled: true,
286            system_prompt_cache: true,
287            message_cache: false,
288        }
289    }
290
291    pub fn with_system_cache(mut self, enabled: bool) -> Self {
292        self.system_prompt_cache = enabled;
293        self
294    }
295
296    pub fn with_message_cache(mut self, enabled: bool) -> Self {
297        self.message_cache = enabled;
298        self
299    }
300}
301
302/// Server-side tools configuration.
303///
304/// Anthropic's built-in server-side tools (Brave Search, web fetch).
305/// These are automatically enabled when "WebSearch" or "WebFetch" are in ToolAccess.
306#[derive(Debug, Clone, Default)]
307pub struct ServerToolsConfig {
308    pub web_search: Option<crate::types::WebSearchTool>,
309    pub web_fetch: Option<crate::types::WebFetchTool>,
310}
311
312impl ServerToolsConfig {
313    pub fn web_search() -> Self {
314        Self {
315            web_search: Some(crate::types::WebSearchTool::default()),
316            web_fetch: None,
317        }
318    }
319
320    pub fn web_fetch() -> Self {
321        Self {
322            web_search: None,
323            web_fetch: Some(crate::types::WebFetchTool::default()),
324        }
325    }
326
327    pub fn all() -> Self {
328        Self {
329            web_search: Some(crate::types::WebSearchTool::default()),
330            web_fetch: Some(crate::types::WebFetchTool::default()),
331        }
332    }
333
334    pub fn with_web_search(mut self, config: crate::types::WebSearchTool) -> Self {
335        self.web_search = Some(config);
336        self
337    }
338
339    pub fn with_web_fetch(mut self, config: crate::types::WebFetchTool) -> Self {
340        self.web_fetch = Some(config);
341        self
342    }
343}
344
345/// Complete agent configuration combining all domain configs.
346#[derive(Debug, Clone, Default)]
347pub struct AgentConfig {
348    pub model: AgentModelConfig,
349    pub execution: ExecutionConfig,
350    pub security: SecurityConfig,
351    pub budget: BudgetConfig,
352    pub prompt: PromptConfig,
353    pub cache: CacheConfig,
354    pub working_dir: Option<PathBuf>,
355    pub server_tools: ServerToolsConfig,
356    pub coding_mode: bool,
357}
358
359impl AgentConfig {
360    pub fn new() -> Self {
361        Self::default()
362    }
363
364    pub fn with_model(mut self, config: AgentModelConfig) -> Self {
365        self.model = config;
366        self
367    }
368
369    pub fn with_execution(mut self, config: ExecutionConfig) -> Self {
370        self.execution = config;
371        self
372    }
373
374    pub fn with_security(mut self, config: SecurityConfig) -> Self {
375        self.security = config;
376        self
377    }
378
379    pub fn with_budget(mut self, config: BudgetConfig) -> Self {
380        self.budget = config;
381        self
382    }
383
384    pub fn with_prompt(mut self, config: PromptConfig) -> Self {
385        self.prompt = config;
386        self
387    }
388
389    pub fn with_cache(mut self, config: CacheConfig) -> Self {
390        self.cache = config;
391        self
392    }
393
394    pub fn with_working_dir(mut self, dir: impl Into<PathBuf>) -> Self {
395        self.working_dir = Some(dir.into());
396        self
397    }
398
399    pub fn with_server_tools(mut self, config: ServerToolsConfig) -> Self {
400        self.server_tools = config;
401        self
402    }
403
404    pub fn with_coding_mode(mut self, enabled: bool) -> Self {
405        self.coding_mode = enabled;
406        self
407    }
408}
409
410#[cfg(test)]
411mod tests {
412    use super::*;
413
414    #[test]
415    fn test_model_config() {
416        let config = AgentModelConfig::new("claude-opus-4")
417            .with_small("claude-haiku")
418            .with_max_tokens(4096);
419
420        assert_eq!(config.primary, "claude-opus-4");
421        assert_eq!(config.small, "claude-haiku");
422        assert_eq!(config.max_tokens, 4096);
423    }
424
425    #[test]
426    fn test_execution_config() {
427        let config = ExecutionConfig::default()
428            .with_max_iterations(50)
429            .with_timeout(Duration::from_secs(600))
430            .with_auto_compact(false);
431
432        assert_eq!(config.max_iterations, 50);
433        assert_eq!(config.timeout, Some(Duration::from_secs(600)));
434        assert!(!config.auto_compact);
435    }
436
437    #[test]
438    fn test_security_config() {
439        let config = SecurityConfig::permissive().with_env("API_KEY", "secret");
440
441        assert_eq!(config.env.get("API_KEY"), Some(&"secret".to_string()));
442    }
443
444    #[test]
445    fn test_budget_config() {
446        let config = BudgetConfig::unlimited()
447            .with_max_cost(10.0)
448            .with_tenant("org-123")
449            .with_fallback("claude-haiku");
450
451        assert_eq!(config.max_cost_usd, Some(10.0));
452        assert_eq!(config.tenant_id, Some("org-123".to_string()));
453        assert_eq!(config.fallback_model, Some("claude-haiku".to_string()));
454    }
455
456    #[test]
457    fn test_agent_config() {
458        let config = AgentConfig::new()
459            .with_model(AgentModelConfig::new("claude-opus-4"))
460            .with_budget(BudgetConfig::unlimited().with_max_cost(5.0))
461            .with_working_dir("/project");
462
463        assert_eq!(config.model.primary, "claude-opus-4");
464        assert_eq!(config.budget.max_cost_usd, Some(5.0));
465        assert_eq!(config.working_dir, Some(PathBuf::from("/project")));
466    }
467}