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    /// Enable automatic context compaction
61    pub auto_compact: bool,
62    /// Context usage threshold for compaction (0.0-1.0)
63    pub compact_threshold: f32,
64    /// Messages to preserve during compaction
65    pub compact_keep_messages: usize,
66}
67
68impl Default for ExecutionConfig {
69    fn default() -> Self {
70        Self {
71            max_iterations: 100,
72            timeout: Some(Duration::from_secs(300)),
73            auto_compact: true,
74            compact_threshold: crate::types::DEFAULT_COMPACT_THRESHOLD,
75            compact_keep_messages: 4,
76        }
77    }
78}
79
80impl ExecutionConfig {
81    pub fn with_max_iterations(mut self, max: usize) -> Self {
82        self.max_iterations = max;
83        self
84    }
85
86    pub fn with_timeout(mut self, timeout: Duration) -> Self {
87        self.timeout = Some(timeout);
88        self
89    }
90
91    pub fn without_timeout(mut self) -> Self {
92        self.timeout = None;
93        self
94    }
95
96    pub fn with_auto_compact(mut self, enabled: bool) -> Self {
97        self.auto_compact = enabled;
98        self
99    }
100
101    pub fn with_compact_threshold(mut self, threshold: f32) -> Self {
102        self.compact_threshold = threshold.clamp(0.0, 1.0);
103        self
104    }
105
106    pub fn with_compact_keep_messages(mut self, count: usize) -> Self {
107        self.compact_keep_messages = count;
108        self
109    }
110}
111
112/// Security and permission configuration.
113#[derive(Debug, Clone, Default)]
114pub struct SecurityConfig {
115    /// Tool permission policy
116    pub permission_policy: PermissionPolicy,
117    /// Tool access control
118    pub tool_access: ToolAccess,
119    /// Environment variables for tool execution
120    pub env: HashMap<String, String>,
121}
122
123impl SecurityConfig {
124    pub fn permissive() -> Self {
125        Self {
126            permission_policy: PermissionPolicy::permissive(),
127            tool_access: ToolAccess::All,
128            ..Default::default()
129        }
130    }
131
132    pub fn read_only() -> Self {
133        Self {
134            permission_policy: PermissionPolicy::read_only(),
135            tool_access: ToolAccess::only(["Read", "Glob", "Grep", "Task", "TaskOutput"]),
136            ..Default::default()
137        }
138    }
139
140    pub fn with_permission_policy(mut self, policy: PermissionPolicy) -> Self {
141        self.permission_policy = policy;
142        self
143    }
144
145    pub fn with_tool_access(mut self, access: ToolAccess) -> Self {
146        self.tool_access = access;
147        self
148    }
149
150    pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
151        self.env.insert(key.into(), value.into());
152        self
153    }
154
155    pub fn with_envs(
156        mut self,
157        vars: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
158    ) -> Self {
159        for (k, v) in vars {
160            self.env.insert(k.into(), v.into());
161        }
162        self
163    }
164}
165
166/// Budget and cost control configuration.
167#[derive(Debug, Clone, Default)]
168pub struct BudgetConfig {
169    /// Maximum cost in USD
170    pub max_cost_usd: Option<f64>,
171    /// Tenant identifier for multi-tenant tracking
172    pub tenant_id: Option<String>,
173    /// Model to fall back to when budget exceeded
174    pub fallback_model: Option<String>,
175}
176
177impl BudgetConfig {
178    pub fn unlimited() -> Self {
179        Self::default()
180    }
181
182    pub fn with_max_cost(mut self, usd: f64) -> Self {
183        self.max_cost_usd = Some(usd);
184        self
185    }
186
187    pub fn with_tenant(mut self, tenant_id: impl Into<String>) -> Self {
188        self.tenant_id = Some(tenant_id.into());
189        self
190    }
191
192    pub fn with_fallback(mut self, model: impl Into<String>) -> Self {
193        self.fallback_model = Some(model.into());
194        self
195    }
196}
197
198/// Prompt and output configuration.
199#[derive(Debug, Clone, Default)]
200pub struct PromptConfig {
201    /// Custom system prompt
202    pub system_prompt: Option<String>,
203    /// How to apply system prompt
204    pub system_prompt_mode: SystemPromptMode,
205    /// Output style customization
206    pub output_style: Option<OutputStyle>,
207    /// Structured output schema
208    pub output_schema: Option<serde_json::Value>,
209}
210
211#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
212pub enum SystemPromptMode {
213    /// Replace default system prompt
214    #[default]
215    Replace,
216    /// Append to default system prompt
217    Append,
218}
219
220impl PromptConfig {
221    pub fn with_system_prompt(mut self, prompt: impl Into<String>) -> Self {
222        self.system_prompt = Some(prompt.into());
223        self
224    }
225
226    pub fn with_append_mode(mut self) -> Self {
227        self.system_prompt_mode = SystemPromptMode::Append;
228        self
229    }
230
231    pub fn with_output_style(mut self, style: OutputStyle) -> Self {
232        self.output_style = Some(style);
233        self
234    }
235
236    pub fn with_output_schema(mut self, schema: serde_json::Value) -> Self {
237        self.output_schema = Some(schema);
238        self
239    }
240
241    pub fn with_structured_output<T: schemars::JsonSchema>(mut self) -> Self {
242        let schema = schemars::schema_for!(T);
243        self.output_schema = serde_json::to_value(schema).ok();
244        self
245    }
246}
247
248/// Server-side tools configuration.
249///
250/// Anthropic's built-in server-side tools (Brave Search, web fetch).
251/// These are automatically enabled when "WebSearch" or "WebFetch" are in ToolAccess.
252#[derive(Debug, Clone, Default)]
253pub struct ServerToolsConfig {
254    pub web_search: Option<crate::types::WebSearchTool>,
255    pub web_fetch: Option<crate::types::WebFetchTool>,
256}
257
258impl ServerToolsConfig {
259    pub fn web_search() -> Self {
260        Self {
261            web_search: Some(crate::types::WebSearchTool::default()),
262            web_fetch: None,
263        }
264    }
265
266    pub fn web_fetch() -> Self {
267        Self {
268            web_search: None,
269            web_fetch: Some(crate::types::WebFetchTool::default()),
270        }
271    }
272
273    pub fn all() -> Self {
274        Self {
275            web_search: Some(crate::types::WebSearchTool::default()),
276            web_fetch: Some(crate::types::WebFetchTool::default()),
277        }
278    }
279
280    pub fn with_web_search(mut self, config: crate::types::WebSearchTool) -> Self {
281        self.web_search = Some(config);
282        self
283    }
284
285    pub fn with_web_fetch(mut self, config: crate::types::WebFetchTool) -> Self {
286        self.web_fetch = Some(config);
287        self
288    }
289}
290
291/// Complete agent configuration combining all domain configs.
292#[derive(Debug, Clone, Default)]
293pub struct AgentConfig {
294    pub model: AgentModelConfig,
295    pub execution: ExecutionConfig,
296    pub security: SecurityConfig,
297    pub budget: BudgetConfig,
298    pub prompt: PromptConfig,
299    pub working_dir: Option<PathBuf>,
300    /// Server-side tools configuration.
301    ///
302    /// When ToolAccess allows "WebSearch" or "WebFetch", these server-side tools
303    /// are injected into API requests. Pricing: $10 per 1,000 searches.
304    pub server_tools: ServerToolsConfig,
305    /// Enable coding mode (captures environment context: cwd, git branch, etc.)
306    pub coding_mode: bool,
307}
308
309impl AgentConfig {
310    pub fn new() -> Self {
311        Self::default()
312    }
313
314    pub fn with_model(mut self, config: AgentModelConfig) -> Self {
315        self.model = config;
316        self
317    }
318
319    pub fn with_execution(mut self, config: ExecutionConfig) -> Self {
320        self.execution = config;
321        self
322    }
323
324    pub fn with_security(mut self, config: SecurityConfig) -> Self {
325        self.security = config;
326        self
327    }
328
329    pub fn with_budget(mut self, config: BudgetConfig) -> Self {
330        self.budget = config;
331        self
332    }
333
334    pub fn with_prompt(mut self, config: PromptConfig) -> Self {
335        self.prompt = config;
336        self
337    }
338
339    pub fn with_working_dir(mut self, dir: impl Into<PathBuf>) -> Self {
340        self.working_dir = Some(dir.into());
341        self
342    }
343
344    pub fn with_server_tools(mut self, config: ServerToolsConfig) -> Self {
345        self.server_tools = config;
346        self
347    }
348
349    pub fn with_coding_mode(mut self, enabled: bool) -> Self {
350        self.coding_mode = enabled;
351        self
352    }
353}
354
355#[cfg(test)]
356mod tests {
357    use super::*;
358
359    #[test]
360    fn test_model_config() {
361        let config = AgentModelConfig::new("claude-opus-4")
362            .with_small("claude-haiku")
363            .with_max_tokens(4096);
364
365        assert_eq!(config.primary, "claude-opus-4");
366        assert_eq!(config.small, "claude-haiku");
367        assert_eq!(config.max_tokens, 4096);
368    }
369
370    #[test]
371    fn test_execution_config() {
372        let config = ExecutionConfig::default()
373            .with_max_iterations(50)
374            .with_timeout(Duration::from_secs(600))
375            .with_auto_compact(false);
376
377        assert_eq!(config.max_iterations, 50);
378        assert_eq!(config.timeout, Some(Duration::from_secs(600)));
379        assert!(!config.auto_compact);
380    }
381
382    #[test]
383    fn test_security_config() {
384        let config = SecurityConfig::permissive().with_env("API_KEY", "secret");
385
386        assert_eq!(config.env.get("API_KEY"), Some(&"secret".to_string()));
387    }
388
389    #[test]
390    fn test_budget_config() {
391        let config = BudgetConfig::unlimited()
392            .with_max_cost(10.0)
393            .with_tenant("org-123")
394            .with_fallback("claude-haiku");
395
396        assert_eq!(config.max_cost_usd, Some(10.0));
397        assert_eq!(config.tenant_id, Some("org-123".to_string()));
398        assert_eq!(config.fallback_model, Some("claude-haiku".to_string()));
399    }
400
401    #[test]
402    fn test_agent_config() {
403        let config = AgentConfig::new()
404            .with_model(AgentModelConfig::new("claude-opus-4"))
405            .with_budget(BudgetConfig::unlimited().with_max_cost(5.0))
406            .with_working_dir("/project");
407
408        assert_eq!(config.model.primary, "claude-opus-4");
409        assert_eq!(config.budget.max_cost_usd, Some(5.0));
410        assert_eq!(config.working_dir, Some(PathBuf::from("/project")));
411    }
412}