Skip to main content

swarm_engine_core/
environment.rs

1//! Environment - アクション実行環境の抽象化
2//!
3//! すべてのアクション実行を Environment 経由で行うことで、
4//! Actions/Env の組み合わせを外部から注入可能にする。
5//!
6//! # 設計
7//!
8//! ```text
9//! GenericWorker.execute_action()
10//!     │
11//!     └── Extensions.get::<EnvironmentBox>()
12//!             │
13//!             └── env.step(worker_id, action) → WorkResult
14//! ```
15//!
16//! # 重要
17//!
18//! - `step()` が唯一のアクション実行メソッド
19//! - `WorkResult::Done` で終了を通知
20//! - 観察が必要な場合は `Action("Look")` を送り、`step()` で処理する
21//!
22//! # 使用例
23//!
24//! ```ignore
25//! // デフォルト環境(Bash/Read/Write/Grep/Glob)
26//! let orchestrator = OrchestratorBuilder::new()
27//!     .environment(Box::new(DefaultEnvironment::new()))
28//!     .build();
29//!
30//! // カスタム環境(迷路)
31//! let orchestrator = OrchestratorBuilder::new()
32//!     .environment(Box::new(MazeEnvironment::from_map(map)))
33//!     .build();
34//! ```
35
36use crate::agent::WorkResult;
37use crate::types::{Action, WorkerId};
38
39// ============================================================================
40// Environment Trait
41// ============================================================================
42
43/// アクション実行環境トレイト
44///
45/// すべてのアクション実行はこのトレイトを通じて行われる。
46/// DefaultEnvironment(Bash/Read等)やカスタム環境(Maze等)を
47/// 同じインターフェースで扱える。
48///
49/// # 設計原則
50///
51/// - `step()` がすべてのアクションを処理する唯一のメソッド
52/// - `WorkResult::Done` で終了を通知
53/// - 観察(Look等)も Action として `step()` 経由で実行
54///
55/// # 内部可変性
56///
57/// `step` メソッドは `&self` を受け取るため、内部状態を変更する必要がある
58/// 環境(MazeEnvironment など)は `Mutex` や `RwLock` を使用すること。
59pub trait Environment: Send + Sync {
60    /// アクション実行
61    ///
62    /// すべてのアクション(移動、観察、待機等)をこのメソッドで処理する。
63    ///
64    /// # Arguments
65    ///
66    /// * `worker_id` - 実行する Worker の ID
67    /// * `action` - 実行するアクション
68    ///
69    /// # Returns
70    ///
71    /// `WorkResult` - 実行結果を直接返す
72    ///
73    /// - `WorkResult::Acted` - 通常のアクション結果
74    /// - `WorkResult::Done` - タスク完了
75    /// - `WorkResult::env_success()` / `WorkResult::env_failure()` 等のヘルパーを使用
76    ///
77    /// # Example
78    ///
79    /// ```ignore
80    /// // 移動アクション
81    /// let action = Action::new("Move").with_arg("target", "north");
82    /// let result = env.step(worker_id, &action);
83    ///
84    /// // 観察アクション
85    /// let action = Action::new("Look");
86    /// let result = env.step(worker_id, &action);
87    /// // ActionResult.output に JSON データ
88    /// ```
89    fn step(&self, worker_id: WorkerId, action: &Action) -> WorkResult;
90
91    /// 環境をリセット
92    ///
93    /// 評価システムが複数回実行する際に使用。
94    fn reset(&self);
95
96    /// 環境名
97    fn name(&self) -> &str;
98}
99
100// ============================================================================
101// EnvironmentBox (Type alias)
102// ============================================================================
103
104/// Environment の Box 型エイリアス
105pub type EnvironmentBox = Box<dyn Environment>;
106
107// ============================================================================
108// DefaultEnvironment
109// ============================================================================
110
111use std::fs;
112use std::io::{BufRead, BufReader};
113use std::path::{Path, PathBuf};
114use std::process::Command;
115
116/// デフォルト環境 - ファイル操作・シェルコマンド
117///
118/// 従来の GenericWorker がサポートしていたアクションを Environment 経由で提供。
119///
120/// # サポートするアクション
121///
122/// - `Bash`: シェルコマンド実行
123/// - `Read`: ファイル読み込み
124/// - `Write`: ファイル書き込み
125/// - `Grep`: パターン検索(ファイル内)
126/// - `Glob`: ファイル検索(パターンマッチ)
127/// - `Answer`: 回答(成功扱い)
128/// - `Continue`: 継続(成功扱い)
129pub struct DefaultEnvironment {
130    /// 作業ディレクトリ
131    working_dir: PathBuf,
132}
133
134impl DefaultEnvironment {
135    /// 新しい DefaultEnvironment を作成
136    pub fn new() -> Self {
137        Self {
138            working_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
139        }
140    }
141
142    /// 作業ディレクトリを指定して作成
143    pub fn with_working_dir(working_dir: impl Into<PathBuf>) -> Self {
144        Self {
145            working_dir: working_dir.into(),
146        }
147    }
148
149    // ------------------------------------------------------------------------
150    // Action Handlers
151    // ------------------------------------------------------------------------
152
153    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        // 親ディレクトリを作成
201        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        // args["pattern"] または target からパターンを取得
217        // target がファイルパスっぽくない場合はパターンとして扱う(後方互換性)
218        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 がパターンの場合(例: "fn main")
234            // カレントディレクトリの全 .rs ファイルを検索
235            (target_str, ".")
236        } else {
237            ("", target_str)
238        };
239
240        let full_path = self.resolve_path(search_path);
241
242        // ディレクトリの場合は再帰検索
243        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    /// ディレクトリを再帰的に grep
272    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                        // .git 等を除外
281                        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        // args["pattern"] または target からパターンを取得
316        // target が "*" を含む場合はパターンとして扱う(後方互換性)
317        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 がパターンの場合(例: "**/*.rs")
322            (target_str, ".")
323        } else {
324            ("*", target_str)
325        };
326        let full_path = self.resolve_path(search_dir);
327
328        // ** を含む場合は再帰検索
329        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    /// 再帰的な glob 検索
359    fn glob_recursive(&self, dir: &Path, pattern: &str) -> WorkResult {
360        let mut files = Vec::new();
361
362        // パターンから拡張子を抽出(例: **/*.rs -> rs)
363        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                        // 隠しディレクトリと特定のディレクトリを除外
375                        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    // ------------------------------------------------------------------------
408    // Helpers
409    // ------------------------------------------------------------------------
410
411    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        // 状態なし
443    }
444
445    fn name(&self) -> &str {
446        "DefaultEnvironment"
447    }
448}