1use std::collections::HashMap;
6
7#[derive(Debug, Clone)]
9pub struct ParamSpec {
10 pub name: String,
12 pub required: bool,
14 pub description: String,
16}
17
18impl ParamSpec {
19 pub fn required(name: impl Into<String>, description: impl Into<String>) -> Self {
21 Self {
22 name: name.into(),
23 required: true,
24 description: description.into(),
25 }
26 }
27
28 pub fn optional(name: impl Into<String>, description: impl Into<String>) -> Self {
30 Self {
31 name: name.into(),
32 required: false,
33 description: description.into(),
34 }
35 }
36}
37
38#[derive(Debug, Clone)]
40pub struct ActionSpec {
41 pub canonical_name: String,
43 pub aliases: Vec<String>,
45 pub params: Vec<ParamSpec>,
47 pub description: String,
49}
50
51impl ActionSpec {
52 pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
54 Self {
55 canonical_name: name.into(),
56 aliases: Vec::new(),
57 params: Vec::new(),
58 description: description.into(),
59 }
60 }
61
62 pub fn aliases<I, S>(mut self, aliases: I) -> Self
64 where
65 I: IntoIterator<Item = S>,
66 S: Into<String>,
67 {
68 self.aliases = aliases.into_iter().map(|s| s.into()).collect();
69 self
70 }
71
72 pub fn required_param(mut self, name: impl Into<String>, desc: impl Into<String>) -> Self {
74 self.params.push(ParamSpec::required(name, desc));
75 self
76 }
77
78 pub fn optional_param(mut self, name: impl Into<String>, desc: impl Into<String>) -> Self {
80 self.params.push(ParamSpec::optional(name, desc));
81 self
82 }
83
84 pub fn matches(&self, name: &str) -> bool {
86 let lower = name.to_lowercase();
87 self.canonical_name.to_lowercase() == lower
88 || self.aliases.iter().any(|a| a.to_lowercase() == lower)
89 }
90}
91
92#[derive(Debug, Clone)]
94pub struct EnvironmentSpec {
95 pub env_type: String,
97 pub actions: Vec<ActionSpec>,
99 pub description: String,
101}
102
103impl EnvironmentSpec {
104 pub fn new(env_type: impl Into<String>, description: impl Into<String>) -> Self {
106 Self {
107 env_type: env_type.into(),
108 actions: Vec::new(),
109 description: description.into(),
110 }
111 }
112
113 pub fn action(mut self, spec: ActionSpec) -> Self {
115 self.actions.push(spec);
116 self
117 }
118
119 pub fn supports_action(&self, name: &str) -> bool {
121 self.actions.iter().any(|a| a.matches(name))
122 }
123
124 pub fn get_action(&self, name: &str) -> Option<&ActionSpec> {
126 self.actions.iter().find(|a| a.matches(name))
127 }
128}
129
130pub struct EnvironmentSpecRegistry {
132 specs: HashMap<String, EnvironmentSpec>,
133}
134
135impl Default for EnvironmentSpecRegistry {
136 fn default() -> Self {
137 Self::new()
138 }
139}
140
141impl EnvironmentSpecRegistry {
142 pub fn new() -> Self {
144 let mut registry = Self {
145 specs: HashMap::new(),
146 };
147
148 registry.register(
150 EnvironmentSpec::new("troubleshooting", "Service troubleshooting environment")
151 .action(
152 ActionSpec::new("CheckStatus", "Check service status")
153 .aliases(["check_status", "status"])
154 .optional_param("service", "Target service name"),
155 )
156 .action(
157 ActionSpec::new("ReadLogs", "Read service logs")
158 .aliases(["read_logs", "logs"])
159 .required_param("service", "Target service name"),
160 )
161 .action(
162 ActionSpec::new("AnalyzeMetrics", "Analyze service metrics")
163 .aliases(["analyze_metrics", "metrics"])
164 .required_param("service", "Target service name"),
165 )
166 .action(
167 ActionSpec::new("Diagnose", "Diagnose service problem")
168 .aliases(["diagnosis"])
169 .required_param("service", "Target service name"),
170 )
171 .action(
172 ActionSpec::new("Restart", "Restart service")
173 .aliases(["reboot"])
174 .required_param("service", "Target service name"),
175 ),
176 );
177
178 registry.register(
180 EnvironmentSpec::new("search", "File search environment")
181 .action(
182 ActionSpec::new("SearchFiles", "Search for files")
183 .aliases(["search_files", "search", "list", "listfiles", "list_files"])
184 .optional_param("query", "Search query pattern"),
185 )
186 .action(
187 ActionSpec::new("ReadFile", "Read file content")
188 .aliases(["read_file", "read", "cat"])
189 .required_param("file", "File path to read"),
190 )
191 .action(
192 ActionSpec::new("Analyze", "Analyze file content")
193 .aliases(["process", "complete"])
194 .required_param("file", "File path to analyze"),
195 ),
196 );
197
198 registry.register(
200 EnvironmentSpec::new("code", "Code exploration environment")
201 .action(
202 ActionSpec::new("Grep", "Search for pattern in files")
203 .aliases(["grep"])
204 .required_param("pattern", "Search pattern"),
205 )
206 .action(
207 ActionSpec::new("Read", "Read file content")
208 .aliases(["read"])
209 .required_param("path", "File path to read"),
210 )
211 .action(ActionSpec::new("List", "List files in codebase").aliases(["list", "ls"])),
212 );
213
214 registry.register(
216 EnvironmentSpec::new("internal_diagnosis", "SwarmEngine internal diagnosis")
217 .action(
218 ActionSpec::new("ParseConfig", "Parse configuration file")
219 .aliases(["parse_config", "config"])
220 .optional_param("path", "Config file path"),
221 )
222 .action(
223 ActionSpec::new("AnalyzeLog", "Analyze log entries")
224 .aliases(["analyze_log", "log"])
225 .optional_param("filter", "Log filter pattern"),
226 )
227 .action(
228 ActionSpec::new("TraceError", "Trace error code")
229 .aliases(["trace_error", "trace"])
230 .required_param("code", "Error code (SW-XXXX)"),
231 )
232 .action(
233 ActionSpec::new("ApplyFix", "Apply configuration fix")
234 .aliases(["apply_fix", "fix"])
235 .required_param("key", "Configuration key")
236 .required_param("value", "New value"),
237 ),
238 );
239
240 registry.register(
242 EnvironmentSpec::new("deep_search", "Deep search with multiple sources")
243 .action(
244 ActionSpec::new("Search", "Search for information")
245 .required_param("query", "Search query"),
246 )
247 .action(
248 ActionSpec::new("ReadDocument", "Read document content")
249 .required_param("doc_id", "Document ID"),
250 )
251 .action(
252 ActionSpec::new("EvaluateSource", "Evaluate source reliability")
253 .required_param("source", "Source to evaluate"),
254 )
255 .action(ActionSpec::new("Synthesize", "Synthesize findings"))
256 .action(
257 ActionSpec::new("Answer", "Submit final answer")
258 .required_param("answer", "Final answer"),
259 ),
260 );
261
262 registry.register(
264 EnvironmentSpec::new("maze", "Grid navigation environment")
265 .action(
266 ActionSpec::new("Move", "Move to adjacent cell")
267 .required_param("direction", "Direction (north/south/east/west)"),
268 )
269 .action(ActionSpec::new("Look", "Look around current position"))
270 .action(ActionSpec::new("Wait", "Wait at current position")),
271 );
272
273 registry.register(EnvironmentSpec::new(
275 "none",
276 "No-op environment for testing",
277 ));
278
279 registry.register(
281 EnvironmentSpec::new("default", "Real filesystem environment")
282 .action(
283 ActionSpec::new("Bash", "Execute bash command")
284 .required_param("command", "Command to execute"),
285 )
286 .action(ActionSpec::new("Read", "Read file").required_param("path", "File path"))
287 .action(
288 ActionSpec::new("Write", "Write file")
289 .required_param("path", "File path")
290 .required_param("content", "File content"),
291 )
292 .action(
293 ActionSpec::new("Grep", "Search pattern in files")
294 .required_param("pattern", "Search pattern"),
295 )
296 .action(
297 ActionSpec::new("Glob", "Find files by pattern")
298 .required_param("pattern", "Glob pattern"),
299 ),
300 );
301
302 if let Some(default_spec) = registry.specs.get("default").cloned() {
304 let mut realworld_spec = default_spec;
305 realworld_spec.env_type = "realworld".to_string();
306 registry
307 .specs
308 .insert("realworld".to_string(), realworld_spec);
309 }
310
311 registry
312 }
313
314 pub fn register(&mut self, spec: EnvironmentSpec) {
316 self.specs.insert(spec.env_type.clone(), spec);
317 }
318
319 pub fn get(&self, env_type: &str) -> Option<&EnvironmentSpec> {
321 self.specs.get(env_type)
322 }
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328
329 #[test]
330 fn test_action_spec_matches() {
331 let spec = ActionSpec::new("CheckStatus", "Check service status")
332 .aliases(["check_status", "status"]);
333
334 assert!(spec.matches("CheckStatus"));
335 assert!(spec.matches("checkstatus")); assert!(spec.matches("check_status"));
337 assert!(spec.matches("status"));
338 assert!(!spec.matches("unknown"));
339 }
340
341 #[test]
342 fn test_environment_spec_supports_action() {
343 let spec = EnvironmentSpec::new("test", "Test environment")
344 .action(ActionSpec::new("Action1", "desc").aliases(["a1"]))
345 .action(ActionSpec::new("Action2", "desc"));
346
347 assert!(spec.supports_action("Action1"));
348 assert!(spec.supports_action("a1"));
349 assert!(spec.supports_action("Action2"));
350 assert!(!spec.supports_action("Action3"));
351 }
352
353 #[test]
354 fn test_registry_builtin_specs() {
355 let registry = EnvironmentSpecRegistry::new();
356
357 assert!(registry.get("troubleshooting").is_some());
358 assert!(registry.get("search").is_some());
359 assert!(registry.get("code").is_some());
360 assert!(registry.get("internal_diagnosis").is_some());
361 assert!(registry.get("deep_search").is_some());
362 assert!(registry.get("maze").is_some());
363 assert!(registry.get("none").is_some());
364 assert!(registry.get("default").is_some());
365 assert!(registry.get("realworld").is_some());
366 }
367
368 #[test]
369 fn test_troubleshooting_actions() {
370 let registry = EnvironmentSpecRegistry::new();
371 let spec = registry.get("troubleshooting").unwrap();
372
373 assert!(spec.supports_action("CheckStatus"));
374 assert!(spec.supports_action("check_status"));
375 assert!(spec.supports_action("ReadLogs"));
376 assert!(spec.supports_action("AnalyzeMetrics"));
377 assert!(spec.supports_action("Diagnose"));
378 assert!(spec.supports_action("Restart"));
379 assert!(!spec.supports_action("Unknown"));
380 }
381}