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)]
276#[allow(clippy::struct_excessive_bools)] pub struct AgentConfig {
278 pub name: String,
280 #[serde(default = "default_max_tool_iterations")]
283 pub max_tool_iterations: usize,
284 #[serde(default = "default_auto_update_check")]
286 pub auto_update_check: bool,
287 #[serde(default)]
289 pub instruction_files: Vec<std::path::PathBuf>,
290 #[serde(default = "default_instruction_auto_detect")]
293 pub instruction_auto_detect: bool,
294 #[serde(default = "default_max_tool_retries")]
296 pub max_tool_retries: usize,
297 #[serde(default = "default_tool_repeat_threshold")]
300 pub tool_repeat_threshold: usize,
301 #[serde(default = "default_max_retry_duration_secs")]
303 pub max_retry_duration_secs: u64,
304 #[serde(default)]
306 pub focus: FocusConfig,
307 #[serde(default)]
309 pub tool_filter: ToolFilterConfig,
310 #[serde(default = "default_budget_hint_enabled")]
314 pub budget_hint_enabled: bool,
315 #[serde(default)]
317 pub supervisor: TaskSupervisorConfig,
318 #[serde(default = "default_time_reminder_enabled")]
324 pub time_reminder_enabled: bool,
325 #[serde(default = "default_time_reminder_interval_requests")]
332 pub time_reminder_interval_requests: u32,
333}
334
335fn default_budget_hint_enabled() -> bool {
336 true
337}
338
339fn default_time_reminder_enabled() -> bool {
340 false
341}
342
343fn default_time_reminder_interval_requests() -> u32 {
344 10
345}
346
347fn default_goal_max_text_chars() -> usize {
348 2000
349}
350
351fn default_goal_max_history() -> usize {
352 50
353}
354
355fn default_autonomous_max_turns() -> u32 {
356 20
357}
358
359fn default_verify_interval() -> u32 {
360 5
361}
362
363fn default_supervisor_timeout_secs() -> u64 {
364 30
365}
366
367fn default_max_stuck_count() -> u32 {
368 3
369}
370
371fn default_autonomous_turn_delay_ms() -> u64 {
372 500
373}
374
375fn default_autonomous_turn_timeout_secs() -> u64 {
376 300
377}
378
379fn default_max_supervisor_fail_count() -> u32 {
380 3
381}
382
383#[derive(Debug, Clone, Deserialize, Serialize)]
408#[serde(default)]
409pub struct GoalConfig {
410 pub enabled: bool,
412 pub inject_into_system_prompt: bool,
414 #[serde(default = "default_goal_max_text_chars")]
416 pub max_text_chars: usize,
417 pub default_token_budget: Option<u64>,
419 #[serde(default = "default_goal_max_history")]
421 pub max_history: usize,
422 pub autonomous_enabled: bool,
424 #[serde(default = "default_autonomous_max_turns")]
426 pub autonomous_max_turns: u32,
427 pub supervisor_provider: Option<ProviderName>,
430 #[serde(default = "default_verify_interval")]
432 pub verify_interval: u32,
433 #[serde(default = "default_supervisor_timeout_secs")]
435 pub supervisor_timeout_secs: u64,
436 #[serde(default = "default_max_stuck_count")]
438 pub max_stuck_count: u32,
439 #[serde(default = "default_autonomous_turn_delay_ms")]
441 pub autonomous_turn_delay_ms: u64,
442 #[serde(default = "default_autonomous_turn_timeout_secs")]
445 pub autonomous_turn_timeout_secs: u64,
446 #[serde(default = "default_max_supervisor_fail_count")]
449 pub max_supervisor_fail_count: u32,
450}
451
452impl Default for GoalConfig {
453 fn default() -> Self {
454 Self {
455 enabled: false,
456 inject_into_system_prompt: true,
457 max_text_chars: default_goal_max_text_chars(),
458 default_token_budget: None,
459 max_history: default_goal_max_history(),
460 autonomous_enabled: false,
461 autonomous_max_turns: default_autonomous_max_turns(),
462 supervisor_provider: None,
463 verify_interval: default_verify_interval(),
464 supervisor_timeout_secs: default_supervisor_timeout_secs(),
465 max_stuck_count: default_max_stuck_count(),
466 autonomous_turn_delay_ms: default_autonomous_turn_delay_ms(),
467 autonomous_turn_timeout_secs: default_autonomous_turn_timeout_secs(),
468 max_supervisor_fail_count: default_max_supervisor_fail_count(),
469 }
470 }
471}
472
473fn default_enrichment_limit() -> usize {
474 4
475}
476
477fn default_telemetry_limit() -> usize {
478 8
479}
480
481fn default_background_shell_limit() -> usize {
482 8
483}
484
485#[derive(Debug, Clone, Deserialize, Serialize)]
501#[serde(default)]
502pub struct TaskSupervisorConfig {
503 #[serde(default = "default_enrichment_limit")]
506 pub enrichment_limit: usize,
507 #[serde(default = "default_telemetry_limit")]
510 pub telemetry_limit: usize,
511 #[serde(default)]
514 pub abort_enrichment_on_turn: bool,
515 #[serde(default = "default_background_shell_limit")]
520 pub background_shell_limit: usize,
521}
522
523impl Default for TaskSupervisorConfig {
524 fn default() -> Self {
525 Self {
526 enrichment_limit: default_enrichment_limit(),
527 telemetry_limit: default_telemetry_limit(),
528 abort_enrichment_on_turn: false,
529 background_shell_limit: default_background_shell_limit(),
530 }
531 }
532}
533
534#[derive(Debug, Clone, Deserialize, Serialize)]
549#[serde(default)]
550#[allow(clippy::struct_excessive_bools)] pub struct SubAgentConfig {
552 pub enabled: bool,
554 #[serde(default = "default_max_concurrent")]
556 pub max_concurrent: usize,
557 pub extra_dirs: Vec<PathBuf>,
559 #[serde(default)]
561 pub user_agents_dir: Option<PathBuf>,
562 pub default_permission_mode: Option<PermissionMode>,
564 #[serde(default)]
566 pub default_disallowed_tools: Vec<String>,
567 #[serde(default)]
569 pub allow_bypass_permissions: bool,
570 #[serde(default)]
572 pub default_memory_scope: Option<MemoryScope>,
573 #[serde(default)]
575 pub hooks: SubAgentLifecycleHooks,
576 #[serde(default)]
578 pub transcript_dir: Option<PathBuf>,
579 #[serde(default = "default_transcript_enabled")]
581 pub transcript_enabled: bool,
582 #[serde(default = "default_transcript_max_files")]
584 pub transcript_max_files: usize,
585 #[serde(default)]
593 pub forward_transcript: bool,
594 #[serde(default = "default_context_window_turns")]
597 pub context_window_turns: usize,
598 #[serde(default = "default_max_spawn_depth")]
600 pub max_spawn_depth: u32,
601 #[serde(default)]
603 pub context_injection_mode: ContextInjectionMode,
604 #[serde(default)]
610 pub parent_context_policy: ParentContextPolicy,
611 #[serde(default = "default_max_parent_messages")]
618 pub max_parent_messages: usize,
619 #[serde(default = "default_summary_max_chars")]
627 pub summary_max_chars: usize,
628 #[serde(default = "default_llm_timeout_secs")]
633 pub llm_timeout_secs: u64,
634 #[serde(default)]
646 pub worktree: crate::worktree::WorktreeConfig,
647}
648
649impl Default for SubAgentConfig {
650 fn default() -> Self {
651 Self {
652 enabled: false,
653 max_concurrent: default_max_concurrent(),
654 extra_dirs: Vec::new(),
655 user_agents_dir: None,
656 default_permission_mode: None,
657 default_disallowed_tools: Vec::new(),
658 allow_bypass_permissions: false,
659 default_memory_scope: None,
660 hooks: SubAgentLifecycleHooks::default(),
661 transcript_dir: None,
662 transcript_enabled: default_transcript_enabled(),
663 transcript_max_files: default_transcript_max_files(),
664 forward_transcript: false,
665 context_window_turns: default_context_window_turns(),
666 max_spawn_depth: default_max_spawn_depth(),
667 context_injection_mode: ContextInjectionMode::default(),
668 parent_context_policy: ParentContextPolicy::default(),
669 max_parent_messages: default_max_parent_messages(),
670 summary_max_chars: default_summary_max_chars(),
671 llm_timeout_secs: default_llm_timeout_secs(),
672 worktree: crate::worktree::WorktreeConfig::default(),
673 }
674 }
675}
676
677#[derive(Debug, Clone, Default, Deserialize, Serialize)]
679#[serde(default)]
680pub struct SubAgentLifecycleHooks {
681 pub start: Vec<HookDef>,
683 pub stop: Vec<HookDef>,
685}
686
687#[cfg(test)]
688mod tests {
689 use super::*;
690
691 #[test]
692 fn subagent_config_defaults() {
693 let cfg = SubAgentConfig::default();
694 assert_eq!(cfg.context_window_turns, 10);
695 assert_eq!(cfg.max_spawn_depth, 3);
696 assert_eq!(
697 cfg.context_injection_mode,
698 ContextInjectionMode::LastAssistantTurn
699 );
700 assert_eq!(
701 cfg.parent_context_policy,
702 ParentContextPolicy::InheritSanitized
703 );
704 assert_eq!(cfg.max_parent_messages, 20);
705 assert!(
706 !cfg.forward_transcript,
707 "forward_transcript must default to false (NFR-003)"
708 );
709 }
710
711 #[test]
712 fn subagent_config_deserialize_forward_transcript() {
713 let toml_str = "forward_transcript = true";
714 let cfg: SubAgentConfig = toml::from_str(toml_str).unwrap();
715 assert!(cfg.forward_transcript);
716 }
717
718 #[test]
719 fn subagent_config_forward_transcript_omitted_defaults_false() {
720 let toml_str = "enabled = true";
721 let cfg: SubAgentConfig = toml::from_str(toml_str).unwrap();
722 assert!(!cfg.forward_transcript);
723 }
724
725 #[test]
726 fn subagent_config_deserialize_new_fields() {
727 let toml_str = r#"
728 enabled = true
729 context_window_turns = 5
730 max_spawn_depth = 2
731 context_injection_mode = "none"
732 "#;
733 let cfg: SubAgentConfig = toml::from_str(toml_str).unwrap();
734 assert_eq!(cfg.context_window_turns, 5);
735 assert_eq!(cfg.max_spawn_depth, 2);
736 assert_eq!(cfg.context_injection_mode, ContextInjectionMode::None);
737 }
738
739 #[test]
740 fn subagent_config_deserialize_parent_context_policy() {
741 let toml_str = r#"
742 parent_context_policy = "none"
743 max_parent_messages = 10
744 "#;
745 let cfg: SubAgentConfig = toml::from_str(toml_str).unwrap();
746 assert_eq!(cfg.parent_context_policy, ParentContextPolicy::None);
747 assert_eq!(cfg.max_parent_messages, 10);
748 }
749
750 #[test]
751 fn subagent_config_deserialize_parent_context_policy_inherit_sanitized() {
752 let toml_str = r#"
753 parent_context_policy = "inherit_sanitized"
754 "#;
755 let cfg: SubAgentConfig = toml::from_str(toml_str).unwrap();
756 assert_eq!(
757 cfg.parent_context_policy,
758 ParentContextPolicy::InheritSanitized
759 );
760 }
761
762 #[test]
763 fn model_spec_deserialize_inherit() {
764 let spec: ModelSpec = serde_json::from_str("\"inherit\"").unwrap();
765 assert_eq!(spec, ModelSpec::Inherit);
766 }
767
768 #[test]
769 fn model_spec_deserialize_named() {
770 let spec: ModelSpec = serde_json::from_str("\"fast\"").unwrap();
771 assert_eq!(spec, ModelSpec::Named("fast".to_owned()));
772 }
773
774 #[test]
775 fn model_spec_as_str() {
776 assert_eq!(ModelSpec::Inherit.as_str(), "inherit");
777 assert_eq!(ModelSpec::Named("x".to_owned()).as_str(), "x");
778 }
779
780 #[test]
781 fn focus_config_auto_consolidate_min_window_default_is_six() {
782 let cfg = FocusConfig::default();
783 assert_eq!(cfg.auto_consolidate_min_window, 6);
784 }
785
786 #[test]
787 fn focus_config_auto_consolidate_min_window_deserializes() {
788 let toml_str = "auto_consolidate_min_window = 10";
789 let cfg: FocusConfig = toml::from_str(toml_str).unwrap();
790 assert_eq!(cfg.auto_consolidate_min_window, 10);
791 }
792
793 #[test]
794 fn goal_config_new_field_defaults() {
795 let cfg = GoalConfig::default();
796 assert_eq!(cfg.autonomous_turn_timeout_secs, 300);
797 assert_eq!(cfg.max_supervisor_fail_count, 3);
798 }
799
800 #[test]
801 fn goal_config_new_fields_deserialize() {
802 let toml_str = r"
803 autonomous_turn_timeout_secs = 120
804 max_supervisor_fail_count = 5
805 ";
806 let cfg: GoalConfig = toml::from_str(toml_str).unwrap();
807 assert_eq!(cfg.autonomous_turn_timeout_secs, 120);
808 assert_eq!(cfg.max_supervisor_fail_count, 5);
809 }
810
811 #[test]
812 fn goal_config_omitted_new_fields_use_defaults() {
813 let toml_str = "enabled = true";
814 let cfg: GoalConfig = toml::from_str(toml_str).unwrap();
815 assert_eq!(cfg.autonomous_turn_timeout_secs, 300);
816 assert_eq!(cfg.max_supervisor_fail_count, 3);
817 }
818}