1use crate::agent::WorkResult;
37use crate::types::{Action, WorkerId};
38
39pub trait Environment: Send + Sync {
60 fn step(&self, worker_id: WorkerId, action: &Action) -> WorkResult;
90
91 fn reset(&self);
95
96 fn name(&self) -> &str;
98}
99
100pub type EnvironmentBox = Box<dyn Environment>;
106
107use std::fs;
112use std::io::{BufRead, BufReader};
113use std::path::{Path, PathBuf};
114use std::process::Command;
115
116pub struct DefaultEnvironment {
130 working_dir: PathBuf,
132}
133
134impl DefaultEnvironment {
135 pub fn new() -> Self {
137 Self {
138 working_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
139 }
140 }
141
142 pub fn with_working_dir(working_dir: impl Into<PathBuf>) -> Self {
144 Self {
145 working_dir: working_dir.into(),
146 }
147 }
148
149 fn handle_bash(&self, action: &Action) -> WorkResult {
154 let command = action.params.target.as_deref().unwrap_or("");
155
156 let mut cmd = Command::new("sh");
157 cmd.arg("-c").arg(command);
158 cmd.current_dir(&self.working_dir);
159
160 match cmd.output() {
161 Ok(output) => {
162 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
163 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
164
165 if output.status.success() {
166 WorkResult::env_success_with_data("Command executed successfully", stdout)
167 } else {
168 WorkResult::env_failure(format!(
169 "Exit code: {:?}\nstderr: {}",
170 output.status.code(),
171 stderr
172 ))
173 }
174 }
175 Err(e) => WorkResult::env_failure(format!("Failed to execute: {}", e)),
176 }
177 }
178
179 fn handle_read(&self, action: &Action) -> WorkResult {
180 let path = action.params.target.as_deref().unwrap_or("");
181 let full_path = self.resolve_path(path);
182
183 match fs::read_to_string(&full_path) {
184 Ok(content) => WorkResult::env_success_with_data("File read successfully", content),
185 Err(e) => WorkResult::env_failure(format!("Failed to read {}: {}", path, e)),
186 }
187 }
188
189 fn handle_write(&self, action: &Action) -> WorkResult {
190 let path = action.params.target.as_deref().unwrap_or("");
191 let content = action
192 .params
193 .args
194 .get("content")
195 .map(|s| s.as_str())
196 .unwrap_or("");
197
198 let full_path = self.resolve_path(path);
199
200 if let Some(parent) = full_path.parent() {
202 if !parent.exists() {
203 if let Err(e) = fs::create_dir_all(parent) {
204 return WorkResult::env_failure(format!("Failed to create directory: {}", e));
205 }
206 }
207 }
208
209 match fs::write(&full_path, content) {
210 Ok(()) => WorkResult::env_success(format!("Written to {}", path)),
211 Err(e) => WorkResult::env_failure(format!("Failed to write {}: {}", path, e)),
212 }
213 }
214
215 fn handle_grep(&self, action: &Action) -> WorkResult {
216 let target_str = action.params.target.as_deref().unwrap_or("");
219 let (pattern, search_path) = if let Some(p) = action.params.args.get("pattern") {
220 let path = if target_str.is_empty() {
221 "."
222 } else {
223 target_str
224 };
225 (p.as_str(), path)
226 } else if !target_str.is_empty()
227 && !target_str.contains('/')
228 && !target_str.contains('\\')
229 && !target_str.ends_with(".rs")
230 && !target_str.ends_with(".txt")
231 && !target_str.ends_with(".toml")
232 {
233 (target_str, ".")
236 } else {
237 ("", target_str)
238 };
239
240 let full_path = self.resolve_path(search_path);
241
242 if full_path.is_dir() {
244 return self.grep_directory(&full_path, pattern);
245 }
246
247 let file = match fs::File::open(&full_path) {
248 Ok(f) => f,
249 Err(e) => {
250 return WorkResult::env_failure(format!("Failed to open {}: {}", search_path, e))
251 }
252 };
253
254 let reader = BufReader::new(file);
255 let mut matches = Vec::new();
256
257 for (line_num, line) in reader.lines().enumerate() {
258 if let Ok(line) = line {
259 if line.contains(pattern) {
260 matches.push(format!("{}:{}:{}", search_path, line_num + 1, line));
261 }
262 }
263 }
264
265 WorkResult::env_success_with_data(
266 format!("Found {} matches", matches.len()),
267 matches.join("\n"),
268 )
269 }
270
271 fn grep_directory(&self, dir: &Path, pattern: &str) -> WorkResult {
273 let mut matches = Vec::new();
274
275 fn search_dir(dir: &Path, pattern: &str, matches: &mut Vec<String>) {
276 if let Ok(entries) = fs::read_dir(dir) {
277 for entry in entries.filter_map(|e| e.ok()) {
278 let path = entry.path();
279 if path.is_dir() {
280 if let Some(name) = path.file_name() {
282 let name = name.to_string_lossy();
283 if !name.starts_with('.') && name != "target" && name != "node_modules"
284 {
285 search_dir(&path, pattern, matches);
286 }
287 }
288 } else if path.extension().map(|e| e == "rs").unwrap_or(false) {
289 if let Ok(content) = fs::read_to_string(&path) {
290 for (line_num, line) in content.lines().enumerate() {
291 if line.contains(pattern) {
292 matches.push(format!(
293 "{}:{}:{}",
294 path.display(),
295 line_num + 1,
296 line
297 ));
298 }
299 }
300 }
301 }
302 }
303 }
304 }
305
306 search_dir(dir, pattern, &mut matches);
307
308 WorkResult::env_success_with_data(
309 format!("Found {} matches", matches.len()),
310 matches.join("\n"),
311 )
312 }
313
314 fn handle_glob(&self, action: &Action) -> WorkResult {
315 let target_str = action.params.target.as_deref().unwrap_or(".");
318 let (pattern, search_dir) = if let Some(p) = action.params.args.get("pattern") {
319 (p.as_str(), target_str)
320 } else if target_str.contains('*') {
321 (target_str, ".")
323 } else {
324 ("*", target_str)
325 };
326 let full_path = self.resolve_path(search_dir);
327
328 if pattern.contains("**") {
330 return self.glob_recursive(&full_path, pattern);
331 }
332
333 match fs::read_dir(&full_path) {
334 Ok(entries) => {
335 let files: Vec<String> = entries
336 .filter_map(|e| e.ok())
337 .filter(|e| {
338 if pattern == "*" {
339 return true;
340 }
341 if let Some(ext) = pattern.strip_prefix("*.") {
342 return e.path().extension().map(|x| x == ext).unwrap_or(false);
343 }
344 e.file_name().to_string_lossy().contains(pattern)
345 })
346 .map(|e| e.path().display().to_string())
347 .collect();
348
349 WorkResult::env_success_with_data(
350 format!("Found {} files", files.len()),
351 files.join("\n"),
352 )
353 }
354 Err(e) => WorkResult::env_failure(format!("Failed to read directory: {}", e)),
355 }
356 }
357
358 fn glob_recursive(&self, dir: &Path, pattern: &str) -> WorkResult {
360 let mut files = Vec::new();
361
362 let ext = if pattern.contains("*.") {
364 pattern.rsplit("*.").next()
365 } else {
366 None
367 };
368
369 fn collect_files(dir: &Path, ext: Option<&str>, files: &mut Vec<String>) {
370 if let Ok(entries) = fs::read_dir(dir) {
371 for entry in entries.filter_map(|e| e.ok()) {
372 let path = entry.path();
373 if path.is_dir() {
374 if let Some(name) = path.file_name() {
376 let name = name.to_string_lossy();
377 if !name.starts_with('.') && name != "target" && name != "node_modules"
378 {
379 collect_files(&path, ext, files);
380 }
381 }
382 } else if let Some(ext) = ext {
383 if path.extension().map(|e| e == ext).unwrap_or(false) {
384 files.push(path.display().to_string());
385 }
386 } else {
387 files.push(path.display().to_string());
388 }
389 }
390 }
391 }
392
393 collect_files(dir, ext, &mut files);
394
395 WorkResult::env_success_with_data(format!("Found {} files", files.len()), files.join("\n"))
396 }
397
398 fn handle_answer(&self, action: &Action) -> WorkResult {
399 let answer = action.params.target.as_deref().unwrap_or("");
400 WorkResult::done_success(format!("Answer: {}", answer))
401 }
402
403 fn handle_continue(&self, _action: &Action) -> WorkResult {
404 WorkResult::env_success("Continuing...")
405 }
406
407 fn resolve_path(&self, path: &str) -> PathBuf {
412 let p = Path::new(path);
413 if p.is_absolute() {
414 p.to_path_buf()
415 } else {
416 self.working_dir.join(p)
417 }
418 }
419}
420
421impl Default for DefaultEnvironment {
422 fn default() -> Self {
423 Self::new()
424 }
425}
426
427impl Environment for DefaultEnvironment {
428 fn step(&self, _worker_id: WorkerId, action: &Action) -> WorkResult {
429 match action.name.as_str() {
430 "Bash" => self.handle_bash(action),
431 "Read" => self.handle_read(action),
432 "Write" => self.handle_write(action),
433 "Grep" => self.handle_grep(action),
434 "Glob" => self.handle_glob(action),
435 "Answer" => self.handle_answer(action),
436 "Continue" => self.handle_continue(action),
437 _ => WorkResult::unsupported(&action.name),
438 }
439 }
440
441 fn reset(&self) {
442 }
444
445 fn name(&self) -> &str {
446 "DefaultEnvironment"
447 }
448}