writerslogic_warm/core/cognition/
loop.rs1use crate::core::sync::RwLock;
11use std::sync::atomic::{AtomicBool, Ordering};
12use std::sync::Arc;
13use std::time::Duration;
14
15use crate::core::atom_memory::AtomMemory;
16use crate::core::cognition::abstraction::{Abstraction, AbstractionEngine};
17use crate::core::cognition::analogy::{Analogy, AnalogyDetector};
18use crate::core::cognition::gaps::{GapDetector, KnowledgeGap};
19use crate::core::cognition::hypothesis::{Hypothesis, HypothesisEngine};
20use crate::core::cognition::patterns::{PatternScanner, RelationPattern};
21use crate::core::triple_store::TripleStore;
22
23#[derive(Clone, Debug)]
25pub enum Insight {
26 Pattern(RelationPattern),
27 Abstraction(Abstraction),
28 Gap(KnowledgeGap),
29 Hypothesis(Hypothesis),
30 Analogy(Analogy),
31}
32
33#[derive(Clone, Debug)]
35pub struct CognitionConfig {
36 pub interval: Duration,
38 pub min_pattern_freq: usize,
40 pub min_abstraction_members: usize,
42 pub min_shared_relations: usize,
44 pub min_peer_coverage: f64,
46 pub hypothesis_beta: f64,
48 pub min_hypothesis_confidence: f64,
50 pub min_analogy_relations: usize,
52}
53
54impl Default for CognitionConfig {
55 fn default() -> Self {
56 Self {
57 interval: Duration::from_secs(60),
58 min_pattern_freq: 3,
59 min_abstraction_members: 3,
60 min_shared_relations: 2,
61 min_peer_coverage: 0.5,
62 hypothesis_beta: 24.0,
63 min_hypothesis_confidence: 0.3,
64 min_analogy_relations: 2,
65 }
66 }
67}
68
69pub struct CognitionState {
71 insights: RwLock<Vec<Insight>>,
72 running: AtomicBool,
73 stop: AtomicBool,
74 cycle_count: std::sync::atomic::AtomicU64,
75}
76
77impl CognitionState {
78 fn new() -> Self {
79 Self {
80 insights: RwLock::new(Vec::new()),
81 running: AtomicBool::new(false),
82 stop: AtomicBool::new(false),
83 cycle_count: std::sync::atomic::AtomicU64::new(0),
84 }
85 }
86
87 pub fn is_running(&self) -> bool {
88 self.running.load(Ordering::SeqCst)
89 }
90
91 pub fn cycle_count(&self) -> u64 {
92 self.cycle_count.load(Ordering::SeqCst)
93 }
94
95 pub fn take_insights(&self) -> Vec<Insight> {
96 std::mem::take(&mut *self.insights.write())
97 }
98
99 pub fn insight_count(&self) -> usize {
100 self.insights.read().len()
101 }
102}
103
104pub struct CognitionLoop {
107 state: Arc<CognitionState>,
108 handle: Option<std::thread::JoinHandle<()>>,
109}
110
111impl CognitionLoop {
112 pub fn start(
117 atom_memory: Arc<AtomMemory>,
118 triple_store: Arc<TripleStore>,
119 config: CognitionConfig,
120 ) -> Self {
121 let state = Arc::new(CognitionState::new());
122 let thread_state = Arc::clone(&state);
123
124 state.running.store(true, Ordering::SeqCst);
125
126 let handle = std::thread::Builder::new()
127 .name("warm-cognition".to_string())
128 .spawn(move || {
129 cognition_thread(&thread_state, &atom_memory, &triple_store, &config);
130 })
131 .expect("failed to spawn cognition thread");
132
133 Self {
134 state,
135 handle: Some(handle),
136 }
137 }
138
139 pub fn stop(&mut self) {
141 self.state.stop.store(true, Ordering::SeqCst);
142 if let Some(handle) = self.handle.take() {
143 let _ = handle.join();
144 }
145 self.state.running.store(false, Ordering::SeqCst);
146 }
147
148 pub fn state(&self) -> &Arc<CognitionState> {
150 &self.state
151 }
152
153 pub fn run_once(
155 atom_memory: &AtomMemory,
156 triple_store: &TripleStore,
157 config: &CognitionConfig,
158 ) -> Vec<Insight> {
159 run_cycle(atom_memory, triple_store, config)
160 }
161}
162
163impl Drop for CognitionLoop {
164 fn drop(&mut self) {
165 self.stop();
166 }
167}
168
169fn cognition_thread(
170 state: &CognitionState,
171 atom_memory: &AtomMemory,
172 triple_store: &TripleStore,
173 config: &CognitionConfig,
174) {
175 while !state.stop.load(Ordering::SeqCst) {
176 *state.insights.write() = run_cycle(atom_memory, triple_store, config);
179
180 state.cycle_count.fetch_add(1, Ordering::SeqCst);
181
182 let mut remaining = config.interval;
184 let tick = Duration::from_millis(100);
185 while remaining > Duration::ZERO && !state.stop.load(Ordering::SeqCst) {
186 let sleep_time = remaining.min(tick);
187 std::thread::sleep(sleep_time);
188 remaining = remaining.saturating_sub(sleep_time);
189 }
190 }
191
192 state.running.store(false, Ordering::SeqCst);
193}
194
195fn run_cycle(
196 atom_memory: &AtomMemory,
197 triple_store: &TripleStore,
198 config: &CognitionConfig,
199) -> Vec<Insight> {
200 let mut insights = Vec::new();
201
202 let patterns = PatternScanner::scan_relation_patterns(triple_store, config.min_pattern_freq);
204 for p in patterns {
205 insights.push(Insight::Pattern(p));
206 }
207
208 let abstractions =
210 AbstractionEngine::discover(triple_store, atom_memory, config.min_abstraction_members);
211 for a in abstractions {
212 insights.push(Insight::Abstraction(a));
213 }
214
215 let gaps = GapDetector::detect(
217 triple_store,
218 config.min_shared_relations,
219 config.min_peer_coverage,
220 );
221
222 let hypotheses = HypothesisEngine::propose(
224 &gaps,
225 triple_store,
226 atom_memory,
227 config.hypothesis_beta,
228 config.min_hypothesis_confidence,
229 );
230 for h in hypotheses {
231 insights.push(Insight::Hypothesis(h));
232 }
233
234 for g in gaps {
236 insights.push(Insight::Gap(g));
237 }
238
239 let analogies = AnalogyDetector::detect(triple_store, config.min_analogy_relations);
241 for a in analogies {
242 insights.push(Insight::Analogy(a));
243 }
244
245 insights
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251
252 fn make_test_data() -> (AtomMemory, TripleStore) {
253 let dim = 16384;
254 let atom_mem = AtomMemory::new(dim, 3.0);
255 let triple_store = TripleStore::new();
256
257 for city in &["paris", "berlin", "tokyo", "london", "madrid"] {
258 atom_mem.get_or_insert(city);
259 }
260 for country in &["france", "germany", "japan", "uk", "spain", "europe"] {
261 atom_mem.get_or_insert(country);
262 }
263
264 triple_store.add("paris", "capital_of", "france", "c1");
265 triple_store.add("berlin", "capital_of", "germany", "c2");
266 triple_store.add("tokyo", "capital_of", "japan", "c3");
267 triple_store.add("london", "capital_of", "uk", "c4");
268 triple_store.add("madrid", "capital_of", "spain", "c5");
269 triple_store.add("paris", "located_in", "europe", "c6");
270 triple_store.add("berlin", "located_in", "europe", "c7");
271 triple_store.add("london", "located_in", "europe", "c8");
272 triple_store.add("madrid", "located_in", "europe", "c9");
273 (atom_mem, triple_store)
276 }
277
278 #[test]
279 fn test_run_once() {
280 let (atom_mem, triple_store) = make_test_data();
281 let config = CognitionConfig::default();
282 let insights = CognitionLoop::run_once(&atom_mem, &triple_store, &config);
283 assert!(!insights.is_empty());
284
285 assert!(insights.iter().any(|i| matches!(i, Insight::Pattern(_))));
287 }
288
289 #[test]
290 fn test_run_once_finds_gaps() {
291 let (atom_mem, triple_store) = make_test_data();
292 let config = CognitionConfig {
293 min_shared_relations: 1,
294 min_peer_coverage: 0.5,
295 ..Default::default()
296 };
297 let insights = CognitionLoop::run_once(&atom_mem, &triple_store, &config);
298
299 let has_gap = insights.iter().any(|i| {
300 if let Insight::Gap(g) = i {
301 g.entity == "tokyo" && g.missing_relation == "located_in"
302 } else {
303 false
304 }
305 });
306 assert!(has_gap, "Should detect tokyo missing located_in");
307 }
308
309 #[test]
310 fn test_background_loop_start_stop() {
311 let (atom_mem, triple_store) = make_test_data();
312 let config = CognitionConfig {
313 interval: Duration::from_millis(50),
314 ..Default::default()
315 };
316
317 let mut cognition =
318 CognitionLoop::start(Arc::new(atom_mem), Arc::new(triple_store), config);
319
320 assert!(cognition.state().is_running());
321
322 std::thread::sleep(Duration::from_millis(200));
324
325 let cycles = cognition.state().cycle_count();
326 assert!(
327 cycles >= 1,
328 "Should have completed at least 1 cycle, got {}",
329 cycles
330 );
331
332 cognition.stop();
333 assert!(!cognition.state().is_running());
334 }
335
336 #[test]
337 fn test_take_insights() {
338 let (atom_mem, triple_store) = make_test_data();
339 let config = CognitionConfig {
340 interval: Duration::from_millis(50),
341 ..Default::default()
342 };
343
344 let mut cognition =
345 CognitionLoop::start(Arc::new(atom_mem), Arc::new(triple_store), config);
346
347 std::thread::sleep(Duration::from_millis(200));
348
349 let insights = cognition.state().take_insights();
350 assert!(!insights.is_empty());
351
352 cognition.stop();
355 let remaining = cognition.state().take_insights();
356 let _ = remaining;
358 }
359
360 #[test]
361 fn pending_insights_do_not_grow_across_cycles_of_an_unchanged_store() {
362 let (atom_mem, triple_store) = make_test_data();
363 let config = CognitionConfig {
364 interval: Duration::from_millis(1),
365 ..Default::default()
366 };
367 let per_cycle = CognitionLoop::run_once(&atom_mem, &triple_store, &config).len();
368 assert!(per_cycle > 0);
369 let mut cognition =
370 CognitionLoop::start(Arc::new(atom_mem), Arc::new(triple_store), config);
371 let deadline = std::time::Instant::now() + Duration::from_secs(10);
372 while cognition.state().cycle_count() < 3 && std::time::Instant::now() < deadline {
373 std::thread::sleep(Duration::from_millis(5));
374 }
375 cognition.stop();
376 assert!(cognition.state().cycle_count() >= 3);
377 assert_eq!(cognition.state().insight_count(), per_cycle);
378 }
379
380 #[test]
381 fn test_empty_store_no_panic() {
382 let atom_mem = AtomMemory::new(16384, 3.0);
383 let triple_store = TripleStore::new();
384 let config = CognitionConfig::default();
385 let insights = CognitionLoop::run_once(&atom_mem, &triple_store, &config);
386 assert!(insights.is_empty());
387 }
388}