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 "bash".into(),
115 "read".into(),
116 "edit".into(),
117 ]
118}
119
120fn default_instruction_auto_detect() -> bool {
121 true
122}
123
124fn default_max_concurrent() -> usize {
125 5
126}
127
128fn default_context_window_turns() -> usize {
129 10
130}
131
132fn default_max_spawn_depth() -> u32 {
133 3
134}
135
136fn default_transcript_enabled() -> bool {
137 true
138}
139
140fn default_transcript_max_files() -> usize {
141 50
142}
143
144#[derive(Debug, Clone, Deserialize, Serialize)]
146#[serde(default)]
147pub struct FocusConfig {
148 pub enabled: bool,
150 #[serde(default = "default_focus_compression_interval")]
152 pub compression_interval: usize,
153 #[serde(default = "default_focus_reminder_interval")]
155 pub reminder_interval: usize,
156 #[serde(default = "default_focus_min_messages_per_focus")]
158 pub min_messages_per_focus: usize,
159 #[serde(default = "default_focus_max_knowledge_tokens")]
162 pub max_knowledge_tokens: usize,
163}
164
165impl Default for FocusConfig {
166 fn default() -> Self {
167 Self {
168 enabled: false,
169 compression_interval: default_focus_compression_interval(),
170 reminder_interval: default_focus_reminder_interval(),
171 min_messages_per_focus: default_focus_min_messages_per_focus(),
172 max_knowledge_tokens: default_focus_max_knowledge_tokens(),
173 }
174 }
175}
176
177#[derive(Debug, Clone, Deserialize, Serialize)]
182#[serde(default)]
183pub struct ToolFilterConfig {
184 pub enabled: bool,
186 #[serde(default = "default_tool_filter_top_k")]
189 pub top_k: usize,
190 #[serde(default = "default_tool_filter_always_on")]
192 pub always_on: Vec<String>,
193 #[serde(default = "default_tool_filter_min_description_words")]
195 pub min_description_words: usize,
196}
197
198impl Default for ToolFilterConfig {
199 fn default() -> Self {
200 Self {
201 enabled: false,
202 top_k: default_tool_filter_top_k(),
203 always_on: default_tool_filter_always_on(),
204 min_description_words: default_tool_filter_min_description_words(),
205 }
206 }
207}
208
209#[derive(Debug, Deserialize, Serialize)]
210pub struct AgentConfig {
211 pub name: String,
212 #[serde(default = "default_max_tool_iterations")]
213 pub max_tool_iterations: usize,
214 #[serde(default = "default_auto_update_check")]
215 pub auto_update_check: bool,
216 #[serde(default)]
218 pub instruction_files: Vec<std::path::PathBuf>,
219 #[serde(default = "default_instruction_auto_detect")]
222 pub instruction_auto_detect: bool,
223 #[serde(default = "default_max_tool_retries")]
225 pub max_tool_retries: usize,
226 #[serde(default = "default_tool_repeat_threshold")]
229 pub tool_repeat_threshold: usize,
230 #[serde(default = "default_max_retry_duration_secs")]
232 pub max_retry_duration_secs: u64,
233 #[serde(default)]
235 pub focus: FocusConfig,
236 #[serde(default)]
238 pub tool_filter: ToolFilterConfig,
239 #[serde(default = "default_budget_hint_enabled")]
243 pub budget_hint_enabled: bool,
244}
245
246fn default_budget_hint_enabled() -> bool {
247 true
248}
249
250#[derive(Debug, Clone, Deserialize, Serialize)]
251#[serde(default)]
252pub struct SubAgentConfig {
253 pub enabled: bool,
254 #[serde(default = "default_max_concurrent")]
256 pub max_concurrent: usize,
257 pub extra_dirs: Vec<PathBuf>,
258 #[serde(default)]
260 pub user_agents_dir: Option<PathBuf>,
261 pub default_permission_mode: Option<PermissionMode>,
263 #[serde(default)]
265 pub default_disallowed_tools: Vec<String>,
266 #[serde(default)]
268 pub allow_bypass_permissions: bool,
269 #[serde(default)]
271 pub default_memory_scope: Option<MemoryScope>,
272 #[serde(default)]
274 pub hooks: SubAgentLifecycleHooks,
275 #[serde(default)]
277 pub transcript_dir: Option<PathBuf>,
278 #[serde(default = "default_transcript_enabled")]
280 pub transcript_enabled: bool,
281 #[serde(default = "default_transcript_max_files")]
283 pub transcript_max_files: usize,
284 #[serde(default = "default_context_window_turns")]
287 pub context_window_turns: usize,
288 #[serde(default = "default_max_spawn_depth")]
290 pub max_spawn_depth: u32,
291 #[serde(default)]
293 pub context_injection_mode: ContextInjectionMode,
294}
295
296impl Default for SubAgentConfig {
297 fn default() -> Self {
298 Self {
299 enabled: false,
300 max_concurrent: default_max_concurrent(),
301 extra_dirs: Vec::new(),
302 user_agents_dir: None,
303 default_permission_mode: None,
304 default_disallowed_tools: Vec::new(),
305 allow_bypass_permissions: false,
306 default_memory_scope: None,
307 hooks: SubAgentLifecycleHooks::default(),
308 transcript_dir: None,
309 transcript_enabled: default_transcript_enabled(),
310 transcript_max_files: default_transcript_max_files(),
311 context_window_turns: default_context_window_turns(),
312 max_spawn_depth: default_max_spawn_depth(),
313 context_injection_mode: ContextInjectionMode::default(),
314 }
315 }
316}
317
318#[derive(Debug, Clone, Default, Deserialize, Serialize)]
320#[serde(default)]
321pub struct SubAgentLifecycleHooks {
322 pub start: Vec<HookDef>,
324 pub stop: Vec<HookDef>,
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331
332 #[test]
333 fn subagent_config_defaults() {
334 let cfg = SubAgentConfig::default();
335 assert_eq!(cfg.context_window_turns, 10);
336 assert_eq!(cfg.max_spawn_depth, 3);
337 assert_eq!(
338 cfg.context_injection_mode,
339 ContextInjectionMode::LastAssistantTurn
340 );
341 }
342
343 #[test]
344 fn subagent_config_deserialize_new_fields() {
345 let toml_str = r#"
346 enabled = true
347 context_window_turns = 5
348 max_spawn_depth = 2
349 context_injection_mode = "none"
350 "#;
351 let cfg: SubAgentConfig = toml::from_str(toml_str).unwrap();
352 assert_eq!(cfg.context_window_turns, 5);
353 assert_eq!(cfg.max_spawn_depth, 2);
354 assert_eq!(cfg.context_injection_mode, ContextInjectionMode::None);
355 }
356
357 #[test]
358 fn model_spec_deserialize_inherit() {
359 let spec: ModelSpec = serde_json::from_str("\"inherit\"").unwrap();
360 assert_eq!(spec, ModelSpec::Inherit);
361 }
362
363 #[test]
364 fn model_spec_deserialize_named() {
365 let spec: ModelSpec = serde_json::from_str("\"fast\"").unwrap();
366 assert_eq!(spec, ModelSpec::Named("fast".to_owned()));
367 }
368
369 #[test]
370 fn model_spec_as_str() {
371 assert_eq!(ModelSpec::Inherit.as_str(), "inherit");
372 assert_eq!(ModelSpec::Named("x".to_owned()).as_str(), "x");
373 }
374}