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 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#[derive(Debug, Clone)]
64pub struct ExecutionConfig {
65 pub max_iterations: usize,
67 pub timeout: Option<Duration>,
69 pub chunk_timeout: Duration,
71 pub auto_compact: bool,
73 pub compact_threshold: f32,
75 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#[derive(Debug, Clone, Default)]
131pub struct SecurityConfig {
132 pub permission_policy: PermissionPolicy,
134 pub tool_access: ToolAccess,
136 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#[derive(Debug, Clone, Default)]
185pub struct BudgetConfig {
186 pub max_cost_usd: Option<f64>,
188 pub tenant_id: Option<String>,
190 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#[derive(Debug, Clone, Default)]
217pub struct PromptConfig {
218 pub system_prompt: Option<String>,
220 pub system_prompt_mode: SystemPromptMode,
222 pub output_style: Option<OutputStyle>,
224 pub output_schema: Option<serde_json::Value>,
226}
227
228#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
229pub enum SystemPromptMode {
230 #[default]
232 Replace,
233 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
270pub enum CacheStrategy {
271 Disabled,
273 SystemOnly,
275 MessagesOnly,
277 #[default]
279 Full,
280}
281
282impl CacheStrategy {
283 pub fn cache_system(&self) -> bool {
285 matches!(self, Self::SystemOnly | Self::Full)
286 }
287
288 pub fn cache_messages(&self) -> bool {
290 matches!(self, Self::MessagesOnly | Self::Full)
291 }
292
293 pub fn is_enabled(&self) -> bool {
295 !matches!(self, Self::Disabled)
296 }
297}
298
299#[derive(Debug, Clone)]
306pub struct CacheConfig {
307 pub strategy: CacheStrategy,
309 pub static_ttl: crate::types::CacheTtl,
311 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 pub fn disabled() -> Self {
328 Self {
329 strategy: CacheStrategy::Disabled,
330 ..Default::default()
331 }
332 }
333
334 pub fn system_only() -> Self {
336 Self {
337 strategy: CacheStrategy::SystemOnly,
338 ..Default::default()
339 }
340 }
341
342 pub fn messages_only() -> Self {
344 Self {
345 strategy: CacheStrategy::MessagesOnly,
346 ..Default::default()
347 }
348 }
349
350 pub fn with_strategy(mut self, strategy: CacheStrategy) -> Self {
352 self.strategy = strategy;
353 self
354 }
355
356 pub fn with_static_ttl(mut self, ttl: crate::types::CacheTtl) -> Self {
358 self.static_ttl = ttl;
359 self
360 }
361
362 pub fn with_message_ttl(mut self, ttl: crate::types::CacheTtl) -> Self {
364 self.message_ttl = ttl;
365 self
366 }
367
368 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#[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#[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}