Skip to main content

common/
types.rs

1use serde::{Deserialize, Serialize};
2
3/// Unique identifier for a vector
4pub type VectorId = String;
5
6/// Namespace identifier
7pub type NamespaceId = String;
8
9/// A vector with associated metadata
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct Vector {
12    pub id: VectorId,
13    pub values: Vec<f32>,
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub metadata: Option<serde_json::Value>,
16    /// TTL in seconds (optional, for upsert requests)
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub ttl_seconds: Option<u64>,
19    /// Unix timestamp when this vector expires (internal use)
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub expires_at: Option<u64>,
22}
23
24impl Vector {
25    /// Check if this vector has expired
26    pub fn is_expired(&self) -> bool {
27        if let Some(expires_at) = self.expires_at {
28            let now = std::time::SystemTime::now()
29                .duration_since(std::time::UNIX_EPOCH)
30                .unwrap_or_default()
31                .as_secs();
32            now >= expires_at
33        } else {
34            false
35        }
36    }
37
38    /// Check if this vector has expired against a pre-captured timestamp.
39    /// Prefer this over `is_expired()` inside loops to avoid N syscalls.
40    #[inline]
41    pub fn is_expired_at(&self, now_secs: u64) -> bool {
42        self.expires_at.is_some_and(|exp| now_secs >= exp)
43    }
44
45    /// Calculate and set expires_at from ttl_seconds
46    pub fn apply_ttl(&mut self) {
47        if let Some(ttl) = self.ttl_seconds {
48            let now = std::time::SystemTime::now()
49                .duration_since(std::time::UNIX_EPOCH)
50                .unwrap_or_default()
51                .as_secs();
52            self.expires_at = Some(now + ttl);
53        }
54    }
55
56    /// Get remaining TTL in seconds (None if no expiration or expired)
57    pub fn remaining_ttl(&self) -> Option<u64> {
58        self.expires_at.and_then(|expires_at| {
59            let now = std::time::SystemTime::now()
60                .duration_since(std::time::UNIX_EPOCH)
61                .unwrap_or_default()
62                .as_secs();
63            if now < expires_at {
64                Some(expires_at - now)
65            } else {
66                None
67            }
68        })
69    }
70}
71
72/// Request to upsert vectors
73#[derive(Debug, Deserialize)]
74pub struct UpsertRequest {
75    pub vectors: Vec<Vector>,
76}
77
78/// Response from upsert operation
79#[derive(Debug, Serialize, Deserialize)]
80pub struct UpsertResponse {
81    pub upserted_count: usize,
82}
83
84/// Column-based upsert request (Turbopuffer-inspired)
85/// All arrays must have equal length. Use null for missing values.
86#[derive(Debug, Deserialize)]
87pub struct ColumnUpsertRequest {
88    /// Array of document IDs (required)
89    pub ids: Vec<VectorId>,
90    /// Array of vectors (required for vector namespaces)
91    pub vectors: Vec<Vec<f32>>,
92    /// Additional attributes as columns (optional)
93    /// Each key is an attribute name, value is array of attribute values
94    #[serde(default)]
95    pub attributes: std::collections::HashMap<String, Vec<serde_json::Value>>,
96    /// TTL in seconds for all vectors (optional)
97    #[serde(default)]
98    pub ttl_seconds: Option<u64>,
99    /// Expected dimension (optional, for validation)
100    #[serde(default)]
101    pub dimension: Option<usize>,
102}
103
104impl ColumnUpsertRequest {
105    /// Convert column format to row format (Vec<Vector>)
106    pub fn to_vectors(&self) -> Result<Vec<Vector>, String> {
107        let count = self.ids.len();
108
109        // Validate all arrays have same length
110        if self.vectors.len() != count {
111            return Err(format!(
112                "vectors array length ({}) doesn't match ids array length ({})",
113                self.vectors.len(),
114                count
115            ));
116        }
117
118        for (attr_name, attr_values) in &self.attributes {
119            if attr_values.len() != count {
120                return Err(format!(
121                    "attribute '{}' array length ({}) doesn't match ids array length ({})",
122                    attr_name,
123                    attr_values.len(),
124                    count
125                ));
126            }
127        }
128
129        // Validate vector dimensions
130        // Use explicit dimension if provided, otherwise derive from first vector
131        let expected_dim = if let Some(dim) = self.dimension {
132            Some(dim)
133        } else {
134            self.vectors.first().map(|v| v.len())
135        };
136
137        if let Some(expected) = expected_dim {
138            for (i, vec) in self.vectors.iter().enumerate() {
139                if vec.len() != expected {
140                    return Err(format!(
141                        "vectors[{}] has dimension {} but expected {}",
142                        i,
143                        vec.len(),
144                        expected
145                    ));
146                }
147            }
148        }
149
150        // Convert to row format
151        let mut vectors = Vec::with_capacity(count);
152        for i in 0..count {
153            // Build metadata from attributes
154            let metadata = if self.attributes.is_empty() {
155                None
156            } else {
157                let mut meta = serde_json::Map::new();
158                for (attr_name, attr_values) in &self.attributes {
159                    let value = &attr_values[i];
160                    if !value.is_null() {
161                        meta.insert(attr_name.clone(), value.clone());
162                    }
163                }
164                if meta.is_empty() {
165                    None
166                } else {
167                    Some(serde_json::Value::Object(meta))
168                }
169            };
170
171            let mut vector = Vector {
172                id: self.ids[i].clone(),
173                values: self.vectors[i].clone(),
174                metadata,
175                ttl_seconds: self.ttl_seconds,
176                expires_at: None,
177            };
178            vector.apply_ttl();
179            vectors.push(vector);
180        }
181
182        Ok(vectors)
183    }
184}
185
186/// Distance metric for vector comparison
187#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
188#[serde(rename_all = "snake_case")]
189pub enum DistanceMetric {
190    #[default]
191    Cosine,
192    Euclidean,
193    DotProduct,
194}
195
196/// Read consistency level for queries (Turbopuffer-inspired)
197///
198/// Controls the trade-off between read latency and data freshness.
199/// - `Strong`: Read from primary only, ensures latest data (higher latency)
200/// - `Eventual`: Read from any replica, may return stale data (lower latency)
201/// - `BoundedStaleness`: Allow reads from replicas within staleness threshold
202#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
203#[serde(rename_all = "snake_case")]
204pub enum ReadConsistency {
205    /// Read from primary replica only - ensures latest data
206    Strong,
207    /// Read from any available replica - faster but may be stale
208    #[default]
209    Eventual,
210    /// Allow staleness up to specified milliseconds
211    #[serde(rename = "bounded_staleness")]
212    BoundedStaleness,
213}
214
215/// Configuration for bounded staleness reads
216#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
217pub struct StalenessConfig {
218    /// Maximum acceptable staleness in milliseconds
219    #[serde(default = "default_max_staleness_ms")]
220    pub max_staleness_ms: u64,
221}
222
223fn default_max_staleness_ms() -> u64 {
224    5000 // 5 seconds default
225}
226
227/// Query request for vector search
228#[derive(Debug, Deserialize)]
229pub struct QueryRequest {
230    pub vector: Vec<f32>,
231    #[serde(default = "default_top_k")]
232    pub top_k: usize,
233    #[serde(default)]
234    pub distance_metric: DistanceMetric,
235    #[serde(default = "default_true")]
236    pub include_metadata: bool,
237    #[serde(default)]
238    pub include_vectors: bool,
239    /// Optional metadata filter
240    #[serde(default)]
241    pub filter: Option<FilterExpression>,
242    /// Cursor for pagination (from previous response's next_cursor)
243    #[serde(default)]
244    pub cursor: Option<String>,
245    /// Read consistency level (Turbopuffer-inspired)
246    /// Controls trade-off between latency and data freshness
247    #[serde(default)]
248    pub consistency: ReadConsistency,
249    /// Staleness configuration for bounded_staleness consistency
250    #[serde(default)]
251    pub staleness_config: Option<StalenessConfig>,
252}
253
254fn default_top_k() -> usize {
255    10
256}
257
258fn default_true() -> bool {
259    true
260}
261
262/// Single search result
263#[derive(Debug, Serialize, Deserialize)]
264pub struct SearchResult {
265    pub id: VectorId,
266    pub score: f32,
267    #[serde(skip_serializing_if = "Option::is_none")]
268    pub metadata: Option<serde_json::Value>,
269    #[serde(skip_serializing_if = "Option::is_none")]
270    pub vector: Option<Vec<f32>>,
271}
272
273/// Query response
274#[derive(Debug, Serialize, Deserialize)]
275pub struct QueryResponse {
276    pub results: Vec<SearchResult>,
277    /// Cursor for fetching next page of results
278    #[serde(skip_serializing_if = "Option::is_none")]
279    pub next_cursor: Option<String>,
280    /// Whether there are more results available
281    #[serde(skip_serializing_if = "Option::is_none")]
282    pub has_more: Option<bool>,
283    /// Server-side search time in milliseconds
284    #[serde(default)]
285    pub search_time_ms: u64,
286}
287
288// ============================================================================
289// Cursor-based pagination types
290// ============================================================================
291
292/// Internal cursor state for pagination
293#[derive(Debug, Clone, Serialize, Deserialize)]
294pub struct PaginationCursor {
295    /// Last seen score for cursor-based pagination
296    pub last_score: f32,
297    /// Last seen ID for tie-breaking
298    pub last_id: String,
299}
300
301impl PaginationCursor {
302    /// Create a new pagination cursor
303    pub fn new(last_score: f32, last_id: String) -> Self {
304        Self {
305            last_score,
306            last_id,
307        }
308    }
309
310    /// Encode cursor to base64 string
311    pub fn encode(&self) -> String {
312        use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
313        let json = serde_json::to_string(self).unwrap_or_default();
314        URL_SAFE_NO_PAD.encode(json.as_bytes())
315    }
316
317    /// Decode cursor from base64 string
318    pub fn decode(cursor: &str) -> Option<Self> {
319        use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
320        let bytes = URL_SAFE_NO_PAD.decode(cursor).ok()?;
321        let json = String::from_utf8(bytes).ok()?;
322        serde_json::from_str(&json).ok()
323    }
324}
325
326/// Delete request
327#[derive(Debug, Deserialize)]
328pub struct DeleteRequest {
329    pub ids: Vec<VectorId>,
330}
331
332/// Delete response
333#[derive(Debug, Serialize)]
334pub struct DeleteResponse {
335    pub deleted_count: usize,
336}
337
338// ============================================================================
339// Batch query types
340// ============================================================================
341
342/// A single query within a batch request
343#[derive(Debug, Clone, Deserialize)]
344pub struct BatchQueryItem {
345    /// Unique identifier for this query within the batch
346    #[serde(default)]
347    pub id: Option<String>,
348    /// The query vector
349    pub vector: Vec<f32>,
350    /// Number of results to return
351    #[serde(default = "default_batch_top_k")]
352    pub top_k: u32,
353    /// Optional filter expression
354    #[serde(default)]
355    pub filter: Option<FilterExpression>,
356    /// Whether to include metadata in results
357    #[serde(default)]
358    pub include_metadata: bool,
359    /// Read consistency level (Turbopuffer-inspired)
360    #[serde(default)]
361    pub consistency: ReadConsistency,
362    /// Staleness configuration for bounded_staleness consistency
363    #[serde(default)]
364    pub staleness_config: Option<StalenessConfig>,
365}
366
367fn default_batch_top_k() -> u32 {
368    10
369}
370
371/// Batch query request - execute multiple queries in parallel
372#[derive(Debug, Deserialize)]
373pub struct BatchQueryRequest {
374    /// List of queries to execute
375    pub queries: Vec<BatchQueryItem>,
376}
377
378/// Results for a single query within a batch
379#[derive(Debug, Serialize)]
380pub struct BatchQueryResult {
381    /// The query identifier (if provided in request)
382    #[serde(skip_serializing_if = "Option::is_none")]
383    pub id: Option<String>,
384    /// Query results (empty if an error occurred)
385    pub results: Vec<SearchResult>,
386    /// Query execution time in milliseconds
387    pub latency_ms: f64,
388    /// Error message if this individual query failed
389    #[serde(skip_serializing_if = "Option::is_none")]
390    pub error: Option<String>,
391}
392
393/// Batch query response
394#[derive(Debug, Serialize)]
395pub struct BatchQueryResponse {
396    /// Results for each query in the batch
397    pub results: Vec<BatchQueryResult>,
398    /// Total execution time in milliseconds
399    pub total_latency_ms: f64,
400    /// Number of queries executed
401    pub query_count: usize,
402}
403
404// ============================================================================
405// Multi-vector search types
406// ============================================================================
407
408/// Request for multi-vector search with positive and negative vectors
409#[derive(Debug, Deserialize)]
410pub struct MultiVectorSearchRequest {
411    /// Positive vectors to search towards (required, at least one)
412    pub positive_vectors: Vec<Vec<f32>>,
413    /// Weights for positive vectors (optional, defaults to equal weights)
414    #[serde(default)]
415    pub positive_weights: Option<Vec<f32>>,
416    /// Negative vectors to search away from (optional)
417    #[serde(default)]
418    pub negative_vectors: Option<Vec<Vec<f32>>>,
419    /// Weights for negative vectors (optional, defaults to equal weights)
420    #[serde(default)]
421    pub negative_weights: Option<Vec<f32>>,
422    /// Number of results to return
423    #[serde(default = "default_top_k")]
424    pub top_k: usize,
425    /// Distance metric to use
426    #[serde(default)]
427    pub distance_metric: DistanceMetric,
428    /// Minimum score threshold
429    #[serde(default)]
430    pub score_threshold: Option<f32>,
431    /// Enable MMR (Maximal Marginal Relevance) for diversity
432    #[serde(default)]
433    pub enable_mmr: bool,
434    /// Lambda parameter for MMR (0 = max diversity, 1 = max relevance)
435    #[serde(default = "default_mmr_lambda")]
436    pub mmr_lambda: f32,
437    /// Include metadata in results
438    #[serde(default = "default_true")]
439    pub include_metadata: bool,
440    /// Include vectors in results
441    #[serde(default)]
442    pub include_vectors: bool,
443    /// Optional metadata filter
444    #[serde(default)]
445    pub filter: Option<FilterExpression>,
446    /// Read consistency level (Turbopuffer-inspired)
447    #[serde(default)]
448    pub consistency: ReadConsistency,
449    /// Staleness configuration for bounded_staleness consistency
450    #[serde(default)]
451    pub staleness_config: Option<StalenessConfig>,
452}
453
454fn default_mmr_lambda() -> f32 {
455    0.5
456}
457
458/// Single result from multi-vector search
459#[derive(Debug, Serialize, Deserialize)]
460pub struct MultiVectorSearchResult {
461    pub id: VectorId,
462    /// Similarity score
463    pub score: f32,
464    /// MMR score (if MMR enabled)
465    #[serde(skip_serializing_if = "Option::is_none")]
466    pub mmr_score: Option<f32>,
467    /// Original rank before reranking
468    #[serde(skip_serializing_if = "Option::is_none")]
469    pub original_rank: Option<usize>,
470    #[serde(skip_serializing_if = "Option::is_none")]
471    pub metadata: Option<serde_json::Value>,
472    #[serde(skip_serializing_if = "Option::is_none")]
473    pub vector: Option<Vec<f32>>,
474}
475
476/// Response from multi-vector search
477#[derive(Debug, Serialize, Deserialize)]
478pub struct MultiVectorSearchResponse {
479    pub results: Vec<MultiVectorSearchResult>,
480    /// The computed query vector (weighted combination of positive - negative)
481    #[serde(skip_serializing_if = "Option::is_none")]
482    pub computed_query_vector: Option<Vec<f32>>,
483}
484
485// ============================================================================
486// Full-text search types
487// ============================================================================
488
489/// Request to index a document for full-text search
490#[derive(Debug, Serialize, Deserialize)]
491pub struct IndexDocumentRequest {
492    pub id: String,
493    pub text: String,
494    #[serde(skip_serializing_if = "Option::is_none")]
495    pub metadata: Option<serde_json::Value>,
496}
497
498/// Request to index multiple documents
499#[derive(Debug, Deserialize)]
500pub struct IndexDocumentsRequest {
501    pub documents: Vec<IndexDocumentRequest>,
502}
503
504/// Response from indexing operation
505#[derive(Debug, Serialize, Deserialize)]
506pub struct IndexDocumentsResponse {
507    pub indexed_count: usize,
508}
509
510/// Request to search for documents
511#[derive(Debug, Deserialize)]
512pub struct FullTextSearchRequest {
513    pub query: String,
514    #[serde(default = "default_top_k")]
515    pub top_k: usize,
516    /// Optional metadata filter
517    #[serde(default)]
518    pub filter: Option<FilterExpression>,
519}
520
521/// Single full-text search result
522#[derive(Debug, Serialize, Deserialize)]
523pub struct FullTextSearchResult {
524    pub id: String,
525    pub score: f32,
526    #[serde(skip_serializing_if = "Option::is_none")]
527    pub metadata: Option<serde_json::Value>,
528}
529
530/// Full-text search response
531#[derive(Debug, Serialize, Deserialize)]
532pub struct FullTextSearchResponse {
533    pub results: Vec<FullTextSearchResult>,
534    /// Server-side search time in milliseconds
535    #[serde(default)]
536    pub search_time_ms: u64,
537}
538
539/// Request to delete documents from full-text index
540#[derive(Debug, Deserialize)]
541pub struct DeleteDocumentsRequest {
542    pub ids: Vec<String>,
543}
544
545/// Response from deleting documents
546#[derive(Debug, Serialize)]
547pub struct DeleteDocumentsResponse {
548    pub deleted_count: usize,
549}
550
551/// Full-text index statistics
552#[derive(Debug, Serialize)]
553pub struct FullTextIndexStats {
554    pub document_count: u32,
555    pub unique_terms: usize,
556    pub avg_doc_length: f32,
557}
558
559// ============================================================================
560// Hybrid search types (vector + full-text)
561// ============================================================================
562
563/// Hybrid search request combining vector similarity and full-text search
564#[derive(Debug, Deserialize)]
565pub struct HybridSearchRequest {
566    /// Query vector for similarity search. Optional — if omitted, falls back to fulltext-only BM25
567    /// (equivalent to vector_weight=0.0).
568    #[serde(default)]
569    pub vector: Option<Vec<f32>>,
570    /// Text query for full-text search
571    pub text: String,
572    /// Number of results to return
573    #[serde(default = "default_top_k")]
574    pub top_k: usize,
575    /// Weight for vector search score (0.0 to 1.0)
576    /// Text search weight is (1.0 - vector_weight)
577    #[serde(default = "default_vector_weight")]
578    pub vector_weight: f32,
579    /// Distance metric for vector search
580    #[serde(default)]
581    pub distance_metric: DistanceMetric,
582    /// Include metadata in results
583    #[serde(default = "default_true")]
584    pub include_metadata: bool,
585    /// Include vectors in results
586    #[serde(default)]
587    pub include_vectors: bool,
588    /// Optional metadata filter
589    #[serde(default)]
590    pub filter: Option<FilterExpression>,
591}
592
593fn default_vector_weight() -> f32 {
594    0.5 // Equal weight by default
595}
596
597/// Single hybrid search result
598#[derive(Debug, Serialize, Deserialize)]
599pub struct HybridSearchResult {
600    pub id: String,
601    /// Combined score
602    pub score: f32,
603    /// Vector similarity score (normalized 0-1)
604    pub vector_score: f32,
605    /// Text search BM25 score (normalized 0-1)
606    pub text_score: f32,
607    #[serde(skip_serializing_if = "Option::is_none")]
608    pub metadata: Option<serde_json::Value>,
609    #[serde(skip_serializing_if = "Option::is_none")]
610    pub vector: Option<Vec<f32>>,
611}
612
613/// Hybrid search response
614#[derive(Debug, Serialize, Deserialize)]
615pub struct HybridSearchResponse {
616    pub results: Vec<HybridSearchResult>,
617    /// Server-side search time in milliseconds
618    #[serde(default)]
619    pub search_time_ms: u64,
620}
621
622// ============================================================================
623// Filter types for metadata filtering
624// ============================================================================
625
626/// A filter value that can be compared against metadata fields
627#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
628#[serde(untagged)]
629pub enum FilterValue {
630    String(String),
631    Number(f64),
632    Integer(i64),
633    Boolean(bool),
634    StringArray(Vec<String>),
635    NumberArray(Vec<f64>),
636}
637
638impl FilterValue {
639    /// Try to get as f64 for numeric comparisons
640    pub fn as_f64(&self) -> Option<f64> {
641        match self {
642            FilterValue::Number(n) => Some(*n),
643            FilterValue::Integer(i) => Some(*i as f64),
644            _ => None,
645        }
646    }
647
648    /// Try to get as string
649    pub fn as_str(&self) -> Option<&str> {
650        match self {
651            FilterValue::String(s) => Some(s.as_str()),
652            _ => None,
653        }
654    }
655
656    /// Try to get as bool
657    pub fn as_bool(&self) -> Option<bool> {
658        match self {
659            FilterValue::Boolean(b) => Some(*b),
660            _ => None,
661        }
662    }
663}
664
665/// Comparison operators for filter conditions
666#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
667#[serde(rename_all = "snake_case")]
668pub enum FilterCondition {
669    /// Equal to
670    #[serde(rename = "$eq")]
671    Eq(FilterValue),
672    /// Not equal to
673    #[serde(rename = "$ne")]
674    Ne(FilterValue),
675    /// Greater than
676    #[serde(rename = "$gt")]
677    Gt(FilterValue),
678    /// Greater than or equal to
679    #[serde(rename = "$gte")]
680    Gte(FilterValue),
681    /// Less than
682    #[serde(rename = "$lt")]
683    Lt(FilterValue),
684    /// Less than or equal to
685    #[serde(rename = "$lte")]
686    Lte(FilterValue),
687    /// In array
688    #[serde(rename = "$in")]
689    In(Vec<FilterValue>),
690    /// Not in array
691    #[serde(rename = "$nin")]
692    NotIn(Vec<FilterValue>),
693    /// Field exists
694    #[serde(rename = "$exists")]
695    Exists(bool),
696    // =========================================================================
697    // Enhanced string operators (Turbopuffer-inspired)
698    // =========================================================================
699    /// Contains substring (case-sensitive)
700    #[serde(rename = "$contains")]
701    Contains(String),
702    /// Contains substring (case-insensitive)
703    #[serde(rename = "$icontains")]
704    IContains(String),
705    /// Starts with prefix
706    #[serde(rename = "$startsWith")]
707    StartsWith(String),
708    /// Ends with suffix
709    #[serde(rename = "$endsWith")]
710    EndsWith(String),
711    /// Glob pattern matching (supports * and ? wildcards)
712    #[serde(rename = "$glob")]
713    Glob(String),
714    /// Regular expression matching
715    #[serde(rename = "$regex")]
716    Regex(String),
717}
718
719/// A filter expression that can be a single field condition or a logical combinator
720#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
721#[serde(untagged)]
722pub enum FilterExpression {
723    /// Logical AND of multiple expressions
724    And {
725        #[serde(rename = "$and")]
726        conditions: Vec<FilterExpression>,
727    },
728    /// Logical OR of multiple expressions
729    Or {
730        #[serde(rename = "$or")]
731        conditions: Vec<FilterExpression>,
732    },
733    /// Single field condition
734    Field {
735        #[serde(flatten)]
736        field: std::collections::HashMap<String, FilterCondition>,
737    },
738}
739
740// ============================================================================
741// Namespace quota types
742// ============================================================================
743
744/// Quota configuration for a namespace
745#[derive(Debug, Clone, Serialize, Deserialize, Default)]
746pub struct QuotaConfig {
747    /// Maximum number of vectors allowed (None = unlimited)
748    #[serde(skip_serializing_if = "Option::is_none")]
749    pub max_vectors: Option<u64>,
750    /// Maximum storage size in bytes (None = unlimited)
751    #[serde(skip_serializing_if = "Option::is_none")]
752    pub max_storage_bytes: Option<u64>,
753    /// Maximum dimensions per vector (None = unlimited)
754    #[serde(skip_serializing_if = "Option::is_none")]
755    pub max_dimensions: Option<usize>,
756    /// Maximum metadata size per vector in bytes (None = unlimited)
757    #[serde(skip_serializing_if = "Option::is_none")]
758    pub max_metadata_bytes: Option<usize>,
759    /// Whether to enforce quotas (soft limit = warn only, hard = reject)
760    #[serde(default)]
761    pub enforcement: QuotaEnforcement,
762}
763
764/// Quota enforcement mode
765#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
766#[serde(rename_all = "snake_case")]
767pub enum QuotaEnforcement {
768    /// No enforcement, just tracking
769    None,
770    /// Log warnings when quota exceeded but allow operations
771    Soft,
772    /// Reject operations that would exceed quota
773    #[default]
774    Hard,
775}
776
777/// Current quota usage for a namespace
778#[derive(Debug, Clone, Serialize, Deserialize, Default)]
779pub struct QuotaUsage {
780    /// Current number of vectors
781    pub vector_count: u64,
782    /// Current storage size in bytes (estimated)
783    pub storage_bytes: u64,
784    /// Average vector dimensions
785    pub avg_dimensions: Option<usize>,
786    /// Average metadata size in bytes
787    pub avg_metadata_bytes: Option<usize>,
788    /// Last updated timestamp (Unix epoch)
789    pub last_updated: u64,
790}
791
792impl QuotaUsage {
793    /// Create new usage with current timestamp
794    pub fn new(vector_count: u64, storage_bytes: u64) -> Self {
795        let now = std::time::SystemTime::now()
796            .duration_since(std::time::UNIX_EPOCH)
797            .unwrap_or_default()
798            .as_secs();
799        Self {
800            vector_count,
801            storage_bytes,
802            avg_dimensions: None,
803            avg_metadata_bytes: None,
804            last_updated: now,
805        }
806    }
807
808    /// Update the timestamp to now
809    pub fn touch(&mut self) {
810        self.last_updated = std::time::SystemTime::now()
811            .duration_since(std::time::UNIX_EPOCH)
812            .unwrap_or_default()
813            .as_secs();
814    }
815}
816
817/// Combined quota status showing config and current usage
818#[derive(Debug, Clone, Serialize, Deserialize)]
819pub struct QuotaStatus {
820    /// Namespace name
821    pub namespace: String,
822    /// Quota configuration
823    pub config: QuotaConfig,
824    /// Current usage
825    pub usage: QuotaUsage,
826    /// Percentage of vector quota used (0-100, None if unlimited)
827    #[serde(skip_serializing_if = "Option::is_none")]
828    pub vector_usage_percent: Option<f32>,
829    /// Percentage of storage quota used (0-100, None if unlimited)
830    #[serde(skip_serializing_if = "Option::is_none")]
831    pub storage_usage_percent: Option<f32>,
832    /// Whether any quota is exceeded
833    pub is_exceeded: bool,
834    /// List of exceeded quota types
835    #[serde(skip_serializing_if = "Vec::is_empty")]
836    pub exceeded_quotas: Vec<String>,
837}
838
839impl QuotaStatus {
840    /// Create a new quota status from config and usage
841    pub fn new(namespace: String, config: QuotaConfig, usage: QuotaUsage) -> Self {
842        let vector_usage_percent = config
843            .max_vectors
844            .map(|max| (usage.vector_count as f32 / max as f32) * 100.0);
845
846        let storage_usage_percent = config
847            .max_storage_bytes
848            .map(|max| (usage.storage_bytes as f32 / max as f32) * 100.0);
849
850        let mut exceeded_quotas = Vec::new();
851
852        if let Some(max) = config.max_vectors {
853            if usage.vector_count > max {
854                exceeded_quotas.push("max_vectors".to_string());
855            }
856        }
857
858        if let Some(max) = config.max_storage_bytes {
859            if usage.storage_bytes > max {
860                exceeded_quotas.push("max_storage_bytes".to_string());
861            }
862        }
863
864        let is_exceeded = !exceeded_quotas.is_empty();
865
866        Self {
867            namespace,
868            config,
869            usage,
870            vector_usage_percent,
871            storage_usage_percent,
872            is_exceeded,
873            exceeded_quotas,
874        }
875    }
876}
877
878/// Request to set quota for a namespace
879#[derive(Debug, Deserialize)]
880pub struct SetQuotaRequest {
881    /// Quota configuration to apply
882    pub config: QuotaConfig,
883}
884
885/// Response from setting quota
886#[derive(Debug, Serialize)]
887pub struct SetQuotaResponse {
888    /// Whether the operation succeeded
889    pub success: bool,
890    /// Namespace name
891    pub namespace: String,
892    /// Applied quota configuration
893    pub config: QuotaConfig,
894    /// Status message
895    pub message: String,
896}
897
898/// Quota check result
899#[derive(Debug, Clone, Serialize)]
900pub struct QuotaCheckResult {
901    /// Whether the operation is allowed
902    pub allowed: bool,
903    /// Reason if not allowed
904    #[serde(skip_serializing_if = "Option::is_none")]
905    pub reason: Option<String>,
906    /// Current usage
907    pub usage: QuotaUsage,
908    /// Quota that would be exceeded
909    #[serde(skip_serializing_if = "Option::is_none")]
910    pub exceeded_quota: Option<String>,
911}
912
913/// Response listing all namespace quotas
914#[derive(Debug, Serialize)]
915pub struct QuotaListResponse {
916    /// List of quota statuses per namespace
917    pub quotas: Vec<QuotaStatus>,
918    /// Total number of namespaces with quotas
919    pub total: u64,
920    /// Default quota configuration (if set)
921    #[serde(skip_serializing_if = "Option::is_none")]
922    pub default_config: Option<QuotaConfig>,
923}
924
925/// Response for default quota query
926#[derive(Debug, Serialize)]
927pub struct DefaultQuotaResponse {
928    /// Default quota configuration (None if not set)
929    pub config: Option<QuotaConfig>,
930}
931
932/// Request to set default quota configuration
933#[derive(Debug, Deserialize)]
934pub struct SetDefaultQuotaRequest {
935    /// Default quota configuration (None to remove)
936    pub config: Option<QuotaConfig>,
937}
938
939/// Request to check if an operation would exceed quota
940#[derive(Debug, Deserialize)]
941pub struct QuotaCheckRequest {
942    /// Vector IDs to check (simulated vectors)
943    pub vector_ids: Vec<String>,
944    /// Dimension of vectors (for size estimation)
945    #[serde(default)]
946    pub dimensions: Option<usize>,
947    /// Estimated metadata size per vector
948    #[serde(default)]
949    pub metadata_bytes: Option<usize>,
950}
951
952// ============================================================================
953// Export API Types (Turbopuffer-inspired)
954// ============================================================================
955
956/// Request to export vectors from a namespace with pagination
957#[derive(Debug, Deserialize)]
958pub struct ExportRequest {
959    /// Maximum number of vectors to return per page (default: 1000, max: 10000)
960    #[serde(default = "default_export_top_k")]
961    pub top_k: usize,
962    /// Cursor for pagination - the last vector ID from previous page
963    #[serde(skip_serializing_if = "Option::is_none")]
964    pub cursor: Option<String>,
965    /// Whether to include vector values in the response (default: true)
966    #[serde(default = "default_true")]
967    pub include_vectors: bool,
968    /// Whether to include metadata in the response (default: true)
969    #[serde(default = "default_true")]
970    pub include_metadata: bool,
971}
972
973fn default_export_top_k() -> usize {
974    1000
975}
976
977impl Default for ExportRequest {
978    fn default() -> Self {
979        Self {
980            top_k: 1000,
981            cursor: None,
982            include_vectors: true,
983            include_metadata: true,
984        }
985    }
986}
987
988/// A single exported vector record
989#[derive(Debug, Clone, Serialize, Deserialize)]
990pub struct ExportedVector {
991    /// Vector ID
992    pub id: String,
993    /// Vector values (optional based on include_vectors)
994    #[serde(skip_serializing_if = "Option::is_none")]
995    pub values: Option<Vec<f32>>,
996    /// Metadata (optional based on include_metadata)
997    #[serde(skip_serializing_if = "Option::is_none")]
998    pub metadata: Option<serde_json::Value>,
999    /// TTL in seconds if set
1000    #[serde(skip_serializing_if = "Option::is_none")]
1001    pub ttl_seconds: Option<u64>,
1002}
1003
1004impl From<&Vector> for ExportedVector {
1005    fn from(v: &Vector) -> Self {
1006        Self {
1007            id: v.id.clone(),
1008            values: Some(v.values.clone()),
1009            metadata: v.metadata.clone(),
1010            ttl_seconds: v.ttl_seconds,
1011        }
1012    }
1013}
1014
1015/// Response from export operation
1016#[derive(Debug, Serialize)]
1017pub struct ExportResponse {
1018    /// Exported vectors for this page
1019    pub vectors: Vec<ExportedVector>,
1020    /// Cursor for next page (None if this is the last page)
1021    #[serde(skip_serializing_if = "Option::is_none")]
1022    pub next_cursor: Option<String>,
1023    /// Total vectors in namespace (for progress tracking)
1024    pub total_count: usize,
1025    /// Number of vectors returned in this page
1026    pub returned_count: usize,
1027}
1028
1029// ============================================================================
1030// Unified Query API with rank_by (Turbopuffer-inspired)
1031// ============================================================================
1032
1033/// Ranking function for unified query API
1034/// Supports vector search (ANN/kNN), full-text BM25, and attribute ordering
1035#[derive(Debug, Clone, Serialize, Deserialize)]
1036#[serde(untagged)]
1037pub enum RankBy {
1038    /// Vector search: ["vector_field", "ANN"|"kNN", [query_vector]]
1039    /// or simplified: ["ANN", [query_vector]] for default "vector" field
1040    VectorSearch {
1041        field: String,
1042        method: VectorSearchMethod,
1043        query_vector: Vec<f32>,
1044    },
1045    /// Full-text BM25 search: ["text_field", "BM25", "query string"]
1046    FullTextSearch {
1047        field: String,
1048        method: String, // Always "BM25"
1049        query: String,
1050    },
1051    /// Attribute ordering: ["field_name", "asc"|"desc"]
1052    AttributeOrder {
1053        field: String,
1054        direction: SortDirection,
1055    },
1056    /// Sum of multiple ranking functions: ["Sum", [...rankings]]
1057    Sum(Vec<RankBy>),
1058    /// Max of multiple ranking functions: ["Max", [...rankings]]
1059    Max(Vec<RankBy>),
1060    /// Product with weight: ["Product", weight, ranking]
1061    Product { weight: f32, ranking: Box<RankBy> },
1062}
1063
1064/// Vector search method
1065#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
1066pub enum VectorSearchMethod {
1067    /// Approximate Nearest Neighbor (fast, default)
1068    #[default]
1069    ANN,
1070    /// Exact k-Nearest Neighbor (exhaustive, requires filters)
1071    #[serde(rename = "kNN")]
1072    KNN,
1073}
1074
1075/// Sort direction for attribute ordering
1076#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1077#[serde(rename_all = "lowercase")]
1078#[derive(Default)]
1079pub enum SortDirection {
1080    Asc,
1081    #[default]
1082    Desc,
1083}
1084
1085/// Unified query request with rank_by parameter (Turbopuffer-inspired)
1086#[derive(Debug, Deserialize)]
1087pub struct UnifiedQueryRequest {
1088    /// How to rank documents (required unless using aggregations)
1089    pub rank_by: RankByInput,
1090    /// Number of results to return
1091    #[serde(default = "default_top_k")]
1092    pub top_k: usize,
1093    /// Optional metadata filter
1094    #[serde(default)]
1095    pub filter: Option<FilterExpression>,
1096    /// Include metadata in results
1097    #[serde(default = "default_true")]
1098    pub include_metadata: bool,
1099    /// Include vectors in results
1100    #[serde(default)]
1101    pub include_vectors: bool,
1102    /// Distance metric for vector search (default: cosine)
1103    #[serde(default)]
1104    pub distance_metric: DistanceMetric,
1105}
1106
1107/// Input format for rank_by that handles JSON array syntax
1108/// Examples:
1109/// - ["vector", "ANN", [0.1, 0.2, 0.3]]
1110/// - ["text", "BM25", "search query"]
1111/// - ["timestamp", "desc"]
1112/// - ["Sum", [["title", "BM25", "query"], ["content", "BM25", "query"]]]
1113/// - ["Product", 2.0, ["title", "BM25", "query"]]
1114#[derive(Debug, Clone, Serialize, Deserialize)]
1115#[serde(from = "serde_json::Value")]
1116pub struct RankByInput(pub RankBy);
1117
1118impl From<serde_json::Value> for RankByInput {
1119    fn from(value: serde_json::Value) -> Self {
1120        RankByInput(parse_rank_by(&value).unwrap_or_else(|| {
1121            // Default fallback - shouldn't happen with valid input
1122            RankBy::AttributeOrder {
1123                field: "id".to_string(),
1124                direction: SortDirection::Asc,
1125            }
1126        }))
1127    }
1128}
1129
1130/// Parse rank_by JSON array into RankBy enum
1131fn parse_rank_by(value: &serde_json::Value) -> Option<RankBy> {
1132    let arr = value.as_array()?;
1133    if arr.is_empty() {
1134        return None;
1135    }
1136
1137    let first = arr.first()?.as_str()?;
1138
1139    match first {
1140        // Combination operators
1141        "Sum" => {
1142            let rankings = arr.get(1)?.as_array()?;
1143            let parsed: Option<Vec<RankBy>> = rankings.iter().map(parse_rank_by).collect();
1144            Some(RankBy::Sum(parsed?))
1145        }
1146        "Max" => {
1147            let rankings = arr.get(1)?.as_array()?;
1148            let parsed: Option<Vec<RankBy>> = rankings.iter().map(parse_rank_by).collect();
1149            Some(RankBy::Max(parsed?))
1150        }
1151        "Product" => {
1152            let weight = arr.get(1)?.as_f64()? as f32;
1153            let ranking = parse_rank_by(arr.get(2)?)?;
1154            Some(RankBy::Product {
1155                weight,
1156                ranking: Box::new(ranking),
1157            })
1158        }
1159        // Vector search shorthand: ["ANN", [vector]] or ["kNN", [vector]]
1160        "ANN" => {
1161            let query_vector = parse_vector_array(arr.get(1)?)?;
1162            Some(RankBy::VectorSearch {
1163                field: "vector".to_string(),
1164                method: VectorSearchMethod::ANN,
1165                query_vector,
1166            })
1167        }
1168        "kNN" => {
1169            let query_vector = parse_vector_array(arr.get(1)?)?;
1170            Some(RankBy::VectorSearch {
1171                field: "vector".to_string(),
1172                method: VectorSearchMethod::KNN,
1173                query_vector,
1174            })
1175        }
1176        // Field-based operations
1177        field => {
1178            let second = arr.get(1)?;
1179
1180            // Check if second element is a method string
1181            if let Some(method_str) = second.as_str() {
1182                match method_str {
1183                    "ANN" => {
1184                        let query_vector = parse_vector_array(arr.get(2)?)?;
1185                        Some(RankBy::VectorSearch {
1186                            field: field.to_string(),
1187                            method: VectorSearchMethod::ANN,
1188                            query_vector,
1189                        })
1190                    }
1191                    "kNN" => {
1192                        let query_vector = parse_vector_array(arr.get(2)?)?;
1193                        Some(RankBy::VectorSearch {
1194                            field: field.to_string(),
1195                            method: VectorSearchMethod::KNN,
1196                            query_vector,
1197                        })
1198                    }
1199                    "BM25" => {
1200                        let query = arr.get(2)?.as_str()?;
1201                        Some(RankBy::FullTextSearch {
1202                            field: field.to_string(),
1203                            method: "BM25".to_string(),
1204                            query: query.to_string(),
1205                        })
1206                    }
1207                    "asc" => Some(RankBy::AttributeOrder {
1208                        field: field.to_string(),
1209                        direction: SortDirection::Asc,
1210                    }),
1211                    "desc" => Some(RankBy::AttributeOrder {
1212                        field: field.to_string(),
1213                        direction: SortDirection::Desc,
1214                    }),
1215                    _ => None,
1216                }
1217            } else {
1218                None
1219            }
1220        }
1221    }
1222}
1223
1224/// Parse a JSON value into a vector of f32
1225fn parse_vector_array(value: &serde_json::Value) -> Option<Vec<f32>> {
1226    let arr = value.as_array()?;
1227    arr.iter().map(|v| v.as_f64().map(|n| n as f32)).collect()
1228}
1229
1230/// Unified query response with $dist scoring
1231#[derive(Debug, Serialize, Deserialize)]
1232pub struct UnifiedQueryResponse {
1233    /// Search results ordered by rank_by score
1234    pub results: Vec<UnifiedSearchResult>,
1235    /// Cursor for pagination (if more results available)
1236    #[serde(skip_serializing_if = "Option::is_none")]
1237    pub next_cursor: Option<String>,
1238}
1239
1240/// Single result from unified query
1241#[derive(Debug, Serialize, Deserialize)]
1242pub struct UnifiedSearchResult {
1243    /// Vector/document ID
1244    pub id: String,
1245    /// Ranking score (distance for vector search, BM25 score for text)
1246    /// Named $dist for Turbopuffer compatibility
1247    #[serde(rename = "$dist", skip_serializing_if = "Option::is_none")]
1248    pub dist: Option<f32>,
1249    /// Metadata if requested
1250    #[serde(skip_serializing_if = "Option::is_none")]
1251    pub metadata: Option<serde_json::Value>,
1252    /// Vector values if requested
1253    #[serde(skip_serializing_if = "Option::is_none")]
1254    pub vector: Option<Vec<f32>>,
1255}
1256
1257// ============================================================================
1258// Aggregation types (Turbopuffer-inspired)
1259// ============================================================================
1260
1261/// Aggregate function for computing values across documents
1262#[derive(Debug, Clone, Serialize, Deserialize)]
1263pub enum AggregateFunction {
1264    /// Count matching documents: ["Count"]
1265    Count,
1266    /// Sum numeric attribute values: ["Sum", "attribute_name"]
1267    Sum { field: String },
1268    /// Average numeric attribute values: ["Avg", "attribute_name"]
1269    Avg { field: String },
1270    /// Minimum numeric attribute value: ["Min", "attribute_name"]
1271    Min { field: String },
1272    /// Maximum numeric attribute value: ["Max", "attribute_name"]
1273    Max { field: String },
1274}
1275
1276/// Wrapper for parsing aggregate function from JSON array
1277#[derive(Debug, Clone, Serialize, Deserialize)]
1278#[serde(from = "serde_json::Value")]
1279pub struct AggregateFunctionInput(pub AggregateFunction);
1280
1281impl From<serde_json::Value> for AggregateFunctionInput {
1282    fn from(value: serde_json::Value) -> Self {
1283        parse_aggregate_function(&value)
1284            .map(AggregateFunctionInput)
1285            .unwrap_or_else(|| {
1286                // Default to count if parsing fails
1287                AggregateFunctionInput(AggregateFunction::Count)
1288            })
1289    }
1290}
1291
1292/// Parse aggregate function from JSON array
1293fn parse_aggregate_function(value: &serde_json::Value) -> Option<AggregateFunction> {
1294    let arr = value.as_array()?;
1295    if arr.is_empty() {
1296        return None;
1297    }
1298
1299    let func_name = arr.first()?.as_str()?;
1300
1301    match func_name {
1302        "Count" => Some(AggregateFunction::Count),
1303        "Sum" => {
1304            let field = arr.get(1)?.as_str()?;
1305            Some(AggregateFunction::Sum {
1306                field: field.to_string(),
1307            })
1308        }
1309        "Avg" => {
1310            let field = arr.get(1)?.as_str()?;
1311            Some(AggregateFunction::Avg {
1312                field: field.to_string(),
1313            })
1314        }
1315        "Min" => {
1316            let field = arr.get(1)?.as_str()?;
1317            Some(AggregateFunction::Min {
1318                field: field.to_string(),
1319            })
1320        }
1321        "Max" => {
1322            let field = arr.get(1)?.as_str()?;
1323            Some(AggregateFunction::Max {
1324                field: field.to_string(),
1325            })
1326        }
1327        _ => None,
1328    }
1329}
1330
1331/// Request for aggregation query (Turbopuffer-inspired)
1332#[derive(Debug, Deserialize)]
1333pub struct AggregationRequest {
1334    /// Named aggregations to compute
1335    /// Example: {"my_count": ["Count"], "total_score": ["Sum", "score"]}
1336    pub aggregate_by: std::collections::HashMap<String, AggregateFunctionInput>,
1337    /// Fields to group results by (optional)
1338    /// Example: ["category", "status"]
1339    #[serde(default)]
1340    pub group_by: Vec<String>,
1341    /// Filter to apply before aggregation
1342    #[serde(default)]
1343    pub filter: Option<FilterExpression>,
1344    /// Maximum number of groups to return (default: 100)
1345    #[serde(default = "default_agg_limit")]
1346    pub limit: usize,
1347}
1348
1349fn default_agg_limit() -> usize {
1350    100
1351}
1352
1353/// Response for aggregation query
1354#[derive(Debug, Serialize, Deserialize)]
1355pub struct AggregationResponse {
1356    /// Aggregation results (without grouping)
1357    #[serde(skip_serializing_if = "Option::is_none")]
1358    pub aggregations: Option<std::collections::HashMap<String, serde_json::Value>>,
1359    /// Grouped aggregation results (with group_by)
1360    #[serde(skip_serializing_if = "Option::is_none")]
1361    pub aggregation_groups: Option<Vec<AggregationGroup>>,
1362}
1363
1364/// Single group in aggregation results
1365#[derive(Debug, Serialize, Deserialize)]
1366pub struct AggregationGroup {
1367    /// Group key values (flattened into object)
1368    #[serde(flatten)]
1369    pub group_key: std::collections::HashMap<String, serde_json::Value>,
1370    /// Aggregation results for this group
1371    #[serde(flatten)]
1372    pub aggregations: std::collections::HashMap<String, serde_json::Value>,
1373}
1374
1375// =============================================================================
1376// TEXT-BASED API TYPES (Embedded Inference)
1377// =============================================================================
1378
1379/// A text document with metadata for text-based upsert
1380#[derive(Debug, Clone, Serialize, Deserialize)]
1381pub struct TextDocument {
1382    /// Unique identifier for this document
1383    pub id: VectorId,
1384    /// The text content to be embedded
1385    pub text: String,
1386    /// Optional metadata to store with the vector
1387    #[serde(skip_serializing_if = "Option::is_none")]
1388    pub metadata: Option<serde_json::Value>,
1389    /// TTL in seconds (optional)
1390    #[serde(skip_serializing_if = "Option::is_none")]
1391    pub ttl_seconds: Option<u64>,
1392}
1393
1394/// Request to upsert text documents (auto-embedded)
1395#[derive(Debug, Deserialize)]
1396pub struct TextUpsertRequest {
1397    /// Text documents to embed and store
1398    pub documents: Vec<TextDocument>,
1399    /// Embedding model to use (default: `minilm`).
1400    #[serde(default)]
1401    pub model: Option<EmbeddingModelType>,
1402}
1403
1404/// Response from text upsert operation
1405#[derive(Debug, Serialize, Deserialize)]
1406pub struct TextUpsertResponse {
1407    /// Number of documents successfully upserted
1408    pub upserted_count: usize,
1409    /// Number of tokens processed for embedding
1410    pub tokens_processed: usize,
1411    /// Embedding model used
1412    pub model: EmbeddingModelType,
1413    /// Time taken for embedding generation (ms)
1414    pub embedding_time_ms: u64,
1415}
1416
1417/// Request for text-based query (auto-embedded)
1418#[derive(Debug, Deserialize)]
1419pub struct TextQueryRequest {
1420    /// The query text to search for
1421    pub text: String,
1422    /// Number of results to return
1423    #[serde(default = "default_top_k")]
1424    pub top_k: usize,
1425    /// Optional filter to apply
1426    #[serde(default)]
1427    pub filter: Option<FilterExpression>,
1428    /// Whether to include vectors in response
1429    #[serde(default)]
1430    pub include_vectors: bool,
1431    /// Whether to include the original text in response (if stored in metadata)
1432    #[serde(default = "default_true")]
1433    pub include_text: bool,
1434    /// Embedding model to use (must match upsert model; default: `minilm`).
1435    #[serde(default)]
1436    pub model: Option<EmbeddingModelType>,
1437}
1438
1439/// Response from text-based query
1440#[derive(Debug, Serialize, Deserialize)]
1441pub struct TextQueryResponse {
1442    /// Search results with similarity scores
1443    pub results: Vec<TextSearchResult>,
1444    /// Embedding model used
1445    pub model: EmbeddingModelType,
1446    /// Time taken for embedding generation (ms)
1447    pub embedding_time_ms: u64,
1448    /// Time taken for search (ms)
1449    pub search_time_ms: u64,
1450}
1451
1452/// Single result from text search
1453#[derive(Debug, Serialize, Deserialize)]
1454pub struct TextSearchResult {
1455    /// Document ID
1456    pub id: VectorId,
1457    /// Similarity score (higher is better)
1458    pub score: f32,
1459    /// Original text (if include_text=true and stored in metadata)
1460    #[serde(skip_serializing_if = "Option::is_none")]
1461    pub text: Option<String>,
1462    /// Associated metadata
1463    #[serde(skip_serializing_if = "Option::is_none")]
1464    pub metadata: Option<serde_json::Value>,
1465    /// Vector values (if include_vectors=true)
1466    #[serde(skip_serializing_if = "Option::is_none")]
1467    pub vector: Option<Vec<f32>>,
1468}
1469
1470/// Batch text query request
1471#[derive(Debug, Deserialize)]
1472pub struct BatchTextQueryRequest {
1473    /// Multiple query texts
1474    pub queries: Vec<String>,
1475    /// Number of results per query
1476    #[serde(default = "default_top_k")]
1477    pub top_k: usize,
1478    /// Optional filter to apply to all queries
1479    #[serde(default)]
1480    pub filter: Option<FilterExpression>,
1481    /// Whether to include vectors in response
1482    #[serde(default)]
1483    pub include_vectors: bool,
1484    /// Embedding model to use (default: `minilm`).
1485    #[serde(default)]
1486    pub model: Option<EmbeddingModelType>,
1487}
1488
1489/// Response from batch text query
1490#[derive(Debug, Serialize, Deserialize)]
1491pub struct BatchTextQueryResponse {
1492    /// Results for each query
1493    pub results: Vec<Vec<TextSearchResult>>,
1494    /// Embedding model used
1495    pub model: EmbeddingModelType,
1496    /// Total time for embedding generation (ms)
1497    pub embedding_time_ms: u64,
1498    /// Total time for search (ms)
1499    pub search_time_ms: u64,
1500}
1501
1502/// Available embedding models.
1503///
1504/// Replaces the previous `model: String` field — callers now supply a
1505/// typed enum value, eliminating runtime string-mismatch bugs.
1506///
1507/// JSON serialisation uses lowercase identifiers:
1508/// `"bge-large"`, `"minilm"`, `"bge-small"`, `"e5-small"`.
1509#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1510pub enum EmbeddingModelType {
1511    /// BAAI/bge-large-en-v1.5 — highest quality, 1024 dimensions (default)
1512    #[default]
1513    #[serde(rename = "bge-large")]
1514    BgeLarge,
1515    /// all-MiniLM-L6-v2 — fast and memory-efficient, 384 dimensions
1516    #[serde(rename = "minilm")]
1517    MiniLM,
1518    /// BAAI/bge-small-en-v1.5 — balanced quality and speed, 384 dimensions
1519    #[serde(rename = "bge-small")]
1520    BgeSmall,
1521    /// intfloat/e5-small-v2 — quality-focused, 384 dimensions
1522    #[serde(rename = "e5-small")]
1523    E5Small,
1524}
1525
1526impl EmbeddingModelType {
1527    /// Embedding vector dimension for this model.
1528    pub fn dimension(&self) -> usize {
1529        match self {
1530            EmbeddingModelType::BgeLarge => 1024,
1531            EmbeddingModelType::MiniLM => 384,
1532            EmbeddingModelType::BgeSmall => 384,
1533            EmbeddingModelType::E5Small => 384,
1534        }
1535    }
1536}
1537
1538impl std::fmt::Display for EmbeddingModelType {
1539    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1540        match self {
1541            EmbeddingModelType::BgeLarge => write!(f, "bge-large"),
1542            EmbeddingModelType::MiniLM => write!(f, "minilm"),
1543            EmbeddingModelType::BgeSmall => write!(f, "bge-small"),
1544            EmbeddingModelType::E5Small => write!(f, "e5-small"),
1545        }
1546    }
1547}
1548
1549// ============================================================================
1550// Dakera Memory Types — AI Agent Memory Platform
1551// ============================================================================
1552
1553/// Type of memory stored by an agent
1554#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1555#[serde(rename_all = "snake_case")]
1556#[derive(Default)]
1557pub enum MemoryType {
1558    /// Personal experiences and events
1559    #[default]
1560    Episodic,
1561    /// Facts and general knowledge
1562    Semantic,
1563    /// How-to knowledge and skills
1564    Procedural,
1565    /// Short-term, temporary context
1566    Working,
1567}
1568
1569impl std::fmt::Display for MemoryType {
1570    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1571        match self {
1572            MemoryType::Episodic => write!(f, "episodic"),
1573            MemoryType::Semantic => write!(f, "semantic"),
1574            MemoryType::Procedural => write!(f, "procedural"),
1575            MemoryType::Working => write!(f, "working"),
1576        }
1577    }
1578}
1579
1580/// A memory stored by an AI agent
1581#[derive(Debug, Clone, Serialize, Deserialize)]
1582pub struct Memory {
1583    pub id: String,
1584    #[serde(default)]
1585    pub memory_type: MemoryType,
1586    pub content: String,
1587    pub agent_id: String,
1588    #[serde(skip_serializing_if = "Option::is_none")]
1589    pub session_id: Option<String>,
1590    #[serde(default = "default_importance")]
1591    pub importance: f32,
1592    #[serde(default)]
1593    pub tags: Vec<String>,
1594    #[serde(skip_serializing_if = "Option::is_none")]
1595    pub metadata: Option<serde_json::Value>,
1596    pub created_at: u64,
1597    pub last_accessed_at: u64,
1598    #[serde(default)]
1599    pub access_count: u32,
1600    #[serde(skip_serializing_if = "Option::is_none")]
1601    pub ttl_seconds: Option<u64>,
1602    #[serde(skip_serializing_if = "Option::is_none")]
1603    pub expires_at: Option<u64>,
1604}
1605
1606fn default_importance() -> f32 {
1607    0.5
1608}
1609
1610impl Memory {
1611    /// Create a new memory with current timestamps
1612    pub fn new(id: String, content: String, agent_id: String, memory_type: MemoryType) -> Self {
1613        let now = std::time::SystemTime::now()
1614            .duration_since(std::time::UNIX_EPOCH)
1615            .unwrap_or_default()
1616            .as_secs();
1617        Self {
1618            id,
1619            memory_type,
1620            content,
1621            agent_id,
1622            session_id: None,
1623            importance: 0.5,
1624            tags: Vec::new(),
1625            metadata: None,
1626            created_at: now,
1627            last_accessed_at: now,
1628            access_count: 0,
1629            ttl_seconds: None,
1630            expires_at: None,
1631        }
1632    }
1633
1634    /// Check if this memory has expired
1635    pub fn is_expired(&self) -> bool {
1636        if let Some(expires_at) = self.expires_at {
1637            let now = std::time::SystemTime::now()
1638                .duration_since(std::time::UNIX_EPOCH)
1639                .unwrap_or_default()
1640                .as_secs();
1641            now >= expires_at
1642        } else {
1643            false
1644        }
1645    }
1646
1647    /// Pack memory fields into metadata for Vector storage
1648    pub fn to_vector_metadata(&self) -> serde_json::Value {
1649        let mut meta = serde_json::Map::new();
1650        meta.insert("_dakera_type".to_string(), serde_json::json!("memory"));
1651        meta.insert(
1652            "memory_type".to_string(),
1653            serde_json::json!(self.memory_type),
1654        );
1655        meta.insert("content".to_string(), serde_json::json!(self.content));
1656        meta.insert("agent_id".to_string(), serde_json::json!(self.agent_id));
1657        if let Some(ref sid) = self.session_id {
1658            meta.insert("session_id".to_string(), serde_json::json!(sid));
1659        }
1660        meta.insert("importance".to_string(), serde_json::json!(self.importance));
1661        meta.insert("tags".to_string(), serde_json::json!(self.tags));
1662        meta.insert("created_at".to_string(), serde_json::json!(self.created_at));
1663        meta.insert(
1664            "last_accessed_at".to_string(),
1665            serde_json::json!(self.last_accessed_at),
1666        );
1667        meta.insert(
1668            "access_count".to_string(),
1669            serde_json::json!(self.access_count),
1670        );
1671        if let Some(ref ttl) = self.ttl_seconds {
1672            meta.insert("ttl_seconds".to_string(), serde_json::json!(ttl));
1673        }
1674        if let Some(ref expires) = self.expires_at {
1675            meta.insert("expires_at".to_string(), serde_json::json!(expires));
1676        }
1677        if let Some(ref user_meta) = self.metadata {
1678            meta.insert("user_metadata".to_string(), user_meta.clone());
1679        }
1680        serde_json::Value::Object(meta)
1681    }
1682
1683    /// Convert a Memory to a Vector (for storage layer)
1684    pub fn to_vector(&self, embedding: Vec<f32>) -> Vector {
1685        let mut v = Vector {
1686            id: self.id.clone(),
1687            values: embedding,
1688            metadata: Some(self.to_vector_metadata()),
1689            ttl_seconds: self.ttl_seconds,
1690            expires_at: self.expires_at,
1691        };
1692        v.apply_ttl();
1693        v
1694    }
1695
1696    /// Reconstruct a Memory from a Vector's metadata
1697    pub fn from_vector(vector: &Vector) -> Option<Self> {
1698        let meta = vector.metadata.as_ref()?.as_object()?;
1699        let entry_type = meta.get("_dakera_type")?.as_str()?;
1700        if entry_type != "memory" {
1701            return None;
1702        }
1703
1704        Some(Memory {
1705            id: vector.id.clone(),
1706            memory_type: serde_json::from_value(meta.get("memory_type")?.clone())
1707                .unwrap_or_default(),
1708            content: meta.get("content")?.as_str()?.to_string(),
1709            agent_id: meta.get("agent_id")?.as_str()?.to_string(),
1710            session_id: meta
1711                .get("session_id")
1712                .and_then(|v| v.as_str())
1713                .map(String::from),
1714            importance: meta
1715                .get("importance")
1716                .and_then(|v| v.as_f64())
1717                .unwrap_or(0.5) as f32,
1718            tags: meta
1719                .get("tags")
1720                .and_then(|v| serde_json::from_value(v.clone()).ok())
1721                .unwrap_or_default(),
1722            metadata: meta.get("user_metadata").cloned(),
1723            created_at: meta.get("created_at").and_then(|v| v.as_u64()).unwrap_or(0),
1724            last_accessed_at: meta
1725                .get("last_accessed_at")
1726                .and_then(|v| v.as_u64())
1727                .unwrap_or(0),
1728            access_count: meta
1729                .get("access_count")
1730                .and_then(|v| v.as_u64())
1731                .unwrap_or(0) as u32,
1732            ttl_seconds: vector.ttl_seconds,
1733            expires_at: vector.expires_at,
1734        })
1735    }
1736}
1737
1738/// An agent session
1739#[derive(Debug, Clone, Serialize, Deserialize)]
1740pub struct Session {
1741    pub id: String,
1742    pub agent_id: String,
1743    pub started_at: u64,
1744    #[serde(skip_serializing_if = "Option::is_none")]
1745    pub ended_at: Option<u64>,
1746    #[serde(skip_serializing_if = "Option::is_none")]
1747    pub summary: Option<String>,
1748    #[serde(skip_serializing_if = "Option::is_none")]
1749    pub metadata: Option<serde_json::Value>,
1750    /// Cached count of memories in this session (updated on store/forget)
1751    #[serde(default)]
1752    pub memory_count: usize,
1753}
1754
1755impl Session {
1756    pub fn new(id: String, agent_id: String) -> Self {
1757        let now = std::time::SystemTime::now()
1758            .duration_since(std::time::UNIX_EPOCH)
1759            .unwrap_or_default()
1760            .as_secs();
1761        Self {
1762            id,
1763            agent_id,
1764            started_at: now,
1765            ended_at: None,
1766            summary: None,
1767            metadata: None,
1768            memory_count: 0,
1769        }
1770    }
1771
1772    /// Pack session into metadata for Vector storage
1773    pub fn to_vector_metadata(&self) -> serde_json::Value {
1774        let mut meta = serde_json::Map::new();
1775        meta.insert("_dakera_type".to_string(), serde_json::json!("session"));
1776        meta.insert("agent_id".to_string(), serde_json::json!(self.agent_id));
1777        meta.insert("started_at".to_string(), serde_json::json!(self.started_at));
1778        if let Some(ref ended) = self.ended_at {
1779            meta.insert("ended_at".to_string(), serde_json::json!(ended));
1780        }
1781        if let Some(ref summary) = self.summary {
1782            meta.insert("summary".to_string(), serde_json::json!(summary));
1783        }
1784        if let Some(ref user_meta) = self.metadata {
1785            meta.insert("user_metadata".to_string(), user_meta.clone());
1786        }
1787        meta.insert(
1788            "memory_count".to_string(),
1789            serde_json::json!(self.memory_count),
1790        );
1791        serde_json::Value::Object(meta)
1792    }
1793
1794    /// Convert to a Vector for storage (use summary or agent_id as embedding source)
1795    pub fn to_vector(&self, embedding: Vec<f32>) -> Vector {
1796        Vector {
1797            id: self.id.clone(),
1798            values: embedding,
1799            metadata: Some(self.to_vector_metadata()),
1800            ttl_seconds: None,
1801            expires_at: None,
1802        }
1803    }
1804
1805    /// Reconstruct a Session from a Vector's metadata
1806    pub fn from_vector(vector: &Vector) -> Option<Self> {
1807        let meta = vector.metadata.as_ref()?.as_object()?;
1808        let entry_type = meta.get("_dakera_type")?.as_str()?;
1809        if entry_type != "session" {
1810            return None;
1811        }
1812
1813        Some(Session {
1814            id: vector.id.clone(),
1815            agent_id: meta.get("agent_id")?.as_str()?.to_string(),
1816            started_at: meta.get("started_at").and_then(|v| v.as_u64()).unwrap_or(0),
1817            ended_at: meta.get("ended_at").and_then(|v| v.as_u64()),
1818            summary: meta
1819                .get("summary")
1820                .and_then(|v| v.as_str())
1821                .map(String::from),
1822            metadata: meta.get("user_metadata").cloned(),
1823            memory_count: meta
1824                .get("memory_count")
1825                .and_then(|v| v.as_u64())
1826                .unwrap_or(0) as usize,
1827        })
1828    }
1829}
1830
1831/// Strategy for importance decay
1832#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1833#[serde(rename_all = "snake_case")]
1834#[derive(Default)]
1835pub enum DecayStrategy {
1836    #[default]
1837    Exponential,
1838    Linear,
1839    StepFunction,
1840    /// Power-law decay: I(t) = I₀ / (1 + k·t)^α — natural for episodic memories
1841    PowerLaw,
1842    /// Logarithmic decay: I(t) = I₀ · (1 − log₂(1 + t/h)) — slow for semantic knowledge
1843    Logarithmic,
1844    /// Flat (no decay) — for procedural/skill memories
1845    Flat,
1846}
1847
1848/// Configuration for importance decay
1849#[derive(Debug, Clone, Serialize, Deserialize)]
1850pub struct DecayConfig {
1851    #[serde(default)]
1852    pub strategy: DecayStrategy,
1853    #[serde(default = "default_half_life_hours")]
1854    pub half_life_hours: f64,
1855    #[serde(default = "default_min_importance")]
1856    pub min_importance: f32,
1857}
1858
1859fn default_half_life_hours() -> f64 {
1860    168.0 // 1 week
1861}
1862
1863fn default_min_importance() -> f32 {
1864    0.01
1865}
1866
1867impl Default for DecayConfig {
1868    fn default() -> Self {
1869        Self {
1870            strategy: DecayStrategy::default(),
1871            half_life_hours: default_half_life_hours(),
1872            min_importance: default_min_importance(),
1873        }
1874    }
1875}
1876
1877// ============================================================================
1878// Dakera Memory Request/Response Types
1879// ============================================================================
1880
1881/// Request to store a memory
1882#[derive(Debug, Deserialize)]
1883pub struct StoreMemoryRequest {
1884    pub content: String,
1885    pub agent_id: String,
1886    #[serde(default)]
1887    pub memory_type: MemoryType,
1888    #[serde(skip_serializing_if = "Option::is_none")]
1889    pub session_id: Option<String>,
1890    #[serde(default = "default_importance")]
1891    pub importance: f32,
1892    #[serde(default)]
1893    pub tags: Vec<String>,
1894    #[serde(skip_serializing_if = "Option::is_none")]
1895    pub metadata: Option<serde_json::Value>,
1896    #[serde(skip_serializing_if = "Option::is_none")]
1897    pub ttl_seconds: Option<u64>,
1898    /// Optional explicit expiry Unix timestamp (seconds).
1899    /// If provided, takes precedence over ttl_seconds.
1900    /// On expiry the memory is hard-deleted by the decay engine, bypassing
1901    /// importance scoring.
1902    #[serde(skip_serializing_if = "Option::is_none")]
1903    pub expires_at: Option<u64>,
1904    /// Optional custom ID (auto-generated if not provided)
1905    #[serde(skip_serializing_if = "Option::is_none")]
1906    pub id: Option<String>,
1907}
1908
1909/// Response from storing a memory
1910#[derive(Debug, Serialize)]
1911pub struct StoreMemoryResponse {
1912    pub memory: Memory,
1913    pub embedding_time_ms: u64,
1914}
1915
1916/// CE-12: Routing mode for smart query dispatch.
1917///
1918/// Controls which retrieval backend(s) are used when recalling or searching memories.
1919#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
1920#[serde(rename_all = "lowercase")]
1921pub enum RoutingMode {
1922    /// Automatically select the best backend based on query characteristics (default).
1923    #[default]
1924    Auto,
1925    /// Force pure vector-similarity search (always embeds the query).
1926    Vector,
1927    /// Force pure BM25 full-text search (no embedding inference).
1928    Bm25,
1929    /// Force hybrid search: combine vector + BM25 with adaptive weighting.
1930    Hybrid,
1931}
1932
1933/// CE-14: Fusion strategy for hybrid search results.
1934///
1935/// Selects how vector-similarity and BM25 scores are combined when
1936/// `routing=hybrid` (or auto-classified as hybrid).
1937#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
1938#[serde(rename_all = "lowercase")]
1939pub enum FusionStrategy {
1940    /// Reciprocal Rank Fusion — Cormack et al., SIGIR 2009.
1941    /// Used by all 90%+ LoCoMo benchmark systems (HyperMem, ByteRover, MemMachine).
1942    /// Formula: score(d) = Σ 1 / (k + rank_r(d)), k=60.
1943    #[default]
1944    Rrf,
1945    /// Linear weighted min-max normalization (legacy behavior).
1946    MinMax,
1947}
1948
1949/// Request to recall memories by semantic query
1950#[derive(Debug, Deserialize)]
1951pub struct RecallRequest {
1952    pub query: String,
1953    pub agent_id: String,
1954    #[serde(default = "default_top_k")]
1955    pub top_k: usize,
1956    #[serde(default)]
1957    pub memory_type: Option<MemoryType>,
1958    #[serde(default)]
1959    pub session_id: Option<String>,
1960    #[serde(default)]
1961    pub tags: Option<Vec<String>>,
1962    #[serde(default)]
1963    pub min_importance: Option<f32>,
1964    /// Include importance-weighted re-ranking (default: true)
1965    #[serde(default = "default_true")]
1966    pub importance_weighted: bool,
1967    /// COG-2: traverse KG depth-1 from recalled memories and include associatively linked memories
1968    #[serde(default)]
1969    pub include_associated: bool,
1970    /// COG-2: max number of associated memories to return (default: 10, max: 10)
1971    #[serde(default)]
1972    pub associated_memories_cap: Option<usize>,
1973    /// CE-7: only include memories created at or after this ISO-8601 timestamp (e.g. "2024-01-01T00:00:00Z")
1974    #[serde(default)]
1975    pub since: Option<String>,
1976    /// CE-7: only include memories created at or before this ISO-8601 timestamp (e.g. "2024-12-31T23:59:59Z")
1977    #[serde(default)]
1978    pub until: Option<String>,
1979    /// KG-3: KG traversal depth for associative recall (1–3, default 1).
1980    /// Requires `include_associated: true`. Depth 1 = direct neighbours only (COG-2 behaviour).
1981    #[serde(default)]
1982    pub associated_memories_depth: Option<u8>,
1983    /// KG-3: minimum edge weight to traverse (0.0–1.0, default 0.0 = all edges).
1984    /// Requires `include_associated: true`.
1985    #[serde(default)]
1986    pub associated_memories_min_weight: Option<f32>,
1987    /// CE-12: retrieval routing mode.
1988    /// `auto` (default) classifies the query heuristically; `vector`/`bm25`/`hybrid`
1989    /// force a specific backend.
1990    #[serde(default)]
1991    pub routing: RoutingMode,
1992    /// CE-13: apply cross-encoder reranking after ANN candidate retrieval.
1993    /// Fetches `top_k * 3` candidates and rescores with `bge-reranker-base`.
1994    /// Default: `true` (improves recall precision significantly).
1995    #[serde(default = "default_true")]
1996    pub rerank: bool,
1997    /// CE-14: fusion strategy when routing=Hybrid.
1998    /// `rrf` (default) uses Reciprocal Rank Fusion; `minmax` uses weighted min-max normalization.
1999    #[serde(default)]
2000    pub fusion: FusionStrategy,
2001    /// v0.11.0 Phase 2: after main recall, fetch session-adjacent memories within ±5 min
2002    /// of each top result as context enrichment. Default: true.
2003    /// Set to false to disable neighborhood expansion (reduces latency, lower recall).
2004    #[serde(default = "default_true")]
2005    pub neighborhood: bool,
2006}
2007
2008/// Single recall result
2009#[derive(Debug, Serialize, Deserialize)]
2010pub struct RecallResult {
2011    pub memory: Memory,
2012    pub score: f32,
2013    /// Score after importance-weighted re-ranking
2014    #[serde(skip_serializing_if = "Option::is_none")]
2015    pub weighted_score: Option<f32>,
2016    /// Always-on multi-signal smart score (vector + importance + recency + frequency)
2017    #[serde(skip_serializing_if = "Option::is_none")]
2018    pub smart_score: Option<f32>,
2019    /// KG-3: traversal depth at which this memory was found (only set on associated_memories entries).
2020    /// 1 = direct neighbour of a primary result, 2 = two hops, 3 = three hops.
2021    #[serde(skip_serializing_if = "Option::is_none")]
2022    pub depth: Option<u8>,
2023}
2024
2025/// Response from recall
2026#[derive(Debug, Serialize)]
2027pub struct RecallResponse {
2028    pub memories: Vec<RecallResult>,
2029    pub query_embedding_time_ms: u64,
2030    pub search_time_ms: u64,
2031    /// COG-2: memories linked to recalled memories via KG depth-1 traversal.
2032    /// Only populated when `include_associated: true` in the request.
2033    #[serde(skip_serializing_if = "Option::is_none")]
2034    pub associated_memories: Option<Vec<RecallResult>>,
2035}
2036
2037/// Request to forget (delete) memories
2038#[derive(Debug, Deserialize)]
2039pub struct ForgetRequest {
2040    pub agent_id: String,
2041    #[serde(default)]
2042    pub memory_ids: Option<Vec<String>>,
2043    #[serde(default)]
2044    pub memory_type: Option<MemoryType>,
2045    #[serde(default)]
2046    pub session_id: Option<String>,
2047    #[serde(default)]
2048    pub tags: Option<Vec<String>>,
2049    /// Delete memories below this importance threshold
2050    #[serde(default)]
2051    pub below_importance: Option<f32>,
2052}
2053
2054/// Response from forget
2055#[derive(Debug, Serialize)]
2056pub struct ForgetResponse {
2057    pub deleted_count: usize,
2058}
2059
2060/// Request to update a memory
2061#[derive(Debug, Deserialize)]
2062pub struct UpdateMemoryRequest {
2063    #[serde(default)]
2064    pub content: Option<String>,
2065    #[serde(default)]
2066    pub importance: Option<f32>,
2067    #[serde(default)]
2068    pub tags: Option<Vec<String>>,
2069    #[serde(default)]
2070    pub metadata: Option<serde_json::Value>,
2071    #[serde(default)]
2072    pub memory_type: Option<MemoryType>,
2073}
2074
2075/// Request to update importance of a memory
2076#[derive(Debug, Deserialize)]
2077pub struct UpdateImportanceRequest {
2078    pub memory_id: String,
2079    pub importance: f32,
2080    pub agent_id: String,
2081}
2082
2083/// Request to consolidate related memories
2084#[derive(Debug, Deserialize)]
2085pub struct ConsolidateRequest {
2086    pub agent_id: String,
2087    /// Memory IDs to consolidate (if empty, auto-detect similar memories)
2088    #[serde(default)]
2089    pub memory_ids: Option<Vec<String>>,
2090    /// Similarity threshold for auto-detection (default: 0.85)
2091    #[serde(default = "default_consolidation_threshold")]
2092    pub threshold: f32,
2093    /// Type for the consolidated memory
2094    #[serde(default)]
2095    pub target_type: Option<MemoryType>,
2096}
2097
2098fn default_consolidation_threshold() -> f32 {
2099    0.85
2100}
2101
2102/// Response from consolidation
2103#[derive(Debug, Serialize)]
2104pub struct ConsolidateResponse {
2105    pub consolidated_memory: Memory,
2106    pub source_memory_ids: Vec<String>,
2107    pub memories_removed: usize,
2108}
2109
2110/// Feedback signal for active learning
2111#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2112#[serde(rename_all = "lowercase")]
2113pub enum FeedbackSignal {
2114    /// Boost importance (×1.15, capped at 1.0). INT-1 canonical name.
2115    Upvote,
2116    /// Penalise importance (×0.85, floor 0.0). INT-1 canonical name.
2117    Downvote,
2118    /// Mark as irrelevant — sets `decay_flag=true`, no immediate importance change.
2119    Flag,
2120    /// Backward-compatible alias for `upvote`.
2121    Positive,
2122    /// Backward-compatible alias for `downvote`.
2123    Negative,
2124}
2125
2126/// One recorded feedback event stored in memory metadata (feedback_history).
2127#[derive(Debug, Clone, Serialize, Deserialize)]
2128pub struct FeedbackHistoryEntry {
2129    pub signal: FeedbackSignal,
2130    pub timestamp: u64,
2131    pub old_importance: f32,
2132    pub new_importance: f32,
2133}
2134
2135/// Request to provide feedback on a recalled memory (legacy — body contains memory_id)
2136#[derive(Debug, Deserialize)]
2137pub struct FeedbackRequest {
2138    pub agent_id: String,
2139    pub memory_id: String,
2140    pub signal: FeedbackSignal,
2141}
2142
2143/// Request for `POST /v1/memories/{id}/feedback` (INT-1 — memory_id in path)
2144#[derive(Debug, Deserialize)]
2145pub struct MemoryFeedbackRequest {
2146    pub agent_id: String,
2147    pub signal: FeedbackSignal,
2148}
2149
2150/// Response from feedback
2151#[derive(Debug, Serialize)]
2152pub struct FeedbackResponse {
2153    pub memory_id: String,
2154    pub new_importance: f32,
2155    pub signal: FeedbackSignal,
2156}
2157
2158/// Response from `GET /v1/memories/{id}/feedback`
2159#[derive(Debug, Serialize)]
2160pub struct FeedbackHistoryResponse {
2161    pub memory_id: String,
2162    pub entries: Vec<FeedbackHistoryEntry>,
2163}
2164
2165/// Response from `GET /v1/agents/{id}/feedback/summary`
2166#[derive(Debug, Serialize)]
2167pub struct AgentFeedbackSummary {
2168    pub agent_id: String,
2169    pub upvotes: u64,
2170    pub downvotes: u64,
2171    pub flags: u64,
2172    pub total_feedback: u64,
2173    /// Weighted-average importance across all non-expired memories (0.0–1.0).
2174    pub health_score: f32,
2175}
2176
2177/// Request for `PATCH /v1/memories/{id}/importance` (INT-1 — memory_id in path)
2178#[derive(Debug, Deserialize)]
2179pub struct MemoryImportancePatchRequest {
2180    pub agent_id: String,
2181    pub importance: f32,
2182}
2183
2184/// Query params for `GET /v1/feedback/health`
2185#[derive(Debug, Deserialize)]
2186pub struct FeedbackHealthQuery {
2187    pub agent_id: String,
2188}
2189
2190/// Response from `GET /v1/feedback/health`
2191#[derive(Debug, Serialize)]
2192pub struct FeedbackHealthResponse {
2193    pub agent_id: String,
2194    /// Mean importance of all non-expired memories (0.0–1.0). Higher = healthier.
2195    pub health_score: f32,
2196    pub memory_count: usize,
2197    pub avg_importance: f32,
2198}
2199
2200/// Request for advanced memory search
2201#[derive(Debug, Deserialize)]
2202pub struct SearchMemoriesRequest {
2203    pub agent_id: String,
2204    #[serde(default)]
2205    pub query: Option<String>,
2206    #[serde(default)]
2207    pub memory_type: Option<MemoryType>,
2208    #[serde(default)]
2209    pub session_id: Option<String>,
2210    #[serde(default)]
2211    pub tags: Option<Vec<String>>,
2212    #[serde(default)]
2213    pub min_importance: Option<f32>,
2214    #[serde(default)]
2215    pub max_importance: Option<f32>,
2216    #[serde(default)]
2217    pub created_after: Option<u64>,
2218    #[serde(default)]
2219    pub created_before: Option<u64>,
2220    #[serde(default = "default_top_k")]
2221    pub top_k: usize,
2222    #[serde(default)]
2223    pub sort_by: Option<MemorySortField>,
2224    /// CE-12: retrieval routing mode (auto-detected when not specified).
2225    #[serde(default)]
2226    pub routing: RoutingMode,
2227    /// CE-13: apply cross-encoder reranking on vector/hybrid query results.
2228    /// Default: `false` (search is typically used for browsing, not precision recall).
2229    #[serde(default)]
2230    pub rerank: bool,
2231}
2232
2233/// Fields to sort memories by
2234#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
2235#[serde(rename_all = "snake_case")]
2236pub enum MemorySortField {
2237    CreatedAt,
2238    LastAccessedAt,
2239    Importance,
2240    AccessCount,
2241}
2242
2243/// Response from memory search
2244#[derive(Debug, Serialize)]
2245pub struct SearchMemoriesResponse {
2246    pub memories: Vec<RecallResult>,
2247    pub total_count: usize,
2248}
2249
2250// ============================================================================
2251// Dakera Session Request/Response Types
2252// ============================================================================
2253
2254/// Request to start a session
2255#[derive(Debug, Deserialize)]
2256pub struct SessionStartRequest {
2257    pub agent_id: String,
2258    #[serde(skip_serializing_if = "Option::is_none")]
2259    pub metadata: Option<serde_json::Value>,
2260    /// Optional custom session ID
2261    #[serde(skip_serializing_if = "Option::is_none")]
2262    pub id: Option<String>,
2263}
2264
2265/// Response from starting a session
2266#[derive(Debug, Serialize)]
2267pub struct SessionStartResponse {
2268    pub session: Session,
2269}
2270
2271/// Request to end a session
2272#[derive(Debug, Deserialize)]
2273pub struct SessionEndRequest {
2274    #[serde(default)]
2275    pub summary: Option<String>,
2276    /// Auto-generate summary from session memories
2277    #[serde(default)]
2278    pub auto_summarize: bool,
2279}
2280
2281/// Response from ending a session
2282#[derive(Debug, Serialize)]
2283pub struct SessionEndResponse {
2284    pub session: Session,
2285    pub memory_count: usize,
2286}
2287
2288/// Response listing sessions
2289#[derive(Debug, Serialize)]
2290pub struct ListSessionsResponse {
2291    pub sessions: Vec<Session>,
2292    pub total: usize,
2293}
2294
2295/// Response for session memories
2296#[derive(Debug, Serialize)]
2297pub struct SessionMemoriesResponse {
2298    pub session: Session,
2299    pub memories: Vec<Memory>,
2300    /// Total number of memories in this session (before pagination)
2301    #[serde(skip_serializing_if = "Option::is_none")]
2302    pub total: Option<usize>,
2303}
2304
2305// ============================================================================
2306// Dakera Agent & Knowledge Types
2307// ============================================================================
2308
2309/// Lightweight agent summary for batch listing (uses count() not get_all)
2310#[derive(Debug, Serialize, Deserialize, Clone)]
2311pub struct AgentSummary {
2312    pub agent_id: String,
2313    pub memory_count: usize,
2314    pub session_count: usize,
2315    pub active_sessions: usize,
2316}
2317
2318/// Agent memory statistics
2319#[derive(Debug, Serialize)]
2320pub struct AgentStats {
2321    pub agent_id: String,
2322    pub total_memories: usize,
2323    pub memories_by_type: std::collections::HashMap<String, usize>,
2324    pub total_sessions: usize,
2325    pub active_sessions: usize,
2326    pub avg_importance: f32,
2327    pub oldest_memory_at: Option<u64>,
2328    pub newest_memory_at: Option<u64>,
2329}
2330
2331/// Response from `GET /v1/agents/{agent_id}/wake-up` (DAK-1690).
2332///
2333/// Returns the highest-scored memories for an agent using a pure metadata
2334/// sort (`importance × recency_weight`). No embedding inference is performed,
2335/// making this suitable for fast agent startup context loading.
2336#[derive(Debug, Serialize)]
2337pub struct WakeUpResponse {
2338    pub agent_id: String,
2339    /// Top-N memories sorted by `importance × recency_weight` descending.
2340    pub memories: Vec<Memory>,
2341    /// Total memories available before the top_n cap was applied.
2342    pub total_available: usize,
2343}
2344
2345/// Request for knowledge graph traversal
2346#[derive(Debug, Deserialize)]
2347pub struct KnowledgeGraphRequest {
2348    pub agent_id: String,
2349    pub memory_id: String,
2350    #[serde(default = "default_graph_depth")]
2351    pub depth: usize,
2352    #[serde(default = "default_graph_min_similarity")]
2353    pub min_similarity: f32,
2354}
2355
2356fn default_graph_depth() -> usize {
2357    2
2358}
2359
2360fn default_graph_min_similarity() -> f32 {
2361    0.7
2362}
2363
2364/// Knowledge graph node
2365#[derive(Debug, Serialize)]
2366pub struct KnowledgeGraphNode {
2367    pub memory: Memory,
2368    pub similarity: f32,
2369    pub related: Vec<KnowledgeGraphEdge>,
2370}
2371
2372/// Knowledge graph edge
2373#[derive(Debug, Serialize)]
2374pub struct KnowledgeGraphEdge {
2375    pub memory_id: String,
2376    pub similarity: f32,
2377    pub shared_tags: Vec<String>,
2378}
2379
2380/// Response from knowledge graph query
2381#[derive(Debug, Serialize)]
2382pub struct KnowledgeGraphResponse {
2383    pub root: KnowledgeGraphNode,
2384    pub total_nodes: usize,
2385}
2386
2387// ============================================================================
2388// Full Knowledge Graph Types (Global Network Topology)
2389// ============================================================================
2390
2391fn default_full_graph_max_nodes() -> usize {
2392    200
2393}
2394
2395fn default_full_graph_min_similarity() -> f32 {
2396    0.50
2397}
2398
2399fn default_full_graph_cluster_threshold() -> f32 {
2400    0.60
2401}
2402
2403fn default_full_graph_max_edges_per_node() -> usize {
2404    8
2405}
2406
2407/// Request for full knowledge graph (all memories, pairwise similarity)
2408#[derive(Debug, Deserialize)]
2409pub struct FullKnowledgeGraphRequest {
2410    pub agent_id: String,
2411    #[serde(default = "default_full_graph_max_nodes")]
2412    pub max_nodes: usize,
2413    #[serde(default = "default_full_graph_min_similarity")]
2414    pub min_similarity: f32,
2415    #[serde(default = "default_full_graph_cluster_threshold")]
2416    pub cluster_threshold: f32,
2417    #[serde(default = "default_full_graph_max_edges_per_node")]
2418    pub max_edges_per_node: usize,
2419}
2420
2421/// A node in the full knowledge graph
2422#[derive(Debug, Serialize)]
2423pub struct FullGraphNode {
2424    pub id: String,
2425    pub content: String,
2426    pub memory_type: String,
2427    pub importance: f32,
2428    pub tags: Vec<String>,
2429    pub created_at: Option<String>,
2430    pub cluster_id: usize,
2431    pub centrality: f32,
2432}
2433
2434/// An edge in the full knowledge graph
2435#[derive(Debug, Serialize)]
2436pub struct FullGraphEdge {
2437    pub source: String,
2438    pub target: String,
2439    pub similarity: f32,
2440    pub shared_tags: Vec<String>,
2441}
2442
2443/// A cluster of related memories
2444#[derive(Debug, Serialize)]
2445pub struct GraphCluster {
2446    pub id: usize,
2447    pub node_count: usize,
2448    pub top_tags: Vec<String>,
2449    pub avg_importance: f32,
2450}
2451
2452/// Statistics about the full knowledge graph
2453#[derive(Debug, Serialize)]
2454pub struct GraphStats {
2455    pub total_memories: usize,
2456    pub included_memories: usize,
2457    pub total_edges: usize,
2458    pub cluster_count: usize,
2459    pub density: f32,
2460    pub hub_memory_id: Option<String>,
2461}
2462
2463/// Response from full knowledge graph query
2464#[derive(Debug, Serialize)]
2465pub struct FullKnowledgeGraphResponse {
2466    pub nodes: Vec<FullGraphNode>,
2467    pub edges: Vec<FullGraphEdge>,
2468    pub clusters: Vec<GraphCluster>,
2469    pub stats: GraphStats,
2470}
2471
2472/// Request to summarize memories
2473#[derive(Debug, Deserialize)]
2474pub struct SummarizeRequest {
2475    pub agent_id: String,
2476    pub memory_ids: Vec<String>,
2477    #[serde(default)]
2478    pub target_type: Option<MemoryType>,
2479}
2480
2481/// Response from summarization
2482#[derive(Debug, Serialize)]
2483pub struct SummarizeResponse {
2484    pub summary_memory: Memory,
2485    pub source_count: usize,
2486}
2487
2488/// Request to deduplicate memories
2489#[derive(Debug, Deserialize)]
2490pub struct DeduplicateRequest {
2491    pub agent_id: String,
2492    #[serde(default = "default_dedup_threshold")]
2493    pub threshold: f32,
2494    #[serde(default)]
2495    pub memory_type: Option<MemoryType>,
2496    /// Dry run — report duplicates without merging
2497    #[serde(default)]
2498    pub dry_run: bool,
2499}
2500
2501fn default_dedup_threshold() -> f32 {
2502    0.92
2503}
2504
2505/// A group of duplicate memories
2506#[derive(Debug, Serialize)]
2507pub struct DuplicateGroup {
2508    pub canonical_id: String,
2509    pub duplicate_ids: Vec<String>,
2510    pub avg_similarity: f32,
2511}
2512
2513/// Response from deduplication
2514#[derive(Debug, Serialize)]
2515pub struct DeduplicateResponse {
2516    pub groups: Vec<DuplicateGroup>,
2517    pub duplicates_found: usize,
2518    pub duplicates_merged: usize,
2519}
2520
2521// ============================================================================
2522// Cross-Agent Memory Network Types (DASH-A)
2523// ============================================================================
2524
2525fn default_cross_agent_min_similarity() -> f32 {
2526    0.3
2527}
2528
2529fn default_cross_agent_max_nodes_per_agent() -> usize {
2530    50
2531}
2532
2533fn default_cross_agent_max_cross_edges() -> usize {
2534    200
2535}
2536
2537/// Request for cross-agent memory network graph
2538#[derive(Debug, Deserialize)]
2539pub struct CrossAgentNetworkRequest {
2540    /// Specific agent IDs to include (None = all agents)
2541    #[serde(default)]
2542    pub agent_ids: Option<Vec<String>>,
2543    /// Minimum cosine similarity for a cross-agent edge (default 0.3)
2544    #[serde(default = "default_cross_agent_min_similarity")]
2545    pub min_similarity: f32,
2546    /// Maximum memories per agent to include (top N by importance, default 50)
2547    #[serde(default = "default_cross_agent_max_nodes_per_agent")]
2548    pub max_nodes_per_agent: usize,
2549    /// Minimum importance score for a memory to be included (default 0.0)
2550    #[serde(default)]
2551    pub min_importance: f32,
2552    /// Maximum cross-agent edges to return (default 200)
2553    #[serde(default = "default_cross_agent_max_cross_edges")]
2554    pub max_cross_edges: usize,
2555}
2556
2557/// Summary info for an agent in the cross-agent network
2558#[derive(Debug, Serialize)]
2559pub struct AgentNetworkInfo {
2560    pub agent_id: String,
2561    pub memory_count: usize,
2562    pub avg_importance: f32,
2563}
2564
2565/// A memory node in the cross-agent network (includes agent_id)
2566#[derive(Debug, Serialize)]
2567pub struct AgentNetworkNode {
2568    pub id: String,
2569    pub agent_id: String,
2570    pub content: String,
2571    pub importance: f32,
2572    pub tags: Vec<String>,
2573    pub memory_type: String,
2574    pub created_at: u64,
2575}
2576
2577/// An edge between memories from two different agents
2578#[derive(Debug, Serialize)]
2579pub struct AgentNetworkEdge {
2580    pub source: String,
2581    pub target: String,
2582    pub source_agent: String,
2583    pub target_agent: String,
2584    pub similarity: f32,
2585}
2586
2587/// Statistics for the cross-agent network
2588#[derive(Debug, Serialize)]
2589pub struct AgentNetworkStats {
2590    pub total_agents: usize,
2591    pub total_nodes: usize,
2592    pub total_cross_edges: usize,
2593    pub density: f32,
2594}
2595
2596/// Response from cross-agent network query
2597#[derive(Debug, Serialize)]
2598pub struct CrossAgentNetworkResponse {
2599    pub node_count: usize,
2600    pub agents: Vec<AgentNetworkInfo>,
2601    pub nodes: Vec<AgentNetworkNode>,
2602    pub edges: Vec<AgentNetworkEdge>,
2603    pub stats: AgentNetworkStats,
2604}
2605
2606// ---------------------------------------------------------------------------
2607// CE-2: Batch recall / forget types
2608// ---------------------------------------------------------------------------
2609
2610/// Filter predicates for batch memory operations.
2611///
2612/// At least one field must be set for forget operations (safety guard).
2613#[derive(Debug, Deserialize, Default)]
2614pub struct BatchMemoryFilter {
2615    /// Restrict to memories that carry **all** listed tags.
2616    #[serde(default)]
2617    pub tags: Option<Vec<String>>,
2618    /// Minimum importance (inclusive).
2619    #[serde(default)]
2620    pub min_importance: Option<f32>,
2621    /// Maximum importance (inclusive).
2622    #[serde(default)]
2623    pub max_importance: Option<f32>,
2624    /// Only memories created at or after this Unix timestamp (seconds).
2625    #[serde(default)]
2626    pub created_after: Option<u64>,
2627    /// Only memories created before or at this Unix timestamp (seconds).
2628    #[serde(default)]
2629    pub created_before: Option<u64>,
2630    /// Restrict to a specific memory type.
2631    #[serde(default)]
2632    pub memory_type: Option<MemoryType>,
2633    /// Restrict to memories from a specific session.
2634    #[serde(default)]
2635    pub session_id: Option<String>,
2636}
2637
2638impl BatchMemoryFilter {
2639    /// Returns `true` if the filter has at least one constraint set.
2640    pub fn has_any(&self) -> bool {
2641        self.tags.is_some()
2642            || self.min_importance.is_some()
2643            || self.max_importance.is_some()
2644            || self.created_after.is_some()
2645            || self.created_before.is_some()
2646            || self.memory_type.is_some()
2647            || self.session_id.is_some()
2648    }
2649
2650    /// Returns `true` if the given memory matches all active filter predicates.
2651    pub fn matches(&self, memory: &Memory) -> bool {
2652        if let Some(ref tags) = self.tags {
2653            if !tags.is_empty() && !tags.iter().all(|t| memory.tags.contains(t)) {
2654                return false;
2655            }
2656        }
2657        if let Some(min) = self.min_importance {
2658            if memory.importance < min {
2659                return false;
2660            }
2661        }
2662        if let Some(max) = self.max_importance {
2663            if memory.importance > max {
2664                return false;
2665            }
2666        }
2667        if let Some(after) = self.created_after {
2668            if memory.created_at < after {
2669                return false;
2670            }
2671        }
2672        if let Some(before) = self.created_before {
2673            if memory.created_at > before {
2674                return false;
2675            }
2676        }
2677        if let Some(ref mt) = self.memory_type {
2678            if memory.memory_type != *mt {
2679                return false;
2680            }
2681        }
2682        if let Some(ref sid) = self.session_id {
2683            if memory.session_id.as_ref() != Some(sid) {
2684                return false;
2685            }
2686        }
2687        true
2688    }
2689}
2690
2691/// Request for `POST /v1/memories/recall/batch`
2692#[derive(Debug, Deserialize)]
2693pub struct BatchRecallRequest {
2694    /// Agent whose memory namespace to search.
2695    pub agent_id: String,
2696    /// Filter predicates to apply.
2697    #[serde(default)]
2698    pub filter: BatchMemoryFilter,
2699    /// Maximum number of results to return (default: 100).
2700    #[serde(default = "default_batch_limit")]
2701    pub limit: usize,
2702}
2703
2704fn default_batch_limit() -> usize {
2705    100
2706}
2707
2708/// Response from `POST /v1/memories/recall/batch`
2709#[derive(Debug, Serialize)]
2710pub struct BatchRecallResponse {
2711    pub memories: Vec<Memory>,
2712    pub total: usize,
2713    pub filtered: usize,
2714}
2715
2716/// Request for `DELETE /v1/memories/forget/batch`
2717#[derive(Debug, Deserialize)]
2718pub struct BatchForgetRequest {
2719    /// Agent whose memory namespace to purge from.
2720    pub agent_id: String,
2721    /// Filter predicates — **at least one must be set** (safety guard).
2722    pub filter: BatchMemoryFilter,
2723}
2724
2725/// Response from `DELETE /v1/memories/forget/batch`
2726#[derive(Debug, Serialize)]
2727pub struct BatchForgetResponse {
2728    pub deleted_count: usize,
2729}
2730
2731// ─────────────────────────────────────────────────────────────────────────────
2732// CE-4 — Entity extraction types
2733// ─────────────────────────────────────────────────────────────────────────────
2734
2735/// Request to update entity extraction config for a namespace.
2736/// `PATCH /v1/namespaces/{namespace}/config`
2737#[derive(Debug, Deserialize)]
2738pub struct NamespaceEntityConfigRequest {
2739    /// Enable or disable entity extraction for this namespace.
2740    pub extract_entities: bool,
2741    /// Entity types to extract via GLiNER (e.g. ["person","org","location"]).
2742    /// If empty and extract_entities=true, only the rule-based pre-pass runs.
2743    #[serde(default)]
2744    pub entity_types: Vec<String>,
2745}
2746
2747/// Response from `PATCH /v1/namespaces/{namespace}/config`
2748#[derive(Debug, Serialize, Deserialize)]
2749pub struct NamespaceEntityConfigResponse {
2750    pub namespace: String,
2751    pub extract_entities: bool,
2752    pub entity_types: Vec<String>,
2753}
2754
2755/// Request to extract entities from content without storing.
2756/// `POST /v1/memories/extract`
2757#[derive(Debug, Deserialize)]
2758pub struct ExtractEntitiesRequest {
2759    /// Text content to extract entities from.
2760    pub content: String,
2761    /// Entity types for GLiNER inference (optional).
2762    /// If omitted, only the rule-based pre-pass runs.
2763    #[serde(default)]
2764    pub entity_types: Vec<String>,
2765}
2766
2767/// A single extracted entity (shared with inference crate — mirrored here for API types).
2768#[derive(Debug, Clone, Serialize, Deserialize)]
2769pub struct EntityResult {
2770    pub entity_type: String,
2771    pub value: String,
2772    pub score: f32,
2773    pub start: usize,
2774    pub end: usize,
2775    /// Canonical tag form: `entity:<type>:<value>`
2776    pub tag: String,
2777}
2778
2779/// Response from `POST /v1/memories/extract` and `GET /v1/memories/{id}/entities`
2780#[derive(Debug, Serialize)]
2781pub struct ExtractEntitiesResponse {
2782    pub entities: Vec<EntityResult>,
2783    pub count: usize,
2784}
2785
2786// ============================================================================
2787// CE-5: Memory Knowledge Graph — request / response types
2788// ============================================================================
2789
2790/// GET /v1/memories/:id/graph
2791#[derive(Debug, Deserialize)]
2792pub struct GraphTraverseQuery {
2793    /// BFS depth limit (default 3, max 5).
2794    #[serde(default = "default_ce5_graph_depth")]
2795    pub depth: u32,
2796}
2797
2798fn default_ce5_graph_depth() -> u32 {
2799    3
2800}
2801
2802/// GET /v1/memories/:id/path
2803#[derive(Debug, Deserialize)]
2804pub struct GraphPathQuery {
2805    /// Target memory ID.
2806    pub to: String,
2807}
2808
2809/// POST /v1/memories/:id/links — create an explicit edge
2810#[derive(Debug, Deserialize)]
2811pub struct MemoryLinkRequest {
2812    /// The other memory ID to link to.
2813    pub target_id: String,
2814    /// Optional human-readable label (stored as `linked_by` edge).
2815    #[serde(skip_serializing_if = "Option::is_none")]
2816    pub label: Option<String>,
2817    /// Agent ID (for authorization).
2818    pub agent_id: String,
2819}
2820
2821/// Response from graph traversal.
2822#[derive(Debug, Serialize)]
2823pub struct GraphTraverseResponse {
2824    pub root_id: String,
2825    pub depth: u32,
2826    pub node_count: usize,
2827    pub nodes: Vec<GraphNodeResponse>,
2828}
2829
2830/// A single node in a graph traversal response.
2831#[derive(Debug, Serialize)]
2832pub struct GraphNodeResponse {
2833    pub memory_id: String,
2834    pub depth: u32,
2835    pub edges: Vec<GraphEdgeResponse>,
2836}
2837
2838/// A single edge in a graph response.
2839#[derive(Debug, Serialize)]
2840pub struct GraphEdgeResponse {
2841    pub from_id: String,
2842    pub to_id: String,
2843    pub edge_type: String,
2844    pub weight: f32,
2845    pub created_at: u64,
2846}
2847
2848/// Response from shortest-path query.
2849#[derive(Debug, Serialize)]
2850pub struct GraphPathResponse {
2851    pub from_id: String,
2852    pub to_id: String,
2853    /// Ordered list of memory IDs along the shortest path (inclusive).
2854    pub path: Vec<String>,
2855    pub hop_count: usize,
2856}
2857
2858/// Response from explicit link creation.
2859#[derive(Debug, Serialize)]
2860pub struct MemoryLinkResponse {
2861    pub from_id: String,
2862    pub to_id: String,
2863    pub edge_type: String,
2864}
2865
2866/// Response from agent graph export.
2867#[derive(Debug, Serialize)]
2868pub struct GraphExportResponse {
2869    pub agent_id: String,
2870    pub namespace: String,
2871    pub node_count: usize,
2872    pub edge_count: usize,
2873    pub edges: Vec<GraphEdgeResponse>,
2874}
2875
2876// ============================================================================
2877// KG-2: Graph Query & Export — request / response types
2878// ============================================================================
2879
2880/// GET /v1/knowledge/query — JSON DSL for graph filtering/traversal
2881#[derive(Debug, Deserialize)]
2882pub struct KgQueryParams {
2883    /// Agent ID whose graph to query (required).
2884    pub agent_id: String,
2885    /// Optional root memory ID — if set, performs BFS from this node first.
2886    #[serde(default)]
2887    pub root_id: Option<String>,
2888    /// Filter edges by type (comma-separated, e.g. "related_to,shares_entity").
2889    #[serde(default)]
2890    pub edge_type: Option<String>,
2891    /// Minimum edge weight (0.0–1.0).
2892    #[serde(default)]
2893    pub min_weight: Option<f32>,
2894    /// BFS depth when root_id is set (1–5, default 3).
2895    #[serde(default = "default_kg_depth")]
2896    pub max_depth: u32,
2897    /// Maximum number of edges to return (default 100, max 1000).
2898    #[serde(default = "default_kg_limit")]
2899    pub limit: usize,
2900}
2901
2902fn default_kg_depth() -> u32 {
2903    3
2904}
2905
2906fn default_kg_limit() -> usize {
2907    100
2908}
2909
2910/// Response from GET /v1/knowledge/query
2911#[derive(Debug, Serialize)]
2912pub struct KgQueryResponse {
2913    pub agent_id: String,
2914    pub node_count: usize,
2915    pub edge_count: usize,
2916    pub edges: Vec<GraphEdgeResponse>,
2917}
2918
2919/// GET /v1/knowledge/path — shortest path between two memory IDs
2920#[derive(Debug, Deserialize)]
2921pub struct KgPathParams {
2922    /// Agent ID for authorization.
2923    pub agent_id: String,
2924    /// Source memory ID.
2925    pub from: String,
2926    /// Target memory ID.
2927    pub to: String,
2928}
2929
2930/// Response from GET /v1/knowledge/path
2931#[derive(Debug, Serialize)]
2932pub struct KgPathResponse {
2933    pub agent_id: String,
2934    pub from_id: String,
2935    pub to_id: String,
2936    pub hop_count: usize,
2937    pub path: Vec<String>,
2938}
2939
2940/// GET /v1/knowledge/export — export graph as JSON or GraphML
2941#[derive(Debug, Deserialize)]
2942pub struct KgExportParams {
2943    /// Agent ID whose graph to export.
2944    pub agent_id: String,
2945    /// Export format: "json" (default) or "graphml".
2946    #[serde(default = "default_kg_format")]
2947    pub format: String,
2948}
2949
2950fn default_kg_format() -> String {
2951    "json".to_string()
2952}
2953
2954/// Response from GET /v1/knowledge/export (format=json)
2955#[derive(Debug, Serialize)]
2956pub struct KgExportJsonResponse {
2957    pub agent_id: String,
2958    pub format: String,
2959    pub node_count: usize,
2960    pub edge_count: usize,
2961    pub edges: Vec<GraphEdgeResponse>,
2962}
2963
2964// ============================================================================
2965// COG-1: Cognitive Memory Lifecycle — per-namespace memory policy
2966// ============================================================================
2967
2968fn default_working_ttl() -> Option<u64> {
2969    Some(14_400) // 4 hours
2970}
2971fn default_episodic_ttl() -> Option<u64> {
2972    Some(2_592_000) // 30 days
2973}
2974fn default_semantic_ttl() -> Option<u64> {
2975    Some(31_536_000) // 365 days
2976}
2977fn default_procedural_ttl() -> Option<u64> {
2978    Some(63_072_000) // 730 days
2979}
2980fn default_working_decay() -> DecayStrategy {
2981    DecayStrategy::Exponential
2982}
2983fn default_episodic_decay() -> DecayStrategy {
2984    DecayStrategy::PowerLaw
2985}
2986fn default_semantic_decay() -> DecayStrategy {
2987    DecayStrategy::Logarithmic
2988}
2989fn default_procedural_decay() -> DecayStrategy {
2990    DecayStrategy::Flat
2991}
2992fn default_sr_factor() -> f64 {
2993    1.0
2994}
2995fn default_sr_base_interval() -> u64 {
2996    86_400 // 1 day
2997}
2998fn default_consolidation_enabled() -> bool {
2999    false
3000}
3001fn default_policy_consolidation_threshold() -> f32 {
3002    0.92
3003}
3004fn default_consolidation_interval_hours() -> u32 {
3005    24
3006}
3007fn default_store_dedup_threshold() -> f32 {
3008    0.95
3009}
3010
3011/// Per-namespace memory lifecycle policy (COG-1).
3012///
3013/// Controls type-specific TTLs, decay curves, and spaced repetition behaviour.
3014/// All fields have sensible defaults; only override what you need.
3015#[derive(Debug, Clone, Serialize, Deserialize)]
3016pub struct MemoryPolicy {
3017    // ── Differential TTLs ────────────────────────────────────────────────────
3018    /// Default TTL for `working` memories in seconds (default: 4 h = 14 400 s).
3019    #[serde(
3020        default = "default_working_ttl",
3021        skip_serializing_if = "Option::is_none"
3022    )]
3023    pub working_ttl_seconds: Option<u64>,
3024    /// Default TTL for `episodic` memories in seconds (default: 30 d = 2 592 000 s).
3025    #[serde(
3026        default = "default_episodic_ttl",
3027        skip_serializing_if = "Option::is_none"
3028    )]
3029    pub episodic_ttl_seconds: Option<u64>,
3030    /// Default TTL for `semantic` memories in seconds (default: 365 d = 31 536 000 s).
3031    #[serde(
3032        default = "default_semantic_ttl",
3033        skip_serializing_if = "Option::is_none"
3034    )]
3035    pub semantic_ttl_seconds: Option<u64>,
3036    /// Default TTL for `procedural` memories in seconds (default: 730 d = 63 072 000 s).
3037    #[serde(
3038        default = "default_procedural_ttl",
3039        skip_serializing_if = "Option::is_none"
3040    )]
3041    pub procedural_ttl_seconds: Option<u64>,
3042
3043    // ── Decay curves ─────────────────────────────────────────────────────────
3044    /// Decay strategy for `working` memories (default: exponential).
3045    #[serde(default = "default_working_decay")]
3046    pub working_decay: DecayStrategy,
3047    /// Decay strategy for `episodic` memories (default: power_law).
3048    #[serde(default = "default_episodic_decay")]
3049    pub episodic_decay: DecayStrategy,
3050    /// Decay strategy for `semantic` memories (default: logarithmic).
3051    #[serde(default = "default_semantic_decay")]
3052    pub semantic_decay: DecayStrategy,
3053    /// Decay strategy for `procedural` memories (default: flat — no decay).
3054    #[serde(default = "default_procedural_decay")]
3055    pub procedural_decay: DecayStrategy,
3056
3057    // ── Spaced repetition ────────────────────────────────────────────────────
3058    /// Multiplier applied to the TTL extension on each recall.
3059    /// Extension = `access_count × sr_factor × sr_base_interval_seconds`.
3060    /// Set to 0.0 to disable spaced repetition. (default: 1.0)
3061    #[serde(default = "default_sr_factor")]
3062    pub spaced_repetition_factor: f64,
3063    /// Base interval in seconds for spaced repetition TTL extension (default: 86 400 = 1 day).
3064    #[serde(default = "default_sr_base_interval")]
3065    pub spaced_repetition_base_interval_seconds: u64,
3066
3067    // ── COG-3: Proactive consolidation ───────────────────────────────────────
3068    /// Enable background deduplication of semantically similar memories (default: false).
3069    #[serde(default = "default_consolidation_enabled")]
3070    pub consolidation_enabled: bool,
3071    /// Cosine-similarity threshold for merging memories (default: 0.92, range 0.85–0.99).
3072    #[serde(default = "default_policy_consolidation_threshold")]
3073    pub consolidation_threshold: f32,
3074    /// How often the background consolidation job runs, in hours (default: 24).
3075    #[serde(default = "default_consolidation_interval_hours")]
3076    pub consolidation_interval_hours: u32,
3077    /// Total number of memories merged since namespace creation (read-only).
3078    #[serde(default)]
3079    pub consolidated_count: u64,
3080
3081    // ── SEC-5: Per-namespace rate limiting ───────────────────────────────────
3082    /// Master rate-limit switch (default: false — opt-in to avoid breaking existing clients).
3083    /// Set to `true` to enforce `rate_limit_stores_per_minute` / `rate_limit_recalls_per_minute`.
3084    #[serde(default)]
3085    pub rate_limit_enabled: bool,
3086    /// Maximum `POST /v1/memory/store` operations per minute for this namespace.
3087    /// `None` = unlimited. Only enforced when `rate_limit_enabled = true`.
3088    #[serde(default, skip_serializing_if = "Option::is_none")]
3089    pub rate_limit_stores_per_minute: Option<u32>,
3090    /// Maximum `POST /v1/memory/recall` operations per minute for this namespace.
3091    /// `None` = unlimited. Only enforced when `rate_limit_enabled = true`.
3092    #[serde(default, skip_serializing_if = "Option::is_none")]
3093    pub rate_limit_recalls_per_minute: Option<u32>,
3094
3095    // ── CE-10a: Store-time deduplication ─────────────────────────────────────
3096    /// Enable near-duplicate detection on every `store` call (default: false).
3097    ///
3098    /// When enabled, a quick vector-search (top-1) runs after embedding; if the
3099    /// nearest neighbour has cosine similarity ≥ 0.95 the new store is rejected
3100    /// and the existing memory ID is returned instead.  Adds one ANN query to
3101    /// every store operation — keep disabled for high-throughput namespaces.
3102    #[serde(default)]
3103    pub dedup_on_store: bool,
3104    /// Similarity threshold for store-time deduplication (default: 0.95).
3105    #[serde(default = "default_store_dedup_threshold")]
3106    pub dedup_threshold: f32,
3107}
3108
3109impl Default for MemoryPolicy {
3110    fn default() -> Self {
3111        Self {
3112            working_ttl_seconds: default_working_ttl(),
3113            episodic_ttl_seconds: default_episodic_ttl(),
3114            semantic_ttl_seconds: default_semantic_ttl(),
3115            procedural_ttl_seconds: default_procedural_ttl(),
3116            working_decay: default_working_decay(),
3117            episodic_decay: default_episodic_decay(),
3118            semantic_decay: default_semantic_decay(),
3119            procedural_decay: default_procedural_decay(),
3120            spaced_repetition_factor: default_sr_factor(),
3121            spaced_repetition_base_interval_seconds: default_sr_base_interval(),
3122            consolidation_enabled: default_consolidation_enabled(),
3123            consolidation_threshold: default_policy_consolidation_threshold(),
3124            consolidation_interval_hours: default_consolidation_interval_hours(),
3125            consolidated_count: 0,
3126            rate_limit_enabled: false,
3127            rate_limit_stores_per_minute: None,
3128            rate_limit_recalls_per_minute: None,
3129            dedup_on_store: false,
3130            dedup_threshold: default_store_dedup_threshold(),
3131        }
3132    }
3133}
3134
3135impl MemoryPolicy {
3136    /// Return the configured TTL for the given memory type, in seconds.
3137    pub fn ttl_for_type(&self, memory_type: &MemoryType) -> Option<u64> {
3138        match memory_type {
3139            MemoryType::Working => self.working_ttl_seconds,
3140            MemoryType::Episodic => self.episodic_ttl_seconds,
3141            MemoryType::Semantic => self.semantic_ttl_seconds,
3142            MemoryType::Procedural => self.procedural_ttl_seconds,
3143        }
3144    }
3145
3146    /// Return the configured decay strategy for the given memory type.
3147    pub fn decay_for_type(&self, memory_type: &MemoryType) -> DecayStrategy {
3148        match memory_type {
3149            MemoryType::Working => self.working_decay,
3150            MemoryType::Episodic => self.episodic_decay,
3151            MemoryType::Semantic => self.semantic_decay,
3152            MemoryType::Procedural => self.procedural_decay,
3153        }
3154    }
3155
3156    /// Compute the spaced repetition TTL extension in seconds.
3157    ///
3158    /// `extension = access_count × sr_factor × sr_base_interval_seconds`
3159    pub fn spaced_repetition_extension(&self, access_count: u32) -> u64 {
3160        if self.spaced_repetition_factor <= 0.0 {
3161            return 0;
3162        }
3163        let ext = access_count as f64
3164            * self.spaced_repetition_factor
3165            * self.spaced_repetition_base_interval_seconds as f64;
3166        ext.round() as u64
3167    }
3168}