orchestrator_config/config/
agent.rs1use crate::cli_types::AgentEnvEntry;
2use crate::selection::{SelectionStrategy, SelectionWeights};
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
10pub struct HealthPolicyConfig {
11 #[serde(default = "default_disease_duration_hours")]
14 pub disease_duration_hours: u64,
15
16 #[serde(default = "default_disease_threshold")]
18 pub disease_threshold: u32,
19
20 #[serde(default = "default_capability_success_threshold")]
22 pub capability_success_threshold: f64,
23}
24
25fn default_disease_duration_hours() -> u64 {
26 5
27}
28
29fn default_disease_threshold() -> u32 {
30 2
31}
32
33fn default_capability_success_threshold() -> f64 {
34 0.5
35}
36
37impl Default for HealthPolicyConfig {
38 fn default() -> Self {
39 Self {
40 disease_duration_hours: default_disease_duration_hours(),
41 disease_threshold: default_disease_threshold(),
42 capability_success_threshold: default_capability_success_threshold(),
43 }
44 }
45}
46
47impl HealthPolicyConfig {
48 pub fn is_default(&self) -> bool {
50 *self == Self::default()
51 }
52}
53
54#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
61#[serde(rename_all = "snake_case")]
62pub enum PromptDelivery {
63 Stdin,
65 File,
67 Env,
69 #[default]
71 Arg,
72}
73
74impl PromptDelivery {
75 pub fn is_default(&self) -> bool {
77 *self == Self::Arg
78 }
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize, Default)]
83pub struct AgentMetadata {
84 pub name: String,
86 #[serde(default, skip_serializing_if = "Option::is_none")]
88 pub description: Option<String>,
89 pub version: Option<String>,
91 pub cost: Option<u8>,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
101pub struct AgentCommandRule {
102 pub when: String,
104 pub command: String,
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct AgentConfig {
111 #[serde(default)]
112 pub metadata: AgentMetadata,
114 #[serde(default = "default_true")]
117 pub enabled: bool,
118 #[serde(default)]
119 pub capabilities: Vec<String>,
121 #[serde(default)]
123 pub command: String,
124 #[serde(default, skip_serializing_if = "Vec::is_empty")]
127 pub command_rules: Vec<AgentCommandRule>,
128 #[serde(default)]
129 pub selection: AgentSelectionConfig,
131 #[serde(default, skip_serializing_if = "Option::is_none")]
134 pub env: Option<Vec<AgentEnvEntry>>,
135 #[serde(default, skip_serializing_if = "PromptDelivery::is_default")]
137 pub prompt_delivery: PromptDelivery,
138 #[serde(default, skip_serializing_if = "HealthPolicyConfig::is_default")]
140 pub health_policy: HealthPolicyConfig,
141}
142
143fn default_true() -> bool {
144 true
145}
146
147impl AgentConfig {
148 pub fn new() -> Self {
150 Self {
151 metadata: AgentMetadata::default(),
152 enabled: true,
153 capabilities: Vec::new(),
154 command: String::new(),
155 command_rules: Vec::new(),
156 selection: AgentSelectionConfig::default(),
157 env: None,
158 prompt_delivery: PromptDelivery::default(),
159 health_policy: HealthPolicyConfig::default(),
160 }
161 }
162
163 pub fn supports_capability(&self, capability: &str) -> bool {
165 self.capabilities.contains(&capability.to_string())
166 }
167}
168
169impl Default for AgentConfig {
170 fn default() -> Self {
171 Self::new()
172 }
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize, Default)]
177pub struct AgentSelectionConfig {
178 #[serde(default = "default_selection_strategy")]
180 pub strategy: SelectionStrategy,
181 #[serde(default)]
183 pub weights: Option<SelectionWeights>,
184}
185
186fn default_selection_strategy() -> SelectionStrategy {
187 SelectionStrategy::CapabilityAware
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193
194 #[test]
195 fn test_agent_config_default_and_new() {
196 let cfg = AgentConfig::default();
197 assert!(cfg.capabilities.is_empty());
198 assert!(cfg.command.is_empty());
199 assert_eq!(cfg.metadata.name, "");
200 assert!(cfg.metadata.description.is_none());
201 assert!(cfg.metadata.version.is_none());
202 assert!(cfg.metadata.cost.is_none());
203 assert_eq!(cfg.prompt_delivery, PromptDelivery::Arg);
204
205 let cfg2 = AgentConfig::new();
206 assert!(cfg2.capabilities.is_empty());
207 assert_eq!(cfg2.prompt_delivery, PromptDelivery::Arg);
208 }
209
210 #[test]
211 fn prompt_delivery_default_is_arg() {
212 assert_eq!(PromptDelivery::default(), PromptDelivery::Arg);
213 assert!(PromptDelivery::Arg.is_default());
214 assert!(!PromptDelivery::Stdin.is_default());
215 assert!(!PromptDelivery::File.is_default());
216 assert!(!PromptDelivery::Env.is_default());
217 }
218
219 #[test]
220 fn prompt_delivery_serde_roundtrip() {
221 for (variant, expected_str) in [
222 (PromptDelivery::Stdin, "\"stdin\""),
223 (PromptDelivery::File, "\"file\""),
224 (PromptDelivery::Env, "\"env\""),
225 (PromptDelivery::Arg, "\"arg\""),
226 ] {
227 let json = serde_json::to_string(&variant).unwrap();
228 assert_eq!(json, expected_str);
229 let deserialized: PromptDelivery = serde_json::from_str(&json).unwrap();
230 assert_eq!(deserialized, variant);
231 }
232 }
233
234 #[test]
235 fn prompt_delivery_skip_serializing_default() {
236 let cfg = AgentConfig::new();
237 let json = serde_json::to_string(&cfg).unwrap();
238 assert!(
239 !json.contains("prompt_delivery"),
240 "default Arg should be omitted"
241 );
242
243 let mut cfg2 = AgentConfig::new();
244 cfg2.prompt_delivery = PromptDelivery::Stdin;
245 let json2 = serde_json::to_string(&cfg2).unwrap();
246 assert!(
247 json2.contains("prompt_delivery"),
248 "non-default should be present"
249 );
250 }
251
252 #[test]
253 fn test_agent_supports_capability() {
254 let mut agent = AgentConfig::new();
255 agent.capabilities = vec!["plan".to_string(), "qa".to_string()];
256 assert!(agent.supports_capability("plan"));
257 assert!(agent.supports_capability("qa"));
258 assert!(!agent.supports_capability("fix"));
259 }
260
261 #[test]
262 fn test_agent_command_field() {
263 let mut agent = AgentConfig::new();
264 agent.command = "glmcode -p \"{prompt}\"".to_string();
265 assert!(agent.command.contains("{prompt}"));
266 }
267
268 #[test]
269 fn test_agent_selection_config_default() {
270 let cfg = AgentSelectionConfig::default();
271 assert!(cfg.weights.is_none());
272 }
273
274 #[test]
275 fn command_rules_default_empty() {
276 let cfg = AgentConfig::new();
277 assert!(cfg.command_rules.is_empty());
278 }
279
280 #[test]
281 fn command_rules_serde_roundtrip() {
282 let mut cfg = AgentConfig::new();
283 cfg.command_rules = vec![AgentCommandRule {
284 when: "vars.loop_session_id != \"\"".to_string(),
285 command: "claude --resume {loop_session_id} -p \"{prompt}\"".to_string(),
286 }];
287 let json = serde_json::to_string(&cfg).unwrap();
288 assert!(json.contains("command_rules"));
289 assert!(json.contains("loop_session_id"));
290
291 let deserialized: AgentConfig = serde_json::from_str(&json).unwrap();
292 assert_eq!(deserialized.command_rules.len(), 1);
293 assert_eq!(
294 deserialized.command_rules[0].when,
295 cfg.command_rules[0].when
296 );
297 assert_eq!(
298 deserialized.command_rules[0].command,
299 cfg.command_rules[0].command
300 );
301 }
302
303 #[test]
304 fn command_rules_omitted_when_empty() {
305 let cfg = AgentConfig::new();
306 let json = serde_json::to_string(&cfg).unwrap();
307 assert!(
308 !json.contains("command_rules"),
309 "empty command_rules should be omitted"
310 );
311 }
312}