1use std::path::PathBuf;
5
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7
8use crate::providers::ProviderName;
9use crate::subagent::{HookDef, MemoryScope, PermissionMode};
10
11#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum ModelSpec {
16 Inherit,
18 Named(String),
20}
21
22impl ModelSpec {
23 #[must_use]
25 pub fn as_str(&self) -> &str {
26 match self {
27 ModelSpec::Inherit => "inherit",
28 ModelSpec::Named(s) => s.as_str(),
29 }
30 }
31}
32
33impl Serialize for ModelSpec {
34 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
35 match self {
36 ModelSpec::Inherit => serializer.serialize_str("inherit"),
37 ModelSpec::Named(s) => serializer.serialize_str(s),
38 }
39 }
40}
41
42impl<'de> Deserialize<'de> for ModelSpec {
43 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
44 let s = String::deserialize(deserializer)?;
45 if s == "inherit" {
46 Ok(ModelSpec::Inherit)
47 } else {
48 Ok(ModelSpec::Named(s))
49 }
50 }
51}
52
53#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
67#[serde(rename_all = "snake_case")]
68pub enum ParentContextPolicy {
69 Inherit,
71 #[default]
73 InheritSanitized,
74 None,
76}
77
78#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
80#[serde(rename_all = "snake_case")]
81pub enum ContextInjectionMode {
82 None,
84 #[default]
86 LastAssistantTurn,
87 Summary,
89}
90
91fn default_max_parent_messages() -> usize {
92 20
93}
94
95fn default_summary_max_chars() -> usize {
96 600
97}
98
99fn default_max_tool_iterations() -> usize {
100 10
101}
102
103fn default_auto_update_check() -> bool {
104 true
105}
106
107fn default_focus_compression_interval() -> usize {
108 12
109}
110
111fn default_focus_reminder_interval() -> usize {
112 15
113}
114
115fn default_focus_min_messages_per_focus() -> usize {
116 8
117}
118
119fn default_focus_max_knowledge_tokens() -> usize {
120 4096
121}
122
123fn default_focus_auto_consolidate_min_window() -> usize {
124 6
125}
126
127fn default_max_tool_retries() -> usize {
128 2
129}
130
131fn default_max_retry_duration_secs() -> u64 {
132 30
133}
134
135fn default_tool_repeat_threshold() -> usize {
136 2
137}
138
139fn default_tool_filter_top_k() -> usize {
140 6
141}
142
143fn default_tool_filter_min_description_words() -> usize {
144 5
145}
146
147fn default_tool_filter_always_on() -> Vec<String> {
148 vec![
149 "memory_search".into(),
150 "memory_save".into(),
151 "load_skill".into(),
152 "invoke_skill".into(),
153 "bash".into(),
154 "read".into(),
155 "edit".into(),
156 ]
157}
158
159fn default_instruction_auto_detect() -> bool {
160 true
161}
162
163fn default_max_concurrent() -> usize {
164 5
165}
166
167fn default_context_window_turns() -> usize {
168 10
169}
170
171fn default_max_spawn_depth() -> u32 {
172 3
173}
174
175fn default_transcript_enabled() -> bool {
176 true
177}
178
179fn default_transcript_max_files() -> usize {
180 50
181}
182
183#[derive(Debug, Clone, Deserialize, Serialize)]
185#[serde(default)]
186pub struct FocusConfig {
187 pub enabled: bool,
189 #[serde(default = "default_focus_compression_interval")]
191 pub compression_interval: usize,
192 #[serde(default = "default_focus_reminder_interval")]
194 pub reminder_interval: usize,
195 #[serde(default = "default_focus_min_messages_per_focus")]
197 pub min_messages_per_focus: usize,
198 #[serde(default = "default_focus_max_knowledge_tokens")]
201 pub max_knowledge_tokens: usize,
202 #[serde(default = "default_focus_auto_consolidate_min_window")]
206 pub auto_consolidate_min_window: usize,
207}
208
209impl Default for FocusConfig {
210 fn default() -> Self {
211 Self {
212 enabled: false,
213 compression_interval: default_focus_compression_interval(),
214 reminder_interval: default_focus_reminder_interval(),
215 min_messages_per_focus: default_focus_min_messages_per_focus(),
216 max_knowledge_tokens: default_focus_max_knowledge_tokens(),
217 auto_consolidate_min_window: default_focus_auto_consolidate_min_window(),
218 }
219 }
220}
221
222#[derive(Debug, Clone, Deserialize, Serialize)]
227#[serde(default)]
228pub struct ToolFilterConfig {
229 pub enabled: bool,
231 #[serde(default = "default_tool_filter_top_k")]
234 pub top_k: usize,
235 #[serde(default = "default_tool_filter_always_on")]
237 pub always_on: Vec<String>,
238 #[serde(default = "default_tool_filter_min_description_words")]
240 pub min_description_words: usize,
241}
242
243impl Default for ToolFilterConfig {
244 fn default() -> Self {
245 Self {
246 enabled: false,
247 top_k: default_tool_filter_top_k(),
248 always_on: default_tool_filter_always_on(),
249 min_description_words: default_tool_filter_min_description_words(),
250 }
251 }
252}
253
254#[derive(Debug, Deserialize, Serialize)]
269pub struct AgentConfig {
270 pub name: String,
272 #[serde(default = "default_max_tool_iterations")]
275 pub max_tool_iterations: usize,
276 #[serde(default = "default_auto_update_check")]
278 pub auto_update_check: bool,
279 #[serde(default)]
281 pub instruction_files: Vec<std::path::PathBuf>,
282 #[serde(default = "default_instruction_auto_detect")]
285 pub instruction_auto_detect: bool,
286 #[serde(default = "default_max_tool_retries")]
288 pub max_tool_retries: usize,
289 #[serde(default = "default_tool_repeat_threshold")]
292 pub tool_repeat_threshold: usize,
293 #[serde(default = "default_max_retry_duration_secs")]
295 pub max_retry_duration_secs: u64,
296 #[serde(default)]
298 pub focus: FocusConfig,
299 #[serde(default)]
301 pub tool_filter: ToolFilterConfig,
302 #[serde(default = "default_budget_hint_enabled")]
306 pub budget_hint_enabled: bool,
307 #[serde(default)]
309 pub supervisor: TaskSupervisorConfig,
310}
311
312fn default_budget_hint_enabled() -> bool {
313 true
314}
315
316fn default_goal_max_text_chars() -> usize {
317 2000
318}
319
320fn default_goal_max_history() -> usize {
321 50
322}
323
324fn default_autonomous_max_turns() -> u32 {
325 20
326}
327
328fn default_verify_interval() -> u32 {
329 5
330}
331
332fn default_supervisor_timeout_secs() -> u64 {
333 30
334}
335
336fn default_max_stuck_count() -> u32 {
337 3
338}
339
340fn default_autonomous_turn_delay_ms() -> u64 {
341 500
342}
343
344fn default_autonomous_turn_timeout_secs() -> u64 {
345 300
346}
347
348fn default_max_supervisor_fail_count() -> u32 {
349 3
350}
351
352#[derive(Debug, Clone, Deserialize, Serialize)]
377#[serde(default)]
378pub struct GoalConfig {
379 pub enabled: bool,
381 pub inject_into_system_prompt: bool,
383 #[serde(default = "default_goal_max_text_chars")]
385 pub max_text_chars: usize,
386 pub default_token_budget: Option<u64>,
388 #[serde(default = "default_goal_max_history")]
390 pub max_history: usize,
391 pub autonomous_enabled: bool,
393 #[serde(default = "default_autonomous_max_turns")]
395 pub autonomous_max_turns: u32,
396 pub supervisor_provider: Option<ProviderName>,
399 #[serde(default = "default_verify_interval")]
401 pub verify_interval: u32,
402 #[serde(default = "default_supervisor_timeout_secs")]
404 pub supervisor_timeout_secs: u64,
405 #[serde(default = "default_max_stuck_count")]
407 pub max_stuck_count: u32,
408 #[serde(default = "default_autonomous_turn_delay_ms")]
410 pub autonomous_turn_delay_ms: u64,
411 #[serde(default = "default_autonomous_turn_timeout_secs")]
414 pub autonomous_turn_timeout_secs: u64,
415 #[serde(default = "default_max_supervisor_fail_count")]
418 pub max_supervisor_fail_count: u32,
419}
420
421impl Default for GoalConfig {
422 fn default() -> Self {
423 Self {
424 enabled: false,
425 inject_into_system_prompt: true,
426 max_text_chars: default_goal_max_text_chars(),
427 default_token_budget: None,
428 max_history: default_goal_max_history(),
429 autonomous_enabled: false,
430 autonomous_max_turns: default_autonomous_max_turns(),
431 supervisor_provider: None,
432 verify_interval: default_verify_interval(),
433 supervisor_timeout_secs: default_supervisor_timeout_secs(),
434 max_stuck_count: default_max_stuck_count(),
435 autonomous_turn_delay_ms: default_autonomous_turn_delay_ms(),
436 autonomous_turn_timeout_secs: default_autonomous_turn_timeout_secs(),
437 max_supervisor_fail_count: default_max_supervisor_fail_count(),
438 }
439 }
440}
441
442fn default_enrichment_limit() -> usize {
443 4
444}
445
446fn default_telemetry_limit() -> usize {
447 8
448}
449
450fn default_background_shell_limit() -> usize {
451 8
452}
453
454#[derive(Debug, Clone, Deserialize, Serialize)]
470#[serde(default)]
471pub struct TaskSupervisorConfig {
472 #[serde(default = "default_enrichment_limit")]
475 pub enrichment_limit: usize,
476 #[serde(default = "default_telemetry_limit")]
479 pub telemetry_limit: usize,
480 #[serde(default)]
483 pub abort_enrichment_on_turn: bool,
484 #[serde(default = "default_background_shell_limit")]
489 pub background_shell_limit: usize,
490}
491
492impl Default for TaskSupervisorConfig {
493 fn default() -> Self {
494 Self {
495 enrichment_limit: default_enrichment_limit(),
496 telemetry_limit: default_telemetry_limit(),
497 abort_enrichment_on_turn: false,
498 background_shell_limit: default_background_shell_limit(),
499 }
500 }
501}
502
503#[derive(Debug, Clone, Deserialize, Serialize)]
518#[serde(default)]
519pub struct SubAgentConfig {
520 pub enabled: bool,
522 #[serde(default = "default_max_concurrent")]
524 pub max_concurrent: usize,
525 pub extra_dirs: Vec<PathBuf>,
527 #[serde(default)]
529 pub user_agents_dir: Option<PathBuf>,
530 pub default_permission_mode: Option<PermissionMode>,
532 #[serde(default)]
534 pub default_disallowed_tools: Vec<String>,
535 #[serde(default)]
537 pub allow_bypass_permissions: bool,
538 #[serde(default)]
540 pub default_memory_scope: Option<MemoryScope>,
541 #[serde(default)]
543 pub hooks: SubAgentLifecycleHooks,
544 #[serde(default)]
546 pub transcript_dir: Option<PathBuf>,
547 #[serde(default = "default_transcript_enabled")]
549 pub transcript_enabled: bool,
550 #[serde(default = "default_transcript_max_files")]
552 pub transcript_max_files: usize,
553 #[serde(default = "default_context_window_turns")]
556 pub context_window_turns: usize,
557 #[serde(default = "default_max_spawn_depth")]
559 pub max_spawn_depth: u32,
560 #[serde(default)]
562 pub context_injection_mode: ContextInjectionMode,
563 #[serde(default)]
569 pub parent_context_policy: ParentContextPolicy,
570 #[serde(default = "default_max_parent_messages")]
577 pub max_parent_messages: usize,
578 #[serde(default = "default_summary_max_chars")]
586 pub summary_max_chars: usize,
587}
588
589impl Default for SubAgentConfig {
590 fn default() -> Self {
591 Self {
592 enabled: false,
593 max_concurrent: default_max_concurrent(),
594 extra_dirs: Vec::new(),
595 user_agents_dir: None,
596 default_permission_mode: None,
597 default_disallowed_tools: Vec::new(),
598 allow_bypass_permissions: false,
599 default_memory_scope: None,
600 hooks: SubAgentLifecycleHooks::default(),
601 transcript_dir: None,
602 transcript_enabled: default_transcript_enabled(),
603 transcript_max_files: default_transcript_max_files(),
604 context_window_turns: default_context_window_turns(),
605 max_spawn_depth: default_max_spawn_depth(),
606 context_injection_mode: ContextInjectionMode::default(),
607 parent_context_policy: ParentContextPolicy::default(),
608 max_parent_messages: default_max_parent_messages(),
609 summary_max_chars: default_summary_max_chars(),
610 }
611 }
612}
613
614#[derive(Debug, Clone, Default, Deserialize, Serialize)]
616#[serde(default)]
617pub struct SubAgentLifecycleHooks {
618 pub start: Vec<HookDef>,
620 pub stop: Vec<HookDef>,
622}
623
624#[cfg(test)]
625mod tests {
626 use super::*;
627
628 #[test]
629 fn subagent_config_defaults() {
630 let cfg = SubAgentConfig::default();
631 assert_eq!(cfg.context_window_turns, 10);
632 assert_eq!(cfg.max_spawn_depth, 3);
633 assert_eq!(
634 cfg.context_injection_mode,
635 ContextInjectionMode::LastAssistantTurn
636 );
637 assert_eq!(
638 cfg.parent_context_policy,
639 ParentContextPolicy::InheritSanitized
640 );
641 assert_eq!(cfg.max_parent_messages, 20);
642 }
643
644 #[test]
645 fn subagent_config_deserialize_new_fields() {
646 let toml_str = r#"
647 enabled = true
648 context_window_turns = 5
649 max_spawn_depth = 2
650 context_injection_mode = "none"
651 "#;
652 let cfg: SubAgentConfig = toml::from_str(toml_str).unwrap();
653 assert_eq!(cfg.context_window_turns, 5);
654 assert_eq!(cfg.max_spawn_depth, 2);
655 assert_eq!(cfg.context_injection_mode, ContextInjectionMode::None);
656 }
657
658 #[test]
659 fn subagent_config_deserialize_parent_context_policy() {
660 let toml_str = r#"
661 parent_context_policy = "none"
662 max_parent_messages = 10
663 "#;
664 let cfg: SubAgentConfig = toml::from_str(toml_str).unwrap();
665 assert_eq!(cfg.parent_context_policy, ParentContextPolicy::None);
666 assert_eq!(cfg.max_parent_messages, 10);
667 }
668
669 #[test]
670 fn subagent_config_deserialize_parent_context_policy_inherit_sanitized() {
671 let toml_str = r#"
672 parent_context_policy = "inherit_sanitized"
673 "#;
674 let cfg: SubAgentConfig = toml::from_str(toml_str).unwrap();
675 assert_eq!(
676 cfg.parent_context_policy,
677 ParentContextPolicy::InheritSanitized
678 );
679 }
680
681 #[test]
682 fn model_spec_deserialize_inherit() {
683 let spec: ModelSpec = serde_json::from_str("\"inherit\"").unwrap();
684 assert_eq!(spec, ModelSpec::Inherit);
685 }
686
687 #[test]
688 fn model_spec_deserialize_named() {
689 let spec: ModelSpec = serde_json::from_str("\"fast\"").unwrap();
690 assert_eq!(spec, ModelSpec::Named("fast".to_owned()));
691 }
692
693 #[test]
694 fn model_spec_as_str() {
695 assert_eq!(ModelSpec::Inherit.as_str(), "inherit");
696 assert_eq!(ModelSpec::Named("x".to_owned()).as_str(), "x");
697 }
698
699 #[test]
700 fn focus_config_auto_consolidate_min_window_default_is_six() {
701 let cfg = FocusConfig::default();
702 assert_eq!(cfg.auto_consolidate_min_window, 6);
703 }
704
705 #[test]
706 fn focus_config_auto_consolidate_min_window_deserializes() {
707 let toml_str = "auto_consolidate_min_window = 10";
708 let cfg: FocusConfig = toml::from_str(toml_str).unwrap();
709 assert_eq!(cfg.auto_consolidate_min_window, 10);
710 }
711
712 #[test]
713 fn goal_config_new_field_defaults() {
714 let cfg = GoalConfig::default();
715 assert_eq!(cfg.autonomous_turn_timeout_secs, 300);
716 assert_eq!(cfg.max_supervisor_fail_count, 3);
717 }
718
719 #[test]
720 fn goal_config_new_fields_deserialize() {
721 let toml_str = r"
722 autonomous_turn_timeout_secs = 120
723 max_supervisor_fail_count = 5
724 ";
725 let cfg: GoalConfig = toml::from_str(toml_str).unwrap();
726 assert_eq!(cfg.autonomous_turn_timeout_secs, 120);
727 assert_eq!(cfg.max_supervisor_fail_count, 5);
728 }
729
730 #[test]
731 fn goal_config_omitted_new_fields_use_defaults() {
732 let toml_str = "enabled = true";
733 let cfg: GoalConfig = toml::from_str(toml_str).unwrap();
734 assert_eq!(cfg.autonomous_turn_timeout_secs, 300);
735 assert_eq!(cfg.max_supervisor_fail_count, 3);
736 }
737}