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