Skip to main content

swarm_engine_core/exploration/
dependency_planner.rs

1//! Action Dependency Planner - アクション依存グラフの動的構築
2//!
3//! タスク開始時に LLM を使ってアクション間の依存関係を一度だけ計画する。
4//! 毎 tick で LLM を呼ぶ代わりに、グラフに従って探索を進める。
5//!
6//! # コンセプト
7//!
8//! ```text
9//! Task + AvailableActions → [LLM] → DependencyGraph
10//!
11//! DependencyGraph:
12//!   - edges: [(Grep, Read, 0.95), (List, Grep, 0.60)]
13//!   - start_nodes: [Grep]
14//!   - terminal_nodes: [Read]
15//!
16//! 実行時:
17//!   Worker は Graph に従うだけ → 毎 tick の LLM 不要
18//! ```
19//!
20//! # 利点
21//!
22//! 1. **軽量 Meta-Planning**: タスク開始時に1回だけ LLM を呼ぶ
23//! 2. **構造的な制約**: 逆流(Read → Grep)が起きない
24//! 3. **高精度**: パターンが決まっているから LLM の判断が簡単
25//!
26//! # 使用例
27//!
28//! ```ignore
29//! let planner = ActionDependencyPlanner::new();
30//!
31//! // タスク開始時に依存グラフを生成
32//! let graph = planner.plan(
33//!     "src/auth.rs の verify_token 関数を見つける",
34//!     &["Grep", "List", "Read"],
35//!     &llm_client,
36//! ).await?;
37//!
38//! // 探索時はグラフに従う
39//! let next_actions = graph.valid_next_actions("Grep"); // → ["Read"]
40//! ```
41
42use std::collections::{HashMap, HashSet};
43
44use serde::{Deserialize, Serialize};
45
46// ============================================================================
47// Core Types
48// ============================================================================
49
50/// アクション間の依存エッジ
51///
52/// `from` アクションの成功後に `to` アクションを実行可能。
53/// `confidence` は LLM の確信度(0.0〜1.0)。
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct DependencyEdge {
56    /// 元アクション名
57    pub from: String,
58    /// 先アクション名
59    pub to: String,
60    /// 確信度(0.0〜1.0)
61    pub confidence: f64,
62}
63
64impl DependencyEdge {
65    pub fn new(from: impl Into<String>, to: impl Into<String>, confidence: f64) -> Self {
66        Self {
67            from: from.into(),
68            to: to.into(),
69            confidence: confidence.clamp(0.0, 1.0),
70        }
71    }
72}
73
74/// アクション依存グラフ
75///
76/// タスクに対して LLM が生成したアクション間の依存関係。
77/// 探索時はこのグラフに従ってアクションを選択する。
78#[derive(Debug, Clone, Default, Serialize, Deserialize)]
79pub struct DependencyGraph {
80    /// 依存エッジ(from → to の関係)
81    edges: Vec<DependencyEdge>,
82    /// 開始可能なアクション(依存なしで実行可能)
83    start_nodes: HashSet<String>,
84    /// 終端アクション(これが成功したらタスク完了の可能性)
85    terminal_nodes: HashSet<String>,
86    /// タスク説明(デバッグ用)
87    task: String,
88    /// 利用可能なアクション一覧(検証用)
89    available_actions: Vec<String>,
90    /// アクションごとのパラメータバリアント(action → (key, values))
91    ///
92    /// 例: "Move" → ("target", ["north", "south", "east", "west"])
93    #[serde(default)]
94    param_variants: HashMap<String, (String, Vec<String>)>,
95    /// Discover(NodeExpand)アクションの順序(キャッシュ用)
96    #[serde(default)]
97    discover_order: Vec<String>,
98    /// NotDiscover(NodeStateChange)アクションの順序(キャッシュ用)
99    #[serde(default)]
100    not_discover_order: Vec<String>,
101    /// 学習用記録(LLM 推論を学習データとして保存)
102    #[serde(skip)]
103    learn_record: Option<crate::learn::DependencyGraphRecord>,
104}
105
106impl DependencyGraph {
107    /// 空のグラフを作成
108    pub fn new() -> Self {
109        Self::default()
110    }
111
112    /// Builder パターンでグラフを構築
113    pub fn builder() -> DependencyGraphBuilder {
114        DependencyGraphBuilder::new()
115    }
116
117    // ------------------------------------------------------------------------
118    // Query API
119    // ------------------------------------------------------------------------
120
121    /// 指定アクションの後に実行可能なアクションを取得
122    ///
123    /// 確信度順にソートして返す。
124    pub fn valid_next_actions(&self, current_action: &str) -> Vec<String> {
125        let mut edges: Vec<_> = self
126            .edges
127            .iter()
128            .filter(|e| e.from == current_action)
129            .collect();
130
131        // 確信度が高い順にソート(NaN対策で unwrap_or を使用)
132        edges.sort_by(|a, b| {
133            b.confidence
134                .partial_cmp(&a.confidence)
135                .unwrap_or(std::cmp::Ordering::Equal)
136        });
137
138        edges.iter().map(|e| e.to.clone()).collect()
139    }
140
141    /// 開始可能なアクションを取得
142    ///
143    /// アルファベット順にソートして一貫した順序を保証。
144    pub fn start_actions(&self) -> Vec<String> {
145        let mut actions: Vec<_> = self.start_nodes.iter().cloned().collect();
146        actions.sort();
147        actions
148    }
149
150    /// 終端アクションを取得
151    ///
152    /// アルファベット順にソートして一貫した順序を保証。
153    pub fn terminal_actions(&self) -> Vec<String> {
154        let mut actions: Vec<_> = self.terminal_nodes.iter().cloned().collect();
155        actions.sort();
156        actions
157    }
158
159    /// 指定アクションが終端か
160    pub fn is_terminal(&self, action: &str) -> bool {
161        self.terminal_nodes.contains(action)
162    }
163
164    /// 指定アクションが開始ノードか
165    pub fn is_start(&self, action: &str) -> bool {
166        self.start_nodes.contains(action)
167    }
168
169    /// 指定アクションから指定アクションへの遷移が有効か
170    pub fn can_transition(&self, from: &str, to: &str) -> bool {
171        self.edges.iter().any(|e| e.from == from && e.to == to)
172    }
173
174    /// 遷移の確信度を取得
175    pub fn transition_confidence(&self, from: &str, to: &str) -> Option<f64> {
176        self.edges
177            .iter()
178            .find(|e| e.from == from && e.to == to)
179            .map(|e| e.confidence)
180    }
181
182    /// 全エッジを取得
183    pub fn edges(&self) -> &[DependencyEdge] {
184        &self.edges
185    }
186
187    /// タスク説明を取得
188    pub fn task(&self) -> &str {
189        &self.task
190    }
191
192    /// 利用可能なアクション一覧を取得
193    pub fn available_actions(&self) -> &[String] {
194        &self.available_actions
195    }
196
197    /// 指定アクションのパラメータバリアントを取得
198    pub fn param_variants(&self, action: &str) -> Option<(&str, &[String])> {
199        self.param_variants
200            .get(action)
201            .map(|(key, values)| (key.as_str(), values.as_slice()))
202    }
203
204    /// 全パラメータバリアントを取得
205    pub fn all_param_variants(&self) -> &HashMap<String, (String, Vec<String>)> {
206        &self.param_variants
207    }
208
209    // ------------------------------------------------------------------------
210    // Action Order (for caching)
211    // ------------------------------------------------------------------------
212
213    /// Discover アクションの順序を取得
214    pub fn discover_order(&self) -> &[String] {
215        &self.discover_order
216    }
217
218    /// NotDiscover アクションの順序を取得
219    pub fn not_discover_order(&self) -> &[String] {
220        &self.not_discover_order
221    }
222
223    /// アクション順序を設定
224    pub fn set_action_order(&mut self, discover: Vec<String>, not_discover: Vec<String>) {
225        self.discover_order = discover;
226        self.not_discover_order = not_discover;
227    }
228
229    /// アクション順序が設定されているか
230    pub fn has_action_order(&self) -> bool {
231        !self.discover_order.is_empty() || !self.not_discover_order.is_empty()
232    }
233
234    // ------------------------------------------------------------------------
235    // Learning Record
236    // ------------------------------------------------------------------------
237
238    /// 学習用記録を設定
239    pub fn set_learn_record(&mut self, record: crate::learn::DependencyGraphRecord) {
240        self.learn_record = Some(record);
241    }
242
243    /// 学習用記録を取得
244    pub fn learn_record(&self) -> Option<&crate::learn::DependencyGraphRecord> {
245        self.learn_record.as_ref()
246    }
247
248    /// 学習用記録を取り出し(所有権移動)
249    pub fn take_learn_record(&mut self) -> Option<crate::learn::DependencyGraphRecord> {
250        self.learn_record.take()
251    }
252
253    // ------------------------------------------------------------------------
254    // Validation
255    // ------------------------------------------------------------------------
256
257    /// グラフが有効か検証
258    ///
259    /// - 少なくとも1つの開始ノードがある
260    /// - 少なくとも1つの終端ノードがある
261    /// - 全エッジのアクションが available_actions に含まれる
262    /// - 全開始ノードが available_actions に含まれる
263    /// - 全終端ノードが available_actions に含まれる
264    pub fn validate(&self) -> Result<(), DependencyGraphError> {
265        if self.start_nodes.is_empty() {
266            return Err(DependencyGraphError::NoStartNodes);
267        }
268
269        if self.terminal_nodes.is_empty() {
270            return Err(DependencyGraphError::NoTerminalNodes);
271        }
272
273        // 開始ノードが available_actions に含まれるか
274        for node in &self.start_nodes {
275            if !self.available_actions.contains(node) {
276                return Err(DependencyGraphError::UnknownAction(node.clone()));
277            }
278        }
279
280        // 終端ノードが available_actions に含まれるか
281        for node in &self.terminal_nodes {
282            if !self.available_actions.contains(node) {
283                return Err(DependencyGraphError::UnknownAction(node.clone()));
284            }
285        }
286
287        // エッジのアクションが available_actions に含まれるか
288        for edge in &self.edges {
289            if !self.available_actions.contains(&edge.from) {
290                return Err(DependencyGraphError::UnknownAction(edge.from.clone()));
291            }
292            if !self.available_actions.contains(&edge.to) {
293                return Err(DependencyGraphError::UnknownAction(edge.to.clone()));
294            }
295        }
296
297        Ok(())
298    }
299
300    // ------------------------------------------------------------------------
301    // Visualization
302    // ------------------------------------------------------------------------
303
304    /// グラフを Mermaid 形式で出力
305    pub fn to_mermaid(&self) -> String {
306        let mut lines = vec!["graph LR".to_string()];
307
308        for edge in &self.edges {
309            let label = format!("{:.0}%", edge.confidence * 100.0);
310            lines.push(format!("    {} -->|{}| {}", edge.from, label, edge.to));
311        }
312
313        // 開始ノードをスタイル
314        for start in &self.start_nodes {
315            lines.push(format!("    style {} fill:#9f9", start));
316        }
317
318        // 終端ノードをスタイル
319        for terminal in &self.terminal_nodes {
320            lines.push(format!("    style {} fill:#f99", terminal));
321        }
322
323        lines.join("\n")
324    }
325}
326
327/// グラフ構築エラー
328#[derive(Debug, Clone, thiserror::Error)]
329pub enum DependencyGraphError {
330    #[error("No start nodes defined")]
331    NoStartNodes,
332
333    #[error("No terminal nodes defined")]
334    NoTerminalNodes,
335
336    #[error("Unknown action: {0}")]
337    UnknownAction(String),
338
339    #[error("Parse error: {0}")]
340    ParseError(String),
341
342    #[error("LLM error: {0}")]
343    LlmError(String),
344}
345
346// ============================================================================
347// Provider Trait
348// ============================================================================
349
350/// DependencyGraph を提供するトレイト
351///
352/// このトレイトを実装することで、様々な方法で DependencyGraph を生成できる:
353/// - LLM による動的生成(BatchInvoker が実装)
354/// - 静的な事前定義グラフ
355/// - テスト用のモック
356///
357/// # Example
358///
359/// ```ignore
360/// use swarm_engine_core::exploration::{DependencyGraph, DependencyGraphProvider};
361///
362/// struct StaticProvider {
363///     graph: DependencyGraph,
364/// }
365///
366/// impl DependencyGraphProvider for StaticProvider {
367///     fn provide_graph(&self, _task: &str, _actions: &[String]) -> Option<DependencyGraph> {
368///         Some(self.graph.clone())
369///     }
370/// }
371/// ```
372pub trait DependencyGraphProvider: Send + Sync {
373    /// タスクとアクション一覧から DependencyGraph を生成
374    ///
375    /// # Arguments
376    /// - `task`: タスクの説明(goal)
377    /// - `available_actions`: 利用可能なアクション名の一覧
378    ///
379    /// # Returns
380    /// - `Some(DependencyGraph)`: グラフが生成された場合
381    /// - `None`: 生成をスキップする場合(LLM エラー等)
382    fn provide_graph(&self, task: &str, available_actions: &[String]) -> Option<DependencyGraph>;
383}
384
385// ============================================================================
386// Builder
387// ============================================================================
388
389/// DependencyGraph のビルダー
390#[derive(Debug, Clone, Default)]
391pub struct DependencyGraphBuilder {
392    edges: Vec<DependencyEdge>,
393    start_nodes: HashSet<String>,
394    terminal_nodes: HashSet<String>,
395    task: String,
396    available_actions: Vec<String>,
397    param_variants: HashMap<String, (String, Vec<String>)>,
398}
399
400impl DependencyGraphBuilder {
401    pub fn new() -> Self {
402        Self::default()
403    }
404
405    /// タスク説明を設定
406    pub fn task(mut self, task: impl Into<String>) -> Self {
407        self.task = task.into();
408        self
409    }
410
411    /// 利用可能なアクションを設定
412    pub fn available_actions<I, S>(mut self, actions: I) -> Self
413    where
414        I: IntoIterator<Item = S>,
415        S: Into<String>,
416    {
417        self.available_actions = actions.into_iter().map(|s| s.into()).collect();
418        self
419    }
420
421    /// エッジを追加
422    pub fn edge(mut self, from: impl Into<String>, to: impl Into<String>, confidence: f64) -> Self {
423        self.edges.push(DependencyEdge::new(from, to, confidence));
424        self
425    }
426
427    /// 開始ノードを追加
428    pub fn start_node(mut self, action: impl Into<String>) -> Self {
429        self.start_nodes.insert(action.into());
430        self
431    }
432
433    /// 複数の開始ノードを追加
434    pub fn start_nodes<I, S>(mut self, actions: I) -> Self
435    where
436        I: IntoIterator<Item = S>,
437        S: Into<String>,
438    {
439        self.start_nodes
440            .extend(actions.into_iter().map(|s| s.into()));
441        self
442    }
443
444    /// 終端ノードを追加
445    pub fn terminal_node(mut self, action: impl Into<String>) -> Self {
446        self.terminal_nodes.insert(action.into());
447        self
448    }
449
450    /// 複数の終端ノードを追加
451    pub fn terminal_nodes<I, S>(mut self, actions: I) -> Self
452    where
453        I: IntoIterator<Item = S>,
454        S: Into<String>,
455    {
456        self.terminal_nodes
457            .extend(actions.into_iter().map(|s| s.into()));
458        self
459    }
460
461    /// パラメータバリアントを追加
462    ///
463    /// 指定アクションの後続ノード展開時に、各バリアントごとにノードを生成する。
464    pub fn param_variants<I, S>(
465        mut self,
466        action: impl Into<String>,
467        key: impl Into<String>,
468        values: I,
469    ) -> Self
470    where
471        I: IntoIterator<Item = S>,
472        S: Into<String>,
473    {
474        self.param_variants.insert(
475            action.into(),
476            (key.into(), values.into_iter().map(|s| s.into()).collect()),
477        );
478        self
479    }
480
481    /// グラフを構築
482    pub fn build(self) -> DependencyGraph {
483        DependencyGraph {
484            edges: self.edges,
485            start_nodes: self.start_nodes,
486            terminal_nodes: self.terminal_nodes,
487            task: self.task,
488            available_actions: self.available_actions,
489            param_variants: self.param_variants,
490            discover_order: Vec::new(),
491            not_discover_order: Vec::new(),
492            learn_record: None,
493        }
494    }
495
496    /// グラフを構築してバリデーション
497    pub fn build_validated(self) -> Result<DependencyGraph, DependencyGraphError> {
498        let graph = self.build();
499        graph.validate()?;
500        Ok(graph)
501    }
502}
503
504// ============================================================================
505// LLM Response Parsing
506// ============================================================================
507
508/// LLM からの依存グラフ生成レスポンス
509///
510/// LLM が JSON で返すことを想定。
511#[derive(Debug, Clone, Serialize, Deserialize)]
512pub struct LlmDependencyResponse {
513    /// 依存エッジ
514    pub edges: Vec<LlmEdge>,
515    /// 開始アクション
516    pub start: Vec<String>,
517    /// 終端アクション
518    pub terminal: Vec<String>,
519    /// 推論理由(オプション)
520    #[serde(default)]
521    pub reasoning: Option<String>,
522}
523
524/// LLM レスポンス内のエッジ
525#[derive(Debug, Clone, Serialize, Deserialize)]
526pub struct LlmEdge {
527    pub from: String,
528    pub to: String,
529    pub confidence: f64,
530}
531
532impl LlmDependencyResponse {
533    /// DependencyGraph に変換
534    pub fn into_graph(
535        self,
536        task: impl Into<String>,
537        available_actions: Vec<String>,
538    ) -> DependencyGraph {
539        let mut builder = DependencyGraphBuilder::new()
540            .task(task)
541            .available_actions(available_actions)
542            .start_nodes(self.start)
543            .terminal_nodes(self.terminal);
544
545        for edge in self.edges {
546            builder = builder.edge(edge.from, edge.to, edge.confidence);
547        }
548
549        builder.build()
550    }
551
552    /// テキストからパース(矢印形式 or JSON)
553    ///
554    /// 優先順位:
555    /// 1. 矢印形式(A -> B -> C)
556    /// 2. JSON形式
557    pub fn parse(text: &str) -> Result<Self, DependencyGraphError> {
558        // 1. 矢印形式をパース
559        if let Some(response) = Self::parse_arrow_format(text) {
560            return Ok(response);
561        }
562
563        // 2. JSON形式を試行
564        if let Ok(parsed) = serde_json::from_str(text) {
565            return Ok(parsed);
566        }
567
568        // 3. テキストからJSONを抽出して再試行
569        if let Some(json) = Self::extract_json(text) {
570            serde_json::from_str(&json).map_err(|e| DependencyGraphError::ParseError(e.to_string()))
571        } else {
572            Err(DependencyGraphError::ParseError(format!(
573                "No valid format found in response: {}",
574                text.chars().take(200).collect::<String>()
575            )))
576        }
577    }
578
579    /// アクション順序をパース(矢印形式 or 番号リスト形式)
580    ///
581    /// 対応形式:
582    /// - 矢印: "Grep -> Read -> List"
583    /// - 番号: "1. Grep 2. Read 3. List"
584    /// - コンマ: "Grep, Read, List"
585    fn parse_arrow_format(text: &str) -> Option<Self> {
586        // 矢印形式を試行
587        if let Some(result) = Self::parse_arrow_only(text) {
588            return Some(result);
589        }
590
591        // 番号リスト形式を試行(例: "1. Read 2. List 3. Grep")
592        if let Some(result) = Self::parse_numbered_list(text) {
593            return Some(result);
594        }
595
596        None
597    }
598
599    /// 矢印形式のみをパース
600    fn parse_arrow_only(text: &str) -> Option<Self> {
601        let normalized = text.replace('→', "->");
602
603        // 矢印を含む行を探す
604        let arrow_line = normalized.lines().find(|line| line.contains("->"))?;
605
606        let parts: Vec<&str> = arrow_line.split("->").collect();
607        if parts.len() < 2 {
608            return None;
609        }
610
611        // 最後の単語のみを抽出("So the order is Read" -> "Read")
612        let actions_in_order: Vec<String> = parts
613            .iter()
614            .filter_map(|part| {
615                let trimmed = part.trim();
616                // 最後の単語を取得
617                let last_word = trimmed.split_whitespace().last()?;
618                // 英字のみを抽出
619                let action: String = last_word.chars().filter(|c| c.is_alphabetic()).collect();
620                if action.is_empty() {
621                    None
622                } else {
623                    Some(action)
624                }
625            })
626            .collect();
627
628        if actions_in_order.len() < 2 {
629            return None;
630        }
631
632        Self::build_response(actions_in_order)
633    }
634
635    /// 番号リスト形式をパース(例: "1. Read 2. List 3. Grep")
636    fn parse_numbered_list(text: &str) -> Option<Self> {
637        let mut actions_in_order: Vec<String> = Vec::new();
638
639        // "1." "2." "3." などを探す
640        for i in 1..=10 {
641            let pattern = format!("{}.", i);
642            if let Some(pos) = text.find(&pattern) {
643                // 番号の後の単語を取得
644                let after = &text[pos + pattern.len()..];
645                if let Some(word) = after.split_whitespace().next() {
646                    // 英字のみを抽出
647                    let action: String = word.chars().filter(|c| c.is_alphabetic()).collect();
648                    if !action.is_empty() && !actions_in_order.contains(&action) {
649                        actions_in_order.push(action);
650                    }
651                }
652            }
653        }
654
655        if actions_in_order.len() < 2 {
656            return None;
657        }
658
659        Self::build_response(actions_in_order)
660    }
661
662    /// LlmDependencyResponse を構築
663    fn build_response(actions_in_order: Vec<String>) -> Option<Self> {
664        let mut edges = Vec::new();
665        for window in actions_in_order.windows(2) {
666            edges.push(LlmEdge {
667                from: window[0].clone(),
668                to: window[1].clone(),
669                confidence: 0.9,
670            });
671        }
672
673        Some(Self {
674            edges,
675            start: vec![actions_in_order.first()?.clone()],
676            terminal: vec![actions_in_order.last()?.clone()],
677            reasoning: Some("Parsed from text format".to_string()),
678        })
679    }
680
681    /// テキストからJSONオブジェクトを抽出
682    fn extract_json(text: &str) -> Option<String> {
683        // { ... } を探す(バランスを取って)
684        let start = text.find('{')?;
685        let chars: Vec<char> = text[start..].chars().collect();
686        let mut depth = 0;
687        let mut in_string = false;
688        let mut escape_next = false;
689
690        for (i, &ch) in chars.iter().enumerate() {
691            if escape_next {
692                escape_next = false;
693                continue;
694            }
695
696            match ch {
697                '\\' if in_string => escape_next = true,
698                '"' => in_string = !in_string,
699                '{' if !in_string => depth += 1,
700                '}' if !in_string => {
701                    depth -= 1;
702                    if depth == 0 {
703                        return Some(chars[..=i].iter().collect());
704                    }
705                }
706                _ => {}
707            }
708        }
709
710        None
711    }
712}
713
714// ============================================================================
715// Planner Trait
716// ============================================================================
717
718/// 依存グラフ生成プランナー trait
719///
720/// LLM を使った実装と、静的な実装の両方をサポート。
721pub trait DependencyPlanner: Send + Sync {
722    /// タスクとアクション一覧から依存グラフを生成
723    ///
724    /// 非同期処理は呼び出し側で管理(LLM 呼び出しは別レイヤー)。
725    fn plan(
726        &self,
727        task: &str,
728        available_actions: &[String],
729    ) -> Result<DependencyGraph, DependencyGraphError>;
730
731    /// プランナー名
732    fn name(&self) -> &str;
733}
734
735/// 静的な依存グラフプランナー(LLM 不使用)
736///
737/// 事前定義されたパターンから依存グラフを生成。
738/// テストや LLM なしでの動作確認に使用。
739#[derive(Debug, Clone, Default)]
740pub struct StaticDependencyPlanner {
741    /// パターン名 → グラフ のマッピング
742    patterns: HashMap<String, DependencyGraph>,
743    /// デフォルトパターン名
744    default_pattern: Option<String>,
745}
746
747impl StaticDependencyPlanner {
748    pub fn new() -> Self {
749        Self::default()
750    }
751
752    /// パターンを追加
753    pub fn with_pattern(mut self, name: impl Into<String>, graph: DependencyGraph) -> Self {
754        let name = name.into();
755        if self.default_pattern.is_none() {
756            self.default_pattern = Some(name.clone());
757        }
758        self.patterns.insert(name, graph);
759        self
760    }
761
762    /// デフォルトパターンを設定
763    pub fn with_default_pattern(mut self, name: impl Into<String>) -> Self {
764        self.default_pattern = Some(name.into());
765        self
766    }
767
768    /// ファイル探索パターンを追加(組み込み)
769    ///
770    /// Grep|List → Read のパターン。
771    pub fn with_file_exploration_pattern(self) -> Self {
772        let graph = DependencyGraph::builder()
773            .task("File exploration")
774            .available_actions(["Grep", "List", "Read"])
775            .edge("Grep", "Read", 0.95)
776            .edge("List", "Grep", 0.60)
777            .edge("List", "Read", 0.40)
778            .start_nodes(["Grep", "List"])
779            .terminal_node("Read")
780            .build();
781
782        self.with_pattern("file_exploration", graph)
783    }
784
785    /// コード検索パターンを追加(組み込み)
786    ///
787    /// Grep → Read の強いパターン。
788    pub fn with_code_search_pattern(self) -> Self {
789        let graph = DependencyGraph::builder()
790            .task("Code search")
791            .available_actions(["Grep", "Read"])
792            .edge("Grep", "Read", 0.95)
793            .start_node("Grep")
794            .terminal_node("Read")
795            .build();
796
797        self.with_pattern("code_search", graph)
798    }
799}
800
801impl DependencyPlanner for StaticDependencyPlanner {
802    fn plan(
803        &self,
804        task: &str,
805        available_actions: &[String],
806    ) -> Result<DependencyGraph, DependencyGraphError> {
807        // デフォルトパターンを使用
808        if let Some(pattern_name) = &self.default_pattern {
809            if let Some(graph) = self.patterns.get(pattern_name) {
810                let mut graph = graph.clone();
811                graph.task = task.to_string();
812                graph.available_actions = available_actions.to_vec();
813                return Ok(graph);
814            }
815        }
816
817        // パターンがない場合は単純な線形グラフを生成
818        // 全アクションを順番に実行する想定
819        if available_actions.is_empty() {
820            return Err(DependencyGraphError::NoStartNodes);
821        }
822
823        let mut builder = DependencyGraphBuilder::new()
824            .task(task)
825            .available_actions(available_actions.to_vec())
826            .start_node(&available_actions[0]);
827
828        if available_actions.len() > 1 {
829            for window in available_actions.windows(2) {
830                builder = builder.edge(&window[0], &window[1], 0.80);
831            }
832            builder = builder.terminal_node(&available_actions[available_actions.len() - 1]);
833        } else {
834            builder = builder.terminal_node(&available_actions[0]);
835        }
836
837        Ok(builder.build())
838    }
839
840    fn name(&self) -> &str {
841        "StaticDependencyPlanner"
842    }
843}
844
845// ============================================================================
846// LLM Prompt Generation
847// ============================================================================
848
849use crate::actions::ActionDef;
850
851/// LLM 向けプロンプト生成
852///
853/// 分割クエリ方式:first/lastを個別に聞いて安定した結果を得る。
854/// ActionDef の name と description を使ってヒントを生成。
855pub struct DependencyPromptGenerator;
856
857impl DependencyPromptGenerator {
858    /// 依存グラフ生成用のプロンプトを生成
859    ///
860    /// 従来の全順序プロンプト(フォールバック用)
861    pub fn generate_prompt(task: &str, actions: &[ActionDef]) -> String {
862        let actions_list = actions
863            .iter()
864            .map(|a| a.name.as_str())
865            .collect::<Vec<_>>()
866            .join(", ");
867
868        format!(
869            r#"{task}
870Steps: {actions_list}
871The very first step is:"#
872        )
873    }
874
875    /// First step を聞くプロンプト
876    ///
877    /// ActionDef の description から動詞を抽出してヒントを生成。
878    /// 例: "Search for patterns" → "Which step SEARCHES?"
879    pub fn generate_first_prompt(_task: &str, actions: &[ActionDef]) -> String {
880        // アルファベット順にソート(順序バイアスを軽減)
881        let mut sorted_actions: Vec<&ActionDef> = actions.iter().collect();
882        sorted_actions.sort_by(|a, b| a.name.cmp(&b.name));
883
884        let actions_list = sorted_actions
885            .iter()
886            .map(|a| a.name.as_str())
887            .collect::<Vec<_>>()
888            .join(", ");
889
890        // description から動詞を抽出(最初の単語を大文字化)
891        let descriptions: Vec<String> = sorted_actions
892            .iter()
893            .map(|a| format!("- {}: {}", a.name, a.description))
894            .collect();
895        let descriptions_block = descriptions.join("\n");
896
897        // 最初のアクションの description から動詞を取得してヒントに
898        let first_verb = sorted_actions
899            .first()
900            .map(|a| Self::extract_verb(&a.description))
901            .unwrap_or_else(|| "CHECK".to_string());
902
903        format!(
904            r#"Steps: {actions_list}
905{descriptions_block}
906Which step {first_verb}S first?
907Answer:"#
908        )
909    }
910
911    /// Last step を聞くプロンプト
912    ///
913    /// ActionDef の name と description をそのまま渡す。
914    /// description に "requires X first" 等の情報があれば LLM が判断できる。
915    pub fn generate_last_prompt(_task: &str, actions: &[ActionDef]) -> String {
916        // アルファベット順にソート(順序バイアスを軽減)
917        let mut sorted_actions: Vec<&ActionDef> = actions.iter().collect();
918        sorted_actions.sort_by(|a, b| a.name.cmp(&b.name));
919
920        let actions_list = sorted_actions
921            .iter()
922            .map(|a| a.name.as_str())
923            .collect::<Vec<_>>()
924            .join(", ");
925
926        let descriptions: Vec<String> = sorted_actions
927            .iter()
928            .map(|a| format!("- {}: {}", a.name, a.description))
929            .collect();
930        let descriptions_block = descriptions.join("\n");
931
932        format!(
933            r#"Steps: {actions_list}
934{descriptions_block}
935Which step should be done last?
936Answer:"#
937        )
938    }
939
940    /// ペア比較プロンプト(どちらが先か)
941    pub fn generate_pair_prompt(task: &str, action_a: &str, action_b: &str) -> String {
942        format!(
943            r#"For {task}, which comes first: {action_a} or {action_b}?
944Answer (one word):"#
945        )
946    }
947
948    /// description から動詞を抽出(大文字化)
949    ///
950    /// 例: "Search for patterns" → "SEARCH"
951    /// 例: "Reads file contents" → "READ"
952    fn extract_verb(description: &str) -> String {
953        description
954            .split_whitespace()
955            .next()
956            .map(|w| {
957                // 末尾の 's' を除去("Reads" → "Read")
958                let word = w.trim_end_matches('s').trim_end_matches('S');
959                word.to_uppercase()
960            })
961            .unwrap_or_else(|| "CHECK".to_string())
962    }
963}
964
965// ============================================================================
966// Graph Navigation Helper
967// ============================================================================
968
969/// 依存グラフに基づくアクション選択ヘルパー
970///
971/// 現在の探索状態と依存グラフを組み合わせて、
972/// 次に実行すべきアクションを推奨する。
973#[derive(Debug, Clone)]
974pub struct GraphNavigator {
975    graph: DependencyGraph,
976    /// 実行済みアクション(成功したもの)
977    completed_actions: HashSet<String>,
978}
979
980impl GraphNavigator {
981    pub fn new(graph: DependencyGraph) -> Self {
982        Self {
983            graph,
984            completed_actions: HashSet::new(),
985        }
986    }
987
988    /// アクションを完了としてマーク
989    pub fn mark_completed(&mut self, action: &str) {
990        self.completed_actions.insert(action.to_string());
991    }
992
993    /// 次に実行すべきアクションを取得
994    ///
995    /// - まだ開始していない場合: 開始ノードを返す
996    /// - 実行中の場合: 完了したアクションの後続を返す
997    /// - 全て完了している場合: 空を返す
998    pub fn suggest_next(&self) -> Vec<String> {
999        if self.completed_actions.is_empty() {
1000            // まだ何も実行していない → 開始ノード
1001            return self.graph.start_actions();
1002        }
1003
1004        // 完了したアクションの後続を収集
1005        let mut candidates = Vec::new();
1006        for completed in &self.completed_actions {
1007            for next in self.graph.valid_next_actions(completed) {
1008                if !self.completed_actions.contains(&next) && !candidates.contains(&next) {
1009                    candidates.push(next);
1010                }
1011            }
1012        }
1013
1014        candidates
1015    }
1016
1017    /// タスクが完了したか判定
1018    ///
1019    /// 終端アクションのいずれかが完了していれば true。
1020    pub fn is_task_complete(&self) -> bool {
1021        self.graph
1022            .terminal_actions()
1023            .iter()
1024            .any(|t| self.completed_actions.contains(t))
1025    }
1026
1027    /// 進捗を取得(完了アクション数 / 全アクション数)
1028    pub fn progress(&self) -> f64 {
1029        if self.graph.available_actions.is_empty() {
1030            return 0.0;
1031        }
1032        self.completed_actions.len() as f64 / self.graph.available_actions.len() as f64
1033    }
1034
1035    /// 依存グラフへの参照を取得
1036    pub fn graph(&self) -> &DependencyGraph {
1037        &self.graph
1038    }
1039}
1040
1041// ============================================================================
1042// LearnedDependencyProvider - 学習データからグラフを生成
1043// ============================================================================
1044
1045use crate::learn::offline::LearnedActionOrder;
1046
1047/// 学習済みアクション順序から DependencyGraph を生成する Provider
1048///
1049/// `OfflineModel.action_order` を使用して、LLM を呼ばずに即座にグラフを構築する。
1050/// アクション集合が一致しない場合は `None` を返し、LLM フォールバックさせる。
1051///
1052/// # Example
1053///
1054/// ```ignore
1055/// use swarm_engine_core::learn::offline::LearnedActionOrder;
1056/// use swarm_engine_core::exploration::LearnedDependencyProvider;
1057///
1058/// let order = LearnedActionOrder::new(
1059///     vec!["Grep".to_string(), "Read".to_string()],  // discover
1060///     vec!["Restart".to_string()],                    // not_discover
1061///     &["Grep", "Read", "Restart"].map(String::from).to_vec(),
1062/// );
1063///
1064/// let provider = LearnedDependencyProvider::new(order);
1065/// let graph = provider.provide_graph("task", &actions);
1066/// ```
1067#[derive(Debug, Clone)]
1068pub struct LearnedDependencyProvider {
1069    action_order: LearnedActionOrder,
1070}
1071
1072impl LearnedDependencyProvider {
1073    /// 新しい LearnedDependencyProvider を作成
1074    pub fn new(action_order: LearnedActionOrder) -> Self {
1075        Self { action_order }
1076    }
1077
1078    /// 学習済み順序を取得
1079    pub fn action_order(&self) -> &LearnedActionOrder {
1080        &self.action_order
1081    }
1082}
1083
1084impl DependencyGraphProvider for LearnedDependencyProvider {
1085    fn provide_graph(&self, task: &str, available_actions: &[String]) -> Option<DependencyGraph> {
1086        // アクション集合が一致するか確認
1087        if !self.action_order.matches_actions(available_actions) {
1088            tracing::debug!(
1089                expected_hash = self.action_order.action_set_hash,
1090                actual_hash = LearnedActionOrder::compute_hash(available_actions),
1091                "Action set mismatch, falling back to LLM"
1092            );
1093            return None;
1094        }
1095
1096        // グラフを構築
1097        let sorted_discover = &self.action_order.discover;
1098        let sorted_not_discover = &self.action_order.not_discover;
1099
1100        // 両方空の場合は有効なグラフを構築できない
1101        if sorted_discover.is_empty() && sorted_not_discover.is_empty() {
1102            tracing::warn!("Both discover and not_discover are empty, cannot build graph");
1103            return None;
1104        }
1105
1106        tracing::info!(
1107            discover = ?self.action_order.discover,
1108            not_discover = ?self.action_order.not_discover,
1109            "Using learned action order (LLM skipped)"
1110        );
1111
1112        let mut builder = DependencyGraphBuilder::new()
1113            .task(task)
1114            .available_actions(available_actions.iter().cloned());
1115
1116        // 開始ノード設定
1117        if !sorted_discover.is_empty() {
1118            builder = builder.start_node(&sorted_discover[0]);
1119        } else if !sorted_not_discover.is_empty() {
1120            builder = builder.start_node(&sorted_not_discover[0]);
1121        }
1122
1123        // 終端ノード設定
1124        if let Some(last) = sorted_not_discover.last() {
1125            builder = builder.terminal_node(last);
1126        } else if !sorted_discover.is_empty() {
1127            builder = builder.terminal_node(sorted_discover.last().unwrap());
1128        }
1129
1130        // Discover 間のエッジ
1131        for window in sorted_discover.windows(2) {
1132            builder = builder.edge(&window[0], &window[1], 0.9);
1133        }
1134
1135        // Discover → NotDiscover へのエッジ
1136        if !sorted_discover.is_empty() && !sorted_not_discover.is_empty() {
1137            builder = builder.edge(
1138                sorted_discover.last().unwrap(),
1139                &sorted_not_discover[0],
1140                0.9,
1141            );
1142        }
1143
1144        // NotDiscover 間のエッジ
1145        for window in sorted_not_discover.windows(2) {
1146            builder = builder.edge(&window[0], &window[1], 0.9);
1147        }
1148
1149        // バリデーション付きでビルド(失敗時は None)
1150        builder.build_validated().ok()
1151    }
1152}
1153
1154// ============================================================================
1155// Tests
1156// ============================================================================
1157
1158#[cfg(test)]
1159mod tests {
1160    use super::*;
1161
1162    #[test]
1163    fn test_dependency_graph_builder() {
1164        let graph = DependencyGraph::builder()
1165            .task("Find auth function")
1166            .available_actions(["Grep", "List", "Read"])
1167            .edge("Grep", "Read", 0.95)
1168            .edge("List", "Grep", 0.60)
1169            .start_nodes(["Grep", "List"])
1170            .terminal_node("Read")
1171            .build();
1172
1173        assert_eq!(graph.task(), "Find auth function");
1174        assert!(graph.is_start("Grep"));
1175        assert!(graph.is_start("List"));
1176        assert!(graph.is_terminal("Read"));
1177        assert!(graph.can_transition("Grep", "Read"));
1178        assert!(!graph.can_transition("Read", "Grep"));
1179    }
1180
1181    #[test]
1182    fn test_valid_next_actions() {
1183        let graph = DependencyGraph::builder()
1184            .available_actions(["Grep", "List", "Read"])
1185            .edge("Grep", "Read", 0.95)
1186            .edge("List", "Grep", 0.60)
1187            .edge("List", "Read", 0.40)
1188            .start_nodes(["Grep", "List"])
1189            .terminal_node("Read")
1190            .build();
1191
1192        // Grep の後は Read
1193        let next = graph.valid_next_actions("Grep");
1194        assert_eq!(next, vec!["Read"]);
1195
1196        // List の後は Grep (0.60) と Read (0.40)、確信度順
1197        let next = graph.valid_next_actions("List");
1198        assert_eq!(next, vec!["Grep", "Read"]);
1199
1200        // Read の後は何もない(終端)
1201        let next = graph.valid_next_actions("Read");
1202        assert!(next.is_empty());
1203    }
1204
1205    #[test]
1206    fn test_static_planner_file_exploration() {
1207        let planner = StaticDependencyPlanner::new().with_file_exploration_pattern();
1208
1209        let graph = planner
1210            .plan("Find auth.rs", &["Grep".to_string(), "Read".to_string()])
1211            .unwrap();
1212
1213        assert!(graph.is_start("Grep"));
1214        assert!(graph.is_terminal("Read"));
1215    }
1216
1217    #[test]
1218    fn test_graph_navigator() {
1219        let graph = DependencyGraph::builder()
1220            .available_actions(["Grep", "Read"])
1221            .edge("Grep", "Read", 0.95)
1222            .start_node("Grep")
1223            .terminal_node("Read")
1224            .build();
1225
1226        let mut nav = GraphNavigator::new(graph);
1227
1228        // 最初は Grep を推奨
1229        assert_eq!(nav.suggest_next(), vec!["Grep"]);
1230        assert!(!nav.is_task_complete());
1231
1232        // Grep 完了
1233        nav.mark_completed("Grep");
1234        assert_eq!(nav.suggest_next(), vec!["Read"]);
1235        assert!(!nav.is_task_complete());
1236
1237        // Read 完了 → タスク完了
1238        nav.mark_completed("Read");
1239        assert!(nav.is_task_complete());
1240        assert!(nav.suggest_next().is_empty());
1241    }
1242
1243    #[test]
1244    fn test_llm_response_parsing() {
1245        let json = r#"{
1246            "edges": [
1247                {"from": "Grep", "to": "Read", "confidence": 0.95}
1248            ],
1249            "start": ["Grep"],
1250            "terminal": ["Read"],
1251            "reasoning": "Search first, then read"
1252        }"#;
1253
1254        let response = LlmDependencyResponse::parse(json).unwrap();
1255        assert_eq!(response.edges.len(), 1);
1256        assert_eq!(response.start, vec!["Grep"]);
1257        assert_eq!(response.terminal, vec!["Read"]);
1258        assert!(response.reasoning.is_some());
1259
1260        let graph = response.into_graph(
1261            "Find function",
1262            vec!["Grep".to_string(), "Read".to_string()],
1263        );
1264        assert!(graph.can_transition("Grep", "Read"));
1265    }
1266
1267    #[test]
1268    fn test_mermaid_output() {
1269        let graph = DependencyGraph::builder()
1270            .available_actions(["Grep", "List", "Read"])
1271            .edge("Grep", "Read", 0.95)
1272            .edge("List", "Grep", 0.60)
1273            .start_nodes(["Grep", "List"])
1274            .terminal_node("Read")
1275            .build();
1276
1277        let mermaid = graph.to_mermaid();
1278        assert!(mermaid.contains("graph LR"));
1279        assert!(mermaid.contains("Grep -->|95%| Read"));
1280        assert!(mermaid.contains("style Read fill:#f99"));
1281    }
1282
1283    // =========================================================================
1284    // LearnedDependencyProvider Tests
1285    // =========================================================================
1286
1287    #[test]
1288    fn test_learned_action_order_hash() {
1289        let actions = vec![
1290            "Grep".to_string(),
1291            "Read".to_string(),
1292            "Restart".to_string(),
1293        ];
1294
1295        let order = LearnedActionOrder::new(
1296            vec!["Grep".to_string(), "Read".to_string()],
1297            vec!["Restart".to_string()],
1298            &actions,
1299        );
1300
1301        // 同じアクション集合(順序違い)なら同じハッシュ
1302        let actions_reordered = vec![
1303            "Restart".to_string(),
1304            "Grep".to_string(),
1305            "Read".to_string(),
1306        ];
1307        assert!(order.matches_actions(&actions_reordered));
1308
1309        // 異なるアクション集合なら不一致
1310        let actions_different = vec!["Grep".to_string(), "Read".to_string()];
1311        assert!(!order.matches_actions(&actions_different));
1312    }
1313
1314    #[test]
1315    fn test_learned_dependency_provider_cache_hit() {
1316        let actions = vec![
1317            "Grep".to_string(),
1318            "Read".to_string(),
1319            "Restart".to_string(),
1320        ];
1321
1322        let order = LearnedActionOrder::new(
1323            vec!["Grep".to_string(), "Read".to_string()],
1324            vec!["Restart".to_string()],
1325            &actions,
1326        );
1327
1328        let provider = LearnedDependencyProvider::new(order);
1329
1330        // キャッシュヒット
1331        let graph = provider.provide_graph("troubleshooting", &actions);
1332        assert!(graph.is_some());
1333
1334        let graph = graph.unwrap();
1335        assert!(graph.is_start("Grep"));
1336        assert!(graph.is_terminal("Restart"));
1337        assert!(graph.can_transition("Grep", "Read"));
1338        assert!(graph.can_transition("Read", "Restart"));
1339    }
1340
1341    #[test]
1342    fn test_learned_dependency_provider_cache_miss() {
1343        let original_actions = vec![
1344            "Grep".to_string(),
1345            "Read".to_string(),
1346            "Restart".to_string(),
1347        ];
1348
1349        let order = LearnedActionOrder::new(
1350            vec!["Grep".to_string(), "Read".to_string()],
1351            vec!["Restart".to_string()],
1352            &original_actions,
1353        );
1354
1355        let provider = LearnedDependencyProvider::new(order);
1356
1357        // 異なるアクション集合 → キャッシュミス(None)
1358        let different_actions = vec!["Grep".to_string(), "Read".to_string()];
1359        let graph = provider.provide_graph("troubleshooting", &different_actions);
1360        assert!(graph.is_none());
1361    }
1362
1363    #[test]
1364    fn test_learned_dependency_provider_discover_only() {
1365        let actions = vec!["Grep".to_string(), "Read".to_string()];
1366
1367        let order = LearnedActionOrder::new(
1368            vec!["Grep".to_string(), "Read".to_string()],
1369            vec![], // NotDiscover なし
1370            &actions,
1371        );
1372
1373        let provider = LearnedDependencyProvider::new(order);
1374        let graph = provider.provide_graph("search task", &actions);
1375        assert!(graph.is_some());
1376
1377        let graph = graph.unwrap();
1378        assert!(graph.is_start("Grep"));
1379        assert!(graph.is_terminal("Read")); // Discover の最後が Terminal
1380        assert!(graph.can_transition("Grep", "Read"));
1381    }
1382
1383    #[test]
1384    fn test_learned_dependency_provider_not_discover_only() {
1385        let actions = vec!["Restart".to_string(), "CheckStatus".to_string()];
1386
1387        let order = LearnedActionOrder::new(
1388            vec![], // Discover なし
1389            vec!["Restart".to_string(), "CheckStatus".to_string()],
1390            &actions,
1391        );
1392
1393        let provider = LearnedDependencyProvider::new(order);
1394        let graph = provider.provide_graph("ops task", &actions);
1395        assert!(graph.is_some());
1396
1397        let graph = graph.unwrap();
1398        assert!(graph.is_start("Restart")); // NotDiscover の最初が Start
1399        assert!(graph.is_terminal("CheckStatus"));
1400        assert!(graph.can_transition("Restart", "CheckStatus"));
1401    }
1402
1403    #[test]
1404    fn test_learned_dependency_provider_empty_lists() {
1405        let actions = vec!["Grep".to_string(), "Read".to_string()];
1406
1407        let order = LearnedActionOrder::new(
1408            vec![], // Discover なし
1409            vec![], // NotDiscover なし
1410            &actions,
1411        );
1412
1413        let provider = LearnedDependencyProvider::new(order);
1414        // 両方空の場合は None を返す
1415        let graph = provider.provide_graph("empty task", &actions);
1416        assert!(graph.is_none());
1417    }
1418
1419    // =========================================================================
1420    // extract_json Tests
1421    // =========================================================================
1422
1423    #[test]
1424    fn test_extract_json_simple() {
1425        let text = r#"Here is the result: {"edges": [], "start": ["A"], "terminal": ["B"]}"#;
1426        let json = LlmDependencyResponse::extract_json(text);
1427        assert!(json.is_some());
1428        let json = json.unwrap();
1429        assert!(json.starts_with('{'));
1430        assert!(json.ends_with('}'));
1431    }
1432
1433    #[test]
1434    fn test_extract_json_nested() {
1435        let text = r#"Result: {"edges": [{"from": "A", "to": "B", "confidence": 0.9}], "start": ["A"], "terminal": ["B"]}"#;
1436        let json = LlmDependencyResponse::extract_json(text);
1437        assert!(json.is_some());
1438
1439        // パースできることを確認
1440        let parsed: Result<LlmDependencyResponse, _> = serde_json::from_str(&json.unwrap());
1441        assert!(parsed.is_ok());
1442    }
1443
1444    #[test]
1445    fn test_extract_json_with_string_braces() {
1446        // 文字列内の {} は無視される
1447        let text =
1448            r#"{"edges": [], "start": ["A"], "terminal": ["B"], "reasoning": "Use {pattern}"}"#;
1449        let json = LlmDependencyResponse::extract_json(text);
1450        assert!(json.is_some());
1451        assert_eq!(json.unwrap(), text);
1452    }
1453
1454    #[test]
1455    fn test_extract_json_no_json() {
1456        let text = "This is just plain text without JSON";
1457        let json = LlmDependencyResponse::extract_json(text);
1458        assert!(json.is_none());
1459    }
1460
1461    // =========================================================================
1462    // validate() Tests
1463    // =========================================================================
1464
1465    #[test]
1466    fn test_validate_unknown_start_node() {
1467        let graph = DependencyGraph::builder()
1468            .available_actions(["Grep", "Read"])
1469            .start_node("Unknown") // available_actions に含まれない
1470            .terminal_node("Read")
1471            .build();
1472
1473        let result = graph.validate();
1474        assert!(result.is_err());
1475        assert!(matches!(
1476            result.unwrap_err(),
1477            DependencyGraphError::UnknownAction(name) if name == "Unknown"
1478        ));
1479    }
1480
1481    #[test]
1482    fn test_validate_unknown_terminal_node() {
1483        let graph = DependencyGraph::builder()
1484            .available_actions(["Grep", "Read"])
1485            .start_node("Grep")
1486            .terminal_node("Unknown") // available_actions に含まれない
1487            .build();
1488
1489        let result = graph.validate();
1490        assert!(result.is_err());
1491        assert!(matches!(
1492            result.unwrap_err(),
1493            DependencyGraphError::UnknownAction(name) if name == "Unknown"
1494        ));
1495    }
1496
1497    #[test]
1498    fn test_validate_valid_graph() {
1499        let graph = DependencyGraph::builder()
1500            .available_actions(["Grep", "Read"])
1501            .edge("Grep", "Read", 0.9)
1502            .start_node("Grep")
1503            .terminal_node("Read")
1504            .build();
1505
1506        assert!(graph.validate().is_ok());
1507    }
1508
1509    // =========================================================================
1510    // start_actions / terminal_actions Order Tests
1511    // =========================================================================
1512
1513    #[test]
1514    fn test_start_actions_sorted() {
1515        let graph = DependencyGraph::builder()
1516            .available_actions(["Zebra", "Apple", "Mango"])
1517            .start_nodes(["Zebra", "Apple", "Mango"])
1518            .terminal_node("Zebra")
1519            .build();
1520
1521        let actions = graph.start_actions();
1522        // アルファベット順にソートされる
1523        assert_eq!(actions, vec!["Apple", "Mango", "Zebra"]);
1524    }
1525
1526    #[test]
1527    fn test_terminal_actions_sorted() {
1528        let graph = DependencyGraph::builder()
1529            .available_actions(["Zebra", "Apple", "Mango"])
1530            .start_node("Apple")
1531            .terminal_nodes(["Zebra", "Apple", "Mango"])
1532            .build();
1533
1534        let actions = graph.terminal_actions();
1535        // アルファベット順にソートされる
1536        assert_eq!(actions, vec!["Apple", "Mango", "Zebra"]);
1537    }
1538}