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
46use crate::learn::offline::LearnedActionOrder;
47use crate::types::LoraConfig;
48
49// ============================================================================
50// VotingStrategy - 投票回数決定戦略
51// ============================================================================
52
53/// 投票回数決定戦略
54///
55/// ActionSet の一致率に基づいて、LLM 投票回数を決定する。
56#[derive(Debug, Clone)]
57pub struct VotingStrategy {
58    /// 高一致率の閾値(この以上なら 1回投票)
59    pub high_threshold: f64,
60    /// 中一致率の閾値(この以上なら 3回投票、未満なら Base Model)
61    pub medium_threshold: f64,
62}
63
64impl Default for VotingStrategy {
65    fn default() -> Self {
66        Self {
67            high_threshold: 0.8,
68            medium_threshold: 0.6,
69        }
70    }
71}
72
73impl VotingStrategy {
74    /// 投票回数を決定
75    ///
76    /// | 一致率 | LoRA | 投票回数 |
77    /// |--------|------|----------|
78    /// | 100%   | any  | 0        |
79    /// | 80%+   | yes  | 1        |
80    /// | 60%+   | yes  | 3        |
81    /// | <60%   | any  | 3        |
82    pub fn determine(&self, match_rate: f64, has_lora: bool) -> u8 {
83        if match_rate >= 1.0 {
84            0 // 完全一致: 投票不要
85        } else if match_rate >= self.high_threshold && has_lora {
86            1 // 高一致 + LoRA: 1回
87        } else {
88            3 // その他: 3回
89        }
90    }
91}
92
93// ============================================================================
94// SelectResult - エントリ選択結果
95// ============================================================================
96
97/// LearnedDependencyProvider の選択結果
98#[derive(Debug, Clone)]
99pub enum SelectResult {
100    /// 学習済みグラフをそのまま使用(LLM 不要)
101    UseLearnedGraph {
102        /// 構築済みグラフ(Box でサイズ最適化)
103        graph: Box<DependencyGraph>,
104        /// 使用した LoRA(あれば)
105        lora: Option<LoraConfig>,
106    },
107
108    /// LLM を使用(ヒント/LoRA 付き)
109    UseLlm {
110        /// 使用する LoRA(None = Base Model)
111        lora: Option<LoraConfig>,
112        /// ヒントとして使用する学習済み順序(部分一致時)
113        hint: Option<LearnedActionOrder>,
114        /// 投票回数(1 または 3)
115        vote_count: u8,
116        /// 一致率
117        match_rate: f64,
118    },
119}
120
121impl SelectResult {
122    /// LLM 呼び出しが必要か
123    pub fn needs_llm(&self) -> bool {
124        matches!(self, SelectResult::UseLlm { .. })
125    }
126
127    /// 投票回数を取得(UseLearnedGraph の場合は 0)
128    pub fn vote_count(&self) -> u8 {
129        match self {
130            SelectResult::UseLearnedGraph { .. } => 0,
131            SelectResult::UseLlm { vote_count, .. } => *vote_count,
132        }
133    }
134
135    /// LoRA 設定を取得
136    pub fn lora(&self) -> Option<&LoraConfig> {
137        match self {
138            SelectResult::UseLearnedGraph { lora, .. } => lora.as_ref(),
139            SelectResult::UseLlm { lora, .. } => lora.as_ref(),
140        }
141    }
142}
143
144// ============================================================================
145// Core Types
146// ============================================================================
147
148/// アクション間の依存エッジ
149///
150/// `from` アクションの成功後に `to` アクションを実行可能。
151/// `confidence` は LLM の確信度(0.0〜1.0)。
152#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct DependencyEdge {
154    /// 元アクション名
155    pub from: String,
156    /// 先アクション名
157    pub to: String,
158    /// 確信度(0.0〜1.0)
159    pub confidence: f64,
160}
161
162impl DependencyEdge {
163    pub fn new(from: impl Into<String>, to: impl Into<String>, confidence: f64) -> Self {
164        Self {
165            from: from.into(),
166            to: to.into(),
167            confidence: confidence.clamp(0.0, 1.0),
168        }
169    }
170}
171
172/// アクション依存グラフ
173///
174/// タスクに対して LLM が生成したアクション間の依存関係。
175/// 探索時はこのグラフに従ってアクションを選択する。
176#[derive(Debug, Clone, Default, Serialize, Deserialize)]
177pub struct DependencyGraph {
178    /// 依存エッジ(from → to の関係)
179    edges: Vec<DependencyEdge>,
180    /// 開始可能なアクション(依存なしで実行可能)
181    start_nodes: HashSet<String>,
182    /// 終端アクション(これが成功したらタスク完了の可能性)
183    terminal_nodes: HashSet<String>,
184    /// タスク説明(デバッグ用)
185    task: String,
186    /// 利用可能なアクション一覧(検証用)
187    available_actions: Vec<String>,
188    /// アクションごとのパラメータバリアント(action → (key, values))
189    ///
190    /// 例: "Move" → ("target", ["north", "south", "east", "west"])
191    #[serde(default)]
192    param_variants: HashMap<String, (String, Vec<String>)>,
193    /// Discover(NodeExpand)アクションの順序(キャッシュ用)
194    #[serde(default)]
195    discover_order: Vec<String>,
196    /// NotDiscover(NodeStateChange)アクションの順序(キャッシュ用)
197    #[serde(default)]
198    not_discover_order: Vec<String>,
199    /// 学習用記録(LLM 推論を学習データとして保存)
200    #[serde(skip)]
201    learn_record: Option<crate::learn::DependencyGraphRecord>,
202}
203
204impl DependencyGraph {
205    /// 空のグラフを作成
206    pub fn new() -> Self {
207        Self::default()
208    }
209
210    /// Builder パターンでグラフを構築
211    pub fn builder() -> DependencyGraphBuilder {
212        DependencyGraphBuilder::new()
213    }
214
215    // ------------------------------------------------------------------------
216    // Query API
217    // ------------------------------------------------------------------------
218
219    /// 指定アクションの後に実行可能なアクションを取得
220    ///
221    /// 確信度順にソートして返す。
222    pub fn valid_next_actions(&self, current_action: &str) -> Vec<String> {
223        let mut edges: Vec<_> = self
224            .edges
225            .iter()
226            .filter(|e| e.from == current_action)
227            .collect();
228
229        // 確信度が高い順にソート(NaN対策で unwrap_or を使用)
230        edges.sort_by(|a, b| {
231            b.confidence
232                .partial_cmp(&a.confidence)
233                .unwrap_or(std::cmp::Ordering::Equal)
234        });
235
236        edges.iter().map(|e| e.to.clone()).collect()
237    }
238
239    /// 開始可能なアクションを取得
240    ///
241    /// アルファベット順にソートして一貫した順序を保証。
242    pub fn start_actions(&self) -> Vec<String> {
243        let mut actions: Vec<_> = self.start_nodes.iter().cloned().collect();
244        actions.sort();
245        actions
246    }
247
248    /// 終端アクションを取得
249    ///
250    /// アルファベット順にソートして一貫した順序を保証。
251    pub fn terminal_actions(&self) -> Vec<String> {
252        let mut actions: Vec<_> = self.terminal_nodes.iter().cloned().collect();
253        actions.sort();
254        actions
255    }
256
257    /// 指定アクションが終端か
258    pub fn is_terminal(&self, action: &str) -> bool {
259        self.terminal_nodes.contains(action)
260    }
261
262    /// 指定アクションが開始ノードか
263    pub fn is_start(&self, action: &str) -> bool {
264        self.start_nodes.contains(action)
265    }
266
267    /// 指定アクションから指定アクションへの遷移が有効か
268    pub fn can_transition(&self, from: &str, to: &str) -> bool {
269        self.edges.iter().any(|e| e.from == from && e.to == to)
270    }
271
272    /// 遷移の確信度を取得
273    pub fn transition_confidence(&self, from: &str, to: &str) -> Option<f64> {
274        self.edges
275            .iter()
276            .find(|e| e.from == from && e.to == to)
277            .map(|e| e.confidence)
278    }
279
280    /// 全エッジを取得
281    pub fn edges(&self) -> &[DependencyEdge] {
282        &self.edges
283    }
284
285    /// タスク説明を取得
286    pub fn task(&self) -> &str {
287        &self.task
288    }
289
290    /// 利用可能なアクション一覧を取得
291    pub fn available_actions(&self) -> &[String] {
292        &self.available_actions
293    }
294
295    /// 指定アクションのパラメータバリアントを取得
296    pub fn param_variants(&self, action: &str) -> Option<(&str, &[String])> {
297        self.param_variants
298            .get(action)
299            .map(|(key, values)| (key.as_str(), values.as_slice()))
300    }
301
302    /// 全パラメータバリアントを取得
303    pub fn all_param_variants(&self) -> &HashMap<String, (String, Vec<String>)> {
304        &self.param_variants
305    }
306
307    // ------------------------------------------------------------------------
308    // Action Order (for caching)
309    // ------------------------------------------------------------------------
310
311    /// Discover アクションの順序を取得
312    pub fn discover_order(&self) -> &[String] {
313        &self.discover_order
314    }
315
316    /// NotDiscover アクションの順序を取得
317    pub fn not_discover_order(&self) -> &[String] {
318        &self.not_discover_order
319    }
320
321    /// アクション順序を設定
322    pub fn set_action_order(&mut self, discover: Vec<String>, not_discover: Vec<String>) {
323        self.discover_order = discover;
324        self.not_discover_order = not_discover;
325    }
326
327    /// アクション順序が設定されているか
328    pub fn has_action_order(&self) -> bool {
329        !self.discover_order.is_empty() || !self.not_discover_order.is_empty()
330    }
331
332    // ------------------------------------------------------------------------
333    // Learning Record
334    // ------------------------------------------------------------------------
335
336    /// 学習用記録を設定
337    pub fn set_learn_record(&mut self, record: crate::learn::DependencyGraphRecord) {
338        self.learn_record = Some(record);
339    }
340
341    /// 学習用記録を取得
342    pub fn learn_record(&self) -> Option<&crate::learn::DependencyGraphRecord> {
343        self.learn_record.as_ref()
344    }
345
346    /// 学習用記録を取り出し(所有権移動)
347    pub fn take_learn_record(&mut self) -> Option<crate::learn::DependencyGraphRecord> {
348        self.learn_record.take()
349    }
350
351    // ------------------------------------------------------------------------
352    // Validation
353    // ------------------------------------------------------------------------
354
355    /// グラフが有効か検証
356    ///
357    /// - 少なくとも1つの開始ノードがある
358    /// - 少なくとも1つの終端ノードがある
359    /// - 全エッジのアクションが available_actions に含まれる
360    /// - 全開始ノードが available_actions に含まれる
361    /// - 全終端ノードが available_actions に含まれる
362    pub fn validate(&self) -> Result<(), DependencyGraphError> {
363        if self.start_nodes.is_empty() {
364            return Err(DependencyGraphError::NoStartNodes);
365        }
366
367        if self.terminal_nodes.is_empty() {
368            return Err(DependencyGraphError::NoTerminalNodes);
369        }
370
371        // 開始ノードが available_actions に含まれるか
372        for node in &self.start_nodes {
373            if !self.available_actions.contains(node) {
374                return Err(DependencyGraphError::UnknownAction(node.clone()));
375            }
376        }
377
378        // 終端ノードが available_actions に含まれるか
379        for node in &self.terminal_nodes {
380            if !self.available_actions.contains(node) {
381                return Err(DependencyGraphError::UnknownAction(node.clone()));
382            }
383        }
384
385        // エッジのアクションが available_actions に含まれるか
386        for edge in &self.edges {
387            if !self.available_actions.contains(&edge.from) {
388                return Err(DependencyGraphError::UnknownAction(edge.from.clone()));
389            }
390            if !self.available_actions.contains(&edge.to) {
391                return Err(DependencyGraphError::UnknownAction(edge.to.clone()));
392            }
393        }
394
395        Ok(())
396    }
397
398    // ------------------------------------------------------------------------
399    // Visualization
400    // ------------------------------------------------------------------------
401
402    /// グラフを Mermaid 形式で出力
403    pub fn to_mermaid(&self) -> String {
404        let mut lines = vec!["graph LR".to_string()];
405
406        for edge in &self.edges {
407            let label = format!("{:.0}%", edge.confidence * 100.0);
408            lines.push(format!("    {} -->|{}| {}", edge.from, label, edge.to));
409        }
410
411        // 開始ノードをスタイル
412        for start in &self.start_nodes {
413            lines.push(format!("    style {} fill:#9f9", start));
414        }
415
416        // 終端ノードをスタイル
417        for terminal in &self.terminal_nodes {
418            lines.push(format!("    style {} fill:#f99", terminal));
419        }
420
421        lines.join("\n")
422    }
423}
424
425/// グラフ構築エラー
426#[derive(Debug, Clone, thiserror::Error)]
427pub enum DependencyGraphError {
428    #[error("No start nodes defined")]
429    NoStartNodes,
430
431    #[error("No terminal nodes defined")]
432    NoTerminalNodes,
433
434    #[error("Unknown action: {0}")]
435    UnknownAction(String),
436
437    #[error("Parse error: {0}")]
438    ParseError(String),
439
440    #[error("LLM error: {0}")]
441    LlmError(String),
442}
443
444// ============================================================================
445// Provider Trait
446// ============================================================================
447
448/// DependencyGraph を提供するトレイト
449///
450/// このトレイトを実装することで、様々な方法で DependencyGraph を生成できる:
451/// - LLM による動的生成(BatchInvoker が実装)
452/// - 静的な事前定義グラフ
453/// - テスト用のモック
454///
455/// # Example
456///
457/// ```ignore
458/// use swarm_engine_core::exploration::{DependencyGraph, DependencyGraphProvider};
459///
460/// struct StaticProvider {
461///     graph: DependencyGraph,
462/// }
463///
464/// impl DependencyGraphProvider for StaticProvider {
465///     fn provide_graph(&self, _task: &str, _actions: &[String]) -> Option<DependencyGraph> {
466///         Some(self.graph.clone())
467///     }
468/// }
469/// ```
470pub trait DependencyGraphProvider: Send + Sync {
471    /// タスクとアクション一覧から DependencyGraph を生成
472    ///
473    /// # Arguments
474    /// - `task`: タスクの説明(goal)
475    /// - `available_actions`: 利用可能なアクション名の一覧
476    ///
477    /// # Returns
478    /// - `Some(DependencyGraph)`: グラフが生成された場合
479    /// - `None`: 生成をスキップする場合(LLM エラー等)
480    fn provide_graph(&self, task: &str, available_actions: &[String]) -> Option<DependencyGraph>;
481
482    /// 最適なエントリを選択し、LLM 呼び出しのヒントを返す
483    ///
484    /// `provide_graph()` が `None` を返した場合に、BatchInvoker へ渡すヒントを取得する。
485    /// デフォルト実装は `None` を返す(ヒントなし)。
486    ///
487    /// # Returns
488    /// - `Some(SelectResult::UseLlm { .. })`: LLM 呼び出しに使用するパラメータ
489    /// - `None`: ヒントなし(デフォルトパラメータを使用)
490    fn select(&self, _task: &str, _available_actions: &[String]) -> Option<SelectResult> {
491        None
492    }
493}
494
495// ============================================================================
496// Builder
497// ============================================================================
498
499/// DependencyGraph のビルダー
500#[derive(Debug, Clone, Default)]
501pub struct DependencyGraphBuilder {
502    edges: Vec<DependencyEdge>,
503    start_nodes: HashSet<String>,
504    terminal_nodes: HashSet<String>,
505    task: String,
506    available_actions: Vec<String>,
507    param_variants: HashMap<String, (String, Vec<String>)>,
508    discover_order: Vec<String>,
509    not_discover_order: Vec<String>,
510}
511
512impl DependencyGraphBuilder {
513    pub fn new() -> Self {
514        Self::default()
515    }
516
517    /// タスク説明を設定
518    pub fn task(mut self, task: impl Into<String>) -> Self {
519        self.task = task.into();
520        self
521    }
522
523    /// 利用可能なアクションを設定
524    pub fn available_actions<I, S>(mut self, actions: I) -> Self
525    where
526        I: IntoIterator<Item = S>,
527        S: Into<String>,
528    {
529        self.available_actions = actions.into_iter().map(|s| s.into()).collect();
530        self
531    }
532
533    /// エッジを追加
534    pub fn edge(mut self, from: impl Into<String>, to: impl Into<String>, confidence: f64) -> Self {
535        self.edges.push(DependencyEdge::new(from, to, confidence));
536        self
537    }
538
539    /// 開始ノードを追加
540    pub fn start_node(mut self, action: impl Into<String>) -> Self {
541        self.start_nodes.insert(action.into());
542        self
543    }
544
545    /// 複数の開始ノードを追加
546    pub fn start_nodes<I, S>(mut self, actions: I) -> Self
547    where
548        I: IntoIterator<Item = S>,
549        S: Into<String>,
550    {
551        self.start_nodes
552            .extend(actions.into_iter().map(|s| s.into()));
553        self
554    }
555
556    /// 終端ノードを追加
557    pub fn terminal_node(mut self, action: impl Into<String>) -> Self {
558        self.terminal_nodes.insert(action.into());
559        self
560    }
561
562    /// 複数の終端ノードを追加
563    pub fn terminal_nodes<I, S>(mut self, actions: I) -> Self
564    where
565        I: IntoIterator<Item = S>,
566        S: Into<String>,
567    {
568        self.terminal_nodes
569            .extend(actions.into_iter().map(|s| s.into()));
570        self
571    }
572
573    /// パラメータバリアントを追加
574    ///
575    /// 指定アクションの後続ノード展開時に、各バリアントごとにノードを生成する。
576    pub fn param_variants<I, S>(
577        mut self,
578        action: impl Into<String>,
579        key: impl Into<String>,
580        values: I,
581    ) -> Self
582    where
583        I: IntoIterator<Item = S>,
584        S: Into<String>,
585    {
586        self.param_variants.insert(
587            action.into(),
588            (key.into(), values.into_iter().map(|s| s.into()).collect()),
589        );
590        self
591    }
592
593    /// アクション順序を設定(discover/not_discover)
594    ///
595    /// LearnedDependencyProvider 等で学習済みアクション順序を設定する際に使用。
596    pub fn with_orders(
597        mut self,
598        discover_order: Vec<String>,
599        not_discover_order: Vec<String>,
600    ) -> Self {
601        self.discover_order = discover_order;
602        self.not_discover_order = not_discover_order;
603        self
604    }
605
606    /// グラフを構築
607    pub fn build(self) -> DependencyGraph {
608        DependencyGraph {
609            edges: self.edges,
610            start_nodes: self.start_nodes,
611            terminal_nodes: self.terminal_nodes,
612            task: self.task,
613            available_actions: self.available_actions,
614            param_variants: self.param_variants,
615            discover_order: self.discover_order,
616            not_discover_order: self.not_discover_order,
617            learn_record: None,
618        }
619    }
620
621    /// グラフを構築してバリデーション
622    pub fn build_validated(self) -> Result<DependencyGraph, DependencyGraphError> {
623        let graph = self.build();
624        graph.validate()?;
625        Ok(graph)
626    }
627}
628
629// ============================================================================
630// LLM Response Parsing
631// ============================================================================
632
633/// LLM からの依存グラフ生成レスポンス
634///
635/// LLM が JSON で返すことを想定。
636#[derive(Debug, Clone, Serialize, Deserialize)]
637pub struct LlmDependencyResponse {
638    /// 依存エッジ
639    pub edges: Vec<LlmEdge>,
640    /// 開始アクション
641    pub start: Vec<String>,
642    /// 終端アクション
643    pub terminal: Vec<String>,
644    /// 推論理由(オプション)
645    #[serde(default)]
646    pub reasoning: Option<String>,
647}
648
649/// LLM レスポンス内のエッジ
650#[derive(Debug, Clone, Serialize, Deserialize)]
651pub struct LlmEdge {
652    pub from: String,
653    pub to: String,
654    pub confidence: f64,
655}
656
657impl LlmDependencyResponse {
658    /// DependencyGraph に変換
659    pub fn into_graph(
660        self,
661        task: impl Into<String>,
662        available_actions: Vec<String>,
663    ) -> DependencyGraph {
664        let mut builder = DependencyGraphBuilder::new()
665            .task(task)
666            .available_actions(available_actions)
667            .start_nodes(self.start)
668            .terminal_nodes(self.terminal);
669
670        for edge in self.edges {
671            builder = builder.edge(edge.from, edge.to, edge.confidence);
672        }
673
674        builder.build()
675    }
676
677    /// テキストからパース(矢印形式 or JSON)
678    ///
679    /// 優先順位:
680    /// 1. 矢印形式(A -> B -> C)
681    /// 2. JSON形式
682    pub fn parse(text: &str) -> Result<Self, DependencyGraphError> {
683        // 1. 矢印形式をパース
684        if let Some(response) = Self::parse_arrow_format(text) {
685            return Ok(response);
686        }
687
688        // 2. JSON形式を試行
689        if let Ok(parsed) = serde_json::from_str(text) {
690            return Ok(parsed);
691        }
692
693        // 3. テキストからJSONを抽出して再試行
694        if let Some(json) = Self::extract_json(text) {
695            serde_json::from_str(&json).map_err(|e| DependencyGraphError::ParseError(e.to_string()))
696        } else {
697            Err(DependencyGraphError::ParseError(format!(
698                "No valid format found in response: {}",
699                text.chars().take(200).collect::<String>()
700            )))
701        }
702    }
703
704    /// アクション順序をパース(矢印形式 or 番号リスト形式)
705    ///
706    /// 対応形式:
707    /// - 矢印: "Grep -> Read -> List"
708    /// - 番号: "1. Grep 2. Read 3. List"
709    /// - コンマ: "Grep, Read, List"
710    fn parse_arrow_format(text: &str) -> Option<Self> {
711        // 矢印形式を試行
712        if let Some(result) = Self::parse_arrow_only(text) {
713            return Some(result);
714        }
715
716        // 番号リスト形式を試行(例: "1. Read 2. List 3. Grep")
717        if let Some(result) = Self::parse_numbered_list(text) {
718            return Some(result);
719        }
720
721        None
722    }
723
724    /// 矢印形式のみをパース
725    fn parse_arrow_only(text: &str) -> Option<Self> {
726        let normalized = text.replace('→', "->");
727
728        // 矢印を含む行を探す
729        let arrow_line = normalized.lines().find(|line| line.contains("->"))?;
730
731        let parts: Vec<&str> = arrow_line.split("->").collect();
732        if parts.len() < 2 {
733            return None;
734        }
735
736        // 最後の単語のみを抽出("So the order is Read" -> "Read")
737        let actions_in_order: Vec<String> = parts
738            .iter()
739            .filter_map(|part| {
740                let trimmed = part.trim();
741                // 最後の単語を取得
742                let last_word = trimmed.split_whitespace().last()?;
743                // 英字のみを抽出
744                let action: String = last_word.chars().filter(|c| c.is_alphabetic()).collect();
745                if action.is_empty() {
746                    None
747                } else {
748                    Some(action)
749                }
750            })
751            .collect();
752
753        if actions_in_order.len() < 2 {
754            return None;
755        }
756
757        Self::build_response(actions_in_order)
758    }
759
760    /// 番号リスト形式をパース(例: "1. Read 2. List 3. Grep")
761    fn parse_numbered_list(text: &str) -> Option<Self> {
762        let mut actions_in_order: Vec<String> = Vec::new();
763
764        // "1." "2." "3." などを探す
765        for i in 1..=10 {
766            let pattern = format!("{}.", i);
767            if let Some(pos) = text.find(&pattern) {
768                // 番号の後の単語を取得
769                let after = &text[pos + pattern.len()..];
770                if let Some(word) = after.split_whitespace().next() {
771                    // 英字のみを抽出
772                    let action: String = word.chars().filter(|c| c.is_alphabetic()).collect();
773                    if !action.is_empty() && !actions_in_order.contains(&action) {
774                        actions_in_order.push(action);
775                    }
776                }
777            }
778        }
779
780        if actions_in_order.len() < 2 {
781            return None;
782        }
783
784        Self::build_response(actions_in_order)
785    }
786
787    /// LlmDependencyResponse を構築
788    fn build_response(actions_in_order: Vec<String>) -> Option<Self> {
789        let mut edges = Vec::new();
790        for window in actions_in_order.windows(2) {
791            edges.push(LlmEdge {
792                from: window[0].clone(),
793                to: window[1].clone(),
794                confidence: 0.9,
795            });
796        }
797
798        Some(Self {
799            edges,
800            start: vec![actions_in_order.first()?.clone()],
801            terminal: vec![actions_in_order.last()?.clone()],
802            reasoning: Some("Parsed from text format".to_string()),
803        })
804    }
805
806    /// テキストからJSONオブジェクトを抽出
807    fn extract_json(text: &str) -> Option<String> {
808        // { ... } を探す(バランスを取って)
809        let start = text.find('{')?;
810        let chars: Vec<char> = text[start..].chars().collect();
811        let mut depth = 0;
812        let mut in_string = false;
813        let mut escape_next = false;
814
815        for (i, &ch) in chars.iter().enumerate() {
816            if escape_next {
817                escape_next = false;
818                continue;
819            }
820
821            match ch {
822                '\\' if in_string => escape_next = true,
823                '"' => in_string = !in_string,
824                '{' if !in_string => depth += 1,
825                '}' if !in_string => {
826                    depth -= 1;
827                    if depth == 0 {
828                        return Some(chars[..=i].iter().collect());
829                    }
830                }
831                _ => {}
832            }
833        }
834
835        None
836    }
837}
838
839// ============================================================================
840// Planner Trait
841// ============================================================================
842
843/// 依存グラフ生成プランナー trait
844///
845/// LLM を使った実装と、静的な実装の両方をサポート。
846pub trait DependencyPlanner: Send + Sync {
847    /// タスクとアクション一覧から依存グラフを生成
848    ///
849    /// 非同期処理は呼び出し側で管理(LLM 呼び出しは別レイヤー)。
850    fn plan(
851        &self,
852        task: &str,
853        available_actions: &[String],
854    ) -> Result<DependencyGraph, DependencyGraphError>;
855
856    /// プランナー名
857    fn name(&self) -> &str;
858}
859
860/// 静的な依存グラフプランナー(LLM 不使用)
861///
862/// 事前定義されたパターンから依存グラフを生成。
863/// テストや LLM なしでの動作確認に使用。
864#[derive(Debug, Clone, Default)]
865pub struct StaticDependencyPlanner {
866    /// パターン名 → グラフ のマッピング
867    patterns: HashMap<String, DependencyGraph>,
868    /// デフォルトパターン名
869    default_pattern: Option<String>,
870}
871
872impl StaticDependencyPlanner {
873    pub fn new() -> Self {
874        Self::default()
875    }
876
877    /// パターンを追加
878    pub fn with_pattern(mut self, name: impl Into<String>, graph: DependencyGraph) -> Self {
879        let name = name.into();
880        if self.default_pattern.is_none() {
881            self.default_pattern = Some(name.clone());
882        }
883        self.patterns.insert(name, graph);
884        self
885    }
886
887    /// デフォルトパターンを設定
888    pub fn with_default_pattern(mut self, name: impl Into<String>) -> Self {
889        self.default_pattern = Some(name.into());
890        self
891    }
892
893    /// ファイル探索パターンを追加(組み込み)
894    ///
895    /// Grep|List → Read のパターン。
896    pub fn with_file_exploration_pattern(self) -> Self {
897        let graph = DependencyGraph::builder()
898            .task("File exploration")
899            .available_actions(["Grep", "List", "Read"])
900            .edge("Grep", "Read", 0.95)
901            .edge("List", "Grep", 0.60)
902            .edge("List", "Read", 0.40)
903            .start_nodes(["Grep", "List"])
904            .terminal_node("Read")
905            .build();
906
907        self.with_pattern("file_exploration", graph)
908    }
909
910    /// コード検索パターンを追加(組み込み)
911    ///
912    /// Grep → Read の強いパターン。
913    pub fn with_code_search_pattern(self) -> Self {
914        let graph = DependencyGraph::builder()
915            .task("Code search")
916            .available_actions(["Grep", "Read"])
917            .edge("Grep", "Read", 0.95)
918            .start_node("Grep")
919            .terminal_node("Read")
920            .build();
921
922        self.with_pattern("code_search", graph)
923    }
924}
925
926impl DependencyPlanner for StaticDependencyPlanner {
927    fn plan(
928        &self,
929        task: &str,
930        available_actions: &[String],
931    ) -> Result<DependencyGraph, DependencyGraphError> {
932        // デフォルトパターンを使用
933        if let Some(pattern_name) = &self.default_pattern {
934            if let Some(graph) = self.patterns.get(pattern_name) {
935                let mut graph = graph.clone();
936                graph.task = task.to_string();
937                graph.available_actions = available_actions.to_vec();
938                return Ok(graph);
939            }
940        }
941
942        // パターンがない場合は単純な線形グラフを生成
943        // 全アクションを順番に実行する想定
944        if available_actions.is_empty() {
945            return Err(DependencyGraphError::NoStartNodes);
946        }
947
948        let mut builder = DependencyGraphBuilder::new()
949            .task(task)
950            .available_actions(available_actions.to_vec())
951            .start_node(&available_actions[0]);
952
953        if available_actions.len() > 1 {
954            for window in available_actions.windows(2) {
955                builder = builder.edge(&window[0], &window[1], 0.80);
956            }
957            builder = builder.terminal_node(&available_actions[available_actions.len() - 1]);
958        } else {
959            builder = builder.terminal_node(&available_actions[0]);
960        }
961
962        Ok(builder.build())
963    }
964
965    fn name(&self) -> &str {
966        "StaticDependencyPlanner"
967    }
968}
969
970// ============================================================================
971// LLM Prompt Generation
972// ============================================================================
973
974use crate::actions::ActionDef;
975
976/// LLM 向けプロンプト生成
977///
978/// 分割クエリ方式:first/lastを個別に聞いて安定した結果を得る。
979/// ActionDef の name と description を使ってヒントを生成。
980pub struct DependencyPromptGenerator;
981
982impl DependencyPromptGenerator {
983    /// 依存グラフ生成用のプロンプトを生成
984    ///
985    /// 従来の全順序プロンプト(フォールバック用)
986    pub fn generate_prompt(task: &str, actions: &[ActionDef]) -> String {
987        let actions_list = actions
988            .iter()
989            .map(|a| a.name.as_str())
990            .collect::<Vec<_>>()
991            .join(", ");
992
993        format!(
994            r#"{task}
995Steps: {actions_list}
996The very first step is:"#
997        )
998    }
999
1000    /// First step を聞くプロンプト
1001    ///
1002    /// ActionDef の description から動詞を抽出してヒントを生成。
1003    /// 例: "Search for patterns" → "Which step SEARCHES?"
1004    pub fn generate_first_prompt(_task: &str, actions: &[ActionDef]) -> String {
1005        // アルファベット順にソート(順序バイアスを軽減)
1006        let mut sorted_actions: Vec<&ActionDef> = actions.iter().collect();
1007        sorted_actions.sort_by(|a, b| a.name.cmp(&b.name));
1008
1009        let actions_list = sorted_actions
1010            .iter()
1011            .map(|a| a.name.as_str())
1012            .collect::<Vec<_>>()
1013            .join(", ");
1014
1015        // description から動詞を抽出(最初の単語を大文字化)
1016        let descriptions: Vec<String> = sorted_actions
1017            .iter()
1018            .map(|a| format!("- {}: {}", a.name, a.description))
1019            .collect();
1020        let descriptions_block = descriptions.join("\n");
1021
1022        // 最初のアクションの description から動詞を取得してヒントに
1023        let first_verb = sorted_actions
1024            .first()
1025            .map(|a| Self::extract_verb(&a.description))
1026            .unwrap_or_else(|| "CHECK".to_string());
1027
1028        format!(
1029            r#"Steps: {actions_list}
1030{descriptions_block}
1031Which step {first_verb}S first?
1032Answer:"#
1033        )
1034    }
1035
1036    /// Last step を聞くプロンプト
1037    ///
1038    /// ActionDef の name と description をそのまま渡す。
1039    /// description に "requires X first" 等の情報があれば LLM が判断できる。
1040    pub fn generate_last_prompt(_task: &str, actions: &[ActionDef]) -> String {
1041        // アルファベット順にソート(順序バイアスを軽減)
1042        let mut sorted_actions: Vec<&ActionDef> = actions.iter().collect();
1043        sorted_actions.sort_by(|a, b| a.name.cmp(&b.name));
1044
1045        let actions_list = sorted_actions
1046            .iter()
1047            .map(|a| a.name.as_str())
1048            .collect::<Vec<_>>()
1049            .join(", ");
1050
1051        let descriptions: Vec<String> = sorted_actions
1052            .iter()
1053            .map(|a| format!("- {}: {}", a.name, a.description))
1054            .collect();
1055        let descriptions_block = descriptions.join("\n");
1056
1057        format!(
1058            r#"Steps: {actions_list}
1059{descriptions_block}
1060Which step should be done last?
1061Answer:"#
1062        )
1063    }
1064
1065    /// ペア比較プロンプト(どちらが先か)
1066    pub fn generate_pair_prompt(task: &str, action_a: &str, action_b: &str) -> String {
1067        format!(
1068            r#"For {task}, which comes first: {action_a} or {action_b}?
1069Answer (one word):"#
1070        )
1071    }
1072
1073    /// description から動詞を抽出(大文字化)
1074    ///
1075    /// 例: "Search for patterns" → "SEARCH"
1076    /// 例: "Reads file contents" → "READ"
1077    fn extract_verb(description: &str) -> String {
1078        description
1079            .split_whitespace()
1080            .next()
1081            .map(|w| {
1082                // 末尾の 's' を除去("Reads" → "Read")
1083                let word = w.trim_end_matches('s').trim_end_matches('S');
1084                word.to_uppercase()
1085            })
1086            .unwrap_or_else(|| "CHECK".to_string())
1087    }
1088}
1089
1090// ============================================================================
1091// Graph Navigation Helper
1092// ============================================================================
1093
1094/// 依存グラフに基づくアクション選択ヘルパー
1095///
1096/// 現在の探索状態と依存グラフを組み合わせて、
1097/// 次に実行すべきアクションを推奨する。
1098#[derive(Debug, Clone)]
1099pub struct GraphNavigator {
1100    graph: DependencyGraph,
1101    /// 実行済みアクション(成功したもの)
1102    completed_actions: HashSet<String>,
1103}
1104
1105impl GraphNavigator {
1106    pub fn new(graph: DependencyGraph) -> Self {
1107        Self {
1108            graph,
1109            completed_actions: HashSet::new(),
1110        }
1111    }
1112
1113    /// アクションを完了としてマーク
1114    pub fn mark_completed(&mut self, action: &str) {
1115        self.completed_actions.insert(action.to_string());
1116    }
1117
1118    /// 次に実行すべきアクションを取得
1119    ///
1120    /// - まだ開始していない場合: 開始ノードを返す
1121    /// - 実行中の場合: 完了したアクションの後続を返す
1122    /// - 全て完了している場合: 空を返す
1123    pub fn suggest_next(&self) -> Vec<String> {
1124        if self.completed_actions.is_empty() {
1125            // まだ何も実行していない → 開始ノード
1126            return self.graph.start_actions();
1127        }
1128
1129        // 完了したアクションの後続を収集
1130        let mut candidates = Vec::new();
1131        for completed in &self.completed_actions {
1132            for next in self.graph.valid_next_actions(completed) {
1133                if !self.completed_actions.contains(&next) && !candidates.contains(&next) {
1134                    candidates.push(next);
1135                }
1136            }
1137        }
1138
1139        candidates
1140    }
1141
1142    /// タスクが完了したか判定
1143    ///
1144    /// 終端アクションのいずれかが完了していれば true。
1145    pub fn is_task_complete(&self) -> bool {
1146        self.graph
1147            .terminal_actions()
1148            .iter()
1149            .any(|t| self.completed_actions.contains(t))
1150    }
1151
1152    /// 進捗を取得(完了アクション数 / 全アクション数)
1153    pub fn progress(&self) -> f64 {
1154        if self.graph.available_actions.is_empty() {
1155            return 0.0;
1156        }
1157        self.completed_actions.len() as f64 / self.graph.available_actions.len() as f64
1158    }
1159
1160    /// 依存グラフへの参照を取得
1161    pub fn graph(&self) -> &DependencyGraph {
1162        &self.graph
1163    }
1164}
1165
1166// ============================================================================
1167// Graph Building Utility
1168// ============================================================================
1169
1170/// アクション順序から DependencyGraph を構築する共通関数
1171///
1172/// `LearnedDependencyProvider` で使用。
1173/// discover/not_discover の順序に基づいて線形グラフを構築する。
1174///
1175/// # Arguments
1176/// - `task`: タスク説明
1177/// - `available_actions`: 利用可能なアクション一覧
1178/// - `discover`: Discover アクションの順序
1179/// - `not_discover`: NotDiscover アクションの順序
1180///
1181/// # Returns
1182/// - `Some(DependencyGraph)`: 構築成功
1183/// - `None`: 両リストが空の場合
1184pub fn build_graph_from_action_order(
1185    task: &str,
1186    available_actions: &[String],
1187    discover: &[String],
1188    not_discover: &[String],
1189) -> Option<DependencyGraph> {
1190    // 両方空の場合は有効なグラフを構築できない
1191    if discover.is_empty() && not_discover.is_empty() {
1192        return None;
1193    }
1194
1195    let mut builder = DependencyGraphBuilder::new()
1196        .task(task)
1197        .available_actions(available_actions.iter().cloned());
1198
1199    // 開始ノード設定
1200    if !discover.is_empty() {
1201        builder = builder.start_node(&discover[0]);
1202    } else if !not_discover.is_empty() {
1203        builder = builder.start_node(&not_discover[0]);
1204    }
1205
1206    // 終端ノード設定
1207    if let Some(last) = not_discover.last() {
1208        builder = builder.terminal_node(last);
1209    } else if !discover.is_empty() {
1210        builder = builder.terminal_node(discover.last().unwrap());
1211    }
1212
1213    // Discover 間のエッジ
1214    for window in discover.windows(2) {
1215        builder = builder.edge(&window[0], &window[1], 0.9);
1216    }
1217
1218    // Discover → NotDiscover へのエッジ
1219    if !discover.is_empty() && !not_discover.is_empty() {
1220        builder = builder.edge(discover.last().unwrap(), &not_discover[0], 0.9);
1221    }
1222
1223    // NotDiscover 間のエッジ
1224    for window in not_discover.windows(2) {
1225        builder = builder.edge(&window[0], &window[1], 0.9);
1226    }
1227
1228    // discover_order / not_discover_order を設定
1229    builder = builder.with_orders(discover.to_vec(), not_discover.to_vec());
1230
1231    Some(builder.build())
1232}
1233
1234// ============================================================================
1235// LearnedDependencyProvider - 学習データからグラフを生成
1236// ============================================================================
1237
1238/// 学習済みアクション順序から DependencyGraph を生成する Provider
1239///
1240/// 複数の `LearnedActionOrder` エントリを保持し、入力アクション集合との一致率に応じて
1241/// 適切な戦略(学習済みグラフ使用 or LLM 呼び出し)を決定する。
1242///
1243/// # Example
1244///
1245/// ```ignore
1246/// use swarm_engine_core::learn::offline::LearnedActionOrder;
1247/// use swarm_engine_core::exploration::LearnedDependencyProvider;
1248///
1249/// // 単一エントリの場合
1250/// let order = LearnedActionOrder::new(
1251///     vec!["Grep".to_string(), "Read".to_string()],  // discover
1252///     vec!["Restart".to_string()],                    // not_discover
1253///     &["Grep", "Read", "Restart"].map(String::from).to_vec(),
1254/// );
1255/// let provider = LearnedDependencyProvider::new(order);
1256///
1257/// // 複数エントリの場合
1258/// let provider = LearnedDependencyProvider::with_entries(vec![order1, order2, order3]);
1259///
1260/// // select() で詳細な結果を取得
1261/// match provider.select("task", &actions) {
1262///     SelectResult::UseLearnedGraph { graph, lora } => { /* LLM 不要 */ }
1263///     SelectResult::UseLlm { hint, vote_count, .. } => { /* LLM 使用 */ }
1264/// }
1265/// ```
1266#[derive(Debug, Clone, Default)]
1267pub struct LearnedDependencyProvider {
1268    /// 学習済みエントリ(複数可)
1269    entries: Vec<LearnedActionOrder>,
1270    /// 投票戦略
1271    strategy: VotingStrategy,
1272}
1273
1274impl LearnedDependencyProvider {
1275    /// 空の Provider を作成
1276    pub fn empty() -> Self {
1277        Self::default()
1278    }
1279
1280    /// 単一エントリで作成(後方互換性)
1281    pub fn new(action_order: LearnedActionOrder) -> Self {
1282        Self {
1283            entries: vec![action_order],
1284            strategy: VotingStrategy::default(),
1285        }
1286    }
1287
1288    /// 複数エントリで作成
1289    pub fn with_entries(entries: Vec<LearnedActionOrder>) -> Self {
1290        Self {
1291            entries,
1292            strategy: VotingStrategy::default(),
1293        }
1294    }
1295
1296    /// 投票戦略を設定
1297    pub fn with_strategy(mut self, strategy: VotingStrategy) -> Self {
1298        self.strategy = strategy;
1299        self
1300    }
1301
1302    /// エントリを追加
1303    pub fn add_entry(&mut self, entry: LearnedActionOrder) {
1304        self.entries.push(entry);
1305    }
1306
1307    /// 登録されたエントリ数
1308    pub fn entry_count(&self) -> usize {
1309        self.entries.len()
1310    }
1311
1312    /// 学習済み順序を取得(後方互換性: 最初のエントリ)
1313    pub fn action_order(&self) -> Option<&LearnedActionOrder> {
1314        self.entries.first()
1315    }
1316
1317    /// 全エントリを取得
1318    pub fn entries(&self) -> &[LearnedActionOrder] {
1319        &self.entries
1320    }
1321
1322    /// 最適なエントリを選択し、適切な戦略を返す
1323    ///
1324    /// # 判定ロジック
1325    ///
1326    /// | 一致率 | LoRA | 結果 |
1327    /// |--------|------|------|
1328    /// | 100%   | any  | UseLearnedGraph(LLM不要) |
1329    /// | 80%+   | あり | UseLlm { vote_count: 1 } |
1330    /// | 80%+   | なし | UseLlm { vote_count: 3 } |
1331    /// | 60%+   | any  | UseLlm { vote_count: 3 } |
1332    /// | <60%   | any  | UseLlm { vote_count: 3, lora: None } |
1333    pub fn select(&self, task: &str, available_actions: &[String]) -> SelectResult {
1334        // 1. 完全一致を探す
1335        for entry in &self.entries {
1336            if entry.is_exact_match(available_actions) {
1337                return self.build_learned_result(task, available_actions, entry);
1338            }
1339        }
1340
1341        // 2. 最も一致率の高いエントリを探す
1342        let mut best_match: Option<(&LearnedActionOrder, f64)> = None;
1343
1344        for entry in &self.entries {
1345            let rate = entry.match_rate(available_actions);
1346            if let Some((_, best_rate)) = best_match {
1347                if rate > best_rate {
1348                    best_match = Some((entry, rate));
1349                }
1350            } else if rate > 0.0 {
1351                best_match = Some((entry, rate));
1352            }
1353        }
1354
1355        // 3. 結果を返す
1356        match best_match {
1357            Some((entry, rate)) if rate >= self.strategy.medium_threshold => {
1358                self.build_llm_result(entry, rate)
1359            }
1360            _ => self.build_fallback_result(),
1361        }
1362    }
1363
1364    /// 100% 一致時: 学習済みグラフを構築
1365    fn build_learned_result(
1366        &self,
1367        task: &str,
1368        available_actions: &[String],
1369        entry: &LearnedActionOrder,
1370    ) -> SelectResult {
1371        let graph = build_graph_from_action_order(
1372            task,
1373            available_actions,
1374            &entry.discover,
1375            &entry.not_discover,
1376        );
1377
1378        match graph {
1379            Some(g) => {
1380                tracing::info!(
1381                    discover = ?entry.discover,
1382                    not_discover = ?entry.not_discover,
1383                    lora = ?entry.lora.as_ref().map(|l| &l.name),
1384                    "Using learned action order (LLM skipped)"
1385                );
1386                SelectResult::UseLearnedGraph {
1387                    graph: Box::new(g),
1388                    lora: entry.lora.clone(),
1389                }
1390            }
1391            None => {
1392                // グラフ構築失敗時は LLM フォールバック
1393                tracing::warn!("Failed to build graph from exact match, falling back to LLM");
1394                self.build_llm_result(entry, 1.0)
1395            }
1396        }
1397    }
1398
1399    /// 部分一致時: LLM 使用結果を構築
1400    fn build_llm_result(&self, entry: &LearnedActionOrder, match_rate: f64) -> SelectResult {
1401        let vote_count = self.strategy.determine(match_rate, entry.lora.is_some());
1402
1403        tracing::debug!(
1404            match_rate = match_rate,
1405            vote_count = vote_count,
1406            has_lora = entry.lora.is_some(),
1407            "LLM invocation needed (partial match)"
1408        );
1409
1410        SelectResult::UseLlm {
1411            lora: entry.lora.clone(),
1412            hint: Some(entry.clone()),
1413            vote_count,
1414            match_rate,
1415        }
1416    }
1417
1418    /// マッチなし時: フォールバック結果
1419    fn build_fallback_result(&self) -> SelectResult {
1420        tracing::debug!("No matching entry, using base model with 3 votes");
1421
1422        SelectResult::UseLlm {
1423            lora: None,
1424            hint: None,
1425            vote_count: 3,
1426            match_rate: 0.0,
1427        }
1428    }
1429}
1430
1431impl DependencyGraphProvider for LearnedDependencyProvider {
1432    fn provide_graph(&self, task: &str, available_actions: &[String]) -> Option<DependencyGraph> {
1433        // select() を使用し、UseLearnedGraph の場合のみグラフを返す
1434        match self.select(task, available_actions) {
1435            SelectResult::UseLearnedGraph { graph, .. } => {
1436                // バリデーション(失敗時は None)
1437                graph.validate().ok()?;
1438                Some(*graph)
1439            }
1440            SelectResult::UseLlm { .. } => {
1441                // LLM フォールバックが必要
1442                None
1443            }
1444        }
1445    }
1446
1447    fn select(&self, task: &str, available_actions: &[String]) -> Option<SelectResult> {
1448        // LearnedDependencyProvider::select() を呼び出して Some でラップ
1449        Some(LearnedDependencyProvider::select(
1450            self,
1451            task,
1452            available_actions,
1453        ))
1454    }
1455}
1456
1457// ============================================================================
1458// Tests
1459// ============================================================================
1460
1461#[cfg(test)]
1462mod tests {
1463    use super::*;
1464
1465    #[test]
1466    fn test_dependency_graph_builder() {
1467        let graph = DependencyGraph::builder()
1468            .task("Find auth function")
1469            .available_actions(["Grep", "List", "Read"])
1470            .edge("Grep", "Read", 0.95)
1471            .edge("List", "Grep", 0.60)
1472            .start_nodes(["Grep", "List"])
1473            .terminal_node("Read")
1474            .build();
1475
1476        assert_eq!(graph.task(), "Find auth function");
1477        assert!(graph.is_start("Grep"));
1478        assert!(graph.is_start("List"));
1479        assert!(graph.is_terminal("Read"));
1480        assert!(graph.can_transition("Grep", "Read"));
1481        assert!(!graph.can_transition("Read", "Grep"));
1482    }
1483
1484    #[test]
1485    fn test_valid_next_actions() {
1486        let graph = DependencyGraph::builder()
1487            .available_actions(["Grep", "List", "Read"])
1488            .edge("Grep", "Read", 0.95)
1489            .edge("List", "Grep", 0.60)
1490            .edge("List", "Read", 0.40)
1491            .start_nodes(["Grep", "List"])
1492            .terminal_node("Read")
1493            .build();
1494
1495        // Grep の後は Read
1496        let next = graph.valid_next_actions("Grep");
1497        assert_eq!(next, vec!["Read"]);
1498
1499        // List の後は Grep (0.60) と Read (0.40)、確信度順
1500        let next = graph.valid_next_actions("List");
1501        assert_eq!(next, vec!["Grep", "Read"]);
1502
1503        // Read の後は何もない(終端)
1504        let next = graph.valid_next_actions("Read");
1505        assert!(next.is_empty());
1506    }
1507
1508    #[test]
1509    fn test_static_planner_file_exploration() {
1510        let planner = StaticDependencyPlanner::new().with_file_exploration_pattern();
1511
1512        let graph = planner
1513            .plan("Find auth.rs", &["Grep".to_string(), "Read".to_string()])
1514            .unwrap();
1515
1516        assert!(graph.is_start("Grep"));
1517        assert!(graph.is_terminal("Read"));
1518    }
1519
1520    #[test]
1521    fn test_graph_navigator() {
1522        let graph = DependencyGraph::builder()
1523            .available_actions(["Grep", "Read"])
1524            .edge("Grep", "Read", 0.95)
1525            .start_node("Grep")
1526            .terminal_node("Read")
1527            .build();
1528
1529        let mut nav = GraphNavigator::new(graph);
1530
1531        // 最初は Grep を推奨
1532        assert_eq!(nav.suggest_next(), vec!["Grep"]);
1533        assert!(!nav.is_task_complete());
1534
1535        // Grep 完了
1536        nav.mark_completed("Grep");
1537        assert_eq!(nav.suggest_next(), vec!["Read"]);
1538        assert!(!nav.is_task_complete());
1539
1540        // Read 完了 → タスク完了
1541        nav.mark_completed("Read");
1542        assert!(nav.is_task_complete());
1543        assert!(nav.suggest_next().is_empty());
1544    }
1545
1546    #[test]
1547    fn test_llm_response_parsing() {
1548        let json = r#"{
1549            "edges": [
1550                {"from": "Grep", "to": "Read", "confidence": 0.95}
1551            ],
1552            "start": ["Grep"],
1553            "terminal": ["Read"],
1554            "reasoning": "Search first, then read"
1555        }"#;
1556
1557        let response = LlmDependencyResponse::parse(json).unwrap();
1558        assert_eq!(response.edges.len(), 1);
1559        assert_eq!(response.start, vec!["Grep"]);
1560        assert_eq!(response.terminal, vec!["Read"]);
1561        assert!(response.reasoning.is_some());
1562
1563        let graph = response.into_graph(
1564            "Find function",
1565            vec!["Grep".to_string(), "Read".to_string()],
1566        );
1567        assert!(graph.can_transition("Grep", "Read"));
1568    }
1569
1570    #[test]
1571    fn test_mermaid_output() {
1572        let graph = DependencyGraph::builder()
1573            .available_actions(["Grep", "List", "Read"])
1574            .edge("Grep", "Read", 0.95)
1575            .edge("List", "Grep", 0.60)
1576            .start_nodes(["Grep", "List"])
1577            .terminal_node("Read")
1578            .build();
1579
1580        let mermaid = graph.to_mermaid();
1581        assert!(mermaid.contains("graph LR"));
1582        assert!(mermaid.contains("Grep -->|95%| Read"));
1583        assert!(mermaid.contains("style Read fill:#f99"));
1584    }
1585
1586    // =========================================================================
1587    // LearnedDependencyProvider Tests
1588    // =========================================================================
1589
1590    #[test]
1591    fn test_learned_action_order_hash() {
1592        let actions = vec![
1593            "Grep".to_string(),
1594            "Read".to_string(),
1595            "Restart".to_string(),
1596        ];
1597
1598        let order = LearnedActionOrder::new(
1599            vec!["Grep".to_string(), "Read".to_string()],
1600            vec!["Restart".to_string()],
1601            &actions,
1602        );
1603
1604        // 同じアクション集合(順序違い)なら同じハッシュ
1605        let actions_reordered = vec![
1606            "Restart".to_string(),
1607            "Grep".to_string(),
1608            "Read".to_string(),
1609        ];
1610        assert!(order.matches_actions(&actions_reordered));
1611
1612        // 異なるアクション集合なら不一致
1613        let actions_different = vec!["Grep".to_string(), "Read".to_string()];
1614        assert!(!order.matches_actions(&actions_different));
1615    }
1616
1617    #[test]
1618    fn test_learned_dependency_provider_cache_hit() {
1619        let actions = vec![
1620            "Grep".to_string(),
1621            "Read".to_string(),
1622            "Restart".to_string(),
1623        ];
1624
1625        let order = LearnedActionOrder::new(
1626            vec!["Grep".to_string(), "Read".to_string()],
1627            vec!["Restart".to_string()],
1628            &actions,
1629        );
1630
1631        let provider = LearnedDependencyProvider::new(order);
1632
1633        // キャッシュヒット
1634        let graph = provider.provide_graph("troubleshooting", &actions);
1635        assert!(graph.is_some());
1636
1637        let graph = graph.unwrap();
1638        assert!(graph.is_start("Grep"));
1639        assert!(graph.is_terminal("Restart"));
1640        assert!(graph.can_transition("Grep", "Read"));
1641        assert!(graph.can_transition("Read", "Restart"));
1642    }
1643
1644    #[test]
1645    fn test_learned_dependency_provider_cache_miss() {
1646        let original_actions = vec![
1647            "Grep".to_string(),
1648            "Read".to_string(),
1649            "Restart".to_string(),
1650        ];
1651
1652        let order = LearnedActionOrder::new(
1653            vec!["Grep".to_string(), "Read".to_string()],
1654            vec!["Restart".to_string()],
1655            &original_actions,
1656        );
1657
1658        let provider = LearnedDependencyProvider::new(order);
1659
1660        // 異なるアクション集合 → キャッシュミス(None)
1661        let different_actions = vec!["Grep".to_string(), "Read".to_string()];
1662        let graph = provider.provide_graph("troubleshooting", &different_actions);
1663        assert!(graph.is_none());
1664    }
1665
1666    #[test]
1667    fn test_learned_dependency_provider_discover_only() {
1668        let actions = vec!["Grep".to_string(), "Read".to_string()];
1669
1670        let order = LearnedActionOrder::new(
1671            vec!["Grep".to_string(), "Read".to_string()],
1672            vec![], // NotDiscover なし
1673            &actions,
1674        );
1675
1676        let provider = LearnedDependencyProvider::new(order);
1677        let graph = provider.provide_graph("search task", &actions);
1678        assert!(graph.is_some());
1679
1680        let graph = graph.unwrap();
1681        assert!(graph.is_start("Grep"));
1682        assert!(graph.is_terminal("Read")); // Discover の最後が Terminal
1683        assert!(graph.can_transition("Grep", "Read"));
1684    }
1685
1686    #[test]
1687    fn test_learned_dependency_provider_not_discover_only() {
1688        let actions = vec!["Restart".to_string(), "CheckStatus".to_string()];
1689
1690        let order = LearnedActionOrder::new(
1691            vec![], // Discover なし
1692            vec!["Restart".to_string(), "CheckStatus".to_string()],
1693            &actions,
1694        );
1695
1696        let provider = LearnedDependencyProvider::new(order);
1697        let graph = provider.provide_graph("ops task", &actions);
1698        assert!(graph.is_some());
1699
1700        let graph = graph.unwrap();
1701        assert!(graph.is_start("Restart")); // NotDiscover の最初が Start
1702        assert!(graph.is_terminal("CheckStatus"));
1703        assert!(graph.can_transition("Restart", "CheckStatus"));
1704    }
1705
1706    #[test]
1707    fn test_learned_dependency_provider_empty_lists() {
1708        let actions = vec!["Grep".to_string(), "Read".to_string()];
1709
1710        let order = LearnedActionOrder::new(
1711            vec![], // Discover なし
1712            vec![], // NotDiscover なし
1713            &actions,
1714        );
1715
1716        let provider = LearnedDependencyProvider::new(order);
1717        // 両方空の場合は None を返す
1718        let graph = provider.provide_graph("empty task", &actions);
1719        assert!(graph.is_none());
1720    }
1721
1722    // =========================================================================
1723    // extract_json Tests
1724    // =========================================================================
1725
1726    #[test]
1727    fn test_extract_json_simple() {
1728        let text = r#"Here is the result: {"edges": [], "start": ["A"], "terminal": ["B"]}"#;
1729        let json = LlmDependencyResponse::extract_json(text);
1730        assert!(json.is_some());
1731        let json = json.unwrap();
1732        assert!(json.starts_with('{'));
1733        assert!(json.ends_with('}'));
1734    }
1735
1736    #[test]
1737    fn test_extract_json_nested() {
1738        let text = r#"Result: {"edges": [{"from": "A", "to": "B", "confidence": 0.9}], "start": ["A"], "terminal": ["B"]}"#;
1739        let json = LlmDependencyResponse::extract_json(text);
1740        assert!(json.is_some());
1741
1742        // パースできることを確認
1743        let parsed: Result<LlmDependencyResponse, _> = serde_json::from_str(&json.unwrap());
1744        assert!(parsed.is_ok());
1745    }
1746
1747    #[test]
1748    fn test_extract_json_with_string_braces() {
1749        // 文字列内の {} は無視される
1750        let text =
1751            r#"{"edges": [], "start": ["A"], "terminal": ["B"], "reasoning": "Use {pattern}"}"#;
1752        let json = LlmDependencyResponse::extract_json(text);
1753        assert!(json.is_some());
1754        assert_eq!(json.unwrap(), text);
1755    }
1756
1757    #[test]
1758    fn test_extract_json_no_json() {
1759        let text = "This is just plain text without JSON";
1760        let json = LlmDependencyResponse::extract_json(text);
1761        assert!(json.is_none());
1762    }
1763
1764    // =========================================================================
1765    // validate() Tests
1766    // =========================================================================
1767
1768    #[test]
1769    fn test_validate_unknown_start_node() {
1770        let graph = DependencyGraph::builder()
1771            .available_actions(["Grep", "Read"])
1772            .start_node("Unknown") // available_actions に含まれない
1773            .terminal_node("Read")
1774            .build();
1775
1776        let result = graph.validate();
1777        assert!(result.is_err());
1778        assert!(matches!(
1779            result.unwrap_err(),
1780            DependencyGraphError::UnknownAction(name) if name == "Unknown"
1781        ));
1782    }
1783
1784    #[test]
1785    fn test_validate_unknown_terminal_node() {
1786        let graph = DependencyGraph::builder()
1787            .available_actions(["Grep", "Read"])
1788            .start_node("Grep")
1789            .terminal_node("Unknown") // available_actions に含まれない
1790            .build();
1791
1792        let result = graph.validate();
1793        assert!(result.is_err());
1794        assert!(matches!(
1795            result.unwrap_err(),
1796            DependencyGraphError::UnknownAction(name) if name == "Unknown"
1797        ));
1798    }
1799
1800    #[test]
1801    fn test_validate_valid_graph() {
1802        let graph = DependencyGraph::builder()
1803            .available_actions(["Grep", "Read"])
1804            .edge("Grep", "Read", 0.9)
1805            .start_node("Grep")
1806            .terminal_node("Read")
1807            .build();
1808
1809        assert!(graph.validate().is_ok());
1810    }
1811
1812    // =========================================================================
1813    // start_actions / terminal_actions Order Tests
1814    // =========================================================================
1815
1816    #[test]
1817    fn test_start_actions_sorted() {
1818        let graph = DependencyGraph::builder()
1819            .available_actions(["Zebra", "Apple", "Mango"])
1820            .start_nodes(["Zebra", "Apple", "Mango"])
1821            .terminal_node("Zebra")
1822            .build();
1823
1824        let actions = graph.start_actions();
1825        // アルファベット順にソートされる
1826        assert_eq!(actions, vec!["Apple", "Mango", "Zebra"]);
1827    }
1828
1829    #[test]
1830    fn test_terminal_actions_sorted() {
1831        let graph = DependencyGraph::builder()
1832            .available_actions(["Zebra", "Apple", "Mango"])
1833            .start_node("Apple")
1834            .terminal_nodes(["Zebra", "Apple", "Mango"])
1835            .build();
1836
1837        let actions = graph.terminal_actions();
1838        // アルファベット順にソートされる
1839        assert_eq!(actions, vec!["Apple", "Mango", "Zebra"]);
1840    }
1841
1842    // =========================================================================
1843    // VotingStrategy Tests
1844    // =========================================================================
1845
1846    #[test]
1847    fn test_voting_strategy_exact_match() {
1848        let strategy = VotingStrategy::default();
1849        assert_eq!(strategy.determine(1.0, true), 0);
1850        assert_eq!(strategy.determine(1.0, false), 0);
1851    }
1852
1853    #[test]
1854    fn test_voting_strategy_high_with_lora() {
1855        let strategy = VotingStrategy::default();
1856        assert_eq!(strategy.determine(0.85, true), 1);
1857        assert_eq!(strategy.determine(0.80, true), 1);
1858    }
1859
1860    #[test]
1861    fn test_voting_strategy_high_without_lora() {
1862        let strategy = VotingStrategy::default();
1863        assert_eq!(strategy.determine(0.85, false), 3);
1864    }
1865
1866    #[test]
1867    fn test_voting_strategy_medium() {
1868        let strategy = VotingStrategy::default();
1869        assert_eq!(strategy.determine(0.65, true), 3);
1870        assert_eq!(strategy.determine(0.60, true), 3);
1871    }
1872
1873    #[test]
1874    fn test_voting_strategy_low() {
1875        let strategy = VotingStrategy::default();
1876        assert_eq!(strategy.determine(0.5, true), 3);
1877        assert_eq!(strategy.determine(0.5, false), 3);
1878    }
1879
1880    // =========================================================================
1881    // LearnedDependencyProvider::select() Tests
1882    // =========================================================================
1883
1884    #[test]
1885    fn test_select_exact_match_returns_learned_graph() {
1886        let actions = vec![
1887            "CheckStatus".to_string(),
1888            "ReadLogs".to_string(),
1889            "Restart".to_string(),
1890        ];
1891
1892        let order = LearnedActionOrder::new(
1893            vec!["CheckStatus".to_string(), "ReadLogs".to_string()],
1894            vec!["Restart".to_string()],
1895            &actions,
1896        );
1897
1898        let provider = LearnedDependencyProvider::new(order);
1899        let result = provider.select("test task", &actions);
1900
1901        assert!(!result.needs_llm());
1902        assert_eq!(result.vote_count(), 0);
1903        assert!(matches!(result, SelectResult::UseLearnedGraph { .. }));
1904    }
1905
1906    #[test]
1907    fn test_select_no_match_returns_llm_fallback() {
1908        let order = LearnedActionOrder::new(
1909            vec!["A".to_string(), "B".to_string()],
1910            vec!["C".to_string()],
1911            &["A".to_string(), "B".to_string(), "C".to_string()],
1912        );
1913
1914        let provider = LearnedDependencyProvider::new(order);
1915        let result = provider.select("test task", &["X".to_string(), "Y".to_string()]);
1916
1917        assert!(result.needs_llm());
1918        assert_eq!(result.vote_count(), 3);
1919        assert!(result.lora().is_none());
1920        assert!(matches!(
1921            result,
1922            SelectResult::UseLlm {
1923                hint: None,
1924                match_rate,
1925                ..
1926            } if match_rate == 0.0
1927        ));
1928    }
1929
1930    #[test]
1931    fn test_select_partial_match_with_lora_returns_1_vote() {
1932        use crate::types::LoraConfig;
1933
1934        let lora = LoraConfig {
1935            id: 1,
1936            name: Some("test-lora".to_string()),
1937            scale: 1.0,
1938        };
1939
1940        // 5 アクションを登録
1941        let all_actions: Vec<String> = ["A", "B", "C", "D", "E"]
1942            .iter()
1943            .map(|s| s.to_string())
1944            .collect();
1945
1946        let order = LearnedActionOrder::new(
1947            vec![
1948                "A".to_string(),
1949                "B".to_string(),
1950                "C".to_string(),
1951                "D".to_string(),
1952            ],
1953            vec!["E".to_string()],
1954            &all_actions,
1955        )
1956        .with_lora(lora);
1957
1958        let provider = LearnedDependencyProvider::new(order);
1959
1960        // 4/5 = 80% マッチ
1961        let query_actions: Vec<String> =
1962            ["A", "B", "C", "D"].iter().map(|s| s.to_string()).collect();
1963        let result = provider.select("test task", &query_actions);
1964
1965        assert!(result.needs_llm());
1966        // Jaccard: 4/5 = 0.8, LoRA あり → 1 vote
1967        assert_eq!(result.vote_count(), 1);
1968        assert!(result.lora().is_some());
1969    }
1970
1971    #[test]
1972    fn test_select_empty_provider_returns_fallback() {
1973        let provider = LearnedDependencyProvider::empty();
1974        let result = provider.select("test task", &["A".to_string()]);
1975
1976        assert!(result.needs_llm());
1977        assert_eq!(result.vote_count(), 3);
1978        assert!(result.lora().is_none());
1979    }
1980
1981    #[test]
1982    fn test_select_multiple_entries_best_match() {
1983        use crate::types::LoraConfig;
1984
1985        // エントリ 1: A, B, C(LoRA なし)
1986        let order1 = LearnedActionOrder::new(
1987            vec!["A".to_string(), "B".to_string()],
1988            vec!["C".to_string()],
1989            &["A".to_string(), "B".to_string(), "C".to_string()],
1990        );
1991
1992        // エントリ 2: A, B, D(LoRA あり)
1993        let lora = LoraConfig {
1994            id: 2,
1995            name: Some("better-lora".to_string()),
1996            scale: 1.0,
1997        };
1998        let order2 = LearnedActionOrder::new(
1999            vec!["A".to_string(), "B".to_string()],
2000            vec!["D".to_string()],
2001            &["A".to_string(), "B".to_string(), "D".to_string()],
2002        )
2003        .with_lora(lora);
2004
2005        let provider = LearnedDependencyProvider::with_entries(vec![order1, order2]);
2006
2007        // A, B, D でクエリ → エントリ 2 が完全一致
2008        let query = vec!["A".to_string(), "B".to_string(), "D".to_string()];
2009        let result = provider.select("test task", &query);
2010
2011        assert!(!result.needs_llm());
2012        assert!(matches!(
2013            result,
2014            SelectResult::UseLearnedGraph { lora: Some(l), .. } if l.name == Some("better-lora".to_string())
2015        ));
2016    }
2017
2018    #[test]
2019    fn test_provide_graph_exact_match_via_select() {
2020        let actions = vec![
2021            "CheckStatus".to_string(),
2022            "ReadLogs".to_string(),
2023            "Restart".to_string(),
2024        ];
2025
2026        let order = LearnedActionOrder::new(
2027            vec!["CheckStatus".to_string(), "ReadLogs".to_string()],
2028            vec!["Restart".to_string()],
2029            &actions,
2030        );
2031
2032        let provider = LearnedDependencyProvider::new(order);
2033        let graph = provider.provide_graph("test task", &actions);
2034
2035        assert!(graph.is_some());
2036        let graph = graph.unwrap();
2037        assert!(graph.is_start("CheckStatus"));
2038        assert!(graph.is_terminal("Restart"));
2039    }
2040
2041    #[test]
2042    fn test_provide_graph_no_match_returns_none() {
2043        let order = LearnedActionOrder::new(
2044            vec!["A".to_string(), "B".to_string()],
2045            vec!["C".to_string()],
2046            &["A".to_string(), "B".to_string(), "C".to_string()],
2047        );
2048
2049        let provider = LearnedDependencyProvider::new(order);
2050        let graph = provider.provide_graph("test task", &["X".to_string(), "Y".to_string()]);
2051
2052        assert!(graph.is_none());
2053    }
2054
2055    #[test]
2056    fn test_provide_graph_partial_match_returns_none() {
2057        let order = LearnedActionOrder::new(
2058            vec![
2059                "A".to_string(),
2060                "B".to_string(),
2061                "C".to_string(),
2062                "D".to_string(),
2063            ],
2064            vec!["E".to_string()],
2065            &[
2066                "A".to_string(),
2067                "B".to_string(),
2068                "C".to_string(),
2069                "D".to_string(),
2070                "E".to_string(),
2071            ],
2072        );
2073
2074        let provider = LearnedDependencyProvider::new(order);
2075
2076        // 部分一致 (< 100%) → None
2077        let graph = provider.provide_graph(
2078            "test task",
2079            &[
2080                "A".to_string(),
2081                "B".to_string(),
2082                "C".to_string(),
2083                "D".to_string(),
2084            ],
2085        );
2086
2087        assert!(graph.is_none());
2088    }
2089}