vtcode_core/tools/registry/
justification_extractor.rs1use super::justification::ToolJustification;
2use super::risk_scorer::RiskLevel;
3use crate::core::decision_tracker::{Decision, DecisionTracker};
8
9pub struct JustificationExtractor;
11
12impl JustificationExtractor {
13 pub fn extract_from_decision(
18 decision: &Decision,
19 tool_name: &str,
20 risk_level: &RiskLevel,
21 ) -> Option<ToolJustification> {
22 match risk_level {
24 RiskLevel::Low => None,
25 _ => {
26 if decision.reasoning.is_empty() {
28 return None;
29 }
30
31 let just = ToolJustification::new(tool_name, &decision.reasoning, risk_level);
32
33 Some(just)
35 }
36 }
37 }
38
39 pub fn extract_latest_from_tracker(
41 tracker: &DecisionTracker,
42 tool_name: &str,
43 risk_level: &RiskLevel,
44 ) -> Option<ToolJustification> {
45 tracker
46 .latest_decision()
47 .and_then(|decision| Self::extract_from_decision(decision, tool_name, risk_level))
48 }
49
50 pub fn extract_from_recent_decisions(
53 tracker: &DecisionTracker,
54 tool_name: &str,
55 risk_level: &RiskLevel,
56 depth: usize,
57 ) -> Option<ToolJustification> {
58 let decisions = tracker.recent_decisions(depth);
59 if decisions.is_empty() {
60 return None;
61 }
62
63 let combined_reasoning = decisions
65 .iter()
66 .filter(|d| !d.reasoning.is_empty())
67 .map(|d| d.reasoning.as_str())
68 .collect::<Vec<_>>()
69 .join(" ");
70
71 if combined_reasoning.is_empty() {
72 return None;
73 }
74
75 Some(ToolJustification::new(tool_name, combined_reasoning, risk_level))
76 }
77
78 pub fn suggest_default_justification(tool_name: &str, risk_level: &RiskLevel) -> Option<ToolJustification> {
81 let (reason, outcome) = match tool_name {
82 "run_command" | "execute" => (
83 "Execute command to perform necessary system operation or build/test task.",
84 Some("Will capture command output for analysis and decision-making."),
85 ),
86 "write_file" | "create_file" | "edit_file" => (
87 "Modify code or configuration to implement necessary changes.",
88 Some("Will create/update file with the generated content."),
89 ),
90 crate::config::constants::tools::GREP_FILE | "find_files" | crate::config::constants::tools::LIST_FILES => {
91 (
92 "Search or list files to understand codebase structure.",
93 Some("Will return matching files and their contents for analysis."),
94 )
95 }
96 "delete_file" | "remove_file" => (
97 "Remove unnecessary or generated files as part of cleanup.",
98 Some("Will delete the specified file(s)."),
99 ),
100 "apply_patch" => (
101 "Apply code changes to fix issues or implement features.",
102 Some("Will apply the patch and verify the changes."),
103 ),
104 _ => return None,
105 };
106
107 let just = ToolJustification::new(tool_name, reason, risk_level);
108 Some(outcome.map_or(just.clone(), |outcome_str| just.with_outcome(outcome_str)))
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115 use crate::core::decision_tracker::{Action, DecisionContext, DecisionOutcome};
116 use hashbrown::HashMap;
117 use std::time::{SystemTime, UNIX_EPOCH};
118
119 fn create_test_decision(reasoning: &str) -> Decision {
120 let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
121
122 Decision {
123 id: "test-1".to_string(),
124 timestamp: now,
125 context: DecisionContext {
126 conversation_turn: 1,
127 user_input: Some("analyze code".to_string()),
128 previous_actions: vec![],
129 available_tools: vec!["read_file".to_string()],
130 current_state: HashMap::new(),
131 },
132 reasoning: reasoning.to_string(),
133 action: Action::ToolCall {
134 name: "read_file".to_string(),
135 args: serde_json::json!({"path": "src/main.rs"}),
136 expected_outcome: "Get file contents".to_string(),
137 },
138 outcome: Some(DecisionOutcome::Success {
139 result: "File read successfully".to_string(),
140 metrics: HashMap::new(),
141 }),
142 confidence_score: Some(0.95),
143 }
144 }
145
146 #[test]
147 fn test_extract_from_decision_low_risk() {
148 let decision = create_test_decision("Read the source file");
149 let just = JustificationExtractor::extract_from_decision(&decision, "read_file", &RiskLevel::Low);
150
151 assert!(just.is_none()); }
153
154 #[test]
155 fn test_extract_from_decision_high_risk() {
156 let decision = create_test_decision("Need to understand code structure deeply");
157 let just = JustificationExtractor::extract_from_decision(&decision, "run_command", &RiskLevel::High);
158
159 assert!(just.is_some());
160 let just = just.unwrap();
161 assert_eq!(just.tool_name, "run_command");
162 assert_eq!(just.reason, "Need to understand code structure deeply");
163 assert_eq!(just.risk_level, "High");
164 }
165
166 #[test]
167 fn test_extract_from_decision_empty_reasoning() {
168 let decision = create_test_decision("");
169 let just = JustificationExtractor::extract_from_decision(&decision, "run_command", &RiskLevel::High);
170
171 assert!(just.is_none()); }
173
174 #[test]
175 fn test_suggest_default_justification() {
176 let just = JustificationExtractor::suggest_default_justification("run_command", &RiskLevel::High);
177
178 assert!(just.is_some());
179 let just = just.unwrap();
180 assert!(just.reason.contains("Execute command"));
181 assert!(just.expected_outcome.is_some());
182 }
183
184 #[test]
185 fn test_suggest_default_for_write_file() {
186 let just = JustificationExtractor::suggest_default_justification("write_file", &RiskLevel::Medium);
187
188 assert!(just.is_some());
189 let just = just.unwrap();
190 assert!(just.reason.contains("Modify code"));
191 }
192
193 #[test]
194 fn test_suggest_default_for_unknown_tool() {
195 let just = JustificationExtractor::suggest_default_justification("unknown_tool", &RiskLevel::High);
196
197 assert!(just.is_none()); }
199}