1pub mod adapter;
10pub mod extractor;
11pub mod hook;
12pub mod observer;
13pub mod store;
14
15pub use hook::EvolutionHookHandler;
16
17use chrono::{DateTime, Utc};
18use serde::{Deserialize, Serialize};
19use thiserror::Error;
20
21#[derive(Debug, Error)]
23pub enum EvolutionError {
24 #[error("filesystem operation failed: {0}")]
26 Io(#[from] std::io::Error),
27 #[error("knowledge store operation failed: {0}")]
29 Store(#[from] StoreError),
30}
31
32#[derive(Debug, Error)]
34pub enum StoreError {
35 #[error("database operation failed: {0}")]
37 Database(String),
38}
39
40impl From<rusqlite::Error> for StoreError {
41 fn from(err: rusqlite::Error) -> Self {
42 StoreError::Database(err.to_string())
43 }
44}
45
46impl From<rusqlite::Error> for EvolutionError {
47 fn from(err: rusqlite::Error) -> Self {
48 EvolutionError::Store(StoreError::from(err))
49 }
50}
51
52pub type EvolutionResult<T> = std::result::Result<T, EvolutionError>;
54
55#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
61pub enum SignalKind {
62 Correction,
64 Error,
66 Satisfaction,
68 Inefficiency,
70}
71
72impl std::fmt::Display for SignalKind {
73 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74 match self {
75 SignalKind::Correction => write!(f, "Correction"),
76 SignalKind::Error => write!(f, "Error"),
77 SignalKind::Satisfaction => write!(f, "Satisfaction"),
78 SignalKind::Inefficiency => write!(f, "Inefficiency"),
79 }
80 }
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
89pub struct Signal {
90 pub kind: SignalKind,
92 pub intensity: f32,
94 pub context: String,
96 pub tool_name: Option<String>,
98}
99
100#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
102pub enum TurnOutcome {
103 Success,
105 PartialSuccess,
107 Failure,
109 UserCorrected,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
115pub struct ToolUsage {
116 pub tool_name: String,
118 pub arguments_hash: u64,
120 pub result_summary: String,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct TurnObservation {
131 pub signals: Vec<Signal>,
133 pub tools_used: Vec<ToolUsage>,
135 pub outcome: TurnOutcome,
137 pub duration_ms: u64,
139 pub session_id: uuid::Uuid,
141 pub turn_number: u32,
143}
144
145impl TurnObservation {
146 pub fn new(
148 session_id: uuid::Uuid,
149 turn_number: u32,
150 outcome: TurnOutcome,
151 duration_ms: u64,
152 ) -> Self {
153 Self {
154 signals: Vec::new(),
155 tools_used: Vec::new(),
156 outcome,
157 duration_ms,
158 session_id,
159 turn_number,
160 }
161 }
162
163 pub fn add_signal(&mut self, signal: Signal) {
165 self.signals.push(signal);
166 }
167
168 pub fn add_tool_usage(&mut self, usage: ToolUsage) {
170 self.tools_used.push(usage);
171 }
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
181pub enum SignalType {
182 Correction,
184 Error,
186 Satisfaction,
188 Inefficiency,
190}
191
192#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct Observation {
198 pub id: String,
200 pub signal_type: SignalType,
202 pub intensity: f64,
204 pub context: String,
206 pub timestamp: DateTime<Utc>,
208 pub session_id: Option<String>,
210 pub turn_number: Option<u32>,
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize)]
216pub struct Pattern {
217 pub id: String,
219 pub description: String,
221 pub instruction: String,
223 pub confidence: f64,
225 pub evidence_count: u32,
227 pub first_observed: DateTime<Utc>,
229 pub last_updated: DateTime<Utc>,
231 pub category: String,
233 pub active: bool,
235 pub content_hash: String,
237 pub key: String,
240 pub value: serde_json::Value,
242 pub contradicting_count: u32,
244 pub last_reinforced: DateTime<Utc>,
246 pub source_sessions: Vec<uuid::Uuid>,
248}
249
250#[derive(Debug, Clone, Serialize, Deserialize)]
252pub struct Conflict {
253 pub id: String,
255 pub pattern_a_id: String,
257 pub pattern_b_id: String,
259 pub description: String,
261 pub detected_at: DateTime<Utc>,
263 pub resolved: bool,
265 pub winner_id: Option<String>,
267}
268
269#[derive(Debug, Clone, Serialize, Deserialize)]
271pub struct EvolutionConfig {
272 pub min_confidence: f64,
274 pub min_evidence: u32,
276 pub half_life_days: f64,
278 pub max_patterns: usize,
280 pub auto_extract: bool,
282 pub max_context_bytes: usize,
285 pub max_output_bytes: usize,
288}
289
290impl Default for EvolutionConfig {
291 fn default() -> Self {
292 Self {
293 min_confidence: 0.7,
294 min_evidence: 3,
295 half_life_days: 70.0,
296 max_patterns: 5,
297 auto_extract: true,
298 max_context_bytes: 4096,
299 max_output_bytes: 8192,
300 }
301 }
302}
303
304impl Observation {
305 pub fn new(
307 signal_type: SignalType,
308 intensity: f64,
309 context: String,
310 session_id: Option<String>,
311 turn_number: Option<u32>,
312 ) -> Self {
313 Self {
314 id: uuid::Uuid::new_v4().to_string(),
315 signal_type,
316 intensity,
317 context,
318 timestamp: Utc::now(),
319 session_id,
320 turn_number,
321 }
322 }
323
324 pub fn age_days(&self) -> f64 {
326 let now = Utc::now();
327 let duration = now.signed_duration_since(self.timestamp);
328 duration.num_days() as f64
329 }
330}
331
332pub fn compute_content_hash(category: &str, instruction: &str) -> String {
335 use std::collections::hash_map::DefaultHasher;
336 use std::hash::{Hash, Hasher};
337
338 let prefix = if instruction.len() > 1024 {
339 &instruction[..1024]
340 } else {
341 instruction
342 };
343 let fingerprint = format!("{category}|{prefix}");
344
345 let mut hasher = DefaultHasher::new();
346 fingerprint.hash(&mut hasher);
347 format!("{:016x}", hasher.finish())
348}
349
350impl Pattern {
351 pub fn new(description: String, instruction: String, category: String) -> Self {
356 let now = Utc::now();
357 let content_hash = compute_content_hash(&category, &instruction);
358 let key = category.clone();
359 let value = serde_json::json!({ "instruction": instruction });
360 Self {
361 id: uuid::Uuid::new_v4().to_string(),
362 description,
363 instruction,
364 confidence: 0.0,
365 evidence_count: 0,
366 first_observed: now,
367 last_updated: now,
368 category,
369 active: true,
370 content_hash,
371 key,
372 value,
373 contradicting_count: 0,
374 last_reinforced: now,
375 source_sessions: Vec::new(),
376 }
377 }
378
379 pub fn new_with_key(
384 key: String,
385 value: serde_json::Value,
386 category: String,
387 source_session: Option<uuid::Uuid>,
388 ) -> Self {
389 let now = Utc::now();
390 let description = format!("Pattern: {key}");
391 let instruction = value.to_string();
392 let content_hash = compute_content_hash(&category, &instruction);
393 let mut source_sessions = Vec::new();
394 if let Some(sid) = source_session {
395 source_sessions.push(sid);
396 }
397 Self {
398 id: uuid::Uuid::new_v4().to_string(),
399 description,
400 instruction,
401 confidence: 0.0,
402 evidence_count: 0,
403 first_observed: now,
404 last_updated: now,
405 category,
406 active: true,
407 content_hash,
408 key,
409 value,
410 contradicting_count: 0,
411 last_reinforced: now,
412 source_sessions,
413 }
414 }
415
416 pub fn decayed_confidence(&self, half_life_days: f64) -> f64 {
418 let age = self.last_updated.signed_duration_since(self.first_observed);
419 let days = age.num_days() as f64;
420 let decay = (-0.693 * days / half_life_days).exp();
421 self.confidence * decay
422 }
423}
424
425#[cfg(test)]
426#[allow(warnings)]
427mod tests {
428 use super::*;
429
430 #[test]
431 fn test_observation_new() {
432 let obs = Observation::new(
433 SignalType::Correction,
434 0.8,
435 "User said to use functional style".to_string(),
436 Some("session-1".to_string()),
437 Some(5),
438 );
439
440 assert_eq!(obs.signal_type, SignalType::Correction);
441 assert_eq!(obs.intensity, 0.8);
442 assert!(obs.id.len() > 0);
443 }
444
445 #[test]
446 fn test_pattern_new() {
447 let pattern = Pattern::new(
448 "Prefer functional style".to_string(),
449 "Use functional programming patterns when writing Rust code".to_string(),
450 "preference".to_string(),
451 );
452
453 assert_eq!(pattern.confidence, 0.0);
454 assert_eq!(pattern.evidence_count, 0);
455 assert!(pattern.active);
456 }
457
458 #[test]
459 fn test_pattern_decay() {
460 let mut pattern = Pattern::new(
461 "Test pattern".to_string(),
462 "Test instruction".to_string(),
463 "test".to_string(),
464 );
465 pattern.confidence = 0.8;
466
467 let decayed = pattern.decayed_confidence(70.0);
469 assert!((decayed - 0.8).abs() < 0.01);
470 }
471
472 #[test]
473 fn test_evolution_config_default() {
474 let config = EvolutionConfig::default();
475 assert_eq!(config.min_confidence, 0.7);
476 assert_eq!(config.min_evidence, 3);
477 assert_eq!(config.half_life_days, 70.0);
478 }
479
480 #[test]
481 fn test_evolution_config_default_has_byte_caps() {
482 let config = EvolutionConfig::default();
483 assert_eq!(config.max_context_bytes, 4096);
484 assert_eq!(config.max_output_bytes, 8192);
485 }
486
487 #[test]
488 fn test_evolution_config_max_context_bytes_default_4kb() {
489 let config = EvolutionConfig::default();
490 assert_eq!(config.max_context_bytes, 4 * 1024);
491 }
492
493 #[test]
494 fn test_evolution_config_max_output_bytes_default_8kb() {
495 let config = EvolutionConfig::default();
496 assert_eq!(config.max_output_bytes, 8 * 1024);
497 }
498
499 #[test]
502 fn test_signal_roundtrip_preserves_all_fields() {
503 let signal = Signal {
504 kind: SignalKind::Correction,
505 intensity: 0.85,
506 context: "不要用 sed".to_string(),
507 tool_name: Some("bash".to_string()),
508 };
509
510 let json = serde_json::to_string(&signal).expect("serialize Signal");
512 let restored: Signal = serde_json::from_str(&json).expect("deserialize Signal");
513
514 assert_eq!(restored.kind, SignalKind::Correction);
515 assert!((restored.intensity - 0.85).abs() < f32::EPSILON);
516 assert_eq!(restored.context, "不要用 sed");
517 assert_eq!(restored.tool_name, Some("bash".to_string()));
518 }
519
520 #[test]
521 fn test_turn_observation_multi_signal_flush() {
522 let session_id = uuid::Uuid::new_v4();
523 let mut turn = TurnObservation::new(session_id, 3, TurnOutcome::Success, 1500);
524
525 turn.add_signal(Signal {
526 kind: SignalKind::Correction,
527 intensity: 0.9,
528 context: "use HashMap".to_string(),
529 tool_name: None,
530 });
531 turn.add_signal(Signal {
532 kind: SignalKind::Inefficiency,
533 intensity: 0.4,
534 context: "took 10 steps".to_string(),
535 tool_name: Some("bash".to_string()),
536 });
537 turn.add_tool_usage(ToolUsage {
538 tool_name: "read".to_string(),
539 arguments_hash: 42,
540 result_summary: "file contents".to_string(),
541 });
542
543 assert_eq!(turn.signals.len(), 2);
544 assert_eq!(turn.tools_used.len(), 1);
545 assert_eq!(turn.outcome, TurnOutcome::Success);
546 assert_eq!(turn.duration_ms, 1500);
547 assert_eq!(turn.session_id, session_id);
548 assert_eq!(turn.turn_number, 3);
549
550 let json = serde_json::to_string(&turn).expect("serialize TurnObservation");
552 let restored: TurnObservation =
553 serde_json::from_str(&json).expect("deserialize TurnObservation");
554 assert_eq!(restored.signals.len(), 2);
555 assert_eq!(restored.signals[0].kind, SignalKind::Correction);
556 assert_eq!(restored.signals[1].kind, SignalKind::Inefficiency);
557 assert_eq!(restored.tools_used[0].tool_name, "read");
558 }
559
560 #[test]
561 fn test_signal_kind_display() {
562 assert_eq!(format!("{}", SignalKind::Correction), "Correction");
563 assert_eq!(format!("{}", SignalKind::Error), "Error");
564 assert_eq!(format!("{}", SignalKind::Satisfaction), "Satisfaction");
565 assert_eq!(format!("{}", SignalKind::Inefficiency), "Inefficiency");
566 }
567
568 #[test]
569 fn test_tool_usage_roundtrip() {
570 let usage = ToolUsage {
571 tool_name: "write".to_string(),
572 arguments_hash: 12345,
573 result_summary: "wrote 50 bytes".to_string(),
574 };
575
576 let json = serde_json::to_string(&usage).expect("serialize ToolUsage");
577 let restored: ToolUsage = serde_json::from_str(&json).expect("deserialize ToolUsage");
578
579 assert_eq!(restored.tool_name, "write");
580 assert_eq!(restored.arguments_hash, 12345);
581 assert_eq!(restored.result_summary, "wrote 50 bytes");
582 }
583
584 #[test]
585 fn test_pattern_roundtrip_with_mentedb_fields() {
586 let session_id = uuid::Uuid::new_v4();
587 let mut pattern = Pattern::new(
588 "Prefer functional style".to_string(),
589 "Use functional programming patterns".to_string(),
590 "preference".to_string(),
591 );
592 pattern.key = "prefer_functional_style".to_string();
593 pattern.value = serde_json::json!({ "style": "functional", "language": "rust" });
594 pattern.contradicting_count = 2;
595 pattern.last_reinforced = Utc::now();
596 pattern.source_sessions = vec![session_id];
597
598 let json = serde_json::to_string(&pattern).expect("serialize Pattern");
599 let restored: Pattern = serde_json::from_str(&json).expect("deserialize Pattern");
600
601 assert_eq!(restored.key, "prefer_functional_style");
602 assert_eq!(restored.value["style"], "functional");
603 assert_eq!(restored.contradicting_count, 2);
604 assert_eq!(restored.source_sessions.len(), 1);
605 assert_eq!(restored.source_sessions[0], session_id);
606 }
607}