swarm_engine_eval/environments/
search.rs1use std::collections::HashSet;
22use std::sync::RwLock;
23
24use swarm_engine_core::actions::ParamResolver;
25use swarm_engine_core::agent::WorkResult;
26use swarm_engine_core::environment::Environment;
27use swarm_engine_core::types::{Action, WorkerId};
28
29pub struct SearchEnvironment {
37 files: Vec<String>,
39 target_file: String,
41 target_content: String,
43 state: RwLock<SearchState>,
45}
46
47#[derive(Debug, Default)]
48struct SearchState {
49 searched: bool,
51 read_files: HashSet<String>,
53 found_target: bool,
55 completed: Vec<WorkerId>,
57}
58
59impl SearchEnvironment {
60 pub fn new(
62 files: Vec<String>,
63 target_file: impl Into<String>,
64 target_content: impl Into<String>,
65 ) -> Self {
66 Self {
67 files,
68 target_file: target_file.into(),
69 target_content: target_content.into(),
70 state: RwLock::new(SearchState::default()),
71 }
72 }
73
74 pub fn basic_scenario() -> Self {
76 let files = vec![
77 "src/config.rs".to_string(),
78 "src/utils.rs".to_string(),
79 "src/handler.rs".to_string(), "src/types.rs".to_string(),
81 "src/error.rs".to_string(),
82 ];
83 Self::new(
84 files,
85 "src/handler.rs",
86 "fn handle_request() { /* implementation */ }",
87 )
88 }
89
90 pub fn medium_scenario() -> Self {
92 let files = vec![
93 "src/lib.rs".to_string(),
94 "src/config.rs".to_string(),
95 "src/utils.rs".to_string(),
96 "src/handler.rs".to_string(),
97 "src/types.rs".to_string(),
98 "src/error.rs".to_string(),
99 "src/api/mod.rs".to_string(),
100 "src/api/routes.rs".to_string(), "src/api/middleware.rs".to_string(),
102 "src/db/connection.rs".to_string(),
103 ];
104 Self::new(
105 files,
106 "src/api/routes.rs",
107 "pub fn register_routes(app: &mut App) { /* routes */ }",
108 )
109 }
110
111 pub fn large_scenario() -> Self {
113 let files = vec![
114 "src/lib.rs".to_string(),
115 "src/main.rs".to_string(),
116 "src/config.rs".to_string(),
117 "src/utils.rs".to_string(),
118 "src/handler.rs".to_string(),
119 "src/types.rs".to_string(),
120 "src/error.rs".to_string(),
121 "src/api/mod.rs".to_string(),
122 "src/api/routes.rs".to_string(),
123 "src/api/middleware.rs".to_string(),
124 "src/db/mod.rs".to_string(),
125 "src/db/connection.rs".to_string(),
126 "src/db/queries.rs".to_string(),
127 "src/services/mod.rs".to_string(),
128 "src/services/auth.rs".to_string(), "src/services/user.rs".to_string(),
130 "src/services/payment.rs".to_string(),
131 "src/models/mod.rs".to_string(),
132 "src/models/user.rs".to_string(),
133 "src/models/order.rs".to_string(),
134 ];
135 Self::new(
136 files,
137 "src/services/auth.rs",
138 "pub fn authenticate(token: &str) -> Result<User> { /* auth logic */ }",
139 )
140 }
141
142 pub fn custom_scenario(file_count: usize, target_index: usize, seed: u64) -> Self {
144 let file_count = file_count.clamp(2, 50);
145 let target_index = target_index.min(file_count - 1);
146
147 let mut rng_state = seed;
149 let mut next_rand = || {
150 rng_state = rng_state.wrapping_mul(6364136223846793005).wrapping_add(1);
151 rng_state
152 };
153
154 let prefixes = ["src", "lib", "core", "api", "services", "models", "utils"];
155 let suffixes = [
156 "mod", "types", "error", "handler", "config", "utils", "impl",
157 ];
158
159 let files: Vec<String> = (0..file_count)
160 .map(|i| {
161 let prefix = prefixes[(next_rand() as usize) % prefixes.len()];
162 let suffix = suffixes[(next_rand() as usize) % suffixes.len()];
163 format!("{}/{}_{}.rs", prefix, suffix, i)
164 })
165 .collect();
166
167 let target_file = files[target_index].clone();
168
169 Self::new(
170 files,
171 target_file,
172 "// TARGET FILE CONTENT\nfn target_function() { /* found! */ }",
173 )
174 }
175
176 fn handle_search_files(&self, _worker_id: WorkerId, action: &Action) -> WorkResult {
181 let resolver = ParamResolver::new(action);
182 let query = resolver.get("query").unwrap_or("*.rs");
183
184 let mut state = self.state.write().unwrap();
185 state.searched = true;
186
187 let files_json: Vec<serde_json::Value> = self
189 .files
190 .iter()
191 .map(|f| serde_json::Value::String(f.clone()))
192 .collect();
193
194 let result = serde_json::json!({
195 "query": query,
196 "count": self.files.len(),
197 "files": files_json,
198 "message": "Use ReadFile to examine each file."
199 });
200
201 WorkResult::env_success_structured(result)
202 }
203
204 fn handle_read_file(&self, _worker_id: WorkerId, action: &Action) -> WorkResult {
205 let resolver = ParamResolver::new(action);
206 let file_path = match resolver.require("file") {
207 Ok(s) => s,
208 Err(e) => return WorkResult::env_failure(format!("ReadFile: {}", e)),
209 };
210
211 let mut state = self.state.write().unwrap();
212
213 if !self.files.contains(&file_path.to_string()) {
216 return WorkResult::env_failure(format!(
217 "File '{}' not found. Available files: {:?}",
218 file_path, self.files
219 ));
220 }
221
222 state.read_files.insert(file_path.to_string());
223
224 if file_path == self.target_file {
226 state.found_target = true;
227 WorkResult::env_success(format!(
228 "=== {} ===\n{}\n\n[TARGET FOUND] This file contains the target content!",
229 file_path, self.target_content
230 ))
231 } else {
232 WorkResult::env_failure(format!(
234 "=== {} ===\n// Empty or irrelevant content\n// Not the target file",
235 file_path
236 ))
237 }
238 }
239
240 fn handle_analyze(&self, worker_id: WorkerId, action: &Action) -> WorkResult {
241 let resolver = ParamResolver::new(action);
242 let file_path = match resolver.require("file") {
243 Ok(s) => s,
244 Err(e) => return WorkResult::env_failure(format!("Analyze: {}", e)),
245 };
246
247 let mut state = self.state.write().unwrap();
248
249 if !state.found_target {
251 return WorkResult::env_failure("Cannot analyze without finding the target file first. Read files to find the target.");
252 }
253
254 if file_path != self.target_file {
256 return WorkResult::env_failure(format!(
257 "Cannot analyze '{}'. The target file is different.",
258 file_path
259 ));
260 }
261
262 if !state.completed.contains(&worker_id) {
264 state.completed.push(worker_id);
265 }
266
267 WorkResult::done_success(format!(
268 "=== Analysis Complete ===\nFile: {}\nContent analyzed successfully!\n\nTask completed!",
269 file_path
270 ))
271 }
272}
273
274impl Environment for SearchEnvironment {
275 fn step(&self, worker_id: WorkerId, action: &Action) -> WorkResult {
276 match action.name.to_lowercase().as_str() {
277 "searchfiles" | "search_files" | "search" | "list" | "listfiles" | "list_files" => {
278 self.handle_search_files(worker_id, action)
279 }
280 "readfile" | "read_file" | "read" | "cat" => self.handle_read_file(worker_id, action),
281 "analyze" | "process" | "complete" => self.handle_analyze(worker_id, action),
282 "continue" => WorkResult::env_success("Continuing..."),
283 _ => WorkResult::unsupported(&action.name),
284 }
285 }
286
287 fn reset(&self) {
288 let mut state = self.state.write().unwrap();
289 state.searched = false;
290 state.read_files.clear();
291 state.found_target = false;
292 state.completed.clear();
293 }
294
295 fn name(&self) -> &str {
296 "SearchEnvironment"
297 }
298}
299
300#[cfg(test)]
305mod tests {
306 use super::*;
307 use std::collections::HashMap;
308
309 fn is_success(result: &WorkResult) -> bool {
310 match result {
311 WorkResult::Acted { action_result, .. } => action_result.success,
312 WorkResult::Done { success, .. } => *success,
313 _ => false,
314 }
315 }
316
317 fn is_done(result: &WorkResult) -> bool {
318 matches!(result, WorkResult::Done { .. })
319 }
320
321 fn action(name: &str, target: Option<&str>) -> Action {
322 Action {
323 name: name.into(),
324 params: swarm_engine_core::types::ActionParams {
325 target: target.map(|s| s.into()),
326 args: HashMap::new(),
327 data: vec![],
328 },
329 }
330 }
331
332 #[test]
333 fn test_search_files() {
334 let env = SearchEnvironment::basic_scenario();
335 let worker = WorkerId(0);
336
337 let result = env.step(worker, &action("SearchFiles", None));
338 assert!(is_success(&result));
339 assert!(!is_done(&result));
340 }
341
342 #[test]
343 fn test_read_file_without_search() {
344 let env = SearchEnvironment::basic_scenario();
345 let worker = WorkerId(0);
346
347 let result = env.step(worker, &action("ReadFile", Some("src/handler.rs")));
350 assert!(is_success(&result)); let env2 = SearchEnvironment::basic_scenario();
354 let result2 = env2.step(worker, &action("ReadFile", Some("src/config.rs")));
355 assert!(!is_success(&result2));
356 }
357
358 #[test]
359 fn test_read_target_file_success() {
360 let env = SearchEnvironment::basic_scenario();
361 let worker = WorkerId(0);
362
363 env.step(worker, &action("SearchFiles", None));
365
366 let result = env.step(worker, &action("ReadFile", Some("src/handler.rs")));
368 assert!(is_success(&result));
369 }
370
371 #[test]
372 fn test_read_wrong_file_failure() {
373 let env = SearchEnvironment::basic_scenario();
374 let worker = WorkerId(0);
375
376 env.step(worker, &action("SearchFiles", None));
378
379 let result = env.step(worker, &action("ReadFile", Some("src/config.rs")));
381 assert!(!is_success(&result)); }
383
384 #[test]
385 fn test_full_search_flow() {
386 let env = SearchEnvironment::basic_scenario();
387 let worker = WorkerId(0);
388
389 let result = env.step(worker, &action("SearchFiles", None));
391 assert!(is_success(&result));
392 assert!(!is_done(&result));
393
394 let result = env.step(worker, &action("ReadFile", Some("src/config.rs")));
396 assert!(!is_success(&result));
397 assert!(!is_done(&result));
398
399 let result = env.step(worker, &action("ReadFile", Some("src/handler.rs")));
401 assert!(is_success(&result));
402 assert!(!is_done(&result));
403
404 let result = env.step(worker, &action("Analyze", Some("src/handler.rs")));
406 assert!(is_success(&result));
407 assert!(is_done(&result));
408 }
409
410 #[test]
411 fn test_analyze_requires_found_target() {
412 let env = SearchEnvironment::basic_scenario();
413 let worker = WorkerId(0);
414
415 env.step(worker, &action("SearchFiles", None));
417
418 let result = env.step(worker, &action("Analyze", Some("src/handler.rs")));
420 assert!(!is_success(&result));
421 }
422
423 #[test]
424 fn test_custom_scenario() {
425 let env = SearchEnvironment::custom_scenario(10, 3, 42);
426 let worker = WorkerId(0);
427
428 let result = env.step(worker, &action("SearchFiles", None));
430 assert!(is_success(&result));
431 }
432}