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)]
15#[non_exhaustive]
16pub enum ModelSpec {
17 Inherit,
19 Named(String),
21}
22
23impl ModelSpec {
24 #[must_use]
26 pub fn as_str(&self) -> &str {
27 match self {
28 ModelSpec::Inherit => "inherit",
29 ModelSpec::Named(s) => s.as_str(),
30 }
31 }
32}
33
34impl Serialize for ModelSpec {
35 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
36 match self {
37 ModelSpec::Inherit => serializer.serialize_str("inherit"),
38 ModelSpec::Named(s) => serializer.serialize_str(s),
39 }
40 }
41}
42
43impl<'de> Deserialize<'de> for ModelSpec {
44 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
45 let s = String::deserialize(deserializer)?;
46 if s == "inherit" {
47 Ok(ModelSpec::Inherit)
48 } else {
49 Ok(ModelSpec::Named(s))
50 }
51 }
52}
53
54#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
68#[serde(rename_all = "snake_case")]
69#[non_exhaustive]
70pub enum ParentContextPolicy {
71 Inherit,
73 #[default]
75 InheritSanitized,
76 None,
78}
79
80#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
82#[serde(rename_all = "snake_case")]
83#[non_exhaustive]
84pub enum ContextInjectionMode {
85 None,
87 #[default]
89 LastAssistantTurn,
90 Summary,
92}
93
94fn default_max_parent_messages() -> usize {
95 20
96}
97
98fn default_summary_max_chars() -> usize {
99 600
100}
101
102fn default_llm_timeout_secs() -> u64 {
103 120
104}
105
106fn default_max_tool_iterations() -> usize {
107 10
108}
109
110fn default_auto_update_check() -> bool {
111 true
112}
113
114fn default_focus_compression_interval() -> usize {
115 12
116}
117
118fn default_focus_reminder_interval() -> usize {
119 15
120}
121
122fn default_focus_min_messages_per_focus() -> usize {
123 8
124}
125
126fn default_focus_max_knowledge_tokens() -> usize {
127 4096
128}
129
130fn default_focus_auto_consolidate_min_window() -> usize {
131 6
132}
133
134fn default_max_tool_retries() -> usize {
135 2
136}
137
138fn default_max_retry_duration_secs() -> u64 {
139 30
140}
141
142fn default_tool_repeat_threshold() -> usize {
143 2
144}
145
146fn default_tool_filter_top_k() -> usize {
147 6
148}
149
150fn default_tool_filter_min_description_words() -> usize {
151 5
152}
153
154fn default_tool_filter_always_on() -> Vec<String> {
155 vec![
156 "memory_search".into(),
157 "memory_save".into(),
158 "load_skill".into(),
159 "invoke_skill".into(),
160 "bash".into(),
161 "read".into(),
162 "edit".into(),
163 ]
164}
165
166fn default_instruction_auto_detect() -> bool {
167 true
168}
169
170fn default_max_concurrent() -> usize {
171 5
172}
173
174fn default_context_window_turns() -> usize {
175 10
176}
177
178fn default_max_spawn_depth() -> u32 {
179 3
180}
181
182fn default_transcript_enabled() -> bool {
183 true
184}
185
186fn default_transcript_max_files() -> usize {
187 50
188}
189
190#[derive(Debug, Clone, Deserialize, Serialize)]
192#[serde(default)]
193pub struct FocusConfig {
194 pub enabled: bool,
196 #[serde(default = "default_focus_compression_interval")]
198 pub compression_interval: usize,
199 #[serde(default = "default_focus_reminder_interval")]
201 pub reminder_interval: usize,
202 #[serde(default = "default_focus_min_messages_per_focus")]
204 pub min_messages_per_focus: usize,
205 #[serde(default = "default_focus_max_knowledge_tokens")]
208 pub max_knowledge_tokens: usize,
209 #[serde(default = "default_focus_auto_consolidate_min_window")]
213 pub auto_consolidate_min_window: usize,
214}
215
216impl Default for FocusConfig {
217 fn default() -> Self {
218 Self {
219 enabled: false,
220 compression_interval: default_focus_compression_interval(),
221 reminder_interval: default_focus_reminder_interval(),
222 min_messages_per_focus: default_focus_min_messages_per_focus(),
223 max_knowledge_tokens: default_focus_max_knowledge_tokens(),
224 auto_consolidate_min_window: default_focus_auto_consolidate_min_window(),
225 }
226 }
227}
228
229#[derive(Debug, Clone, Deserialize, Serialize)]
234#[serde(default)]
235pub struct ToolFilterConfig {
236 pub enabled: bool,
238 #[serde(default = "default_tool_filter_top_k")]
241 pub top_k: usize,
242 #[serde(default = "default_tool_filter_always_on")]
244 pub always_on: Vec<String>,
245 #[serde(default = "default_tool_filter_min_description_words")]
247 pub min_description_words: usize,
248}
249
250impl Default for ToolFilterConfig {
251 fn default() -> Self {
252 Self {
253 enabled: false,
254 top_k: default_tool_filter_top_k(),
255 always_on: default_tool_filter_always_on(),
256 min_description_words: default_tool_filter_min_description_words(),
257 }
258 }
259}
260
261#[derive(Debug, Deserialize, Serialize)]
276pub struct AgentConfig {
277 pub name: String,
279 #[serde(default = "default_max_tool_iterations")]
282 pub max_tool_iterations: usize,
283 #[serde(default = "default_auto_update_check")]
285 pub auto_update_check: bool,
286 #[serde(default)]
288 pub instruction_files: Vec<std::path::PathBuf>,
289 #[serde(default = "default_instruction_auto_detect")]
292 pub instruction_auto_detect: bool,
293 #[serde(default = "default_max_tool_retries")]
295 pub max_tool_retries: usize,
296 #[serde(default = "default_tool_repeat_threshold")]
299 pub tool_repeat_threshold: usize,
300 #[serde(default = "default_max_retry_duration_secs")]
302 pub max_retry_duration_secs: u64,
303 #[serde(default)]
305 pub focus: FocusConfig,
306 #[serde(default)]
308 pub tool_filter: ToolFilterConfig,
309 #[serde(default = "default_budget_hint_enabled")]
313 pub budget_hint_enabled: bool,
314 #[serde(default)]
316 pub supervisor: TaskSupervisorConfig,
317}
318
319fn default_budget_hint_enabled() -> bool {
320 true
321}
322
323fn default_goal_max_text_chars() -> usize {
324 2000
325}
326
327fn default_goal_max_history() -> usize {
328 50
329}
330
331fn default_autonomous_max_turns() -> u32 {
332 20
333}
334
335fn default_verify_interval() -> u32 {
336 5
337}
338
339fn default_supervisor_timeout_secs() -> u64 {
340 30
341}
342
343fn default_max_stuck_count() -> u32 {
344 3
345}
346
347fn default_autonomous_turn_delay_ms() -> u64 {
348 500
349}
350
351fn default_autonomous_turn_timeout_secs() -> u64 {
352 300
353}
354
355fn default_max_supervisor_fail_count() -> u32 {
356 3
357}
358
359#[derive(Debug, Clone, Deserialize, Serialize)]
384#[serde(default)]
385pub struct GoalConfig {
386 pub enabled: bool,
388 pub inject_into_system_prompt: bool,
390 #[serde(default = "default_goal_max_text_chars")]
392 pub max_text_chars: usize,
393 pub default_token_budget: Option<u64>,
395 #[serde(default = "default_goal_max_history")]
397 pub max_history: usize,
398 pub autonomous_enabled: bool,
400 #[serde(default = "default_autonomous_max_turns")]
402 pub autonomous_max_turns: u32,
403 pub supervisor_provider: Option<ProviderName>,
406 #[serde(default = "default_verify_interval")]
408 pub verify_interval: u32,
409 #[serde(default = "default_supervisor_timeout_secs")]
411 pub supervisor_timeout_secs: u64,
412 #[serde(default = "default_max_stuck_count")]
414 pub max_stuck_count: u32,
415 #[serde(default = "default_autonomous_turn_delay_ms")]
417 pub autonomous_turn_delay_ms: u64,
418 #[serde(default = "default_autonomous_turn_timeout_secs")]
421 pub autonomous_turn_timeout_secs: u64,
422 #[serde(default = "default_max_supervisor_fail_count")]
425 pub max_supervisor_fail_count: u32,
426}
427
428impl Default for GoalConfig {
429 fn default() -> Self {
430 Self {
431 enabled: false,
432 inject_into_system_prompt: true,
433 max_text_chars: default_goal_max_text_chars(),
434 default_token_budget: None,
435 max_history: default_goal_max_history(),
436 autonomous_enabled: false,
437 autonomous_max_turns: default_autonomous_max_turns(),
438 supervisor_provider: None,
439 verify_interval: default_verify_interval(),
440 supervisor_timeout_secs: default_supervisor_timeout_secs(),
441 max_stuck_count: default_max_stuck_count(),
442 autonomous_turn_delay_ms: default_autonomous_turn_delay_ms(),
443 autonomous_turn_timeout_secs: default_autonomous_turn_timeout_secs(),
444 max_supervisor_fail_count: default_max_supervisor_fail_count(),
445 }
446 }
447}
448
449fn default_enrichment_limit() -> usize {
450 4
451}
452
453fn default_telemetry_limit() -> usize {
454 8
455}
456
457fn default_background_shell_limit() -> usize {
458 8
459}
460
461#[derive(Debug, Clone, Deserialize, Serialize)]
477#[serde(default)]
478pub struct TaskSupervisorConfig {
479 #[serde(default = "default_enrichment_limit")]
482 pub enrichment_limit: usize,
483 #[serde(default = "default_telemetry_limit")]
486 pub telemetry_limit: usize,
487 #[serde(default)]
490 pub abort_enrichment_on_turn: bool,
491 #[serde(default = "default_background_shell_limit")]
496 pub background_shell_limit: usize,
497}
498
499impl Default for TaskSupervisorConfig {
500 fn default() -> Self {
501 Self {
502 enrichment_limit: default_enrichment_limit(),
503 telemetry_limit: default_telemetry_limit(),
504 abort_enrichment_on_turn: false,
505 background_shell_limit: default_background_shell_limit(),
506 }
507 }
508}
509
510#[derive(Debug, Clone, Deserialize, Serialize)]
525#[serde(default)]
526pub struct SubAgentConfig {
527 pub enabled: bool,
529 #[serde(default = "default_max_concurrent")]
531 pub max_concurrent: usize,
532 pub extra_dirs: Vec<PathBuf>,
534 #[serde(default)]
536 pub user_agents_dir: Option<PathBuf>,
537 pub default_permission_mode: Option<PermissionMode>,
539 #[serde(default)]
541 pub default_disallowed_tools: Vec<String>,
542 #[serde(default)]
544 pub allow_bypass_permissions: bool,
545 #[serde(default)]
547 pub default_memory_scope: Option<MemoryScope>,
548 #[serde(default)]
550 pub hooks: SubAgentLifecycleHooks,
551 #[serde(default)]
553 pub transcript_dir: Option<PathBuf>,
554 #[serde(default = "default_transcript_enabled")]
556 pub transcript_enabled: bool,
557 #[serde(default = "default_transcript_max_files")]
559 pub transcript_max_files: usize,
560 #[serde(default = "default_context_window_turns")]
563 pub context_window_turns: usize,
564 #[serde(default = "default_max_spawn_depth")]
566 pub max_spawn_depth: u32,
567 #[serde(default)]
569 pub context_injection_mode: ContextInjectionMode,
570 #[serde(default)]
576 pub parent_context_policy: ParentContextPolicy,
577 #[serde(default = "default_max_parent_messages")]
584 pub max_parent_messages: usize,
585 #[serde(default = "default_summary_max_chars")]
593 pub summary_max_chars: usize,
594 #[serde(default = "default_llm_timeout_secs")]
599 pub llm_timeout_secs: u64,
600 #[serde(default)]
612 pub worktree: crate::worktree::WorktreeConfig,
613}
614
615impl Default for SubAgentConfig {
616 fn default() -> Self {
617 Self {
618 enabled: false,
619 max_concurrent: default_max_concurrent(),
620 extra_dirs: Vec::new(),
621 user_agents_dir: None,
622 default_permission_mode: None,
623 default_disallowed_tools: Vec::new(),
624 allow_bypass_permissions: false,
625 default_memory_scope: None,
626 hooks: SubAgentLifecycleHooks::default(),
627 transcript_dir: None,
628 transcript_enabled: default_transcript_enabled(),
629 transcript_max_files: default_transcript_max_files(),
630 context_window_turns: default_context_window_turns(),
631 max_spawn_depth: default_max_spawn_depth(),
632 context_injection_mode: ContextInjectionMode::default(),
633 parent_context_policy: ParentContextPolicy::default(),
634 max_parent_messages: default_max_parent_messages(),
635 summary_max_chars: default_summary_max_chars(),
636 llm_timeout_secs: default_llm_timeout_secs(),
637 worktree: crate::worktree::WorktreeConfig::default(),
638 }
639 }
640}
641
642#[derive(Debug, Clone, Default, Deserialize, Serialize)]
644#[serde(default)]
645pub struct SubAgentLifecycleHooks {
646 pub start: Vec<HookDef>,
648 pub stop: Vec<HookDef>,
650}
651
652#[cfg(test)]
653mod tests {
654 use super::*;
655
656 #[test]
657 fn subagent_config_defaults() {
658 let cfg = SubAgentConfig::default();
659 assert_eq!(cfg.context_window_turns, 10);
660 assert_eq!(cfg.max_spawn_depth, 3);
661 assert_eq!(
662 cfg.context_injection_mode,
663 ContextInjectionMode::LastAssistantTurn
664 );
665 assert_eq!(
666 cfg.parent_context_policy,
667 ParentContextPolicy::InheritSanitized
668 );
669 assert_eq!(cfg.max_parent_messages, 20);
670 }
671
672 #[test]
673 fn subagent_config_deserialize_new_fields() {
674 let toml_str = r#"
675 enabled = true
676 context_window_turns = 5
677 max_spawn_depth = 2
678 context_injection_mode = "none"
679 "#;
680 let cfg: SubAgentConfig = toml::from_str(toml_str).unwrap();
681 assert_eq!(cfg.context_window_turns, 5);
682 assert_eq!(cfg.max_spawn_depth, 2);
683 assert_eq!(cfg.context_injection_mode, ContextInjectionMode::None);
684 }
685
686 #[test]
687 fn subagent_config_deserialize_parent_context_policy() {
688 let toml_str = r#"
689 parent_context_policy = "none"
690 max_parent_messages = 10
691 "#;
692 let cfg: SubAgentConfig = toml::from_str(toml_str).unwrap();
693 assert_eq!(cfg.parent_context_policy, ParentContextPolicy::None);
694 assert_eq!(cfg.max_parent_messages, 10);
695 }
696
697 #[test]
698 fn subagent_config_deserialize_parent_context_policy_inherit_sanitized() {
699 let toml_str = r#"
700 parent_context_policy = "inherit_sanitized"
701 "#;
702 let cfg: SubAgentConfig = toml::from_str(toml_str).unwrap();
703 assert_eq!(
704 cfg.parent_context_policy,
705 ParentContextPolicy::InheritSanitized
706 );
707 }
708
709 #[test]
710 fn model_spec_deserialize_inherit() {
711 let spec: ModelSpec = serde_json::from_str("\"inherit\"").unwrap();
712 assert_eq!(spec, ModelSpec::Inherit);
713 }
714
715 #[test]
716 fn model_spec_deserialize_named() {
717 let spec: ModelSpec = serde_json::from_str("\"fast\"").unwrap();
718 assert_eq!(spec, ModelSpec::Named("fast".to_owned()));
719 }
720
721 #[test]
722 fn model_spec_as_str() {
723 assert_eq!(ModelSpec::Inherit.as_str(), "inherit");
724 assert_eq!(ModelSpec::Named("x".to_owned()).as_str(), "x");
725 }
726
727 #[test]
728 fn focus_config_auto_consolidate_min_window_default_is_six() {
729 let cfg = FocusConfig::default();
730 assert_eq!(cfg.auto_consolidate_min_window, 6);
731 }
732
733 #[test]
734 fn focus_config_auto_consolidate_min_window_deserializes() {
735 let toml_str = "auto_consolidate_min_window = 10";
736 let cfg: FocusConfig = toml::from_str(toml_str).unwrap();
737 assert_eq!(cfg.auto_consolidate_min_window, 10);
738 }
739
740 #[test]
741 fn goal_config_new_field_defaults() {
742 let cfg = GoalConfig::default();
743 assert_eq!(cfg.autonomous_turn_timeout_secs, 300);
744 assert_eq!(cfg.max_supervisor_fail_count, 3);
745 }
746
747 #[test]
748 fn goal_config_new_fields_deserialize() {
749 let toml_str = r"
750 autonomous_turn_timeout_secs = 120
751 max_supervisor_fail_count = 5
752 ";
753 let cfg: GoalConfig = toml::from_str(toml_str).unwrap();
754 assert_eq!(cfg.autonomous_turn_timeout_secs, 120);
755 assert_eq!(cfg.max_supervisor_fail_count, 5);
756 }
757
758 #[test]
759 fn goal_config_omitted_new_fields_use_defaults() {
760 let toml_str = "enabled = true";
761 let cfg: GoalConfig = toml::from_str(toml_str).unwrap();
762 assert_eq!(cfg.autonomous_turn_timeout_secs, 300);
763 assert_eq!(cfg.max_supervisor_fail_count, 3);
764 }
765}