1use std::path::PathBuf;
5
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7
8use crate::subagent::{HookDef, MemoryScope, PermissionMode};
9
10#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum ModelSpec {
15 Inherit,
17 Named(String),
19}
20
21impl ModelSpec {
22 #[must_use]
24 pub fn as_str(&self) -> &str {
25 match self {
26 ModelSpec::Inherit => "inherit",
27 ModelSpec::Named(s) => s.as_str(),
28 }
29 }
30}
31
32impl Serialize for ModelSpec {
33 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
34 match self {
35 ModelSpec::Inherit => serializer.serialize_str("inherit"),
36 ModelSpec::Named(s) => serializer.serialize_str(s),
37 }
38 }
39}
40
41impl<'de> Deserialize<'de> for ModelSpec {
42 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
43 let s = String::deserialize(deserializer)?;
44 if s == "inherit" {
45 Ok(ModelSpec::Inherit)
46 } else {
47 Ok(ModelSpec::Named(s))
48 }
49 }
50}
51
52#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
54#[serde(rename_all = "snake_case")]
55pub enum ContextInjectionMode {
56 None,
58 #[default]
60 LastAssistantTurn,
61 Summary,
63}
64
65fn default_max_tool_iterations() -> usize {
66 10
67}
68
69fn default_auto_update_check() -> bool {
70 true
71}
72
73fn default_focus_compression_interval() -> usize {
74 12
75}
76
77fn default_focus_reminder_interval() -> usize {
78 15
79}
80
81fn default_focus_min_messages_per_focus() -> usize {
82 8
83}
84
85fn default_focus_max_knowledge_tokens() -> usize {
86 4096
87}
88
89fn default_focus_auto_consolidate_min_window() -> usize {
90 6
91}
92
93fn default_max_tool_retries() -> usize {
94 2
95}
96
97fn default_max_retry_duration_secs() -> u64 {
98 30
99}
100
101fn default_tool_repeat_threshold() -> usize {
102 2
103}
104
105fn default_tool_filter_top_k() -> usize {
106 6
107}
108
109fn default_tool_filter_min_description_words() -> usize {
110 5
111}
112
113fn default_tool_filter_always_on() -> Vec<String> {
114 vec![
115 "memory_search".into(),
116 "memory_save".into(),
117 "load_skill".into(),
118 "invoke_skill".into(),
119 "bash".into(),
120 "read".into(),
121 "edit".into(),
122 ]
123}
124
125fn default_instruction_auto_detect() -> bool {
126 true
127}
128
129fn default_max_concurrent() -> usize {
130 5
131}
132
133fn default_context_window_turns() -> usize {
134 10
135}
136
137fn default_max_spawn_depth() -> u32 {
138 3
139}
140
141fn default_transcript_enabled() -> bool {
142 true
143}
144
145fn default_transcript_max_files() -> usize {
146 50
147}
148
149#[derive(Debug, Clone, Deserialize, Serialize)]
151#[serde(default)]
152pub struct FocusConfig {
153 pub enabled: bool,
155 #[serde(default = "default_focus_compression_interval")]
157 pub compression_interval: usize,
158 #[serde(default = "default_focus_reminder_interval")]
160 pub reminder_interval: usize,
161 #[serde(default = "default_focus_min_messages_per_focus")]
163 pub min_messages_per_focus: usize,
164 #[serde(default = "default_focus_max_knowledge_tokens")]
167 pub max_knowledge_tokens: usize,
168 #[serde(default = "default_focus_auto_consolidate_min_window")]
172 pub auto_consolidate_min_window: usize,
173}
174
175impl Default for FocusConfig {
176 fn default() -> Self {
177 Self {
178 enabled: false,
179 compression_interval: default_focus_compression_interval(),
180 reminder_interval: default_focus_reminder_interval(),
181 min_messages_per_focus: default_focus_min_messages_per_focus(),
182 max_knowledge_tokens: default_focus_max_knowledge_tokens(),
183 auto_consolidate_min_window: default_focus_auto_consolidate_min_window(),
184 }
185 }
186}
187
188#[derive(Debug, Clone, Deserialize, Serialize)]
193#[serde(default)]
194pub struct ToolFilterConfig {
195 pub enabled: bool,
197 #[serde(default = "default_tool_filter_top_k")]
200 pub top_k: usize,
201 #[serde(default = "default_tool_filter_always_on")]
203 pub always_on: Vec<String>,
204 #[serde(default = "default_tool_filter_min_description_words")]
206 pub min_description_words: usize,
207}
208
209impl Default for ToolFilterConfig {
210 fn default() -> Self {
211 Self {
212 enabled: false,
213 top_k: default_tool_filter_top_k(),
214 always_on: default_tool_filter_always_on(),
215 min_description_words: default_tool_filter_min_description_words(),
216 }
217 }
218}
219
220#[derive(Debug, Deserialize, Serialize)]
235pub struct AgentConfig {
236 pub name: String,
238 #[serde(default = "default_max_tool_iterations")]
241 pub max_tool_iterations: usize,
242 #[serde(default = "default_auto_update_check")]
244 pub auto_update_check: bool,
245 #[serde(default)]
247 pub instruction_files: Vec<std::path::PathBuf>,
248 #[serde(default = "default_instruction_auto_detect")]
251 pub instruction_auto_detect: bool,
252 #[serde(default = "default_max_tool_retries")]
254 pub max_tool_retries: usize,
255 #[serde(default = "default_tool_repeat_threshold")]
258 pub tool_repeat_threshold: usize,
259 #[serde(default = "default_max_retry_duration_secs")]
261 pub max_retry_duration_secs: u64,
262 #[serde(default)]
264 pub focus: FocusConfig,
265 #[serde(default)]
267 pub tool_filter: ToolFilterConfig,
268 #[serde(default = "default_budget_hint_enabled")]
272 pub budget_hint_enabled: bool,
273 #[serde(default)]
275 pub supervisor: TaskSupervisorConfig,
276}
277
278fn default_budget_hint_enabled() -> bool {
279 true
280}
281
282fn default_goal_max_text_chars() -> usize {
283 2000
284}
285
286fn default_goal_max_history() -> usize {
287 50
288}
289
290#[derive(Debug, Clone, Deserialize, Serialize)]
304#[serde(default)]
305pub struct GoalConfig {
306 pub enabled: bool,
308 pub inject_into_system_prompt: bool,
310 #[serde(default = "default_goal_max_text_chars")]
312 pub max_text_chars: usize,
313 pub default_token_budget: Option<u64>,
315 #[serde(default = "default_goal_max_history")]
317 pub max_history: usize,
318}
319
320impl Default for GoalConfig {
321 fn default() -> Self {
322 Self {
323 enabled: false,
324 inject_into_system_prompt: true,
325 max_text_chars: default_goal_max_text_chars(),
326 default_token_budget: None,
327 max_history: default_goal_max_history(),
328 }
329 }
330}
331
332fn default_enrichment_limit() -> usize {
333 4
334}
335
336fn default_telemetry_limit() -> usize {
337 8
338}
339
340fn default_background_shell_limit() -> usize {
341 8
342}
343
344#[derive(Debug, Clone, Deserialize, Serialize)]
360#[serde(default)]
361pub struct TaskSupervisorConfig {
362 #[serde(default = "default_enrichment_limit")]
365 pub enrichment_limit: usize,
366 #[serde(default = "default_telemetry_limit")]
369 pub telemetry_limit: usize,
370 #[serde(default)]
373 pub abort_enrichment_on_turn: bool,
374 #[serde(default = "default_background_shell_limit")]
379 pub background_shell_limit: usize,
380}
381
382impl Default for TaskSupervisorConfig {
383 fn default() -> Self {
384 Self {
385 enrichment_limit: default_enrichment_limit(),
386 telemetry_limit: default_telemetry_limit(),
387 abort_enrichment_on_turn: false,
388 background_shell_limit: default_background_shell_limit(),
389 }
390 }
391}
392
393#[derive(Debug, Clone, Deserialize, Serialize)]
408#[serde(default)]
409pub struct SubAgentConfig {
410 pub enabled: bool,
412 #[serde(default = "default_max_concurrent")]
414 pub max_concurrent: usize,
415 pub extra_dirs: Vec<PathBuf>,
417 #[serde(default)]
419 pub user_agents_dir: Option<PathBuf>,
420 pub default_permission_mode: Option<PermissionMode>,
422 #[serde(default)]
424 pub default_disallowed_tools: Vec<String>,
425 #[serde(default)]
427 pub allow_bypass_permissions: bool,
428 #[serde(default)]
430 pub default_memory_scope: Option<MemoryScope>,
431 #[serde(default)]
433 pub hooks: SubAgentLifecycleHooks,
434 #[serde(default)]
436 pub transcript_dir: Option<PathBuf>,
437 #[serde(default = "default_transcript_enabled")]
439 pub transcript_enabled: bool,
440 #[serde(default = "default_transcript_max_files")]
442 pub transcript_max_files: usize,
443 #[serde(default = "default_context_window_turns")]
446 pub context_window_turns: usize,
447 #[serde(default = "default_max_spawn_depth")]
449 pub max_spawn_depth: u32,
450 #[serde(default)]
452 pub context_injection_mode: ContextInjectionMode,
453}
454
455impl Default for SubAgentConfig {
456 fn default() -> Self {
457 Self {
458 enabled: false,
459 max_concurrent: default_max_concurrent(),
460 extra_dirs: Vec::new(),
461 user_agents_dir: None,
462 default_permission_mode: None,
463 default_disallowed_tools: Vec::new(),
464 allow_bypass_permissions: false,
465 default_memory_scope: None,
466 hooks: SubAgentLifecycleHooks::default(),
467 transcript_dir: None,
468 transcript_enabled: default_transcript_enabled(),
469 transcript_max_files: default_transcript_max_files(),
470 context_window_turns: default_context_window_turns(),
471 max_spawn_depth: default_max_spawn_depth(),
472 context_injection_mode: ContextInjectionMode::default(),
473 }
474 }
475}
476
477#[derive(Debug, Clone, Default, Deserialize, Serialize)]
479#[serde(default)]
480pub struct SubAgentLifecycleHooks {
481 pub start: Vec<HookDef>,
483 pub stop: Vec<HookDef>,
485}
486
487#[cfg(test)]
488mod tests {
489 use super::*;
490
491 #[test]
492 fn subagent_config_defaults() {
493 let cfg = SubAgentConfig::default();
494 assert_eq!(cfg.context_window_turns, 10);
495 assert_eq!(cfg.max_spawn_depth, 3);
496 assert_eq!(
497 cfg.context_injection_mode,
498 ContextInjectionMode::LastAssistantTurn
499 );
500 }
501
502 #[test]
503 fn subagent_config_deserialize_new_fields() {
504 let toml_str = r#"
505 enabled = true
506 context_window_turns = 5
507 max_spawn_depth = 2
508 context_injection_mode = "none"
509 "#;
510 let cfg: SubAgentConfig = toml::from_str(toml_str).unwrap();
511 assert_eq!(cfg.context_window_turns, 5);
512 assert_eq!(cfg.max_spawn_depth, 2);
513 assert_eq!(cfg.context_injection_mode, ContextInjectionMode::None);
514 }
515
516 #[test]
517 fn model_spec_deserialize_inherit() {
518 let spec: ModelSpec = serde_json::from_str("\"inherit\"").unwrap();
519 assert_eq!(spec, ModelSpec::Inherit);
520 }
521
522 #[test]
523 fn model_spec_deserialize_named() {
524 let spec: ModelSpec = serde_json::from_str("\"fast\"").unwrap();
525 assert_eq!(spec, ModelSpec::Named("fast".to_owned()));
526 }
527
528 #[test]
529 fn model_spec_as_str() {
530 assert_eq!(ModelSpec::Inherit.as_str(), "inherit");
531 assert_eq!(ModelSpec::Named("x".to_owned()).as_str(), "x");
532 }
533
534 #[test]
535 fn focus_config_auto_consolidate_min_window_default_is_six() {
536 let cfg = FocusConfig::default();
537 assert_eq!(cfg.auto_consolidate_min_window, 6);
538 }
539
540 #[test]
541 fn focus_config_auto_consolidate_min_window_deserializes() {
542 let toml_str = "auto_consolidate_min_window = 10";
543 let cfg: FocusConfig = toml::from_str(toml_str).unwrap();
544 assert_eq!(cfg.auto_consolidate_min_window, 10);
545 }
546}