1use serde::{Deserialize, Serialize};
13use std::time::{SystemTime, UNIX_EPOCH};
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct AgentBeliefs {
20 pub about_user: Vec<Belief>,
22 pub about_system: Vec<Belief>,
24 pub about_task: Vec<Belief>,
26 pub about_conversation: Vec<Belief>,
28 #[serde(skip)]
30 pub max_beliefs: usize,
31}
32
33impl Default for AgentBeliefs {
34 fn default() -> Self {
35 Self {
36 about_user: Vec::with_capacity(8),
37 about_system: Vec::with_capacity(8),
38 about_task: Vec::with_capacity(8),
39 about_conversation: Vec::with_capacity(8),
40 max_beliefs: 64,
41 }
42 }
43}
44
45impl AgentBeliefs {
46 pub fn add_or_reinforce(&mut self, statement: &str, confidence: f64, source: BeliefSource) {
48 let normalized = statement.trim().to_lowercase();
49 let now = unix_timestamp();
50 let category = source.default_category();
51
52 let needs_prune = {
54 let total =
55 self.about_user.len() + self.about_system.len() + self.about_task.len() + self.about_conversation.len();
56 total >= self.max_beliefs
57 };
58 if needs_prune {
59 self.prune_low_confidence(0.1);
60 }
61
62 match category {
64 BeliefCategory::User => {
65 Self::reinforce_or_push(&mut self.about_user, &normalized, statement, confidence, source, now);
66 }
67 BeliefCategory::System => {
68 Self::reinforce_or_push(&mut self.about_system, &normalized, statement, confidence, source, now);
69 }
70 BeliefCategory::Task => {
71 Self::reinforce_or_push(&mut self.about_task, &normalized, statement, confidence, source, now);
72 }
73 BeliefCategory::Conversation => {
74 Self::reinforce_or_push(&mut self.about_conversation, &normalized, statement, confidence, source, now);
75 }
76 }
77 }
78
79 fn reinforce_or_push(
81 list: &mut Vec<Belief>,
82 normalized: &str,
83 statement: &str,
84 confidence: f64,
85 source: BeliefSource,
86 now: u64,
87 ) {
88 if let Some(existing) = list.iter_mut().find(|b| {
90 let b_norm = b.statement.trim().to_lowercase();
91 b_norm == *normalized || b_norm.contains(normalized) || normalized.contains(&b_norm)
92 }) {
93 existing.confidence = existing.confidence.max(confidence);
94 existing.reinforcement_count = existing.reinforcement_count.saturating_add(1);
95 existing.last_reinforced_at = now;
96 return;
97 }
98
99 list.push(Belief {
100 id: format!("belief_{now}_{}", list.len()),
101 statement: statement.trim().to_string(),
102 confidence: confidence.clamp(0.0, 1.0),
103 source,
104 created_at: now,
105 last_reinforced_at: now,
106 reinforcement_count: 1,
107 });
108 }
109
110 pub fn above_confidence(&self, threshold: f64) -> Vec<&Belief> {
112 let mut result = Vec::new();
113 for belief in self
114 .about_user
115 .iter()
116 .chain(self.about_system.iter())
117 .chain(self.about_task.iter())
118 .chain(self.about_conversation.iter())
119 {
120 if belief.confidence >= threshold {
121 result.push(belief);
122 }
123 }
124 result
125 }
126
127 pub fn prune_low_confidence(&mut self, threshold: f64) {
129 let now = unix_timestamp();
130 let retain = |belief: &Belief| -> bool {
131 belief.confidence >= threshold
132 || belief.reinforcement_count >= 3
133 || now.saturating_sub(belief.last_reinforced_at) < 3600 };
135 self.about_user.retain(retain);
136 self.about_system.retain(retain);
137 self.about_task.retain(retain);
138 self.about_conversation.retain(retain);
139 }
140
141 pub fn format_for_prompt(&self, threshold: f64) -> String {
144 let beliefs = self.above_confidence(threshold);
145 if beliefs.is_empty() {
146 return String::new();
147 }
148 let mut lines = vec!["## Agent Beliefs".to_string()];
149 for belief in &beliefs {
150 if matches!(&belief.source, BeliefSource::ToolResult { .. }) {
152 continue;
153 }
154 let source_str = match &belief.source {
155 BeliefSource::UserStatement => "user said",
156 BeliefSource::ToolResult { .. } => "tool result",
157 BeliefSource::Inference { .. } => "inferred",
158 BeliefSource::SystemPrompt => "configured",
159 BeliefSource::PersistentMemory => "previous session",
160 };
161 lines.push(format!(
162 "- [{}] {} (confidence: {:.1}, reinforced: {})",
163 source_str, belief.statement, belief.confidence, belief.reinforcement_count
164 ));
165 }
166 if lines.len() == 1 {
167 return String::new();
168 }
169 lines.join("\n")
170 }
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct Belief {
176 pub id: String,
178 pub statement: String,
180 pub confidence: f64,
182 pub source: BeliefSource,
184 pub created_at: u64,
186 pub last_reinforced_at: u64,
188 pub reinforcement_count: u64,
190}
191
192#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
194#[serde(rename_all = "snake_case")]
195pub enum BeliefSource {
196 UserStatement,
198 ToolResult {
200 tool: String,
202 },
203 Inference {
205 by: String,
207 },
208 SystemPrompt,
210 PersistentMemory,
212}
213
214impl BeliefSource {
215 pub fn default_category(&self) -> BeliefCategory {
217 match self {
218 BeliefSource::UserStatement => BeliefCategory::User,
219 BeliefSource::ToolResult { .. } => BeliefCategory::System,
220 BeliefSource::Inference { .. } => BeliefCategory::Task,
221 BeliefSource::SystemPrompt => BeliefCategory::System,
222 BeliefSource::PersistentMemory => BeliefCategory::User,
223 }
224 }
225}
226
227#[derive(Debug, Clone, Copy, PartialEq, Eq)]
229pub enum BeliefCategory {
230 User,
232 System,
234 Task,
236 Conversation,
238}
239
240fn unix_timestamp() -> u64 {
241 SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0)
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247
248 #[test]
249 fn test_add_belief() {
250 let mut beliefs = AgentBeliefs::default();
251 beliefs.add_or_reinforce("user prefers Python", 0.8, BeliefSource::UserStatement);
252 assert_eq!(beliefs.about_user.len(), 1);
253 assert_eq!(beliefs.above_confidence(0.5).len(), 1);
254 }
255
256 #[test]
257 fn test_reinforce_belief() {
258 let mut beliefs = AgentBeliefs::default();
259 beliefs.add_or_reinforce("user prefers Python", 0.6, BeliefSource::UserStatement);
260 beliefs.add_or_reinforce("user prefers Python", 0.9, BeliefSource::UserStatement);
261 assert_eq!(beliefs.about_user.len(), 1);
262 assert!((beliefs.about_user[0].confidence - 0.9).abs() < f64::EPSILON);
263 assert_eq!(beliefs.about_user[0].reinforcement_count, 2);
264 }
265
266 #[test]
267 fn test_prune_low_confidence() {
268 let mut beliefs = AgentBeliefs::default();
269 beliefs.add_or_reinforce("high confidence", 0.9, BeliefSource::UserStatement);
270 beliefs.add_or_reinforce("low confidence", 0.05, BeliefSource::ToolResult { tool: "ls".to_string() });
272 if let Some(low) = beliefs.about_system.last_mut() {
273 low.last_reinforced_at = 1; }
275 assert_eq!(beliefs.above_confidence(0.0).len(), 2);
276 beliefs.prune_low_confidence(0.5);
277 let remaining = beliefs.above_confidence(0.0);
278 assert_eq!(remaining.len(), 1);
279 assert!(remaining[0].statement.contains("high"));
280 }
281
282 #[test]
283 fn test_format_for_prompt() {
284 let mut beliefs = AgentBeliefs::default();
285 beliefs.add_or_reinforce("user prefers Python", 0.8, BeliefSource::UserStatement);
286 let formatted = beliefs.format_for_prompt(0.5);
287 assert!(formatted.contains("user prefers Python"));
288 assert!(formatted.contains("[user said]"));
289 }
290
291 #[test]
292 fn test_belief_serde_roundtrip() {
293 let belief = Belief {
294 id: "test_1".to_string(),
295 statement: "the answer is 42".to_string(),
296 confidence: 0.95,
297 source: BeliefSource::Inference { by: "test".to_string() },
298 created_at: 1000,
299 last_reinforced_at: 1000,
300 reinforcement_count: 1,
301 };
302 let json = serde_json::to_string(&belief).unwrap();
303 let deserialized: Belief = serde_json::from_str(&json).unwrap();
304 assert_eq!(deserialized.statement, "the answer is 42");
305 assert!((deserialized.confidence - 0.95).abs() < f64::EPSILON);
306 }
307
308 #[test]
309 fn test_belief_source_default_category() {
310 assert_eq!(BeliefSource::UserStatement.default_category(), BeliefCategory::User);
311 assert_eq!(
312 BeliefSource::ToolResult { tool: "read_file".to_string() }.default_category(),
313 BeliefCategory::System
314 );
315 assert_eq!(BeliefSource::SystemPrompt.default_category(), BeliefCategory::System);
316 assert_eq!(BeliefSource::PersistentMemory.default_category(), BeliefCategory::User);
317 }
318}