vtcode_core/core/
decision_tracker.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use std::collections::HashMap;
4use std::time::{SystemTime, UNIX_EPOCH};
5
6/// Represents a single decision made by the agent
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct Decision {
9    pub id: String,
10    pub timestamp: u64,
11    pub context: DecisionContext,
12    pub reasoning: String,
13    pub action: Action,
14    pub outcome: Option<DecisionOutcome>,
15    pub confidence_score: Option<f64>,
16}
17
18/// Context information that led to a decision
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct DecisionContext {
21    pub conversation_turn: usize,
22    pub user_input: Option<String>,
23    pub previous_actions: Vec<String>,
24    pub available_tools: Vec<String>,
25    pub current_state: HashMap<String, Value>,
26}
27
28/// Action taken as a result of the decision
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub enum Action {
31    ToolCall {
32        name: String,
33        args: Value,
34        expected_outcome: String,
35    },
36    Response {
37        content: String,
38        response_type: ResponseType,
39    },
40    ContextCompression {
41        reason: String,
42        compression_ratio: f64,
43    },
44    ErrorRecovery {
45        error_type: String,
46        recovery_strategy: String,
47    },
48}
49
50/// Type of response given to user
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub enum ResponseType {
53    Text,
54    ToolExecution,
55    ErrorHandling,
56    ContextSummary,
57}
58
59/// Outcome of a decision
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub enum DecisionOutcome {
62    Success {
63        result: String,
64        metrics: HashMap<String, Value>,
65    },
66    Failure {
67        error: String,
68        recovery_attempts: usize,
69        context_preserved: bool,
70    },
71    Partial {
72        result: String,
73        issues: Vec<String>,
74    },
75}
76
77/// Decision tracker for maintaining transparency
78pub struct DecisionTracker {
79    decisions: Vec<Decision>,
80    current_context: DecisionContext,
81    session_start: u64,
82}
83
84impl DecisionTracker {
85    pub fn new() -> Self {
86        let now = SystemTime::now()
87            .duration_since(UNIX_EPOCH)
88            .unwrap()
89            .as_secs();
90
91        Self {
92            decisions: Vec::new(),
93            current_context: DecisionContext {
94                conversation_turn: 0,
95                user_input: None,
96                previous_actions: Vec::new(),
97                available_tools: Vec::new(),
98                current_state: HashMap::new(),
99            },
100            session_start: now,
101        }
102    }
103
104    /// Start tracking a new conversation turn
105    pub fn start_turn(&mut self, turn_number: usize, user_input: Option<String>) {
106        self.current_context.conversation_turn = turn_number;
107        self.current_context.user_input = user_input;
108    }
109
110    /// Update the current context with available tools
111    pub fn update_available_tools(&mut self, tools: Vec<String>) {
112        self.current_context.available_tools = tools;
113    }
114
115    /// Update the current state
116    pub fn update_state(&mut self, key: &str, value: Value) {
117        self.current_context
118            .current_state
119            .insert(key.to_string(), value);
120    }
121
122    /// Record a decision
123    pub fn record_decision(
124        &mut self,
125        reasoning: String,
126        action: Action,
127        confidence_score: Option<f64>,
128    ) -> String {
129        let decision_id = format!("decision_{}_{}", self.session_start, self.decisions.len());
130
131        let decision = Decision {
132            id: decision_id.clone(),
133            timestamp: SystemTime::now()
134                .duration_since(UNIX_EPOCH)
135                .unwrap()
136                .as_secs(),
137            context: self.current_context.clone(),
138            reasoning,
139            action: action.clone(),
140            outcome: None,
141            confidence_score,
142        };
143
144        self.decisions.push(decision);
145
146        // Update previous actions for next decision
147        let action_summary = match &action {
148            Action::ToolCall { name, .. } => format!("tool_call:{}", name),
149            Action::Response { response_type, .. } => format!("response:{:?}", response_type),
150            Action::ContextCompression { .. } => "context_compression".to_string(),
151            Action::ErrorRecovery { .. } => "error_recovery".to_string(),
152        };
153        self.current_context.previous_actions.push(action_summary);
154
155        decision_id
156    }
157
158    /// Record the outcome of a decision
159    pub fn record_outcome(&mut self, decision_id: &str, outcome: DecisionOutcome) {
160        if let Some(decision) = self.decisions.iter_mut().find(|d| d.id == decision_id) {
161            decision.outcome = Some(outcome);
162        }
163    }
164
165    /// Get all decisions for transparency reporting
166    pub fn get_decisions(&self) -> &[Decision] {
167        &self.decisions
168    }
169
170    /// Generate a transparency report
171    pub fn generate_transparency_report(&self) -> TransparencyReport {
172        let total_decisions = self.decisions.len();
173        let successful_decisions = self
174            .decisions
175            .iter()
176            .filter(|d| matches!(d.outcome, Some(DecisionOutcome::Success { .. })))
177            .count();
178        let failed_decisions = self
179            .decisions
180            .iter()
181            .filter(|d| matches!(d.outcome, Some(DecisionOutcome::Failure { .. })))
182            .count();
183
184        let tool_calls = self
185            .decisions
186            .iter()
187            .filter(|d| matches!(d.action, Action::ToolCall { .. }))
188            .count();
189
190        let avg_confidence = self
191            .decisions
192            .iter()
193            .filter_map(|d| d.confidence_score)
194            .collect::<Vec<f64>>();
195
196        let avg_confidence = if avg_confidence.is_empty() {
197            None
198        } else {
199            Some(avg_confidence.iter().sum::<f64>() / avg_confidence.len() as f64)
200        };
201
202        TransparencyReport {
203            session_duration: SystemTime::now()
204                .duration_since(UNIX_EPOCH)
205                .unwrap()
206                .as_secs()
207                - self.session_start,
208            total_decisions,
209            successful_decisions,
210            failed_decisions,
211            tool_calls,
212            avg_confidence,
213            recent_decisions: self.decisions.iter().rev().take(5).cloned().collect(),
214        }
215    }
216
217    /// Get decision context for error recovery
218    pub fn get_decision_context(&self, decision_id: &str) -> Option<&DecisionContext> {
219        self.decisions
220            .iter()
221            .find(|d| d.id == decision_id)
222            .map(|d| &d.context)
223    }
224
225    pub fn get_current_context(&self) -> &DecisionContext {
226        &self.current_context
227    }
228
229    /// Convenience: record a user goal/intention for this turn
230    pub fn record_goal(&mut self, content: String) -> String {
231        self.record_decision(
232            "User goal provided".to_string(),
233            Action::Response {
234                content,
235                response_type: ResponseType::ContextSummary,
236            },
237            None,
238        )
239    }
240
241    /// Render a compact Decision Ledger for injection into the system prompt
242    pub fn render_ledger_brief(&self, max_entries: usize) -> String {
243        let mut out = String::new();
244        out.push_str("Decision Ledger (most recent first)\n");
245        let take_n = max_entries.max(1);
246        for d in self.decisions.iter().rev().take(take_n) {
247            let ts = d.timestamp;
248            let turn = d.context.conversation_turn;
249            let line = match &d.action {
250                Action::ToolCall { name, args, .. } => {
251                    let arg_preview = match args {
252                        serde_json::Value::String(s) => s.clone(),
253                        _ => {
254                            let s = args.to_string();
255                            if s.len() > 120 {
256                                format!("{}…", &s[..120])
257                            } else {
258                                s
259                            }
260                        }
261                    };
262                    format!(
263                        "- [turn {}] tool:{} args={} (t={})",
264                        turn, name, arg_preview, ts
265                    )
266                }
267                Action::Response {
268                    response_type,
269                    content,
270                } => {
271                    let preview = if content.len() > 120 {
272                        format!("{}…", &content[..120])
273                    } else {
274                        content.clone()
275                    };
276                    format!(
277                        "- [turn {}] response:{:?} {} (t={})",
278                        turn, response_type, preview, ts
279                    )
280                }
281                Action::ContextCompression {
282                    reason,
283                    compression_ratio,
284                } => {
285                    format!(
286                        "- [turn {}] compression {:.2} reason={} (t={})",
287                        turn, compression_ratio, reason, ts
288                    )
289                }
290                Action::ErrorRecovery {
291                    error_type,
292                    recovery_strategy,
293                } => {
294                    format!(
295                        "- [turn {}] recovery {} via {} (t={})",
296                        turn, error_type, recovery_strategy, ts
297                    )
298                }
299            };
300            out.push_str(&line);
301            out.push('\n');
302        }
303        if out.is_empty() {
304            "(no decisions yet)".to_string()
305        } else {
306            out
307        }
308    }
309}
310
311/// Transparency report for the current session
312#[derive(Debug, Clone, Serialize, Deserialize)]
313pub struct TransparencyReport {
314    pub session_duration: u64,
315    pub total_decisions: usize,
316    pub successful_decisions: usize,
317    pub failed_decisions: usize,
318    pub tool_calls: usize,
319    pub avg_confidence: Option<f64>,
320    pub recent_decisions: Vec<Decision>,
321}
322
323impl Default for DecisionTracker {
324    fn default() -> Self {
325        Self::new()
326    }
327}