swarm-engine-core 0.1.6

Core types and orchestration for SwarmEngine
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
//! Mutation - 入力を Map 操作に変換するための型定義
//!
//! # 設計思想
//!
//! - **Map は純粋なデータ構造**: ロジックを持たない(Plain Board)
//! - **Operator が変換ロジックを担当**: Input → MapUpdate
//! - **戦略の切り替え**: Operator パターンで Selection を動的に切り替え
//!
//! # 責務の分離
//!
//! | レイヤー | 責務 |
//! |----------|------|
//! | Orchestrator | Operator を保持、MutationInput を渡す |
//! | MutationLogic | 「結果をどう Map に反映するか」を決定 |
//! | SelectionLogic | 「次にどの Node を選ぶか」を決定 |
//! | Operator | MutationLogic + SelectionLogic の連携 |
//! | GraphMap | 純粋なデータ操作 |
//!
//! # 型変換の設計
//!
//! `WorkerResult` のような固定型ではなく、`MutationInput` trait で抽象化。
//! Agent 側の結果型に `MutationInput` を実装すれば直接渡せる。
//!
//! ```ignore
//! // Agent 側で実装
//! impl MutationInput for WorkResult {
//!     fn node_id(&self) -> MapNodeId { ... }
//!     fn action_name(&self) -> &str { ... }
//!     // ...
//! }
//!
//! // Operator 経由で処理
//! let updates = operator.interpret(&work_result, &map, &actions);
//! ```
//!
//! # 親ノードのクローズ戦略
//!
//! 探索マップにおける親ノードのクローズタイミングは **MutationLogic が管理する**。
//! Map 側は状態を保持するだけで、いつ Open→Closed にするかは MutationLogic の責務。
//!
//! ## 主な戦略パターン
//!
//! | 戦略 | タイミング | 特徴 |
//! |------|-----------|------|
//! | **Eager Close** | 子ノード追加時に即座に親を Close | シンプル、BFS/DFS向け |
//! | **Lazy Close** | 全ての子が Closed になったら親を Close | 進捗可視化、バックトラック可能 |
//! | **Reference Count** | pending_children をカウント、0で Close | 依存解決向け |
//!
//! # 使用例
//!
//! ```ignore
//! use exploration::{Operator, RulesBasedMutation, FifoSelection, NodeRules};
//!
//! let rules: NodeRules = dependency_graph.into();
//! let mut operator = Operator::new(RulesBasedMutation::new(), FifoSelection::new(), rules);
//!
//! // MutationInput を実装した型を渡す
//! let updates = operator.interpret(&input, &map, &actions);
//!
//! // Map に適用
//! for update in updates {
//!     map.apply_update(update, |k| k.to_string());
//! }
//! ```

use std::collections::HashMap;
use std::fmt::Debug;
use std::hash::Hash;

use serde::{Deserialize, Serialize};

use super::map::{AddResult, GraphMap, MapNodeId, MapState};

use crate::actions::{Action, ActionParams};

// ============================================================================
// ActionNodeData - Node が保持する Action 情報
// ============================================================================

/// ノードが保持する Action 情報
///
/// 探索空間の各ノードは「どのアクションを実行すべきか」を保持する。
/// `Action` への変換が可能で、Manager が Worker に指示を出す際に使用する。
///
/// # 設計意図
///
/// - **Action ↔ Node の対称性**: Action → Node(実行結果)、Node → Action(復元)
/// - **自動実行**: ノード情報から Action を復元できるので、LLM 不要で実行可能
/// - **discovery 保持**: 実行結果の発見情報も保持
///
/// # Example
///
/// ```ignore
/// // MutationInput から ActionNodeData を生成
/// let node_data = ActionNodeData::from_input(&input);
///
/// // Action に変換して Worker に渡す
/// let action: Action = (&node_data).into();
/// worker.execute(action);
/// ```
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ActionNodeData {
    /// アクション名(例: "grep", "read")
    pub action_name: String,
    /// ターゲット(例: "src/auth.rs")
    pub target: Option<String>,
    /// 追加引数
    pub args: HashMap<String, String>,
    /// 発見情報(アクション実行結果)
    pub discovery: Option<serde_json::Value>,
}

impl ActionNodeData {
    /// 新しい ActionNodeData を作成
    pub fn new(action_name: impl Into<String>) -> Self {
        Self {
            action_name: action_name.into(),
            target: None,
            args: HashMap::new(),
            discovery: None,
        }
    }

    /// ターゲットを設定
    pub fn with_target(mut self, target: impl Into<String>) -> Self {
        self.target = Some(target.into());
        self
    }

    /// 引数を追加
    pub fn with_arg(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
        self.args.insert(key.into(), value.into());
        self
    }

    /// 発見情報を設定
    pub fn with_discovery(mut self, discovery: serde_json::Value) -> Self {
        self.discovery = Some(discovery);
        self
    }

    /// MutationInput から ActionNodeData を生成
    pub fn from_input(input: &dyn MutationInput) -> Self {
        // ExplorationResult::Discover の children を JSON 配列として保存
        let discovery = match input.result() {
            ExplorationResult::Discover(children) => Some(serde_json::Value::Array(
                children
                    .iter()
                    .map(|s| serde_json::Value::String(s.clone()))
                    .collect(),
            )),
            _ => None,
        };
        Self {
            action_name: input.action_name().to_string(),
            target: input.target().map(|s| s.to_string()),
            args: HashMap::new(),
            discovery,
        }
    }
}

/// ActionNodeData から Action への変換
impl From<&ActionNodeData> for Action {
    fn from(data: &ActionNodeData) -> Self {
        Action {
            name: data.action_name.clone(),
            params: ActionParams {
                target: data.target.clone(),
                args: data.args.clone(),
                data: Vec::new(),
            },
        }
    }
}

/// ActionNodeData から Action への変換(所有権移動版)
impl From<ActionNodeData> for Action {
    fn from(data: ActionNodeData) -> Self {
        Action {
            name: data.action_name,
            params: ActionParams {
                target: data.target,
                args: data.args,
                data: Vec::new(),
            },
        }
    }
}

// ============================================================================
// ActionExtractor - ノードデータから action/target を抽出
// ============================================================================

/// ノードデータから action_name と target を抽出する trait
///
/// SelectionLogic の実装でスコアベースの選択を行う際に使用。
/// `GraphMap<N, E, S>` の `N` がこの trait を実装していれば、
/// ノードから action/target を取り出してスコア計算できる。
pub trait ActionExtractor {
    /// アクション名を取得
    fn action_name(&self) -> &str;

    /// ターゲットを取得(Optional)
    fn target(&self) -> Option<&str>;

    /// (action_name, target) のタプルとして取得
    fn extract(&self) -> (&str, Option<&str>) {
        (self.action_name(), self.target())
    }
}

impl ActionExtractor for ActionNodeData {
    fn action_name(&self) -> &str {
        &self.action_name
    }

    fn target(&self) -> Option<&str> {
        self.target.as_deref()
    }
}

// ============================================================================
// ExplorationResult - アクション実行結果
// ============================================================================

/// 探索結果(Exploration Result)
///
/// WorkResult を探索空間のセマンティクスに変換した結果。
/// Strategy は match で全ケースを処理することが強制される。
///
/// ## バリアント
///
/// - `Discover`: List 系アクションで children を発見
/// - `Success`: Check 系アクションで成功(ゴール到達など)
/// - `Fail`: 失敗(行き止まり、エラー)
///
/// ## 設計ノート
///
/// 非同期処理(長時間タスク等)は AsyncRequest パターンで対応予定。
/// Worker から切り離し、Swarm レベルで Job 管理する形式を想定。
/// Continue バリアントは Timeout/Recover 等の考慮が必要なため、
/// 現時点では導入しない。
#[derive(Debug, Clone, PartialEq)]
pub enum ExplorationResult {
    /// 発見: children を見つけた(List 系)
    ///
    /// e.g., `List(dir)` → `["file1.rs", "file2.rs"]`
    /// e.g., `WebSearch(query)` → `["url1", "url2", "url3"]`
    ///
    /// Strategy は各 child を target として後続ノードを展開する。
    Discover(Vec<String>),

    /// 成功: 処理完了(Check 系)
    ///
    /// e.g., `Read(file)` → Found
    /// e.g., `Validate(result)` → Pass
    ///
    /// Strategy は後続ノードを展開して Close。
    Success,

    /// 失敗: 行き止まり
    ///
    /// e.g., `Read(file)` → NotFound
    /// e.g., `Fetch(url)` → 404
    ///
    /// Option<String> にエラー詳細を含められる(SLM に渡す等)。
    /// Strategy は Close のみ。
    Fail(Option<String>),
}

impl ExplorationResult {
    /// 成功系か(Discover または Success)
    pub fn is_success(&self) -> bool {
        matches!(
            self,
            ExplorationResult::Discover(_) | ExplorationResult::Success
        )
    }

    /// 失敗か
    pub fn is_fail(&self) -> bool {
        matches!(self, ExplorationResult::Fail(_))
    }

    /// Discover の children を取得
    pub fn children(&self) -> Option<&[String]> {
        match self {
            ExplorationResult::Discover(children) => Some(children),
            _ => None,
        }
    }

    /// Fail のエラーメッセージを取得
    pub fn error(&self) -> Option<&str> {
        match self {
            ExplorationResult::Fail(Some(msg)) => Some(msg),
            _ => None,
        }
    }
}

// ============================================================================
// MutationInput - 入力型の抽象化
// ============================================================================

/// MutationLogic への入力を抽象化する trait
///
/// 固定型ではなく、任意の型から必要な情報を抽出できる。
/// これにより Agent 側の結果型を直接渡せる。
///
/// # 設計意図
///
/// - 中間型(`WorkerResult`)を排除
/// - Orchestrator と Exploration の結合を緩める
/// - 必要な情報だけを trait で定義
/// - ExplorationResult による結果の明示化(Strategy が全ケース処理を強制される)
pub trait MutationInput {
    /// 対象ノード ID
    fn node_id(&self) -> MapNodeId;

    /// 実行したアクション名
    fn action_name(&self) -> &str;

    /// アクションのターゲット(Optional)
    fn target(&self) -> Option<&str>;

    /// アクション実行結果
    ///
    /// Strategy は match で全ケースを処理する必要がある。
    fn result(&self) -> &ExplorationResult;

    /// 重複チェック用キーを生成
    ///
    /// デフォルト実装: `{action_name}:{target}`
    fn dedup_key(&self) -> String {
        let target = self.target().unwrap_or("_no_target_");
        format!("{}:{}", self.action_name(), target)
    }
}

// ============================================================================
// MapUpdate - Map に対する操作
// ============================================================================

/// Map に対する操作(MutationLogic/Operator が生成)
///
/// GraphMap の具体的な操作を抽象化する。
/// Operator はこの enum を生成し、Orchestrator が Map に適用する。
#[derive(Debug, Clone)]
pub enum MapUpdate<N, E, S: MapState> {
    /// 子ノードを追加
    ///
    /// 重複チェック付き。既に同じキーのノードが存在すれば無視。
    AddChild {
        /// 親ノード
        parent: MapNodeId,
        /// エッジデータ
        edge_data: E,
        /// ノードデータ
        node_data: N,
        /// ノード状態
        node_state: S,
        /// 重複チェック用キー
        dedup_key: String,
    },

    /// ノードを Close
    ///
    /// 「成功」「失敗」の意味付けは利用者の責務。
    /// Map は単に Closed 状態にするだけ。
    Close(MapNodeId),

    /// 何もしない
    Noop,
}

// ============================================================================
// MapUpdateResult - 適用結果
// ============================================================================

/// MapUpdate 適用結果
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MapUpdateResult {
    /// 新ノード作成
    NodeCreated(MapNodeId),
    /// ノード Close
    NodeClosed(MapNodeId),
    /// 無視(重複、Noop 等)
    Ignored,
    /// エラー
    Error(String),
}

// ============================================================================
// GraphMap extension - MapUpdate を適用
// ============================================================================

impl<N, E, S> GraphMap<N, E, S>
where
    N: Debug + Clone,
    E: Debug + Clone,
    S: MapState,
{
    /// MapUpdate を適用
    pub fn apply_update<K, F>(&mut self, update: MapUpdate<N, E, S>, key_fn: F) -> MapUpdateResult
    where
        K: Hash,
        F: Fn(&str) -> K,
    {
        match update {
            MapUpdate::AddChild {
                parent,
                edge_data,
                node_data,
                node_state,
                dedup_key,
            } => {
                let key = key_fn(&dedup_key);
                match self.add_child_if_absent(parent, edge_data, node_data, node_state, |_| key) {
                    Ok(AddResult::Added(id)) => MapUpdateResult::NodeCreated(id),
                    Ok(AddResult::AlreadyExists(_)) => MapUpdateResult::Ignored,
                    Err(e) => MapUpdateResult::Error(e.to_string()),
                }
            }
            MapUpdate::Close(node_id) => {
                self.close_with_cascade_up(node_id);
                MapUpdateResult::NodeClosed(node_id)
            }
            MapUpdate::Noop => MapUpdateResult::Ignored,
        }
    }

    /// 複数の MapUpdate を適用
    pub fn apply_updates<K, F>(
        &mut self,
        updates: Vec<MapUpdate<N, E, S>>,
        key_fn: F,
    ) -> Vec<MapUpdateResult>
    where
        K: Hash + Clone,
        F: Fn(&str) -> K,
    {
        updates
            .into_iter()
            .map(|u| self.apply_update(u, &key_fn))
            .collect()
    }
}

// ============================================================================
// Tests
// ============================================================================

#[cfg(test)]
mod tests {
    use super::*;
    use crate::exploration::map::{ExplorationMap, MapNodeState};

    // テスト用の MutationInput 実装
    struct TestInput {
        node_id: MapNodeId,
        action_name: String,
        target: Option<String>,
        result: ExplorationResult,
    }

    impl MutationInput for TestInput {
        fn node_id(&self) -> MapNodeId {
            self.node_id
        }

        fn action_name(&self) -> &str {
            &self.action_name
        }

        fn target(&self) -> Option<&str> {
            self.target.as_deref()
        }

        fn result(&self) -> &ExplorationResult {
            &self.result
        }
    }

    fn make_test_input(
        node_id: u64,
        action_name: &str,
        target: Option<&str>,
        success: bool,
    ) -> TestInput {
        let result = if success {
            ExplorationResult::Success
        } else {
            ExplorationResult::Fail(None)
        };
        TestInput {
            node_id: MapNodeId::new(node_id),
            action_name: action_name.to_string(),
            target: target.map(|s| s.to_string()),
            result,
        }
    }

    #[test]
    fn test_mutation_input_dedup_key() {
        let input = make_test_input(0, "grep", Some("src/auth.rs"), true);
        assert_eq!(input.dedup_key(), "grep:src/auth.rs");
    }

    #[test]
    fn test_mutation_input_dedup_key_no_target() {
        let input = make_test_input(0, "list", None, true);
        assert_eq!(input.dedup_key(), "list:_no_target_");
    }

    #[test]
    fn test_map_update_add_child() {
        let mut map: GraphMap<String, String, MapNodeState> = GraphMap::new();
        let root = map.create_root("root".to_string(), MapNodeState::Open);

        let update = MapUpdate::AddChild {
            parent: root,
            edge_data: "edge-1".to_string(),
            node_data: "child-1".to_string(),
            node_state: MapNodeState::Open,
            dedup_key: "child-1".to_string(),
        };

        let result = map.apply_update(update, |k| k.to_string());
        assert!(matches!(result, MapUpdateResult::NodeCreated(_)));
        assert_eq!(map.node_count(), 2);
    }

    #[test]
    fn test_map_update_add_child_dedup() {
        let mut map: GraphMap<String, String, MapNodeState> = GraphMap::new();
        let root = map.create_root("root".to_string(), MapNodeState::Open);

        // 1回目: 追加される
        let update1 = MapUpdate::AddChild {
            parent: root,
            edge_data: "edge-1".to_string(),
            node_data: "child-1".to_string(),
            node_state: MapNodeState::Open,
            dedup_key: "same-key".to_string(),
        };
        let result1 = map.apply_update(update1, |k| k.to_string());
        assert!(matches!(result1, MapUpdateResult::NodeCreated(_)));

        // 2回目: 重複で無視
        let update2 = MapUpdate::AddChild {
            parent: root,
            edge_data: "edge-2".to_string(),
            node_data: "child-2".to_string(),
            node_state: MapNodeState::Open,
            dedup_key: "same-key".to_string(),
        };
        let result2 = map.apply_update(update2, |k| k.to_string());
        assert!(matches!(result2, MapUpdateResult::Ignored));

        assert_eq!(map.node_count(), 2); // root + child-1 のみ
    }

    #[test]
    fn test_map_update_close() {
        let mut map: GraphMap<String, String, MapNodeState> = GraphMap::new();
        let root = map.create_root("root".to_string(), MapNodeState::Open);

        let update = MapUpdate::<String, String, MapNodeState>::Close(root);
        let result = map.apply_update(update, |k| k.to_string());

        assert!(matches!(result, MapUpdateResult::NodeClosed(_)));
        assert!(map.frontiers().is_empty());
    }

    #[test]
    fn test_map_update_noop() {
        let mut map: GraphMap<String, String, MapNodeState> = GraphMap::new();
        let _root = map.create_root("root".to_string(), MapNodeState::Open);

        let update = MapUpdate::<String, String, MapNodeState>::Noop;
        let result = map.apply_update(update, |k| k.to_string());

        assert!(matches!(result, MapUpdateResult::Ignored));
    }

    #[test]
    fn test_apply_updates_batch() {
        let mut map: GraphMap<String, String, MapNodeState> = GraphMap::new();
        let root = map.create_root("root".to_string(), MapNodeState::Open);

        let updates = vec![
            MapUpdate::AddChild {
                parent: root,
                edge_data: "e1".to_string(),
                node_data: "c1".to_string(),
                node_state: MapNodeState::Open,
                dedup_key: "c1".to_string(),
            },
            MapUpdate::AddChild {
                parent: root,
                edge_data: "e2".to_string(),
                node_data: "c2".to_string(),
                node_state: MapNodeState::Open,
                dedup_key: "c2".to_string(),
            },
            MapUpdate::Noop,
        ];

        let results = map.apply_updates(updates, |k| k.to_string());

        assert_eq!(results.len(), 3);
        assert!(matches!(results[0], MapUpdateResult::NodeCreated(_)));
        assert!(matches!(results[1], MapUpdateResult::NodeCreated(_)));
        assert!(matches!(results[2], MapUpdateResult::Ignored));
        assert_eq!(map.node_count(), 3);
    }
}