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(
126 &mut self,
127 reasoning: String,
128 action: Action,
129 confidence_score: Option<f64>,
130 ) -> String {
131 let decision_id = format!("decision_{}_{}", self.session_start, self.decisions.len());
132
133 let action_summary: String = match &action {
135 Action::ToolCall { name, .. } => format!("tool_call:{name}"),
136 Action::Response { response_type, .. } => format!("response:{response_type:?}"),
137 Action::ErrorRecovery { .. } => "error_recovery".into(),
138 };
139
140 let decision = Decision {
141 id: decision_id.clone(),
142 timestamp: current_timestamp(),
143 context: self.current_context.clone(),
144 reasoning,
145 action, outcome: None,
147 confidence_score,
148 };
149
150 self.decisions.push(decision);
151
152 self.current_context.previous_actions.push(action_summary);
154
155 decision_id
156 }
157
158 #[inline]
160 pub fn record_outcome(&mut self, decision_id: &str, outcome: DecisionOutcome) {
161 if let Some(decision) = self.decisions.iter_mut().find(|d| d.id == decision_id) {
162 decision.outcome = Some(outcome);
163 }
164 }
165
166 #[inline]
168 pub fn get_decisions(&self) -> &[Decision] {
169 &self.decisions
170 }
171
172 pub fn generate_transparency_report(&self) -> TransparencyReport {
174 let total_decisions = self.decisions.len();
175 let successful_decisions = self
176 .decisions
177 .iter()
178 .filter(|d| matches!(d.outcome, Some(DecisionOutcome::Success { .. })))
179 .count();
180 let failed_decisions = self
181 .decisions
182 .iter()
183 .filter(|d| matches!(d.outcome, Some(DecisionOutcome::Failure { .. })))
184 .count();
185
186 let tool_calls = self
187 .decisions
188 .iter()
189 .filter(|d| matches!(d.action, Action::ToolCall { .. }))
190 .count();
191
192 let (sum, count) = self
193 .decisions
194 .iter()
195 .filter_map(|d| d.confidence_score)
196 .fold((0.0, 0usize), |(s, c), v| (s + v, c + 1));
197
198 let avg_confidence = if count == 0 {
199 None
200 } else {
201 Some(sum / count as f64)
202 };
203
204 TransparencyReport {
205 session_duration: current_timestamp().saturating_sub(self.session_start),
206 total_decisions,
207 successful_decisions,
208 failed_decisions,
209 tool_calls,
210 avg_confidence,
211 recent_decisions: self.decisions.iter().rev().take(5).cloned().collect(),
212 }
213 }
214
215 pub fn get_decision_context(&self, decision_id: &str) -> Option<&DecisionContext> {
217 self.decisions
218 .iter()
219 .find(|d| d.id == decision_id)
220 .map(|d| &d.context)
221 }
222
223 pub fn get_current_context(&self) -> &DecisionContext {
224 &self.current_context
225 }
226
227 pub fn latest_decision(&self) -> Option<&Decision> {
229 self.decisions.last()
230 }
231
232 pub fn recent_decisions(&self, count: usize) -> Vec<&Decision> {
234 self.decisions.iter().rev().take(count).collect()
235 }
236
237 pub fn record_goal(&mut self, content: String) -> String {
239 self.record_decision(
240 "User goal provided".to_owned(),
241 Action::Response {
242 content,
243 response_type: ResponseType::ContextSummary,
244 },
245 None,
246 )
247 }
248
249 pub fn render_ledger_brief(&self, max_entries: usize) -> String {
251 let mut out = String::new();
252 out.push_str("Decision Ledger (most recent first)\n");
253 let take_n = max_entries.max(1);
254 for d in self.decisions.iter().rev().take(take_n) {
255 let ts = d.timestamp;
256 let turn = d.context.conversation_turn;
257 let line = match &d.action {
258 Action::ToolCall { name, args, .. } => {
259 let arg_preview = match args {
260 Value::String(s) => s.clone(),
261 _ => {
262 let s = args.to_string();
263 vtcode_commons::formatting::truncate_byte_budget(&s, 120, "…")
264 }
265 };
266 format!(
267 "- [turn {}] tool:{} args={} (t={})",
268 turn, name, arg_preview, ts
269 )
270 }
271 Action::Response {
272 response_type,
273 content,
274 } => {
275 let preview =
276 vtcode_commons::formatting::truncate_byte_budget(content, 120, "…");
277 format!(
278 "- [turn {}] response:{:?} {} (t={})",
279 turn, response_type, preview, ts
280 )
281 }
282 Action::ErrorRecovery {
283 error_type,
284 recovery_strategy,
285 } => {
286 format!(
287 "- [turn {}] recovery {} via {} (t={})",
288 turn, error_type, recovery_strategy, ts
289 )
290 }
291 };
292 out.push_str(&line);
293 out.push('\n');
294 }
295 if out.is_empty() {
296 "(no decisions yet)".to_string()
297 } else {
298 out
299 }
300 }
301
302 pub fn prune_old_decisions(&mut self, max_age_secs: u64) -> usize {
305 let now = current_timestamp();
306 let cutoff = now.saturating_sub(max_age_secs);
307 let before_len = self.decisions.len();
308
309 self.decisions.retain(|d| d.timestamp >= cutoff);
310
311 before_len.saturating_sub(self.decisions.len())
312 }
313
314 pub fn prune_to_count(&mut self, max_count: usize) -> usize {
317 if self.decisions.len() <= max_count {
318 return 0;
319 }
320
321 let excess = self.decisions.len() - max_count;
322 self.decisions.drain(0..excess);
323 excess
324 }
325
326 pub fn auto_prune(&mut self) -> usize {
329 const MAX_DECISIONS: usize = 500;
330 const MAX_AGE_SECS: u64 = 30 * 60; let by_age = self.prune_old_decisions(MAX_AGE_SECS);
333 let by_count = self.prune_to_count(MAX_DECISIONS);
334
335 by_age + by_count
336 }
337
338 #[inline]
340 pub fn decision_count(&self) -> usize {
341 self.decisions.len()
342 }
343}
344
345#[derive(Debug, Clone, Serialize, Deserialize)]
347pub struct TransparencyReport {
348 pub session_duration: u64,
349 pub total_decisions: usize,
350 pub successful_decisions: usize,
351 pub failed_decisions: usize,
352 pub tool_calls: usize,
353 pub avg_confidence: Option<f64>,
354 pub recent_decisions: Vec<Decision>,
355}