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 pages: 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 remote_access_enabled: bool,
282 #[serde(default, skip_serializing_if = "Option::is_none")]
284 pub routine_model: Option<String>,
285 #[serde(default, skip_serializing_if = "Option::is_none")]
287 pub synthesis_model: Option<String>,
288 #[serde(default, skip_serializing_if = "Option::is_none")]
290 pub external_llm_endpoint: Option<String>,
291 #[serde(default, skip_serializing_if = "Option::is_none")]
293 pub external_llm_model: Option<String>,
294}
295
296#[derive(Debug, Serialize, Deserialize)]
299pub struct IndexedFilesResponse {
300 pub files: Vec<IndexedFileInfo>,
301}
302
303#[derive(Debug, Serialize, Deserialize)]
304pub struct DeleteCountResponse {
305 pub deleted: usize,
306}
307
308#[derive(Debug, Serialize, Deserialize)]
311pub struct SuccessResponse {
312 pub ok: bool,
313}
314
315#[derive(Debug, Serialize, Deserialize)]
318pub struct MemoryDetailResponse {
319 pub memory: Option<MemoryItem>,
320}
321
322#[derive(Debug, Clone, Serialize, Deserialize)]
324pub struct MemoryDetail {
325 pub id: String,
326 pub content: String,
327 pub title: String,
328 pub source_id: String,
329 pub chunk_index: i32,
330 #[serde(skip_serializing_if = "Option::is_none")]
331 pub chunk_type: Option<String>,
332 #[serde(skip_serializing_if = "Option::is_none")]
333 pub language: Option<String>,
334 #[serde(skip_serializing_if = "Option::is_none")]
335 pub semantic_unit: Option<String>,
336 #[serde(skip_serializing_if = "Option::is_none")]
337 pub byte_start: Option<i64>,
338 #[serde(skip_serializing_if = "Option::is_none")]
339 pub byte_end: Option<i64>,
340 #[serde(skip_serializing_if = "Option::is_none")]
341 pub summary: Option<String>,
342}
343
344#[derive(Debug, Clone, Serialize, Deserialize)]
346pub struct PendingRevision {
347 pub source_id: String,
348 pub content: String,
349 pub source_agent: Option<String>,
350}
351
352#[derive(Debug, Serialize, Deserialize)]
353pub struct VersionChainResponse {
354 pub versions: Vec<crate::memory::MemoryVersionItem>,
355}
356
357#[derive(Debug, Serialize, Deserialize)]
360pub struct TagsResponse {
361 pub tags: Vec<String>,
362}
363
364#[derive(Debug, Serialize, Deserialize)]
367pub struct ActivityResponse {
368 pub activities: Vec<crate::memory::AgentActivityRow>,
369}
370
371#[derive(Debug, Serialize, Deserialize)]
374pub struct DecisionsResponse {
375 pub decisions: Vec<MemoryItem>,
376}
377
378#[derive(Debug, Serialize, Deserialize)]
379pub struct DecisionDomainsResponse {
380 pub domains: Vec<String>,
381}
382
383#[derive(Debug, Serialize, Deserialize)]
386pub struct PinnedMemoriesResponse {
387 pub memories: Vec<MemoryItem>,
388}
389
390#[derive(Debug, Serialize, Deserialize)]
393pub struct IngestResponse {
394 pub chunks_created: usize,
395 pub document_id: String,
396}
397
398#[derive(Debug, Default, Serialize, Deserialize)]
405pub struct ExportStats {
406 pub exported: usize,
407 pub skipped: usize,
408 pub failed: usize,
409}
410
411#[derive(Debug, Deserialize, Serialize)]
412pub struct ExportPageResponse {
413 pub path: String,
414}
415
416#[derive(Debug, Deserialize, Serialize)]
419pub struct KnowledgePathResponse {
420 pub path: String,
421}
422
423#[derive(Debug, Deserialize, Serialize)]
424pub struct KnowledgeCountResponse {
425 pub count: u64,
426}
427
428#[doc(hidden)]
431#[derive(Debug, Clone, Serialize, Deserialize)]
432pub struct SyncStatsResponse {
433 pub files_found: usize,
434 pub ingested: usize,
435 pub skipped: usize,
436 pub errors: usize,
437}
438
439#[cfg(test)]
440mod tests {
441 use super::*;
442
443 #[test]
444 fn store_memory_response_deserializes_without_extraction_method() {
445 let json = r#"{
447 "source_id": "mem_abc",
448 "chunks_created": 3,
449 "memory_type": "fact"
450 }"#;
451 let parsed: StoreMemoryResponse = serde_json::from_str(json).unwrap();
452 assert_eq!(parsed.source_id, "mem_abc");
453 assert_eq!(parsed.chunks_created, 3);
454 assert_eq!(parsed.memory_type, "fact");
455 assert_eq!(parsed.extraction_method, "unknown");
456 assert!(parsed.warnings.is_empty());
457 }
458
459 #[test]
460 fn store_memory_response_deserializes_with_all_fields() {
461 let json = r#"{
462 "source_id": "mem_abc",
463 "chunks_created": 3,
464 "memory_type": "fact",
465 "warnings": ["decision memory missing claim"],
466 "extraction_method": "llm"
467 }"#;
468 let parsed: StoreMemoryResponse = serde_json::from_str(json).unwrap();
469 assert_eq!(parsed.warnings.len(), 1);
470 assert_eq!(parsed.extraction_method, "llm");
471 }
472
473 #[test]
474 fn store_memory_response_exposes_enrichment_and_hint() {
475 let json = r#"{
478 "source_id": "mem_xyz",
479 "chunks_created": 1,
480 "memory_type": "fact",
481 "warnings": [],
482 "extraction_method": "unknown",
483 "enrichment": "pending",
484 "hint": "Stored. Origin is compiling classification + concept links in the background (~2s). Recall will surface the enriched form shortly."
485 }"#;
486 let parsed: StoreMemoryResponse = serde_json::from_str(json).unwrap();
487 assert_eq!(parsed.enrichment, "pending");
488 assert!(parsed.hint.contains("compiling"));
489 }
490
491 #[test]
492 fn store_memory_response_defaults_enrichment_for_older_responses() {
493 let json = r#"{
496 "source_id": "mem_old",
497 "chunks_created": 1,
498 "memory_type": "fact"
499 }"#;
500 let parsed: StoreMemoryResponse = serde_json::from_str(json).unwrap();
501 assert_eq!(parsed.enrichment, ""); assert_eq!(parsed.hint, ""); }
504
505 #[test]
506 fn store_memory_response_roundtrips_not_needed_state() {
507 let response = StoreMemoryResponse {
510 source_id: "mem_no_llm".into(),
511 chunks_created: 1,
512 memory_type: "fact".into(),
513 entity_id: None,
514 quality: None,
515 warnings: vec![],
516 extraction_method: "none".into(),
517 enrichment: "not_needed".into(),
518 hint: String::new(),
519 };
520 let json = serde_json::to_string(&response).unwrap();
521 assert!(json.contains("\"enrichment\":\"not_needed\""));
522 assert!(
523 !json.contains("\"hint\""),
524 "empty hint must be skipped on the wire, got: {json}"
525 );
526 let parsed: StoreMemoryResponse = serde_json::from_str(&json).unwrap();
527 assert_eq!(parsed.enrichment, "not_needed");
528 assert_eq!(parsed.hint, "");
529 }
530
531 #[test]
532 fn chat_context_response_roundtrips_with_empty_knowledge_sections() {
533 let response = ChatContextResponse {
534 context: "context".into(),
535 profile: ProfileContext {
536 narrative: "n".into(),
537 identity: vec![],
538 preferences: vec![],
539 goals: vec![],
540 },
541 knowledge: KnowledgeContext {
542 pages: vec![],
543 decisions: vec![],
544 relevant_memories: vec![],
545 graph_context: vec![],
546 },
547 took_ms: 1.0,
548 token_estimates: TierTokenEstimates {
549 tier1_identity: 1,
550 tier2_project: 2,
551 tier3_relevant: 3,
552 total: 6,
553 },
554 };
555
556 let json = serde_json::to_string(&response).unwrap();
557 let parsed: ChatContextResponse = serde_json::from_str(&json).unwrap();
558 assert!(parsed.knowledge.pages.is_empty());
559 assert!(parsed.knowledge.decisions.is_empty());
560 assert!(parsed.knowledge.relevant_memories.is_empty());
561 assert!(parsed.knowledge.graph_context.is_empty());
562 }
563}