umi-memory 0.1.0

Memory library for AI agents with deterministic simulation testing
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
//! FaultInjector - Probabilistic Fault Injection
//!
//! TigerStyle: Explicit fault injection for chaos testing.

use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Mutex;

use super::rng::DeterministicRng;
use crate::constants::DST_FAULT_PROBABILITY_MAX;

/// Types of faults that can be injected.
///
/// TigerStyle: Every fault type is explicit and documented.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum FaultType {
    // =========================================================================
    // Storage Faults
    // =========================================================================
    /// Write operation fails
    StorageWriteFail,
    /// Read operation fails
    StorageReadFail,
    /// Delete operation fails
    StorageDeleteFail,
    /// Storage corruption (data garbled)
    StorageCorruption,
    /// Disk full error
    StorageDiskFull,
    /// Storage latency spike
    StorageLatency,

    // =========================================================================
    // Database Faults
    // =========================================================================
    /// Connection fails
    DbConnectionFail,
    /// Query timeout
    DbQueryTimeout,
    /// Deadlock detected
    DbDeadlock,
    /// Serialization failure (retry needed)
    DbSerializationFail,
    /// Connection pool exhausted
    DbPoolExhausted,

    // =========================================================================
    // Network Faults
    // =========================================================================
    /// Connection timeout
    NetworkTimeout,
    /// Connection refused
    NetworkConnectionRefused,
    /// DNS resolution fails
    NetworkDnsFail,
    /// Partial write (incomplete data)
    NetworkPartialWrite,
    /// Connection reset
    NetworkReset,

    // =========================================================================
    // LLM/API Faults
    // =========================================================================
    /// LLM request timeout
    LlmTimeout,
    /// Rate limit exceeded
    LlmRateLimit,
    /// Context length exceeded
    LlmContextOverflow,
    /// Invalid response format
    LlmInvalidResponse,
    /// Service unavailable
    LlmServiceUnavailable,

    // =========================================================================
    // Embedding Faults
    // =========================================================================
    /// Embedding request timeout
    EmbeddingTimeout,
    /// Embedding rate limit exceeded
    EmbeddingRateLimit,
    /// Embedding context length exceeded
    EmbeddingContextOverflow,
    /// Embedding invalid response format
    EmbeddingInvalidResponse,
    /// Embedding service unavailable
    EmbeddingServiceUnavailable,

    // =========================================================================
    // Vector Search Faults
    // =========================================================================
    /// Vector search operation timeout
    VectorSearchTimeout,
    /// Vector search operation fails
    VectorSearchFail,
    /// Vector store operation fails
    VectorStoreFail,

    // =========================================================================
    // Resource Faults
    // =========================================================================
    /// Out of memory
    ResourceOom,
    /// Too many open files
    ResourceFileLimit,
    /// CPU throttling
    ResourceCpuThrottle,

    // =========================================================================
    // Time Faults
    // =========================================================================
    /// Clock skew (time jumps)
    TimeClockSkew,
    /// Leap second handling
    TimeLeapSecond,
}

impl FaultType {
    /// Get the fault type name as a string.
    #[must_use]
    pub fn as_str(&self) -> &'static str {
        match self {
            Self::StorageWriteFail => "storage_write_fail",
            Self::StorageReadFail => "storage_read_fail",
            Self::StorageDeleteFail => "storage_delete_fail",
            Self::StorageCorruption => "storage_corruption",
            Self::StorageDiskFull => "storage_disk_full",
            Self::StorageLatency => "storage_latency",
            Self::DbConnectionFail => "db_connection_fail",
            Self::DbQueryTimeout => "db_query_timeout",
            Self::DbDeadlock => "db_deadlock",
            Self::DbSerializationFail => "db_serialization_fail",
            Self::DbPoolExhausted => "db_pool_exhausted",
            Self::NetworkTimeout => "network_timeout",
            Self::NetworkConnectionRefused => "network_connection_refused",
            Self::NetworkDnsFail => "network_dns_fail",
            Self::NetworkPartialWrite => "network_partial_write",
            Self::NetworkReset => "network_reset",
            Self::LlmTimeout => "llm_timeout",
            Self::LlmRateLimit => "llm_rate_limit",
            Self::LlmContextOverflow => "llm_context_overflow",
            Self::LlmInvalidResponse => "llm_invalid_response",
            Self::LlmServiceUnavailable => "llm_service_unavailable",
            Self::EmbeddingTimeout => "embedding_timeout",
            Self::EmbeddingRateLimit => "embedding_rate_limit",
            Self::EmbeddingContextOverflow => "embedding_context_overflow",
            Self::EmbeddingInvalidResponse => "embedding_invalid_response",
            Self::EmbeddingServiceUnavailable => "embedding_service_unavailable",
            Self::VectorSearchTimeout => "vector_search_timeout",
            Self::VectorSearchFail => "vector_search_fail",
            Self::VectorStoreFail => "vector_store_fail",
            Self::ResourceOom => "resource_oom",
            Self::ResourceFileLimit => "resource_file_limit",
            Self::ResourceCpuThrottle => "resource_cpu_throttle",
            Self::TimeClockSkew => "time_clock_skew",
            Self::TimeLeapSecond => "time_leap_second",
        }
    }
}

/// Configuration for a specific fault.
#[derive(Debug, Clone)]
pub struct FaultConfig {
    /// The type of fault
    pub fault_type: FaultType,
    /// Probability of injection (0.0 to 1.0)
    pub probability: f64,
    /// Optional operation filter (substring match)
    pub operation_filter: Option<String>,
    /// Maximum number of injections (None = unlimited)
    pub max_injections: Option<u64>,
}

impl FaultConfig {
    /// Create a new fault configuration.
    ///
    /// # Panics
    /// Panics if probability is not in [0, 1].
    #[must_use]
    pub fn new(fault_type: FaultType, probability: f64) -> Self {
        // Precondition
        assert!(
            (0.0..=DST_FAULT_PROBABILITY_MAX).contains(&probability),
            "probability must be in [0, {}], got {}",
            DST_FAULT_PROBABILITY_MAX,
            probability
        );

        Self {
            fault_type,
            probability,
            operation_filter: None,
            max_injections: None,
        }
    }

    /// Set operation filter (fault only applies to matching operations).
    #[must_use]
    pub fn with_filter(mut self, filter: impl Into<String>) -> Self {
        self.operation_filter = Some(filter.into());
        self
    }

    /// Set maximum number of injections.
    #[must_use]
    pub fn with_max_injections(mut self, max: u64) -> Self {
        // Precondition
        assert!(max > 0, "max_injections must be positive");
        self.max_injections = Some(max);
        self
    }
}

/// Fault injection statistics.
#[derive(Debug, Default)]
struct FaultStats {
    injection_count: AtomicU64,
}

/// Fault injector for simulation testing.
///
/// TigerStyle:
/// - Explicit fault registration
/// - Deterministic through RNG
/// - Statistics tracked
/// - Interior mutability for sharing via Arc
#[derive(Debug)]
pub struct FaultInjector {
    /// RNG wrapped in Mutex for interior mutability (allows sharing via Arc)
    rng: Mutex<DeterministicRng>,
    configs: Vec<FaultConfig>,
    stats: HashMap<FaultType, FaultStats>,
    /// Current injection counts (wrapped in Mutex for interior mutability)
    injection_counts: Mutex<HashMap<FaultType, u64>>,
}

impl FaultInjector {
    /// Create a new fault injector with the given RNG.
    #[must_use]
    pub fn new(rng: DeterministicRng) -> Self {
        Self {
            rng: Mutex::new(rng),
            configs: Vec::new(),
            stats: HashMap::new(),
            injection_counts: Mutex::new(HashMap::new()),
        }
    }

    /// Register a fault configuration.
    ///
    /// Note: Registration must happen before sharing via Arc.
    pub fn register(&mut self, config: FaultConfig) {
        // Precondition
        assert!(
            config.probability >= 0.0,
            "probability must be non-negative"
        );
        assert!(config.probability <= 1.0, "probability must be <= 1.0");

        // Initialize stats for this fault type
        self.stats.entry(config.fault_type).or_default();
        self.injection_counts
            .lock()
            .unwrap()
            .entry(config.fault_type)
            .or_insert(0);

        self.configs.push(config);
    }

    /// Check if a fault should be injected for the given operation.
    ///
    /// Returns the fault type if one should be injected, None otherwise.
    ///
    /// TigerStyle: Uses interior mutability (Mutex) so can be called on &self,
    /// allowing FaultInjector to be shared via Arc.
    pub fn should_inject(&self, operation: &str) -> Option<FaultType> {
        for config in &self.configs {
            // Check operation filter
            if let Some(ref filter) = config.operation_filter {
                if !operation.contains(filter) {
                    continue;
                }
            }

            // Check max injections
            if let Some(max) = config.max_injections {
                let counts = self.injection_counts.lock().unwrap();
                let count = counts.get(&config.fault_type).copied().unwrap_or(0);
                if count >= max {
                    continue;
                }
            }

            // Roll for injection (uses interior mutability)
            let should_inject = {
                let mut rng = self.rng.lock().unwrap();
                rng.next_bool(config.probability)
            };

            if should_inject {
                // Update stats
                if let Some(stats) = self.stats.get(&config.fault_type) {
                    stats.injection_count.fetch_add(1, Ordering::Relaxed);
                }
                {
                    let mut counts = self.injection_counts.lock().unwrap();
                    if let Some(count) = counts.get_mut(&config.fault_type) {
                        *count += 1;
                    }
                }

                return Some(config.fault_type);
            }
        }

        None
    }

    /// Get injection statistics.
    #[must_use]
    pub fn injection_stats(&self) -> HashMap<String, u64> {
        self.stats
            .iter()
            .map(|(fault_type, stats)| {
                (
                    fault_type.as_str().to_string(),
                    stats.injection_count.load(Ordering::Relaxed),
                )
            })
            .collect()
    }

    /// Get total number of injections.
    #[must_use]
    pub fn total_injections(&self) -> u64 {
        self.stats
            .values()
            .map(|s| s.injection_count.load(Ordering::Relaxed))
            .sum()
    }

    /// Reset all statistics.
    pub fn reset_stats(&self) {
        for stats in self.stats.values() {
            stats.injection_count.store(0, Ordering::Relaxed);
        }
        let mut counts = self.injection_counts.lock().unwrap();
        for count in counts.values_mut() {
            *count = 0;
        }
    }
}

/// Builder for FaultInjector (Kelpie pattern).
///
/// TigerStyle: Builder pattern for clean configuration before sharing via Arc.
pub struct FaultInjectorBuilder {
    rng: DeterministicRng,
    configs: Vec<FaultConfig>,
}

impl FaultInjectorBuilder {
    /// Create a new builder with the given RNG.
    #[must_use]
    pub fn new(rng: DeterministicRng) -> Self {
        Self {
            rng,
            configs: Vec::new(),
        }
    }

    /// Add a fault configuration.
    #[must_use]
    pub fn with_fault(mut self, config: FaultConfig) -> Self {
        self.configs.push(config);
        self
    }

    /// Add common storage faults.
    #[must_use]
    pub fn with_storage_faults(self, probability: f64) -> Self {
        self.with_fault(FaultConfig::new(FaultType::StorageWriteFail, probability))
            .with_fault(FaultConfig::new(FaultType::StorageReadFail, probability))
    }

    /// Add common database faults.
    #[must_use]
    pub fn with_db_faults(self, probability: f64) -> Self {
        self.with_fault(FaultConfig::new(FaultType::DbConnectionFail, probability))
            .with_fault(FaultConfig::new(FaultType::DbQueryTimeout, probability))
    }

    /// Add common LLM/API faults.
    #[must_use]
    pub fn with_llm_faults(self, probability: f64) -> Self {
        self.with_fault(FaultConfig::new(FaultType::LlmTimeout, probability))
            .with_fault(FaultConfig::new(FaultType::LlmRateLimit, probability))
    }

    /// Build the FaultInjector.
    #[must_use]
    pub fn build(self) -> FaultInjector {
        let mut injector = FaultInjector::new(self.rng);
        for config in self.configs {
            injector.register(config);
        }
        injector
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::Arc;

    #[test]
    fn test_no_faults_registered() {
        let rng = DeterministicRng::new(42);
        let injector = FaultInjector::new(rng);

        for _ in 0..100 {
            assert!(injector.should_inject("any_operation").is_none());
        }
    }

    #[test]
    fn test_always_inject() {
        let rng = DeterministicRng::new(42);
        let mut injector = FaultInjector::new(rng);
        injector.register(FaultConfig::new(FaultType::StorageWriteFail, 1.0));

        for _ in 0..10 {
            assert_eq!(
                injector.should_inject("storage_write"),
                Some(FaultType::StorageWriteFail)
            );
        }
    }

    #[test]
    fn test_never_inject() {
        let rng = DeterministicRng::new(42);
        let mut injector = FaultInjector::new(rng);
        injector.register(FaultConfig::new(FaultType::StorageWriteFail, 0.0));

        for _ in 0..100 {
            assert!(injector.should_inject("storage_write").is_none());
        }
    }

    #[test]
    fn test_operation_filter() {
        let rng = DeterministicRng::new(42);
        let mut injector = FaultInjector::new(rng);
        injector.register(FaultConfig::new(FaultType::StorageWriteFail, 1.0).with_filter("write"));

        // Should inject for write operations
        assert_eq!(
            injector.should_inject("storage_write"),
            Some(FaultType::StorageWriteFail)
        );

        // Should not inject for read operations
        assert!(injector.should_inject("storage_read").is_none());
    }

    #[test]
    fn test_max_injections() {
        let rng = DeterministicRng::new(42);
        let mut injector = FaultInjector::new(rng);
        injector
            .register(FaultConfig::new(FaultType::StorageWriteFail, 1.0).with_max_injections(2));

        // First two should inject
        assert_eq!(
            injector.should_inject("op"),
            Some(FaultType::StorageWriteFail)
        );
        assert_eq!(
            injector.should_inject("op"),
            Some(FaultType::StorageWriteFail)
        );

        // Third should not
        assert!(injector.should_inject("op").is_none());
    }

    #[test]
    fn test_injection_stats() {
        let rng = DeterministicRng::new(42);
        let mut injector = FaultInjector::new(rng);
        injector.register(FaultConfig::new(FaultType::StorageWriteFail, 1.0));

        injector.should_inject("op");
        injector.should_inject("op");
        injector.should_inject("op");

        let stats = injector.injection_stats();
        assert_eq!(stats.get("storage_write_fail"), Some(&3));
        assert_eq!(injector.total_injections(), 3);
    }

    #[test]
    fn test_reset_stats() {
        let rng = DeterministicRng::new(42);
        let mut injector = FaultInjector::new(rng);
        injector.register(FaultConfig::new(FaultType::StorageWriteFail, 1.0));

        injector.should_inject("op");
        assert_eq!(injector.total_injections(), 1);

        injector.reset_stats();
        assert_eq!(injector.total_injections(), 0);
    }

    #[test]
    fn test_fault_type_as_str() {
        assert_eq!(FaultType::StorageWriteFail.as_str(), "storage_write_fail");
        assert_eq!(FaultType::DbDeadlock.as_str(), "db_deadlock");
        assert_eq!(FaultType::LlmRateLimit.as_str(), "llm_rate_limit");
    }

    #[test]
    #[should_panic(expected = "probability must be in")]
    fn test_invalid_probability() {
        let _ = FaultConfig::new(FaultType::StorageWriteFail, 1.5);
    }

    #[test]
    #[should_panic(expected = "max_injections must be positive")]
    fn test_invalid_max_injections() {
        let _ = FaultConfig::new(FaultType::StorageWriteFail, 0.5).with_max_injections(0);
    }

    #[test]
    fn test_builder_pattern() {
        let rng = DeterministicRng::new(42);
        let injector = FaultInjectorBuilder::new(rng)
            .with_storage_faults(0.1)
            .with_db_faults(0.05)
            .build();

        // Just verify it builds
        assert_eq!(injector.total_injections(), 0);
    }

    #[test]
    fn test_arc_sharing() {
        // Verify FaultInjector can be shared via Arc
        let rng = DeterministicRng::new(42);
        let injector = Arc::new(
            FaultInjectorBuilder::new(rng)
                .with_fault(FaultConfig::new(FaultType::StorageWriteFail, 1.0))
                .build(),
        );

        // Can call should_inject on shared Arc
        assert_eq!(
            injector.should_inject("storage_write"),
            Some(FaultType::StorageWriteFail)
        );

        // Clone and use
        let injector2 = Arc::clone(&injector);
        assert_eq!(
            injector2.should_inject("storage_write"),
            Some(FaultType::StorageWriteFail)
        );

        // Stats are shared
        assert_eq!(injector.total_injections(), 2);
    }
}