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_max_tool_retries() -> usize {
90 2
91}
92
93fn default_max_retry_duration_secs() -> u64 {
94 30
95}
96
97fn default_tool_repeat_threshold() -> usize {
98 2
99}
100
101fn default_tool_filter_top_k() -> usize {
102 6
103}
104
105fn default_tool_filter_min_description_words() -> usize {
106 5
107}
108
109fn default_tool_filter_always_on() -> Vec<String> {
110 vec![
111 "memory_search".into(),
112 "memory_save".into(),
113 "load_skill".into(),
114 "invoke_skill".into(),
115 "bash".into(),
116 "read".into(),
117 "edit".into(),
118 ]
119}
120
121fn default_instruction_auto_detect() -> bool {
122 true
123}
124
125fn default_max_concurrent() -> usize {
126 5
127}
128
129fn default_context_window_turns() -> usize {
130 10
131}
132
133fn default_max_spawn_depth() -> u32 {
134 3
135}
136
137fn default_transcript_enabled() -> bool {
138 true
139}
140
141fn default_transcript_max_files() -> usize {
142 50
143}
144
145#[derive(Debug, Clone, Deserialize, Serialize)]
147#[serde(default)]
148pub struct FocusConfig {
149 pub enabled: bool,
151 #[serde(default = "default_focus_compression_interval")]
153 pub compression_interval: usize,
154 #[serde(default = "default_focus_reminder_interval")]
156 pub reminder_interval: usize,
157 #[serde(default = "default_focus_min_messages_per_focus")]
159 pub min_messages_per_focus: usize,
160 #[serde(default = "default_focus_max_knowledge_tokens")]
163 pub max_knowledge_tokens: usize,
164}
165
166impl Default for FocusConfig {
167 fn default() -> Self {
168 Self {
169 enabled: false,
170 compression_interval: default_focus_compression_interval(),
171 reminder_interval: default_focus_reminder_interval(),
172 min_messages_per_focus: default_focus_min_messages_per_focus(),
173 max_knowledge_tokens: default_focus_max_knowledge_tokens(),
174 }
175 }
176}
177
178#[derive(Debug, Clone, Deserialize, Serialize)]
183#[serde(default)]
184pub struct ToolFilterConfig {
185 pub enabled: bool,
187 #[serde(default = "default_tool_filter_top_k")]
190 pub top_k: usize,
191 #[serde(default = "default_tool_filter_always_on")]
193 pub always_on: Vec<String>,
194 #[serde(default = "default_tool_filter_min_description_words")]
196 pub min_description_words: usize,
197}
198
199impl Default for ToolFilterConfig {
200 fn default() -> Self {
201 Self {
202 enabled: false,
203 top_k: default_tool_filter_top_k(),
204 always_on: default_tool_filter_always_on(),
205 min_description_words: default_tool_filter_min_description_words(),
206 }
207 }
208}
209
210#[derive(Debug, Deserialize, Serialize)]
225pub struct AgentConfig {
226 pub name: String,
228 #[serde(default = "default_max_tool_iterations")]
231 pub max_tool_iterations: usize,
232 #[serde(default = "default_auto_update_check")]
234 pub auto_update_check: bool,
235 #[serde(default)]
237 pub instruction_files: Vec<std::path::PathBuf>,
238 #[serde(default = "default_instruction_auto_detect")]
241 pub instruction_auto_detect: bool,
242 #[serde(default = "default_max_tool_retries")]
244 pub max_tool_retries: usize,
245 #[serde(default = "default_tool_repeat_threshold")]
248 pub tool_repeat_threshold: usize,
249 #[serde(default = "default_max_retry_duration_secs")]
251 pub max_retry_duration_secs: u64,
252 #[serde(default)]
254 pub focus: FocusConfig,
255 #[serde(default)]
257 pub tool_filter: ToolFilterConfig,
258 #[serde(default = "default_budget_hint_enabled")]
262 pub budget_hint_enabled: bool,
263 #[serde(default)]
265 pub supervisor: TaskSupervisorConfig,
266}
267
268fn default_budget_hint_enabled() -> bool {
269 true
270}
271
272fn default_enrichment_limit() -> usize {
273 4
274}
275
276fn default_telemetry_limit() -> usize {
277 8
278}
279
280#[derive(Debug, Clone, Deserialize, Serialize)]
296#[serde(default)]
297pub struct TaskSupervisorConfig {
298 #[serde(default = "default_enrichment_limit")]
301 pub enrichment_limit: usize,
302 #[serde(default = "default_telemetry_limit")]
305 pub telemetry_limit: usize,
306 #[serde(default)]
309 pub abort_enrichment_on_turn: bool,
310}
311
312impl Default for TaskSupervisorConfig {
313 fn default() -> Self {
314 Self {
315 enrichment_limit: default_enrichment_limit(),
316 telemetry_limit: default_telemetry_limit(),
317 abort_enrichment_on_turn: false,
318 }
319 }
320}
321
322#[derive(Debug, Clone, Deserialize, Serialize)]
337#[serde(default)]
338pub struct SubAgentConfig {
339 pub enabled: bool,
341 #[serde(default = "default_max_concurrent")]
343 pub max_concurrent: usize,
344 pub extra_dirs: Vec<PathBuf>,
346 #[serde(default)]
348 pub user_agents_dir: Option<PathBuf>,
349 pub default_permission_mode: Option<PermissionMode>,
351 #[serde(default)]
353 pub default_disallowed_tools: Vec<String>,
354 #[serde(default)]
356 pub allow_bypass_permissions: bool,
357 #[serde(default)]
359 pub default_memory_scope: Option<MemoryScope>,
360 #[serde(default)]
362 pub hooks: SubAgentLifecycleHooks,
363 #[serde(default)]
365 pub transcript_dir: Option<PathBuf>,
366 #[serde(default = "default_transcript_enabled")]
368 pub transcript_enabled: bool,
369 #[serde(default = "default_transcript_max_files")]
371 pub transcript_max_files: usize,
372 #[serde(default = "default_context_window_turns")]
375 pub context_window_turns: usize,
376 #[serde(default = "default_max_spawn_depth")]
378 pub max_spawn_depth: u32,
379 #[serde(default)]
381 pub context_injection_mode: ContextInjectionMode,
382}
383
384impl Default for SubAgentConfig {
385 fn default() -> Self {
386 Self {
387 enabled: false,
388 max_concurrent: default_max_concurrent(),
389 extra_dirs: Vec::new(),
390 user_agents_dir: None,
391 default_permission_mode: None,
392 default_disallowed_tools: Vec::new(),
393 allow_bypass_permissions: false,
394 default_memory_scope: None,
395 hooks: SubAgentLifecycleHooks::default(),
396 transcript_dir: None,
397 transcript_enabled: default_transcript_enabled(),
398 transcript_max_files: default_transcript_max_files(),
399 context_window_turns: default_context_window_turns(),
400 max_spawn_depth: default_max_spawn_depth(),
401 context_injection_mode: ContextInjectionMode::default(),
402 }
403 }
404}
405
406#[derive(Debug, Clone, Default, Deserialize, Serialize)]
408#[serde(default)]
409pub struct SubAgentLifecycleHooks {
410 pub start: Vec<HookDef>,
412 pub stop: Vec<HookDef>,
414}
415
416#[cfg(test)]
417mod tests {
418 use super::*;
419
420 #[test]
421 fn subagent_config_defaults() {
422 let cfg = SubAgentConfig::default();
423 assert_eq!(cfg.context_window_turns, 10);
424 assert_eq!(cfg.max_spawn_depth, 3);
425 assert_eq!(
426 cfg.context_injection_mode,
427 ContextInjectionMode::LastAssistantTurn
428 );
429 }
430
431 #[test]
432 fn subagent_config_deserialize_new_fields() {
433 let toml_str = r#"
434 enabled = true
435 context_window_turns = 5
436 max_spawn_depth = 2
437 context_injection_mode = "none"
438 "#;
439 let cfg: SubAgentConfig = toml::from_str(toml_str).unwrap();
440 assert_eq!(cfg.context_window_turns, 5);
441 assert_eq!(cfg.max_spawn_depth, 2);
442 assert_eq!(cfg.context_injection_mode, ContextInjectionMode::None);
443 }
444
445 #[test]
446 fn model_spec_deserialize_inherit() {
447 let spec: ModelSpec = serde_json::from_str("\"inherit\"").unwrap();
448 assert_eq!(spec, ModelSpec::Inherit);
449 }
450
451 #[test]
452 fn model_spec_deserialize_named() {
453 let spec: ModelSpec = serde_json::from_str("\"fast\"").unwrap();
454 assert_eq!(spec, ModelSpec::Named("fast".to_owned()));
455 }
456
457 #[test]
458 fn model_spec_as_str() {
459 assert_eq!(ModelSpec::Inherit.as_str(), "inherit");
460 assert_eq!(ModelSpec::Named("x".to_owned()).as_str(), "x");
461 }
462}