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    // Array operators
719    // =========================================================================
720    /// Array field contains a value (checks if a JSON array field includes the given element)
721    #[serde(rename = "$arrayContains")]
722    ArrayContains(FilterValue),
723    /// Array field contains all specified values
724    #[serde(rename = "$arrayContainsAll")]
725    ArrayContainsAll(Vec<FilterValue>),
726    /// Array field contains any of the specified values
727    #[serde(rename = "$arrayContainsAny")]
728    ArrayContainsAny(Vec<FilterValue>),
729}
730
731/// A filter expression that can be a single field condition or a logical combinator
732#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
733#[serde(untagged)]
734pub enum FilterExpression {
735    /// Logical AND of multiple expressions
736    And {
737        #[serde(rename = "$and")]
738        conditions: Vec<FilterExpression>,
739    },
740    /// Logical OR of multiple expressions
741    Or {
742        #[serde(rename = "$or")]
743        conditions: Vec<FilterExpression>,
744    },
745    /// Single field condition
746    Field {
747        #[serde(flatten)]
748        field: std::collections::HashMap<String, FilterCondition>,
749    },
750}
751
752// ============================================================================
753// Namespace quota types
754// ============================================================================
755
756/// Quota configuration for a namespace
757#[derive(Debug, Clone, Serialize, Deserialize, Default)]
758pub struct QuotaConfig {
759    /// Maximum number of vectors allowed (None = unlimited)
760    #[serde(skip_serializing_if = "Option::is_none")]
761    pub max_vectors: Option<u64>,
762    /// Maximum storage size in bytes (None = unlimited)
763    #[serde(skip_serializing_if = "Option::is_none")]
764    pub max_storage_bytes: Option<u64>,
765    /// Maximum dimensions per vector (None = unlimited)
766    #[serde(skip_serializing_if = "Option::is_none")]
767    pub max_dimensions: Option<usize>,
768    /// Maximum metadata size per vector in bytes (None = unlimited)
769    #[serde(skip_serializing_if = "Option::is_none")]
770    pub max_metadata_bytes: Option<usize>,
771    /// Whether to enforce quotas (soft limit = warn only, hard = reject)
772    #[serde(default)]
773    pub enforcement: QuotaEnforcement,
774}
775
776/// Quota enforcement mode
777#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
778#[serde(rename_all = "snake_case")]
779pub enum QuotaEnforcement {
780    /// No enforcement, just tracking
781    None,
782    /// Log warnings when quota exceeded but allow operations
783    Soft,
784    /// Reject operations that would exceed quota
785    #[default]
786    Hard,
787}
788
789/// Current quota usage for a namespace
790#[derive(Debug, Clone, Serialize, Deserialize, Default)]
791pub struct QuotaUsage {
792    /// Current number of vectors
793    pub vector_count: u64,
794    /// Current storage size in bytes (estimated)
795    pub storage_bytes: u64,
796    /// Average vector dimensions
797    pub avg_dimensions: Option<usize>,
798    /// Average metadata size in bytes
799    pub avg_metadata_bytes: Option<usize>,
800    /// Last updated timestamp (Unix epoch)
801    pub last_updated: u64,
802}
803
804impl QuotaUsage {
805    /// Create new usage with current timestamp
806    pub fn new(vector_count: u64, storage_bytes: u64) -> Self {
807        let now = std::time::SystemTime::now()
808            .duration_since(std::time::UNIX_EPOCH)
809            .unwrap_or_default()
810            .as_secs();
811        Self {
812            vector_count,
813            storage_bytes,
814            avg_dimensions: None,
815            avg_metadata_bytes: None,
816            last_updated: now,
817        }
818    }
819
820    /// Update the timestamp to now
821    pub fn touch(&mut self) {
822        self.last_updated = std::time::SystemTime::now()
823            .duration_since(std::time::UNIX_EPOCH)
824            .unwrap_or_default()
825            .as_secs();
826    }
827}
828
829/// Combined quota status showing config and current usage
830#[derive(Debug, Clone, Serialize, Deserialize)]
831pub struct QuotaStatus {
832    /// Namespace name
833    pub namespace: String,
834    /// Quota configuration
835    pub config: QuotaConfig,
836    /// Current usage
837    pub usage: QuotaUsage,
838    /// Percentage of vector quota used (0-100, None if unlimited)
839    #[serde(skip_serializing_if = "Option::is_none")]
840    pub vector_usage_percent: Option<f32>,
841    /// Percentage of storage quota used (0-100, None if unlimited)
842    #[serde(skip_serializing_if = "Option::is_none")]
843    pub storage_usage_percent: Option<f32>,
844    /// Whether any quota is exceeded
845    pub is_exceeded: bool,
846    /// List of exceeded quota types
847    #[serde(skip_serializing_if = "Vec::is_empty")]
848    pub exceeded_quotas: Vec<String>,
849}
850
851impl QuotaStatus {
852    /// Create a new quota status from config and usage
853    pub fn new(namespace: String, config: QuotaConfig, usage: QuotaUsage) -> Self {
854        let vector_usage_percent = config
855            .max_vectors
856            .map(|max| (usage.vector_count as f32 / max as f32) * 100.0);
857
858        let storage_usage_percent = config
859            .max_storage_bytes
860            .map(|max| (usage.storage_bytes as f32 / max as f32) * 100.0);
861
862        let mut exceeded_quotas = Vec::new();
863
864        if let Some(max) = config.max_vectors {
865            if usage.vector_count > max {
866                exceeded_quotas.push("max_vectors".to_string());
867            }
868        }
869
870        if let Some(max) = config.max_storage_bytes {
871            if usage.storage_bytes > max {
872                exceeded_quotas.push("max_storage_bytes".to_string());
873            }
874        }
875
876        let is_exceeded = !exceeded_quotas.is_empty();
877
878        Self {
879            namespace,
880            config,
881            usage,
882            vector_usage_percent,
883            storage_usage_percent,
884            is_exceeded,
885            exceeded_quotas,
886        }
887    }
888}
889
890/// Request to set quota for a namespace
891#[derive(Debug, Deserialize)]
892pub struct SetQuotaRequest {
893    /// Quota configuration to apply
894    pub config: QuotaConfig,
895}
896
897/// Response from setting quota
898#[derive(Debug, Serialize)]
899pub struct SetQuotaResponse {
900    /// Whether the operation succeeded
901    pub success: bool,
902    /// Namespace name
903    pub namespace: String,
904    /// Applied quota configuration
905    pub config: QuotaConfig,
906    /// Status message
907    pub message: String,
908}
909
910/// Quota check result
911#[derive(Debug, Clone, Serialize)]
912pub struct QuotaCheckResult {
913    /// Whether the operation is allowed
914    pub allowed: bool,
915    /// Reason if not allowed
916    #[serde(skip_serializing_if = "Option::is_none")]
917    pub reason: Option<String>,
918    /// Current usage
919    pub usage: QuotaUsage,
920    /// Quota that would be exceeded
921    #[serde(skip_serializing_if = "Option::is_none")]
922    pub exceeded_quota: Option<String>,
923}
924
925/// Response listing all namespace quotas
926#[derive(Debug, Serialize)]
927pub struct QuotaListResponse {
928    /// List of quota statuses per namespace
929    pub quotas: Vec<QuotaStatus>,
930    /// Total number of namespaces with quotas
931    pub total: u64,
932    /// Default quota configuration (if set)
933    #[serde(skip_serializing_if = "Option::is_none")]
934    pub default_config: Option<QuotaConfig>,
935}
936
937/// Response for default quota query
938#[derive(Debug, Serialize)]
939pub struct DefaultQuotaResponse {
940    /// Default quota configuration (None if not set)
941    pub config: Option<QuotaConfig>,
942}
943
944/// Request to set default quota configuration
945#[derive(Debug, Deserialize)]
946pub struct SetDefaultQuotaRequest {
947    /// Default quota configuration (None to remove)
948    pub config: Option<QuotaConfig>,
949}
950
951/// Request to check if an operation would exceed quota
952#[derive(Debug, Deserialize)]
953pub struct QuotaCheckRequest {
954    /// Vector IDs to check (simulated vectors)
955    pub vector_ids: Vec<String>,
956    /// Dimension of vectors (for size estimation)
957    #[serde(default)]
958    pub dimensions: Option<usize>,
959    /// Estimated metadata size per vector
960    #[serde(default)]
961    pub metadata_bytes: Option<usize>,
962}
963
964// ============================================================================
965// Export API Types (Turbopuffer-inspired)
966// ============================================================================
967
968/// Request to export vectors from a namespace with pagination
969#[derive(Debug, Deserialize)]
970pub struct ExportRequest {
971    /// Maximum number of vectors to return per page (default: 1000, max: 10000)
972    #[serde(default = "default_export_top_k")]
973    pub top_k: usize,
974    /// Cursor for pagination - the last vector ID from previous page
975    #[serde(skip_serializing_if = "Option::is_none")]
976    pub cursor: Option<String>,
977    /// Whether to include vector values in the response (default: true)
978    #[serde(default = "default_true")]
979    pub include_vectors: bool,
980    /// Whether to include metadata in the response (default: true)
981    #[serde(default = "default_true")]
982    pub include_metadata: bool,
983}
984
985fn default_export_top_k() -> usize {
986    1000
987}
988
989impl Default for ExportRequest {
990    fn default() -> Self {
991        Self {
992            top_k: 1000,
993            cursor: None,
994            include_vectors: true,
995            include_metadata: true,
996        }
997    }
998}
999
1000/// A single exported vector record
1001#[derive(Debug, Clone, Serialize, Deserialize)]
1002pub struct ExportedVector {
1003    /// Vector ID
1004    pub id: String,
1005    /// Vector values (optional based on include_vectors)
1006    #[serde(skip_serializing_if = "Option::is_none")]
1007    pub values: Option<Vec<f32>>,
1008    /// Metadata (optional based on include_metadata)
1009    #[serde(skip_serializing_if = "Option::is_none")]
1010    pub metadata: Option<serde_json::Value>,
1011    /// TTL in seconds if set
1012    #[serde(skip_serializing_if = "Option::is_none")]
1013    pub ttl_seconds: Option<u64>,
1014}
1015
1016impl From<&Vector> for ExportedVector {
1017    fn from(v: &Vector) -> Self {
1018        Self {
1019            id: v.id.clone(),
1020            values: Some(v.values.clone()),
1021            metadata: v.metadata.clone(),
1022            ttl_seconds: v.ttl_seconds,
1023        }
1024    }
1025}
1026
1027/// Response from export operation
1028#[derive(Debug, Serialize)]
1029pub struct ExportResponse {
1030    /// Exported vectors for this page
1031    pub vectors: Vec<ExportedVector>,
1032    /// Cursor for next page (None if this is the last page)
1033    #[serde(skip_serializing_if = "Option::is_none")]
1034    pub next_cursor: Option<String>,
1035    /// Total vectors in namespace (for progress tracking)
1036    pub total_count: usize,
1037    /// Number of vectors returned in this page
1038    pub returned_count: usize,
1039}
1040
1041// ============================================================================
1042// Unified Query API with rank_by (Turbopuffer-inspired)
1043// ============================================================================
1044
1045/// Ranking function for unified query API
1046/// Supports vector search (ANN/kNN), full-text BM25, and attribute ordering
1047#[derive(Debug, Clone, Serialize, Deserialize)]
1048#[serde(untagged)]
1049pub enum RankBy {
1050    /// Vector search: ["vector_field", "ANN"|"kNN", [query_vector]]
1051    /// or simplified: ["ANN", [query_vector]] for default "vector" field
1052    VectorSearch {
1053        field: String,
1054        method: VectorSearchMethod,
1055        query_vector: Vec<f32>,
1056    },
1057    /// Full-text BM25 search: ["text_field", "BM25", "query string"]
1058    FullTextSearch {
1059        field: String,
1060        method: String, // Always "BM25"
1061        query: String,
1062    },
1063    /// Attribute ordering: ["field_name", "asc"|"desc"]
1064    AttributeOrder {
1065        field: String,
1066        direction: SortDirection,
1067    },
1068    /// Sum of multiple ranking functions: ["Sum", [...rankings]]
1069    Sum(Vec<RankBy>),
1070    /// Max of multiple ranking functions: ["Max", [...rankings]]
1071    Max(Vec<RankBy>),
1072    /// Product with weight: ["Product", weight, ranking]
1073    Product { weight: f32, ranking: Box<RankBy> },
1074}
1075
1076/// Vector search method
1077#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
1078pub enum VectorSearchMethod {
1079    /// Approximate Nearest Neighbor (fast, default)
1080    #[default]
1081    ANN,
1082    /// Exact k-Nearest Neighbor (exhaustive, requires filters)
1083    #[serde(rename = "kNN")]
1084    KNN,
1085}
1086
1087/// Sort direction for attribute ordering
1088#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1089#[serde(rename_all = "lowercase")]
1090#[derive(Default)]
1091pub enum SortDirection {
1092    Asc,
1093    #[default]
1094    Desc,
1095}
1096
1097/// Unified query request with rank_by parameter (Turbopuffer-inspired)
1098#[derive(Debug, Deserialize)]
1099pub struct UnifiedQueryRequest {
1100    /// How to rank documents (required unless using aggregations)
1101    pub rank_by: RankByInput,
1102    /// Number of results to return
1103    #[serde(default = "default_top_k")]
1104    pub top_k: usize,
1105    /// Optional metadata filter
1106    #[serde(default)]
1107    pub filter: Option<FilterExpression>,
1108    /// Include metadata in results
1109    #[serde(default = "default_true")]
1110    pub include_metadata: bool,
1111    /// Include vectors in results
1112    #[serde(default)]
1113    pub include_vectors: bool,
1114    /// Distance metric for vector search (default: cosine)
1115    #[serde(default)]
1116    pub distance_metric: DistanceMetric,
1117    /// Opaque pagination cursor from a prior page's `next_cursor` (DAK-7424).
1118    /// Only honored for a top-level `VectorSearch` rank_by.
1119    #[serde(default)]
1120    pub cursor: Option<String>,
1121}
1122
1123/// Input format for rank_by that handles JSON array syntax
1124/// Examples:
1125/// - ["vector", "ANN", [0.1, 0.2, 0.3]]
1126/// - ["text", "BM25", "search query"]
1127/// - ["timestamp", "desc"]
1128/// - ["Sum", [["title", "BM25", "query"], ["content", "BM25", "query"]]]
1129/// - ["Product", 2.0, ["title", "BM25", "query"]]
1130#[derive(Debug, Clone, Serialize)]
1131pub struct RankByInput(pub RankBy);
1132
1133// DAK-7428: deserialize fallibly. The previous `#[serde(from)]` + infallible
1134// `From<serde_json::Value>` silently replaced ANY malformed `rank_by` with
1135// "sort by id ascending", so a client with a bad ranking expression got wrong
1136// ordering and no error at all. Fail loud at the request boundary instead —
1137// an unparseable `rank_by` is now a deserialization error (HTTP 400).
1138impl<'de> Deserialize<'de> for RankByInput {
1139    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
1140    where
1141        D: serde::Deserializer<'de>,
1142    {
1143        let value = serde_json::Value::deserialize(deserializer)?;
1144        parse_rank_by(&value).map(RankByInput).ok_or_else(|| {
1145            serde::de::Error::custom(
1146                "invalid rank_by: expected a ranking expression such as \
1147                 [\"vector\",\"ANN\",[...]], [\"text\",\"BM25\",\"query\"], \
1148                 [\"field\",\"desc\"], or a Sum/Product combinator",
1149            )
1150        })
1151    }
1152}
1153
1154/// Parse rank_by JSON array into RankBy enum
1155fn parse_rank_by(value: &serde_json::Value) -> Option<RankBy> {
1156    let arr = value.as_array()?;
1157    if arr.is_empty() {
1158        return None;
1159    }
1160
1161    let first = arr.first()?.as_str()?;
1162
1163    match first {
1164        // Combination operators
1165        "Sum" => {
1166            let rankings = arr.get(1)?.as_array()?;
1167            let parsed: Option<Vec<RankBy>> = rankings.iter().map(parse_rank_by).collect();
1168            Some(RankBy::Sum(parsed?))
1169        }
1170        "Max" => {
1171            let rankings = arr.get(1)?.as_array()?;
1172            let parsed: Option<Vec<RankBy>> = rankings.iter().map(parse_rank_by).collect();
1173            Some(RankBy::Max(parsed?))
1174        }
1175        "Product" => {
1176            let weight = arr.get(1)?.as_f64()? as f32;
1177            let ranking = parse_rank_by(arr.get(2)?)?;
1178            Some(RankBy::Product {
1179                weight,
1180                ranking: Box::new(ranking),
1181            })
1182        }
1183        // Vector search shorthand: ["ANN", [vector]] or ["kNN", [vector]]
1184        "ANN" => {
1185            let query_vector = parse_vector_array(arr.get(1)?)?;
1186            Some(RankBy::VectorSearch {
1187                field: "vector".to_string(),
1188                method: VectorSearchMethod::ANN,
1189                query_vector,
1190            })
1191        }
1192        "kNN" => {
1193            let query_vector = parse_vector_array(arr.get(1)?)?;
1194            Some(RankBy::VectorSearch {
1195                field: "vector".to_string(),
1196                method: VectorSearchMethod::KNN,
1197                query_vector,
1198            })
1199        }
1200        // Field-based operations
1201        field => {
1202            let second = arr.get(1)?;
1203
1204            // Check if second element is a method string
1205            if let Some(method_str) = second.as_str() {
1206                match method_str {
1207                    "ANN" => {
1208                        let query_vector = parse_vector_array(arr.get(2)?)?;
1209                        Some(RankBy::VectorSearch {
1210                            field: field.to_string(),
1211                            method: VectorSearchMethod::ANN,
1212                            query_vector,
1213                        })
1214                    }
1215                    "kNN" => {
1216                        let query_vector = parse_vector_array(arr.get(2)?)?;
1217                        Some(RankBy::VectorSearch {
1218                            field: field.to_string(),
1219                            method: VectorSearchMethod::KNN,
1220                            query_vector,
1221                        })
1222                    }
1223                    "BM25" => {
1224                        let query = arr.get(2)?.as_str()?;
1225                        Some(RankBy::FullTextSearch {
1226                            field: field.to_string(),
1227                            method: "BM25".to_string(),
1228                            query: query.to_string(),
1229                        })
1230                    }
1231                    "asc" => Some(RankBy::AttributeOrder {
1232                        field: field.to_string(),
1233                        direction: SortDirection::Asc,
1234                    }),
1235                    "desc" => Some(RankBy::AttributeOrder {
1236                        field: field.to_string(),
1237                        direction: SortDirection::Desc,
1238                    }),
1239                    _ => None,
1240                }
1241            } else {
1242                None
1243            }
1244        }
1245    }
1246}
1247
1248/// Parse a JSON value into a vector of f32
1249fn parse_vector_array(value: &serde_json::Value) -> Option<Vec<f32>> {
1250    let arr = value.as_array()?;
1251    arr.iter().map(|v| v.as_f64().map(|n| n as f32)).collect()
1252}
1253
1254/// Unified query response with $dist scoring
1255#[derive(Debug, Serialize, Deserialize)]
1256pub struct UnifiedQueryResponse {
1257    /// Search results ordered by rank_by score
1258    pub results: Vec<UnifiedSearchResult>,
1259    /// Cursor for pagination (if more results available)
1260    #[serde(skip_serializing_if = "Option::is_none")]
1261    pub next_cursor: Option<String>,
1262}
1263
1264/// Single result from unified query
1265#[derive(Debug, Serialize, Deserialize)]
1266pub struct UnifiedSearchResult {
1267    /// Vector/document ID
1268    pub id: String,
1269    /// Ranking score (distance for vector search, BM25 score for text)
1270    /// Named $dist for Turbopuffer compatibility
1271    #[serde(rename = "$dist", skip_serializing_if = "Option::is_none")]
1272    pub dist: Option<f32>,
1273    /// Metadata if requested
1274    #[serde(skip_serializing_if = "Option::is_none")]
1275    pub metadata: Option<serde_json::Value>,
1276    /// Vector values if requested
1277    #[serde(skip_serializing_if = "Option::is_none")]
1278    pub vector: Option<Vec<f32>>,
1279}
1280
1281// ============================================================================
1282// Aggregation types (Turbopuffer-inspired)
1283// ============================================================================
1284
1285/// Aggregate function for computing values across documents
1286#[derive(Debug, Clone, Serialize, Deserialize)]
1287pub enum AggregateFunction {
1288    /// Count matching documents: ["Count"]
1289    Count,
1290    /// Sum numeric attribute values: ["Sum", "attribute_name"]
1291    Sum { field: String },
1292    /// Average numeric attribute values: ["Avg", "attribute_name"]
1293    Avg { field: String },
1294    /// Minimum numeric attribute value: ["Min", "attribute_name"]
1295    Min { field: String },
1296    /// Maximum numeric attribute value: ["Max", "attribute_name"]
1297    Max { field: String },
1298}
1299
1300/// Wrapper for parsing aggregate function from JSON array
1301#[derive(Debug, Clone, Serialize, Deserialize)]
1302#[serde(from = "serde_json::Value")]
1303pub struct AggregateFunctionInput(pub AggregateFunction);
1304
1305impl From<serde_json::Value> for AggregateFunctionInput {
1306    fn from(value: serde_json::Value) -> Self {
1307        parse_aggregate_function(&value)
1308            .map(AggregateFunctionInput)
1309            .unwrap_or_else(|| {
1310                // Default to count if parsing fails
1311                AggregateFunctionInput(AggregateFunction::Count)
1312            })
1313    }
1314}
1315
1316/// Parse aggregate function from JSON array
1317fn parse_aggregate_function(value: &serde_json::Value) -> Option<AggregateFunction> {
1318    let arr = value.as_array()?;
1319    if arr.is_empty() {
1320        return None;
1321    }
1322
1323    let func_name = arr.first()?.as_str()?;
1324
1325    match func_name {
1326        "Count" => Some(AggregateFunction::Count),
1327        "Sum" => {
1328            let field = arr.get(1)?.as_str()?;
1329            Some(AggregateFunction::Sum {
1330                field: field.to_string(),
1331            })
1332        }
1333        "Avg" => {
1334            let field = arr.get(1)?.as_str()?;
1335            Some(AggregateFunction::Avg {
1336                field: field.to_string(),
1337            })
1338        }
1339        "Min" => {
1340            let field = arr.get(1)?.as_str()?;
1341            Some(AggregateFunction::Min {
1342                field: field.to_string(),
1343            })
1344        }
1345        "Max" => {
1346            let field = arr.get(1)?.as_str()?;
1347            Some(AggregateFunction::Max {
1348                field: field.to_string(),
1349            })
1350        }
1351        _ => None,
1352    }
1353}
1354
1355/// Request for aggregation query (Turbopuffer-inspired)
1356#[derive(Debug, Deserialize)]
1357pub struct AggregationRequest {
1358    /// Named aggregations to compute
1359    /// Example: {"my_count": ["Count"], "total_score": ["Sum", "score"]}
1360    pub aggregate_by: std::collections::HashMap<String, AggregateFunctionInput>,
1361    /// Fields to group results by (optional)
1362    /// Example: ["category", "status"]
1363    #[serde(default)]
1364    pub group_by: Vec<String>,
1365    /// Filter to apply before aggregation
1366    #[serde(default)]
1367    pub filter: Option<FilterExpression>,
1368    /// Maximum number of groups to return (default: 100)
1369    #[serde(default = "default_agg_limit")]
1370    pub limit: usize,
1371}
1372
1373fn default_agg_limit() -> usize {
1374    100
1375}
1376
1377/// Response for aggregation query
1378#[derive(Debug, Serialize, Deserialize)]
1379pub struct AggregationResponse {
1380    /// Aggregation results (without grouping)
1381    #[serde(skip_serializing_if = "Option::is_none")]
1382    pub aggregations: Option<std::collections::HashMap<String, serde_json::Value>>,
1383    /// Grouped aggregation results (with group_by)
1384    #[serde(skip_serializing_if = "Option::is_none")]
1385    pub aggregation_groups: Option<Vec<AggregationGroup>>,
1386}
1387
1388/// Single group in aggregation results
1389#[derive(Debug, Serialize, Deserialize)]
1390pub struct AggregationGroup {
1391    /// Group key values (flattened into object)
1392    #[serde(flatten)]
1393    pub group_key: std::collections::HashMap<String, serde_json::Value>,
1394    /// Aggregation results for this group
1395    #[serde(flatten)]
1396    pub aggregations: std::collections::HashMap<String, serde_json::Value>,
1397}
1398
1399// =============================================================================
1400// TEXT-BASED API TYPES (Embedded Inference)
1401// =============================================================================
1402
1403/// A text document with metadata for text-based upsert
1404#[derive(Debug, Clone, Serialize, Deserialize)]
1405pub struct TextDocument {
1406    /// Unique identifier for this document
1407    pub id: VectorId,
1408    /// The text content to be embedded
1409    pub text: String,
1410    /// Optional metadata to store with the vector
1411    #[serde(skip_serializing_if = "Option::is_none")]
1412    pub metadata: Option<serde_json::Value>,
1413    /// TTL in seconds (optional)
1414    #[serde(skip_serializing_if = "Option::is_none")]
1415    pub ttl_seconds: Option<u64>,
1416}
1417
1418/// Request to upsert text documents (auto-embedded)
1419#[derive(Debug, Deserialize)]
1420pub struct TextUpsertRequest {
1421    /// Text documents to embed and store
1422    pub documents: Vec<TextDocument>,
1423    /// Embedding model to use (default: `minilm`).
1424    #[serde(default)]
1425    pub model: Option<EmbeddingModelType>,
1426}
1427
1428/// Response from text upsert operation
1429#[derive(Debug, Serialize, Deserialize)]
1430pub struct TextUpsertResponse {
1431    /// Number of documents successfully upserted
1432    pub upserted_count: usize,
1433    /// Number of tokens processed for embedding
1434    pub tokens_processed: usize,
1435    /// Embedding model used
1436    pub model: EmbeddingModelType,
1437    /// Time taken for embedding generation (ms)
1438    pub embedding_time_ms: u64,
1439}
1440
1441/// Request for text-based query (auto-embedded)
1442#[derive(Debug, Deserialize)]
1443pub struct TextQueryRequest {
1444    /// The query text to search for
1445    pub text: String,
1446    /// Number of results to return
1447    #[serde(default = "default_top_k")]
1448    pub top_k: usize,
1449    /// Optional filter to apply
1450    #[serde(default)]
1451    pub filter: Option<FilterExpression>,
1452    /// Whether to include vectors in response
1453    #[serde(default)]
1454    pub include_vectors: bool,
1455    /// Whether to include the original text in response (if stored in metadata)
1456    #[serde(default = "default_true")]
1457    pub include_text: bool,
1458    /// Embedding model to use (must match upsert model; default: `minilm`).
1459    #[serde(default)]
1460    pub model: Option<EmbeddingModelType>,
1461}
1462
1463/// Response from text-based query
1464#[derive(Debug, Serialize, Deserialize)]
1465pub struct TextQueryResponse {
1466    /// Search results with similarity scores
1467    pub results: Vec<TextSearchResult>,
1468    /// Embedding model used
1469    pub model: EmbeddingModelType,
1470    /// Time taken for embedding generation (ms)
1471    pub embedding_time_ms: u64,
1472    /// Time taken for search (ms)
1473    pub search_time_ms: u64,
1474}
1475
1476/// Single result from text search
1477#[derive(Debug, Serialize, Deserialize)]
1478pub struct TextSearchResult {
1479    /// Document ID
1480    pub id: VectorId,
1481    /// Similarity score (higher is better)
1482    pub score: f32,
1483    /// Original text (if include_text=true and stored in metadata)
1484    #[serde(skip_serializing_if = "Option::is_none")]
1485    pub text: Option<String>,
1486    /// Associated metadata
1487    #[serde(skip_serializing_if = "Option::is_none")]
1488    pub metadata: Option<serde_json::Value>,
1489    /// Vector values (if include_vectors=true)
1490    #[serde(skip_serializing_if = "Option::is_none")]
1491    pub vector: Option<Vec<f32>>,
1492}
1493
1494/// Batch text query request
1495#[derive(Debug, Deserialize)]
1496pub struct BatchTextQueryRequest {
1497    /// Multiple query texts
1498    pub queries: Vec<String>,
1499    /// Number of results per query
1500    #[serde(default = "default_top_k")]
1501    pub top_k: usize,
1502    /// Optional filter to apply to all queries
1503    #[serde(default)]
1504    pub filter: Option<FilterExpression>,
1505    /// Whether to include vectors in response
1506    #[serde(default)]
1507    pub include_vectors: bool,
1508    /// Embedding model to use (default: `minilm`).
1509    #[serde(default)]
1510    pub model: Option<EmbeddingModelType>,
1511}
1512
1513/// Response from batch text query
1514#[derive(Debug, Serialize, Deserialize)]
1515pub struct BatchTextQueryResponse {
1516    /// Results for each query
1517    pub results: Vec<Vec<TextSearchResult>>,
1518    /// Embedding model used
1519    pub model: EmbeddingModelType,
1520    /// Total time for embedding generation (ms)
1521    pub embedding_time_ms: u64,
1522    /// Total time for search (ms)
1523    pub search_time_ms: u64,
1524}
1525
1526/// Available embedding models.
1527///
1528/// Replaces the previous `model: String` field — callers now supply a
1529/// typed enum value, eliminating runtime string-mismatch bugs.
1530///
1531/// JSON serialisation uses lowercase identifiers:
1532/// `"bge-large"`, `"minilm"`, `"bge-small"`, `"e5-small"`.
1533#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1534pub enum EmbeddingModelType {
1535    /// BAAI/bge-large-en-v1.5 — highest quality, 1024 dimensions (default)
1536    #[default]
1537    #[serde(rename = "bge-large")]
1538    BgeLarge,
1539    /// all-MiniLM-L6-v2 — fast and memory-efficient, 384 dimensions
1540    #[serde(rename = "minilm")]
1541    MiniLM,
1542    /// BAAI/bge-small-en-v1.5 — balanced quality and speed, 384 dimensions
1543    #[serde(rename = "bge-small")]
1544    BgeSmall,
1545    /// intfloat/e5-small-v2 — quality-focused, 384 dimensions
1546    #[serde(rename = "e5-small")]
1547    E5Small,
1548    /// nomic-ai/modernbert-embed-base — 768 dimensions, MRL, 8192 tokens
1549    #[serde(rename = "modernbert-embed-base")]
1550    ModernBertEmbedBase,
1551    /// Alibaba-NLP/gte-modernbert-base — 768 dimensions, MTEB retrieval 64.38
1552    #[serde(rename = "gte-modernbert-base")]
1553    GteModernBertBase,
1554}
1555
1556impl EmbeddingModelType {
1557    /// Embedding vector dimension for this model.
1558    pub fn dimension(&self) -> usize {
1559        match self {
1560            EmbeddingModelType::BgeLarge => 1024,
1561            EmbeddingModelType::MiniLM => 384,
1562            EmbeddingModelType::BgeSmall => 384,
1563            EmbeddingModelType::E5Small => 384,
1564            EmbeddingModelType::ModernBertEmbedBase => 768,
1565            EmbeddingModelType::GteModernBertBase => 768,
1566        }
1567    }
1568}
1569
1570impl std::fmt::Display for EmbeddingModelType {
1571    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1572        match self {
1573            EmbeddingModelType::BgeLarge => write!(f, "bge-large"),
1574            EmbeddingModelType::MiniLM => write!(f, "minilm"),
1575            EmbeddingModelType::BgeSmall => write!(f, "bge-small"),
1576            EmbeddingModelType::E5Small => write!(f, "e5-small"),
1577            EmbeddingModelType::ModernBertEmbedBase => write!(f, "modernbert-embed-base"),
1578            EmbeddingModelType::GteModernBertBase => write!(f, "gte-modernbert-base"),
1579        }
1580    }
1581}
1582
1583// ============================================================================
1584// Dakera Memory Types — AI Agent Memory Platform
1585// ============================================================================
1586
1587/// Type of memory stored by an agent
1588#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1589#[serde(rename_all = "snake_case")]
1590#[derive(Default)]
1591pub enum MemoryType {
1592    /// Personal experiences and events
1593    #[default]
1594    Episodic,
1595    /// Facts and general knowledge
1596    Semantic,
1597    /// How-to knowledge and skills
1598    Procedural,
1599    /// Short-term, temporary context
1600    Working,
1601}
1602
1603impl std::fmt::Display for MemoryType {
1604    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1605        match self {
1606            MemoryType::Episodic => write!(f, "episodic"),
1607            MemoryType::Semantic => write!(f, "semantic"),
1608            MemoryType::Procedural => write!(f, "procedural"),
1609            MemoryType::Working => write!(f, "working"),
1610        }
1611    }
1612}
1613
1614/// A memory stored by an AI agent
1615#[derive(Debug, Clone, Serialize, Deserialize)]
1616pub struct Memory {
1617    pub id: String,
1618    #[serde(default)]
1619    pub memory_type: MemoryType,
1620    pub content: String,
1621    pub agent_id: String,
1622    #[serde(skip_serializing_if = "Option::is_none")]
1623    pub session_id: Option<String>,
1624    #[serde(default = "default_importance")]
1625    pub importance: f32,
1626    #[serde(default)]
1627    pub tags: Vec<String>,
1628    #[serde(skip_serializing_if = "Option::is_none")]
1629    pub metadata: Option<serde_json::Value>,
1630    pub created_at: u64,
1631    pub last_accessed_at: u64,
1632    #[serde(default)]
1633    pub access_count: u32,
1634    #[serde(skip_serializing_if = "Option::is_none")]
1635    pub ttl_seconds: Option<u64>,
1636    #[serde(skip_serializing_if = "Option::is_none")]
1637    pub expires_at: Option<u64>,
1638}
1639
1640fn default_importance() -> f32 {
1641    0.5
1642}
1643
1644impl Memory {
1645    /// Create a new memory with current timestamps
1646    pub fn new(id: String, content: String, agent_id: String, memory_type: MemoryType) -> Self {
1647        let now = std::time::SystemTime::now()
1648            .duration_since(std::time::UNIX_EPOCH)
1649            .unwrap_or_default()
1650            .as_secs();
1651        Self {
1652            id,
1653            memory_type,
1654            content,
1655            agent_id,
1656            session_id: None,
1657            importance: 0.5,
1658            tags: Vec::new(),
1659            metadata: None,
1660            created_at: now,
1661            last_accessed_at: now,
1662            access_count: 0,
1663            ttl_seconds: None,
1664            expires_at: None,
1665        }
1666    }
1667
1668    /// Check if this memory has expired
1669    pub fn is_expired(&self) -> bool {
1670        if let Some(expires_at) = self.expires_at {
1671            let now = std::time::SystemTime::now()
1672                .duration_since(std::time::UNIX_EPOCH)
1673                .unwrap_or_default()
1674                .as_secs();
1675            now >= expires_at
1676        } else {
1677            false
1678        }
1679    }
1680
1681    /// Pack memory fields into metadata for Vector storage
1682    pub fn to_vector_metadata(&self) -> serde_json::Value {
1683        let mut meta = serde_json::Map::new();
1684        meta.insert("_dakera_type".to_string(), serde_json::json!("memory"));
1685        meta.insert(
1686            "memory_type".to_string(),
1687            serde_json::json!(self.memory_type),
1688        );
1689        meta.insert("content".to_string(), serde_json::json!(self.content));
1690        meta.insert("agent_id".to_string(), serde_json::json!(self.agent_id));
1691        if let Some(ref sid) = self.session_id {
1692            meta.insert("session_id".to_string(), serde_json::json!(sid));
1693        }
1694        meta.insert("importance".to_string(), serde_json::json!(self.importance));
1695        meta.insert("tags".to_string(), serde_json::json!(self.tags));
1696        meta.insert("created_at".to_string(), serde_json::json!(self.created_at));
1697        meta.insert(
1698            "last_accessed_at".to_string(),
1699            serde_json::json!(self.last_accessed_at),
1700        );
1701        meta.insert(
1702            "access_count".to_string(),
1703            serde_json::json!(self.access_count),
1704        );
1705        if let Some(ref ttl) = self.ttl_seconds {
1706            meta.insert("ttl_seconds".to_string(), serde_json::json!(ttl));
1707        }
1708        if let Some(ref expires) = self.expires_at {
1709            meta.insert("expires_at".to_string(), serde_json::json!(expires));
1710        }
1711        if let Some(ref user_meta) = self.metadata {
1712            if let Some(cd) = user_meta.get("_dakera_content_date") {
1713                meta.insert("_dakera_content_date".to_string(), cd.clone());
1714            }
1715            meta.insert("user_metadata".to_string(), user_meta.clone());
1716        }
1717        serde_json::Value::Object(meta)
1718    }
1719
1720    /// Convert a Memory to a Vector (for storage layer)
1721    pub fn to_vector(&self, embedding: Vec<f32>) -> Vector {
1722        let mut v = Vector {
1723            id: self.id.clone(),
1724            values: embedding,
1725            metadata: Some(self.to_vector_metadata()),
1726            ttl_seconds: self.ttl_seconds,
1727            expires_at: self.expires_at,
1728        };
1729        v.apply_ttl();
1730        v
1731    }
1732
1733    /// Reconstruct a Memory from a Vector's metadata
1734    pub fn from_vector(vector: &Vector) -> Option<Self> {
1735        let meta = vector.metadata.as_ref()?.as_object()?;
1736        let entry_type = meta.get("_dakera_type")?.as_str()?;
1737        if entry_type != "memory" {
1738            return None;
1739        }
1740
1741        Some(Memory {
1742            id: vector.id.clone(),
1743            memory_type: serde_json::from_value(meta.get("memory_type")?.clone())
1744                .unwrap_or_default(),
1745            content: meta.get("content")?.as_str()?.to_string(),
1746            agent_id: meta.get("agent_id")?.as_str()?.to_string(),
1747            session_id: meta
1748                .get("session_id")
1749                .and_then(|v| v.as_str())
1750                .map(String::from),
1751            importance: meta
1752                .get("importance")
1753                .and_then(|v| v.as_f64())
1754                .unwrap_or(0.5) as f32,
1755            tags: meta
1756                .get("tags")
1757                .and_then(|v| serde_json::from_value(v.clone()).ok())
1758                .unwrap_or_default(),
1759            metadata: meta.get("user_metadata").cloned(),
1760            created_at: meta.get("created_at").and_then(|v| v.as_u64()).unwrap_or(0),
1761            last_accessed_at: meta
1762                .get("last_accessed_at")
1763                .and_then(|v| v.as_u64())
1764                .unwrap_or(0),
1765            access_count: meta
1766                .get("access_count")
1767                .and_then(|v| v.as_u64())
1768                .unwrap_or(0) as u32,
1769            ttl_seconds: vector.ttl_seconds,
1770            expires_at: vector.expires_at,
1771        })
1772    }
1773}
1774
1775/// An agent session
1776#[derive(Debug, Clone, Serialize, Deserialize)]
1777pub struct Session {
1778    pub id: String,
1779    pub agent_id: String,
1780    pub started_at: u64,
1781    #[serde(skip_serializing_if = "Option::is_none")]
1782    pub ended_at: Option<u64>,
1783    #[serde(skip_serializing_if = "Option::is_none")]
1784    pub summary: Option<String>,
1785    #[serde(skip_serializing_if = "Option::is_none")]
1786    pub metadata: Option<serde_json::Value>,
1787    /// Cached count of memories in this session (updated on store/forget)
1788    #[serde(default)]
1789    pub memory_count: usize,
1790}
1791
1792impl Session {
1793    pub fn new(id: String, agent_id: String) -> Self {
1794        let now = std::time::SystemTime::now()
1795            .duration_since(std::time::UNIX_EPOCH)
1796            .unwrap_or_default()
1797            .as_secs();
1798        Self {
1799            id,
1800            agent_id,
1801            started_at: now,
1802            ended_at: None,
1803            summary: None,
1804            metadata: None,
1805            memory_count: 0,
1806        }
1807    }
1808
1809    /// Pack session into metadata for Vector storage
1810    pub fn to_vector_metadata(&self) -> serde_json::Value {
1811        let mut meta = serde_json::Map::new();
1812        meta.insert("_dakera_type".to_string(), serde_json::json!("session"));
1813        meta.insert("agent_id".to_string(), serde_json::json!(self.agent_id));
1814        meta.insert("started_at".to_string(), serde_json::json!(self.started_at));
1815        if let Some(ref ended) = self.ended_at {
1816            meta.insert("ended_at".to_string(), serde_json::json!(ended));
1817        }
1818        if let Some(ref summary) = self.summary {
1819            meta.insert("summary".to_string(), serde_json::json!(summary));
1820        }
1821        if let Some(ref user_meta) = self.metadata {
1822            meta.insert("user_metadata".to_string(), user_meta.clone());
1823        }
1824        meta.insert(
1825            "memory_count".to_string(),
1826            serde_json::json!(self.memory_count),
1827        );
1828        serde_json::Value::Object(meta)
1829    }
1830
1831    /// Convert to a Vector for storage (use summary or agent_id as embedding source)
1832    pub fn to_vector(&self, embedding: Vec<f32>) -> Vector {
1833        Vector {
1834            id: self.id.clone(),
1835            values: embedding,
1836            metadata: Some(self.to_vector_metadata()),
1837            ttl_seconds: None,
1838            expires_at: None,
1839        }
1840    }
1841
1842    /// Reconstruct a Session from a Vector's metadata
1843    pub fn from_vector(vector: &Vector) -> Option<Self> {
1844        let meta = vector.metadata.as_ref()?.as_object()?;
1845        let entry_type = meta.get("_dakera_type")?.as_str()?;
1846        if entry_type != "session" {
1847            return None;
1848        }
1849
1850        Some(Session {
1851            id: vector.id.clone(),
1852            agent_id: meta.get("agent_id")?.as_str()?.to_string(),
1853            started_at: meta.get("started_at").and_then(|v| v.as_u64()).unwrap_or(0),
1854            ended_at: meta.get("ended_at").and_then(|v| v.as_u64()),
1855            summary: meta
1856                .get("summary")
1857                .and_then(|v| v.as_str())
1858                .map(String::from),
1859            metadata: meta.get("user_metadata").cloned(),
1860            memory_count: meta
1861                .get("memory_count")
1862                .and_then(|v| v.as_u64())
1863                .unwrap_or(0) as usize,
1864        })
1865    }
1866}
1867
1868/// Strategy for importance decay
1869#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1870#[serde(rename_all = "snake_case")]
1871#[derive(Default)]
1872pub enum DecayStrategy {
1873    #[default]
1874    Exponential,
1875    Linear,
1876    StepFunction,
1877    /// Power-law decay: I(t) = I₀ / (1 + k·t)^α — natural for episodic memories
1878    PowerLaw,
1879    /// Logarithmic decay: I(t) = I₀ · (1 − log₂(1 + t/h)) — slow for semantic knowledge
1880    Logarithmic,
1881    /// Flat (no decay) — for procedural/skill memories
1882    Flat,
1883}
1884
1885/// Configuration for importance decay
1886#[derive(Debug, Clone, Serialize, Deserialize)]
1887pub struct DecayConfig {
1888    #[serde(default)]
1889    pub strategy: DecayStrategy,
1890    #[serde(default = "default_half_life_hours")]
1891    pub half_life_hours: f64,
1892    #[serde(default = "default_min_importance")]
1893    pub min_importance: f32,
1894}
1895
1896fn default_half_life_hours() -> f64 {
1897    168.0 // 1 week
1898}
1899
1900fn default_min_importance() -> f32 {
1901    0.01
1902}
1903
1904impl Default for DecayConfig {
1905    fn default() -> Self {
1906        Self {
1907            strategy: DecayStrategy::default(),
1908            half_life_hours: default_half_life_hours(),
1909            min_importance: default_min_importance(),
1910        }
1911    }
1912}
1913
1914// ============================================================================
1915// Dakera Memory Request/Response Types
1916// ============================================================================
1917
1918/// Request to store a memory
1919#[derive(Debug, Deserialize)]
1920pub struct StoreMemoryRequest {
1921    pub content: String,
1922    pub agent_id: String,
1923    #[serde(default)]
1924    pub memory_type: MemoryType,
1925    #[serde(skip_serializing_if = "Option::is_none")]
1926    pub session_id: Option<String>,
1927    #[serde(default = "default_importance")]
1928    pub importance: f32,
1929    #[serde(default)]
1930    pub tags: Vec<String>,
1931    #[serde(skip_serializing_if = "Option::is_none")]
1932    pub metadata: Option<serde_json::Value>,
1933    #[serde(skip_serializing_if = "Option::is_none")]
1934    pub ttl_seconds: Option<u64>,
1935    /// Optional explicit expiry Unix timestamp (seconds).
1936    /// If provided, takes precedence over ttl_seconds.
1937    /// On expiry the memory is hard-deleted by the decay engine, bypassing
1938    /// importance scoring.
1939    #[serde(skip_serializing_if = "Option::is_none")]
1940    pub expires_at: Option<u64>,
1941    /// Optional custom ID (auto-generated if not provided)
1942    #[serde(skip_serializing_if = "Option::is_none")]
1943    pub id: Option<String>,
1944}
1945
1946/// Response from storing a memory
1947#[derive(Debug, Serialize)]
1948pub struct StoreMemoryResponse {
1949    pub memory: Memory,
1950    pub embedding_time_ms: u64,
1951}
1952
1953/// CE-12: Routing mode for smart query dispatch.
1954///
1955/// Controls which retrieval backend(s) are used when recalling or searching memories.
1956#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
1957#[serde(rename_all = "lowercase")]
1958pub enum RoutingMode {
1959    /// Automatically select the best backend based on query characteristics (default).
1960    #[default]
1961    Auto,
1962    /// Force pure vector-similarity search (always embeds the query).
1963    Vector,
1964    /// Force pure BM25 full-text search (no embedding inference).
1965    Bm25,
1966    /// Force hybrid search: combine vector + BM25 with adaptive weighting.
1967    Hybrid,
1968}
1969
1970/// CE-14: Fusion strategy for hybrid search results.
1971///
1972/// Selects how vector-similarity and BM25 scores are combined when
1973/// `routing=hybrid` (or auto-classified as hybrid).
1974#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
1975#[serde(rename_all = "lowercase")]
1976pub enum FusionStrategy {
1977    /// Reciprocal Rank Fusion — Cormack et al., SIGIR 2009.
1978    /// Formula: score(d) = Σ 1 / (k + rank_r(d)), k=60.
1979    /// NOTE: RRF penalises memories that appear in only one retriever — catastrophic for
1980    /// temporal recall (A/B v0.11.1: 29.2% temporal vs 42.7% MinMax). Use MinMax instead.
1981    Rrf,
1982    /// Weighted min-max normalisation: combines BM25 and vector scores independently.
1983    /// Default since v0.11.2 — A/B benchmark shows +6.3pp overall, +13.5pp temporal vs RRF.
1984    #[default]
1985    MinMax,
1986}
1987
1988/// Request to recall memories by semantic query
1989#[derive(Debug, Clone, Deserialize)]
1990pub struct RecallRequest {
1991    pub query: String,
1992    pub agent_id: String,
1993    #[serde(default = "default_top_k")]
1994    pub top_k: usize,
1995    #[serde(default)]
1996    pub memory_type: Option<MemoryType>,
1997    #[serde(default)]
1998    pub session_id: Option<String>,
1999    #[serde(default)]
2000    pub tags: Option<Vec<String>>,
2001    #[serde(default)]
2002    pub min_importance: Option<f32>,
2003    /// Include importance-weighted re-ranking (default: true)
2004    #[serde(default = "default_true")]
2005    pub importance_weighted: bool,
2006    /// COG-2: traverse KG depth-1 from recalled memories and include associatively linked memories
2007    #[serde(default)]
2008    pub include_associated: bool,
2009    /// COG-2: max number of associated memories to return (default: 10, max: 10)
2010    #[serde(default)]
2011    pub associated_memories_cap: Option<usize>,
2012    /// CE-7: only include memories created at or after this ISO-8601 timestamp (e.g. "2024-01-01T00:00:00Z")
2013    #[serde(default)]
2014    pub since: Option<String>,
2015    /// CE-7: only include memories created at or before this ISO-8601 timestamp (e.g. "2024-12-31T23:59:59Z")
2016    #[serde(default)]
2017    pub until: Option<String>,
2018    /// KG-3: KG traversal depth for associative recall (1–3, default 1).
2019    /// Requires `include_associated: true`. Depth 1 = direct neighbours only (COG-2 behaviour).
2020    #[serde(default)]
2021    pub associated_memories_depth: Option<u8>,
2022    /// KG-3: minimum edge weight to traverse (0.0–1.0, default 0.0 = all edges).
2023    /// Requires `include_associated: true`.
2024    #[serde(default)]
2025    pub associated_memories_min_weight: Option<f32>,
2026    /// CE-12: retrieval routing mode.
2027    /// `auto` (default) classifies the query heuristically; `vector`/`bm25`/`hybrid`
2028    /// force a specific backend.
2029    #[serde(default)]
2030    pub routing: RoutingMode,
2031    /// CE-13: apply cross-encoder reranking after ANN candidate retrieval.
2032    /// Fetches `top_k * 3` candidates and rescores with `bge-reranker-v2-m3`.
2033    /// Default: `true` (improves recall precision significantly).
2034    #[serde(default = "default_true")]
2035    pub rerank: bool,
2036    /// CE-14: fusion strategy when routing=Hybrid.
2037    /// `rrf` (default) uses Reciprocal Rank Fusion; `minmax` uses weighted min-max normalization.
2038    #[serde(default)]
2039    pub fusion: FusionStrategy,
2040    /// CE-17: explicit vector weight for Hybrid routing (0.0–1.0).
2041    /// When set, overrides the adaptive heuristic from QueryClassifier.
2042    /// Omit to use adaptive defaults (recommended for most callers).
2043    #[serde(default)]
2044    pub vector_weight: Option<f32>,
2045    /// CE-23/CE-49: pseudo-relevance feedback iteration count.
2046    /// When `iterations >= 2`, a second pass is run with the original query augmented
2047    /// by entity/date terms from pass-1 results, merged via RRF (k=60).
2048    /// Default: 1 (single-pass). Max: 3. Ignored for pure vector routing.
2049    /// BM25 routing: auto-enables PRF for temporal queries (CE-35).
2050    /// Hybrid routing: PRF fires for temporal queries (auto) or when iterations >= 2 (CE-49).
2051    #[serde(default)]
2052    pub iterations: Option<u8>,
2053    /// v0.11.0 Phase 2: after main recall, fetch session-adjacent memories within ±5 min
2054    /// of each top result as context enrichment. Default: true.
2055    /// Set to false to disable neighborhood expansion (reduces latency, lower recall).
2056    #[serde(default = "default_true")]
2057    pub neighborhood: bool,
2058}
2059
2060/// Single recall result
2061#[derive(Debug, Serialize, Deserialize)]
2062pub struct RecallResult {
2063    pub memory: Memory,
2064    pub score: f32,
2065    /// Score after importance-weighted re-ranking
2066    #[serde(skip_serializing_if = "Option::is_none")]
2067    pub weighted_score: Option<f32>,
2068    /// Always-on multi-signal smart score (vector + importance + recency + frequency)
2069    #[serde(skip_serializing_if = "Option::is_none")]
2070    pub smart_score: Option<f32>,
2071    /// KG-3: traversal depth at which this memory was found (only set on associated_memories entries).
2072    /// 1 = direct neighbour of a primary result, 2 = two hops, 3 = three hops.
2073    #[serde(skip_serializing_if = "Option::is_none")]
2074    pub depth: Option<u8>,
2075    /// DAK-7414: normalized dense-vector sub-score from hybrid fusion, surfaced for
2076    /// observability and future learned-fusion. Populated only on hybrid-routed results.
2077    #[serde(skip_serializing_if = "Option::is_none")]
2078    pub vector_score: Option<f32>,
2079    /// DAK-7414: normalized full-text (BM25) sub-score from hybrid fusion, surfaced for
2080    /// observability and future learned-fusion. Populated only on hybrid-routed results.
2081    #[serde(skip_serializing_if = "Option::is_none")]
2082    pub text_score: Option<f32>,
2083}
2084
2085/// Response from recall
2086#[derive(Debug, Serialize)]
2087pub struct RecallResponse {
2088    pub memories: Vec<RecallResult>,
2089    pub query_embedding_time_ms: u64,
2090    pub search_time_ms: u64,
2091    /// COG-2: memories linked to recalled memories via KG depth-1 traversal.
2092    /// Only populated when `include_associated: true` in the request.
2093    #[serde(skip_serializing_if = "Option::is_none")]
2094    pub associated_memories: Option<Vec<RecallResult>>,
2095}
2096
2097/// Request to forget (delete) memories
2098#[derive(Debug, Deserialize)]
2099pub struct ForgetRequest {
2100    pub agent_id: String,
2101    #[serde(default)]
2102    pub memory_ids: Option<Vec<String>>,
2103    #[serde(default)]
2104    pub memory_type: Option<MemoryType>,
2105    #[serde(default)]
2106    pub session_id: Option<String>,
2107    #[serde(default)]
2108    pub tags: Option<Vec<String>>,
2109    /// Delete memories below this importance threshold
2110    #[serde(default)]
2111    pub below_importance: Option<f32>,
2112}
2113
2114/// Response from forget
2115#[derive(Debug, Serialize)]
2116pub struct ForgetResponse {
2117    pub deleted_count: usize,
2118}
2119
2120/// Request to update a memory
2121#[derive(Debug, Deserialize)]
2122pub struct UpdateMemoryRequest {
2123    #[serde(default)]
2124    pub content: Option<String>,
2125    #[serde(default)]
2126    pub importance: Option<f32>,
2127    #[serde(default)]
2128    pub tags: Option<Vec<String>>,
2129    #[serde(default)]
2130    pub metadata: Option<serde_json::Value>,
2131    #[serde(default)]
2132    pub memory_type: Option<MemoryType>,
2133}
2134
2135/// Request to update importance of a memory
2136#[derive(Debug, Deserialize)]
2137pub struct UpdateImportanceRequest {
2138    pub memory_id: String,
2139    pub importance: f32,
2140    pub agent_id: String,
2141}
2142
2143/// Request to consolidate related memories
2144#[derive(Debug, Deserialize)]
2145pub struct ConsolidateRequest {
2146    pub agent_id: String,
2147    /// Memory IDs to consolidate (if empty, auto-detect similar memories)
2148    #[serde(default)]
2149    pub memory_ids: Option<Vec<String>>,
2150    /// Similarity threshold for auto-detection (default: 0.85)
2151    #[serde(default = "default_consolidation_threshold")]
2152    pub threshold: f32,
2153    /// Type for the consolidated memory
2154    #[serde(default)]
2155    pub target_type: Option<MemoryType>,
2156}
2157
2158fn default_consolidation_threshold() -> f32 {
2159    0.85
2160}
2161
2162/// Response from consolidation
2163#[derive(Debug, Serialize)]
2164pub struct ConsolidateResponse {
2165    pub consolidated_memory: Memory,
2166    pub source_memory_ids: Vec<String>,
2167    pub memories_removed: usize,
2168}
2169
2170/// Feedback signal for active learning
2171#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2172#[serde(rename_all = "lowercase")]
2173pub enum FeedbackSignal {
2174    /// Boost importance (×1.15, capped at 1.0). INT-1 canonical name.
2175    Upvote,
2176    /// Penalise importance (×0.85, floor 0.0). INT-1 canonical name.
2177    Downvote,
2178    /// Mark as irrelevant — sets `decay_flag=true`, no immediate importance change.
2179    Flag,
2180    /// Backward-compatible alias for `upvote`.
2181    Positive,
2182    /// Backward-compatible alias for `downvote`.
2183    Negative,
2184}
2185
2186/// One recorded feedback event stored in memory metadata (feedback_history).
2187#[derive(Debug, Clone, Serialize, Deserialize)]
2188pub struct FeedbackHistoryEntry {
2189    pub signal: FeedbackSignal,
2190    pub timestamp: u64,
2191    pub old_importance: f32,
2192    pub new_importance: f32,
2193}
2194
2195/// Request to provide feedback on a recalled memory (legacy — body contains memory_id)
2196#[derive(Debug, Deserialize)]
2197pub struct FeedbackRequest {
2198    pub agent_id: String,
2199    pub memory_id: String,
2200    pub signal: FeedbackSignal,
2201}
2202
2203/// Request for `POST /v1/memories/{id}/feedback` (INT-1 — memory_id in path)
2204#[derive(Debug, Deserialize)]
2205pub struct MemoryFeedbackRequest {
2206    pub agent_id: String,
2207    pub signal: FeedbackSignal,
2208}
2209
2210/// Response from feedback
2211#[derive(Debug, Serialize)]
2212pub struct FeedbackResponse {
2213    pub memory_id: String,
2214    pub new_importance: f32,
2215    pub signal: FeedbackSignal,
2216}
2217
2218/// Response from `GET /v1/memories/{id}/feedback`
2219#[derive(Debug, Serialize)]
2220pub struct FeedbackHistoryResponse {
2221    pub memory_id: String,
2222    pub entries: Vec<FeedbackHistoryEntry>,
2223}
2224
2225/// Response from `GET /v1/agents/{id}/feedback/summary`
2226#[derive(Debug, Serialize)]
2227pub struct AgentFeedbackSummary {
2228    pub agent_id: String,
2229    pub upvotes: u64,
2230    pub downvotes: u64,
2231    pub flags: u64,
2232    pub total_feedback: u64,
2233    /// Weighted-average importance across all non-expired memories (0.0–1.0).
2234    pub health_score: f32,
2235}
2236
2237/// Request for `PATCH /v1/memories/{id}/importance` (INT-1 — memory_id in path)
2238#[derive(Debug, Deserialize)]
2239pub struct MemoryImportancePatchRequest {
2240    pub agent_id: String,
2241    pub importance: f32,
2242}
2243
2244/// Query params for `GET /v1/feedback/health`
2245#[derive(Debug, Deserialize)]
2246pub struct FeedbackHealthQuery {
2247    pub agent_id: String,
2248}
2249
2250/// Response from `GET /v1/feedback/health`
2251#[derive(Debug, Serialize)]
2252pub struct FeedbackHealthResponse {
2253    pub agent_id: String,
2254    /// Mean importance of all non-expired memories (0.0–1.0). Higher = healthier.
2255    pub health_score: f32,
2256    pub memory_count: usize,
2257    pub avg_importance: f32,
2258}
2259
2260/// Request for advanced memory search
2261#[derive(Debug, Deserialize)]
2262pub struct SearchMemoriesRequest {
2263    pub agent_id: String,
2264    #[serde(default)]
2265    pub query: Option<String>,
2266    #[serde(default)]
2267    pub memory_type: Option<MemoryType>,
2268    #[serde(default)]
2269    pub session_id: Option<String>,
2270    #[serde(default)]
2271    pub tags: Option<Vec<String>>,
2272    #[serde(default)]
2273    pub min_importance: Option<f32>,
2274    #[serde(default)]
2275    pub max_importance: Option<f32>,
2276    #[serde(default)]
2277    pub created_after: Option<u64>,
2278    #[serde(default)]
2279    pub created_before: Option<u64>,
2280    #[serde(default = "default_top_k")]
2281    pub top_k: usize,
2282    #[serde(default)]
2283    pub sort_by: Option<MemorySortField>,
2284    /// CE-12: retrieval routing mode (auto-detected when not specified).
2285    #[serde(default)]
2286    pub routing: RoutingMode,
2287    /// CE-13: apply cross-encoder reranking on vector/hybrid query results.
2288    /// Default: `false` (search is typically used for browsing, not precision recall).
2289    #[serde(default)]
2290    pub rerank: bool,
2291}
2292
2293/// Fields to sort memories by
2294#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
2295#[serde(rename_all = "snake_case")]
2296pub enum MemorySortField {
2297    CreatedAt,
2298    LastAccessedAt,
2299    Importance,
2300    AccessCount,
2301}
2302
2303/// Response from memory search
2304#[derive(Debug, Serialize)]
2305pub struct SearchMemoriesResponse {
2306    pub memories: Vec<RecallResult>,
2307    pub total_count: usize,
2308}
2309
2310// ============================================================================
2311// Dakera Session Request/Response Types
2312// ============================================================================
2313
2314/// Request to start a session
2315#[derive(Debug, Deserialize)]
2316pub struct SessionStartRequest {
2317    pub agent_id: String,
2318    #[serde(skip_serializing_if = "Option::is_none")]
2319    pub metadata: Option<serde_json::Value>,
2320    /// Optional custom session ID
2321    #[serde(skip_serializing_if = "Option::is_none")]
2322    pub id: Option<String>,
2323}
2324
2325/// Response from starting a session
2326#[derive(Debug, Serialize)]
2327pub struct SessionStartResponse {
2328    pub session: Session,
2329}
2330
2331/// Request to end a session
2332#[derive(Debug, Deserialize)]
2333pub struct SessionEndRequest {
2334    #[serde(default)]
2335    pub summary: Option<String>,
2336    /// Auto-generate summary from session memories
2337    #[serde(default)]
2338    pub auto_summarize: bool,
2339}
2340
2341/// Response from ending a session
2342#[derive(Debug, Serialize)]
2343pub struct SessionEndResponse {
2344    pub session: Session,
2345    pub memory_count: usize,
2346}
2347
2348/// Response listing sessions
2349#[derive(Debug, Serialize)]
2350pub struct ListSessionsResponse {
2351    pub sessions: Vec<Session>,
2352    pub total: usize,
2353}
2354
2355/// Response for session memories
2356#[derive(Debug, Serialize)]
2357pub struct SessionMemoriesResponse {
2358    pub session: Session,
2359    pub memories: Vec<Memory>,
2360    /// Total number of memories in this session (before pagination)
2361    #[serde(skip_serializing_if = "Option::is_none")]
2362    pub total: Option<usize>,
2363}
2364
2365// ============================================================================
2366// Dakera Agent & Knowledge Types
2367// ============================================================================
2368
2369/// Lightweight agent summary for batch listing (uses count() not get_all)
2370#[derive(Debug, Serialize, Deserialize, Clone)]
2371pub struct AgentSummary {
2372    pub agent_id: String,
2373    pub memory_count: usize,
2374    pub session_count: usize,
2375    pub active_sessions: usize,
2376}
2377
2378/// Agent memory statistics
2379#[derive(Debug, Serialize)]
2380pub struct AgentStats {
2381    pub agent_id: String,
2382    pub total_memories: usize,
2383    pub memories_by_type: std::collections::HashMap<String, usize>,
2384    pub total_sessions: usize,
2385    pub active_sessions: usize,
2386    pub avg_importance: f32,
2387    pub oldest_memory_at: Option<u64>,
2388    pub newest_memory_at: Option<u64>,
2389}
2390
2391/// Response from `GET /v1/agents/{agent_id}/wake-up` (DAK-1690).
2392///
2393/// Returns the highest-scored memories for an agent using a pure metadata
2394/// sort (`importance × recency_weight`). No embedding inference is performed,
2395/// making this suitable for fast agent startup context loading.
2396#[derive(Debug, Serialize)]
2397pub struct WakeUpResponse {
2398    pub agent_id: String,
2399    /// Top-N memories sorted by `importance × recency_weight` descending.
2400    pub memories: Vec<Memory>,
2401    /// Total memories available before the top_n cap was applied.
2402    pub total_available: usize,
2403}
2404
2405/// Request for knowledge graph traversal
2406#[derive(Debug, Deserialize)]
2407pub struct KnowledgeGraphRequest {
2408    pub agent_id: String,
2409    pub memory_id: String,
2410    #[serde(default = "default_graph_depth")]
2411    pub depth: usize,
2412    #[serde(default = "default_graph_min_similarity")]
2413    pub min_similarity: f32,
2414}
2415
2416fn default_graph_depth() -> usize {
2417    2
2418}
2419
2420fn default_graph_min_similarity() -> f32 {
2421    0.7
2422}
2423
2424/// Knowledge graph node
2425#[derive(Debug, Serialize)]
2426pub struct KnowledgeGraphNode {
2427    pub memory: Memory,
2428    pub similarity: f32,
2429    pub related: Vec<KnowledgeGraphEdge>,
2430}
2431
2432/// Knowledge graph edge
2433#[derive(Debug, Serialize)]
2434pub struct KnowledgeGraphEdge {
2435    pub memory_id: String,
2436    pub similarity: f32,
2437    pub shared_tags: Vec<String>,
2438}
2439
2440/// Response from knowledge graph query
2441#[derive(Debug, Serialize)]
2442pub struct KnowledgeGraphResponse {
2443    pub root: KnowledgeGraphNode,
2444    pub total_nodes: usize,
2445}
2446
2447// ============================================================================
2448// Full Knowledge Graph Types (Global Network Topology)
2449// ============================================================================
2450
2451fn default_full_graph_max_nodes() -> usize {
2452    200
2453}
2454
2455fn default_full_graph_min_similarity() -> f32 {
2456    0.50
2457}
2458
2459fn default_full_graph_cluster_threshold() -> f32 {
2460    0.60
2461}
2462
2463fn default_full_graph_max_edges_per_node() -> usize {
2464    8
2465}
2466
2467/// Request for full knowledge graph (all memories, pairwise similarity)
2468#[derive(Debug, Deserialize)]
2469pub struct FullKnowledgeGraphRequest {
2470    pub agent_id: String,
2471    #[serde(default = "default_full_graph_max_nodes")]
2472    pub max_nodes: usize,
2473    #[serde(default = "default_full_graph_min_similarity")]
2474    pub min_similarity: f32,
2475    #[serde(default = "default_full_graph_cluster_threshold")]
2476    pub cluster_threshold: f32,
2477    #[serde(default = "default_full_graph_max_edges_per_node")]
2478    pub max_edges_per_node: usize,
2479}
2480
2481/// A node in the full knowledge graph
2482#[derive(Debug, Serialize)]
2483pub struct FullGraphNode {
2484    pub id: String,
2485    pub content: String,
2486    pub memory_type: String,
2487    pub importance: f32,
2488    pub tags: Vec<String>,
2489    pub created_at: Option<String>,
2490    pub cluster_id: usize,
2491    pub centrality: f32,
2492}
2493
2494/// An edge in the full knowledge graph
2495#[derive(Debug, Serialize)]
2496pub struct FullGraphEdge {
2497    pub source: String,
2498    pub target: String,
2499    pub similarity: f32,
2500    pub shared_tags: Vec<String>,
2501}
2502
2503/// A cluster of related memories
2504#[derive(Debug, Serialize)]
2505pub struct GraphCluster {
2506    pub id: usize,
2507    pub node_count: usize,
2508    pub top_tags: Vec<String>,
2509    pub avg_importance: f32,
2510}
2511
2512/// Statistics about the full knowledge graph
2513#[derive(Debug, Serialize)]
2514pub struct GraphStats {
2515    pub total_memories: usize,
2516    pub included_memories: usize,
2517    pub total_edges: usize,
2518    pub cluster_count: usize,
2519    pub density: f32,
2520    pub hub_memory_id: Option<String>,
2521}
2522
2523/// Response from full knowledge graph query
2524#[derive(Debug, Serialize)]
2525pub struct FullKnowledgeGraphResponse {
2526    pub nodes: Vec<FullGraphNode>,
2527    pub edges: Vec<FullGraphEdge>,
2528    pub clusters: Vec<GraphCluster>,
2529    pub stats: GraphStats,
2530}
2531
2532/// Request to summarize memories
2533#[derive(Debug, Deserialize)]
2534pub struct SummarizeRequest {
2535    pub agent_id: String,
2536    pub memory_ids: Vec<String>,
2537    #[serde(default)]
2538    pub target_type: Option<MemoryType>,
2539}
2540
2541/// Response from summarization
2542#[derive(Debug, Serialize)]
2543pub struct SummarizeResponse {
2544    pub summary_memory: Memory,
2545    pub source_count: usize,
2546}
2547
2548/// Request to deduplicate memories
2549#[derive(Debug, Deserialize)]
2550pub struct DeduplicateRequest {
2551    pub agent_id: String,
2552    #[serde(default = "default_dedup_threshold")]
2553    pub threshold: f32,
2554    #[serde(default)]
2555    pub memory_type: Option<MemoryType>,
2556    /// Dry run — report duplicates without merging
2557    #[serde(default)]
2558    pub dry_run: bool,
2559}
2560
2561fn default_dedup_threshold() -> f32 {
2562    0.92
2563}
2564
2565/// A group of duplicate memories
2566#[derive(Debug, Serialize)]
2567pub struct DuplicateGroup {
2568    pub canonical_id: String,
2569    pub duplicate_ids: Vec<String>,
2570    pub avg_similarity: f32,
2571}
2572
2573/// Response from deduplication
2574#[derive(Debug, Serialize)]
2575pub struct DeduplicateResponse {
2576    pub groups: Vec<DuplicateGroup>,
2577    pub duplicates_found: usize,
2578    pub duplicates_merged: usize,
2579}
2580
2581// ============================================================================
2582// Cross-Agent Memory Network Types (DASH-A)
2583// ============================================================================
2584
2585fn default_cross_agent_min_similarity() -> f32 {
2586    0.3
2587}
2588
2589fn default_cross_agent_max_nodes_per_agent() -> usize {
2590    50
2591}
2592
2593fn default_cross_agent_max_cross_edges() -> usize {
2594    200
2595}
2596
2597/// Request for cross-agent memory network graph
2598#[derive(Debug, Deserialize)]
2599pub struct CrossAgentNetworkRequest {
2600    /// Specific agent IDs to include (None = all agents)
2601    #[serde(default)]
2602    pub agent_ids: Option<Vec<String>>,
2603    /// Minimum cosine similarity for a cross-agent edge (default 0.3)
2604    #[serde(default = "default_cross_agent_min_similarity")]
2605    pub min_similarity: f32,
2606    /// Maximum memories per agent to include (top N by importance, default 50)
2607    #[serde(default = "default_cross_agent_max_nodes_per_agent")]
2608    pub max_nodes_per_agent: usize,
2609    /// Minimum importance score for a memory to be included (default 0.0)
2610    #[serde(default)]
2611    pub min_importance: f32,
2612    /// Maximum cross-agent edges to return (default 200)
2613    #[serde(default = "default_cross_agent_max_cross_edges")]
2614    pub max_cross_edges: usize,
2615}
2616
2617/// Summary info for an agent in the cross-agent network
2618#[derive(Debug, Serialize)]
2619pub struct AgentNetworkInfo {
2620    pub agent_id: String,
2621    pub memory_count: usize,
2622    pub avg_importance: f32,
2623}
2624
2625/// A memory node in the cross-agent network (includes agent_id)
2626#[derive(Debug, Serialize)]
2627pub struct AgentNetworkNode {
2628    pub id: String,
2629    pub agent_id: String,
2630    pub content: String,
2631    pub importance: f32,
2632    pub tags: Vec<String>,
2633    pub memory_type: String,
2634    pub created_at: u64,
2635}
2636
2637/// An edge between memories from two different agents
2638#[derive(Debug, Serialize)]
2639pub struct AgentNetworkEdge {
2640    pub source: String,
2641    pub target: String,
2642    pub source_agent: String,
2643    pub target_agent: String,
2644    pub similarity: f32,
2645}
2646
2647/// Statistics for the cross-agent network
2648#[derive(Debug, Serialize)]
2649pub struct AgentNetworkStats {
2650    pub total_agents: usize,
2651    pub total_nodes: usize,
2652    pub total_cross_edges: usize,
2653    pub density: f32,
2654}
2655
2656/// Response from cross-agent network query
2657#[derive(Debug, Serialize)]
2658pub struct CrossAgentNetworkResponse {
2659    pub node_count: usize,
2660    pub agents: Vec<AgentNetworkInfo>,
2661    pub nodes: Vec<AgentNetworkNode>,
2662    pub edges: Vec<AgentNetworkEdge>,
2663    pub stats: AgentNetworkStats,
2664}
2665
2666// ---------------------------------------------------------------------------
2667// CE-2: Batch recall / forget types
2668// ---------------------------------------------------------------------------
2669
2670/// Filter predicates for batch memory operations.
2671///
2672/// At least one field must be set for forget operations (safety guard).
2673#[derive(Debug, Deserialize, Default)]
2674pub struct BatchMemoryFilter {
2675    /// Restrict to memories that carry **all** listed tags.
2676    #[serde(default)]
2677    pub tags: Option<Vec<String>>,
2678    /// Minimum importance (inclusive).
2679    #[serde(default)]
2680    pub min_importance: Option<f32>,
2681    /// Maximum importance (inclusive).
2682    #[serde(default)]
2683    pub max_importance: Option<f32>,
2684    /// Only memories created at or after this Unix timestamp (seconds).
2685    #[serde(default)]
2686    pub created_after: Option<u64>,
2687    /// Only memories created before or at this Unix timestamp (seconds).
2688    #[serde(default)]
2689    pub created_before: Option<u64>,
2690    /// Restrict to a specific memory type.
2691    #[serde(default)]
2692    pub memory_type: Option<MemoryType>,
2693    /// Restrict to memories from a specific session.
2694    #[serde(default)]
2695    pub session_id: Option<String>,
2696}
2697
2698impl BatchMemoryFilter {
2699    /// Returns `true` if the filter has at least one constraint set.
2700    pub fn has_any(&self) -> bool {
2701        self.tags.is_some()
2702            || self.min_importance.is_some()
2703            || self.max_importance.is_some()
2704            || self.created_after.is_some()
2705            || self.created_before.is_some()
2706            || self.memory_type.is_some()
2707            || self.session_id.is_some()
2708    }
2709
2710    /// Returns `true` if the given memory matches all active filter predicates.
2711    pub fn matches(&self, memory: &Memory) -> bool {
2712        if let Some(ref tags) = self.tags {
2713            if !tags.is_empty() && !tags.iter().all(|t| memory.tags.contains(t)) {
2714                return false;
2715            }
2716        }
2717        if let Some(min) = self.min_importance {
2718            if memory.importance < min {
2719                return false;
2720            }
2721        }
2722        if let Some(max) = self.max_importance {
2723            if memory.importance > max {
2724                return false;
2725            }
2726        }
2727        if let Some(after) = self.created_after {
2728            if memory.created_at < after {
2729                return false;
2730            }
2731        }
2732        if let Some(before) = self.created_before {
2733            if memory.created_at > before {
2734                return false;
2735            }
2736        }
2737        if let Some(ref mt) = self.memory_type {
2738            if memory.memory_type != *mt {
2739                return false;
2740            }
2741        }
2742        if let Some(ref sid) = self.session_id {
2743            if memory.session_id.as_ref() != Some(sid) {
2744                return false;
2745            }
2746        }
2747        true
2748    }
2749}
2750
2751/// Request for `POST /v1/memories/recall/batch`
2752#[derive(Debug, Deserialize)]
2753pub struct BatchRecallRequest {
2754    /// Agent whose memory namespace to search.
2755    pub agent_id: String,
2756    /// Filter predicates to apply.
2757    #[serde(default)]
2758    pub filter: BatchMemoryFilter,
2759    /// Maximum number of results to return (default: 100).
2760    #[serde(default = "default_batch_limit")]
2761    pub limit: usize,
2762}
2763
2764fn default_batch_limit() -> usize {
2765    100
2766}
2767
2768/// Response from `POST /v1/memories/recall/batch`
2769#[derive(Debug, Serialize)]
2770pub struct BatchRecallResponse {
2771    pub memories: Vec<Memory>,
2772    pub total: usize,
2773    pub filtered: usize,
2774}
2775
2776/// Request for `DELETE /v1/memories/forget/batch`
2777#[derive(Debug, Deserialize)]
2778pub struct BatchForgetRequest {
2779    /// Agent whose memory namespace to purge from.
2780    pub agent_id: String,
2781    /// Filter predicates — **at least one must be set** (safety guard).
2782    pub filter: BatchMemoryFilter,
2783}
2784
2785/// Response from `DELETE /v1/memories/forget/batch`
2786#[derive(Debug, Serialize)]
2787pub struct BatchForgetResponse {
2788    pub deleted_count: usize,
2789}
2790
2791// ─────────────────────────────────────────────────────────────────────────────
2792// Batch store types
2793// ─────────────────────────────────────────────────────────────────────────────
2794
2795/// A single memory entry within a `BatchStoreMemoryRequest`.
2796///
2797/// Mirrors `StoreMemoryRequest` but omits `agent_id` (supplied at the batch level).
2798#[derive(Debug, Deserialize)]
2799pub struct BatchStoreMemoryItem {
2800    pub content: String,
2801    #[serde(default)]
2802    pub memory_type: MemoryType,
2803    #[serde(skip_serializing_if = "Option::is_none")]
2804    pub session_id: Option<String>,
2805    #[serde(default = "default_importance")]
2806    pub importance: f32,
2807    #[serde(default)]
2808    pub tags: Vec<String>,
2809    #[serde(skip_serializing_if = "Option::is_none")]
2810    pub metadata: Option<serde_json::Value>,
2811    #[serde(skip_serializing_if = "Option::is_none")]
2812    pub ttl_seconds: Option<u64>,
2813    #[serde(skip_serializing_if = "Option::is_none")]
2814    pub expires_at: Option<u64>,
2815    /// Optional custom ID. Auto-generated (unique within the batch) if not provided.
2816    #[serde(skip_serializing_if = "Option::is_none")]
2817    pub id: Option<String>,
2818}
2819
2820/// Request for `POST /v1/memories/store/batch`
2821///
2822/// Accepts up to 1000 memories per call. All memories are embedded in a single
2823/// ONNX inference call and upserted in one storage write, with HNSW invalidation
2824/// happening exactly once at the end — yielding ≥5× throughput vs. N sequential
2825/// single-store calls.
2826#[derive(Debug, Deserialize)]
2827pub struct BatchStoreMemoryRequest {
2828    pub agent_id: String,
2829    pub memories: Vec<BatchStoreMemoryItem>,
2830}
2831
2832/// Response from `POST /v1/memories/store/batch`
2833#[derive(Debug, Serialize)]
2834pub struct BatchStoreMemoryResponse {
2835    pub stored: Vec<Memory>,
2836    pub stored_count: usize,
2837    pub total_embedding_time_ms: u64,
2838}
2839
2840// ─────────────────────────────────────────────────────────────────────────────
2841// CE-4 — Entity extraction types
2842// ─────────────────────────────────────────────────────────────────────────────
2843
2844/// Request to update entity extraction config for a namespace.
2845/// `PATCH /v1/namespaces/{namespace}/config`
2846#[derive(Debug, Deserialize)]
2847pub struct NamespaceEntityConfigRequest {
2848    /// Enable or disable entity extraction for this namespace.
2849    pub extract_entities: bool,
2850    /// Entity types to extract via GLiNER (e.g. ["person","org","location"]).
2851    /// If empty and extract_entities=true, only the rule-based pre-pass runs.
2852    #[serde(default)]
2853    pub entity_types: Vec<String>,
2854}
2855
2856/// Response from `PATCH /v1/namespaces/{namespace}/config`
2857#[derive(Debug, Serialize, Deserialize)]
2858pub struct NamespaceEntityConfigResponse {
2859    pub namespace: String,
2860    pub extract_entities: bool,
2861    pub entity_types: Vec<String>,
2862}
2863
2864/// Request to extract entities from content without storing.
2865/// `POST /v1/memories/extract`
2866#[derive(Debug, Deserialize)]
2867pub struct ExtractEntitiesRequest {
2868    /// Text content to extract entities from.
2869    pub content: String,
2870    /// Entity types for GLiNER inference (optional).
2871    /// If omitted, only the rule-based pre-pass runs.
2872    #[serde(default)]
2873    pub entity_types: Vec<String>,
2874}
2875
2876/// A single extracted entity (shared with inference crate — mirrored here for API types).
2877#[derive(Debug, Clone, Serialize, Deserialize)]
2878pub struct EntityResult {
2879    pub entity_type: String,
2880    pub value: String,
2881    pub score: f32,
2882    pub start: usize,
2883    pub end: usize,
2884    /// Canonical tag form: `entity:<type>:<value>`
2885    pub tag: String,
2886}
2887
2888/// Response from `POST /v1/memories/extract` and `GET /v1/memories/{id}/entities`
2889#[derive(Debug, Serialize)]
2890pub struct ExtractEntitiesResponse {
2891    pub entities: Vec<EntityResult>,
2892    pub count: usize,
2893}
2894
2895// ============================================================================
2896// CE-5: Memory Knowledge Graph — request / response types
2897// ============================================================================
2898
2899/// GET /v1/memories/:id/graph
2900#[derive(Debug, Deserialize)]
2901pub struct GraphTraverseQuery {
2902    /// BFS depth limit (default 3, max 5).
2903    #[serde(default = "default_ce5_graph_depth")]
2904    pub depth: u32,
2905}
2906
2907fn default_ce5_graph_depth() -> u32 {
2908    3
2909}
2910
2911/// GET /v1/memories/:id/path
2912#[derive(Debug, Deserialize)]
2913pub struct GraphPathQuery {
2914    /// Target memory ID.
2915    pub to: String,
2916}
2917
2918/// POST /v1/memories/:id/links — create an explicit edge
2919#[derive(Debug, Deserialize)]
2920pub struct MemoryLinkRequest {
2921    /// The other memory ID to link to.
2922    pub target_id: String,
2923    /// Optional human-readable label (stored as `linked_by` edge).
2924    #[serde(skip_serializing_if = "Option::is_none")]
2925    pub label: Option<String>,
2926    /// Agent ID (for authorization).
2927    pub agent_id: String,
2928}
2929
2930/// Response from graph traversal.
2931#[derive(Debug, Serialize)]
2932pub struct GraphTraverseResponse {
2933    pub root_id: String,
2934    pub depth: u32,
2935    pub node_count: usize,
2936    pub nodes: Vec<GraphNodeResponse>,
2937}
2938
2939/// A single node in a graph traversal response.
2940#[derive(Debug, Serialize)]
2941pub struct GraphNodeResponse {
2942    pub memory_id: String,
2943    pub depth: u32,
2944    pub edges: Vec<GraphEdgeResponse>,
2945}
2946
2947/// A single edge in a graph response.
2948#[derive(Debug, Serialize)]
2949pub struct GraphEdgeResponse {
2950    pub from_id: String,
2951    pub to_id: String,
2952    pub edge_type: String,
2953    pub weight: f32,
2954    pub created_at: u64,
2955}
2956
2957/// Response from shortest-path query.
2958#[derive(Debug, Serialize)]
2959pub struct GraphPathResponse {
2960    pub from_id: String,
2961    pub to_id: String,
2962    /// Ordered list of memory IDs along the shortest path (inclusive).
2963    pub path: Vec<String>,
2964    pub hop_count: usize,
2965}
2966
2967/// Response from explicit link creation.
2968#[derive(Debug, Serialize)]
2969pub struct MemoryLinkResponse {
2970    pub from_id: String,
2971    pub to_id: String,
2972    pub edge_type: String,
2973}
2974
2975/// Response from agent graph export.
2976#[derive(Debug, Serialize)]
2977pub struct GraphExportResponse {
2978    pub agent_id: String,
2979    pub namespace: String,
2980    pub node_count: usize,
2981    pub edge_count: usize,
2982    pub edges: Vec<GraphEdgeResponse>,
2983}
2984
2985// ============================================================================
2986// KG-2: Graph Query & Export — request / response types
2987// ============================================================================
2988
2989/// GET /v1/knowledge/query — JSON DSL for graph filtering/traversal
2990#[derive(Debug, Deserialize)]
2991pub struct KgQueryParams {
2992    /// Agent ID whose graph to query (required).
2993    pub agent_id: String,
2994    /// Optional root memory ID — if set, performs BFS from this node first.
2995    #[serde(default)]
2996    pub root_id: Option<String>,
2997    /// Filter edges by type (comma-separated, e.g. "related_to,shares_entity").
2998    #[serde(default)]
2999    pub edge_type: Option<String>,
3000    /// Minimum edge weight (0.0–1.0).
3001    #[serde(default)]
3002    pub min_weight: Option<f32>,
3003    /// BFS depth when root_id is set (1–5, default 3).
3004    #[serde(default = "default_kg_depth")]
3005    pub max_depth: u32,
3006    /// Maximum number of edges to return (default 100, max 1000).
3007    #[serde(default = "default_kg_limit")]
3008    pub limit: usize,
3009}
3010
3011fn default_kg_depth() -> u32 {
3012    3
3013}
3014
3015fn default_kg_limit() -> usize {
3016    100
3017}
3018
3019/// Response from GET /v1/knowledge/query
3020#[derive(Debug, Serialize)]
3021pub struct KgQueryResponse {
3022    pub agent_id: String,
3023    pub node_count: usize,
3024    pub edge_count: usize,
3025    pub edges: Vec<GraphEdgeResponse>,
3026}
3027
3028/// GET /v1/knowledge/path — shortest path between two memory IDs
3029#[derive(Debug, Deserialize)]
3030pub struct KgPathParams {
3031    /// Agent ID for authorization.
3032    pub agent_id: String,
3033    /// Source memory ID.
3034    pub from: String,
3035    /// Target memory ID.
3036    pub to: String,
3037}
3038
3039/// Response from GET /v1/knowledge/path
3040#[derive(Debug, Serialize)]
3041pub struct KgPathResponse {
3042    pub agent_id: String,
3043    pub from_id: String,
3044    pub to_id: String,
3045    pub hop_count: usize,
3046    pub path: Vec<String>,
3047}
3048
3049/// GET /v1/knowledge/export — export graph as JSON or GraphML
3050#[derive(Debug, Deserialize)]
3051pub struct KgExportParams {
3052    /// Agent ID whose graph to export.
3053    pub agent_id: String,
3054    /// Export format: "json" (default) or "graphml".
3055    #[serde(default = "default_kg_format")]
3056    pub format: String,
3057}
3058
3059fn default_kg_format() -> String {
3060    "json".to_string()
3061}
3062
3063/// Response from GET /v1/knowledge/export (format=json)
3064#[derive(Debug, Serialize)]
3065pub struct KgExportJsonResponse {
3066    pub agent_id: String,
3067    pub format: String,
3068    pub node_count: usize,
3069    pub edge_count: usize,
3070    pub edges: Vec<GraphEdgeResponse>,
3071}
3072
3073// ============================================================================
3074// COG-1: Cognitive Memory Lifecycle — per-namespace memory policy
3075// ============================================================================
3076
3077fn default_working_ttl() -> Option<u64> {
3078    Some(14_400) // 4 hours
3079}
3080fn default_episodic_ttl() -> Option<u64> {
3081    Some(2_592_000) // 30 days
3082}
3083fn default_semantic_ttl() -> Option<u64> {
3084    Some(31_536_000) // 365 days
3085}
3086fn default_procedural_ttl() -> Option<u64> {
3087    Some(63_072_000) // 730 days
3088}
3089fn default_working_decay() -> DecayStrategy {
3090    DecayStrategy::Exponential
3091}
3092fn default_episodic_decay() -> DecayStrategy {
3093    DecayStrategy::PowerLaw
3094}
3095fn default_semantic_decay() -> DecayStrategy {
3096    DecayStrategy::Logarithmic
3097}
3098fn default_procedural_decay() -> DecayStrategy {
3099    DecayStrategy::Flat
3100}
3101fn default_sr_factor() -> f64 {
3102    1.0
3103}
3104fn default_sr_base_interval() -> u64 {
3105    86_400 // 1 day
3106}
3107fn default_consolidation_enabled() -> bool {
3108    false
3109}
3110fn default_policy_consolidation_threshold() -> f32 {
3111    0.92
3112}
3113fn default_consolidation_interval_hours() -> u32 {
3114    24
3115}
3116fn default_store_dedup_threshold() -> f32 {
3117    0.95
3118}
3119
3120/// Per-namespace memory lifecycle policy (COG-1).
3121///
3122/// Controls type-specific TTLs, decay curves, and spaced repetition behaviour.
3123/// All fields have sensible defaults; only override what you need.
3124#[derive(Debug, Clone, Serialize, Deserialize)]
3125pub struct MemoryPolicy {
3126    // ── Differential TTLs ────────────────────────────────────────────────────
3127    /// Default TTL for `working` memories in seconds (default: 4 h = 14 400 s).
3128    #[serde(
3129        default = "default_working_ttl",
3130        skip_serializing_if = "Option::is_none"
3131    )]
3132    pub working_ttl_seconds: Option<u64>,
3133    /// Default TTL for `episodic` memories in seconds (default: 30 d = 2 592 000 s).
3134    #[serde(
3135        default = "default_episodic_ttl",
3136        skip_serializing_if = "Option::is_none"
3137    )]
3138    pub episodic_ttl_seconds: Option<u64>,
3139    /// Default TTL for `semantic` memories in seconds (default: 365 d = 31 536 000 s).
3140    #[serde(
3141        default = "default_semantic_ttl",
3142        skip_serializing_if = "Option::is_none"
3143    )]
3144    pub semantic_ttl_seconds: Option<u64>,
3145    /// Default TTL for `procedural` memories in seconds (default: 730 d = 63 072 000 s).
3146    #[serde(
3147        default = "default_procedural_ttl",
3148        skip_serializing_if = "Option::is_none"
3149    )]
3150    pub procedural_ttl_seconds: Option<u64>,
3151
3152    // ── Decay curves ─────────────────────────────────────────────────────────
3153    /// Decay strategy for `working` memories (default: exponential).
3154    #[serde(default = "default_working_decay")]
3155    pub working_decay: DecayStrategy,
3156    /// Decay strategy for `episodic` memories (default: power_law).
3157    #[serde(default = "default_episodic_decay")]
3158    pub episodic_decay: DecayStrategy,
3159    /// Decay strategy for `semantic` memories (default: logarithmic).
3160    #[serde(default = "default_semantic_decay")]
3161    pub semantic_decay: DecayStrategy,
3162    /// Decay strategy for `procedural` memories (default: flat — no decay).
3163    #[serde(default = "default_procedural_decay")]
3164    pub procedural_decay: DecayStrategy,
3165
3166    // ── Spaced repetition ────────────────────────────────────────────────────
3167    /// Multiplier applied to the TTL extension on each recall.
3168    /// Extension = `access_count × sr_factor × sr_base_interval_seconds`.
3169    /// Set to 0.0 to disable spaced repetition. (default: 1.0)
3170    #[serde(default = "default_sr_factor")]
3171    pub spaced_repetition_factor: f64,
3172    /// Base interval in seconds for spaced repetition TTL extension (default: 86 400 = 1 day).
3173    #[serde(default = "default_sr_base_interval")]
3174    pub spaced_repetition_base_interval_seconds: u64,
3175
3176    // ── COG-3: Proactive consolidation ───────────────────────────────────────
3177    /// Enable background deduplication of semantically similar memories (default: false).
3178    #[serde(default = "default_consolidation_enabled")]
3179    pub consolidation_enabled: bool,
3180    /// Cosine-similarity threshold for merging memories (default: 0.92, range 0.85–0.99).
3181    #[serde(default = "default_policy_consolidation_threshold")]
3182    pub consolidation_threshold: f32,
3183    /// How often the background consolidation job runs, in hours (default: 24).
3184    #[serde(default = "default_consolidation_interval_hours")]
3185    pub consolidation_interval_hours: u32,
3186    /// Total number of memories merged since namespace creation (read-only).
3187    #[serde(default)]
3188    pub consolidated_count: u64,
3189
3190    // ── SEC-5: Per-namespace rate limiting ───────────────────────────────────
3191    /// Master rate-limit switch (default: false — opt-in to avoid breaking existing clients).
3192    /// Set to `true` to enforce `rate_limit_stores_per_minute` / `rate_limit_recalls_per_minute`.
3193    #[serde(default)]
3194    pub rate_limit_enabled: bool,
3195    /// Maximum `POST /v1/memory/store` operations per minute for this namespace.
3196    /// `None` = unlimited. Only enforced when `rate_limit_enabled = true`.
3197    #[serde(default, skip_serializing_if = "Option::is_none")]
3198    pub rate_limit_stores_per_minute: Option<u32>,
3199    /// Maximum `POST /v1/memory/recall` operations per minute for this namespace.
3200    /// `None` = unlimited. Only enforced when `rate_limit_enabled = true`.
3201    #[serde(default, skip_serializing_if = "Option::is_none")]
3202    pub rate_limit_recalls_per_minute: Option<u32>,
3203
3204    // ── CE-10a: Store-time deduplication ─────────────────────────────────────
3205    /// Enable near-duplicate detection on every `store` call (default: false).
3206    ///
3207    /// When enabled, a quick vector-search (top-1) runs after embedding; if the
3208    /// nearest neighbour has cosine similarity ≥ 0.95 the new store is rejected
3209    /// and the existing memory ID is returned instead.  Adds one ANN query to
3210    /// every store operation — keep disabled for high-throughput namespaces.
3211    #[serde(default)]
3212    pub dedup_on_store: bool,
3213    /// Similarity threshold for store-time deduplication (default: 0.95).
3214    #[serde(default = "default_store_dedup_threshold")]
3215    pub dedup_threshold: f32,
3216}
3217
3218impl Default for MemoryPolicy {
3219    fn default() -> Self {
3220        Self {
3221            working_ttl_seconds: default_working_ttl(),
3222            episodic_ttl_seconds: default_episodic_ttl(),
3223            semantic_ttl_seconds: default_semantic_ttl(),
3224            procedural_ttl_seconds: default_procedural_ttl(),
3225            working_decay: default_working_decay(),
3226            episodic_decay: default_episodic_decay(),
3227            semantic_decay: default_semantic_decay(),
3228            procedural_decay: default_procedural_decay(),
3229            spaced_repetition_factor: default_sr_factor(),
3230            spaced_repetition_base_interval_seconds: default_sr_base_interval(),
3231            consolidation_enabled: default_consolidation_enabled(),
3232            consolidation_threshold: default_policy_consolidation_threshold(),
3233            consolidation_interval_hours: default_consolidation_interval_hours(),
3234            consolidated_count: 0,
3235            rate_limit_enabled: false,
3236            rate_limit_stores_per_minute: None,
3237            rate_limit_recalls_per_minute: None,
3238            dedup_on_store: false,
3239            dedup_threshold: default_store_dedup_threshold(),
3240        }
3241    }
3242}
3243
3244impl MemoryPolicy {
3245    /// Return the configured TTL for the given memory type, in seconds.
3246    pub fn ttl_for_type(&self, memory_type: &MemoryType) -> Option<u64> {
3247        match memory_type {
3248            MemoryType::Working => self.working_ttl_seconds,
3249            MemoryType::Episodic => self.episodic_ttl_seconds,
3250            MemoryType::Semantic => self.semantic_ttl_seconds,
3251            MemoryType::Procedural => self.procedural_ttl_seconds,
3252        }
3253    }
3254
3255    /// Return the configured decay strategy for the given memory type.
3256    pub fn decay_for_type(&self, memory_type: &MemoryType) -> DecayStrategy {
3257        match memory_type {
3258            MemoryType::Working => self.working_decay,
3259            MemoryType::Episodic => self.episodic_decay,
3260            MemoryType::Semantic => self.semantic_decay,
3261            MemoryType::Procedural => self.procedural_decay,
3262        }
3263    }
3264
3265    /// Compute the spaced repetition TTL extension in seconds.
3266    ///
3267    /// `extension = access_count × sr_factor × sr_base_interval_seconds`
3268    pub fn spaced_repetition_extension(&self, access_count: u32) -> u64 {
3269        if self.spaced_repetition_factor <= 0.0 {
3270            return 0;
3271        }
3272        let ext = access_count as f64
3273            * self.spaced_repetition_factor
3274            * self.spaced_repetition_base_interval_seconds as f64;
3275        ext.round() as u64
3276    }
3277}
3278
3279#[cfg(test)]
3280mod tests {
3281    use super::*;
3282
3283    // DAK-7428: a valid rank_by parses; a malformed one now errors instead of
3284    // silently defaulting to id-ascending (fail loud at the request boundary).
3285    #[test]
3286    fn test_rank_by_input_valid_parses() {
3287        let json = serde_json::json!(["timestamp", "desc"]);
3288        let parsed: RankByInput = serde_json::from_value(json).expect("valid rank_by must parse");
3289        assert!(matches!(parsed.0, RankBy::AttributeOrder { .. }));
3290    }
3291
3292    #[test]
3293    fn test_rank_by_input_malformed_errors() {
3294        // Previously this silently became AttributeOrder{id, Asc}; now it is a
3295        // hard deserialization error the API surfaces as a 400.
3296        let json = serde_json::json!({"not": "a valid rank_by"});
3297        let result: Result<RankByInput, _> = serde_json::from_value(json);
3298        assert!(
3299            result.is_err(),
3300            "malformed rank_by must error, not silently default"
3301        );
3302    }
3303
3304    // ── Memory round-trip ────────────────────────────────────────────────────
3305
3306    #[test]
3307    fn test_memory_to_vector_from_vector_roundtrip() {
3308        let memory = Memory {
3309            id: "mem_abc123".to_string(),
3310            memory_type: MemoryType::Episodic,
3311            content: "Test content".to_string(),
3312            agent_id: "test-agent".to_string(),
3313            session_id: Some("sess_001".to_string()),
3314            importance: 0.8,
3315            tags: vec!["tag1".to_string(), "tag2".to_string()],
3316            metadata: Some(serde_json::json!({"key": "value"})),
3317            created_at: 1_700_000_000,
3318            last_accessed_at: 1_700_001_000,
3319            access_count: 5,
3320            ttl_seconds: None,
3321            expires_at: None,
3322        };
3323
3324        let embedding = vec![0.1f32, 0.2, 0.3];
3325        let vector = memory.to_vector(embedding.clone());
3326        assert_eq!(vector.id, memory.id);
3327        assert_eq!(vector.values, embedding);
3328
3329        let recovered =
3330            Memory::from_vector(&vector).expect("should reconstruct memory from vector");
3331        assert_eq!(recovered.id, memory.id);
3332        assert_eq!(recovered.content, memory.content);
3333        assert_eq!(recovered.agent_id, memory.agent_id);
3334        assert_eq!(recovered.session_id, memory.session_id);
3335        assert_eq!(recovered.tags, memory.tags);
3336        assert_eq!(recovered.access_count, memory.access_count);
3337        assert_eq!(recovered.created_at, memory.created_at);
3338        assert_eq!(recovered.last_accessed_at, memory.last_accessed_at);
3339        // importance round-trips through JSON f64 → f32; allow tiny epsilon
3340        assert!(
3341            (recovered.importance - memory.importance).abs() < 1e-5,
3342            "importance mismatch: {} vs {}",
3343            recovered.importance,
3344            memory.importance
3345        );
3346    }
3347
3348    #[test]
3349    fn test_memory_from_vector_rejects_session_type() {
3350        let mut meta = serde_json::Map::new();
3351        meta.insert("_dakera_type".to_string(), serde_json::json!("session"));
3352        let vector = Vector {
3353            id: "v1".to_string(),
3354            values: vec![],
3355            metadata: Some(serde_json::Value::Object(meta)),
3356            ttl_seconds: None,
3357            expires_at: None,
3358        };
3359        assert!(
3360            Memory::from_vector(&vector).is_none(),
3361            "from_vector should return None for wrong _dakera_type"
3362        );
3363    }
3364
3365    #[test]
3366    fn test_memory_from_vector_rejects_missing_metadata() {
3367        let vector = Vector {
3368            id: "v1".to_string(),
3369            values: vec![],
3370            metadata: None,
3371            ttl_seconds: None,
3372            expires_at: None,
3373        };
3374        assert!(Memory::from_vector(&vector).is_none());
3375    }
3376
3377    // ── Session round-trip ───────────────────────────────────────────────────
3378
3379    #[test]
3380    fn test_session_to_vector_from_vector_roundtrip() {
3381        let mut session = Session::new("sess_xyz".to_string(), "agent-007".to_string());
3382        session.ended_at = Some(1_700_005_000);
3383        session.summary = Some("Session ended cleanly".to_string());
3384        session.memory_count = 42;
3385
3386        let embedding = vec![0.5f32, 0.5, 0.5];
3387        let vector = session.to_vector(embedding.clone());
3388        assert_eq!(vector.id, session.id);
3389        assert_eq!(vector.values, embedding);
3390
3391        let recovered = Session::from_vector(&vector).expect("should reconstruct session");
3392        assert_eq!(recovered.id, session.id);
3393        assert_eq!(recovered.agent_id, session.agent_id);
3394        assert_eq!(recovered.ended_at, session.ended_at);
3395        assert_eq!(recovered.summary, session.summary);
3396        assert_eq!(recovered.memory_count, session.memory_count);
3397    }
3398
3399    #[test]
3400    fn test_session_from_vector_rejects_memory_type() {
3401        let mut meta = serde_json::Map::new();
3402        meta.insert("_dakera_type".to_string(), serde_json::json!("memory"));
3403        let vector = Vector {
3404            id: "sess_1".to_string(),
3405            values: vec![],
3406            metadata: Some(serde_json::Value::Object(meta)),
3407            ttl_seconds: None,
3408            expires_at: None,
3409        };
3410        assert!(
3411            Session::from_vector(&vector).is_none(),
3412            "from_vector should return None for wrong _dakera_type"
3413        );
3414    }
3415
3416    #[test]
3417    fn test_session_active_has_no_ended_at() {
3418        let session = Session::new("sess_active".to_string(), "agent-1".to_string());
3419        let vector = session.to_vector(vec![0.1]);
3420        let recovered = Session::from_vector(&vector).unwrap();
3421        assert!(recovered.ended_at.is_none());
3422        assert_eq!(recovered.memory_count, 0);
3423    }
3424
3425    // ── PaginationCursor ─────────────────────────────────────────────────────
3426
3427    #[test]
3428    fn test_pagination_cursor_encode_decode_roundtrip() {
3429        let cursor = PaginationCursor::new(0.75, "mem_abc".to_string());
3430        let encoded = cursor.encode();
3431        let decoded = PaginationCursor::decode(&encoded).expect("should decode valid cursor");
3432        assert!(
3433            (decoded.last_score - cursor.last_score).abs() < 1e-6,
3434            "score mismatch after decode"
3435        );
3436        assert_eq!(decoded.last_id, cursor.last_id);
3437    }
3438
3439    #[test]
3440    fn test_pagination_cursor_decode_invalid_returns_none() {
3441        assert!(PaginationCursor::decode("not-valid-base64!!!").is_none());
3442        assert!(PaginationCursor::decode("").is_none());
3443        // Valid base64 but not valid JSON cursor
3444        assert!(PaginationCursor::decode("aGVsbG8=").is_none()); // "hello"
3445    }
3446
3447    // ── DistanceMetric serde ─────────────────────────────────────────────────
3448
3449    #[test]
3450    fn test_distance_metric_serde_round_trip() {
3451        let cases = [
3452            (DistanceMetric::Cosine, "\"cosine\""),
3453            (DistanceMetric::Euclidean, "\"euclidean\""),
3454            (DistanceMetric::DotProduct, "\"dot_product\""),
3455        ];
3456        for (metric, expected_json) in &cases {
3457            let serialized = serde_json::to_string(metric).unwrap();
3458            assert_eq!(
3459                &serialized, expected_json,
3460                "serialized form mismatch for {:?}",
3461                metric
3462            );
3463            let deserialized: DistanceMetric = serde_json::from_str(&serialized).unwrap();
3464            assert_eq!(*metric, deserialized);
3465        }
3466    }
3467
3468    // ── Vector TTL helpers ───────────────────────────────────────────────────
3469
3470    #[test]
3471    fn test_vector_is_expired_at_not_yet_expired() {
3472        let vector = Vector {
3473            id: "v1".to_string(),
3474            values: vec![1.0],
3475            metadata: None,
3476            ttl_seconds: None,
3477            expires_at: Some(u64::MAX),
3478        };
3479        assert!(!vector.is_expired_at(0));
3480        assert!(!vector.is_expired_at(1_000_000));
3481    }
3482
3483    #[test]
3484    fn test_vector_is_expired_at_past_expiry() {
3485        let vector = Vector {
3486            id: "v1".to_string(),
3487            values: vec![1.0],
3488            metadata: None,
3489            ttl_seconds: None,
3490            expires_at: Some(100),
3491        };
3492        assert!(vector.is_expired_at(100), "at boundary should be expired");
3493        assert!(vector.is_expired_at(200));
3494        assert!(!vector.is_expired_at(99));
3495    }
3496
3497    #[test]
3498    fn test_vector_no_expiry_never_expires() {
3499        let vector = Vector {
3500            id: "v1".to_string(),
3501            values: vec![1.0],
3502            metadata: None,
3503            ttl_seconds: None,
3504            expires_at: None,
3505        };
3506        assert!(!vector.is_expired_at(u64::MAX));
3507    }
3508
3509    // ── ColumnUpsertRequest::to_vectors ──────────────────────────────────────
3510
3511    #[test]
3512    fn test_column_upsert_mismatched_vectors_length_errors() {
3513        let req = ColumnUpsertRequest {
3514            ids: vec!["a".to_string(), "b".to_string()],
3515            vectors: vec![vec![1.0, 2.0]], // only 1 vector for 2 ids
3516            attributes: Default::default(),
3517            ttl_seconds: None,
3518            dimension: None,
3519        };
3520        assert!(
3521            req.to_vectors().is_err(),
3522            "should error when vectors.len() != ids.len()"
3523        );
3524    }
3525
3526    #[test]
3527    fn test_column_upsert_mismatched_attribute_length_errors() {
3528        let mut attrs = std::collections::HashMap::new();
3529        attrs.insert(
3530            "score".to_string(),
3531            vec![serde_json::json!(1.0)], // only 1 value for 2 ids
3532        );
3533        let req = ColumnUpsertRequest {
3534            ids: vec!["a".to_string(), "b".to_string()],
3535            vectors: vec![vec![1.0], vec![2.0]],
3536            attributes: attrs,
3537            ttl_seconds: None,
3538            dimension: None,
3539        };
3540        assert!(
3541            req.to_vectors().is_err(),
3542            "should error when attribute array length != ids.len()"
3543        );
3544    }
3545
3546    #[test]
3547    fn test_column_upsert_mismatched_dimension_errors() {
3548        let req = ColumnUpsertRequest {
3549            ids: vec!["a".to_string(), "b".to_string()],
3550            vectors: vec![vec![1.0, 2.0], vec![1.0]], // second vector has dim 1, first has dim 2
3551            attributes: Default::default(),
3552            ttl_seconds: None,
3553            dimension: None,
3554        };
3555        assert!(
3556            req.to_vectors().is_err(),
3557            "should error on dimension mismatch between vectors"
3558        );
3559    }
3560
3561    #[test]
3562    fn test_column_upsert_success() {
3563        let req = ColumnUpsertRequest {
3564            ids: vec!["a".to_string(), "b".to_string()],
3565            vectors: vec![vec![1.0, 0.0], vec![0.0, 1.0]],
3566            attributes: Default::default(),
3567            ttl_seconds: None,
3568            dimension: Some(2),
3569        };
3570        let result = req.to_vectors().expect("valid request should succeed");
3571        assert_eq!(result.len(), 2);
3572        assert_eq!(result[0].id, "a");
3573        assert_eq!(result[1].id, "b");
3574        assert_eq!(result[0].values, vec![1.0, 0.0]);
3575        assert_eq!(result[1].values, vec![0.0, 1.0]);
3576    }
3577
3578    #[test]
3579    fn test_to_vector_metadata_surfaces_content_date() {
3580        let mut memory = Memory::new(
3581            "m1".to_string(),
3582            "test content".to_string(),
3583            "agent1".to_string(),
3584            MemoryType::Episodic,
3585        );
3586        let mut meta = serde_json::Map::new();
3587        meta.insert(
3588            "_dakera_content_date".to_string(),
3589            serde_json::json!(1625097600_i64),
3590        );
3591        memory.metadata = Some(serde_json::Value::Object(meta));
3592
3593        let vec_meta = memory.to_vector_metadata();
3594        let obj = vec_meta.as_object().unwrap();
3595        assert_eq!(
3596            obj.get("_dakera_content_date"),
3597            Some(&serde_json::json!(1625097600_i64))
3598        );
3599        assert!(obj
3600            .get("user_metadata")
3601            .unwrap()
3602            .get("_dakera_content_date")
3603            .is_some());
3604    }
3605
3606    #[test]
3607    fn test_to_vector_metadata_no_content_date_when_absent() {
3608        let memory = Memory::new(
3609            "m1".to_string(),
3610            "test content".to_string(),
3611            "agent1".to_string(),
3612            MemoryType::Episodic,
3613        );
3614        let vec_meta = memory.to_vector_metadata();
3615        let obj = vec_meta.as_object().unwrap();
3616        assert!(obj.get("_dakera_content_date").is_none());
3617    }
3618}