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}
96
97impl DependencyGraph {
98    /// 空のグラフを作成
99    pub fn new() -> Self {
100        Self::default()
101    }
102
103    /// Builder パターンでグラフを構築
104    pub fn builder() -> DependencyGraphBuilder {
105        DependencyGraphBuilder::new()
106    }
107
108    // ------------------------------------------------------------------------
109    // Query API
110    // ------------------------------------------------------------------------
111
112    /// 指定アクションの後に実行可能なアクションを取得
113    ///
114    /// 確信度順にソートして返す。
115    pub fn valid_next_actions(&self, current_action: &str) -> Vec<String> {
116        let mut edges: Vec<_> = self
117            .edges
118            .iter()
119            .filter(|e| e.from == current_action)
120            .collect();
121
122        // 確信度が高い順にソート
123        edges.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap());
124
125        edges.iter().map(|e| e.to.clone()).collect()
126    }
127
128    /// 開始可能なアクションを取得
129    ///
130    /// 確信度順にソート(開始ノードへのエッジの確信度で判定)。
131    pub fn start_actions(&self) -> Vec<String> {
132        self.start_nodes.iter().cloned().collect()
133    }
134
135    /// 終端アクションを取得
136    pub fn terminal_actions(&self) -> Vec<String> {
137        self.terminal_nodes.iter().cloned().collect()
138    }
139
140    /// 指定アクションが終端か
141    pub fn is_terminal(&self, action: &str) -> bool {
142        self.terminal_nodes.contains(action)
143    }
144
145    /// 指定アクションが開始ノードか
146    pub fn is_start(&self, action: &str) -> bool {
147        self.start_nodes.contains(action)
148    }
149
150    /// 指定アクションから指定アクションへの遷移が有効か
151    pub fn can_transition(&self, from: &str, to: &str) -> bool {
152        self.edges.iter().any(|e| e.from == from && e.to == to)
153    }
154
155    /// 遷移の確信度を取得
156    pub fn transition_confidence(&self, from: &str, to: &str) -> Option<f64> {
157        self.edges
158            .iter()
159            .find(|e| e.from == from && e.to == to)
160            .map(|e| e.confidence)
161    }
162
163    /// 全エッジを取得
164    pub fn edges(&self) -> &[DependencyEdge] {
165        &self.edges
166    }
167
168    /// タスク説明を取得
169    pub fn task(&self) -> &str {
170        &self.task
171    }
172
173    /// 利用可能なアクション一覧を取得
174    pub fn available_actions(&self) -> &[String] {
175        &self.available_actions
176    }
177
178    /// 指定アクションのパラメータバリアントを取得
179    pub fn param_variants(&self, action: &str) -> Option<(&str, &[String])> {
180        self.param_variants
181            .get(action)
182            .map(|(key, values)| (key.as_str(), values.as_slice()))
183    }
184
185    /// 全パラメータバリアントを取得
186    pub fn all_param_variants(&self) -> &HashMap<String, (String, Vec<String>)> {
187        &self.param_variants
188    }
189
190    // ------------------------------------------------------------------------
191    // Validation
192    // ------------------------------------------------------------------------
193
194    /// グラフが有効か検証
195    ///
196    /// - 少なくとも1つの開始ノードがある
197    /// - 少なくとも1つの終端ノードがある
198    /// - 全エッジのアクションが available_actions に含まれる
199    pub fn validate(&self) -> Result<(), DependencyGraphError> {
200        if self.start_nodes.is_empty() {
201            return Err(DependencyGraphError::NoStartNodes);
202        }
203
204        if self.terminal_nodes.is_empty() {
205            return Err(DependencyGraphError::NoTerminalNodes);
206        }
207
208        // エッジのアクションが available_actions に含まれるか
209        for edge in &self.edges {
210            if !self.available_actions.contains(&edge.from) {
211                return Err(DependencyGraphError::UnknownAction(edge.from.clone()));
212            }
213            if !self.available_actions.contains(&edge.to) {
214                return Err(DependencyGraphError::UnknownAction(edge.to.clone()));
215            }
216        }
217
218        Ok(())
219    }
220
221    // ------------------------------------------------------------------------
222    // Visualization
223    // ------------------------------------------------------------------------
224
225    /// グラフを Mermaid 形式で出力
226    pub fn to_mermaid(&self) -> String {
227        let mut lines = vec!["graph LR".to_string()];
228
229        for edge in &self.edges {
230            let label = format!("{:.0}%", edge.confidence * 100.0);
231            lines.push(format!("    {} -->|{}| {}", edge.from, label, edge.to));
232        }
233
234        // 開始ノードをスタイル
235        for start in &self.start_nodes {
236            lines.push(format!("    style {} fill:#9f9", start));
237        }
238
239        // 終端ノードをスタイル
240        for terminal in &self.terminal_nodes {
241            lines.push(format!("    style {} fill:#f99", terminal));
242        }
243
244        lines.join("\n")
245    }
246}
247
248/// グラフ構築エラー
249#[derive(Debug, Clone, thiserror::Error)]
250pub enum DependencyGraphError {
251    #[error("No start nodes defined")]
252    NoStartNodes,
253
254    #[error("No terminal nodes defined")]
255    NoTerminalNodes,
256
257    #[error("Unknown action: {0}")]
258    UnknownAction(String),
259
260    #[error("Parse error: {0}")]
261    ParseError(String),
262
263    #[error("LLM error: {0}")]
264    LlmError(String),
265}
266
267// ============================================================================
268// Provider Trait
269// ============================================================================
270
271/// DependencyGraph を提供するトレイト
272///
273/// このトレイトを実装することで、様々な方法で DependencyGraph を生成できる:
274/// - LLM による動的生成(BatchInvoker が実装)
275/// - 静的な事前定義グラフ
276/// - テスト用のモック
277///
278/// # Example
279///
280/// ```ignore
281/// use swarm_engine_core::exploration::{DependencyGraph, DependencyGraphProvider};
282///
283/// struct StaticProvider {
284///     graph: DependencyGraph,
285/// }
286///
287/// impl DependencyGraphProvider for StaticProvider {
288///     fn provide_graph(&self, _task: &str, _actions: &[String]) -> Option<DependencyGraph> {
289///         Some(self.graph.clone())
290///     }
291/// }
292/// ```
293pub trait DependencyGraphProvider: Send + Sync {
294    /// タスクとアクション一覧から DependencyGraph を生成
295    ///
296    /// # Arguments
297    /// - `task`: タスクの説明(goal)
298    /// - `available_actions`: 利用可能なアクション名の一覧
299    ///
300    /// # Returns
301    /// - `Some(DependencyGraph)`: グラフが生成された場合
302    /// - `None`: 生成をスキップする場合(LLM エラー等)
303    fn provide_graph(&self, task: &str, available_actions: &[String]) -> Option<DependencyGraph>;
304}
305
306// ============================================================================
307// Builder
308// ============================================================================
309
310/// DependencyGraph のビルダー
311#[derive(Debug, Clone, Default)]
312pub struct DependencyGraphBuilder {
313    edges: Vec<DependencyEdge>,
314    start_nodes: HashSet<String>,
315    terminal_nodes: HashSet<String>,
316    task: String,
317    available_actions: Vec<String>,
318    param_variants: HashMap<String, (String, Vec<String>)>,
319}
320
321impl DependencyGraphBuilder {
322    pub fn new() -> Self {
323        Self::default()
324    }
325
326    /// タスク説明を設定
327    pub fn task(mut self, task: impl Into<String>) -> Self {
328        self.task = task.into();
329        self
330    }
331
332    /// 利用可能なアクションを設定
333    pub fn available_actions<I, S>(mut self, actions: I) -> Self
334    where
335        I: IntoIterator<Item = S>,
336        S: Into<String>,
337    {
338        self.available_actions = actions.into_iter().map(|s| s.into()).collect();
339        self
340    }
341
342    /// エッジを追加
343    pub fn edge(mut self, from: impl Into<String>, to: impl Into<String>, confidence: f64) -> Self {
344        self.edges.push(DependencyEdge::new(from, to, confidence));
345        self
346    }
347
348    /// 開始ノードを追加
349    pub fn start_node(mut self, action: impl Into<String>) -> Self {
350        self.start_nodes.insert(action.into());
351        self
352    }
353
354    /// 複数の開始ノードを追加
355    pub fn start_nodes<I, S>(mut self, actions: I) -> Self
356    where
357        I: IntoIterator<Item = S>,
358        S: Into<String>,
359    {
360        self.start_nodes
361            .extend(actions.into_iter().map(|s| s.into()));
362        self
363    }
364
365    /// 終端ノードを追加
366    pub fn terminal_node(mut self, action: impl Into<String>) -> Self {
367        self.terminal_nodes.insert(action.into());
368        self
369    }
370
371    /// 複数の終端ノードを追加
372    pub fn terminal_nodes<I, S>(mut self, actions: I) -> Self
373    where
374        I: IntoIterator<Item = S>,
375        S: Into<String>,
376    {
377        self.terminal_nodes
378            .extend(actions.into_iter().map(|s| s.into()));
379        self
380    }
381
382    /// パラメータバリアントを追加
383    ///
384    /// 指定アクションの後続ノード展開時に、各バリアントごとにノードを生成する。
385    pub fn param_variants<I, S>(
386        mut self,
387        action: impl Into<String>,
388        key: impl Into<String>,
389        values: I,
390    ) -> Self
391    where
392        I: IntoIterator<Item = S>,
393        S: Into<String>,
394    {
395        self.param_variants.insert(
396            action.into(),
397            (key.into(), values.into_iter().map(|s| s.into()).collect()),
398        );
399        self
400    }
401
402    /// グラフを構築
403    pub fn build(self) -> DependencyGraph {
404        DependencyGraph {
405            edges: self.edges,
406            start_nodes: self.start_nodes,
407            terminal_nodes: self.terminal_nodes,
408            task: self.task,
409            available_actions: self.available_actions,
410            param_variants: self.param_variants,
411        }
412    }
413
414    /// グラフを構築してバリデーション
415    pub fn build_validated(self) -> Result<DependencyGraph, DependencyGraphError> {
416        let graph = self.build();
417        graph.validate()?;
418        Ok(graph)
419    }
420}
421
422// ============================================================================
423// LLM Response Parsing
424// ============================================================================
425
426/// LLM からの依存グラフ生成レスポンス
427///
428/// LLM が JSON で返すことを想定。
429#[derive(Debug, Clone, Serialize, Deserialize)]
430pub struct LlmDependencyResponse {
431    /// 依存エッジ
432    pub edges: Vec<LlmEdge>,
433    /// 開始アクション
434    pub start: Vec<String>,
435    /// 終端アクション
436    pub terminal: Vec<String>,
437    /// 推論理由(オプション)
438    #[serde(default)]
439    pub reasoning: Option<String>,
440}
441
442/// LLM レスポンス内のエッジ
443#[derive(Debug, Clone, Serialize, Deserialize)]
444pub struct LlmEdge {
445    pub from: String,
446    pub to: String,
447    pub confidence: f64,
448}
449
450impl LlmDependencyResponse {
451    /// DependencyGraph に変換
452    pub fn into_graph(
453        self,
454        task: impl Into<String>,
455        available_actions: Vec<String>,
456    ) -> DependencyGraph {
457        let mut builder = DependencyGraphBuilder::new()
458            .task(task)
459            .available_actions(available_actions)
460            .start_nodes(self.start)
461            .terminal_nodes(self.terminal);
462
463        for edge in self.edges {
464            builder = builder.edge(edge.from, edge.to, edge.confidence);
465        }
466
467        builder.build()
468    }
469
470    /// テキストからパース(矢印形式 or JSON)
471    ///
472    /// 優先順位:
473    /// 1. 矢印形式(A -> B -> C)
474    /// 2. JSON形式
475    pub fn parse(text: &str) -> Result<Self, DependencyGraphError> {
476        // 1. 矢印形式をパース
477        if let Some(response) = Self::parse_arrow_format(text) {
478            return Ok(response);
479        }
480
481        // 2. JSON形式を試行
482        if let Ok(parsed) = serde_json::from_str(text) {
483            return Ok(parsed);
484        }
485
486        // 3. テキストからJSONを抽出して再試行
487        if let Some(json) = Self::extract_json(text) {
488            serde_json::from_str(&json).map_err(|e| DependencyGraphError::ParseError(e.to_string()))
489        } else {
490            Err(DependencyGraphError::ParseError(format!(
491                "No valid format found in response: {}",
492                text.chars().take(200).collect::<String>()
493            )))
494        }
495    }
496
497    /// アクション順序をパース(矢印形式 or 番号リスト形式)
498    ///
499    /// 対応形式:
500    /// - 矢印: "Grep -> Read -> List"
501    /// - 番号: "1. Grep 2. Read 3. List"
502    /// - コンマ: "Grep, Read, List"
503    fn parse_arrow_format(text: &str) -> Option<Self> {
504        // 矢印形式を試行
505        if let Some(result) = Self::parse_arrow_only(text) {
506            return Some(result);
507        }
508
509        // 番号リスト形式を試行(例: "1. Read 2. List 3. Grep")
510        if let Some(result) = Self::parse_numbered_list(text) {
511            return Some(result);
512        }
513
514        None
515    }
516
517    /// 矢印形式のみをパース
518    fn parse_arrow_only(text: &str) -> Option<Self> {
519        let normalized = text.replace('→', "->");
520
521        // 矢印を含む行を探す
522        let arrow_line = normalized.lines().find(|line| line.contains("->"))?;
523
524        let parts: Vec<&str> = arrow_line.split("->").collect();
525        if parts.len() < 2 {
526            return None;
527        }
528
529        // 最後の単語のみを抽出("So the order is Read" -> "Read")
530        let actions_in_order: Vec<String> = parts
531            .iter()
532            .filter_map(|part| {
533                let trimmed = part.trim();
534                // 最後の単語を取得
535                let last_word = trimmed.split_whitespace().last()?;
536                // 英字のみを抽出
537                let action: String = last_word.chars().filter(|c| c.is_alphabetic()).collect();
538                if action.is_empty() {
539                    None
540                } else {
541                    Some(action)
542                }
543            })
544            .collect();
545
546        if actions_in_order.len() < 2 {
547            return None;
548        }
549
550        Self::build_response(actions_in_order)
551    }
552
553    /// 番号リスト形式をパース(例: "1. Read 2. List 3. Grep")
554    fn parse_numbered_list(text: &str) -> Option<Self> {
555        let mut actions_in_order: Vec<String> = Vec::new();
556
557        // "1." "2." "3." などを探す
558        for i in 1..=10 {
559            let pattern = format!("{}.", i);
560            if let Some(pos) = text.find(&pattern) {
561                // 番号の後の単語を取得
562                let after = &text[pos + pattern.len()..];
563                if let Some(word) = after.split_whitespace().next() {
564                    // 英字のみを抽出
565                    let action: String = word.chars().filter(|c| c.is_alphabetic()).collect();
566                    if !action.is_empty() && !actions_in_order.contains(&action) {
567                        actions_in_order.push(action);
568                    }
569                }
570            }
571        }
572
573        if actions_in_order.len() < 2 {
574            return None;
575        }
576
577        Self::build_response(actions_in_order)
578    }
579
580    /// LlmDependencyResponse を構築
581    fn build_response(actions_in_order: Vec<String>) -> Option<Self> {
582        let mut edges = Vec::new();
583        for window in actions_in_order.windows(2) {
584            edges.push(LlmEdge {
585                from: window[0].clone(),
586                to: window[1].clone(),
587                confidence: 0.9,
588            });
589        }
590
591        Some(Self {
592            edges,
593            start: vec![actions_in_order.first()?.clone()],
594            terminal: vec![actions_in_order.last()?.clone()],
595            reasoning: Some("Parsed from text format".to_string()),
596        })
597    }
598
599    /// テキストからJSONオブジェクトを抽出
600    fn extract_json(text: &str) -> Option<String> {
601        // { ... } を探す(バランスを取って)
602        let start = text.find('{')?;
603        let chars: Vec<char> = text[start..].chars().collect();
604        let mut depth = 0;
605        let mut in_string = false;
606        let mut escape_next = false;
607
608        for (i, &ch) in chars.iter().enumerate() {
609            if escape_next {
610                escape_next = false;
611                continue;
612            }
613
614            match ch {
615                '\\' if in_string => escape_next = true,
616                '"' => in_string = !in_string,
617                '{' if !in_string => depth += 1,
618                '}' if !in_string => {
619                    depth -= 1;
620                    if depth == 0 {
621                        return Some(chars[..=i].iter().collect());
622                    }
623                }
624                _ => {}
625            }
626        }
627
628        None
629    }
630}
631
632// ============================================================================
633// Planner Trait
634// ============================================================================
635
636/// 依存グラフ生成プランナー trait
637///
638/// LLM を使った実装と、静的な実装の両方をサポート。
639pub trait DependencyPlanner: Send + Sync {
640    /// タスクとアクション一覧から依存グラフを生成
641    ///
642    /// 非同期処理は呼び出し側で管理(LLM 呼び出しは別レイヤー)。
643    fn plan(
644        &self,
645        task: &str,
646        available_actions: &[String],
647    ) -> Result<DependencyGraph, DependencyGraphError>;
648
649    /// プランナー名
650    fn name(&self) -> &str;
651}
652
653/// 静的な依存グラフプランナー(LLM 不使用)
654///
655/// 事前定義されたパターンから依存グラフを生成。
656/// テストや LLM なしでの動作確認に使用。
657#[derive(Debug, Clone, Default)]
658pub struct StaticDependencyPlanner {
659    /// パターン名 → グラフ のマッピング
660    patterns: HashMap<String, DependencyGraph>,
661    /// デフォルトパターン名
662    default_pattern: Option<String>,
663}
664
665impl StaticDependencyPlanner {
666    pub fn new() -> Self {
667        Self::default()
668    }
669
670    /// パターンを追加
671    pub fn with_pattern(mut self, name: impl Into<String>, graph: DependencyGraph) -> Self {
672        let name = name.into();
673        if self.default_pattern.is_none() {
674            self.default_pattern = Some(name.clone());
675        }
676        self.patterns.insert(name, graph);
677        self
678    }
679
680    /// デフォルトパターンを設定
681    pub fn with_default_pattern(mut self, name: impl Into<String>) -> Self {
682        self.default_pattern = Some(name.into());
683        self
684    }
685
686    /// ファイル探索パターンを追加(組み込み)
687    ///
688    /// Grep|List → Read のパターン。
689    pub fn with_file_exploration_pattern(self) -> Self {
690        let graph = DependencyGraph::builder()
691            .task("File exploration")
692            .available_actions(["Grep", "List", "Read"])
693            .edge("Grep", "Read", 0.95)
694            .edge("List", "Grep", 0.60)
695            .edge("List", "Read", 0.40)
696            .start_nodes(["Grep", "List"])
697            .terminal_node("Read")
698            .build();
699
700        self.with_pattern("file_exploration", graph)
701    }
702
703    /// コード検索パターンを追加(組み込み)
704    ///
705    /// Grep → Read の強いパターン。
706    pub fn with_code_search_pattern(self) -> Self {
707        let graph = DependencyGraph::builder()
708            .task("Code search")
709            .available_actions(["Grep", "Read"])
710            .edge("Grep", "Read", 0.95)
711            .start_node("Grep")
712            .terminal_node("Read")
713            .build();
714
715        self.with_pattern("code_search", graph)
716    }
717}
718
719impl DependencyPlanner for StaticDependencyPlanner {
720    fn plan(
721        &self,
722        task: &str,
723        available_actions: &[String],
724    ) -> Result<DependencyGraph, DependencyGraphError> {
725        // デフォルトパターンを使用
726        if let Some(pattern_name) = &self.default_pattern {
727            if let Some(graph) = self.patterns.get(pattern_name) {
728                let mut graph = graph.clone();
729                graph.task = task.to_string();
730                graph.available_actions = available_actions.to_vec();
731                return Ok(graph);
732            }
733        }
734
735        // パターンがない場合は単純な線形グラフを生成
736        // 全アクションを順番に実行する想定
737        if available_actions.is_empty() {
738            return Err(DependencyGraphError::NoStartNodes);
739        }
740
741        let mut builder = DependencyGraphBuilder::new()
742            .task(task)
743            .available_actions(available_actions.to_vec())
744            .start_node(&available_actions[0]);
745
746        if available_actions.len() > 1 {
747            for window in available_actions.windows(2) {
748                builder = builder.edge(&window[0], &window[1], 0.80);
749            }
750            builder = builder.terminal_node(&available_actions[available_actions.len() - 1]);
751        } else {
752            builder = builder.terminal_node(&available_actions[0]);
753        }
754
755        Ok(builder.build())
756    }
757
758    fn name(&self) -> &str {
759        "StaticDependencyPlanner"
760    }
761}
762
763// ============================================================================
764// LLM Prompt Generation
765// ============================================================================
766
767use crate::actions::ActionDef;
768
769/// LLM 向けプロンプト生成
770///
771/// 分割クエリ方式:first/lastを個別に聞いて安定した結果を得る。
772/// ActionDef の name と description を使ってヒントを生成。
773pub struct DependencyPromptGenerator;
774
775impl DependencyPromptGenerator {
776    /// 依存グラフ生成用のプロンプトを生成
777    ///
778    /// 従来の全順序プロンプト(フォールバック用)
779    pub fn generate_prompt(task: &str, actions: &[ActionDef]) -> String {
780        let actions_list = actions
781            .iter()
782            .map(|a| a.name.as_str())
783            .collect::<Vec<_>>()
784            .join(", ");
785
786        format!(
787            r#"{task}
788Steps: {actions_list}
789The very first step is:"#
790        )
791    }
792
793    /// First step を聞くプロンプト
794    ///
795    /// ActionDef の description から動詞を抽出してヒントを生成。
796    /// 例: "Search for patterns" → "Which step SEARCHES?"
797    pub fn generate_first_prompt(_task: &str, actions: &[ActionDef]) -> String {
798        // アルファベット順にソート(順序バイアスを軽減)
799        let mut sorted_actions: Vec<&ActionDef> = actions.iter().collect();
800        sorted_actions.sort_by(|a, b| a.name.cmp(&b.name));
801
802        let actions_list = sorted_actions
803            .iter()
804            .map(|a| a.name.as_str())
805            .collect::<Vec<_>>()
806            .join(", ");
807
808        // description から動詞を抽出(最初の単語を大文字化)
809        let descriptions: Vec<String> = sorted_actions
810            .iter()
811            .map(|a| format!("- {}: {}", a.name, a.description))
812            .collect();
813        let descriptions_block = descriptions.join("\n");
814
815        // 最初のアクションの description から動詞を取得してヒントに
816        let first_verb = sorted_actions
817            .first()
818            .map(|a| Self::extract_verb(&a.description))
819            .unwrap_or_else(|| "CHECK".to_string());
820
821        format!(
822            r#"Steps: {actions_list}
823{descriptions_block}
824Which step {first_verb}S first?
825Answer:"#
826        )
827    }
828
829    /// Last step を聞くプロンプト
830    ///
831    /// ActionDef の name と description をそのまま渡す。
832    /// description に "requires X first" 等の情報があれば LLM が判断できる。
833    pub fn generate_last_prompt(_task: &str, actions: &[ActionDef]) -> String {
834        // アルファベット順にソート(順序バイアスを軽減)
835        let mut sorted_actions: Vec<&ActionDef> = actions.iter().collect();
836        sorted_actions.sort_by(|a, b| a.name.cmp(&b.name));
837
838        let actions_list = sorted_actions
839            .iter()
840            .map(|a| a.name.as_str())
841            .collect::<Vec<_>>()
842            .join(", ");
843
844        let descriptions: Vec<String> = sorted_actions
845            .iter()
846            .map(|a| format!("- {}: {}", a.name, a.description))
847            .collect();
848        let descriptions_block = descriptions.join("\n");
849
850        format!(
851            r#"Steps: {actions_list}
852{descriptions_block}
853Which step should be done last?
854Answer:"#
855        )
856    }
857
858    /// ペア比較プロンプト(どちらが先か)
859    pub fn generate_pair_prompt(task: &str, action_a: &str, action_b: &str) -> String {
860        format!(
861            r#"For {task}, which comes first: {action_a} or {action_b}?
862Answer (one word):"#
863        )
864    }
865
866    /// description から動詞を抽出(大文字化)
867    ///
868    /// 例: "Search for patterns" → "SEARCH"
869    /// 例: "Reads file contents" → "READ"
870    fn extract_verb(description: &str) -> String {
871        description
872            .split_whitespace()
873            .next()
874            .map(|w| {
875                // 末尾の 's' を除去("Reads" → "Read")
876                let word = w.trim_end_matches('s').trim_end_matches('S');
877                word.to_uppercase()
878            })
879            .unwrap_or_else(|| "CHECK".to_string())
880    }
881}
882
883// ============================================================================
884// Graph Navigation Helper
885// ============================================================================
886
887/// 依存グラフに基づくアクション選択ヘルパー
888///
889/// 現在の探索状態と依存グラフを組み合わせて、
890/// 次に実行すべきアクションを推奨する。
891#[derive(Debug, Clone)]
892pub struct GraphNavigator {
893    graph: DependencyGraph,
894    /// 実行済みアクション(成功したもの)
895    completed_actions: HashSet<String>,
896}
897
898impl GraphNavigator {
899    pub fn new(graph: DependencyGraph) -> Self {
900        Self {
901            graph,
902            completed_actions: HashSet::new(),
903        }
904    }
905
906    /// アクションを完了としてマーク
907    pub fn mark_completed(&mut self, action: &str) {
908        self.completed_actions.insert(action.to_string());
909    }
910
911    /// 次に実行すべきアクションを取得
912    ///
913    /// - まだ開始していない場合: 開始ノードを返す
914    /// - 実行中の場合: 完了したアクションの後続を返す
915    /// - 全て完了している場合: 空を返す
916    pub fn suggest_next(&self) -> Vec<String> {
917        if self.completed_actions.is_empty() {
918            // まだ何も実行していない → 開始ノード
919            return self.graph.start_actions();
920        }
921
922        // 完了したアクションの後続を収集
923        let mut candidates = Vec::new();
924        for completed in &self.completed_actions {
925            for next in self.graph.valid_next_actions(completed) {
926                if !self.completed_actions.contains(&next) && !candidates.contains(&next) {
927                    candidates.push(next);
928                }
929            }
930        }
931
932        candidates
933    }
934
935    /// タスクが完了したか判定
936    ///
937    /// 終端アクションのいずれかが完了していれば true。
938    pub fn is_task_complete(&self) -> bool {
939        self.graph
940            .terminal_actions()
941            .iter()
942            .any(|t| self.completed_actions.contains(t))
943    }
944
945    /// 進捗を取得(完了アクション数 / 全アクション数)
946    pub fn progress(&self) -> f64 {
947        if self.graph.available_actions.is_empty() {
948            return 0.0;
949        }
950        self.completed_actions.len() as f64 / self.graph.available_actions.len() as f64
951    }
952
953    /// 依存グラフへの参照を取得
954    pub fn graph(&self) -> &DependencyGraph {
955        &self.graph
956    }
957}
958
959// ============================================================================
960// Tests
961// ============================================================================
962
963#[cfg(test)]
964mod tests {
965    use super::*;
966
967    #[test]
968    fn test_dependency_graph_builder() {
969        let graph = DependencyGraph::builder()
970            .task("Find auth function")
971            .available_actions(["Grep", "List", "Read"])
972            .edge("Grep", "Read", 0.95)
973            .edge("List", "Grep", 0.60)
974            .start_nodes(["Grep", "List"])
975            .terminal_node("Read")
976            .build();
977
978        assert_eq!(graph.task(), "Find auth function");
979        assert!(graph.is_start("Grep"));
980        assert!(graph.is_start("List"));
981        assert!(graph.is_terminal("Read"));
982        assert!(graph.can_transition("Grep", "Read"));
983        assert!(!graph.can_transition("Read", "Grep"));
984    }
985
986    #[test]
987    fn test_valid_next_actions() {
988        let graph = DependencyGraph::builder()
989            .available_actions(["Grep", "List", "Read"])
990            .edge("Grep", "Read", 0.95)
991            .edge("List", "Grep", 0.60)
992            .edge("List", "Read", 0.40)
993            .start_nodes(["Grep", "List"])
994            .terminal_node("Read")
995            .build();
996
997        // Grep の後は Read
998        let next = graph.valid_next_actions("Grep");
999        assert_eq!(next, vec!["Read"]);
1000
1001        // List の後は Grep (0.60) と Read (0.40)、確信度順
1002        let next = graph.valid_next_actions("List");
1003        assert_eq!(next, vec!["Grep", "Read"]);
1004
1005        // Read の後は何もない(終端)
1006        let next = graph.valid_next_actions("Read");
1007        assert!(next.is_empty());
1008    }
1009
1010    #[test]
1011    fn test_static_planner_file_exploration() {
1012        let planner = StaticDependencyPlanner::new().with_file_exploration_pattern();
1013
1014        let graph = planner
1015            .plan("Find auth.rs", &["Grep".to_string(), "Read".to_string()])
1016            .unwrap();
1017
1018        assert!(graph.is_start("Grep"));
1019        assert!(graph.is_terminal("Read"));
1020    }
1021
1022    #[test]
1023    fn test_graph_navigator() {
1024        let graph = DependencyGraph::builder()
1025            .available_actions(["Grep", "Read"])
1026            .edge("Grep", "Read", 0.95)
1027            .start_node("Grep")
1028            .terminal_node("Read")
1029            .build();
1030
1031        let mut nav = GraphNavigator::new(graph);
1032
1033        // 最初は Grep を推奨
1034        assert_eq!(nav.suggest_next(), vec!["Grep"]);
1035        assert!(!nav.is_task_complete());
1036
1037        // Grep 完了
1038        nav.mark_completed("Grep");
1039        assert_eq!(nav.suggest_next(), vec!["Read"]);
1040        assert!(!nav.is_task_complete());
1041
1042        // Read 完了 → タスク完了
1043        nav.mark_completed("Read");
1044        assert!(nav.is_task_complete());
1045        assert!(nav.suggest_next().is_empty());
1046    }
1047
1048    #[test]
1049    fn test_llm_response_parsing() {
1050        let json = r#"{
1051            "edges": [
1052                {"from": "Grep", "to": "Read", "confidence": 0.95}
1053            ],
1054            "start": ["Grep"],
1055            "terminal": ["Read"],
1056            "reasoning": "Search first, then read"
1057        }"#;
1058
1059        let response = LlmDependencyResponse::parse(json).unwrap();
1060        assert_eq!(response.edges.len(), 1);
1061        assert_eq!(response.start, vec!["Grep"]);
1062        assert_eq!(response.terminal, vec!["Read"]);
1063        assert!(response.reasoning.is_some());
1064
1065        let graph = response.into_graph(
1066            "Find function",
1067            vec!["Grep".to_string(), "Read".to_string()],
1068        );
1069        assert!(graph.can_transition("Grep", "Read"));
1070    }
1071
1072    #[test]
1073    fn test_mermaid_output() {
1074        let graph = DependencyGraph::builder()
1075            .available_actions(["Grep", "List", "Read"])
1076            .edge("Grep", "Read", 0.95)
1077            .edge("List", "Grep", 0.60)
1078            .start_nodes(["Grep", "List"])
1079            .terminal_node("Read")
1080            .build();
1081
1082        let mermaid = graph.to_mermaid();
1083        assert!(mermaid.contains("graph LR"));
1084        assert!(mermaid.contains("Grep -->|95%| Read"));
1085        assert!(mermaid.contains("style Read fill:#f99"));
1086    }
1087}