aivcs_core/memory/
rationale.rs1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(rename_all = "snake_case")]
9pub enum RationaleOutcome {
10 Success,
11 Failure,
12 Partial,
13 Skipped,
14}
15
16#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
18pub struct DecisionRationale {
19 pub decision: String,
20 pub reasoning: String,
21 pub alternatives_considered: Vec<String>,
22 pub constraints: Vec<String>,
23 pub confidence: f64,
24}
25
26impl DecisionRationale {
27 pub fn new(decision: &str, reasoning: &str) -> Self {
28 Self {
29 decision: decision.into(),
30 reasoning: reasoning.into(),
31 alternatives_considered: Vec::new(),
32 constraints: Vec::new(),
33 confidence: 0.0,
34 }
35 }
36
37 pub fn with_alternative(mut self, alt: &str) -> Self {
38 self.alternatives_considered.push(alt.into());
39 self
40 }
41
42 pub fn with_constraint(mut self, constraint: &str) -> Self {
43 self.constraints.push(constraint.into());
44 self
45 }
46
47 pub fn with_confidence(mut self, confidence: f64) -> Self {
48 self.confidence = confidence.clamp(0.0, 1.0);
49 self
50 }
51}
52
53#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
55pub struct RationaleEntry {
56 pub rationale: DecisionRationale,
57 pub run_id: String,
58 pub event_seq: u64,
59 pub decided_at: DateTime<Utc>,
60 pub outcome: Option<RationaleOutcome>,
61 pub tags: Vec<String>,
62}
63
64impl RationaleEntry {
65 pub fn new(rationale: DecisionRationale, run_id: &str, event_seq: u64) -> Self {
66 Self {
67 rationale,
68 run_id: run_id.into(),
69 event_seq,
70 decided_at: Utc::now(),
71 outcome: None,
72 tags: Vec::new(),
73 }
74 }
75
76 pub fn with_outcome(mut self, outcome: RationaleOutcome) -> Self {
77 self.outcome = Some(outcome);
78 self
79 }
80
81 pub fn with_tag(mut self, tag: &str) -> Self {
82 self.tags.push(tag.into());
83 self
84 }
85
86 pub fn token_estimate(&self) -> usize {
88 let chars = self.rationale.decision.len()
89 + self.rationale.reasoning.len()
90 + self
91 .rationale
92 .alternatives_considered
93 .iter()
94 .map(|s| s.len())
95 .sum::<usize>()
96 + self
97 .rationale
98 .constraints
99 .iter()
100 .map(|s| s.len())
101 .sum::<usize>();
102 (chars / 4).max(1)
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 #[test]
111 fn test_rationale_builder() {
112 let r = DecisionRationale::new("do X", "because Y")
113 .with_alternative("do Z")
114 .with_constraint("must be fast")
115 .with_confidence(0.85);
116
117 assert_eq!(r.decision, "do X");
118 assert_eq!(r.reasoning, "because Y");
119 assert_eq!(r.alternatives_considered, vec!["do Z"]);
120 assert_eq!(r.constraints, vec!["must be fast"]);
121 assert!((r.confidence - 0.85).abs() < f64::EPSILON);
122 }
123
124 #[test]
125 fn test_confidence_clamped() {
126 let r = DecisionRationale::new("a", "b").with_confidence(2.0);
127 assert!((r.confidence - 1.0).abs() < f64::EPSILON);
128
129 let r = DecisionRationale::new("a", "b").with_confidence(-1.0);
130 assert!(r.confidence.abs() < f64::EPSILON);
131 }
132
133 #[test]
134 fn test_rationale_entry_token_estimate() {
135 let r = DecisionRationale::new("decision", "reasoning");
136 let e = RationaleEntry::new(r, "run1", 1);
137 assert!(e.token_estimate() > 0);
138 }
139
140 #[test]
141 fn test_serde_roundtrip() {
142 let e = RationaleEntry::new(
143 DecisionRationale::new("d", "r")
144 .with_alternative("a")
145 .with_confidence(0.5),
146 "run",
147 1,
148 )
149 .with_outcome(RationaleOutcome::Success)
150 .with_tag("test");
151
152 let json = serde_json::to_string(&e).unwrap();
153 let back: RationaleEntry = serde_json::from_str(&json).unwrap();
154 assert_eq!(e, back);
155 }
156}