Skip to main content

origin_types/
responses.rs

1// SPDX-License-Identifier: Apache-2.0
2//! API response types for all HTTP endpoints.
3
4use crate::entities::{Entity, EntitySearchResult};
5use crate::memory::{IndexedFileInfo, MemoryItem, MemoryStats, SearchResult};
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9// ===== Memory CRUD =====
10
11#[derive(Debug, Serialize, Deserialize)]
12pub struct StoreMemoryResponse {
13    pub source_id: String,
14    pub chunks_created: usize,
15    pub memory_type: String,
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub entity_id: Option<String>,
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub quality: Option<String>,
20    /// Schema-validation issues — actionable by the agent.
21    #[serde(default, skip_serializing_if = "Vec::is_empty")]
22    pub warnings: Vec<String>,
23    /// How structured fields were populated. "agent" | "llm" | "none" | "unknown" (forward-compat default).
24    #[serde(default = "default_extraction_method")]
25    pub extraction_method: String,
26}
27
28fn default_extraction_method() -> String {
29    "unknown".to_string()
30}
31
32#[derive(Debug, Serialize, Deserialize)]
33pub struct SearchMemoryResponse {
34    pub results: Vec<SearchResult>,
35    pub took_ms: f64,
36}
37
38#[derive(Debug, Serialize, Deserialize)]
39pub struct ListMemoriesResponse {
40    pub memories: Vec<IndexedFileInfo>,
41}
42
43/// Shared wire format for any `deleted: bool` response.
44///
45/// Reused by:
46/// - `DELETE /api/memory/delete/{id}` (server/memory.rs)
47/// - `DELETE /api/documents/{source}/{source_id}` (server/ingest.rs)
48#[derive(Debug, Serialize, Deserialize)]
49pub struct DeleteResponse {
50    pub deleted: bool,
51}
52
53#[derive(Debug, Serialize, Deserialize)]
54pub struct ConfirmResponse {
55    pub confirmed: bool,
56}
57
58#[derive(Debug, Serialize, Deserialize)]
59pub struct ReclassifyMemoryResponse {
60    pub source_id: String,
61    pub memory_type: String,
62}
63
64#[derive(Debug, Serialize, Deserialize)]
65pub struct MemoryStatsResponse {
66    pub stats: MemoryStats,
67}
68
69#[derive(Debug, Serialize, Deserialize)]
70pub struct NurtureCardsResponse {
71    pub cards: Vec<MemoryItem>,
72}
73
74// ===== General search/context =====
75
76#[derive(Debug, Serialize, Deserialize)]
77pub struct HealthResponse {
78    pub status: String,
79    pub db_initialized: bool,
80    pub version: String,
81}
82
83#[derive(Debug, Serialize, Deserialize)]
84pub struct StatusResponse {
85    pub is_running: bool,
86    pub files_indexed: u64,
87    pub files_total: u64,
88    pub sources_connected: Vec<String>,
89}
90
91#[derive(Debug, Serialize, Deserialize)]
92pub struct SearchResponse {
93    pub results: Vec<SearchResult>,
94    pub took_ms: f64,
95}
96
97#[doc(hidden)]
98#[derive(Debug, Serialize, Deserialize)]
99pub struct ContextSuggestion {
100    pub content: String,
101    pub score: f32,
102    pub source: String,
103}
104
105#[doc(hidden)]
106#[derive(Debug, Serialize, Deserialize)]
107pub struct ContextResponse {
108    pub suggestions: Vec<ContextSuggestion>,
109    pub took_ms: f64,
110}
111
112#[derive(Debug, Default, Serialize, Deserialize)]
113pub struct TierTokenEstimates {
114    pub tier1_identity: usize,
115    pub tier2_project: usize,
116    pub tier3_relevant: usize,
117    pub total: usize,
118}
119
120#[derive(Debug, Serialize, Deserialize)]
121pub struct ProfileContext {
122    pub narrative: String,
123    pub identity: Vec<String>,
124    pub preferences: Vec<String>,
125    pub goals: Vec<String>,
126}
127
128#[derive(Debug, Serialize, Deserialize)]
129pub struct KnowledgeContext {
130    #[serde(default, skip_serializing_if = "Vec::is_empty")]
131    pub concepts: Vec<String>,
132    #[serde(default, skip_serializing_if = "Vec::is_empty")]
133    pub decisions: Vec<String>,
134    #[serde(default)]
135    pub relevant_memories: Vec<SearchResult>,
136    #[serde(default, skip_serializing_if = "Vec::is_empty")]
137    pub graph_context: Vec<String>,
138}
139
140#[derive(Debug, Serialize, Deserialize)]
141pub struct ChatContextResponse {
142    pub context: String,
143    pub profile: ProfileContext,
144    pub knowledge: KnowledgeContext,
145    pub took_ms: f64,
146    pub token_estimates: TierTokenEstimates,
147}
148
149// ===== Profile & Agents =====
150
151#[derive(Debug, Serialize, Deserialize)]
152pub struct ProfileResponse {
153    pub id: String,
154    pub name: String,
155    pub display_name: Option<String>,
156    pub email: Option<String>,
157    pub bio: Option<String>,
158    pub avatar_path: Option<String>,
159    pub created_at: i64,
160    pub updated_at: i64,
161}
162
163#[derive(Debug, Serialize, Deserialize)]
164pub struct AgentResponse {
165    pub id: String,
166    pub name: String,
167    #[serde(default, skip_serializing_if = "Option::is_none")]
168    pub display_name: Option<String>,
169    pub agent_type: String,
170    pub description: Option<String>,
171    pub enabled: bool,
172    pub trust_level: String,
173    pub last_seen_at: Option<i64>,
174    pub memory_count: i64,
175    pub created_at: i64,
176    pub updated_at: i64,
177}
178
179// ===== Knowledge graph =====
180
181#[derive(Debug, Serialize, Deserialize)]
182pub struct CreateEntityResponse {
183    pub id: String,
184}
185
186#[doc(hidden)]
187#[derive(Debug, Serialize, Deserialize)]
188pub struct CreateRelationResponse {
189    pub id: String,
190}
191
192#[derive(Debug, Serialize, Deserialize)]
193pub struct AddObservationResponse {
194    pub id: String,
195}
196
197#[derive(Debug, Serialize, Deserialize)]
198pub struct ListEntitiesResponse {
199    pub entities: Vec<Entity>,
200}
201
202#[derive(Debug, Serialize, Deserialize)]
203pub struct SearchEntitiesResponse {
204    pub results: Vec<EntitySearchResult>,
205}
206
207// ===== Import =====
208
209#[derive(Debug, Serialize, Deserialize)]
210pub struct ImportMemoriesResponse {
211    pub imported: usize,
212    pub skipped: usize,
213    pub breakdown: HashMap<String, usize>,
214    pub entities_created: usize,
215    pub observations_added: usize,
216    pub relations_created: usize,
217    pub batch_id: String,
218}
219
220// ===== Steep =====
221
222/// How loud Origin should be about a phase's output.
223#[doc(hidden)]
224#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
225pub enum Nudge {
226    Silent,
227    Ambient,
228    Notable,
229    Wow,
230}
231
232/// Result of a single phase within a steep cycle.
233#[doc(hidden)]
234#[derive(Debug, Clone, Serialize, Deserialize)]
235pub struct PhaseResult {
236    pub name: String,
237    pub duration_ms: u64,
238    pub items_processed: usize,
239    pub error: Option<String>,
240    pub nudge: Nudge,
241    pub headline: Option<String>,
242}
243
244#[doc(hidden)]
245#[derive(Debug, Serialize, Deserialize)]
246pub struct SteepResponse {
247    pub memories_decayed: u64,
248    pub recaps_generated: u32,
249    pub distilled: u32,
250    pub pending_remaining: u32,
251    pub phases: Vec<PhaseResult>,
252}
253
254// ===== Config =====
255
256#[derive(Debug, Serialize, Deserialize)]
257pub struct ConfigResponse {
258    pub skip_apps: Vec<String>,
259    pub skip_title_patterns: Vec<String>,
260    pub private_browsing_detection: bool,
261    pub setup_completed: bool,
262    pub clipboard_enabled: bool,
263    pub screen_capture_enabled: bool,
264    pub selection_capture_enabled: bool,
265    pub remote_access_enabled: bool,
266}
267
268// ===== Indexed files / chunks =====
269
270#[derive(Debug, Serialize, Deserialize)]
271pub struct IndexedFilesResponse {
272    pub files: Vec<IndexedFileInfo>,
273}
274
275#[derive(Debug, Serialize, Deserialize)]
276pub struct DeleteCountResponse {
277    pub deleted: usize,
278}
279
280// ===== Entity / Observation =====
281
282#[derive(Debug, Serialize, Deserialize)]
283pub struct SuccessResponse {
284    pub ok: bool,
285}
286
287// ===== Memory detail =====
288
289#[derive(Debug, Serialize, Deserialize)]
290pub struct MemoryDetailResponse {
291    pub memory: Option<MemoryItem>,
292}
293
294#[derive(Debug, Serialize, Deserialize)]
295pub struct VersionChainResponse {
296    pub versions: Vec<crate::memory::MemoryVersionItem>,
297}
298
299// ===== Tags =====
300
301#[derive(Debug, Serialize, Deserialize)]
302pub struct TagsResponse {
303    pub tags: Vec<String>,
304}
305
306// ===== Activity =====
307
308#[derive(Debug, Serialize, Deserialize)]
309pub struct ActivityResponse {
310    pub activities: Vec<crate::memory::AgentActivityRow>,
311}
312
313// ===== Decisions =====
314
315#[derive(Debug, Serialize, Deserialize)]
316pub struct DecisionsResponse {
317    pub decisions: Vec<MemoryItem>,
318}
319
320#[derive(Debug, Serialize, Deserialize)]
321pub struct DecisionDomainsResponse {
322    pub domains: Vec<String>,
323}
324
325// ===== Pinned =====
326
327#[derive(Debug, Serialize, Deserialize)]
328pub struct PinnedMemoriesResponse {
329    pub memories: Vec<MemoryItem>,
330}
331
332// ===== Ingest =====
333
334#[derive(Debug, Serialize, Deserialize)]
335pub struct IngestResponse {
336    pub chunks_created: usize,
337    pub document_id: String,
338}
339
340// Note: ingest's `DELETE /api/documents/{source}/{source_id}` reuses the
341// `DeleteResponse { deleted: bool }` defined above — same wire format.
342
343// ===== Concept Export =====
344
345#[derive(Debug, Deserialize, Serialize)]
346pub struct ExportConceptResponse {
347    pub path: String,
348}
349
350// ===== Knowledge Directory =====
351
352#[derive(Debug, Deserialize, Serialize)]
353pub struct KnowledgePathResponse {
354    pub path: String,
355}
356
357#[derive(Debug, Deserialize, Serialize)]
358pub struct KnowledgeCountResponse {
359    pub count: u64,
360}
361
362// ===== Sources =====
363
364#[doc(hidden)]
365#[derive(Debug, Clone, Serialize, Deserialize)]
366pub struct SyncStatsResponse {
367    pub files_found: usize,
368    pub ingested: usize,
369    pub skipped: usize,
370    pub errors: usize,
371}
372
373#[cfg(test)]
374mod tests {
375    use super::*;
376
377    #[test]
378    fn store_memory_response_deserializes_without_extraction_method() {
379        // Forward-compat: older server responses (pre-D9) omit extraction_method entirely.
380        let json = r#"{
381            "source_id": "mem_abc",
382            "chunks_created": 3,
383            "memory_type": "fact"
384        }"#;
385        let parsed: StoreMemoryResponse = serde_json::from_str(json).unwrap();
386        assert_eq!(parsed.source_id, "mem_abc");
387        assert_eq!(parsed.chunks_created, 3);
388        assert_eq!(parsed.memory_type, "fact");
389        assert_eq!(parsed.extraction_method, "unknown");
390        assert!(parsed.warnings.is_empty());
391    }
392
393    #[test]
394    fn store_memory_response_deserializes_with_all_fields() {
395        let json = r#"{
396            "source_id": "mem_abc",
397            "chunks_created": 3,
398            "memory_type": "fact",
399            "warnings": ["decision memory missing claim"],
400            "extraction_method": "llm"
401        }"#;
402        let parsed: StoreMemoryResponse = serde_json::from_str(json).unwrap();
403        assert_eq!(parsed.warnings.len(), 1);
404        assert_eq!(parsed.extraction_method, "llm");
405    }
406
407    #[test]
408    fn chat_context_response_roundtrips_with_empty_knowledge_sections() {
409        let response = ChatContextResponse {
410            context: "context".into(),
411            profile: ProfileContext {
412                narrative: "n".into(),
413                identity: vec![],
414                preferences: vec![],
415                goals: vec![],
416            },
417            knowledge: KnowledgeContext {
418                concepts: vec![],
419                decisions: vec![],
420                relevant_memories: vec![],
421                graph_context: vec![],
422            },
423            took_ms: 1.0,
424            token_estimates: TierTokenEstimates {
425                tier1_identity: 1,
426                tier2_project: 2,
427                tier3_relevant: 3,
428                total: 6,
429            },
430        };
431
432        let json = serde_json::to_string(&response).unwrap();
433        let parsed: ChatContextResponse = serde_json::from_str(&json).unwrap();
434        assert!(parsed.knowledge.concepts.is_empty());
435        assert!(parsed.knowledge.decisions.is_empty());
436        assert!(parsed.knowledge.relevant_memories.is_empty());
437        assert!(parsed.knowledge.graph_context.is_empty());
438    }
439}