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