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