1use crate::utils::current_timestamp;
2use hashbrown::HashMap;
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6#[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#[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#[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
41 ErrorRecovery {
42 error_type: String,
43 recovery_strategy: String,
44 },
45}
46
47#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
49pub enum ResponseType {
50 Text,
51 ToolExecution,
52 ErrorHandling,
53 ContextSummary,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
58pub enum DecisionOutcome {
59 Success {
60 result: String,
61 metrics: HashMap<String, Value>,
62 },
63 Failure {
64 error: String,
65 recovery_attempts: usize,
66 context_preserved: bool,
67 },
68 Partial {
69 result: String,
70 issues: Vec<String>,
71 },
72}
73
74pub struct DecisionTracker {
76 decisions: Vec<Decision>,
77 current_context: DecisionContext,
78 session_start: u64,
79}
80
81impl Default for DecisionTracker {
82 fn default() -> Self {
83 Self::new()
84 }
85}
86
87impl DecisionTracker {
88 pub fn new() -> Self {
89 let now = current_timestamp();
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 #[inline]
106 pub fn start_turn(&mut self, turn_number: usize, user_input: Option<String>) {
107 self.current_context.conversation_turn = turn_number;
108 self.current_context.user_input = user_input;
109 }
110
111 #[inline]
113 pub fn update_available_tools(&mut self, tools: Vec<String>) {
114 self.current_context.available_tools = tools;
115 }
116
117 #[inline]
119 pub fn update_state(&mut self, key: &str, value: Value) {
120 self.current_context.current_state.insert(key.into(), value);
121 }
122
123 pub fn record_decision(&mut self, reasoning: String, action: Action, confidence_score: Option<f64>) -> String {
126 let decision_id = format!("decision_{}_{}", self.session_start, self.decisions.len());
127
128 let action_summary: String = match &action {
130 Action::ToolCall { name, .. } => format!("tool_call:{name}"),
131 Action::Response { response_type, .. } => format!("response:{response_type:?}"),
132 Action::ErrorRecovery { .. } => "error_recovery".into(),
133 };
134
135 let decision = Decision {
136 id: decision_id.clone(),
137 timestamp: current_timestamp(),
138 context: self.current_context.clone(),
139 reasoning,
140 action, outcome: None,
142 confidence_score,
143 };
144
145 self.decisions.push(decision);
146
147 self.current_context.previous_actions.push(action_summary);
149
150 decision_id
151 }
152
153 #[inline]
155 pub fn record_outcome(&mut self, decision_id: &str, outcome: DecisionOutcome) {
156 if let Some(decision) = self.decisions.iter_mut().find(|d| d.id == decision_id) {
157 decision.outcome = Some(outcome);
158 }
159 }
160
161 #[inline]
163 pub fn get_decisions(&self) -> &[Decision] {
164 &self.decisions
165 }
166
167 pub fn generate_transparency_report(&self) -> TransparencyReport {
169 let total_decisions = self.decisions.len();
170 let successful_decisions = self
171 .decisions
172 .iter()
173 .filter(|d| matches!(d.outcome, Some(DecisionOutcome::Success { .. })))
174 .count();
175 let failed_decisions = self
176 .decisions
177 .iter()
178 .filter(|d| matches!(d.outcome, Some(DecisionOutcome::Failure { .. })))
179 .count();
180
181 let tool_calls = self
182 .decisions
183 .iter()
184 .filter(|d| matches!(d.action, Action::ToolCall { .. }))
185 .count();
186
187 let (sum, count) = self
188 .decisions
189 .iter()
190 .filter_map(|d| d.confidence_score)
191 .fold((0.0, 0usize), |(s, c), v| (s + v, c + 1));
192
193 let avg_confidence = if count == 0 { None } else { Some(sum / count as f64) };
194
195 TransparencyReport {
196 session_duration: current_timestamp().saturating_sub(self.session_start),
197 total_decisions,
198 successful_decisions,
199 failed_decisions,
200 tool_calls,
201 avg_confidence,
202 recent_decisions: self.decisions.iter().rev().take(5).cloned().collect(),
203 }
204 }
205
206 pub fn get_decision_context(&self, decision_id: &str) -> Option<&DecisionContext> {
208 self.decisions.iter().find(|d| d.id == decision_id).map(|d| &d.context)
209 }
210
211 pub fn get_current_context(&self) -> &DecisionContext {
212 &self.current_context
213 }
214
215 pub fn latest_decision(&self) -> Option<&Decision> {
217 self.decisions.last()
218 }
219
220 pub fn recent_decisions(&self, count: usize) -> Vec<&Decision> {
222 self.decisions.iter().rev().take(count).collect()
223 }
224
225 pub fn record_goal(&mut self, content: String) -> String {
227 self.record_decision(
228 "User goal provided".to_owned(),
229 Action::Response {
230 content,
231 response_type: ResponseType::ContextSummary,
232 },
233 None,
234 )
235 }
236
237 pub fn render_ledger_brief(&self, max_entries: usize) -> String {
239 let mut out = String::new();
240 out.push_str("Decision Ledger (most recent first)\n");
241 let take_n = max_entries.max(1);
242 for d in self.decisions.iter().rev().take(take_n) {
243 let ts = d.timestamp;
244 let turn = d.context.conversation_turn;
245 let line = match &d.action {
246 Action::ToolCall { name, args, .. } => {
247 let arg_preview = match args {
248 Value::String(s) => s.clone(),
249 _ => {
250 let s = args.to_string();
251 vtcode_commons::formatting::truncate_byte_budget(&s, 120, "…")
252 }
253 };
254 format!("- [turn {turn}] tool:{name} args={arg_preview} (t={ts})")
255 }
256 Action::Response { response_type, content } => {
257 let preview = vtcode_commons::formatting::truncate_byte_budget(content, 120, "…");
258 format!("- [turn {turn}] response:{response_type:?} {preview} (t={ts})")
259 }
260 Action::ErrorRecovery { error_type, recovery_strategy } => {
261 format!("- [turn {turn}] recovery {error_type} via {recovery_strategy} (t={ts})")
262 }
263 };
264 out.push_str(&line);
265 out.push('\n');
266 }
267 if out.is_empty() {
268 "(no decisions yet)".to_string()
269 } else {
270 out
271 }
272 }
273
274 pub fn prune_old_decisions(&mut self, max_age_secs: u64) -> usize {
277 let now = current_timestamp();
278 let cutoff = now.saturating_sub(max_age_secs);
279 let before_len = self.decisions.len();
280
281 self.decisions.retain(|d| d.timestamp >= cutoff);
282
283 before_len.saturating_sub(self.decisions.len())
284 }
285
286 pub fn prune_to_count(&mut self, max_count: usize) -> usize {
289 if self.decisions.len() <= max_count {
290 return 0;
291 }
292
293 let excess = self.decisions.len() - max_count;
294 self.decisions.drain(0..excess);
295 excess
296 }
297
298 pub fn auto_prune(&mut self) -> usize {
301 const MAX_DECISIONS: usize = 500;
302 const MAX_AGE_SECS: u64 = 30 * 60; let by_age = self.prune_old_decisions(MAX_AGE_SECS);
305 let by_count = self.prune_to_count(MAX_DECISIONS);
306
307 by_age + by_count
308 }
309
310 #[inline]
312 pub fn decision_count(&self) -> usize {
313 self.decisions.len()
314 }
315}
316
317#[derive(Debug, Clone, Serialize, Deserialize)]
319pub struct TransparencyReport {
320 pub session_duration: u64,
321 pub total_decisions: usize,
322 pub successful_decisions: usize,
323 pub failed_decisions: usize,
324 pub tool_calls: usize,
325 pub avg_confidence: Option<f64>,
326 pub recent_decisions: Vec<Decision>,
327}