Skip to main content

swarm_engine_core/exploration/
mutation.rs

1//! Mutation - 入力を Map 操作に変換するための型定義
2//!
3//! # 設計思想
4//!
5//! - **Map は純粋なデータ構造**: ロジックを持たない(Plain Board)
6//! - **Operator が変換ロジックを担当**: Input → MapUpdate
7//! - **戦略の切り替え**: Operator パターンで Selection を動的に切り替え
8//!
9//! # 責務の分離
10//!
11//! | レイヤー | 責務 |
12//! |----------|------|
13//! | Orchestrator | Operator を保持、MutationInput を渡す |
14//! | MutationLogic | 「結果をどう Map に反映するか」を決定 |
15//! | SelectionLogic | 「次にどの Node を選ぶか」を決定 |
16//! | Operator | MutationLogic + SelectionLogic の連携 |
17//! | GraphMap | 純粋なデータ操作 |
18//!
19//! # 型変換の設計
20//!
21//! `WorkerResult` のような固定型ではなく、`MutationInput` trait で抽象化。
22//! Agent 側の結果型に `MutationInput` を実装すれば直接渡せる。
23//!
24//! ```ignore
25//! // Agent 側で実装
26//! impl MutationInput for WorkResult {
27//!     fn node_id(&self) -> MapNodeId { ... }
28//!     fn action_name(&self) -> &str { ... }
29//!     // ...
30//! }
31//!
32//! // Operator 経由で処理
33//! let updates = operator.interpret(&work_result, &map, &actions);
34//! ```
35//!
36//! # 親ノードのクローズ戦略
37//!
38//! 探索マップにおける親ノードのクローズタイミングは **MutationLogic が管理する**。
39//! Map 側は状態を保持するだけで、いつ Open→Closed にするかは MutationLogic の責務。
40//!
41//! ## 主な戦略パターン
42//!
43//! | 戦略 | タイミング | 特徴 |
44//! |------|-----------|------|
45//! | **Eager Close** | 子ノード追加時に即座に親を Close | シンプル、BFS/DFS向け |
46//! | **Lazy Close** | 全ての子が Closed になったら親を Close | 進捗可視化、バックトラック可能 |
47//! | **Reference Count** | pending_children をカウント、0で Close | 依存解決向け |
48//!
49//! # 使用例
50//!
51//! ```ignore
52//! use exploration::{Operator, RulesBasedMutation, FifoSelection, NodeRules};
53//!
54//! let rules: NodeRules = dependency_graph.into();
55//! let mut operator = Operator::new(RulesBasedMutation::new(), FifoSelection::new(), rules);
56//!
57//! // MutationInput を実装した型を渡す
58//! let updates = operator.interpret(&input, &map, &actions);
59//!
60//! // Map に適用
61//! for update in updates {
62//!     map.apply_update(update, |k| k.to_string());
63//! }
64//! ```
65
66use std::collections::HashMap;
67use std::fmt::Debug;
68use std::hash::Hash;
69
70use serde::{Deserialize, Serialize};
71
72use super::map::{AddResult, GraphMap, MapNodeId, MapState};
73
74use crate::actions::{Action, ActionParams};
75
76// ============================================================================
77// ActionNodeData - Node が保持する Action 情報
78// ============================================================================
79
80/// ノードが保持する Action 情報
81///
82/// 探索空間の各ノードは「どのアクションを実行すべきか」を保持する。
83/// `Action` への変換が可能で、Manager が Worker に指示を出す際に使用する。
84///
85/// # 設計意図
86///
87/// - **Action ↔ Node の対称性**: Action → Node(実行結果)、Node → Action(復元)
88/// - **自動実行**: ノード情報から Action を復元できるので、LLM 不要で実行可能
89/// - **discovery 保持**: 実行結果の発見情報も保持
90///
91/// # Example
92///
93/// ```ignore
94/// // MutationInput から ActionNodeData を生成
95/// let node_data = ActionNodeData::from_input(&input);
96///
97/// // Action に変換して Worker に渡す
98/// let action: Action = (&node_data).into();
99/// worker.execute(action);
100/// ```
101#[derive(Debug, Clone, Default, Serialize, Deserialize)]
102pub struct ActionNodeData {
103    /// アクション名(例: "grep", "read")
104    pub action_name: String,
105    /// ターゲット(例: "src/auth.rs")
106    pub target: Option<String>,
107    /// 追加引数
108    pub args: HashMap<String, String>,
109    /// 発見情報(アクション実行結果)
110    pub discovery: Option<serde_json::Value>,
111}
112
113impl ActionNodeData {
114    /// 新しい ActionNodeData を作成
115    pub fn new(action_name: impl Into<String>) -> Self {
116        Self {
117            action_name: action_name.into(),
118            target: None,
119            args: HashMap::new(),
120            discovery: None,
121        }
122    }
123
124    /// ターゲットを設定
125    pub fn with_target(mut self, target: impl Into<String>) -> Self {
126        self.target = Some(target.into());
127        self
128    }
129
130    /// 引数を追加
131    pub fn with_arg(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
132        self.args.insert(key.into(), value.into());
133        self
134    }
135
136    /// 発見情報を設定
137    pub fn with_discovery(mut self, discovery: serde_json::Value) -> Self {
138        self.discovery = Some(discovery);
139        self
140    }
141
142    /// MutationInput から ActionNodeData を生成
143    pub fn from_input(input: &dyn MutationInput) -> Self {
144        // ExplorationResult::Discover の children を JSON 配列として保存
145        let discovery = match input.result() {
146            ExplorationResult::Discover(children) => Some(serde_json::Value::Array(
147                children
148                    .iter()
149                    .map(|s| serde_json::Value::String(s.clone()))
150                    .collect(),
151            )),
152            _ => None,
153        };
154        Self {
155            action_name: input.action_name().to_string(),
156            target: input.target().map(|s| s.to_string()),
157            args: HashMap::new(),
158            discovery,
159        }
160    }
161}
162
163/// ActionNodeData から Action への変換
164impl From<&ActionNodeData> for Action {
165    fn from(data: &ActionNodeData) -> Self {
166        Action {
167            name: data.action_name.clone(),
168            params: ActionParams {
169                target: data.target.clone(),
170                args: data.args.clone(),
171                data: Vec::new(),
172            },
173        }
174    }
175}
176
177/// ActionNodeData から Action への変換(所有権移動版)
178impl From<ActionNodeData> for Action {
179    fn from(data: ActionNodeData) -> Self {
180        Action {
181            name: data.action_name,
182            params: ActionParams {
183                target: data.target,
184                args: data.args,
185                data: Vec::new(),
186            },
187        }
188    }
189}
190
191// ============================================================================
192// ActionExtractor - ノードデータから action/target を抽出
193// ============================================================================
194
195/// ノードデータから action_name と target を抽出する trait
196///
197/// SelectionLogic の実装でスコアベースの選択を行う際に使用。
198/// `GraphMap<N, E, S>` の `N` がこの trait を実装していれば、
199/// ノードから action/target を取り出してスコア計算できる。
200pub trait ActionExtractor {
201    /// アクション名を取得
202    fn action_name(&self) -> &str;
203
204    /// ターゲットを取得(Optional)
205    fn target(&self) -> Option<&str>;
206
207    /// (action_name, target) のタプルとして取得
208    fn extract(&self) -> (&str, Option<&str>) {
209        (self.action_name(), self.target())
210    }
211}
212
213impl ActionExtractor for ActionNodeData {
214    fn action_name(&self) -> &str {
215        &self.action_name
216    }
217
218    fn target(&self) -> Option<&str> {
219        self.target.as_deref()
220    }
221}
222
223// ============================================================================
224// ExplorationResult - アクション実行結果
225// ============================================================================
226
227/// 探索結果(Exploration Result)
228///
229/// WorkResult を探索空間のセマンティクスに変換した結果。
230/// Strategy は match で全ケースを処理することが強制される。
231///
232/// ## バリアント
233///
234/// - `Discover`: List 系アクションで children を発見
235/// - `Success`: Check 系アクションで成功(ゴール到達など)
236/// - `Fail`: 失敗(行き止まり、エラー)
237///
238/// ## 設計ノート
239///
240/// 非同期処理(長時間タスク等)は AsyncRequest パターンで対応予定。
241/// Worker から切り離し、Swarm レベルで Job 管理する形式を想定。
242/// Continue バリアントは Timeout/Recover 等の考慮が必要なため、
243/// 現時点では導入しない。
244#[derive(Debug, Clone, PartialEq)]
245pub enum ExplorationResult {
246    /// 発見: children を見つけた(List 系)
247    ///
248    /// e.g., `List(dir)` → `["file1.rs", "file2.rs"]`
249    /// e.g., `WebSearch(query)` → `["url1", "url2", "url3"]`
250    ///
251    /// Strategy は各 child を target として後続ノードを展開する。
252    Discover(Vec<String>),
253
254    /// 成功: 処理完了(Check 系)
255    ///
256    /// e.g., `Read(file)` → Found
257    /// e.g., `Validate(result)` → Pass
258    ///
259    /// Strategy は後続ノードを展開して Close。
260    Success,
261
262    /// 失敗: 行き止まり
263    ///
264    /// e.g., `Read(file)` → NotFound
265    /// e.g., `Fetch(url)` → 404
266    ///
267    /// Option<String> にエラー詳細を含められる(SLM に渡す等)。
268    /// Strategy は Close のみ。
269    Fail(Option<String>),
270}
271
272impl ExplorationResult {
273    /// 成功系か(Discover または Success)
274    pub fn is_success(&self) -> bool {
275        matches!(
276            self,
277            ExplorationResult::Discover(_) | ExplorationResult::Success
278        )
279    }
280
281    /// 失敗か
282    pub fn is_fail(&self) -> bool {
283        matches!(self, ExplorationResult::Fail(_))
284    }
285
286    /// Discover の children を取得
287    pub fn children(&self) -> Option<&[String]> {
288        match self {
289            ExplorationResult::Discover(children) => Some(children),
290            _ => None,
291        }
292    }
293
294    /// Fail のエラーメッセージを取得
295    pub fn error(&self) -> Option<&str> {
296        match self {
297            ExplorationResult::Fail(Some(msg)) => Some(msg),
298            _ => None,
299        }
300    }
301}
302
303// ============================================================================
304// MutationInput - 入力型の抽象化
305// ============================================================================
306
307/// MutationLogic への入力を抽象化する trait
308///
309/// 固定型ではなく、任意の型から必要な情報を抽出できる。
310/// これにより Agent 側の結果型を直接渡せる。
311///
312/// # 設計意図
313///
314/// - 中間型(`WorkerResult`)を排除
315/// - Orchestrator と Exploration の結合を緩める
316/// - 必要な情報だけを trait で定義
317/// - ExplorationResult による結果の明示化(Strategy が全ケース処理を強制される)
318pub trait MutationInput {
319    /// 対象ノード ID
320    fn node_id(&self) -> MapNodeId;
321
322    /// 実行したアクション名
323    fn action_name(&self) -> &str;
324
325    /// アクションのターゲット(Optional)
326    fn target(&self) -> Option<&str>;
327
328    /// アクション実行結果
329    ///
330    /// Strategy は match で全ケースを処理する必要がある。
331    fn result(&self) -> &ExplorationResult;
332
333    /// 重複チェック用キーを生成
334    ///
335    /// デフォルト実装: `{action_name}:{target}`
336    fn dedup_key(&self) -> String {
337        let target = self.target().unwrap_or("_no_target_");
338        format!("{}:{}", self.action_name(), target)
339    }
340}
341
342// ============================================================================
343// MapUpdate - Map に対する操作
344// ============================================================================
345
346/// Map に対する操作(MutationLogic/Operator が生成)
347///
348/// GraphMap の具体的な操作を抽象化する。
349/// Operator はこの enum を生成し、Orchestrator が Map に適用する。
350#[derive(Debug, Clone)]
351pub enum MapUpdate<N, E, S: MapState> {
352    /// 子ノードを追加
353    ///
354    /// 重複チェック付き。既に同じキーのノードが存在すれば無視。
355    AddChild {
356        /// 親ノード
357        parent: MapNodeId,
358        /// エッジデータ
359        edge_data: E,
360        /// ノードデータ
361        node_data: N,
362        /// ノード状態
363        node_state: S,
364        /// 重複チェック用キー
365        dedup_key: String,
366    },
367
368    /// ノードを Close
369    ///
370    /// 「成功」「失敗」の意味付けは利用者の責務。
371    /// Map は単に Closed 状態にするだけ。
372    Close(MapNodeId),
373
374    /// 何もしない
375    Noop,
376}
377
378// ============================================================================
379// MapUpdateResult - 適用結果
380// ============================================================================
381
382/// MapUpdate 適用結果
383#[derive(Debug, Clone, PartialEq, Eq)]
384pub enum MapUpdateResult {
385    /// 新ノード作成
386    NodeCreated(MapNodeId),
387    /// ノード Close
388    NodeClosed(MapNodeId),
389    /// 無視(重複、Noop 等)
390    Ignored,
391    /// エラー
392    Error(String),
393}
394
395// ============================================================================
396// GraphMap extension - MapUpdate を適用
397// ============================================================================
398
399impl<N, E, S> GraphMap<N, E, S>
400where
401    N: Debug + Clone,
402    E: Debug + Clone,
403    S: MapState,
404{
405    /// MapUpdate を適用
406    pub fn apply_update<K, F>(&mut self, update: MapUpdate<N, E, S>, key_fn: F) -> MapUpdateResult
407    where
408        K: Hash,
409        F: Fn(&str) -> K,
410    {
411        match update {
412            MapUpdate::AddChild {
413                parent,
414                edge_data,
415                node_data,
416                node_state,
417                dedup_key,
418            } => {
419                let key = key_fn(&dedup_key);
420                match self.add_child_if_absent(parent, edge_data, node_data, node_state, |_| key) {
421                    Ok(AddResult::Added(id)) => MapUpdateResult::NodeCreated(id),
422                    Ok(AddResult::AlreadyExists(_)) => MapUpdateResult::Ignored,
423                    Err(e) => MapUpdateResult::Error(e.to_string()),
424                }
425            }
426            MapUpdate::Close(node_id) => {
427                self.close_with_cascade_up(node_id);
428                MapUpdateResult::NodeClosed(node_id)
429            }
430            MapUpdate::Noop => MapUpdateResult::Ignored,
431        }
432    }
433
434    /// 複数の MapUpdate を適用
435    pub fn apply_updates<K, F>(
436        &mut self,
437        updates: Vec<MapUpdate<N, E, S>>,
438        key_fn: F,
439    ) -> Vec<MapUpdateResult>
440    where
441        K: Hash + Clone,
442        F: Fn(&str) -> K,
443    {
444        updates
445            .into_iter()
446            .map(|u| self.apply_update(u, &key_fn))
447            .collect()
448    }
449}
450
451// ============================================================================
452// Tests
453// ============================================================================
454
455#[cfg(test)]
456mod tests {
457    use super::*;
458    use crate::exploration::map::{ExplorationMap, MapNodeState};
459
460    // テスト用の MutationInput 実装
461    struct TestInput {
462        node_id: MapNodeId,
463        action_name: String,
464        target: Option<String>,
465        result: ExplorationResult,
466    }
467
468    impl MutationInput for TestInput {
469        fn node_id(&self) -> MapNodeId {
470            self.node_id
471        }
472
473        fn action_name(&self) -> &str {
474            &self.action_name
475        }
476
477        fn target(&self) -> Option<&str> {
478            self.target.as_deref()
479        }
480
481        fn result(&self) -> &ExplorationResult {
482            &self.result
483        }
484    }
485
486    fn make_test_input(
487        node_id: u64,
488        action_name: &str,
489        target: Option<&str>,
490        success: bool,
491    ) -> TestInput {
492        let result = if success {
493            ExplorationResult::Success
494        } else {
495            ExplorationResult::Fail(None)
496        };
497        TestInput {
498            node_id: MapNodeId::new(node_id),
499            action_name: action_name.to_string(),
500            target: target.map(|s| s.to_string()),
501            result,
502        }
503    }
504
505    #[test]
506    fn test_mutation_input_dedup_key() {
507        let input = make_test_input(0, "grep", Some("src/auth.rs"), true);
508        assert_eq!(input.dedup_key(), "grep:src/auth.rs");
509    }
510
511    #[test]
512    fn test_mutation_input_dedup_key_no_target() {
513        let input = make_test_input(0, "list", None, true);
514        assert_eq!(input.dedup_key(), "list:_no_target_");
515    }
516
517    #[test]
518    fn test_map_update_add_child() {
519        let mut map: GraphMap<String, String, MapNodeState> = GraphMap::new();
520        let root = map.create_root("root".to_string(), MapNodeState::Open);
521
522        let update = MapUpdate::AddChild {
523            parent: root,
524            edge_data: "edge-1".to_string(),
525            node_data: "child-1".to_string(),
526            node_state: MapNodeState::Open,
527            dedup_key: "child-1".to_string(),
528        };
529
530        let result = map.apply_update(update, |k| k.to_string());
531        assert!(matches!(result, MapUpdateResult::NodeCreated(_)));
532        assert_eq!(map.node_count(), 2);
533    }
534
535    #[test]
536    fn test_map_update_add_child_dedup() {
537        let mut map: GraphMap<String, String, MapNodeState> = GraphMap::new();
538        let root = map.create_root("root".to_string(), MapNodeState::Open);
539
540        // 1回目: 追加される
541        let update1 = MapUpdate::AddChild {
542            parent: root,
543            edge_data: "edge-1".to_string(),
544            node_data: "child-1".to_string(),
545            node_state: MapNodeState::Open,
546            dedup_key: "same-key".to_string(),
547        };
548        let result1 = map.apply_update(update1, |k| k.to_string());
549        assert!(matches!(result1, MapUpdateResult::NodeCreated(_)));
550
551        // 2回目: 重複で無視
552        let update2 = MapUpdate::AddChild {
553            parent: root,
554            edge_data: "edge-2".to_string(),
555            node_data: "child-2".to_string(),
556            node_state: MapNodeState::Open,
557            dedup_key: "same-key".to_string(),
558        };
559        let result2 = map.apply_update(update2, |k| k.to_string());
560        assert!(matches!(result2, MapUpdateResult::Ignored));
561
562        assert_eq!(map.node_count(), 2); // root + child-1 のみ
563    }
564
565    #[test]
566    fn test_map_update_close() {
567        let mut map: GraphMap<String, String, MapNodeState> = GraphMap::new();
568        let root = map.create_root("root".to_string(), MapNodeState::Open);
569
570        let update = MapUpdate::<String, String, MapNodeState>::Close(root);
571        let result = map.apply_update(update, |k| k.to_string());
572
573        assert!(matches!(result, MapUpdateResult::NodeClosed(_)));
574        assert!(map.frontiers().is_empty());
575    }
576
577    #[test]
578    fn test_map_update_noop() {
579        let mut map: GraphMap<String, String, MapNodeState> = GraphMap::new();
580        let _root = map.create_root("root".to_string(), MapNodeState::Open);
581
582        let update = MapUpdate::<String, String, MapNodeState>::Noop;
583        let result = map.apply_update(update, |k| k.to_string());
584
585        assert!(matches!(result, MapUpdateResult::Ignored));
586    }
587
588    #[test]
589    fn test_apply_updates_batch() {
590        let mut map: GraphMap<String, String, MapNodeState> = GraphMap::new();
591        let root = map.create_root("root".to_string(), MapNodeState::Open);
592
593        let updates = vec![
594            MapUpdate::AddChild {
595                parent: root,
596                edge_data: "e1".to_string(),
597                node_data: "c1".to_string(),
598                node_state: MapNodeState::Open,
599                dedup_key: "c1".to_string(),
600            },
601            MapUpdate::AddChild {
602                parent: root,
603                edge_data: "e2".to_string(),
604                node_data: "c2".to_string(),
605                node_state: MapNodeState::Open,
606                dedup_key: "c2".to_string(),
607            },
608            MapUpdate::Noop,
609        ];
610
611        let results = map.apply_updates(updates, |k| k.to_string());
612
613        assert_eq!(results.len(), 3);
614        assert!(matches!(results[0], MapUpdateResult::NodeCreated(_)));
615        assert!(matches!(results[1], MapUpdateResult::NodeCreated(_)));
616        assert!(matches!(results[2], MapUpdateResult::Ignored));
617        assert_eq!(map.node_count(), 3);
618    }
619}