1use 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#[derive(Debug, Clone)]
16pub struct AgentModelConfig {
17 pub primary: String,
19 pub small: String,
21 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#[derive(Debug, Clone)]
56pub struct ExecutionConfig {
57 pub max_iterations: usize,
59 pub timeout: Option<Duration>,
61 pub chunk_timeout: Duration,
63 pub auto_compact: bool,
65 pub compact_threshold: f32,
67 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#[derive(Debug, Clone, Default)]
123pub struct SecurityConfig {
124 pub permission_policy: PermissionPolicy,
126 pub tool_access: ToolAccess,
128 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#[derive(Debug, Clone, Default)]
177pub struct BudgetConfig {
178 pub max_cost_usd: Option<f64>,
180 pub tenant_id: Option<String>,
182 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#[derive(Debug, Clone, Default)]
209pub struct PromptConfig {
210 pub system_prompt: Option<String>,
212 pub system_prompt_mode: SystemPromptMode,
214 pub output_style: Option<OutputStyle>,
216 pub output_schema: Option<serde_json::Value>,
218}
219
220#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
221pub enum SystemPromptMode {
222 #[default]
224 Replace,
225 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
262pub enum CacheStrategy {
263 Disabled,
265 SystemOnly,
267 MessagesOnly,
269 #[default]
271 Full,
272}
273
274impl CacheStrategy {
275 pub fn cache_system(&self) -> bool {
277 matches!(self, Self::SystemOnly | Self::Full)
278 }
279
280 pub fn cache_messages(&self) -> bool {
282 matches!(self, Self::MessagesOnly | Self::Full)
283 }
284
285 pub fn is_enabled(&self) -> bool {
287 !matches!(self, Self::Disabled)
288 }
289}
290
291#[derive(Debug, Clone)]
298pub struct CacheConfig {
299 pub strategy: CacheStrategy,
301 pub static_ttl: crate::types::CacheTtl,
303 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 pub fn disabled() -> Self {
320 Self {
321 strategy: CacheStrategy::Disabled,
322 ..Default::default()
323 }
324 }
325
326 pub fn system_only() -> Self {
328 Self {
329 strategy: CacheStrategy::SystemOnly,
330 ..Default::default()
331 }
332 }
333
334 pub fn messages_only() -> Self {
336 Self {
337 strategy: CacheStrategy::MessagesOnly,
338 ..Default::default()
339 }
340 }
341
342 pub fn with_strategy(mut self, strategy: CacheStrategy) -> Self {
344 self.strategy = strategy;
345 self
346 }
347
348 pub fn with_static_ttl(mut self, ttl: crate::types::CacheTtl) -> Self {
350 self.static_ttl = ttl;
351 self
352 }
353
354 pub fn with_message_ttl(mut self, ttl: crate::types::CacheTtl) -> Self {
356 self.message_ttl = ttl;
357 self
358 }
359
360 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#[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#[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}