1use crate::entities::{Entity, EntitySearchResult};
5use crate::memory::{IndexedFileInfo, MemoryItem, MemoryStats, SearchResult};
6use crate::pages::Page;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10#[derive(Debug, Serialize, Deserialize)]
13pub struct StoreMemoryResponse {
14 pub source_id: String,
15 pub chunks_created: usize,
16 pub memory_type: String,
20 #[serde(skip_serializing_if = "Option::is_none")]
21 pub entity_id: Option<String>,
22 #[serde(skip_serializing_if = "Option::is_none")]
23 pub quality: Option<String>,
24 #[serde(default, skip_serializing_if = "Vec::is_empty")]
26 pub warnings: Vec<String>,
27 #[serde(default = "default_extraction_method")]
29 pub extraction_method: String,
30 #[serde(default)]
37 pub enrichment: String,
38 #[serde(default, skip_serializing_if = "String::is_empty")]
43 pub hint: String,
44}
45
46fn default_extraction_method() -> String {
47 "unknown".to_string()
48}
49
50#[derive(Debug, Serialize, Deserialize)]
51pub struct SearchMemoryResponse {
52 pub results: Vec<SearchResult>,
53 pub took_ms: f64,
54}
55
56#[derive(Debug, Serialize, Deserialize)]
57pub struct ListMemoriesResponse {
58 pub memories: Vec<IndexedFileInfo>,
59}
60
61#[derive(Debug, Serialize, Deserialize)]
67pub struct DeleteResponse {
68 pub deleted: bool,
69}
70
71#[derive(Debug, Serialize, Deserialize)]
72pub struct ConfirmResponse {
73 pub confirmed: bool,
74}
75
76#[derive(Debug, Serialize, Deserialize)]
77pub struct ReclassifyMemoryResponse {
78 pub source_id: String,
79 pub memory_type: String,
80}
81
82#[derive(Debug, Serialize, Deserialize)]
83pub struct MemoryStatsResponse {
84 pub stats: MemoryStats,
85}
86
87#[derive(Debug, Serialize, Deserialize)]
88pub struct NurtureCardsResponse {
89 pub cards: Vec<MemoryItem>,
90}
91
92#[derive(Debug, Serialize, Deserialize)]
95pub struct HealthResponse {
96 pub status: String,
97 pub db_initialized: bool,
98 pub version: String,
99}
100
101#[derive(Debug, Serialize, Deserialize)]
102pub struct StatusResponse {
103 pub is_running: bool,
104 pub files_indexed: u64,
105 pub files_total: u64,
106 pub sources_connected: Vec<String>,
107}
108
109#[derive(Debug, Serialize, Deserialize)]
110pub struct SearchResponse {
111 pub results: Vec<SearchResult>,
112 pub took_ms: f64,
113}
114
115#[doc(hidden)]
116#[derive(Debug, Serialize, Deserialize)]
117pub struct ContextSuggestion {
118 pub content: String,
119 pub score: f32,
120 pub source: String,
121}
122
123#[doc(hidden)]
124#[derive(Debug, Serialize, Deserialize)]
125pub struct ContextResponse {
126 pub suggestions: Vec<ContextSuggestion>,
127 pub took_ms: f64,
128}
129
130#[derive(Debug, Default, Serialize, Deserialize)]
131pub struct TierTokenEstimates {
132 pub tier1_identity: usize,
133 pub tier2_project: usize,
134 pub tier3_relevant: usize,
135 pub total: usize,
136}
137
138#[derive(Debug, Serialize, Deserialize)]
139pub struct ProfileContext {
140 pub narrative: String,
141 pub identity: Vec<String>,
142 pub preferences: Vec<String>,
143 #[deprecated(
147 since = "0.3.2",
148 note = "Goal taxonomy folded into Identity by migration 45 (Phase 0). \
149 Always empty. Will be removed in 0.4."
150 )]
151 #[serde(default, skip_serializing_if = "Vec::is_empty")]
152 pub goals: Vec<String>,
153}
154
155#[derive(Debug, Serialize, Deserialize)]
156pub struct KnowledgeContext {
157 #[serde(default, skip_serializing_if = "Vec::is_empty")]
158 pub pages: Vec<String>,
159 #[serde(default, skip_serializing_if = "Vec::is_empty")]
160 pub decisions: Vec<String>,
161 #[serde(default)]
162 pub relevant_memories: Vec<SearchResult>,
163 #[serde(default, skip_serializing_if = "Vec::is_empty")]
164 pub graph_context: Vec<String>,
165}
166
167#[derive(Debug, Serialize, Deserialize)]
168pub struct ChatContextResponse {
169 pub context: String,
170 pub profile: ProfileContext,
171 pub knowledge: KnowledgeContext,
172 pub took_ms: f64,
173 pub token_estimates: TierTokenEstimates,
174}
175
176#[derive(Debug, Serialize, Deserialize)]
179pub struct ProfileResponse {
180 pub id: String,
181 pub name: String,
182 pub display_name: Option<String>,
183 pub email: Option<String>,
184 pub bio: Option<String>,
185 pub avatar_path: Option<String>,
186 pub created_at: i64,
187 pub updated_at: i64,
188}
189
190#[derive(Debug, Serialize, Deserialize)]
191pub struct AgentResponse {
192 pub id: String,
193 pub name: String,
194 #[serde(default, skip_serializing_if = "Option::is_none")]
195 pub display_name: Option<String>,
196 pub agent_type: String,
197 pub description: Option<String>,
198 pub enabled: bool,
199 pub trust_level: String,
200 pub last_seen_at: Option<i64>,
201 pub memory_count: i64,
202 pub created_at: i64,
203 pub updated_at: i64,
204}
205
206#[derive(Debug, Serialize, Deserialize)]
209pub struct CreateEntityResponse {
210 pub id: String,
211}
212
213#[doc(hidden)]
214#[derive(Debug, Serialize, Deserialize)]
215pub struct CreateRelationResponse {
216 pub id: String,
217}
218
219#[derive(Debug, Serialize, Deserialize)]
220pub struct AddObservationResponse {
221 pub id: String,
222}
223
224#[derive(Debug, Serialize, Deserialize)]
225pub struct ListEntitiesResponse {
226 pub entities: Vec<Entity>,
227}
228
229#[derive(Debug, Serialize, Deserialize)]
230pub struct SearchEntitiesResponse {
231 pub results: Vec<EntitySearchResult>,
232}
233
234#[derive(Debug, Serialize, Deserialize)]
235pub struct SearchPagesResponse {
236 pub pages: Vec<Page>,
237}
238
239#[derive(Debug, Serialize, Deserialize)]
243pub struct PageLinksResponse {
244 pub outbound: Vec<PageLinkOutbound>,
245 pub inbound: Vec<PageLinkInbound>,
246}
247
248#[derive(Debug, Serialize, Deserialize)]
249pub struct PageLinkOutbound {
250 pub label: String,
251 pub target_page_id: Option<String>,
254}
255
256#[derive(Debug, Serialize, Deserialize)]
257pub struct PageLinkInbound {
258 pub source_page_id: String,
259 pub label: String,
260}
261
262#[derive(Debug, Serialize, Deserialize)]
265pub struct ImportMemoriesResponse {
266 pub imported: usize,
267 pub skipped: usize,
268 pub breakdown: HashMap<String, usize>,
269 pub entities_created: usize,
270 pub observations_added: usize,
271 pub relations_created: usize,
272 pub batch_id: String,
273}
274
275#[doc(hidden)]
279#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
280pub enum Nudge {
281 Silent,
282 Ambient,
283 Notable,
284 Wow,
285}
286
287#[doc(hidden)]
289#[derive(Debug, Clone, Serialize, Deserialize)]
290pub struct PhaseResult {
291 pub name: String,
292 pub duration_ms: u64,
293 pub items_processed: usize,
294 pub error: Option<String>,
295 pub nudge: Nudge,
296 pub headline: Option<String>,
297}
298
299#[doc(hidden)]
300#[derive(Debug, Serialize, Deserialize)]
301pub struct SteepResponse {
302 pub memories_decayed: u64,
303 pub recaps_generated: u32,
304 pub distilled: u32,
305 pub pending_remaining: u32,
306 pub phases: Vec<PhaseResult>,
307}
308
309#[derive(Debug, Serialize, Deserialize)]
312pub struct ConfigResponse {
313 pub skip_apps: Vec<String>,
314 pub skip_title_patterns: Vec<String>,
315 pub private_browsing_detection: bool,
316 pub setup_completed: bool,
317 pub clipboard_enabled: bool,
318 pub screen_capture_enabled: bool,
319 pub remote_access_enabled: bool,
320 #[serde(default, skip_serializing_if = "Option::is_none")]
322 pub routine_model: Option<String>,
323 #[serde(default, skip_serializing_if = "Option::is_none")]
325 pub synthesis_model: Option<String>,
326 #[serde(default, skip_serializing_if = "Option::is_none")]
328 pub external_llm_endpoint: Option<String>,
329 #[serde(default, skip_serializing_if = "Option::is_none")]
331 pub external_llm_model: Option<String>,
332}
333
334#[derive(Debug, Serialize, Deserialize)]
337pub struct IndexedFilesResponse {
338 pub files: Vec<IndexedFileInfo>,
339}
340
341#[derive(Debug, Serialize, Deserialize)]
342pub struct DeleteCountResponse {
343 pub deleted: usize,
344}
345
346#[derive(Debug, Serialize, Deserialize)]
349pub struct SuccessResponse {
350 pub ok: bool,
351}
352
353#[derive(Debug, Serialize, Deserialize)]
356pub struct MemoryDetailResponse {
357 pub memory: Option<MemoryItem>,
358}
359
360#[derive(Debug, Clone, Serialize, Deserialize)]
362pub struct MemoryDetail {
363 pub id: String,
364 pub content: String,
365 pub title: String,
366 pub source_id: String,
367 pub chunk_index: i32,
368 #[serde(skip_serializing_if = "Option::is_none")]
369 pub chunk_type: Option<String>,
370 #[serde(skip_serializing_if = "Option::is_none")]
371 pub language: Option<String>,
372 #[serde(skip_serializing_if = "Option::is_none")]
373 pub semantic_unit: Option<String>,
374 #[serde(skip_serializing_if = "Option::is_none")]
375 pub byte_start: Option<i64>,
376 #[serde(skip_serializing_if = "Option::is_none")]
377 pub byte_end: Option<i64>,
378 #[serde(skip_serializing_if = "Option::is_none")]
379 pub summary: Option<String>,
380}
381
382#[derive(Debug, Clone, Serialize, Deserialize)]
384pub struct PendingRevision {
385 pub source_id: String,
386 pub content: String,
387 pub source_agent: Option<String>,
388}
389
390#[derive(Debug, Serialize, Deserialize)]
391pub struct VersionChainResponse {
392 pub versions: Vec<crate::memory::MemoryVersionItem>,
393}
394
395#[derive(Debug, Serialize, Deserialize)]
398pub struct TagsResponse {
399 pub tags: Vec<String>,
400}
401
402#[derive(Debug, Serialize, Deserialize)]
405pub struct ActivityResponse {
406 pub activities: Vec<crate::memory::AgentActivityRow>,
407}
408
409#[derive(Debug, Serialize, Deserialize)]
412pub struct DecisionsResponse {
413 pub decisions: Vec<MemoryItem>,
414}
415
416#[derive(Debug, Serialize, Deserialize)]
417pub struct DecisionDomainsResponse {
418 pub domains: Vec<String>,
419}
420
421#[derive(Debug, Serialize, Deserialize)]
424pub struct PinnedMemoriesResponse {
425 pub memories: Vec<MemoryItem>,
426}
427
428#[derive(Debug, Serialize, Deserialize)]
431pub struct IngestResponse {
432 pub chunks_created: usize,
433 pub document_id: String,
434}
435
436#[derive(Debug, Default, Serialize, Deserialize)]
443pub struct ExportStats {
444 pub exported: usize,
445 pub skipped: usize,
446 pub failed: usize,
447}
448
449#[derive(Debug, Deserialize, Serialize)]
450pub struct ExportPageResponse {
451 pub path: String,
452}
453
454#[derive(Debug, Deserialize, Serialize)]
457pub struct KnowledgePathResponse {
458 pub path: String,
459}
460
461#[derive(Debug, Deserialize, Serialize)]
462pub struct KnowledgeCountResponse {
463 pub count: u64,
464}
465
466#[doc(hidden)]
469#[derive(Debug, Clone, Serialize, Deserialize)]
470pub struct SyncStatsResponse {
471 pub files_found: usize,
472 pub ingested: usize,
473 pub skipped: usize,
474 pub errors: usize,
475}
476
477#[cfg(test)]
478mod tests {
479 use super::*;
480
481 #[test]
482 fn store_memory_response_deserializes_without_extraction_method() {
483 let json = r#"{
485 "source_id": "mem_abc",
486 "chunks_created": 3,
487 "memory_type": "fact"
488 }"#;
489 let parsed: StoreMemoryResponse = serde_json::from_str(json).unwrap();
490 assert_eq!(parsed.source_id, "mem_abc");
491 assert_eq!(parsed.chunks_created, 3);
492 assert_eq!(parsed.memory_type, "fact");
493 assert_eq!(parsed.extraction_method, "unknown");
494 assert!(parsed.warnings.is_empty());
495 }
496
497 #[test]
498 fn store_memory_response_deserializes_with_all_fields() {
499 let json = r#"{
500 "source_id": "mem_abc",
501 "chunks_created": 3,
502 "memory_type": "fact",
503 "warnings": ["decision memory missing claim"],
504 "extraction_method": "llm"
505 }"#;
506 let parsed: StoreMemoryResponse = serde_json::from_str(json).unwrap();
507 assert_eq!(parsed.warnings.len(), 1);
508 assert_eq!(parsed.extraction_method, "llm");
509 }
510
511 #[test]
512 fn store_memory_response_exposes_enrichment_and_hint() {
513 let json = r#"{
516 "source_id": "mem_xyz",
517 "chunks_created": 1,
518 "memory_type": "fact",
519 "warnings": [],
520 "extraction_method": "unknown",
521 "enrichment": "pending",
522 "hint": "Stored. Origin is compiling classification + concept links in the background (~2s). Recall will surface the enriched form shortly."
523 }"#;
524 let parsed: StoreMemoryResponse = serde_json::from_str(json).unwrap();
525 assert_eq!(parsed.enrichment, "pending");
526 assert!(parsed.hint.contains("compiling"));
527 }
528
529 #[test]
530 fn store_memory_response_defaults_enrichment_for_older_responses() {
531 let json = r#"{
534 "source_id": "mem_old",
535 "chunks_created": 1,
536 "memory_type": "fact"
537 }"#;
538 let parsed: StoreMemoryResponse = serde_json::from_str(json).unwrap();
539 assert_eq!(parsed.enrichment, ""); assert_eq!(parsed.hint, ""); }
542
543 #[test]
544 fn store_memory_response_roundtrips_not_needed_state() {
545 let response = StoreMemoryResponse {
548 source_id: "mem_no_llm".into(),
549 chunks_created: 1,
550 memory_type: "fact".into(),
551 entity_id: None,
552 quality: None,
553 warnings: vec![],
554 extraction_method: "none".into(),
555 enrichment: "not_needed".into(),
556 hint: String::new(),
557 };
558 let json = serde_json::to_string(&response).unwrap();
559 assert!(json.contains("\"enrichment\":\"not_needed\""));
560 assert!(
561 !json.contains("\"hint\""),
562 "empty hint must be skipped on the wire, got: {json}"
563 );
564 let parsed: StoreMemoryResponse = serde_json::from_str(&json).unwrap();
565 assert_eq!(parsed.enrichment, "not_needed");
566 assert_eq!(parsed.hint, "");
567 }
568
569 #[test]
570 fn chat_context_response_roundtrips_with_empty_knowledge_sections() {
571 #[allow(deprecated)]
574 let profile = ProfileContext {
575 narrative: "n".into(),
576 identity: vec![],
577 preferences: vec![],
578 goals: vec![],
579 };
580 let response = ChatContextResponse {
581 context: "context".into(),
582 profile,
583 knowledge: KnowledgeContext {
584 pages: vec![],
585 decisions: vec![],
586 relevant_memories: vec![],
587 graph_context: vec![],
588 },
589 took_ms: 1.0,
590 token_estimates: TierTokenEstimates {
591 tier1_identity: 1,
592 tier2_project: 2,
593 tier3_relevant: 3,
594 total: 6,
595 },
596 };
597
598 let json = serde_json::to_string(&response).unwrap();
599 let parsed: ChatContextResponse = serde_json::from_str(&json).unwrap();
600 assert!(parsed.knowledge.pages.is_empty());
601 assert!(parsed.knowledge.decisions.is_empty());
602 assert!(parsed.knowledge.relevant_memories.is_empty());
603 assert!(parsed.knowledge.graph_context.is_empty());
604 }
605}