1pub mod entities;
9pub mod import;
10pub mod memory;
11pub mod requests;
12pub mod responses;
13pub mod sources;
14
15pub use entities::{
17 Entity, EntityDetail, EntitySearchResult, EntitySuggestion, Observation, RecentRelation,
18 Relation, RelationWithEntity,
19};
20pub use memory::{
21 ActivityBadge, ActivityKind, AgentActivityRow, AgentConnection, ConceptChange,
22 ConceptChangeKind, DomainInfo, HomeStats, IndexedFileInfo, MemoryItem, MemoryStats,
23 MemoryVersionItem, Profile, RecentActivityItem, RejectionRecord, RetrievalEvent, SearchResult,
24 SessionSnapshot, SnapshotCapture, SnapshotCaptureWithContent, Space, TopMemory, TypeBreakdown,
25};
26pub use sources::{MemoryType, RawDocument, SourceType, StabilityTier, SyncStatus};
27
28use serde::{Deserialize, Serialize};
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct ChangelogEntry {
33 pub version: i64,
34 pub at: i64,
36 pub delta: String,
39 #[serde(skip_serializing_if = "Option::is_none")]
40 pub source_agent: Option<String>,
41 #[serde(skip_serializing_if = "Option::is_none")]
43 pub incoming_source_id: Option<String>,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct ConceptSource {
49 pub concept_id: String,
50 pub memory_source_id: String,
51 pub linked_at: i64,
53 #[serde(skip_serializing_if = "Option::is_none")]
54 pub link_reason: Option<String>,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct ConceptSourceWithMemory {
60 pub source: ConceptSource,
61 pub memory: Option<crate::memory::MemoryItem>,
62}
63
64pub fn version() -> &'static str {
66 env!("CARGO_PKG_VERSION")
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72
73 #[test]
74 fn version_is_set() {
75 assert!(!version().is_empty());
76 }
77
78 #[test]
79 fn memory_type_roundtrip() {
80 for variant in [
81 MemoryType::Identity,
82 MemoryType::Preference,
83 MemoryType::Decision,
84 MemoryType::Fact,
85 MemoryType::Goal,
86 ] {
87 let s = variant.to_string();
88 let parsed: MemoryType = s.parse().unwrap();
89 assert_eq!(parsed, variant);
90 }
91 }
92
93 #[test]
94 fn search_result_serializes() {
95 let sr = SearchResult {
96 id: "1".into(),
97 content: "test".into(),
98 source: "memory".into(),
99 source_id: "mem_abc".into(),
100 title: "Test".into(),
101 url: None,
102 chunk_index: 0,
103 last_modified: 1000,
104 score: 0.9,
105 chunk_type: None,
106 language: None,
107 semantic_unit: None,
108 memory_type: Some("fact".into()),
109 domain: None,
110 source_agent: None,
111 confidence: Some(0.8),
112 confirmed: Some(true),
113 stability: None,
114 supersedes: None,
115 summary: None,
116 entity_id: None,
117 entity_name: None,
118 quality: None,
119 is_archived: false,
120 is_recap: false,
121 structured_fields: None,
122 retrieval_cue: None,
123 source_text: None,
124 raw_score: 0.0,
125 };
126 let json = serde_json::to_string(&sr).unwrap();
127 assert!(json.contains("mem_abc"));
128 assert!(!json.contains("entity_id"));
130 }
131
132 #[test]
133 fn raw_document_default() {
134 let doc = RawDocument::default();
135 assert_eq!(doc.enrichment_status, "raw");
136 assert_eq!(doc.supersede_mode, "hide");
137 assert!(!doc.pending_revision);
138 assert!(!doc.is_recap);
139 }
140
141 #[test]
142 fn stability_tier_mapping() {
143 use sources::stability_tier;
144 assert_eq!(stability_tier(Some("identity")), StabilityTier::Protected);
145 assert_eq!(stability_tier(Some("preference")), StabilityTier::Protected);
146 assert_eq!(stability_tier(Some("fact")), StabilityTier::Standard);
147 assert_eq!(stability_tier(Some("decision")), StabilityTier::Standard);
148 assert_eq!(stability_tier(Some("goal")), StabilityTier::Ephemeral);
149 assert_eq!(stability_tier(None), StabilityTier::Ephemeral);
150 }
151}
152
153#[cfg(test)]
154mod retrieval_event_tests {
155 use super::*;
156
157 #[test]
158 fn retrieval_event_roundtrips() {
159 let e = RetrievalEvent {
160 timestamp_ms: 1_700_000_000_000,
161 agent_name: "claude-code".into(),
162 query: Some("origin positioning".into()),
163 concept_titles: vec!["Origin positioning".into(), "Daemon architecture".into()],
164 concept_ids: vec![],
165 memory_snippets: vec![],
166 };
167 let s = serde_json::to_string(&e).unwrap();
168 let back: RetrievalEvent = serde_json::from_str(&s).unwrap();
169 assert_eq!(back.agent_name, "claude-code");
170 assert_eq!(back.concept_titles.len(), 2);
171 assert_eq!(back.query.as_deref(), Some("origin positioning"));
172 }
173
174 #[test]
175 fn retrieval_event_omits_none_query() {
176 let e = RetrievalEvent {
177 timestamp_ms: 1_700_000_000_000,
178 agent_name: "claude-code".into(),
179 query: None,
180 concept_titles: vec![],
181 concept_ids: vec![],
182 memory_snippets: vec![],
183 };
184 let s = serde_json::to_string(&e).unwrap();
185 assert!(
186 !s.contains("\"query\""),
187 "expected None query to be skipped on the wire, got: {s}",
188 );
189 let back: RetrievalEvent = serde_json::from_str(&s).unwrap();
190 assert_eq!(back.query, None);
191 assert!(back.concept_titles.is_empty());
192 }
193
194 #[test]
195 fn concept_change_roundtrips() {
196 let c = ConceptChange {
197 concept_id: "concept_abc".into(),
198 title: "Wiki-style prose concepts".into(),
199 change_kind: ConceptChangeKind::Revised,
200 changed_at_ms: 1_700_000_000_000,
201 };
202 let s = serde_json::to_string(&c).unwrap();
203 assert!(
204 s.contains("\"change_kind\":\"revised\""),
205 "expected snake_case change_kind on the wire, got: {s}",
206 );
207 let back: ConceptChange = serde_json::from_str(&s).unwrap();
208 assert_eq!(back.change_kind, ConceptChangeKind::Revised);
209 }
210}