microscope_memory/
curiosity.rs1use std::fs;
15use std::path::Path;
16use std::time::{SystemTime, UNIX_EPOCH};
17
18use crate::config::Config;
19use crate::hebbian::HebbianState;
20use crate::reader::MicroscopeReader;
21use crate::spaced_repetition::SpacedRepetition;
22use colored::Colorize;
23
24const MAX_CURIOSITY_QUERIES: usize = 20;
25const MAX_STR_LEN: usize = 256;
26
27#[derive(Clone, Debug)]
28pub struct CuriosityQuery {
29 pub timestamp_ms: u64,
30 pub query: String,
31 pub reason: String,
32 pub score: f32,
33}
34
35impl CuriosityQuery {
36 pub fn to_bytes(&self) -> Vec<u8> {
37 let qb = self.query.as_bytes();
38 let rb = self.reason.as_bytes();
39 let qlen = qb.len().min(MAX_STR_LEN) as u16;
40 let rlen = rb.len().min(MAX_STR_LEN) as u16;
41 let mut buf = Vec::with_capacity(20 + qlen as usize + rlen as usize);
42 buf.extend_from_slice(b"CUR1");
43 buf.extend_from_slice(&self.timestamp_ms.to_le_bytes());
44 buf.extend_from_slice(&qlen.to_le_bytes());
45 buf.extend_from_slice(&qb[..qlen as usize]);
46 buf.extend_from_slice(&rlen.to_le_bytes());
47 buf.extend_from_slice(&rb[..rlen as usize]);
48 buf.extend_from_slice(&self.score.to_le_bytes());
49 buf
50 }
51
52 pub fn from_bytes(data: &[u8]) -> Option<Self> {
53 if data.len() < 20 || &data[0..4] != b"CUR1" {
54 return None;
55 }
56 let mut pos = 4;
57 let ts = u64::from_le_bytes(data[pos..pos + 8].try_into().ok()?);
58 pos += 8;
59 let qlen = u16::from_le_bytes(data[pos..pos + 2].try_into().unwrap_or([0; 2])) as usize;
60 pos += 2;
61 let query = if qlen > 0 {
62 String::from_utf8_lossy(&data[pos..pos + qlen]).to_string()
63 } else {
64 String::new()
65 };
66 pos += qlen;
67 let rlen = u16::from_le_bytes(data[pos..pos + 2].try_into().unwrap_or([0; 2])) as usize;
68 pos += 2;
69 let reason = if rlen > 0 {
70 String::from_utf8_lossy(&data[pos..pos + rlen]).to_string()
71 } else {
72 String::new()
73 };
74 pos += rlen;
75 let score = f32::from_le_bytes(data[pos..pos + 4].try_into().ok()?);
76 Some(Self {
77 timestamp_ms: ts,
78 query,
79 reason,
80 score,
81 })
82 }
83}
84
85pub struct CuriosityState {
86 pub queries: Vec<CuriosityQuery>,
87}
88
89impl CuriosityState {
90 pub fn load_or_init(output_dir: &Path) -> Self {
91 let path = output_dir.join("curiosity.bin");
92 let mut queries = Vec::new();
93 if let Ok(data) = fs::read(&path) {
94 let mut pos = 0;
95 while pos + 20 <= data.len() {
96 if let Some(q) = CuriosityQuery::from_bytes(&data[pos..]) {
97 let size = 4
98 + 8
99 + 2
100 + q.query.len().min(MAX_STR_LEN)
101 + 2
102 + q.reason.len().min(MAX_STR_LEN)
103 + 4;
104 pos += size;
105 queries.push(q);
106 } else {
107 pos += 1;
108 }
109 }
110 }
111 Self { queries }
112 }
113
114 pub fn save(&self, output_dir: &Path) -> Result<(), String> {
115 let path = output_dir.join("curiosity.bin");
116 let mut buf = Vec::new();
117 for q in self.queries.iter().rev().take(MAX_CURIOSITY_QUERIES).rev() {
118 buf.extend_from_slice(&q.to_bytes());
119 }
120 let tmp_path = output_dir.join("curiosity.bin.tmp");
121 fs::write(&tmp_path, &buf).map_err(|e| format!("write curiosity.bin: {}", e))?;
122 fs::rename(&tmp_path, &path).map_err(|e| format!("rename curiosity.bin: {}", e))
123 }
124
125 pub fn generate_queries(
126 &mut self,
127 _config: &Config,
128 reader: &MicroscopeReader,
129 output_dir: &Path,
130 ) -> Vec<CuriosityQuery> {
131 let now_ms = SystemTime::now()
132 .duration_since(UNIX_EPOCH)
133 .unwrap_or_default()
134 .as_millis() as u64;
135 let hebb = HebbianState::load_or_init(output_dir, reader.block_count);
136 let spaced = SpacedRepetition::load_or_init(output_dir);
137 let mut new_queries = Vec::new();
138
139 let due = spaced.due_blocks();
141 if !due.is_empty() {
142 let texts: Vec<String> = due
143 .iter()
144 .take(3)
145 .map(|&idx| {
146 let text = reader.text(idx as usize);
147 crate::safe_truncate(text, 60)
148 })
149 .collect();
150 if !texts.is_empty() {
151 new_queries.push(CuriosityQuery {
152 timestamp_ms: now_ms,
153 query: format!("Why have I not recalled {} recently?", texts.join(", ")),
154 reason: format!(
155 "{} memories are due for spaced repetition review",
156 due.len()
157 ),
158 score: 0.5 + (due.len() as f32 * 0.05).min(0.5),
159 });
160 }
161 }
162
163 let mut hot_blocks: Vec<(u32, f32)> = hebb
165 .activations
166 .iter()
167 .enumerate()
168 .filter(|(_, a)| a.energy > 0.2)
169 .map(|(i, a)| (i as u32, a.energy))
170 .collect();
171 hot_blocks.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
172 for &(idx, energy) in hot_blocks.iter().take(2) {
173 let text = reader.text(idx as usize);
174 let preview = crate::safe_truncate(text, 50);
175 new_queries.push(CuriosityQuery {
176 timestamp_ms: now_ms,
177 query: format!("What makes '{}' so active?", preview),
178 reason: format!(
179 "block {} has energy {:.2} - highest in the system",
180 idx, energy
181 ),
182 score: energy * 0.8,
183 });
184 }
185
186 let archetypes = crate::archetype::ArchetypeState::load_or_init(output_dir);
188 if !archetypes.archetypes.is_empty() {
189 for arch in archetypes.archetypes.iter().take(2) {
190 new_queries.push(CuriosityQuery {
191 timestamp_ms: now_ms,
192 query: format!(
193 "What is the '{}' archetype and why is it forming?",
194 arch.label
195 ),
196 reason: format!(
197 "archetype '{}' has {} members and strength {:.3}",
198 arch.label,
199 arch.members.len(),
200 arch.strength
201 ),
202 score: arch.strength * 0.7,
203 });
204 }
205 }
206
207 let thought_graph = crate::thought_graph::ThoughtGraphState::load_or_init(output_dir);
209 let patterns = thought_graph.top_patterns(2);
210 for p in patterns {
211 new_queries.push(CuriosityQuery {
212 timestamp_ms: now_ms,
213 query: "What connects the blocks in this thought pattern?".to_string(),
214 reason: format!(
215 "thought pattern with {} blocks, frequency {}",
216 p.result_blocks.len(),
217 p.frequency
218 ),
219 score: (p.frequency as f32) * 0.1,
220 });
221 }
222
223 new_queries.push(CuriosityQuery {
225 timestamp_ms: now_ms,
226 query: "How am I changing over time?".to_string(),
227 reason: format!(
228 "{} total blocks, {} hot memories, {} due for review",
229 reader.block_count,
230 hot_blocks.len(),
231 due.len()
232 ),
233 score: 0.3,
234 });
235
236 self.queries.extend(new_queries.clone());
237 if self.queries.len() > MAX_CURIOSITY_QUERIES * 2 {
238 self.queries = self.queries[self.queries.len() - MAX_CURIOSITY_QUERIES..].to_vec();
239 }
240 let _ = self.save(output_dir);
241 new_queries
242 }
243}
244
245pub fn format_curiosity(queries: &[CuriosityQuery]) -> String {
246 let mut out = format!(" {} I am curious about:\n", "CURIOUS:".yellow().bold());
247 for q in queries.iter().take(5) {
248 out.push_str(&format!(
249 " \x20 [{:.2}] {} ({})\n",
250 q.score, q.query, q.reason
251 ));
252 }
253 out
254}