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 #[deprecated(
146 since = "0.3.2",
147 note = "Goal taxonomy folded into Identity by migration 45 (Phase 0). \
148 Always empty. Will be removed in 0.4."
149 )]
150 #[serde(default, skip_serializing_if = "Vec::is_empty")]
151 pub goals: Vec<String>,
152}
153
154#[derive(Debug, Serialize, Deserialize)]
155pub struct KnowledgeContext {
156 #[serde(default, skip_serializing_if = "Vec::is_empty")]
157 pub pages: Vec<String>,
158 #[serde(default, skip_serializing_if = "Vec::is_empty")]
159 pub decisions: Vec<String>,
160 #[serde(default)]
161 pub relevant_memories: Vec<SearchResult>,
162 #[serde(default, skip_serializing_if = "Vec::is_empty")]
163 pub graph_context: Vec<String>,
164}
165
166#[derive(Debug, Serialize, Deserialize)]
167pub struct ChatContextResponse {
168 pub context: String,
169 pub profile: ProfileContext,
170 pub knowledge: KnowledgeContext,
171 pub took_ms: f64,
172 pub token_estimates: TierTokenEstimates,
173}
174
175#[derive(Debug, Serialize, Deserialize)]
178pub struct ProfileResponse {
179 pub id: String,
180 pub name: String,
181 pub display_name: Option<String>,
182 pub email: Option<String>,
183 pub bio: Option<String>,
184 pub avatar_path: Option<String>,
185 pub created_at: i64,
186 pub updated_at: i64,
187}
188
189#[derive(Debug, Serialize, Deserialize)]
190pub struct AgentResponse {
191 pub id: String,
192 pub name: String,
193 #[serde(default, skip_serializing_if = "Option::is_none")]
194 pub display_name: Option<String>,
195 pub agent_type: String,
196 pub description: Option<String>,
197 pub enabled: bool,
198 pub trust_level: String,
199 pub last_seen_at: Option<i64>,
200 pub memory_count: i64,
201 pub created_at: i64,
202 pub updated_at: i64,
203}
204
205#[derive(Debug, Serialize, Deserialize)]
208pub struct CreateEntityResponse {
209 pub id: String,
210}
211
212#[doc(hidden)]
213#[derive(Debug, Serialize, Deserialize)]
214pub struct CreateRelationResponse {
215 pub id: String,
216}
217
218#[derive(Debug, Serialize, Deserialize)]
219pub struct AddObservationResponse {
220 pub id: String,
221}
222
223#[derive(Debug, Serialize, Deserialize)]
224pub struct ListEntitiesResponse {
225 pub entities: Vec<Entity>,
226}
227
228#[derive(Debug, Serialize, Deserialize)]
229pub struct SearchEntitiesResponse {
230 pub results: Vec<EntitySearchResult>,
231}
232
233#[derive(Debug, Serialize, Deserialize)]
236pub struct ImportMemoriesResponse {
237 pub imported: usize,
238 pub skipped: usize,
239 pub breakdown: HashMap<String, usize>,
240 pub entities_created: usize,
241 pub observations_added: usize,
242 pub relations_created: usize,
243 pub batch_id: String,
244}
245
246#[doc(hidden)]
250#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
251pub enum Nudge {
252 Silent,
253 Ambient,
254 Notable,
255 Wow,
256}
257
258#[doc(hidden)]
260#[derive(Debug, Clone, Serialize, Deserialize)]
261pub struct PhaseResult {
262 pub name: String,
263 pub duration_ms: u64,
264 pub items_processed: usize,
265 pub error: Option<String>,
266 pub nudge: Nudge,
267 pub headline: Option<String>,
268}
269
270#[doc(hidden)]
271#[derive(Debug, Serialize, Deserialize)]
272pub struct SteepResponse {
273 pub memories_decayed: u64,
274 pub recaps_generated: u32,
275 pub distilled: u32,
276 pub pending_remaining: u32,
277 pub phases: Vec<PhaseResult>,
278}
279
280#[derive(Debug, Serialize, Deserialize)]
283pub struct ConfigResponse {
284 pub skip_apps: Vec<String>,
285 pub skip_title_patterns: Vec<String>,
286 pub private_browsing_detection: bool,
287 pub setup_completed: bool,
288 pub clipboard_enabled: bool,
289 pub screen_capture_enabled: bool,
290 pub remote_access_enabled: bool,
291 #[serde(default, skip_serializing_if = "Option::is_none")]
293 pub routine_model: Option<String>,
294 #[serde(default, skip_serializing_if = "Option::is_none")]
296 pub synthesis_model: Option<String>,
297 #[serde(default, skip_serializing_if = "Option::is_none")]
299 pub external_llm_endpoint: Option<String>,
300 #[serde(default, skip_serializing_if = "Option::is_none")]
302 pub external_llm_model: Option<String>,
303}
304
305#[derive(Debug, Serialize, Deserialize)]
308pub struct IndexedFilesResponse {
309 pub files: Vec<IndexedFileInfo>,
310}
311
312#[derive(Debug, Serialize, Deserialize)]
313pub struct DeleteCountResponse {
314 pub deleted: usize,
315}
316
317#[derive(Debug, Serialize, Deserialize)]
320pub struct SuccessResponse {
321 pub ok: bool,
322}
323
324#[derive(Debug, Serialize, Deserialize)]
327pub struct MemoryDetailResponse {
328 pub memory: Option<MemoryItem>,
329}
330
331#[derive(Debug, Clone, Serialize, Deserialize)]
333pub struct MemoryDetail {
334 pub id: String,
335 pub content: String,
336 pub title: String,
337 pub source_id: String,
338 pub chunk_index: i32,
339 #[serde(skip_serializing_if = "Option::is_none")]
340 pub chunk_type: Option<String>,
341 #[serde(skip_serializing_if = "Option::is_none")]
342 pub language: Option<String>,
343 #[serde(skip_serializing_if = "Option::is_none")]
344 pub semantic_unit: Option<String>,
345 #[serde(skip_serializing_if = "Option::is_none")]
346 pub byte_start: Option<i64>,
347 #[serde(skip_serializing_if = "Option::is_none")]
348 pub byte_end: Option<i64>,
349 #[serde(skip_serializing_if = "Option::is_none")]
350 pub summary: Option<String>,
351}
352
353#[derive(Debug, Clone, Serialize, Deserialize)]
355pub struct PendingRevision {
356 pub source_id: String,
357 pub content: String,
358 pub source_agent: Option<String>,
359}
360
361#[derive(Debug, Serialize, Deserialize)]
362pub struct VersionChainResponse {
363 pub versions: Vec<crate::memory::MemoryVersionItem>,
364}
365
366#[derive(Debug, Serialize, Deserialize)]
369pub struct TagsResponse {
370 pub tags: Vec<String>,
371}
372
373#[derive(Debug, Serialize, Deserialize)]
376pub struct ActivityResponse {
377 pub activities: Vec<crate::memory::AgentActivityRow>,
378}
379
380#[derive(Debug, Serialize, Deserialize)]
383pub struct DecisionsResponse {
384 pub decisions: Vec<MemoryItem>,
385}
386
387#[derive(Debug, Serialize, Deserialize)]
388pub struct DecisionDomainsResponse {
389 pub domains: Vec<String>,
390}
391
392#[derive(Debug, Serialize, Deserialize)]
395pub struct PinnedMemoriesResponse {
396 pub memories: Vec<MemoryItem>,
397}
398
399#[derive(Debug, Serialize, Deserialize)]
402pub struct IngestResponse {
403 pub chunks_created: usize,
404 pub document_id: String,
405}
406
407#[derive(Debug, Default, Serialize, Deserialize)]
414pub struct ExportStats {
415 pub exported: usize,
416 pub skipped: usize,
417 pub failed: usize,
418}
419
420#[derive(Debug, Deserialize, Serialize)]
421pub struct ExportPageResponse {
422 pub path: String,
423}
424
425#[derive(Debug, Deserialize, Serialize)]
428pub struct KnowledgePathResponse {
429 pub path: String,
430}
431
432#[derive(Debug, Deserialize, Serialize)]
433pub struct KnowledgeCountResponse {
434 pub count: u64,
435}
436
437#[doc(hidden)]
440#[derive(Debug, Clone, Serialize, Deserialize)]
441pub struct SyncStatsResponse {
442 pub files_found: usize,
443 pub ingested: usize,
444 pub skipped: usize,
445 pub errors: usize,
446}
447
448#[cfg(test)]
449mod tests {
450 use super::*;
451
452 #[test]
453 fn store_memory_response_deserializes_without_extraction_method() {
454 let json = r#"{
456 "source_id": "mem_abc",
457 "chunks_created": 3,
458 "memory_type": "fact"
459 }"#;
460 let parsed: StoreMemoryResponse = serde_json::from_str(json).unwrap();
461 assert_eq!(parsed.source_id, "mem_abc");
462 assert_eq!(parsed.chunks_created, 3);
463 assert_eq!(parsed.memory_type, "fact");
464 assert_eq!(parsed.extraction_method, "unknown");
465 assert!(parsed.warnings.is_empty());
466 }
467
468 #[test]
469 fn store_memory_response_deserializes_with_all_fields() {
470 let json = r#"{
471 "source_id": "mem_abc",
472 "chunks_created": 3,
473 "memory_type": "fact",
474 "warnings": ["decision memory missing claim"],
475 "extraction_method": "llm"
476 }"#;
477 let parsed: StoreMemoryResponse = serde_json::from_str(json).unwrap();
478 assert_eq!(parsed.warnings.len(), 1);
479 assert_eq!(parsed.extraction_method, "llm");
480 }
481
482 #[test]
483 fn store_memory_response_exposes_enrichment_and_hint() {
484 let json = r#"{
487 "source_id": "mem_xyz",
488 "chunks_created": 1,
489 "memory_type": "fact",
490 "warnings": [],
491 "extraction_method": "unknown",
492 "enrichment": "pending",
493 "hint": "Stored. Origin is compiling classification + concept links in the background (~2s). Recall will surface the enriched form shortly."
494 }"#;
495 let parsed: StoreMemoryResponse = serde_json::from_str(json).unwrap();
496 assert_eq!(parsed.enrichment, "pending");
497 assert!(parsed.hint.contains("compiling"));
498 }
499
500 #[test]
501 fn store_memory_response_defaults_enrichment_for_older_responses() {
502 let json = r#"{
505 "source_id": "mem_old",
506 "chunks_created": 1,
507 "memory_type": "fact"
508 }"#;
509 let parsed: StoreMemoryResponse = serde_json::from_str(json).unwrap();
510 assert_eq!(parsed.enrichment, ""); assert_eq!(parsed.hint, ""); }
513
514 #[test]
515 fn store_memory_response_roundtrips_not_needed_state() {
516 let response = StoreMemoryResponse {
519 source_id: "mem_no_llm".into(),
520 chunks_created: 1,
521 memory_type: "fact".into(),
522 entity_id: None,
523 quality: None,
524 warnings: vec![],
525 extraction_method: "none".into(),
526 enrichment: "not_needed".into(),
527 hint: String::new(),
528 };
529 let json = serde_json::to_string(&response).unwrap();
530 assert!(json.contains("\"enrichment\":\"not_needed\""));
531 assert!(
532 !json.contains("\"hint\""),
533 "empty hint must be skipped on the wire, got: {json}"
534 );
535 let parsed: StoreMemoryResponse = serde_json::from_str(&json).unwrap();
536 assert_eq!(parsed.enrichment, "not_needed");
537 assert_eq!(parsed.hint, "");
538 }
539
540 #[test]
541 fn chat_context_response_roundtrips_with_empty_knowledge_sections() {
542 #[allow(deprecated)]
545 let profile = ProfileContext {
546 narrative: "n".into(),
547 identity: vec![],
548 preferences: vec![],
549 goals: vec![],
550 };
551 let response = ChatContextResponse {
552 context: "context".into(),
553 profile,
554 knowledge: KnowledgeContext {
555 pages: vec![],
556 decisions: vec![],
557 relevant_memories: vec![],
558 graph_context: vec![],
559 },
560 took_ms: 1.0,
561 token_estimates: TierTokenEstimates {
562 tier1_identity: 1,
563 tier2_project: 2,
564 tier3_relevant: 3,
565 total: 6,
566 },
567 };
568
569 let json = serde_json::to_string(&response).unwrap();
570 let parsed: ChatContextResponse = serde_json::from_str(&json).unwrap();
571 assert!(parsed.knowledge.pages.is_empty());
572 assert!(parsed.knowledge.decisions.is_empty());
573 assert!(parsed.knowledge.relevant_memories.is_empty());
574 assert!(parsed.knowledge.graph_context.is_empty());
575 }
576}