1use std::fs;
14use std::io::Write;
15use std::path::Path;
16use std::time::{SystemTime, UNIX_EPOCH};
17
18pub const NUM_LAYERS: usize = 7;
22pub const LAYER_NAMES: [&str; NUM_LAYERS] = [
23 "Hebbian",
24 "Mirror",
25 "Resonance",
26 "Archetype",
27 "Emotional",
28 "ThoughtGraph",
29 "PredictiveCache",
30];
31
32const MAX_HISTORY: usize = 200;
33const LEARN_RATE: f32 = 0.05;
34const QUALITY_GOOD_THRESHOLD: f32 = 0.7;
35const QUALITY_BAD_THRESHOLD: f32 = 0.3;
36
37const SATISFIED_MS: u64 = 60_000; const UNSATISFIED_MS: u64 = 5_000; pub struct AttentionSignals {
45 pub query_length: usize,
46 pub emotional_energy: f32,
47 pub emotional_intensity: f32,
49 pub session_depth: usize,
50 pub pattern_confidence: f32,
51 pub cache_hit_rate: f32,
52 pub archetype_match_score: f32,
53}
54
55#[derive(Clone, Debug)]
59pub struct AttentionVector {
60 pub weights: [f32; NUM_LAYERS],
61}
62
63impl AttentionVector {
64 pub fn weight(&self, layer: usize) -> f32 {
66 if layer < NUM_LAYERS {
67 self.weights[layer]
68 } else {
69 1.0
70 }
71 }
72}
73
74#[derive(Clone, Debug)]
78pub struct AttentionOutcome {
79 pub weights: [f32; NUM_LAYERS],
80 pub timestamp_ms: u64,
81 pub quality: f32,
82}
83
84const OUTCOME_BYTES: usize = NUM_LAYERS * 4 + 8 + 4; #[derive(Clone)]
89pub struct AttentionState {
90 pub learned_weights: [f32; NUM_LAYERS],
92 pub history: Vec<AttentionOutcome>,
94 pub last_recall_ms: u64,
96 pub total_recalls: u32,
98}
99
100impl AttentionState {
101 pub fn load_or_init(output_dir: &Path) -> Self {
102 let path = output_dir.join("attention.bin");
103 if path.exists() {
104 load_attention(&path)
105 } else {
106 Self {
107 learned_weights: [1.0; NUM_LAYERS],
108 history: Vec::new(),
109 last_recall_ms: 0,
110 total_recalls: 0,
111 }
112 }
113 }
114
115 pub fn compute_attention(&self, signals: &AttentionSignals) -> AttentionVector {
117 let mut raw = [1.0f32; NUM_LAYERS];
118
119 if signals.query_length <= 10 {
121 raw[0] *= 1.5; raw[3] *= 1.3; } else {
124 raw[5] *= 1.3; }
126
127 raw[4] *= 1.0 + signals.emotional_energy.min(2.0);
129 raw[4] *= 1.0 + signals.emotional_intensity.min(2.0);
130
131 let depth_factor = (signals.session_depth as f32 / 5.0).min(1.0);
133 raw[5] *= 1.0 + depth_factor * 0.5; raw[6] *= 1.0 + depth_factor * 0.3; raw[5] *= 1.0 + signals.pattern_confidence;
138
139 raw[6] *= 1.0 + signals.cache_hit_rate;
141
142 raw[3] *= 1.0 + signals.archetype_match_score.min(2.0);
144
145 for (i, w) in raw.iter_mut().enumerate() {
147 *w = *w * 0.8 + self.learned_weights[i] * 0.2;
148 }
149
150 let sum: f32 = raw.iter().sum();
152 if sum > 0.0 && sum.is_finite() {
153 let scale = NUM_LAYERS as f32 / sum;
154 for w in &mut raw {
155 *w *= scale;
156 }
157 }
158
159 AttentionVector { weights: raw }
160 }
161
162 pub fn infer_quality(&self) -> f32 {
165 if self.last_recall_ms == 0 {
166 return 0.5; }
168 let now = now_epoch_ms();
169 let gap = now.saturating_sub(self.last_recall_ms);
170
171 if gap >= SATISFIED_MS {
172 1.0
173 } else if gap <= UNSATISFIED_MS {
174 0.2
175 } else {
176 let t = (gap - UNSATISFIED_MS) as f32 / (SATISFIED_MS - UNSATISFIED_MS) as f32;
178 0.2 + t * 0.8
179 }
180 }
181
182 pub fn record_outcome(&mut self, quality: f32, weights: &[f32; NUM_LAYERS]) {
184 self.history.push(AttentionOutcome {
185 weights: *weights,
186 timestamp_ms: now_epoch_ms(),
187 quality,
188 });
189
190 if self.history.len() > MAX_HISTORY {
191 self.history.drain(0..(self.history.len() - MAX_HISTORY));
192 }
193
194 self.update_learned_weights();
195 }
196
197 pub fn mark_recall(&mut self) {
199 self.last_recall_ms = now_epoch_ms();
200 self.total_recalls += 1;
201 }
202
203 fn update_learned_weights(&mut self) {
205 let good: Vec<&AttentionOutcome> = self
206 .history
207 .iter()
208 .filter(|o| o.quality >= QUALITY_GOOD_THRESHOLD)
209 .collect();
210 let bad: Vec<&AttentionOutcome> = self
211 .history
212 .iter()
213 .filter(|o| o.quality <= QUALITY_BAD_THRESHOLD)
214 .collect();
215
216 if good.is_empty() && bad.is_empty() {
217 return;
218 }
219
220 for i in 0..NUM_LAYERS {
221 let good_avg = if good.is_empty() {
222 self.learned_weights[i]
223 } else {
224 good.iter().map(|o| o.weights[i]).sum::<f32>() / good.len() as f32
225 };
226 let bad_avg = if bad.is_empty() {
227 self.learned_weights[i]
228 } else {
229 bad.iter().map(|o| o.weights[i]).sum::<f32>() / bad.len() as f32
230 };
231
232 let delta = good_avg - bad_avg;
233 self.learned_weights[i] += delta * LEARN_RATE;
234 self.learned_weights[i] = self.learned_weights[i].clamp(0.1, 3.0);
235 }
236 }
237
238 pub fn save(&self, output_dir: &Path) -> Result<(), String> {
240 save_attention(&output_dir.join("attention.bin"), self)
241 }
242}
243
244fn now_epoch_ms() -> u64 {
247 SystemTime::now()
248 .duration_since(UNIX_EPOCH)
249 .unwrap_or_default()
250 .as_millis() as u64
251}
252
253fn save_attention(path: &Path, state: &AttentionState) -> Result<(), String> {
254 let mut buf = Vec::with_capacity(48 + state.history.len() * OUTCOME_BYTES);
255
256 buf.write_all(b"ATT1").map_err(|e| e.to_string())?;
258 buf.write_all(&state.total_recalls.to_le_bytes())
259 .map_err(|e| e.to_string())?;
260 buf.write_all(&state.last_recall_ms.to_le_bytes())
261 .map_err(|e| e.to_string())?;
262
263 for &w in &state.learned_weights {
265 buf.write_all(&w.to_le_bytes()).map_err(|e| e.to_string())?;
266 }
267
268 buf.write_all(&(state.history.len() as u32).to_le_bytes())
270 .map_err(|e| e.to_string())?;
271
272 for outcome in &state.history {
273 for &w in &outcome.weights {
274 buf.write_all(&w.to_le_bytes()).map_err(|e| e.to_string())?;
275 }
276 buf.write_all(&outcome.timestamp_ms.to_le_bytes())
277 .map_err(|e| e.to_string())?;
278 buf.write_all(&outcome.quality.to_le_bytes())
279 .map_err(|e| e.to_string())?;
280 }
281
282 let tmp_path = path.with_extension("bin.tmp");
283 fs::write(&tmp_path, &buf).map_err(|e| e.to_string())?;
284 fs::rename(&tmp_path, path).map_err(|e| e.to_string())
285}
286
287fn load_attention(path: &Path) -> AttentionState {
288 let data = match fs::read(path) {
289 Ok(d) => d,
290 Err(_) => {
291 return AttentionState {
292 learned_weights: [1.0; NUM_LAYERS],
293 history: Vec::new(),
294 last_recall_ms: 0,
295 total_recalls: 0,
296 }
297 }
298 };
299
300 if data.len() < 48 || &data[0..4] != b"ATT1" {
302 return AttentionState {
303 learned_weights: [1.0; NUM_LAYERS],
304 history: Vec::new(),
305 last_recall_ms: 0,
306 total_recalls: 0,
307 };
308 }
309
310 let total_recalls = u32::from_le_bytes([data[4], data[5], data[6], data[7]]);
311 let last_recall_ms = u64::from_le_bytes([
312 data[8], data[9], data[10], data[11], data[12], data[13], data[14], data[15],
313 ]);
314
315 let mut learned_weights = [0.0f32; NUM_LAYERS];
316 let mut offset = 16;
317 for w in &mut learned_weights {
318 *w = f32::from_le_bytes([
319 data[offset],
320 data[offset + 1],
321 data[offset + 2],
322 data[offset + 3],
323 ]);
324 if w.is_nan() || w.is_infinite() {
326 *w = 1.0;
327 }
328 offset += 4;
329 }
330
331 let history_count = u32::from_le_bytes([
332 data[offset],
333 data[offset + 1],
334 data[offset + 2],
335 data[offset + 3],
336 ]) as usize;
337 offset += 4;
338
339 let mut history = Vec::with_capacity(history_count);
340 for _ in 0..history_count {
341 if offset + OUTCOME_BYTES > data.len() {
342 break;
343 }
344
345 let mut weights = [0.0f32; NUM_LAYERS];
346 for w in &mut weights {
347 *w = f32::from_le_bytes([
348 data[offset],
349 data[offset + 1],
350 data[offset + 2],
351 data[offset + 3],
352 ]);
353 if w.is_nan() || w.is_infinite() {
354 *w = 1.0;
355 }
356 offset += 4;
357 }
358
359 let timestamp_ms = u64::from_le_bytes([
360 data[offset],
361 data[offset + 1],
362 data[offset + 2],
363 data[offset + 3],
364 data[offset + 4],
365 data[offset + 5],
366 data[offset + 6],
367 data[offset + 7],
368 ]);
369 offset += 8;
370
371 let quality = f32::from_le_bytes([
372 data[offset],
373 data[offset + 1],
374 data[offset + 2],
375 data[offset + 3],
376 ]);
377 let quality = if quality.is_nan() || quality.is_infinite() {
378 0.5
379 } else {
380 quality
381 };
382 offset += 4;
383
384 history.push(AttentionOutcome {
385 weights,
386 timestamp_ms,
387 quality,
388 });
389 }
390
391 AttentionState {
392 learned_weights,
393 history,
394 last_recall_ms,
395 total_recalls,
396 }
397}
398
399#[cfg(test)]
402mod tests {
403 use super::*;
404
405 fn default_signals() -> AttentionSignals {
406 AttentionSignals {
407 query_length: 20,
408 emotional_energy: 0.0,
409 emotional_intensity: 0.0,
410 session_depth: 0,
411 pattern_confidence: 0.0,
412 cache_hit_rate: 0.0,
413 archetype_match_score: 0.0,
414 }
415 }
416
417 #[test]
418 fn test_compute_attention_short_query() {
419 let state = AttentionState {
420 learned_weights: [1.0; NUM_LAYERS],
421 history: Vec::new(),
422 last_recall_ms: 0,
423 total_recalls: 0,
424 };
425 let mut signals = default_signals();
426 signals.query_length = 5; let attn = state.compute_attention(&signals);
429 assert!(attn.weights[0] > attn.weights[1]); assert!(attn.weights[3] > attn.weights[1]); }
433
434 #[test]
435 fn test_compute_attention_long_query() {
436 let state = AttentionState {
437 learned_weights: [1.0; NUM_LAYERS],
438 history: Vec::new(),
439 last_recall_ms: 0,
440 total_recalls: 0,
441 };
442 let mut signals = default_signals();
443 signals.query_length = 50; let attn = state.compute_attention(&signals);
446 assert!(attn.weights[5] > attn.weights[1]); }
449
450 #[test]
451 fn test_compute_attention_high_emotion() {
452 let state = AttentionState {
453 learned_weights: [1.0; NUM_LAYERS],
454 history: Vec::new(),
455 last_recall_ms: 0,
456 total_recalls: 0,
457 };
458 let mut signals = default_signals();
459 signals.emotional_energy = 2.0;
460
461 let attn = state.compute_attention(&signals);
462 assert!(attn.weights[4] > attn.weights[0]);
464 }
465
466 #[test]
467 fn test_compute_attention_deep_session() {
468 let state = AttentionState {
469 learned_weights: [1.0; NUM_LAYERS],
470 history: Vec::new(),
471 last_recall_ms: 0,
472 total_recalls: 0,
473 };
474 let mut signals = default_signals();
475 signals.session_depth = 10;
476
477 let attn = state.compute_attention(&signals);
478 assert!(attn.weights[5] > attn.weights[1]);
480 assert!(attn.weights[6] > attn.weights[1]);
481 }
482
483 #[test]
484 fn test_normalization() {
485 let state = AttentionState {
486 learned_weights: [1.0; NUM_LAYERS],
487 history: Vec::new(),
488 last_recall_ms: 0,
489 total_recalls: 0,
490 };
491 let signals = default_signals();
492 let attn = state.compute_attention(&signals);
493
494 let sum: f32 = attn.weights.iter().sum();
495 assert!((sum - NUM_LAYERS as f32).abs() < 0.01);
496 }
497
498 #[test]
499 fn test_quality_inference_fast_requery() {
500 let state = AttentionState {
501 learned_weights: [1.0; NUM_LAYERS],
502 history: Vec::new(),
503 last_recall_ms: now_epoch_ms() - 2_000, total_recalls: 1,
505 };
506 let q = state.infer_quality();
507 assert!(q < 0.3); }
509
510 #[test]
511 fn test_quality_inference_satisfied() {
512 let state = AttentionState {
513 learned_weights: [1.0; NUM_LAYERS],
514 history: Vec::new(),
515 last_recall_ms: now_epoch_ms() - 120_000, total_recalls: 1,
517 };
518 let q = state.infer_quality();
519 assert!((q - 1.0).abs() < 0.01);
520 }
521
522 #[test]
523 fn test_learned_weights_update() {
524 let mut state = AttentionState {
525 learned_weights: [1.0; NUM_LAYERS],
526 history: Vec::new(),
527 last_recall_ms: 0,
528 total_recalls: 0,
529 };
530
531 let mut good_weights = [1.0f32; NUM_LAYERS];
533 good_weights[0] = 2.0; for _ in 0..5 {
535 state.record_outcome(0.9, &good_weights);
536 }
537
538 let mut bad_weights = [1.0f32; NUM_LAYERS];
540 bad_weights[1] = 2.0; for _ in 0..5 {
542 state.record_outcome(0.1, &bad_weights);
543 }
544
545 assert!(state.learned_weights[0] > state.learned_weights[1]);
547 }
548
549 #[test]
550 fn test_save_load_roundtrip() {
551 let dir = tempfile::tempdir().unwrap();
552 let mut state = AttentionState {
553 learned_weights: [1.1, 0.9, 1.0, 1.3, 0.8, 1.2, 0.7],
554 history: Vec::new(),
555 last_recall_ms: 12345678,
556 total_recalls: 42,
557 };
558 state.history.push(AttentionOutcome {
559 weights: [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0],
560 timestamp_ms: 999,
561 quality: 0.75,
562 });
563
564 state.save(dir.path()).unwrap();
565 let loaded = AttentionState::load_or_init(dir.path());
566
567 assert_eq!(loaded.total_recalls, 42);
568 assert_eq!(loaded.last_recall_ms, 12345678);
569 assert!((loaded.learned_weights[0] - 1.1).abs() < 0.001);
570 assert!((loaded.learned_weights[6] - 0.7).abs() < 0.001);
571 assert_eq!(loaded.history.len(), 1);
572 assert!((loaded.history[0].quality - 0.75).abs() < 0.001);
573 }
574
575 #[test]
576 fn test_history_cap() {
577 let mut state = AttentionState {
578 learned_weights: [1.0; NUM_LAYERS],
579 history: Vec::new(),
580 last_recall_ms: 0,
581 total_recalls: 0,
582 };
583 let weights = [1.0; NUM_LAYERS];
584 for _ in 0..MAX_HISTORY + 50 {
585 state.record_outcome(0.5, &weights);
586 }
587 assert!(state.history.len() <= MAX_HISTORY);
588 }
589}