1use 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#[derive(Debug, Clone)]
15pub struct AgentModelConfig {
16 pub primary: String,
18 pub small: String,
20 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#[derive(Debug, Clone)]
55pub struct ExecutionConfig {
56 pub max_iterations: usize,
58 pub timeout: Option<Duration>,
60 pub auto_compact: bool,
62 pub compact_threshold: f32,
64 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#[derive(Debug, Clone, Default)]
114pub struct SecurityConfig {
115 pub permission_policy: PermissionPolicy,
117 pub tool_access: ToolAccess,
119 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#[derive(Debug, Clone, Default)]
168pub struct BudgetConfig {
169 pub max_cost_usd: Option<f64>,
171 pub tenant_id: Option<String>,
173 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#[derive(Debug, Clone, Default)]
200pub struct PromptConfig {
201 pub system_prompt: Option<String>,
203 pub system_prompt_mode: SystemPromptMode,
205 pub output_style: Option<OutputStyle>,
207 pub output_schema: Option<serde_json::Value>,
209}
210
211#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
212pub enum SystemPromptMode {
213 #[default]
215 Replace,
216 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#[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#[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 pub server_tools: ServerToolsConfig,
305 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}