1use std::collections::HashMap;
8use std::path::PathBuf;
9use std::sync::Arc;
10use std::sync::atomic::{AtomicBool, Ordering};
11
12use crate::agent::compaction::{HistoryCompactionStrategy, SummaryHistoryCompaction};
13use crate::agent::policies::PolicyRegistry;
14use crate::agent::prompt::PromptProfile;
15use crate::diagnostics::ToolDiagnosticsSink;
16use crate::error::AgentError;
17use crate::storage::Storage;
18use crate::tools::approval::ApprovalHandler;
19
20pub fn platform_base_env() -> HashMap<String, String> {
24 let mut env = HashMap::new();
25 for key in platform_base_env_keys() {
26 if let Some((actual_key, value)) =
27 std::env::vars().find(|(name, _)| name.eq_ignore_ascii_case(key))
28 {
29 env.insert(actual_key, value);
30 }
31 }
32 env
33}
34
35#[cfg(windows)]
36fn platform_base_env_keys() -> &'static [&'static str] {
37 &[
38 "Path",
39 "SystemRoot",
40 "WINDIR",
41 "USERPROFILE",
42 "TEMP",
43 "TMP",
44 "APPDATA",
45 "LOCALAPPDATA",
46 "ComSpec",
47 "PATHEXT",
48 "PSModulePath",
49 ]
50}
51
52#[cfg(not(windows))]
53fn platform_base_env_keys() -> &'static [&'static str] {
54 &["PATH", "HOME"]
55}
56
57#[derive(Clone, Debug)]
63pub struct CancellationState {
64 flag: Arc<AtomicBool>,
65 notify: Arc<tokio::sync::Notify>,
66}
67
68impl CancellationState {
69 pub fn new() -> Self {
70 Self {
71 flag: Arc::new(AtomicBool::new(false)),
72 notify: Arc::new(tokio::sync::Notify::new()),
73 }
74 }
75
76 pub fn from_flag(flag: Arc<AtomicBool>) -> Self {
77 Self { flag, notify: Arc::new(tokio::sync::Notify::new()) }
78 }
79
80 pub fn flag(&self) -> Arc<AtomicBool> {
81 Arc::clone(&self.flag)
82 }
83
84 pub fn is_cancelled(&self) -> bool {
85 self.flag.load(Ordering::Relaxed)
86 }
87
88 pub fn cancel(&self) {
89 self.flag.store(true, Ordering::Relaxed);
90 self.notify.notify_waiters();
91 }
92
93 pub fn reset(&self) {
94 self.flag.store(false, Ordering::Relaxed);
95 }
96
97 pub async fn cancelled(&self) {
98 if self.is_cancelled() {
99 return;
100 }
101
102 loop {
103 self.notify.notified().await;
104 if self.is_cancelled() {
105 return;
106 }
107 }
108 }
109}
110
111impl Default for CancellationState {
112 fn default() -> Self {
113 Self::new()
114 }
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
130pub enum TaskPath {
131 Fast,
133 #[default]
135 Standard,
136 Heavy,
138}
139
140#[derive(Clone)]
145pub struct AgentConfig {
146 pub path: TaskPath,
150 pub base_system_prompt: Option<String>,
153 pub prompt_assembly: Option<std::sync::Arc<crate::agent::prompt::PromptAssembly>>,
156 pub prompt_profile: PromptProfile,
159 pub max_iterations: Option<usize>,
165 pub cwd: PathBuf,
167 pub env: HashMap<String, String>,
169 pub max_tool_result_chars: usize,
172 pub max_message_tool_results_chars: usize,
178 pub policies: Arc<PolicyRegistry>,
180 pub storage: Option<Arc<dyn Storage>>,
182 pub tool_diagnostics: Option<Arc<dyn ToolDiagnosticsSink>>,
184 pub compaction: Option<Arc<dyn HistoryCompactionStrategy>>,
186 pub permission_engine: Option<crate::tools::permissions::PermissionEngine>,
188 pub approval_handler: Option<Arc<dyn ApprovalHandler>>,
190 pub tool_concurrency_limit: usize,
192 pub token_budget: Option<TokenBudget>,
194 pub retry: RetryConfig,
197 pub auto_validate_schema: bool,
200 pub cancellation: CancellationState,
203 pub tool_timeout_ms: Option<u64>,
207 pub max_file_read_bytes: usize,
210 pub skill_registry: Option<Arc<crate::knowledge::skills::SkillRegistry>>,
213 pub plugin_registry: Option<Arc<crate::integrations::plugin::PluginRegistry>>,
216 pub task_manager: Option<Arc<crate::knowledge::tasks::TaskManager>>,
219 pub memory_injector: Option<Arc<crate::agent::context::MemoryInjector>>,
224 pub skill_injector: Option<Arc<crate::agent::context::SkillInjector>>,
228 pub mcp_manager: Option<Arc<crate::integrations::mcp::McpManager>>,
231 pub event_channel: Option<crate::integrations::event_channel::EventChannelConfig>,
236}
237
238impl std::fmt::Debug for AgentConfig {
239 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
240 let env_keys: Vec<&String> = self.env.keys().collect();
243 f.debug_struct("AgentConfig")
244 .field("path", &self.path)
245 .field("base_system_prompt", &self.base_system_prompt)
246 .field("prompt_assembly", &self.prompt_assembly.as_ref().map(|_| "<set>"))
247 .field("prompt_profile", &self.prompt_profile)
248 .field("max_iterations", &self.max_iterations)
249 .field("cwd", &self.cwd)
250 .field("env", &format!("{} keys: [REDACTED]", env_keys.len()))
251 .field("max_tool_result_chars", &self.max_tool_result_chars)
252 .field("max_message_tool_results_chars", &self.max_message_tool_results_chars)
253 .field("policies", &self.policies)
254 .field("storage", &self.storage)
255 .field("tool_diagnostics", &self.tool_diagnostics.as_ref().map(|_| "<set>"))
256 .field("compaction", &self.compaction)
257 .field("permission_engine", &self.permission_engine)
258 .field("approval_handler", &self.approval_handler)
259 .field("tool_concurrency_limit", &self.tool_concurrency_limit)
260 .field("token_budget", &self.token_budget)
261 .field("retry", &self.retry)
262 .field("auto_validate_schema", &self.auto_validate_schema)
263 .field("cancellation", &self.cancellation)
264 .field("tool_timeout_ms", &self.tool_timeout_ms)
265 .field("max_file_read_bytes", &self.max_file_read_bytes)
266 .field("skill_registry", &self.skill_registry.as_ref().map(|_| "<set>"))
267 .field("plugin_registry", &self.plugin_registry.as_ref().map(|_| "<set>"))
268 .field("task_manager", &self.task_manager.as_ref().map(|_| "<set>"))
269 .field("memory_injector", &self.memory_injector.as_ref().map(|_| "<set>"))
270 .field("skill_injector", &self.skill_injector.as_ref().map(|_| "<set>"))
271 .field("mcp_manager", &self.mcp_manager.as_ref().map(|_| "<set>"))
272 .field("event_channel", &self.event_channel)
273 .finish()
274 }
275}
276
277impl Default for AgentConfig {
278 fn default() -> Self {
279 Self {
280 path: TaskPath::default(),
281 base_system_prompt: None,
282 prompt_assembly: None,
283 prompt_profile: PromptProfile::default(),
284 max_iterations: None,
285 cwd: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
286 env: platform_base_env(),
291 max_tool_result_chars: 50_000,
292 max_message_tool_results_chars: 300_000,
293 policies: Arc::new(PolicyRegistry::new()),
294 storage: None,
295 tool_diagnostics: None,
296 compaction: Some(Arc::new(SummaryHistoryCompaction {
297 keep_recent: 12,
298 max_summary_input_tokens: 800_000,
299 summary_output_tokens: 4_000,
300 })),
301 permission_engine: None,
302 approval_handler: None,
303 tool_concurrency_limit: 10,
304 token_budget: None,
305 retry: RetryConfig::default(),
306 auto_validate_schema: true,
307 cancellation: CancellationState::new(),
308 tool_timeout_ms: None,
309 max_file_read_bytes: 50 * 1024 * 1024,
310 skill_registry: None,
311 plugin_registry: None,
312 task_manager: None,
313 memory_injector: None,
314 skill_injector: None,
315 mcp_manager: None,
316 event_channel: None,
317 }
318 }
319}
320
321#[derive(Debug, Clone, Copy, PartialEq, Eq)]
331pub struct TokenBudget {
332 pub max_tokens: usize,
334 pub compact_at_tokens: usize,
336}
337
338impl TokenBudget {
339 pub fn new(max_tokens: usize) -> Self {
341 Self { max_tokens, compact_at_tokens: ((max_tokens as f64) * 0.8).ceil() as usize }
342 }
343}
344
345#[derive(Debug, Clone, PartialEq, Eq)]
350pub struct RetryConfig {
351 pub max_retries: usize,
353 pub base_delay_ms: u64,
355 pub max_delay_ms: u64,
357}
358
359impl Default for RetryConfig {
360 fn default() -> Self {
361 Self { max_retries: 3, base_delay_ms: 500, max_delay_ms: 10_000 }
362 }
363}
364
365impl RetryConfig {
366 pub const NONE: Self = Self { max_retries: 0, base_delay_ms: 0, max_delay_ms: 0 };
368
369 pub fn delay_for(&self, attempt: usize) -> std::time::Duration {
371 let shift = attempt.saturating_sub(1).min(63);
374 let delay = self.base_delay_ms.saturating_mul(1u64 << shift);
375 let capped = delay.min(self.max_delay_ms);
376 std::time::Duration::from_millis(capped)
377 }
378}
379
380impl AgentConfig {
381 pub fn request_cancel(&self) {
383 self.cancellation.cancel();
384 }
385
386 pub fn reset_cancel(&self) {
388 self.cancellation.reset();
389 }
390
391 pub fn validate(&self) -> Result<(), AgentError> {
394 if self.max_iterations == Some(0) {
395 return Err(AgentError::Config("max_iterations must be greater than 0".into()));
396 }
397 if self.tool_concurrency_limit == 0 {
398 return Err(AgentError::Config("tool_concurrency_limit must be greater than 0".into()));
399 }
400 if self.max_file_read_bytes == 0 {
401 return Err(AgentError::Config("max_file_read_bytes must be greater than 0".into()));
402 }
403 if self.max_tool_result_chars == 0 {
404 return Err(AgentError::Config("max_tool_result_chars must be greater than 0".into()));
405 }
406 if let Some(budget) = self.token_budget {
407 if budget.max_tokens == 0 {
408 return Err(AgentError::Config(
409 "token_budget.max_tokens must be greater than 0".into(),
410 ));
411 }
412 if budget.compact_at_tokens == 0 {
413 return Err(AgentError::Config(
414 "token_budget.compact_at_tokens must be greater than 0".into(),
415 ));
416 }
417 if budget.compact_at_tokens > budget.max_tokens {
418 return Err(AgentError::Config(
419 "token_budget.compact_at_tokens cannot exceed max_tokens".into(),
420 ));
421 }
422 }
423 Ok(())
424 }
425
426 pub fn with_default_prompt_assembly(
431 mut self,
432 tools: Arc<crate::tools::api::ToolRegistry>,
433 ) -> Result<Self, AgentError> {
434 let assembly = crate::agent::prompt::default_coding_assembly_for_profile(
435 tools,
436 self.cwd.clone(),
437 self.skill_registry.clone(),
438 self.path,
439 self.prompt_profile,
440 );
441 self.base_system_prompt = None;
442 self.prompt_assembly = Some(Arc::new(assembly));
443 Ok(self)
444 }
445
446 pub fn with_path(mut self, path: TaskPath) -> Self {
457 self.path = path;
458 match path {
459 TaskPath::Fast => {
460 self.max_iterations = None;
461 self.tool_timeout_ms = Some(30_000);
462 self.token_budget = None;
464 }
465 TaskPath::Standard => {
466 self.max_iterations = None;
467 self.tool_timeout_ms = None;
468 self.token_budget = Some(TokenBudget::new(900_000));
470 }
471 TaskPath::Heavy => {
472 self.max_iterations = None;
473 self.tool_timeout_ms = Some(60_000);
474 self.token_budget = Some(TokenBudget::new(950_000));
475 self.tool_concurrency_limit = 5;
476 }
477 }
478 self
479 }
480
481 pub fn with_bundled_skills(mut self) -> Self {
483 let mut registry = crate::knowledge::skills::SkillRegistry::new();
484 registry.load_bundled_skills();
485 let registry = Arc::new(registry);
486 self.skill_injector =
487 Some(Arc::new(crate::agent::context::SkillInjector::new(Arc::clone(®istry))));
488 self.skill_registry = Some(registry);
489 self
490 }
491
492 pub fn apply_plugins(
499 &self,
500 mut tools: crate::tools::api::ToolRegistry,
501 mut policies: crate::agent::policies::PolicyRegistry,
502 mut skills: crate::knowledge::skills::SkillRegistry,
503 mut mcp: crate::integrations::mcp::McpManager,
504 mut prompt: crate::agent::prompt::PromptAssembly,
505 ) -> (
506 crate::tools::api::ToolRegistry,
507 crate::agent::policies::PolicyRegistry,
508 crate::knowledge::skills::SkillRegistry,
509 crate::integrations::mcp::McpManager,
510 crate::agent::prompt::PromptAssembly,
511 Result<(), Vec<crate::integrations::plugin::PluginError>>,
512 ) {
513 let result = if let Some(registry) = &self.plugin_registry {
514 registry.apply(&mut tools, &mut policies, &self.env, &mut skills, &mut mcp, &mut prompt)
515 } else {
516 Ok(())
517 };
518 (tools, policies, skills, mcp, prompt, result)
519 }
520}
521
522#[cfg(test)]
523mod tests {
524 use super::*;
525
526 #[test]
527 fn token_budget_new_sets_compact_at_80_percent() {
528 let budget = TokenBudget::new(1000);
529 assert_eq!(budget.max_tokens, 1000);
530 assert_eq!(budget.compact_at_tokens, 800);
531 }
532
533 #[test]
534 fn token_budget_rounds_up() {
535 let budget = TokenBudget::new(100);
536 assert_eq!(budget.compact_at_tokens, 80);
538 let budget2 = TokenBudget::new(101);
539 assert_eq!(budget2.compact_at_tokens, 81);
541 }
542
543 #[test]
544 #[cfg(not(windows))]
545 fn default_env_contains_unix_runtime_env() {
546 let config = AgentConfig::default();
547 assert!(config.env.contains_key("PATH"));
548 assert_eq!(config.env.get("HOME"), std::env::var("HOME").ok().as_ref());
549 }
550
551 #[test]
552 #[cfg(windows)]
553 fn default_env_preserves_windows_runtime_env() {
554 let config = AgentConfig::default();
555 let expected = [
556 "Path",
557 "SystemRoot",
558 "WINDIR",
559 "USERPROFILE",
560 "TEMP",
561 "TMP",
562 "APPDATA",
563 "LOCALAPPDATA",
564 "ComSpec",
565 "PATHEXT",
566 "PSModulePath",
567 ];
568
569 let mut present = 0;
570 for key in expected {
571 if let Some((actual_key, actual_value)) =
572 std::env::vars().find(|(name, _)| name.eq_ignore_ascii_case(key))
573 {
574 present += 1;
575 assert_eq!(
576 config
577 .env
578 .iter()
579 .find(|(name, _)| name.eq_ignore_ascii_case(&actual_key))
580 .map(|(_, value)| value.as_str()),
581 Some(actual_value.as_str()),
582 "missing preserved env var {actual_key}"
583 );
584 }
585 }
586 assert!(present > 0, "test expected at least one Windows runtime env var");
587 }
588
589 #[test]
590 #[cfg(windows)]
591 fn platform_base_env_does_not_invent_unix_path_on_windows() {
592 let env = platform_base_env();
593 assert_ne!(
594 env.iter()
595 .find(|(name, _)| name.eq_ignore_ascii_case("PATH"))
596 .map(|(_, value)| value.as_str()),
597 Some("/usr/local/bin:/usr/bin:/bin")
598 );
599 }
600
601 #[test]
602 fn default_config_validates() {
603 let config = AgentConfig::default();
604 assert!(config.validate().is_ok());
605 }
606
607 #[test]
608 fn zero_max_iterations_invalid() {
609 let config = AgentConfig { max_iterations: Some(0), ..AgentConfig::default() };
610 assert!(config.validate().is_err());
611 }
612
613 #[test]
614 fn token_budget_compact_exceeding_max_invalid() {
615 let config = AgentConfig {
616 token_budget: Some(TokenBudget { max_tokens: 100, compact_at_tokens: 200 }),
617 ..AgentConfig::default()
618 };
619 assert!(config.validate().is_err());
620 }
621}