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(
76 tool_name,
77 combined_reasoning,
78 risk_level,
79 ))
80 }
81
82 pub fn suggest_default_justification(
85 tool_name: &str,
86 risk_level: &RiskLevel,
87 ) -> Option<ToolJustification> {
88 let (reason, outcome) = match tool_name {
89 "run_command" | "execute" => (
90 "Execute command to perform necessary system operation or build/test task.",
91 Some("Will capture command output for analysis and decision-making."),
92 ),
93 "write_file" | "create_file" | "edit_file" => (
94 "Modify code or configuration to implement necessary changes.",
95 Some("Will create/update file with the generated content."),
96 ),
97 crate::config::constants::tools::GREP_FILE
98 | "find_files"
99 | crate::config::constants::tools::LIST_FILES => (
100 "Search or list files to understand codebase structure.",
101 Some("Will return matching files and their contents for analysis."),
102 ),
103 "delete_file" | "remove_file" => (
104 "Remove unnecessary or generated files as part of cleanup.",
105 Some("Will delete the specified file(s)."),
106 ),
107 "apply_patch" => (
108 "Apply code changes to fix issues or implement features.",
109 Some("Will apply the patch and verify the changes."),
110 ),
111 _ => return None,
112 };
113
114 let just = ToolJustification::new(tool_name, reason, risk_level);
115 Some(outcome.map_or(just.clone(), |outcome_str| just.with_outcome(outcome_str)))
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122 use crate::core::decision_tracker::{Action, DecisionContext, DecisionOutcome};
123 use hashbrown::HashMap;
124 use std::time::{SystemTime, UNIX_EPOCH};
125
126 fn create_test_decision(reasoning: &str) -> Decision {
127 let now = SystemTime::now()
128 .duration_since(UNIX_EPOCH)
129 .unwrap()
130 .as_secs();
131
132 Decision {
133 id: "test-1".to_string(),
134 timestamp: now,
135 context: DecisionContext {
136 conversation_turn: 1,
137 user_input: Some("analyze code".to_string()),
138 previous_actions: vec![],
139 available_tools: vec!["read_file".to_string()],
140 current_state: HashMap::new(),
141 },
142 reasoning: reasoning.to_string(),
143 action: Action::ToolCall {
144 name: "read_file".to_string(),
145 args: serde_json::json!({"path": "src/main.rs"}),
146 expected_outcome: "Get file contents".to_string(),
147 },
148 outcome: Some(DecisionOutcome::Success {
149 result: "File read successfully".to_string(),
150 metrics: HashMap::new(),
151 }),
152 confidence_score: Some(0.95),
153 }
154 }
155
156 #[test]
157 fn test_extract_from_decision_low_risk() {
158 let decision = create_test_decision("Read the source file");
159 let just =
160 JustificationExtractor::extract_from_decision(&decision, "read_file", &RiskLevel::Low);
161
162 assert!(just.is_none()); }
164
165 #[test]
166 fn test_extract_from_decision_high_risk() {
167 let decision = create_test_decision("Need to understand code structure deeply");
168 let just = JustificationExtractor::extract_from_decision(
169 &decision,
170 "run_command",
171 &RiskLevel::High,
172 );
173
174 assert!(just.is_some());
175 let just = just.unwrap();
176 assert_eq!(just.tool_name, "run_command");
177 assert_eq!(just.reason, "Need to understand code structure deeply");
178 assert_eq!(just.risk_level, "High");
179 }
180
181 #[test]
182 fn test_extract_from_decision_empty_reasoning() {
183 let decision = create_test_decision("");
184 let just = JustificationExtractor::extract_from_decision(
185 &decision,
186 "run_command",
187 &RiskLevel::High,
188 );
189
190 assert!(just.is_none()); }
192
193 #[test]
194 fn test_suggest_default_justification() {
195 let just =
196 JustificationExtractor::suggest_default_justification("run_command", &RiskLevel::High);
197
198 assert!(just.is_some());
199 let just = just.unwrap();
200 assert!(just.reason.contains("Execute command"));
201 assert!(just.expected_outcome.is_some());
202 }
203
204 #[test]
205 fn test_suggest_default_for_write_file() {
206 let just =
207 JustificationExtractor::suggest_default_justification("write_file", &RiskLevel::Medium);
208
209 assert!(just.is_some());
210 let just = just.unwrap();
211 assert!(just.reason.contains("Modify code"));
212 }
213
214 #[test]
215 fn test_suggest_default_for_unknown_tool() {
216 let just =
217 JustificationExtractor::suggest_default_justification("unknown_tool", &RiskLevel::High);
218
219 assert!(just.is_none()); }
221}