1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use uuid::Uuid;
9
10use crate::types::*;
11
12pub const STORAGE_ERROR_MESSAGE: &str = "An internal storage error occurred";
16pub const SERIALIZATION_ERROR_MESSAGE: &str = "An internal data processing error occurred";
18pub const EXPORT_ERROR_MESSAGE: &str = "An internal error occurred during export";
20pub const INTERNAL_ERROR_MESSAGE: &str = "An internal error occurred";
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct CMPHeader {
28 pub protocol: String,
29 pub version: String,
30 pub request_id: Uuid,
31 pub timestamp: DateTime<Utc>,
32}
33
34impl CMPHeader {
35 pub fn new() -> Self {
36 Self {
37 protocol: "cmp".to_string(),
38 version: "1.0".to_string(),
39 request_id: Uuid::now_v7(),
40 timestamp: Utc::now(),
41 }
42 }
43
44 pub fn is_compatible(&self) -> bool {
45 self.protocol == "cmp" && self.version.starts_with("1.")
46 }
47}
48
49impl Default for CMPHeader {
50 fn default() -> Self {
51 Self::new()
52 }
53}
54
55#[derive(Debug, Clone, Default, Serialize, Deserialize)]
59#[serde(default)]
60pub struct EncodeContext {
61 pub source: Option<String>,
62 pub session_id: Option<String>,
63 pub spatial: Option<serde_json::Value>,
64 pub temporal: Option<serde_json::Value>,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct ManualAssociation {
70 pub target_id: Uuid,
71 pub association_type: AssociationType,
72 pub weight: f64,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct EncodeStoreRequest {
78 #[serde(default)]
79 pub header: Option<CMPHeader>,
80 pub content: MemoryContent,
81 #[serde(default)]
82 pub store: Option<StoreType>,
83 #[serde(default)]
84 pub emotion: Option<EmotionVector>,
85 #[serde(default)]
86 pub context: Option<EncodeContext>,
87 #[serde(default)]
88 pub metadata: Option<serde_json::Value>,
89 #[serde(default)]
90 pub associations: Option<Vec<ManualAssociation>>,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct EncodeStoreResponse {
96 pub record_id: Uuid,
97 pub store: StoreType,
98 pub initial_fidelity: f64,
99 pub associations_created: u32,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct EncodeBatchRequest {
105 #[serde(default)]
106 pub header: Option<CMPHeader>,
107 pub records: Vec<EncodeStoreRequest>,
108 #[serde(default)]
109 pub infer_associations: bool,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct EncodeBatchResponse {
115 pub results: Vec<EncodeStoreResponse>,
116 pub associations_inferred: u32,
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct EncodeUpdateRequest {
122 #[serde(default)]
123 pub header: Option<CMPHeader>,
124 pub record_id: Uuid,
125 #[serde(default)]
126 pub content: Option<MemoryContent>,
127 #[serde(default)]
128 pub emotion: Option<EmotionVector>,
129 #[serde(default)]
130 pub metadata: Option<serde_json::Value>,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct EncodeStoreRawRequest {
136 #[serde(default)]
137 pub header: Option<CMPHeader>,
138 pub session_id: String,
139 #[serde(default)]
140 pub turn_id: Option<String>,
141 #[serde(default)]
142 pub topic_id: Option<String>,
143 pub source: RawSource,
144 pub speaker: RawSpeaker,
145 pub visibility: RawVisibility,
146 pub secrecy_level: SecrecyLevel,
147 pub content: MemoryContent,
148 #[serde(default)]
149 pub metadata: Option<serde_json::Value>,
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct EncodeStoreRawResponse {
155 pub record_id: Uuid,
156 pub session_id: String,
157 pub visibility: RawVisibility,
158 pub secrecy_level: SecrecyLevel,
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct EncodeBatchStoreRawRequest {
164 #[serde(default)]
165 pub header: Option<CMPHeader>,
166 pub records: Vec<EncodeStoreRawRequest>,
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct EncodeBatchStoreRawResponse {
172 pub results: Vec<EncodeStoreRawResponse>,
173}
174
175#[derive(Debug, Clone, Default, Serialize, Deserialize)]
179#[serde(default)]
180pub struct RecallCue {
181 pub text: Option<String>,
182 pub image: Option<Vec<u8>>,
184 pub audio: Option<Vec<u8>>,
186 pub emotion: Option<EmotionVector>,
187 pub temporal: Option<TemporalRange>,
188 pub spatial: Option<serde_json::Value>,
189 pub semantic: Option<serde_json::Value>,
190 pub embedding: Option<Vec<f32>>,
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct TemporalRange {
197 pub start: DateTime<Utc>,
198 pub end: DateTime<Utc>,
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct RecallQueryRequest {
204 #[serde(default)]
205 pub header: Option<CMPHeader>,
206 pub cue: RecallCue,
207 #[serde(default)]
208 pub stores: Option<Vec<StoreType>>,
209 #[serde(default = "default_recall_limit")]
210 pub limit: u32,
211 #[serde(default)]
212 pub min_fidelity: Option<f64>,
213 #[serde(default)]
214 pub include_decayed: bool,
215 #[serde(default = "default_true")]
216 pub reconsolidate: bool,
217 #[serde(default = "default_activation_depth")]
218 pub activation_depth: u32,
219 #[serde(default = "default_recall_mode")]
220 pub recall_mode: RecallMode,
221}
222
223pub const DEFAULT_RECALL_LIMIT: u32 = 10;
225pub const DEFAULT_RECONSOLIDATE: bool = true;
227pub const DEFAULT_ACTIVATION_DEPTH: u32 = 2;
229
230fn default_recall_limit() -> u32 {
231 DEFAULT_RECALL_LIMIT
232}
233fn default_true() -> bool {
234 DEFAULT_RECONSOLIDATE
235}
236fn default_activation_depth() -> u32 {
237 DEFAULT_ACTIVATION_DEPTH
238}
239fn default_recall_mode() -> RecallMode {
240 RecallMode::Human
241}
242
243#[derive(Debug, Clone, Serialize, Deserialize)]
245pub struct RecalledMemory {
246 pub record: MemoryRecord,
247 pub relevance_score: f64,
248 #[serde(default)]
249 pub activation_path: Option<Vec<Uuid>>,
250 pub rendered_content: MemoryContent,
251}
252
253#[derive(Debug, Clone, Serialize, Deserialize)]
255pub struct ActivationTrace {
256 pub source_id: Uuid,
257 pub activations: Vec<ActivationNode>,
258}
259
260#[derive(Debug, Clone, Serialize, Deserialize)]
262pub struct ActivationNode {
263 pub record_id: Uuid,
264 pub activation_level: f64,
265 pub hop: u32,
266 pub edge_type: AssociationType,
267}
268
269#[derive(Debug, Clone, Serialize, Deserialize)]
271pub struct QueryMetadata {
272 pub total_records_scanned: u32,
273 pub stores_searched: Vec<StoreType>,
274 pub fidelity_filtered: u32,
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize)]
279pub struct RecallQueryResponse {
280 pub memories: Vec<RecalledMemory>,
281 #[serde(default)]
282 pub activation_trace: Option<ActivationTrace>,
283 pub total_candidates: u32,
284 #[serde(default, skip_serializing_if = "Option::is_none")]
285 pub query_metadata: Option<QueryMetadata>,
286}
287
288#[derive(Debug, Clone, Serialize, Deserialize)]
290pub struct RecallAssociateRequest {
291 #[serde(default)]
292 pub header: Option<CMPHeader>,
293 pub record_id: Uuid,
294 #[serde(default)]
295 pub association_types: Option<Vec<AssociationType>>,
296 #[serde(default = "default_activation_depth")]
297 pub depth: u32,
298 #[serde(default = "default_min_weight")]
299 pub min_weight: f64,
300 #[serde(default = "default_recall_limit")]
301 pub limit: u32,
302}
303
304fn default_min_weight() -> f64 {
305 0.1
306}
307
308#[derive(Debug, Clone, Serialize, Deserialize)]
310pub struct RecallAssociateResponse {
311 pub memories: Vec<RecalledMemory>,
312 pub total_candidates: u32,
313}
314
315#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
317#[serde(rename_all = "lowercase")]
318pub enum TimeGranularity {
319 Minute,
320 Hour,
321 Day,
322 Week,
323 Month,
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct RecallTimelineRequest {
329 #[serde(default)]
330 pub header: Option<CMPHeader>,
331 pub range: TemporalRange,
332 #[serde(default = "default_granularity")]
333 pub granularity: TimeGranularity,
334 #[serde(default)]
335 pub min_fidelity: Option<f64>,
336 #[serde(default)]
337 pub emotion_filter: Option<EmotionVector>,
338}
339
340fn default_granularity() -> TimeGranularity {
341 TimeGranularity::Hour
342}
343
344#[derive(Debug, Clone, Serialize, Deserialize)]
346pub struct RecallGraphRequest {
347 #[serde(default)]
348 pub header: Option<CMPHeader>,
349 #[serde(default)]
350 pub center_id: Option<Uuid>,
351 #[serde(default = "default_activation_depth")]
352 pub depth: u32,
353 #[serde(default)]
354 pub edge_types: Option<Vec<String>>,
355 #[serde(default = "default_recall_limit")]
356 pub limit_nodes: u32,
357}
358
359#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct RecallRawQueryRequest {
362 #[serde(default)]
363 pub header: Option<CMPHeader>,
364 #[serde(default)]
365 pub session_id: Option<String>,
366 #[serde(default)]
367 pub query: Option<String>,
368 #[serde(default)]
369 pub temporal: Option<TemporalRange>,
370 #[serde(default = "default_recall_limit")]
371 pub limit: u32,
372 #[serde(default)]
373 pub include_private_scratch: bool,
374 #[serde(default)]
375 pub include_sealed: bool,
376 #[serde(default)]
377 pub secrecy_levels: Option<Vec<SecrecyLevel>>,
378}
379
380#[derive(Debug, Clone, Serialize, Deserialize)]
382pub struct RecallRawQueryResponse {
383 pub records: Vec<RawJournalRecord>,
384 pub total_candidates: u32,
385}
386
387#[derive(Debug, Clone, Serialize, Deserialize)]
389pub struct RecallTimelineResponse {
390 pub buckets: Vec<TimelineBucket>,
391}
392
393#[derive(Debug, Clone, Serialize, Deserialize)]
395pub struct TimelineBucket {
396 pub start: DateTime<Utc>,
397 pub end: DateTime<Utc>,
398 pub memories: Vec<RecalledMemory>,
399 pub count: u32,
400}
401
402#[derive(Debug, Clone, Serialize, Deserialize)]
404pub struct RecallGraphResponse {
405 pub nodes: Vec<GraphNode>,
406 pub edges: Vec<GraphEdge>,
407 pub total_nodes: u32,
408}
409
410#[derive(Debug, Clone, Serialize, Deserialize)]
412pub struct GraphNode {
413 pub id: Uuid,
414 pub store: StoreType,
415 pub summary: Option<String>,
416 pub fidelity: f64,
417}
418
419#[derive(Debug, Clone, Serialize, Deserialize)]
421pub struct GraphEdge {
422 pub source: Uuid,
423 pub target: Uuid,
424 pub edge_type: AssociationType,
425 pub weight: f64,
426}
427
428#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
432#[serde(rename_all = "lowercase")]
433pub enum ConsolidationStrategy {
434 Full,
435 Incremental,
436 Selective,
437}
438
439#[derive(Debug, Clone, Serialize, Deserialize)]
441pub struct ConsolidateRequest {
442 #[serde(default)]
443 pub header: Option<CMPHeader>,
444 #[serde(default = "default_consolidation_strategy")]
445 pub strategy: ConsolidationStrategy,
446 #[serde(default)]
447 pub min_age_hours: u32,
448 #[serde(default)]
449 pub min_access_count: u32,
450 #[serde(default)]
451 pub dry_run: bool,
452}
453
454fn default_consolidation_strategy() -> ConsolidationStrategy {
455 ConsolidationStrategy::Incremental
456}
457
458#[derive(Debug, Clone, Serialize, Deserialize)]
460pub struct ConsolidateResponse {
461 pub records_processed: u32,
462 pub records_migrated: u32,
463 pub records_compressed: u32,
464 pub records_pruned: u32,
465 pub semantic_nodes_created: u32,
466}
467
468#[derive(Debug, Clone, Serialize, Deserialize)]
470pub struct DreamTickRequest {
471 #[serde(default)]
472 pub header: Option<CMPHeader>,
473 #[serde(default)]
474 pub session_id: Option<String>,
475 #[serde(default)]
476 pub dry_run: bool,
477 #[serde(default = "default_recall_limit")]
478 pub max_groups: u32,
479 #[serde(default)]
480 pub include_private_scratch: bool,
481 #[serde(default)]
482 pub include_sealed: bool,
483 #[serde(default = "default_true")]
484 pub promote_semantic: bool,
485 #[serde(default)]
486 pub secrecy_levels: Option<Vec<SecrecyLevel>>,
487}
488
489#[derive(Debug, Clone, Serialize, Deserialize)]
491pub struct DreamTickResponse {
492 pub groups_processed: u32,
493 pub raw_records_processed: u32,
494 pub episodic_summaries_created: u32,
495 pub semantic_nodes_created: u32,
496}
497
498#[derive(Debug, Clone, Serialize, Deserialize)]
500pub struct DecayTickRequest {
501 #[serde(default)]
502 pub header: Option<CMPHeader>,
503 #[serde(default)]
504 pub tick_duration_seconds: Option<u32>,
505}
506
507#[derive(Debug, Clone, Serialize, Deserialize)]
509pub struct DecayTickResponse {
510 pub records_updated: u32,
511 pub records_below_threshold: u32,
512 pub records_pruned: u32,
513}
514
515#[derive(Debug, Clone, Serialize, Deserialize)]
517pub struct SetModeRequest {
518 #[serde(default)]
519 pub header: Option<CMPHeader>,
520 pub mode: RecallMode,
521 #[serde(default)]
522 pub scope: Option<Vec<StoreType>>,
523}
524
525#[derive(Debug, Clone, Serialize, Deserialize)]
527pub struct ForgetRequest {
528 #[serde(default)]
529 pub header: Option<CMPHeader>,
530 #[serde(default)]
531 pub record_ids: Option<Vec<Uuid>>,
532 #[serde(default)]
533 pub store: Option<StoreType>,
534 #[serde(default)]
535 pub temporal_range: Option<TemporalRange>,
536 #[serde(default)]
537 pub cascade: bool,
538 pub confirm: bool,
539}
540
541#[derive(Debug, Clone, Serialize, Deserialize)]
543pub struct ForgetResponse {
544 pub records_deleted: u32,
545}
546
547#[derive(Debug, Clone, Serialize, Deserialize)]
549pub struct ExportRequest {
550 #[serde(default)]
551 pub header: Option<CMPHeader>,
552 #[serde(default = "default_format")]
553 pub format: String,
554 #[serde(default)]
555 pub stores: Option<Vec<StoreType>>,
556 #[serde(default)]
557 pub include_raw_journal: bool,
558 #[serde(default)]
559 pub encrypt: bool,
560 #[serde(default)]
561 pub encryption_key: Option<String>,
562}
563
564fn default_format() -> String {
565 "cma".to_string()
566}
567
568#[derive(Debug, Clone, Serialize, Deserialize)]
570pub struct ExportResponse {
571 pub archive_id: String,
572 pub size_bytes: u64,
573 pub record_count: u32,
574 pub checksum: String,
575}
576
577#[derive(Debug, Clone, Serialize, Deserialize)]
579pub struct ExportArchiveResponse {
580 #[serde(flatten)]
581 pub metadata: ExportResponse,
582 pub archive_data: Vec<u8>,
583}
584
585#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
587#[serde(rename_all = "snake_case")]
588pub enum ConflictResolution {
589 KeepExisting,
590 KeepImported,
591 #[default]
592 KeepNewer,
593}
594
595#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
597#[serde(rename_all = "lowercase")]
598pub enum ImportStrategy {
599 Merge,
600 Replace,
601}
602
603#[derive(Debug, Clone, Serialize, Deserialize)]
605pub struct ImportRequest {
606 #[serde(default)]
607 pub header: Option<CMPHeader>,
608 pub archive_id: String,
609 #[serde(default = "default_import_strategy")]
610 pub strategy: ImportStrategy,
611 #[serde(default = "default_conflict_resolution")]
612 pub conflict_resolution: ConflictResolution,
613 #[serde(default)]
614 pub decryption_key: Option<String>,
615 #[serde(default)]
617 pub archive_data: Option<Vec<u8>>,
618}
619
620#[derive(Debug, Clone, Serialize, Deserialize)]
622pub struct ImportResponse {
623 pub records_imported: u32,
624}
625
626fn default_import_strategy() -> ImportStrategy {
627 ImportStrategy::Merge
628}
629
630fn default_conflict_resolution() -> ConflictResolution {
631 ConflictResolution::KeepNewer
632}
633
634#[derive(Debug, Clone, Serialize, Deserialize)]
638pub struct StatsResponse {
639 pub total_records: u32,
640 pub records_by_store: std::collections::HashMap<StoreType, u32>,
641 pub total_associations: u32,
642 pub avg_fidelity: f64,
643 pub avg_fidelity_by_store: std::collections::HashMap<StoreType, f64>,
644 #[serde(default)]
645 pub oldest_record: Option<DateTime<Utc>>,
646 #[serde(default)]
647 pub newest_record: Option<DateTime<Utc>>,
648 pub total_recall_count: u64,
649 #[serde(default)]
650 pub raw_journal_records: u32,
651 #[serde(default)]
652 pub raw_journal_pending_dream: u32,
653 #[serde(default)]
654 pub dream_episodic_summaries: u32,
655 #[serde(default)]
656 pub dream_semantic_nodes: u32,
657 #[serde(default)]
658 pub last_dream_tick_at: Option<DateTime<Utc>>,
659 #[serde(default)]
660 pub evolution_metrics: Option<EvolutionMetrics>,
661 #[serde(default)]
663 pub background_decay_enabled: bool,
664 #[serde(default)]
666 pub background_dream_enabled: bool,
667}
668
669#[derive(Debug, Clone, Serialize, Deserialize)]
671pub struct ParameterAdjustment {
672 pub store: StoreType,
673 pub parameter: String,
674 pub original_value: f64,
675 pub current_value: f64,
676 pub reason: String,
677}
678
679#[derive(Debug, Clone, Default, Serialize, Deserialize)]
681#[serde(default)]
682pub struct EvolutionMetrics {
683 pub parameter_adjustments: Vec<ParameterAdjustment>,
684 pub detected_patterns: Vec<String>,
685 pub schema_adaptations: Vec<String>,
686}
687
688#[derive(Debug, Clone, Serialize, Deserialize)]
690pub struct RecordIntrospectRequest {
691 #[serde(default)]
692 pub header: Option<CMPHeader>,
693 pub record_id: Uuid,
694 #[serde(default)]
695 pub include_history: bool,
696 #[serde(default)]
697 pub include_associations: bool,
698 #[serde(default)]
699 pub include_versions: bool,
700}
701
702#[derive(Debug, Clone, Serialize, Deserialize)]
704pub struct DecayForecastRequest {
705 #[serde(default)]
706 pub header: Option<CMPHeader>,
707 pub record_ids: Vec<Uuid>,
708 pub forecast_at: DateTime<Utc>,
709}
710
711#[derive(Debug, Clone, Serialize, Deserialize)]
713pub struct DecayForecast {
714 pub record_id: Uuid,
715 pub current_fidelity: f64,
716 pub forecasted_fidelity: f64,
717 #[serde(default)]
718 pub estimated_threshold_date: Option<DateTime<Utc>>,
719}
720
721#[derive(Debug, Clone, Serialize, Deserialize)]
723pub struct DecayForecastResponse {
724 pub forecasts: Vec<DecayForecast>,
725}
726
727#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
731#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
732pub enum CMPErrorCode {
733 RecordNotFound,
734 StoreInvalid,
735 ContentTooLarge,
736 ValidationError,
737 ModalityUnsupported,
738 WorkingMemoryFull,
739 DecayEngineBusy,
740 ConsolidationInProgress,
741 ExportFailed,
742 ImportConflict,
743 ForgetUnconfirmed,
744 VersionMismatch,
745 Unauthorized,
746 RateLimited,
747 InternalError,
748}
749
750#[derive(Debug, Clone, Serialize, Deserialize)]
752pub struct CMPError {
753 pub code: CMPErrorCode,
754 pub message: String,
755 #[serde(default)]
756 pub details: Option<serde_json::Value>,
757 #[serde(default)]
758 pub retry_after: Option<u32>,
759 #[serde(default, skip_serializing_if = "Option::is_none")]
760 pub request_id: Option<Uuid>,
761}
762
763impl CMPError {
764 pub fn new(code: CMPErrorCode, message: impl Into<String>) -> Self {
765 Self {
766 code,
767 message: message.into(),
768 details: None,
769 retry_after: None,
770 request_id: None,
771 }
772 }
773}
774
775impl From<&crate::error::CerememoryError> for CMPError {
776 fn from(err: &crate::error::CerememoryError) -> Self {
777 use crate::error::CerememoryError;
778 match err {
779 CerememoryError::RecordNotFound(id) => CMPError::new(
780 CMPErrorCode::RecordNotFound,
781 format!("Record not found: {id}"),
782 ),
783 CerememoryError::StoreInvalid(s) => {
784 CMPError::new(CMPErrorCode::StoreInvalid, format!("Invalid store: {s}"))
785 }
786 CerememoryError::ContentTooLarge { size, limit } => CMPError::new(
787 CMPErrorCode::ContentTooLarge,
788 format!("{size} bytes exceeds {limit}"),
789 ),
790 CerememoryError::ModalityUnsupported(m) => CMPError::new(
791 CMPErrorCode::ModalityUnsupported,
792 format!("Unsupported: {m}"),
793 ),
794 CerememoryError::WorkingMemoryFull => CMPError::new(
795 CMPErrorCode::WorkingMemoryFull,
796 "Working memory at capacity",
797 ),
798 CerememoryError::DecayEngineBusy { retry_after_secs } => {
799 let mut e = CMPError::new(CMPErrorCode::DecayEngineBusy, "Decay engine busy");
800 e.retry_after = Some(*retry_after_secs);
801 e
802 }
803 CerememoryError::ConsolidationInProgress => CMPError::new(
804 CMPErrorCode::ConsolidationInProgress,
805 "Consolidation in progress",
806 ),
807 CerememoryError::ExportFailed(_msg) => {
808 CMPError::new(CMPErrorCode::ExportFailed, EXPORT_ERROR_MESSAGE)
809 }
810 CerememoryError::ImportConflict(msg) => {
811 CMPError::new(CMPErrorCode::ImportConflict, msg.clone())
812 }
813 CerememoryError::ForgetUnconfirmed => CMPError::new(
814 CMPErrorCode::ForgetUnconfirmed,
815 "Forget requires explicit confirmation. Set 'confirm: true' to proceed.",
816 ),
817 CerememoryError::VersionMismatch { expected, got } => CMPError::new(
818 CMPErrorCode::VersionMismatch,
819 format!("Expected {expected}, got {got}"),
820 ),
821 CerememoryError::Validation(msg) => {
822 CMPError::new(CMPErrorCode::ValidationError, msg.clone())
823 }
824 CerememoryError::Storage(_msg) => {
825 CMPError::new(CMPErrorCode::InternalError, STORAGE_ERROR_MESSAGE)
826 }
827 CerememoryError::Serialization(_msg) => {
828 CMPError::new(CMPErrorCode::InternalError, SERIALIZATION_ERROR_MESSAGE)
829 }
830 CerememoryError::Internal(_msg) => {
831 CMPError::new(CMPErrorCode::InternalError, INTERNAL_ERROR_MESSAGE)
832 }
833 CerememoryError::Unauthorized(msg) => {
834 CMPError::new(CMPErrorCode::Unauthorized, msg.clone())
835 }
836 CerememoryError::RateLimited { retry_after_secs } => {
837 let mut e = CMPError::new(CMPErrorCode::RateLimited, "Rate limit exceeded");
838 e.retry_after = Some(*retry_after_secs);
839 e
840 }
841 }
842 }
843}
844
845#[cfg(test)]
846mod tests {
847 use super::*;
848
849 #[test]
850 fn cmp_header_defaults_are_valid() {
851 let header = CMPHeader::new();
852 assert_eq!(header.protocol, "cmp");
853 assert_eq!(header.version, "1.0");
854 assert!(header.is_compatible());
855 }
856
857 #[test]
858 fn cmp_header_version_check() {
859 let mut header = CMPHeader::new();
860 header.version = "1.1".to_string();
861 assert!(header.is_compatible());
862 header.version = "2.0".to_string();
863 assert!(!header.is_compatible());
864 }
865
866 #[test]
867 fn encode_store_request_json_roundtrip() {
868 let req = EncodeStoreRequest {
869 header: Some(CMPHeader::new()),
870 content: MemoryContent {
871 blocks: vec![ContentBlock {
872 modality: Modality::Text,
873 format: "text/plain".to_string(),
874 data: b"Hello world".to_vec(),
875 embedding: None,
876 }],
877 summary: Some("Test".to_string()),
878 },
879 store: Some(StoreType::Episodic),
880 emotion: None,
881 context: None,
882 metadata: None,
883 associations: None,
884 };
885
886 let json = serde_json::to_string(&req).unwrap();
887 let decoded: EncodeStoreRequest = serde_json::from_str(&json).unwrap();
888 assert_eq!(decoded.store, Some(StoreType::Episodic));
889 assert_eq!(decoded.content.blocks.len(), 1);
890 }
891
892 #[test]
893 fn encode_store_request_msgpack_roundtrip() {
894 let req = EncodeStoreRequest {
895 header: None,
896 content: MemoryContent {
897 blocks: vec![ContentBlock {
898 modality: Modality::Text,
899 format: "text/plain".to_string(),
900 data: b"Test data".to_vec(),
901 embedding: None,
902 }],
903 summary: None,
904 },
905 store: None,
906 emotion: None,
907 context: None,
908 metadata: None,
909 associations: None,
910 };
911
912 let packed = rmp_serde::to_vec(&req).unwrap();
913 let decoded: EncodeStoreRequest = rmp_serde::from_slice(&packed).unwrap();
914 assert_eq!(decoded.content.blocks[0].data, b"Test data");
915 }
916
917 #[test]
918 fn recall_query_request_defaults() {
919 let json = r#"{"cue":{"text":"hello"}}"#;
920 let req: RecallQueryRequest = serde_json::from_str(json).unwrap();
921 assert_eq!(req.limit, 10);
922 assert!(req.reconsolidate);
923 assert_eq!(req.activation_depth, 2);
924 assert_eq!(req.recall_mode, RecallMode::Human);
925 }
926
927 #[test]
928 fn cmp_error_from_cerememory_error() {
929 let err = crate::error::CerememoryError::RecordNotFound("abc".to_string());
930 let cmp_err = CMPError::from(&err);
931 assert_eq!(cmp_err.code, CMPErrorCode::RecordNotFound);
932 }
933
934 #[test]
935 fn decay_tick_request_defaults() {
936 let json = r#"{}"#;
937 let req: DecayTickRequest = serde_json::from_str(json).unwrap();
938 assert!(req.tick_duration_seconds.is_none());
939 assert!(req.header.is_none());
940 }
941
942 #[test]
943 fn forget_request_requires_confirm() {
944 let json = r#"{"confirm": true, "record_ids": ["01916e3a-1234-7000-8000-000000000001"]}"#;
945 let req: ForgetRequest = serde_json::from_str(json).unwrap();
946 assert!(req.confirm);
947 assert_eq!(req.record_ids.unwrap().len(), 1);
948 }
949
950 #[test]
951 fn stats_response_roundtrip() {
952 let mut by_store = std::collections::HashMap::new();
953 by_store.insert(StoreType::Episodic, 42u32);
954
955 let stats = StatsResponse {
956 total_records: 42,
957 records_by_store: by_store,
958 total_associations: 10,
959 avg_fidelity: 0.85,
960 avg_fidelity_by_store: std::collections::HashMap::new(),
961 oldest_record: None,
962 newest_record: None,
963 total_recall_count: 100,
964 raw_journal_records: 7,
965 raw_journal_pending_dream: 3,
966 dream_episodic_summaries: 2,
967 dream_semantic_nodes: 1,
968 last_dream_tick_at: None,
969 evolution_metrics: None,
970 background_decay_enabled: false,
971 background_dream_enabled: false,
972 };
973
974 let json = serde_json::to_string(&stats).unwrap();
975 let decoded: StatsResponse = serde_json::from_str(&json).unwrap();
976 assert_eq!(decoded.total_records, 42);
977 }
978}