1use crate::entities::{Entity, EntitySearchResult};
5use crate::memory::{IndexedFileInfo, MemoryItem, MemoryStats, SearchResult};
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Debug, Serialize, Deserialize)]
12pub struct StoreMemoryResponse {
13 pub source_id: String,
14 pub chunks_created: usize,
15 pub memory_type: String,
19 #[serde(skip_serializing_if = "Option::is_none")]
20 pub entity_id: Option<String>,
21 #[serde(skip_serializing_if = "Option::is_none")]
22 pub quality: Option<String>,
23 #[serde(default, skip_serializing_if = "Vec::is_empty")]
25 pub warnings: Vec<String>,
26 #[serde(default = "default_extraction_method")]
28 pub extraction_method: String,
29 #[serde(default)]
36 pub enrichment: String,
37 #[serde(default, skip_serializing_if = "String::is_empty")]
42 pub hint: String,
43}
44
45fn default_extraction_method() -> String {
46 "unknown".to_string()
47}
48
49#[derive(Debug, Serialize, Deserialize)]
50pub struct SearchMemoryResponse {
51 pub results: Vec<SearchResult>,
52 pub took_ms: f64,
53}
54
55#[derive(Debug, Serialize, Deserialize)]
56pub struct ListMemoriesResponse {
57 pub memories: Vec<IndexedFileInfo>,
58}
59
60#[derive(Debug, Serialize, Deserialize)]
66pub struct DeleteResponse {
67 pub deleted: bool,
68}
69
70#[derive(Debug, Serialize, Deserialize)]
71pub struct ConfirmResponse {
72 pub confirmed: bool,
73}
74
75#[derive(Debug, Serialize, Deserialize)]
76pub struct ReclassifyMemoryResponse {
77 pub source_id: String,
78 pub memory_type: String,
79}
80
81#[derive(Debug, Serialize, Deserialize)]
82pub struct MemoryStatsResponse {
83 pub stats: MemoryStats,
84}
85
86#[derive(Debug, Serialize, Deserialize)]
87pub struct NurtureCardsResponse {
88 pub cards: Vec<MemoryItem>,
89}
90
91#[derive(Debug, Serialize, Deserialize)]
94pub struct HealthResponse {
95 pub status: String,
96 pub db_initialized: bool,
97 pub version: String,
98}
99
100#[derive(Debug, Serialize, Deserialize)]
101pub struct StatusResponse {
102 pub is_running: bool,
103 pub files_indexed: u64,
104 pub files_total: u64,
105 pub sources_connected: Vec<String>,
106}
107
108#[derive(Debug, Serialize, Deserialize)]
109pub struct SearchResponse {
110 pub results: Vec<SearchResult>,
111 pub took_ms: f64,
112}
113
114#[doc(hidden)]
115#[derive(Debug, Serialize, Deserialize)]
116pub struct ContextSuggestion {
117 pub content: String,
118 pub score: f32,
119 pub source: String,
120}
121
122#[doc(hidden)]
123#[derive(Debug, Serialize, Deserialize)]
124pub struct ContextResponse {
125 pub suggestions: Vec<ContextSuggestion>,
126 pub took_ms: f64,
127}
128
129#[derive(Debug, Default, Serialize, Deserialize)]
130pub struct TierTokenEstimates {
131 pub tier1_identity: usize,
132 pub tier2_project: usize,
133 pub tier3_relevant: usize,
134 pub total: usize,
135}
136
137#[derive(Debug, Serialize, Deserialize)]
138pub struct ProfileContext {
139 pub narrative: String,
140 pub identity: Vec<String>,
141 pub preferences: Vec<String>,
142 pub goals: Vec<String>,
143}
144
145#[derive(Debug, Serialize, Deserialize)]
146pub struct KnowledgeContext {
147 #[serde(default, skip_serializing_if = "Vec::is_empty")]
148 pub concepts: Vec<String>,
149 #[serde(default, skip_serializing_if = "Vec::is_empty")]
150 pub decisions: Vec<String>,
151 #[serde(default)]
152 pub relevant_memories: Vec<SearchResult>,
153 #[serde(default, skip_serializing_if = "Vec::is_empty")]
154 pub graph_context: Vec<String>,
155}
156
157#[derive(Debug, Serialize, Deserialize)]
158pub struct ChatContextResponse {
159 pub context: String,
160 pub profile: ProfileContext,
161 pub knowledge: KnowledgeContext,
162 pub took_ms: f64,
163 pub token_estimates: TierTokenEstimates,
164}
165
166#[derive(Debug, Serialize, Deserialize)]
169pub struct ProfileResponse {
170 pub id: String,
171 pub name: String,
172 pub display_name: Option<String>,
173 pub email: Option<String>,
174 pub bio: Option<String>,
175 pub avatar_path: Option<String>,
176 pub created_at: i64,
177 pub updated_at: i64,
178}
179
180#[derive(Debug, Serialize, Deserialize)]
181pub struct AgentResponse {
182 pub id: String,
183 pub name: String,
184 #[serde(default, skip_serializing_if = "Option::is_none")]
185 pub display_name: Option<String>,
186 pub agent_type: String,
187 pub description: Option<String>,
188 pub enabled: bool,
189 pub trust_level: String,
190 pub last_seen_at: Option<i64>,
191 pub memory_count: i64,
192 pub created_at: i64,
193 pub updated_at: i64,
194}
195
196#[derive(Debug, Serialize, Deserialize)]
199pub struct CreateEntityResponse {
200 pub id: String,
201}
202
203#[doc(hidden)]
204#[derive(Debug, Serialize, Deserialize)]
205pub struct CreateRelationResponse {
206 pub id: String,
207}
208
209#[derive(Debug, Serialize, Deserialize)]
210pub struct AddObservationResponse {
211 pub id: String,
212}
213
214#[derive(Debug, Serialize, Deserialize)]
215pub struct ListEntitiesResponse {
216 pub entities: Vec<Entity>,
217}
218
219#[derive(Debug, Serialize, Deserialize)]
220pub struct SearchEntitiesResponse {
221 pub results: Vec<EntitySearchResult>,
222}
223
224#[derive(Debug, Serialize, Deserialize)]
227pub struct ImportMemoriesResponse {
228 pub imported: usize,
229 pub skipped: usize,
230 pub breakdown: HashMap<String, usize>,
231 pub entities_created: usize,
232 pub observations_added: usize,
233 pub relations_created: usize,
234 pub batch_id: String,
235}
236
237#[doc(hidden)]
241#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
242pub enum Nudge {
243 Silent,
244 Ambient,
245 Notable,
246 Wow,
247}
248
249#[doc(hidden)]
251#[derive(Debug, Clone, Serialize, Deserialize)]
252pub struct PhaseResult {
253 pub name: String,
254 pub duration_ms: u64,
255 pub items_processed: usize,
256 pub error: Option<String>,
257 pub nudge: Nudge,
258 pub headline: Option<String>,
259}
260
261#[doc(hidden)]
262#[derive(Debug, Serialize, Deserialize)]
263pub struct SteepResponse {
264 pub memories_decayed: u64,
265 pub recaps_generated: u32,
266 pub distilled: u32,
267 pub pending_remaining: u32,
268 pub phases: Vec<PhaseResult>,
269}
270
271#[derive(Debug, Serialize, Deserialize)]
274pub struct ConfigResponse {
275 pub skip_apps: Vec<String>,
276 pub skip_title_patterns: Vec<String>,
277 pub private_browsing_detection: bool,
278 pub setup_completed: bool,
279 pub clipboard_enabled: bool,
280 pub screen_capture_enabled: bool,
281 pub selection_capture_enabled: bool,
282 pub remote_access_enabled: bool,
283}
284
285#[derive(Debug, Serialize, Deserialize)]
288pub struct IndexedFilesResponse {
289 pub files: Vec<IndexedFileInfo>,
290}
291
292#[derive(Debug, Serialize, Deserialize)]
293pub struct DeleteCountResponse {
294 pub deleted: usize,
295}
296
297#[derive(Debug, Serialize, Deserialize)]
300pub struct SuccessResponse {
301 pub ok: bool,
302}
303
304#[derive(Debug, Serialize, Deserialize)]
307pub struct MemoryDetailResponse {
308 pub memory: Option<MemoryItem>,
309}
310
311#[derive(Debug, Serialize, Deserialize)]
312pub struct VersionChainResponse {
313 pub versions: Vec<crate::memory::MemoryVersionItem>,
314}
315
316#[derive(Debug, Serialize, Deserialize)]
319pub struct TagsResponse {
320 pub tags: Vec<String>,
321}
322
323#[derive(Debug, Serialize, Deserialize)]
326pub struct ActivityResponse {
327 pub activities: Vec<crate::memory::AgentActivityRow>,
328}
329
330#[derive(Debug, Serialize, Deserialize)]
333pub struct DecisionsResponse {
334 pub decisions: Vec<MemoryItem>,
335}
336
337#[derive(Debug, Serialize, Deserialize)]
338pub struct DecisionDomainsResponse {
339 pub domains: Vec<String>,
340}
341
342#[derive(Debug, Serialize, Deserialize)]
345pub struct PinnedMemoriesResponse {
346 pub memories: Vec<MemoryItem>,
347}
348
349#[derive(Debug, Serialize, Deserialize)]
352pub struct IngestResponse {
353 pub chunks_created: usize,
354 pub document_id: String,
355}
356
357#[derive(Debug, Deserialize, Serialize)]
363pub struct ExportConceptResponse {
364 pub path: String,
365}
366
367#[derive(Debug, Deserialize, Serialize)]
370pub struct KnowledgePathResponse {
371 pub path: String,
372}
373
374#[derive(Debug, Deserialize, Serialize)]
375pub struct KnowledgeCountResponse {
376 pub count: u64,
377}
378
379#[doc(hidden)]
382#[derive(Debug, Clone, Serialize, Deserialize)]
383pub struct SyncStatsResponse {
384 pub files_found: usize,
385 pub ingested: usize,
386 pub skipped: usize,
387 pub errors: usize,
388}
389
390#[cfg(test)]
391mod tests {
392 use super::*;
393
394 #[test]
395 fn store_memory_response_deserializes_without_extraction_method() {
396 let json = r#"{
398 "source_id": "mem_abc",
399 "chunks_created": 3,
400 "memory_type": "fact"
401 }"#;
402 let parsed: StoreMemoryResponse = serde_json::from_str(json).unwrap();
403 assert_eq!(parsed.source_id, "mem_abc");
404 assert_eq!(parsed.chunks_created, 3);
405 assert_eq!(parsed.memory_type, "fact");
406 assert_eq!(parsed.extraction_method, "unknown");
407 assert!(parsed.warnings.is_empty());
408 }
409
410 #[test]
411 fn store_memory_response_deserializes_with_all_fields() {
412 let json = r#"{
413 "source_id": "mem_abc",
414 "chunks_created": 3,
415 "memory_type": "fact",
416 "warnings": ["decision memory missing claim"],
417 "extraction_method": "llm"
418 }"#;
419 let parsed: StoreMemoryResponse = serde_json::from_str(json).unwrap();
420 assert_eq!(parsed.warnings.len(), 1);
421 assert_eq!(parsed.extraction_method, "llm");
422 }
423
424 #[test]
425 fn store_memory_response_exposes_enrichment_and_hint() {
426 let json = r#"{
429 "source_id": "mem_xyz",
430 "chunks_created": 1,
431 "memory_type": "fact",
432 "warnings": [],
433 "extraction_method": "unknown",
434 "enrichment": "pending",
435 "hint": "Stored. Origin is compiling classification + concept links in the background (~2s). Recall will surface the enriched form shortly."
436 }"#;
437 let parsed: StoreMemoryResponse = serde_json::from_str(json).unwrap();
438 assert_eq!(parsed.enrichment, "pending");
439 assert!(parsed.hint.contains("compiling"));
440 }
441
442 #[test]
443 fn store_memory_response_defaults_enrichment_for_older_responses() {
444 let json = r#"{
447 "source_id": "mem_old",
448 "chunks_created": 1,
449 "memory_type": "fact"
450 }"#;
451 let parsed: StoreMemoryResponse = serde_json::from_str(json).unwrap();
452 assert_eq!(parsed.enrichment, ""); assert_eq!(parsed.hint, ""); }
455
456 #[test]
457 fn store_memory_response_roundtrips_not_needed_state() {
458 let response = StoreMemoryResponse {
461 source_id: "mem_no_llm".into(),
462 chunks_created: 1,
463 memory_type: "fact".into(),
464 entity_id: None,
465 quality: None,
466 warnings: vec![],
467 extraction_method: "none".into(),
468 enrichment: "not_needed".into(),
469 hint: String::new(),
470 };
471 let json = serde_json::to_string(&response).unwrap();
472 assert!(json.contains("\"enrichment\":\"not_needed\""));
473 assert!(
474 !json.contains("\"hint\""),
475 "empty hint must be skipped on the wire, got: {json}"
476 );
477 let parsed: StoreMemoryResponse = serde_json::from_str(&json).unwrap();
478 assert_eq!(parsed.enrichment, "not_needed");
479 assert_eq!(parsed.hint, "");
480 }
481
482 #[test]
483 fn chat_context_response_roundtrips_with_empty_knowledge_sections() {
484 let response = ChatContextResponse {
485 context: "context".into(),
486 profile: ProfileContext {
487 narrative: "n".into(),
488 identity: vec![],
489 preferences: vec![],
490 goals: vec![],
491 },
492 knowledge: KnowledgeContext {
493 concepts: vec![],
494 decisions: vec![],
495 relevant_memories: vec![],
496 graph_context: vec![],
497 },
498 took_ms: 1.0,
499 token_estimates: TierTokenEstimates {
500 tier1_identity: 1,
501 tier2_project: 2,
502 tier3_relevant: 3,
503 total: 6,
504 },
505 };
506
507 let json = serde_json::to_string(&response).unwrap();
508 let parsed: ChatContextResponse = serde_json::from_str(&json).unwrap();
509 assert!(parsed.knowledge.concepts.is_empty());
510 assert!(parsed.knowledge.decisions.is_empty());
511 assert!(parsed.knowledge.relevant_memories.is_empty());
512 assert!(parsed.knowledge.graph_context.is_empty());
513 }
514}