1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use std::collections::HashMap;
4use std::time::{SystemTime, UNIX_EPOCH};
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 ContextCompression {
41 reason: String,
42 compression_ratio: f64,
43 },
44 ErrorRecovery {
45 error_type: String,
46 recovery_strategy: String,
47 },
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
52pub enum ResponseType {
53 Text,
54 ToolExecution,
55 ErrorHandling,
56 ContextSummary,
57}
58
59#[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
77pub 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 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 pub fn update_available_tools(&mut self, tools: Vec<String>) {
112 self.current_context.available_tools = tools;
113 }
114
115 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 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 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 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 pub fn get_decisions(&self) -> &[Decision] {
167 &self.decisions
168 }
169
170 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 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 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 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#[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}