1use std::collections::{HashMap, HashSet};
43
44use serde::{Deserialize, Serialize};
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct DependencyEdge {
56 pub from: String,
58 pub to: String,
60 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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
79pub struct DependencyGraph {
80 edges: Vec<DependencyEdge>,
82 start_nodes: HashSet<String>,
84 terminal_nodes: HashSet<String>,
86 task: String,
88 available_actions: Vec<String>,
90 #[serde(default)]
94 param_variants: HashMap<String, (String, Vec<String>)>,
95 #[serde(default)]
97 discover_order: Vec<String>,
98 #[serde(default)]
100 not_discover_order: Vec<String>,
101 #[serde(skip)]
103 learn_record: Option<crate::learn::DependencyGraphRecord>,
104}
105
106impl DependencyGraph {
107 pub fn new() -> Self {
109 Self::default()
110 }
111
112 pub fn builder() -> DependencyGraphBuilder {
114 DependencyGraphBuilder::new()
115 }
116
117 pub fn valid_next_actions(&self, current_action: &str) -> Vec<String> {
125 let mut edges: Vec<_> = self
126 .edges
127 .iter()
128 .filter(|e| e.from == current_action)
129 .collect();
130
131 edges.sort_by(|a, b| {
133 b.confidence
134 .partial_cmp(&a.confidence)
135 .unwrap_or(std::cmp::Ordering::Equal)
136 });
137
138 edges.iter().map(|e| e.to.clone()).collect()
139 }
140
141 pub fn start_actions(&self) -> Vec<String> {
145 let mut actions: Vec<_> = self.start_nodes.iter().cloned().collect();
146 actions.sort();
147 actions
148 }
149
150 pub fn terminal_actions(&self) -> Vec<String> {
154 let mut actions: Vec<_> = self.terminal_nodes.iter().cloned().collect();
155 actions.sort();
156 actions
157 }
158
159 pub fn is_terminal(&self, action: &str) -> bool {
161 self.terminal_nodes.contains(action)
162 }
163
164 pub fn is_start(&self, action: &str) -> bool {
166 self.start_nodes.contains(action)
167 }
168
169 pub fn can_transition(&self, from: &str, to: &str) -> bool {
171 self.edges.iter().any(|e| e.from == from && e.to == to)
172 }
173
174 pub fn transition_confidence(&self, from: &str, to: &str) -> Option<f64> {
176 self.edges
177 .iter()
178 .find(|e| e.from == from && e.to == to)
179 .map(|e| e.confidence)
180 }
181
182 pub fn edges(&self) -> &[DependencyEdge] {
184 &self.edges
185 }
186
187 pub fn task(&self) -> &str {
189 &self.task
190 }
191
192 pub fn available_actions(&self) -> &[String] {
194 &self.available_actions
195 }
196
197 pub fn param_variants(&self, action: &str) -> Option<(&str, &[String])> {
199 self.param_variants
200 .get(action)
201 .map(|(key, values)| (key.as_str(), values.as_slice()))
202 }
203
204 pub fn all_param_variants(&self) -> &HashMap<String, (String, Vec<String>)> {
206 &self.param_variants
207 }
208
209 pub fn discover_order(&self) -> &[String] {
215 &self.discover_order
216 }
217
218 pub fn not_discover_order(&self) -> &[String] {
220 &self.not_discover_order
221 }
222
223 pub fn set_action_order(&mut self, discover: Vec<String>, not_discover: Vec<String>) {
225 self.discover_order = discover;
226 self.not_discover_order = not_discover;
227 }
228
229 pub fn has_action_order(&self) -> bool {
231 !self.discover_order.is_empty() || !self.not_discover_order.is_empty()
232 }
233
234 pub fn set_learn_record(&mut self, record: crate::learn::DependencyGraphRecord) {
240 self.learn_record = Some(record);
241 }
242
243 pub fn learn_record(&self) -> Option<&crate::learn::DependencyGraphRecord> {
245 self.learn_record.as_ref()
246 }
247
248 pub fn take_learn_record(&mut self) -> Option<crate::learn::DependencyGraphRecord> {
250 self.learn_record.take()
251 }
252
253 pub fn validate(&self) -> Result<(), DependencyGraphError> {
265 if self.start_nodes.is_empty() {
266 return Err(DependencyGraphError::NoStartNodes);
267 }
268
269 if self.terminal_nodes.is_empty() {
270 return Err(DependencyGraphError::NoTerminalNodes);
271 }
272
273 for node in &self.start_nodes {
275 if !self.available_actions.contains(node) {
276 return Err(DependencyGraphError::UnknownAction(node.clone()));
277 }
278 }
279
280 for node in &self.terminal_nodes {
282 if !self.available_actions.contains(node) {
283 return Err(DependencyGraphError::UnknownAction(node.clone()));
284 }
285 }
286
287 for edge in &self.edges {
289 if !self.available_actions.contains(&edge.from) {
290 return Err(DependencyGraphError::UnknownAction(edge.from.clone()));
291 }
292 if !self.available_actions.contains(&edge.to) {
293 return Err(DependencyGraphError::UnknownAction(edge.to.clone()));
294 }
295 }
296
297 Ok(())
298 }
299
300 pub fn to_mermaid(&self) -> String {
306 let mut lines = vec!["graph LR".to_string()];
307
308 for edge in &self.edges {
309 let label = format!("{:.0}%", edge.confidence * 100.0);
310 lines.push(format!(" {} -->|{}| {}", edge.from, label, edge.to));
311 }
312
313 for start in &self.start_nodes {
315 lines.push(format!(" style {} fill:#9f9", start));
316 }
317
318 for terminal in &self.terminal_nodes {
320 lines.push(format!(" style {} fill:#f99", terminal));
321 }
322
323 lines.join("\n")
324 }
325}
326
327#[derive(Debug, Clone, thiserror::Error)]
329pub enum DependencyGraphError {
330 #[error("No start nodes defined")]
331 NoStartNodes,
332
333 #[error("No terminal nodes defined")]
334 NoTerminalNodes,
335
336 #[error("Unknown action: {0}")]
337 UnknownAction(String),
338
339 #[error("Parse error: {0}")]
340 ParseError(String),
341
342 #[error("LLM error: {0}")]
343 LlmError(String),
344}
345
346pub trait DependencyGraphProvider: Send + Sync {
373 fn provide_graph(&self, task: &str, available_actions: &[String]) -> Option<DependencyGraph>;
383}
384
385#[derive(Debug, Clone, Default)]
391pub struct DependencyGraphBuilder {
392 edges: Vec<DependencyEdge>,
393 start_nodes: HashSet<String>,
394 terminal_nodes: HashSet<String>,
395 task: String,
396 available_actions: Vec<String>,
397 param_variants: HashMap<String, (String, Vec<String>)>,
398}
399
400impl DependencyGraphBuilder {
401 pub fn new() -> Self {
402 Self::default()
403 }
404
405 pub fn task(mut self, task: impl Into<String>) -> Self {
407 self.task = task.into();
408 self
409 }
410
411 pub fn available_actions<I, S>(mut self, actions: I) -> Self
413 where
414 I: IntoIterator<Item = S>,
415 S: Into<String>,
416 {
417 self.available_actions = actions.into_iter().map(|s| s.into()).collect();
418 self
419 }
420
421 pub fn edge(mut self, from: impl Into<String>, to: impl Into<String>, confidence: f64) -> Self {
423 self.edges.push(DependencyEdge::new(from, to, confidence));
424 self
425 }
426
427 pub fn start_node(mut self, action: impl Into<String>) -> Self {
429 self.start_nodes.insert(action.into());
430 self
431 }
432
433 pub fn start_nodes<I, S>(mut self, actions: I) -> Self
435 where
436 I: IntoIterator<Item = S>,
437 S: Into<String>,
438 {
439 self.start_nodes
440 .extend(actions.into_iter().map(|s| s.into()));
441 self
442 }
443
444 pub fn terminal_node(mut self, action: impl Into<String>) -> Self {
446 self.terminal_nodes.insert(action.into());
447 self
448 }
449
450 pub fn terminal_nodes<I, S>(mut self, actions: I) -> Self
452 where
453 I: IntoIterator<Item = S>,
454 S: Into<String>,
455 {
456 self.terminal_nodes
457 .extend(actions.into_iter().map(|s| s.into()));
458 self
459 }
460
461 pub fn param_variants<I, S>(
465 mut self,
466 action: impl Into<String>,
467 key: impl Into<String>,
468 values: I,
469 ) -> Self
470 where
471 I: IntoIterator<Item = S>,
472 S: Into<String>,
473 {
474 self.param_variants.insert(
475 action.into(),
476 (key.into(), values.into_iter().map(|s| s.into()).collect()),
477 );
478 self
479 }
480
481 pub fn build(self) -> DependencyGraph {
483 DependencyGraph {
484 edges: self.edges,
485 start_nodes: self.start_nodes,
486 terminal_nodes: self.terminal_nodes,
487 task: self.task,
488 available_actions: self.available_actions,
489 param_variants: self.param_variants,
490 discover_order: Vec::new(),
491 not_discover_order: Vec::new(),
492 learn_record: None,
493 }
494 }
495
496 pub fn build_validated(self) -> Result<DependencyGraph, DependencyGraphError> {
498 let graph = self.build();
499 graph.validate()?;
500 Ok(graph)
501 }
502}
503
504#[derive(Debug, Clone, Serialize, Deserialize)]
512pub struct LlmDependencyResponse {
513 pub edges: Vec<LlmEdge>,
515 pub start: Vec<String>,
517 pub terminal: Vec<String>,
519 #[serde(default)]
521 pub reasoning: Option<String>,
522}
523
524#[derive(Debug, Clone, Serialize, Deserialize)]
526pub struct LlmEdge {
527 pub from: String,
528 pub to: String,
529 pub confidence: f64,
530}
531
532impl LlmDependencyResponse {
533 pub fn into_graph(
535 self,
536 task: impl Into<String>,
537 available_actions: Vec<String>,
538 ) -> DependencyGraph {
539 let mut builder = DependencyGraphBuilder::new()
540 .task(task)
541 .available_actions(available_actions)
542 .start_nodes(self.start)
543 .terminal_nodes(self.terminal);
544
545 for edge in self.edges {
546 builder = builder.edge(edge.from, edge.to, edge.confidence);
547 }
548
549 builder.build()
550 }
551
552 pub fn parse(text: &str) -> Result<Self, DependencyGraphError> {
558 if let Some(response) = Self::parse_arrow_format(text) {
560 return Ok(response);
561 }
562
563 if let Ok(parsed) = serde_json::from_str(text) {
565 return Ok(parsed);
566 }
567
568 if let Some(json) = Self::extract_json(text) {
570 serde_json::from_str(&json).map_err(|e| DependencyGraphError::ParseError(e.to_string()))
571 } else {
572 Err(DependencyGraphError::ParseError(format!(
573 "No valid format found in response: {}",
574 text.chars().take(200).collect::<String>()
575 )))
576 }
577 }
578
579 fn parse_arrow_format(text: &str) -> Option<Self> {
586 if let Some(result) = Self::parse_arrow_only(text) {
588 return Some(result);
589 }
590
591 if let Some(result) = Self::parse_numbered_list(text) {
593 return Some(result);
594 }
595
596 None
597 }
598
599 fn parse_arrow_only(text: &str) -> Option<Self> {
601 let normalized = text.replace('→', "->");
602
603 let arrow_line = normalized.lines().find(|line| line.contains("->"))?;
605
606 let parts: Vec<&str> = arrow_line.split("->").collect();
607 if parts.len() < 2 {
608 return None;
609 }
610
611 let actions_in_order: Vec<String> = parts
613 .iter()
614 .filter_map(|part| {
615 let trimmed = part.trim();
616 let last_word = trimmed.split_whitespace().last()?;
618 let action: String = last_word.chars().filter(|c| c.is_alphabetic()).collect();
620 if action.is_empty() {
621 None
622 } else {
623 Some(action)
624 }
625 })
626 .collect();
627
628 if actions_in_order.len() < 2 {
629 return None;
630 }
631
632 Self::build_response(actions_in_order)
633 }
634
635 fn parse_numbered_list(text: &str) -> Option<Self> {
637 let mut actions_in_order: Vec<String> = Vec::new();
638
639 for i in 1..=10 {
641 let pattern = format!("{}.", i);
642 if let Some(pos) = text.find(&pattern) {
643 let after = &text[pos + pattern.len()..];
645 if let Some(word) = after.split_whitespace().next() {
646 let action: String = word.chars().filter(|c| c.is_alphabetic()).collect();
648 if !action.is_empty() && !actions_in_order.contains(&action) {
649 actions_in_order.push(action);
650 }
651 }
652 }
653 }
654
655 if actions_in_order.len() < 2 {
656 return None;
657 }
658
659 Self::build_response(actions_in_order)
660 }
661
662 fn build_response(actions_in_order: Vec<String>) -> Option<Self> {
664 let mut edges = Vec::new();
665 for window in actions_in_order.windows(2) {
666 edges.push(LlmEdge {
667 from: window[0].clone(),
668 to: window[1].clone(),
669 confidence: 0.9,
670 });
671 }
672
673 Some(Self {
674 edges,
675 start: vec![actions_in_order.first()?.clone()],
676 terminal: vec![actions_in_order.last()?.clone()],
677 reasoning: Some("Parsed from text format".to_string()),
678 })
679 }
680
681 fn extract_json(text: &str) -> Option<String> {
683 let start = text.find('{')?;
685 let chars: Vec<char> = text[start..].chars().collect();
686 let mut depth = 0;
687 let mut in_string = false;
688 let mut escape_next = false;
689
690 for (i, &ch) in chars.iter().enumerate() {
691 if escape_next {
692 escape_next = false;
693 continue;
694 }
695
696 match ch {
697 '\\' if in_string => escape_next = true,
698 '"' => in_string = !in_string,
699 '{' if !in_string => depth += 1,
700 '}' if !in_string => {
701 depth -= 1;
702 if depth == 0 {
703 return Some(chars[..=i].iter().collect());
704 }
705 }
706 _ => {}
707 }
708 }
709
710 None
711 }
712}
713
714pub trait DependencyPlanner: Send + Sync {
722 fn plan(
726 &self,
727 task: &str,
728 available_actions: &[String],
729 ) -> Result<DependencyGraph, DependencyGraphError>;
730
731 fn name(&self) -> &str;
733}
734
735#[derive(Debug, Clone, Default)]
740pub struct StaticDependencyPlanner {
741 patterns: HashMap<String, DependencyGraph>,
743 default_pattern: Option<String>,
745}
746
747impl StaticDependencyPlanner {
748 pub fn new() -> Self {
749 Self::default()
750 }
751
752 pub fn with_pattern(mut self, name: impl Into<String>, graph: DependencyGraph) -> Self {
754 let name = name.into();
755 if self.default_pattern.is_none() {
756 self.default_pattern = Some(name.clone());
757 }
758 self.patterns.insert(name, graph);
759 self
760 }
761
762 pub fn with_default_pattern(mut self, name: impl Into<String>) -> Self {
764 self.default_pattern = Some(name.into());
765 self
766 }
767
768 pub fn with_file_exploration_pattern(self) -> Self {
772 let graph = DependencyGraph::builder()
773 .task("File exploration")
774 .available_actions(["Grep", "List", "Read"])
775 .edge("Grep", "Read", 0.95)
776 .edge("List", "Grep", 0.60)
777 .edge("List", "Read", 0.40)
778 .start_nodes(["Grep", "List"])
779 .terminal_node("Read")
780 .build();
781
782 self.with_pattern("file_exploration", graph)
783 }
784
785 pub fn with_code_search_pattern(self) -> Self {
789 let graph = DependencyGraph::builder()
790 .task("Code search")
791 .available_actions(["Grep", "Read"])
792 .edge("Grep", "Read", 0.95)
793 .start_node("Grep")
794 .terminal_node("Read")
795 .build();
796
797 self.with_pattern("code_search", graph)
798 }
799}
800
801impl DependencyPlanner for StaticDependencyPlanner {
802 fn plan(
803 &self,
804 task: &str,
805 available_actions: &[String],
806 ) -> Result<DependencyGraph, DependencyGraphError> {
807 if let Some(pattern_name) = &self.default_pattern {
809 if let Some(graph) = self.patterns.get(pattern_name) {
810 let mut graph = graph.clone();
811 graph.task = task.to_string();
812 graph.available_actions = available_actions.to_vec();
813 return Ok(graph);
814 }
815 }
816
817 if available_actions.is_empty() {
820 return Err(DependencyGraphError::NoStartNodes);
821 }
822
823 let mut builder = DependencyGraphBuilder::new()
824 .task(task)
825 .available_actions(available_actions.to_vec())
826 .start_node(&available_actions[0]);
827
828 if available_actions.len() > 1 {
829 for window in available_actions.windows(2) {
830 builder = builder.edge(&window[0], &window[1], 0.80);
831 }
832 builder = builder.terminal_node(&available_actions[available_actions.len() - 1]);
833 } else {
834 builder = builder.terminal_node(&available_actions[0]);
835 }
836
837 Ok(builder.build())
838 }
839
840 fn name(&self) -> &str {
841 "StaticDependencyPlanner"
842 }
843}
844
845use crate::actions::ActionDef;
850
851pub struct DependencyPromptGenerator;
856
857impl DependencyPromptGenerator {
858 pub fn generate_prompt(task: &str, actions: &[ActionDef]) -> String {
862 let actions_list = actions
863 .iter()
864 .map(|a| a.name.as_str())
865 .collect::<Vec<_>>()
866 .join(", ");
867
868 format!(
869 r#"{task}
870Steps: {actions_list}
871The very first step is:"#
872 )
873 }
874
875 pub fn generate_first_prompt(_task: &str, actions: &[ActionDef]) -> String {
880 let mut sorted_actions: Vec<&ActionDef> = actions.iter().collect();
882 sorted_actions.sort_by(|a, b| a.name.cmp(&b.name));
883
884 let actions_list = sorted_actions
885 .iter()
886 .map(|a| a.name.as_str())
887 .collect::<Vec<_>>()
888 .join(", ");
889
890 let descriptions: Vec<String> = sorted_actions
892 .iter()
893 .map(|a| format!("- {}: {}", a.name, a.description))
894 .collect();
895 let descriptions_block = descriptions.join("\n");
896
897 let first_verb = sorted_actions
899 .first()
900 .map(|a| Self::extract_verb(&a.description))
901 .unwrap_or_else(|| "CHECK".to_string());
902
903 format!(
904 r#"Steps: {actions_list}
905{descriptions_block}
906Which step {first_verb}S first?
907Answer:"#
908 )
909 }
910
911 pub fn generate_last_prompt(_task: &str, actions: &[ActionDef]) -> String {
916 let mut sorted_actions: Vec<&ActionDef> = actions.iter().collect();
918 sorted_actions.sort_by(|a, b| a.name.cmp(&b.name));
919
920 let actions_list = sorted_actions
921 .iter()
922 .map(|a| a.name.as_str())
923 .collect::<Vec<_>>()
924 .join(", ");
925
926 let descriptions: Vec<String> = sorted_actions
927 .iter()
928 .map(|a| format!("- {}: {}", a.name, a.description))
929 .collect();
930 let descriptions_block = descriptions.join("\n");
931
932 format!(
933 r#"Steps: {actions_list}
934{descriptions_block}
935Which step should be done last?
936Answer:"#
937 )
938 }
939
940 pub fn generate_pair_prompt(task: &str, action_a: &str, action_b: &str) -> String {
942 format!(
943 r#"For {task}, which comes first: {action_a} or {action_b}?
944Answer (one word):"#
945 )
946 }
947
948 fn extract_verb(description: &str) -> String {
953 description
954 .split_whitespace()
955 .next()
956 .map(|w| {
957 let word = w.trim_end_matches('s').trim_end_matches('S');
959 word.to_uppercase()
960 })
961 .unwrap_or_else(|| "CHECK".to_string())
962 }
963}
964
965#[derive(Debug, Clone)]
974pub struct GraphNavigator {
975 graph: DependencyGraph,
976 completed_actions: HashSet<String>,
978}
979
980impl GraphNavigator {
981 pub fn new(graph: DependencyGraph) -> Self {
982 Self {
983 graph,
984 completed_actions: HashSet::new(),
985 }
986 }
987
988 pub fn mark_completed(&mut self, action: &str) {
990 self.completed_actions.insert(action.to_string());
991 }
992
993 pub fn suggest_next(&self) -> Vec<String> {
999 if self.completed_actions.is_empty() {
1000 return self.graph.start_actions();
1002 }
1003
1004 let mut candidates = Vec::new();
1006 for completed in &self.completed_actions {
1007 for next in self.graph.valid_next_actions(completed) {
1008 if !self.completed_actions.contains(&next) && !candidates.contains(&next) {
1009 candidates.push(next);
1010 }
1011 }
1012 }
1013
1014 candidates
1015 }
1016
1017 pub fn is_task_complete(&self) -> bool {
1021 self.graph
1022 .terminal_actions()
1023 .iter()
1024 .any(|t| self.completed_actions.contains(t))
1025 }
1026
1027 pub fn progress(&self) -> f64 {
1029 if self.graph.available_actions.is_empty() {
1030 return 0.0;
1031 }
1032 self.completed_actions.len() as f64 / self.graph.available_actions.len() as f64
1033 }
1034
1035 pub fn graph(&self) -> &DependencyGraph {
1037 &self.graph
1038 }
1039}
1040
1041use crate::learn::offline::LearnedActionOrder;
1046
1047#[derive(Debug, Clone)]
1068pub struct LearnedDependencyProvider {
1069 action_order: LearnedActionOrder,
1070}
1071
1072impl LearnedDependencyProvider {
1073 pub fn new(action_order: LearnedActionOrder) -> Self {
1075 Self { action_order }
1076 }
1077
1078 pub fn action_order(&self) -> &LearnedActionOrder {
1080 &self.action_order
1081 }
1082}
1083
1084impl DependencyGraphProvider for LearnedDependencyProvider {
1085 fn provide_graph(&self, task: &str, available_actions: &[String]) -> Option<DependencyGraph> {
1086 if !self.action_order.matches_actions(available_actions) {
1088 tracing::debug!(
1089 expected_hash = self.action_order.action_set_hash,
1090 actual_hash = LearnedActionOrder::compute_hash(available_actions),
1091 "Action set mismatch, falling back to LLM"
1092 );
1093 return None;
1094 }
1095
1096 let sorted_discover = &self.action_order.discover;
1098 let sorted_not_discover = &self.action_order.not_discover;
1099
1100 if sorted_discover.is_empty() && sorted_not_discover.is_empty() {
1102 tracing::warn!("Both discover and not_discover are empty, cannot build graph");
1103 return None;
1104 }
1105
1106 tracing::info!(
1107 discover = ?self.action_order.discover,
1108 not_discover = ?self.action_order.not_discover,
1109 "Using learned action order (LLM skipped)"
1110 );
1111
1112 let mut builder = DependencyGraphBuilder::new()
1113 .task(task)
1114 .available_actions(available_actions.iter().cloned());
1115
1116 if !sorted_discover.is_empty() {
1118 builder = builder.start_node(&sorted_discover[0]);
1119 } else if !sorted_not_discover.is_empty() {
1120 builder = builder.start_node(&sorted_not_discover[0]);
1121 }
1122
1123 if let Some(last) = sorted_not_discover.last() {
1125 builder = builder.terminal_node(last);
1126 } else if !sorted_discover.is_empty() {
1127 builder = builder.terminal_node(sorted_discover.last().unwrap());
1128 }
1129
1130 for window in sorted_discover.windows(2) {
1132 builder = builder.edge(&window[0], &window[1], 0.9);
1133 }
1134
1135 if !sorted_discover.is_empty() && !sorted_not_discover.is_empty() {
1137 builder = builder.edge(
1138 sorted_discover.last().unwrap(),
1139 &sorted_not_discover[0],
1140 0.9,
1141 );
1142 }
1143
1144 for window in sorted_not_discover.windows(2) {
1146 builder = builder.edge(&window[0], &window[1], 0.9);
1147 }
1148
1149 builder.build_validated().ok()
1151 }
1152}
1153
1154#[cfg(test)]
1159mod tests {
1160 use super::*;
1161
1162 #[test]
1163 fn test_dependency_graph_builder() {
1164 let graph = DependencyGraph::builder()
1165 .task("Find auth function")
1166 .available_actions(["Grep", "List", "Read"])
1167 .edge("Grep", "Read", 0.95)
1168 .edge("List", "Grep", 0.60)
1169 .start_nodes(["Grep", "List"])
1170 .terminal_node("Read")
1171 .build();
1172
1173 assert_eq!(graph.task(), "Find auth function");
1174 assert!(graph.is_start("Grep"));
1175 assert!(graph.is_start("List"));
1176 assert!(graph.is_terminal("Read"));
1177 assert!(graph.can_transition("Grep", "Read"));
1178 assert!(!graph.can_transition("Read", "Grep"));
1179 }
1180
1181 #[test]
1182 fn test_valid_next_actions() {
1183 let graph = DependencyGraph::builder()
1184 .available_actions(["Grep", "List", "Read"])
1185 .edge("Grep", "Read", 0.95)
1186 .edge("List", "Grep", 0.60)
1187 .edge("List", "Read", 0.40)
1188 .start_nodes(["Grep", "List"])
1189 .terminal_node("Read")
1190 .build();
1191
1192 let next = graph.valid_next_actions("Grep");
1194 assert_eq!(next, vec!["Read"]);
1195
1196 let next = graph.valid_next_actions("List");
1198 assert_eq!(next, vec!["Grep", "Read"]);
1199
1200 let next = graph.valid_next_actions("Read");
1202 assert!(next.is_empty());
1203 }
1204
1205 #[test]
1206 fn test_static_planner_file_exploration() {
1207 let planner = StaticDependencyPlanner::new().with_file_exploration_pattern();
1208
1209 let graph = planner
1210 .plan("Find auth.rs", &["Grep".to_string(), "Read".to_string()])
1211 .unwrap();
1212
1213 assert!(graph.is_start("Grep"));
1214 assert!(graph.is_terminal("Read"));
1215 }
1216
1217 #[test]
1218 fn test_graph_navigator() {
1219 let graph = DependencyGraph::builder()
1220 .available_actions(["Grep", "Read"])
1221 .edge("Grep", "Read", 0.95)
1222 .start_node("Grep")
1223 .terminal_node("Read")
1224 .build();
1225
1226 let mut nav = GraphNavigator::new(graph);
1227
1228 assert_eq!(nav.suggest_next(), vec!["Grep"]);
1230 assert!(!nav.is_task_complete());
1231
1232 nav.mark_completed("Grep");
1234 assert_eq!(nav.suggest_next(), vec!["Read"]);
1235 assert!(!nav.is_task_complete());
1236
1237 nav.mark_completed("Read");
1239 assert!(nav.is_task_complete());
1240 assert!(nav.suggest_next().is_empty());
1241 }
1242
1243 #[test]
1244 fn test_llm_response_parsing() {
1245 let json = r#"{
1246 "edges": [
1247 {"from": "Grep", "to": "Read", "confidence": 0.95}
1248 ],
1249 "start": ["Grep"],
1250 "terminal": ["Read"],
1251 "reasoning": "Search first, then read"
1252 }"#;
1253
1254 let response = LlmDependencyResponse::parse(json).unwrap();
1255 assert_eq!(response.edges.len(), 1);
1256 assert_eq!(response.start, vec!["Grep"]);
1257 assert_eq!(response.terminal, vec!["Read"]);
1258 assert!(response.reasoning.is_some());
1259
1260 let graph = response.into_graph(
1261 "Find function",
1262 vec!["Grep".to_string(), "Read".to_string()],
1263 );
1264 assert!(graph.can_transition("Grep", "Read"));
1265 }
1266
1267 #[test]
1268 fn test_mermaid_output() {
1269 let graph = DependencyGraph::builder()
1270 .available_actions(["Grep", "List", "Read"])
1271 .edge("Grep", "Read", 0.95)
1272 .edge("List", "Grep", 0.60)
1273 .start_nodes(["Grep", "List"])
1274 .terminal_node("Read")
1275 .build();
1276
1277 let mermaid = graph.to_mermaid();
1278 assert!(mermaid.contains("graph LR"));
1279 assert!(mermaid.contains("Grep -->|95%| Read"));
1280 assert!(mermaid.contains("style Read fill:#f99"));
1281 }
1282
1283 #[test]
1288 fn test_learned_action_order_hash() {
1289 let actions = vec![
1290 "Grep".to_string(),
1291 "Read".to_string(),
1292 "Restart".to_string(),
1293 ];
1294
1295 let order = LearnedActionOrder::new(
1296 vec!["Grep".to_string(), "Read".to_string()],
1297 vec!["Restart".to_string()],
1298 &actions,
1299 );
1300
1301 let actions_reordered = vec![
1303 "Restart".to_string(),
1304 "Grep".to_string(),
1305 "Read".to_string(),
1306 ];
1307 assert!(order.matches_actions(&actions_reordered));
1308
1309 let actions_different = vec!["Grep".to_string(), "Read".to_string()];
1311 assert!(!order.matches_actions(&actions_different));
1312 }
1313
1314 #[test]
1315 fn test_learned_dependency_provider_cache_hit() {
1316 let actions = vec![
1317 "Grep".to_string(),
1318 "Read".to_string(),
1319 "Restart".to_string(),
1320 ];
1321
1322 let order = LearnedActionOrder::new(
1323 vec!["Grep".to_string(), "Read".to_string()],
1324 vec!["Restart".to_string()],
1325 &actions,
1326 );
1327
1328 let provider = LearnedDependencyProvider::new(order);
1329
1330 let graph = provider.provide_graph("troubleshooting", &actions);
1332 assert!(graph.is_some());
1333
1334 let graph = graph.unwrap();
1335 assert!(graph.is_start("Grep"));
1336 assert!(graph.is_terminal("Restart"));
1337 assert!(graph.can_transition("Grep", "Read"));
1338 assert!(graph.can_transition("Read", "Restart"));
1339 }
1340
1341 #[test]
1342 fn test_learned_dependency_provider_cache_miss() {
1343 let original_actions = vec![
1344 "Grep".to_string(),
1345 "Read".to_string(),
1346 "Restart".to_string(),
1347 ];
1348
1349 let order = LearnedActionOrder::new(
1350 vec!["Grep".to_string(), "Read".to_string()],
1351 vec!["Restart".to_string()],
1352 &original_actions,
1353 );
1354
1355 let provider = LearnedDependencyProvider::new(order);
1356
1357 let different_actions = vec!["Grep".to_string(), "Read".to_string()];
1359 let graph = provider.provide_graph("troubleshooting", &different_actions);
1360 assert!(graph.is_none());
1361 }
1362
1363 #[test]
1364 fn test_learned_dependency_provider_discover_only() {
1365 let actions = vec!["Grep".to_string(), "Read".to_string()];
1366
1367 let order = LearnedActionOrder::new(
1368 vec!["Grep".to_string(), "Read".to_string()],
1369 vec![], &actions,
1371 );
1372
1373 let provider = LearnedDependencyProvider::new(order);
1374 let graph = provider.provide_graph("search task", &actions);
1375 assert!(graph.is_some());
1376
1377 let graph = graph.unwrap();
1378 assert!(graph.is_start("Grep"));
1379 assert!(graph.is_terminal("Read")); assert!(graph.can_transition("Grep", "Read"));
1381 }
1382
1383 #[test]
1384 fn test_learned_dependency_provider_not_discover_only() {
1385 let actions = vec!["Restart".to_string(), "CheckStatus".to_string()];
1386
1387 let order = LearnedActionOrder::new(
1388 vec![], vec!["Restart".to_string(), "CheckStatus".to_string()],
1390 &actions,
1391 );
1392
1393 let provider = LearnedDependencyProvider::new(order);
1394 let graph = provider.provide_graph("ops task", &actions);
1395 assert!(graph.is_some());
1396
1397 let graph = graph.unwrap();
1398 assert!(graph.is_start("Restart")); assert!(graph.is_terminal("CheckStatus"));
1400 assert!(graph.can_transition("Restart", "CheckStatus"));
1401 }
1402
1403 #[test]
1404 fn test_learned_dependency_provider_empty_lists() {
1405 let actions = vec!["Grep".to_string(), "Read".to_string()];
1406
1407 let order = LearnedActionOrder::new(
1408 vec![], vec![], &actions,
1411 );
1412
1413 let provider = LearnedDependencyProvider::new(order);
1414 let graph = provider.provide_graph("empty task", &actions);
1416 assert!(graph.is_none());
1417 }
1418
1419 #[test]
1424 fn test_extract_json_simple() {
1425 let text = r#"Here is the result: {"edges": [], "start": ["A"], "terminal": ["B"]}"#;
1426 let json = LlmDependencyResponse::extract_json(text);
1427 assert!(json.is_some());
1428 let json = json.unwrap();
1429 assert!(json.starts_with('{'));
1430 assert!(json.ends_with('}'));
1431 }
1432
1433 #[test]
1434 fn test_extract_json_nested() {
1435 let text = r#"Result: {"edges": [{"from": "A", "to": "B", "confidence": 0.9}], "start": ["A"], "terminal": ["B"]}"#;
1436 let json = LlmDependencyResponse::extract_json(text);
1437 assert!(json.is_some());
1438
1439 let parsed: Result<LlmDependencyResponse, _> = serde_json::from_str(&json.unwrap());
1441 assert!(parsed.is_ok());
1442 }
1443
1444 #[test]
1445 fn test_extract_json_with_string_braces() {
1446 let text =
1448 r#"{"edges": [], "start": ["A"], "terminal": ["B"], "reasoning": "Use {pattern}"}"#;
1449 let json = LlmDependencyResponse::extract_json(text);
1450 assert!(json.is_some());
1451 assert_eq!(json.unwrap(), text);
1452 }
1453
1454 #[test]
1455 fn test_extract_json_no_json() {
1456 let text = "This is just plain text without JSON";
1457 let json = LlmDependencyResponse::extract_json(text);
1458 assert!(json.is_none());
1459 }
1460
1461 #[test]
1466 fn test_validate_unknown_start_node() {
1467 let graph = DependencyGraph::builder()
1468 .available_actions(["Grep", "Read"])
1469 .start_node("Unknown") .terminal_node("Read")
1471 .build();
1472
1473 let result = graph.validate();
1474 assert!(result.is_err());
1475 assert!(matches!(
1476 result.unwrap_err(),
1477 DependencyGraphError::UnknownAction(name) if name == "Unknown"
1478 ));
1479 }
1480
1481 #[test]
1482 fn test_validate_unknown_terminal_node() {
1483 let graph = DependencyGraph::builder()
1484 .available_actions(["Grep", "Read"])
1485 .start_node("Grep")
1486 .terminal_node("Unknown") .build();
1488
1489 let result = graph.validate();
1490 assert!(result.is_err());
1491 assert!(matches!(
1492 result.unwrap_err(),
1493 DependencyGraphError::UnknownAction(name) if name == "Unknown"
1494 ));
1495 }
1496
1497 #[test]
1498 fn test_validate_valid_graph() {
1499 let graph = DependencyGraph::builder()
1500 .available_actions(["Grep", "Read"])
1501 .edge("Grep", "Read", 0.9)
1502 .start_node("Grep")
1503 .terminal_node("Read")
1504 .build();
1505
1506 assert!(graph.validate().is_ok());
1507 }
1508
1509 #[test]
1514 fn test_start_actions_sorted() {
1515 let graph = DependencyGraph::builder()
1516 .available_actions(["Zebra", "Apple", "Mango"])
1517 .start_nodes(["Zebra", "Apple", "Mango"])
1518 .terminal_node("Zebra")
1519 .build();
1520
1521 let actions = graph.start_actions();
1522 assert_eq!(actions, vec!["Apple", "Mango", "Zebra"]);
1524 }
1525
1526 #[test]
1527 fn test_terminal_actions_sorted() {
1528 let graph = DependencyGraph::builder()
1529 .available_actions(["Zebra", "Apple", "Mango"])
1530 .start_node("Apple")
1531 .terminal_nodes(["Zebra", "Apple", "Mango"])
1532 .build();
1533
1534 let actions = graph.terminal_actions();
1535 assert_eq!(actions, vec!["Apple", "Mango", "Zebra"]);
1537 }
1538}