Skip to main content

dakera_client/
memory.rs

1//! Memory-oriented client methods for Dakera AI Agent Memory Platform
2//!
3//! Provides high-level methods for storing, recalling, and managing
4//! agent memories and sessions through the Dakera API.
5
6use serde::{Deserialize, Serialize};
7
8use crate::error::Result;
9use crate::types::{
10    AgentFeedbackSummary, EdgeType, FeedbackHealthResponse, FeedbackHistoryResponse,
11    FeedbackResponse, FeedbackSignal, GraphExport, GraphLinkRequest, GraphLinkResponse,
12    GraphOptions, GraphPath, MemoryFeedbackBody, MemoryGraph, MemoryImportancePatch, TifScore,
13};
14use crate::DakeraClient;
15
16// ============================================================================
17// Memory Types (client-side)
18// ============================================================================
19
20/// Memory type classification
21#[derive(Debug, Clone, Serialize, Deserialize, Default)]
22#[serde(rename_all = "lowercase")]
23pub enum MemoryType {
24    #[default]
25    Episodic,
26    Semantic,
27    Procedural,
28    Working,
29}
30
31/// Store a memory request
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct StoreMemoryRequest {
34    pub agent_id: String,
35    pub content: String,
36    #[serde(default)]
37    pub memory_type: MemoryType,
38    #[serde(default = "default_importance")]
39    pub importance: f32,
40    #[serde(default)]
41    pub tags: Vec<String>,
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub session_id: Option<String>,
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub metadata: Option<serde_json::Value>,
46    /// Optional TTL in seconds. The memory is hard-deleted after this many
47    /// seconds from creation.
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub ttl_seconds: Option<u64>,
50    /// Optional explicit expiry as a Unix timestamp (seconds). Takes precedence
51    /// over `ttl_seconds` when both are set. The memory is hard-deleted by the
52    /// decay engine on expiry (DECAY-3).
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub expires_at: Option<u64>,
55    /// Bi-temporal validity start — Unix timestamp (seconds) indicating when this
56    /// memory becomes temporally valid. Defaults to ingest time when omitted.
57    /// Used by temporal recall queries (server v0.11.98+, DAK-7424).
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub valid_from: Option<i64>,
60}
61
62fn default_importance() -> f32 {
63    0.5
64}
65
66impl StoreMemoryRequest {
67    /// Create a new store memory request
68    pub fn new(agent_id: impl Into<String>, content: impl Into<String>) -> Self {
69        Self {
70            agent_id: agent_id.into(),
71            content: content.into(),
72            memory_type: MemoryType::default(),
73            importance: 0.5,
74            tags: Vec::new(),
75            session_id: None,
76            metadata: None,
77            ttl_seconds: None,
78            expires_at: None,
79            valid_from: None,
80        }
81    }
82
83    /// Set memory type
84    pub fn with_type(mut self, memory_type: MemoryType) -> Self {
85        self.memory_type = memory_type;
86        self
87    }
88
89    /// Set importance score
90    pub fn with_importance(mut self, importance: f32) -> Self {
91        self.importance = importance.clamp(0.0, 1.0);
92        self
93    }
94
95    /// Set tags
96    pub fn with_tags(mut self, tags: Vec<String>) -> Self {
97        self.tags = tags;
98        self
99    }
100
101    /// Set session ID
102    pub fn with_session(mut self, session_id: impl Into<String>) -> Self {
103        self.session_id = Some(session_id.into());
104        self
105    }
106
107    /// Set metadata
108    pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
109        self.metadata = Some(metadata);
110        self
111    }
112
113    /// Set TTL in seconds. The memory is hard-deleted after this many seconds
114    /// from creation.
115    pub fn with_ttl(mut self, ttl_seconds: u64) -> Self {
116        self.ttl_seconds = Some(ttl_seconds);
117        self
118    }
119
120    /// Set an explicit expiry Unix timestamp (seconds). Takes precedence over
121    /// `ttl_seconds` when both are set (DECAY-3).
122    pub fn with_expires_at(mut self, expires_at: u64) -> Self {
123        self.expires_at = Some(expires_at);
124        self
125    }
126
127    /// Set bi-temporal validity start as a Unix timestamp (seconds).
128    /// Defaults to ingest time when omitted (server v0.11.98+, DAK-7424).
129    pub fn with_valid_from(mut self, valid_from: i64) -> Self {
130        self.valid_from = Some(valid_from);
131        self
132    }
133}
134
135/// Stored memory response from `POST /v1/memory/store`.
136///
137/// The server wraps the memory in a nested `memory` object:
138/// `{"memory": {"id": "...", "agent_id": "...", ...}, "embedding_time_ms": N}`.
139/// The `memory_id` and `agent_id` fields are convenience accessors mapped from
140/// `memory.id` and `memory.agent_id` respectively.
141#[derive(Debug, Clone, Serialize)]
142pub struct StoreMemoryResponse {
143    /// Memory ID (mapped from `memory.id`)
144    pub memory_id: String,
145    /// Agent ID (mapped from `memory.agent_id`)
146    pub agent_id: String,
147    /// Namespace (mapped from `memory.namespace`, defaults to `"default"`)
148    pub namespace: String,
149    /// Embedding latency in milliseconds
150    pub embedding_time_ms: Option<u64>,
151}
152
153impl<'de> serde::Deserialize<'de> for StoreMemoryResponse {
154    fn deserialize<D: serde::Deserializer<'de>>(
155        deserializer: D,
156    ) -> std::result::Result<Self, D::Error> {
157        use serde::de::Error;
158        let val = serde_json::Value::deserialize(deserializer)?;
159
160        // Server response: {"memory": {"id":"...","agent_id":"...",...}, "embedding_time_ms": N}
161        if let Some(memory) = val.get("memory") {
162            let memory_id = memory
163                .get("id")
164                .and_then(|v| v.as_str())
165                .ok_or_else(|| D::Error::missing_field("memory.id"))?
166                .to_string();
167            let agent_id = memory
168                .get("agent_id")
169                .and_then(|v| v.as_str())
170                .unwrap_or("")
171                .to_string();
172            let namespace = memory
173                .get("namespace")
174                .and_then(|v| v.as_str())
175                .unwrap_or("default")
176                .to_string();
177            let embedding_time_ms = val.get("embedding_time_ms").and_then(|v| v.as_u64());
178            return Ok(Self {
179                memory_id,
180                agent_id,
181                namespace,
182                embedding_time_ms,
183            });
184        }
185
186        // Legacy / mock format: {"memory_id":"...","agent_id":"...","namespace":"..."}
187        let memory_id = val
188            .get("memory_id")
189            .and_then(|v| v.as_str())
190            .ok_or_else(|| D::Error::missing_field("memory_id"))?
191            .to_string();
192        let agent_id = val
193            .get("agent_id")
194            .and_then(|v| v.as_str())
195            .unwrap_or("")
196            .to_string();
197        let namespace = val
198            .get("namespace")
199            .and_then(|v| v.as_str())
200            .unwrap_or("default")
201            .to_string();
202        Ok(Self {
203            memory_id,
204            agent_id,
205            namespace,
206            embedding_time_ms: None,
207        })
208    }
209}
210
211/// Fusion strategy for hybrid recall (CE-14).
212///
213/// Controls how vector and BM25 scores are combined when `routing = Hybrid`.
214/// `MinMax` is the server default since v0.11.2 (CEO architecture decision, DAK-1948).
215/// `RecallRequest` sends `None` by default, so the server default applies automatically.
216#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
217#[serde(rename_all = "snake_case")]
218pub enum FusionStrategy {
219    /// Reciprocal Rank Fusion (Cormack et al., SIGIR 2009).
220    /// Formula: score(d) = Σ 1 / (k + rank(d)), k = 60.
221    /// This variant is the Rust `Default` for ergonomic use; pass `None` in
222    /// `RecallRequest` to let the server apply its own default (MinMax since v0.11.2).
223    #[default]
224    Rrf,
225    /// Weighted min-max normalization — server default since v0.11.2.
226    #[serde(rename = "minmax")]
227    MinMax,
228}
229
230/// Retrieval routing mode for recall and search (CE-10).
231///
232/// Controls which retrieval index the server uses. `Auto` (default) lets the
233/// server pick the best strategy based on the query.
234#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
235#[serde(rename_all = "snake_case")]
236pub enum RoutingMode {
237    /// Server picks the best strategy (default).
238    Auto,
239    /// Force ANN vector search (HNSW).
240    Vector,
241    /// Force BM25 full-text search.
242    Bm25,
243    /// Fuse ANN and BM25 scores (RRF).
244    Hybrid,
245}
246
247/// Recall memories request
248#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct RecallRequest {
250    pub agent_id: String,
251    pub query: String,
252    #[serde(default = "default_top_k")]
253    pub top_k: usize,
254    #[serde(skip_serializing_if = "Option::is_none")]
255    pub memory_type: Option<MemoryType>,
256    #[serde(default)]
257    pub min_importance: f32,
258    #[serde(skip_serializing_if = "Option::is_none")]
259    pub session_id: Option<String>,
260    #[serde(default)]
261    pub tags: Vec<String>,
262    /// COG-2: traverse KG depth-1 from recalled memories and include
263    /// associatively linked memories in the response (default: false)
264    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
265    pub include_associated: bool,
266    /// COG-2: max associated memories to return (default: 10, max: 10)
267    #[serde(skip_serializing_if = "Option::is_none")]
268    pub associated_memories_cap: Option<u32>,
269    /// KG-3: KG traversal depth 1–3 (default: 1); requires include_associated
270    #[serde(skip_serializing_if = "Option::is_none")]
271    pub associated_memories_depth: Option<u8>,
272    /// KG-3: minimum edge weight for KG traversal (default: 0.0)
273    #[serde(skip_serializing_if = "Option::is_none")]
274    pub associated_memories_min_weight: Option<f32>,
275    /// CE-7: only recall memories created at or after this ISO-8601 timestamp
276    #[serde(skip_serializing_if = "Option::is_none")]
277    pub since: Option<String>,
278    /// CE-7: only recall memories created at or before this ISO-8601 timestamp
279    #[serde(skip_serializing_if = "Option::is_none")]
280    pub until: Option<String>,
281    /// CE-10: retrieval routing mode. `None` uses the server default (`auto`).
282    #[serde(skip_serializing_if = "Option::is_none")]
283    pub routing: Option<RoutingMode>,
284    /// CE-13: cross-encoder reranking. `None` uses server default (`true` for recall,
285    /// `false` for search). Set to `Some(false)` to disable on latency-sensitive paths.
286    #[serde(skip_serializing_if = "Option::is_none")]
287    pub rerank: Option<bool>,
288    /// CE-14: fusion strategy when `routing = Hybrid`. `None` uses server default (`Rrf`).
289    #[serde(skip_serializing_if = "Option::is_none")]
290    pub fusion: Option<FusionStrategy>,
291    /// CE-17: explicit vector/BM25 weight for Hybrid routing (0.0–1.0).
292    /// When set, overrides the adaptive heuristic from `QueryClassifier`.
293    /// Omit for adaptive defaults (recommended for most callers).
294    /// Only effective when `routing = RoutingMode::Hybrid`.
295    #[serde(skip_serializing_if = "Option::is_none")]
296    pub vector_weight: Option<f32>,
297    /// CE-23: pseudo-relevance feedback (PRF) passes for BM25 routing (1–3, default: 1).
298    /// Pass `Some(2)` or `Some(3)` for multi-hop or temporal queries where a second
299    /// BM25 pass over extracted entities improves recall.
300    /// Only effective when `routing = RoutingMode::Bm25`.
301    #[serde(skip_serializing_if = "Option::is_none")]
302    pub iterations: Option<u8>,
303    /// v0.11.0: fetch session-adjacent memories within ±5 min of each top result.
304    /// `None` uses server default (`true`). Set to `Some(false)` to disable for
305    /// latency-sensitive paths.
306    #[serde(skip_serializing_if = "Option::is_none")]
307    pub neighborhood: Option<bool>,
308}
309
310fn default_top_k() -> usize {
311    5
312}
313
314impl RecallRequest {
315    /// Create a new recall request
316    pub fn new(agent_id: impl Into<String>, query: impl Into<String>) -> Self {
317        Self {
318            agent_id: agent_id.into(),
319            query: query.into(),
320            top_k: 5,
321            memory_type: None,
322            min_importance: 0.0,
323            session_id: None,
324            tags: Vec::new(),
325            include_associated: false,
326            associated_memories_cap: None,
327            associated_memories_depth: None,
328            associated_memories_min_weight: None,
329            since: None,
330            until: None,
331            routing: None,
332            rerank: None,
333            fusion: None,
334            vector_weight: None,
335            iterations: None,
336            neighborhood: None,
337        }
338    }
339
340    /// Set number of results
341    pub fn with_top_k(mut self, top_k: usize) -> Self {
342        self.top_k = top_k;
343        self
344    }
345
346    /// Filter by memory type
347    pub fn with_type(mut self, memory_type: MemoryType) -> Self {
348        self.memory_type = Some(memory_type);
349        self
350    }
351
352    /// Set minimum importance threshold
353    pub fn with_min_importance(mut self, min: f32) -> Self {
354        self.min_importance = min;
355        self
356    }
357
358    /// Filter by session
359    pub fn with_session(mut self, session_id: impl Into<String>) -> Self {
360        self.session_id = Some(session_id.into());
361        self
362    }
363
364    /// Filter by tags
365    pub fn with_tags(mut self, tags: Vec<String>) -> Self {
366        self.tags = tags;
367        self
368    }
369
370    /// COG-2: include KG depth-1 associated memories in the response
371    pub fn with_associated(mut self) -> Self {
372        self.include_associated = true;
373        self
374    }
375
376    /// COG-2: set max associated memories cap (default: 10, max: 10)
377    pub fn with_associated_cap(mut self, cap: u32) -> Self {
378        self.include_associated = true;
379        self.associated_memories_cap = Some(cap);
380        self
381    }
382
383    /// CE-7: only recall memories created at or after this ISO-8601 timestamp
384    pub fn with_since(mut self, since: impl Into<String>) -> Self {
385        self.since = Some(since.into());
386        self
387    }
388
389    /// CE-7: only recall memories created at or before this ISO-8601 timestamp
390    pub fn with_until(mut self, until: impl Into<String>) -> Self {
391        self.until = Some(until.into());
392        self
393    }
394
395    /// CE-10: set retrieval routing mode
396    pub fn with_routing(mut self, routing: RoutingMode) -> Self {
397        self.routing = Some(routing);
398        self
399    }
400
401    /// CE-13: enable or disable cross-encoder reranking (server default: true for recall)
402    pub fn with_rerank(mut self, rerank: bool) -> Self {
403        self.rerank = Some(rerank);
404        self
405    }
406
407    /// KG-3: set KG traversal depth (1–3, default: 1); implies include_associated
408    pub fn with_associated_depth(mut self, depth: u8) -> Self {
409        self.include_associated = true;
410        self.associated_memories_depth = Some(depth);
411        self
412    }
413
414    /// KG-3: set minimum edge weight for KG traversal (default: 0.0)
415    pub fn with_associated_min_weight(mut self, weight: f32) -> Self {
416        self.associated_memories_min_weight = Some(weight);
417        self
418    }
419
420    /// CE-14: set fusion strategy for hybrid recall (server default: `Rrf`)
421    pub fn with_fusion(mut self, fusion: FusionStrategy) -> Self {
422        self.fusion = Some(fusion);
423        self
424    }
425
426    /// CE-17: set explicit vector/BM25 weight for Hybrid routing (0.0–1.0).
427    /// Overrides the adaptive heuristic from `QueryClassifier`.
428    /// Omit for adaptive defaults (recommended for most callers).
429    pub fn with_vector_weight(mut self, weight: f32) -> Self {
430        self.vector_weight = Some(weight);
431        self
432    }
433
434    /// CE-23: set PRF iteration count for BM25 routing (1–3, default: 1).
435    /// Pass `2` or `3` for multi-hop or temporal queries where a second BM25
436    /// pass over extracted entities improves recall.
437    /// Only effective when `routing = RoutingMode::Bm25`.
438    pub fn with_iterations(mut self, iterations: u8) -> Self {
439        self.iterations = Some(iterations);
440        self
441    }
442
443    /// v0.11.0: enable or disable session-adjacent neighborhood enrichment
444    /// (server default: `true`). Set to `false` for latency-sensitive paths.
445    pub fn with_neighborhood(mut self, neighborhood: bool) -> Self {
446        self.neighborhood = Some(neighborhood);
447        self
448    }
449}
450
451/// A recalled memory
452#[derive(Debug, Clone, Serialize)]
453pub struct RecalledMemory {
454    pub id: String,
455    pub content: String,
456    pub memory_type: MemoryType,
457    pub importance: f32,
458    /// The ranking score — equals `smart_score` when present, then `weighted_score`, then raw `score`.
459    pub score: f32,
460    /// Raw smart_score from the server (the primary ranking key).
461    #[serde(skip_serializing_if = "Option::is_none")]
462    pub smart_score: Option<f32>,
463    /// Raw weighted_score from the server.
464    #[serde(skip_serializing_if = "Option::is_none")]
465    pub weighted_score: Option<f32>,
466    #[serde(default)]
467    pub tags: Vec<String>,
468    #[serde(skip_serializing_if = "Option::is_none")]
469    pub session_id: Option<String>,
470    #[serde(skip_serializing_if = "Option::is_none")]
471    pub metadata: Option<serde_json::Value>,
472    pub created_at: u64,
473    pub last_accessed_at: u64,
474    pub access_count: u32,
475    /// KG-3: hop depth at which this memory was found (only set on associated memories)
476    #[serde(skip_serializing_if = "Option::is_none")]
477    pub depth: Option<u8>,
478    /// Hybrid sub-score: vector similarity component (server v0.11.98+, absent when BM25-only)
479    #[serde(skip_serializing_if = "Option::is_none")]
480    pub vector_score: Option<f32>,
481    /// Hybrid sub-score: BM25 text component (server v0.11.98+, absent when vector-only)
482    #[serde(skip_serializing_if = "Option::is_none")]
483    pub text_score: Option<f32>,
484}
485
486impl<'de> serde::Deserialize<'de> for RecalledMemory {
487    fn deserialize<D: serde::Deserializer<'de>>(
488        deserializer: D,
489    ) -> std::result::Result<Self, D::Error> {
490        use serde::de::Error as _;
491        let val = serde_json::Value::deserialize(deserializer)?;
492
493        // Server wraps recall results as {memory:{...}, score, weighted_score, smart_score,
494        // vector_score, text_score}. smart_score is the actual ranking key (server sorts by it).
495        // Fall back to flat format for direct memory-get responses.
496        let smart_score = val
497            .get("smart_score")
498            .and_then(|v| v.as_f64())
499            .map(|v| v as f32);
500        let weighted_score = val
501            .get("weighted_score")
502            .and_then(|v| v.as_f64())
503            .map(|v| v as f32);
504        let score = smart_score
505            .or(weighted_score)
506            .or_else(|| val.get("score").and_then(|v| v.as_f64()).map(|v| v as f32))
507            .unwrap_or(0.0);
508        // Hybrid sub-scores (server v0.11.98+): surfaced at the top level alongside smart_score.
509        let vector_score = val
510            .get("vector_score")
511            .and_then(|v| v.as_f64())
512            .map(|v| v as f32);
513        let text_score = val
514            .get("text_score")
515            .and_then(|v| v.as_f64())
516            .map(|v| v as f32);
517
518        let mem = val.get("memory").unwrap_or(&val);
519
520        let id = mem
521            .get("id")
522            .and_then(|v| v.as_str())
523            .ok_or_else(|| D::Error::missing_field("id"))?
524            .to_string();
525        let content = mem
526            .get("content")
527            .and_then(|v| v.as_str())
528            .ok_or_else(|| D::Error::missing_field("content"))?
529            .to_string();
530        let memory_type: MemoryType = mem
531            .get("memory_type")
532            .and_then(|v| serde_json::from_value(v.clone()).ok())
533            .unwrap_or(MemoryType::Episodic);
534        let importance = mem
535            .get("importance")
536            .and_then(|v| v.as_f64())
537            .unwrap_or(0.5) as f32;
538        let tags: Vec<String> = mem
539            .get("tags")
540            .and_then(|v| serde_json::from_value(v.clone()).ok())
541            .unwrap_or_default();
542        let session_id = mem
543            .get("session_id")
544            .and_then(|v| v.as_str())
545            .map(String::from);
546        let metadata = mem.get("metadata").cloned().filter(|v| !v.is_null());
547        let created_at = mem.get("created_at").and_then(|v| v.as_u64()).unwrap_or(0);
548        let last_accessed_at = mem
549            .get("last_accessed_at")
550            .and_then(|v| v.as_u64())
551            .unwrap_or(0);
552        let access_count = mem
553            .get("access_count")
554            .and_then(|v| v.as_u64())
555            .unwrap_or(0) as u32;
556        let depth = mem.get("depth").and_then(|v| v.as_u64()).map(|v| v as u8);
557
558        Ok(Self {
559            id,
560            content,
561            memory_type,
562            importance,
563            score,
564            smart_score,
565            weighted_score,
566            tags,
567            session_id,
568            metadata,
569            created_at,
570            last_accessed_at,
571            access_count,
572            depth,
573            vector_score,
574            text_score,
575        })
576    }
577}
578
579/// Recall response
580#[derive(Debug, Clone, Serialize, Deserialize)]
581pub struct RecallResponse {
582    pub memories: Vec<RecalledMemory>,
583    #[serde(default)]
584    pub total_found: usize,
585    /// COG-2 / KG-3: KG associated memories at configurable depth (only present when include_associated was true)
586    #[serde(skip_serializing_if = "Option::is_none")]
587    pub associated_memories: Option<Vec<RecalledMemory>>,
588}
589
590/// Forget (delete) memories request
591#[derive(Debug, Clone, Serialize, Deserialize)]
592pub struct ForgetRequest {
593    pub agent_id: String,
594    #[serde(default)]
595    pub memory_ids: Vec<String>,
596    #[serde(default)]
597    pub tags: Vec<String>,
598    #[serde(skip_serializing_if = "Option::is_none")]
599    pub session_id: Option<String>,
600    #[serde(skip_serializing_if = "Option::is_none")]
601    pub before_timestamp: Option<u64>,
602}
603
604impl ForgetRequest {
605    /// Forget specific memories by ID
606    pub fn by_ids(agent_id: impl Into<String>, ids: Vec<String>) -> Self {
607        Self {
608            agent_id: agent_id.into(),
609            memory_ids: ids,
610            tags: Vec::new(),
611            session_id: None,
612            before_timestamp: None,
613        }
614    }
615
616    /// Forget memories with specific tags
617    pub fn by_tags(agent_id: impl Into<String>, tags: Vec<String>) -> Self {
618        Self {
619            agent_id: agent_id.into(),
620            memory_ids: Vec::new(),
621            tags,
622            session_id: None,
623            before_timestamp: None,
624        }
625    }
626
627    /// Forget all memories in a session
628    pub fn by_session(agent_id: impl Into<String>, session_id: impl Into<String>) -> Self {
629        Self {
630            agent_id: agent_id.into(),
631            memory_ids: Vec::new(),
632            tags: Vec::new(),
633            session_id: Some(session_id.into()),
634            before_timestamp: None,
635        }
636    }
637}
638
639/// Forget response
640#[derive(Debug, Clone, Serialize, Deserialize)]
641pub struct ForgetResponse {
642    pub deleted_count: u64,
643}
644
645/// Session start request
646#[derive(Debug, Clone, Serialize, Deserialize)]
647pub struct SessionStartRequest {
648    pub agent_id: String,
649    #[serde(skip_serializing_if = "Option::is_none")]
650    pub metadata: Option<serde_json::Value>,
651}
652
653/// Session information
654#[derive(Debug, Clone, Serialize, Deserialize)]
655pub struct Session {
656    pub id: String,
657    pub agent_id: String,
658    pub started_at: u64,
659    #[serde(skip_serializing_if = "Option::is_none")]
660    pub ended_at: Option<u64>,
661    #[serde(skip_serializing_if = "Option::is_none")]
662    pub summary: Option<String>,
663    #[serde(skip_serializing_if = "Option::is_none")]
664    pub metadata: Option<serde_json::Value>,
665    /// Cached count of memories in this session
666    #[serde(default)]
667    pub memory_count: usize,
668}
669
670/// Session end request
671#[derive(Debug, Clone, Serialize, Deserialize)]
672pub struct SessionEndRequest {
673    #[serde(skip_serializing_if = "Option::is_none")]
674    pub summary: Option<String>,
675}
676
677/// Response from `POST /v1/sessions/start`
678#[derive(Debug, Clone, Serialize, Deserialize)]
679pub struct SessionStartResponse {
680    pub session: Session,
681}
682
683/// Response from `POST /v1/sessions/{id}/end`
684#[derive(Debug, Clone, Serialize, Deserialize)]
685pub struct SessionEndResponse {
686    pub session: Session,
687    pub memory_count: usize,
688}
689
690/// Response from `GET /v1/sessions`
691#[derive(Debug, Clone, Deserialize)]
692pub struct ListSessionsResponse {
693    pub sessions: Vec<Session>,
694    #[allow(dead_code)]
695    pub total: usize,
696}
697
698/// Request to update a memory
699#[derive(Debug, Clone, Serialize, Deserialize)]
700pub struct UpdateMemoryRequest {
701    #[serde(skip_serializing_if = "Option::is_none")]
702    pub content: Option<String>,
703    #[serde(skip_serializing_if = "Option::is_none")]
704    pub metadata: Option<serde_json::Value>,
705    #[serde(skip_serializing_if = "Option::is_none")]
706    pub memory_type: Option<MemoryType>,
707}
708
709/// Request to update memory importance
710#[derive(Debug, Clone, Serialize, Deserialize)]
711pub struct UpdateImportanceRequest {
712    pub memory_ids: Vec<String>,
713    pub importance: f32,
714}
715
716/// DBSCAN algorithm config for adaptive consolidation (CE-6).
717#[derive(Debug, Clone, Serialize, Deserialize, Default)]
718pub struct ConsolidationConfig {
719    /// Clustering algorithm: `"dbscan"` (default) or `"greedy"`.
720    #[serde(skip_serializing_if = "Option::is_none")]
721    pub algorithm: Option<String>,
722    /// Minimum cluster samples for DBSCAN.
723    #[serde(skip_serializing_if = "Option::is_none")]
724    pub min_samples: Option<u32>,
725    /// Epsilon distance parameter for DBSCAN.
726    #[serde(skip_serializing_if = "Option::is_none")]
727    pub eps: Option<f32>,
728}
729
730/// One step in the consolidation execution log (CE-6).
731#[derive(Debug, Clone, Serialize, Deserialize)]
732pub struct ConsolidationLogEntry {
733    pub step: String,
734    pub memories_before: usize,
735    pub memories_after: usize,
736    pub duration_ms: f64,
737}
738
739/// Request to consolidate memories
740#[derive(Debug, Clone, Serialize, Deserialize, Default)]
741pub struct ConsolidateRequest {
742    #[serde(skip_serializing_if = "Option::is_none")]
743    pub memory_type: Option<String>,
744    #[serde(skip_serializing_if = "Option::is_none")]
745    pub threshold: Option<f32>,
746    #[serde(default)]
747    pub dry_run: bool,
748    /// Optional DBSCAN algorithm configuration (CE-6).
749    #[serde(skip_serializing_if = "Option::is_none")]
750    pub config: Option<ConsolidationConfig>,
751}
752
753/// Response from consolidation (`POST /v1/memory/consolidate`).
754///
755/// The server returns `{"memories_removed": N, "source_memory_ids": [...], "consolidated_memory": {...}}`.
756/// `consolidated_count` is mapped from `memories_removed` for backward compat.
757#[derive(Debug, Clone, Serialize)]
758pub struct ConsolidateResponse {
759    /// Number of source memories removed (= `memories_removed` from server)
760    pub consolidated_count: usize,
761    /// Alias for consolidated_count
762    pub removed_count: usize,
763    /// IDs of source memories that were removed
764    #[serde(default)]
765    pub new_memories: Vec<String>,
766    /// Step-by-step consolidation log (CE-6, optional).
767    #[serde(default, skip_serializing_if = "Vec::is_empty")]
768    pub log: Vec<ConsolidationLogEntry>,
769}
770
771impl<'de> serde::Deserialize<'de> for ConsolidateResponse {
772    fn deserialize<D: serde::Deserializer<'de>>(
773        deserializer: D,
774    ) -> std::result::Result<Self, D::Error> {
775        let val = serde_json::Value::deserialize(deserializer)?;
776        // Server format: {"consolidated_memory":{...}, "source_memory_ids":[...], "memories_removed": N}
777        let removed = val
778            .get("memories_removed")
779            .and_then(|v| v.as_u64())
780            .or_else(|| val.get("removed_count").and_then(|v| v.as_u64()))
781            .or_else(|| val.get("consolidated_count").and_then(|v| v.as_u64()))
782            .unwrap_or(0) as usize;
783        let source_ids: Vec<String> = val
784            .get("source_memory_ids")
785            .and_then(|v| v.as_array())
786            .map(|arr| {
787                arr.iter()
788                    .filter_map(|v| v.as_str().map(String::from))
789                    .collect()
790            })
791            .unwrap_or_default();
792        Ok(Self {
793            consolidated_count: removed,
794            removed_count: removed,
795            new_memories: source_ids,
796            log: vec![],
797        })
798    }
799}
800
801// ============================================================================
802// DX-1: Memory Import / Export
803// ============================================================================
804
805/// Response from `POST /v1/import` (DX-1).
806#[derive(Debug, Clone, Serialize, Deserialize)]
807pub struct MemoryImportResponse {
808    pub imported_count: usize,
809    pub skipped_count: usize,
810    #[serde(default)]
811    pub errors: Vec<String>,
812}
813
814/// Response from `GET /v1/export` (DX-1).
815#[derive(Debug, Clone, Serialize, Deserialize)]
816pub struct MemoryExportResponse {
817    pub data: Vec<serde_json::Value>,
818    pub format: String,
819    pub count: usize,
820}
821
822// ============================================================================
823// OBS-1: Business-Event Audit Log
824// ============================================================================
825
826/// A single business-event entry from the audit log (OBS-1).
827#[derive(Debug, Clone, Serialize, Deserialize)]
828pub struct AuditEvent {
829    pub id: String,
830    pub event_type: String,
831    #[serde(skip_serializing_if = "Option::is_none")]
832    pub agent_id: Option<String>,
833    #[serde(skip_serializing_if = "Option::is_none")]
834    pub namespace: Option<String>,
835    pub timestamp: u64,
836    #[serde(default)]
837    pub details: serde_json::Value,
838}
839
840/// Response from `GET /v1/audit` (OBS-1).
841#[derive(Debug, Clone, Serialize, Deserialize)]
842pub struct AuditListResponse {
843    pub events: Vec<AuditEvent>,
844    pub total: usize,
845    #[serde(skip_serializing_if = "Option::is_none")]
846    pub cursor: Option<String>,
847}
848
849/// Response from `POST /v1/audit/export` (OBS-1).
850#[derive(Debug, Clone, Serialize, Deserialize)]
851pub struct AuditExportResponse {
852    pub data: String,
853    pub format: String,
854    pub count: usize,
855}
856
857/// Query parameters for the audit log (OBS-1).
858#[derive(Debug, Clone, Serialize, Deserialize, Default)]
859pub struct AuditQuery {
860    #[serde(skip_serializing_if = "Option::is_none")]
861    pub agent_id: Option<String>,
862    #[serde(skip_serializing_if = "Option::is_none")]
863    pub event_type: Option<String>,
864    #[serde(skip_serializing_if = "Option::is_none")]
865    pub from: Option<u64>,
866    #[serde(skip_serializing_if = "Option::is_none")]
867    pub to: Option<u64>,
868    #[serde(skip_serializing_if = "Option::is_none")]
869    pub limit: Option<u32>,
870    #[serde(skip_serializing_if = "Option::is_none")]
871    pub cursor: Option<String>,
872}
873
874// ============================================================================
875// EXT-1: External Extraction Providers
876// ============================================================================
877
878/// Result from `POST /v1/extract` (EXT-1).
879#[derive(Debug, Clone, Serialize, Deserialize)]
880pub struct ExtractionResult {
881    pub entities: Vec<serde_json::Value>,
882    pub provider: String,
883    #[serde(skip_serializing_if = "Option::is_none")]
884    pub model: Option<String>,
885    pub duration_ms: f64,
886}
887
888/// Metadata for an available extraction provider (EXT-1).
889#[derive(Debug, Clone, Serialize, Deserialize)]
890pub struct ExtractionProviderInfo {
891    pub name: String,
892    pub available: bool,
893    #[serde(default)]
894    pub models: Vec<String>,
895}
896
897/// Response from `GET /v1/extract/providers` (EXT-1).
898#[derive(Debug, Clone, Serialize, Deserialize)]
899#[serde(untagged)]
900pub enum ExtractProvidersResponse {
901    List(Vec<ExtractionProviderInfo>),
902    Object {
903        providers: Vec<ExtractionProviderInfo>,
904    },
905}
906
907// ============================================================================
908// SEC-3: AES-256-GCM Encryption Key Rotation
909// ============================================================================
910
911/// Request body for `POST /v1/admin/encryption/rotate-key` (SEC-3).
912#[derive(Debug, Clone, Serialize, Deserialize)]
913pub struct RotateEncryptionKeyRequest {
914    /// New passphrase or 64-char hex key to rotate to.
915    pub new_key: String,
916    /// If set, rotate only memories in this namespace. Omit to rotate all.
917    #[serde(skip_serializing_if = "Option::is_none")]
918    pub namespace: Option<String>,
919}
920
921/// Response from `POST /v1/admin/encryption/rotate-key` (SEC-3).
922#[derive(Debug, Clone, Serialize, Deserialize)]
923pub struct RotateEncryptionKeyResponse {
924    pub rotated: usize,
925    pub skipped: usize,
926    #[serde(default)]
927    pub namespaces: Vec<String>,
928}
929
930/// Request for memory feedback
931#[derive(Debug, Clone, Serialize, Deserialize)]
932pub struct FeedbackRequest {
933    pub memory_id: String,
934    pub feedback: String,
935    #[serde(skip_serializing_if = "Option::is_none")]
936    pub relevance_score: Option<f32>,
937}
938
939/// Response from legacy feedback endpoint (POST /v1/agents/:id/memories/feedback)
940#[derive(Debug, Clone, Serialize, Deserialize)]
941pub struct LegacyFeedbackResponse {
942    pub status: String,
943    pub updated_importance: Option<f32>,
944}
945
946// ============================================================================
947// CE-2: Batch Recall / Forget Types
948// ============================================================================
949
950/// Filter predicates for batch memory operations (CE-2).
951///
952/// All fields are optional.  For [`BatchForgetRequest`] at least one must be
953/// set (server-side safety guard).
954#[derive(Debug, Clone, Serialize, Deserialize, Default)]
955pub struct BatchMemoryFilter {
956    /// Restrict to memories that carry **all** listed tags.
957    #[serde(skip_serializing_if = "Option::is_none")]
958    pub tags: Option<Vec<String>>,
959    /// Minimum importance (inclusive).
960    #[serde(skip_serializing_if = "Option::is_none")]
961    pub min_importance: Option<f32>,
962    /// Maximum importance (inclusive).
963    #[serde(skip_serializing_if = "Option::is_none")]
964    pub max_importance: Option<f32>,
965    /// Only memories created at or after this Unix timestamp (seconds).
966    #[serde(skip_serializing_if = "Option::is_none")]
967    pub created_after: Option<u64>,
968    /// Only memories created before or at this Unix timestamp (seconds).
969    #[serde(skip_serializing_if = "Option::is_none")]
970    pub created_before: Option<u64>,
971    /// Restrict to a specific memory type.
972    #[serde(skip_serializing_if = "Option::is_none")]
973    pub memory_type: Option<MemoryType>,
974    /// Restrict to memories from a specific session.
975    #[serde(skip_serializing_if = "Option::is_none")]
976    pub session_id: Option<String>,
977}
978
979impl BatchMemoryFilter {
980    /// Convenience: filter by tags.
981    pub fn with_tags(mut self, tags: Vec<String>) -> Self {
982        self.tags = Some(tags);
983        self
984    }
985
986    /// Convenience: filter by minimum importance.
987    pub fn with_min_importance(mut self, min: f32) -> Self {
988        self.min_importance = Some(min);
989        self
990    }
991
992    /// Convenience: filter by maximum importance.
993    pub fn with_max_importance(mut self, max: f32) -> Self {
994        self.max_importance = Some(max);
995        self
996    }
997
998    /// Convenience: filter by session.
999    pub fn with_session(mut self, session_id: impl Into<String>) -> Self {
1000        self.session_id = Some(session_id.into());
1001        self
1002    }
1003}
1004
1005/// Request body for `POST /v1/memories/recall/batch`.
1006#[derive(Debug, Clone, Serialize, Deserialize)]
1007pub struct BatchRecallRequest {
1008    /// Agent whose memory namespace to search.
1009    pub agent_id: String,
1010    /// Filter predicates to apply.
1011    #[serde(default)]
1012    pub filter: BatchMemoryFilter,
1013    /// Maximum number of results to return (default: 100).
1014    #[serde(default = "default_batch_limit")]
1015    pub limit: usize,
1016}
1017
1018fn default_batch_limit() -> usize {
1019    100
1020}
1021
1022impl BatchRecallRequest {
1023    /// Create a new batch recall request for an agent.
1024    pub fn new(agent_id: impl Into<String>) -> Self {
1025        Self {
1026            agent_id: agent_id.into(),
1027            filter: BatchMemoryFilter::default(),
1028            limit: 100,
1029        }
1030    }
1031
1032    /// Set filter predicates.
1033    pub fn with_filter(mut self, filter: BatchMemoryFilter) -> Self {
1034        self.filter = filter;
1035        self
1036    }
1037
1038    /// Set result limit.
1039    pub fn with_limit(mut self, limit: usize) -> Self {
1040        self.limit = limit;
1041        self
1042    }
1043}
1044
1045/// Response from `POST /v1/memories/recall/batch`.
1046#[derive(Debug, Clone, Serialize, Deserialize)]
1047pub struct BatchRecallResponse {
1048    pub memories: Vec<RecalledMemory>,
1049    /// Total memories in the agent namespace.
1050    pub total: usize,
1051    /// Number of memories that passed the filter.
1052    pub filtered: usize,
1053}
1054
1055// ============================================================================
1056// DAK-5508: Batch Store — POST /v1/memories/store/batch
1057// ============================================================================
1058
1059/// A single memory entry within a [`BatchStoreMemoryRequest`].
1060///
1061/// Mirrors [`StoreMemoryRequest`] but omits `agent_id` (supplied at batch level).
1062#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1063pub struct BatchStoreMemoryItem {
1064    /// Memory content (required, max 100 000 chars).
1065    pub content: String,
1066    #[serde(default)]
1067    pub memory_type: MemoryType,
1068    #[serde(skip_serializing_if = "Option::is_none")]
1069    pub session_id: Option<String>,
1070    #[serde(default = "default_importance")]
1071    pub importance: f32,
1072    #[serde(default)]
1073    pub tags: Vec<String>,
1074    #[serde(skip_serializing_if = "Option::is_none")]
1075    pub metadata: Option<serde_json::Value>,
1076    #[serde(skip_serializing_if = "Option::is_none")]
1077    pub ttl_seconds: Option<u64>,
1078    #[serde(skip_serializing_if = "Option::is_none")]
1079    pub expires_at: Option<u64>,
1080    /// Bi-temporal validity start — Unix timestamp (seconds). Defaults to ingest time when omitted.
1081    /// Used by temporal recall queries (server v0.11.98+, DAK-7424).
1082    #[serde(skip_serializing_if = "Option::is_none")]
1083    pub valid_from: Option<i64>,
1084    /// Optional custom ID. Auto-generated if not provided.
1085    #[serde(skip_serializing_if = "Option::is_none")]
1086    pub id: Option<String>,
1087}
1088
1089impl BatchStoreMemoryItem {
1090    /// Create a new item with the given content.
1091    pub fn new(content: impl Into<String>) -> Self {
1092        Self {
1093            content: content.into(),
1094            importance: default_importance(),
1095            ..Default::default()
1096        }
1097    }
1098
1099    /// Set importance.
1100    pub fn with_importance(mut self, importance: f32) -> Self {
1101        self.importance = importance;
1102        self
1103    }
1104
1105    /// Set tags.
1106    pub fn with_tags(mut self, tags: Vec<String>) -> Self {
1107        self.tags = tags;
1108        self
1109    }
1110
1111    /// Set session.
1112    pub fn with_session(mut self, session_id: impl Into<String>) -> Self {
1113        self.session_id = Some(session_id.into());
1114        self
1115    }
1116
1117    /// Set metadata.
1118    pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
1119        self.metadata = Some(metadata);
1120        self
1121    }
1122
1123    /// Set bi-temporal validity start as a Unix timestamp (seconds).
1124    /// Defaults to ingest time when omitted (server v0.11.98+, DAK-7424).
1125    pub fn with_valid_from(mut self, valid_from: i64) -> Self {
1126        self.valid_from = Some(valid_from);
1127        self
1128    }
1129
1130    /// Set a custom memory ID.
1131    pub fn with_id(mut self, id: impl Into<String>) -> Self {
1132        self.id = Some(id.into());
1133        self
1134    }
1135}
1136
1137/// Request for `POST /v1/memories/store/batch` (DAK-5508).
1138///
1139/// Accepts up to 1 000 memories per call. The server embeds all contents in a
1140/// single ONNX inference pass and upserts them in one RocksDB write, with HNSW
1141/// invalidation happening exactly once — yielding ≥100× throughput vs. N
1142/// sequential single-store calls.
1143#[derive(Debug, Clone, Serialize, Deserialize)]
1144pub struct BatchStoreMemoryRequest {
1145    /// Agent namespace to store the memories in.
1146    pub agent_id: String,
1147    /// Memories to store (1–1000 items).
1148    pub memories: Vec<BatchStoreMemoryItem>,
1149}
1150
1151impl BatchStoreMemoryRequest {
1152    /// Create a new batch request.
1153    pub fn new(agent_id: impl Into<String>, memories: Vec<BatchStoreMemoryItem>) -> Self {
1154        Self {
1155            agent_id: agent_id.into(),
1156            memories,
1157        }
1158    }
1159}
1160
1161/// A single stored memory returned in a [`BatchStoreMemoryResponse`].
1162#[derive(Debug, Clone, Serialize, Deserialize)]
1163pub struct BatchStoredMemory {
1164    pub id: String,
1165    pub content: String,
1166    pub agent_id: String,
1167    #[serde(default)]
1168    pub tags: Vec<String>,
1169    #[serde(default)]
1170    pub importance: f32,
1171    pub created_at: u64,
1172}
1173
1174/// Response from `POST /v1/memories/store/batch`.
1175#[derive(Debug, Clone, Serialize, Deserialize)]
1176pub struct BatchStoreMemoryResponse {
1177    /// Stored memories in the same order as the request items.
1178    pub stored: Vec<BatchStoredMemory>,
1179    /// Number of memories successfully stored.
1180    pub stored_count: usize,
1181    /// Time spent on ONNX embedding for the entire batch (milliseconds).
1182    pub total_embedding_time_ms: u64,
1183}
1184
1185/// Request body for `DELETE /v1/memories/forget/batch`.
1186#[derive(Debug, Clone, Serialize, Deserialize)]
1187pub struct BatchForgetRequest {
1188    /// Agent whose memory namespace to purge from.
1189    pub agent_id: String,
1190    /// Filter predicates — **at least one must be set** (server safety guard).
1191    pub filter: BatchMemoryFilter,
1192}
1193
1194impl BatchForgetRequest {
1195    /// Create a new batch forget request with the given filter.
1196    pub fn new(agent_id: impl Into<String>, filter: BatchMemoryFilter) -> Self {
1197        Self {
1198            agent_id: agent_id.into(),
1199            filter,
1200        }
1201    }
1202}
1203
1204/// Response from `DELETE /v1/memories/forget/batch`.
1205#[derive(Debug, Clone, Serialize, Deserialize)]
1206pub struct BatchForgetResponse {
1207    pub deleted_count: usize,
1208}
1209
1210// ============================================================================
1211// Memory Client Methods
1212// ============================================================================
1213
1214impl DakeraClient {
1215    // ========================================================================
1216    // Memory Operations
1217    // ========================================================================
1218
1219    /// Store a memory for an agent
1220    ///
1221    /// # Example
1222    ///
1223    /// ```rust,no_run
1224    /// use dakera_client::{DakeraClient, memory::StoreMemoryRequest};
1225    ///
1226    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1227    /// let client = DakeraClient::new("http://localhost:3000")?;
1228    ///
1229    /// let request = StoreMemoryRequest::new("agent-1", "The user prefers dark mode")
1230    ///     .with_importance(0.8)
1231    ///     .with_tags(vec!["preferences".to_string()]);
1232    ///
1233    /// let response = client.store_memory(request).await?;
1234    /// println!("Stored memory: {}", response.memory_id);
1235    /// # Ok(())
1236    /// # }
1237    /// ```
1238    pub async fn store_memory(&self, request: StoreMemoryRequest) -> Result<StoreMemoryResponse> {
1239        let url = format!("{}/v1/memory/store", self.base_url);
1240        let response = self.client.post(&url).json(&request).send().await?;
1241        self.handle_response(response).await
1242    }
1243
1244    /// Recall memories by semantic query
1245    ///
1246    /// # Example
1247    ///
1248    /// ```rust,no_run
1249    /// use dakera_client::{DakeraClient, memory::RecallRequest};
1250    ///
1251    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1252    /// let client = DakeraClient::new("http://localhost:3000")?;
1253    ///
1254    /// let request = RecallRequest::new("agent-1", "user preferences")
1255    ///     .with_top_k(10);
1256    ///
1257    /// let response = client.recall(request).await?;
1258    /// for memory in response.memories {
1259    ///     println!("{}: {} (score: {})", memory.id, memory.content, memory.score);
1260    /// }
1261    /// # Ok(())
1262    /// # }
1263    /// ```
1264    pub async fn recall(&self, request: RecallRequest) -> Result<RecallResponse> {
1265        let url = format!("{}/v1/memory/recall", self.base_url);
1266        let response = self.client.post(&url).json(&request).send().await?;
1267        self.handle_response(response).await
1268    }
1269
1270    /// Simple recall with just agent_id and query (convenience method)
1271    pub async fn recall_simple(
1272        &self,
1273        agent_id: &str,
1274        query: &str,
1275        top_k: usize,
1276    ) -> Result<RecallResponse> {
1277        self.recall(RecallRequest::new(agent_id, query).with_top_k(top_k))
1278            .await
1279    }
1280
1281    /// Get a specific memory by ID (requires agent_id for namespace lookup)
1282    pub async fn get_memory(&self, agent_id: &str, memory_id: &str) -> Result<RecalledMemory> {
1283        let url = format!(
1284            "{}/v1/memory/get/{}?agent_id={}",
1285            self.base_url, memory_id, agent_id
1286        );
1287        let response = self.client.get(&url).send().await?;
1288        self.handle_response(response).await
1289    }
1290
1291    /// Forget (delete) memories
1292    pub async fn forget(&self, request: ForgetRequest) -> Result<ForgetResponse> {
1293        let url = format!("{}/v1/memory/forget", self.base_url);
1294        let response = self.client.post(&url).json(&request).send().await?;
1295        self.handle_response(response).await
1296    }
1297
1298    /// Search memories with advanced filters
1299    pub async fn search_memories(&self, request: RecallRequest) -> Result<RecallResponse> {
1300        let url = format!("{}/v1/memory/search", self.base_url);
1301        let response = self.client.post(&url).json(&request).send().await?;
1302        self.handle_response(response).await
1303    }
1304
1305    /// Update an existing memory
1306    pub async fn update_memory(
1307        &self,
1308        agent_id: &str,
1309        memory_id: &str,
1310        request: UpdateMemoryRequest,
1311    ) -> Result<StoreMemoryResponse> {
1312        let url = format!(
1313            "{}/v1/agents/{}/memories/{}",
1314            self.base_url, agent_id, memory_id
1315        );
1316        let response = self.client.put(&url).json(&request).send().await?;
1317        self.handle_response(response).await
1318    }
1319
1320    /// Update importance of memories
1321    pub async fn update_importance(
1322        &self,
1323        agent_id: &str,
1324        request: UpdateImportanceRequest,
1325    ) -> Result<serde_json::Value> {
1326        let url = format!("{}/v1/memory/importance", self.base_url);
1327        let mut last_result = serde_json::Value::Null;
1328        for memory_id in &request.memory_ids {
1329            let body = serde_json::json!({
1330                "agent_id": agent_id,
1331                "memory_id": memory_id,
1332                "importance": request.importance,
1333            });
1334            let response = self.client.post(&url).json(&body).send().await?;
1335            last_result = self.handle_response(response).await?;
1336        }
1337        Ok(last_result)
1338    }
1339
1340    /// Consolidate memories for an agent
1341    pub async fn consolidate(
1342        &self,
1343        agent_id: &str,
1344        request: ConsolidateRequest,
1345    ) -> Result<ConsolidateResponse> {
1346        // Server endpoint: POST /v1/memory/consolidate with agent_id in body
1347        let url = format!("{}/v1/memory/consolidate", self.base_url);
1348        let mut body = serde_json::to_value(&request)?;
1349        body["agent_id"] = serde_json::Value::String(agent_id.to_string());
1350        let response = self.client.post(&url).json(&body).send().await?;
1351        self.handle_response(response).await
1352    }
1353
1354    /// Submit feedback on a memory recall
1355    pub async fn memory_feedback(
1356        &self,
1357        agent_id: &str,
1358        request: FeedbackRequest,
1359    ) -> Result<LegacyFeedbackResponse> {
1360        let url = format!("{}/v1/agents/{}/memories/feedback", self.base_url, agent_id);
1361        let response = self.client.post(&url).json(&request).send().await?;
1362        self.handle_response(response).await
1363    }
1364
1365    // ========================================================================
1366    // Memory Feedback Loop — INT-1
1367    // ========================================================================
1368
1369    /// Submit upvote/downvote/flag feedback on a memory (INT-1).
1370    ///
1371    /// # Arguments
1372    /// * `memory_id` – The memory to give feedback on.
1373    /// * `agent_id` – The agent that owns the memory.
1374    /// * `signal` – [`FeedbackSignal`] value: `Upvote`, `Downvote`, or `Flag`.
1375    ///
1376    /// # Example
1377    /// ```no_run
1378    /// # use dakera_client::{DakeraClient, FeedbackSignal};
1379    /// # async fn example(client: &DakeraClient) -> dakera_client::Result<()> {
1380    /// let resp = client.feedback_memory("mem-abc", "agent-1", FeedbackSignal::Upvote).await?;
1381    /// println!("new importance: {}", resp.new_importance);
1382    /// # Ok(()) }
1383    /// ```
1384    pub async fn feedback_memory(
1385        &self,
1386        memory_id: &str,
1387        agent_id: &str,
1388        signal: FeedbackSignal,
1389    ) -> Result<FeedbackResponse> {
1390        let url = format!("{}/v1/memories/{}/feedback", self.base_url, memory_id);
1391        let body = MemoryFeedbackBody {
1392            agent_id: agent_id.to_string(),
1393            signal,
1394        };
1395        let response = self.client.post(&url).json(&body).send().await?;
1396        self.handle_response(response).await
1397    }
1398
1399    /// Get the full feedback history for a memory (INT-1).
1400    pub async fn get_memory_feedback_history(
1401        &self,
1402        memory_id: &str,
1403    ) -> Result<FeedbackHistoryResponse> {
1404        let url = format!("{}/v1/memories/{}/feedback", self.base_url, memory_id);
1405        let response = self.client.get(&url).send().await?;
1406        self.handle_response(response).await
1407    }
1408
1409    /// Compute a T-I-F reliability score for a memory (T-I-F RFC Phase 3).
1410    ///
1411    /// Fetches the full feedback history and reduces it to a [`TifScore`] with
1412    /// truth/indeterminacy/falsity proportions and a [`TifClassification`] label.
1413    ///
1414    /// # Arguments
1415    /// * `memory_id` – The memory to score.
1416    pub async fn evaluate_tif(&self, memory_id: &str) -> Result<TifScore> {
1417        let history = self.get_memory_feedback_history(memory_id).await?;
1418        Ok(TifScore::from_feedback_history(&history))
1419    }
1420
1421    /// Get aggregate feedback counts and health score for an agent (INT-1).
1422    pub async fn get_agent_feedback_summary(&self, agent_id: &str) -> Result<AgentFeedbackSummary> {
1423        let url = format!("{}/v1/agents/{}/feedback/summary", self.base_url, agent_id);
1424        let response = self.client.get(&url).send().await?;
1425        self.handle_response(response).await
1426    }
1427
1428    /// Directly override a memory's importance score (INT-1).
1429    ///
1430    /// # Arguments
1431    /// * `memory_id` – The memory to update.
1432    /// * `agent_id` – The agent that owns the memory.
1433    /// * `importance` – New importance value (0.0–1.0).
1434    pub async fn patch_memory_importance(
1435        &self,
1436        memory_id: &str,
1437        agent_id: &str,
1438        importance: f32,
1439    ) -> Result<FeedbackResponse> {
1440        let url = format!("{}/v1/memories/{}/importance", self.base_url, memory_id);
1441        let body = MemoryImportancePatch {
1442            agent_id: agent_id.to_string(),
1443            importance,
1444        };
1445        let response = self.client.patch(&url).json(&body).send().await?;
1446        self.handle_response(response).await
1447    }
1448
1449    /// Get overall feedback health score for an agent (INT-1).
1450    ///
1451    /// The health score is the mean importance of all non-expired memories (0.0–1.0).
1452    /// A higher score indicates a healthier, more relevant memory store.
1453    pub async fn get_feedback_health(&self, agent_id: &str) -> Result<FeedbackHealthResponse> {
1454        let url = format!("{}/v1/feedback/health?agent_id={}", self.base_url, agent_id);
1455        let response = self.client.get(&url).send().await?;
1456        self.handle_response(response).await
1457    }
1458
1459    // ========================================================================
1460    // Memory Knowledge Graph Operations (CE-5 / SDK-9)
1461    // ========================================================================
1462
1463    /// Traverse the knowledge graph from a memory node.
1464    ///
1465    /// Requires CE-5 (Memory Knowledge Graph) on the server.
1466    ///
1467    /// # Arguments
1468    /// * `memory_id` – Root memory ID to start traversal from.
1469    /// * `options` – Traversal options (depth, edge type filters).
1470    ///
1471    /// # Example
1472    /// ```no_run
1473    /// # use dakera_client::{DakeraClient, GraphOptions};
1474    /// # async fn example(client: &DakeraClient) -> dakera_client::Result<()> {
1475    /// let graph = client.memory_graph("mem-abc", GraphOptions::new().depth(2)).await?;
1476    /// println!("{} nodes, {} edges", graph.nodes.len(), graph.edges.len());
1477    /// # Ok(()) }
1478    /// ```
1479    pub async fn memory_graph(
1480        &self,
1481        memory_id: &str,
1482        options: GraphOptions,
1483    ) -> Result<MemoryGraph> {
1484        let mut url = format!("{}/v1/memories/{}/graph", self.base_url, memory_id);
1485        let depth = options.depth.unwrap_or(1);
1486        url.push_str(&format!("?depth={}", depth));
1487        if let Some(types) = &options.types {
1488            let type_strs: Vec<String> = types
1489                .iter()
1490                .map(|t| {
1491                    serde_json::to_value(t)
1492                        .unwrap()
1493                        .as_str()
1494                        .unwrap_or("")
1495                        .to_string()
1496                })
1497                .collect();
1498            if !type_strs.is_empty() {
1499                url.push_str(&format!("&types={}", type_strs.join(",")));
1500            }
1501        }
1502        let response = self.client.get(&url).send().await?;
1503        self.handle_response(response).await
1504    }
1505
1506    /// Find the shortest path between two memories in the knowledge graph.
1507    ///
1508    /// Requires CE-5 (Memory Knowledge Graph) on the server.
1509    ///
1510    /// # Example
1511    /// ```no_run
1512    /// # use dakera_client::DakeraClient;
1513    /// # async fn example(client: &DakeraClient) -> dakera_client::Result<()> {
1514    /// let path = client.memory_path("mem-abc", "mem-xyz").await?;
1515    /// println!("{} hops: {:?}", path.hops, path.path);
1516    /// # Ok(()) }
1517    /// ```
1518    pub async fn memory_path(&self, source_id: &str, target_id: &str) -> Result<GraphPath> {
1519        let url = format!(
1520            "{}/v1/memories/{}/path?target={}",
1521            self.base_url,
1522            source_id,
1523            urlencoding::encode(target_id)
1524        );
1525        let response = self.client.get(&url).send().await?;
1526        self.handle_response(response).await
1527    }
1528
1529    /// Create an explicit edge between two memories.
1530    ///
1531    /// Requires CE-5 (Memory Knowledge Graph) on the server.
1532    ///
1533    /// # Example
1534    /// ```no_run
1535    /// # use dakera_client::{DakeraClient, EdgeType};
1536    /// # async fn example(client: &DakeraClient) -> dakera_client::Result<()> {
1537    /// let resp = client.memory_link("mem-abc", "mem-xyz", EdgeType::LinkedBy).await?;
1538    /// println!("Created edge: {}", resp.edge.id);
1539    /// # Ok(()) }
1540    /// ```
1541    pub async fn memory_link(
1542        &self,
1543        source_id: &str,
1544        target_id: &str,
1545        edge_type: EdgeType,
1546    ) -> Result<GraphLinkResponse> {
1547        let url = format!("{}/v1/memories/{}/links", self.base_url, source_id);
1548        let request = GraphLinkRequest {
1549            target_id: target_id.to_string(),
1550            edge_type,
1551        };
1552        let response = self.client.post(&url).json(&request).send().await?;
1553        self.handle_response(response).await
1554    }
1555
1556    /// Export the full knowledge graph for an agent.
1557    ///
1558    /// Requires CE-5 (Memory Knowledge Graph) on the server.
1559    ///
1560    /// # Arguments
1561    /// * `agent_id` – Agent whose graph to export.
1562    /// * `format` – Export format: `"json"` (default), `"graphml"`, or `"csv"`.
1563    pub async fn agent_graph_export(&self, agent_id: &str, format: &str) -> Result<GraphExport> {
1564        let url = format!(
1565            "{}/v1/agents/{}/graph/export?format={}",
1566            self.base_url, agent_id, format
1567        );
1568        let response = self.client.get(&url).send().await?;
1569        self.handle_response(response).await
1570    }
1571
1572    // ========================================================================
1573    // Session Operations
1574    // ========================================================================
1575
1576    /// Start a new session for an agent
1577    pub async fn start_session(&self, agent_id: &str) -> Result<Session> {
1578        let url = format!("{}/v1/sessions/start", self.base_url);
1579        let request = SessionStartRequest {
1580            agent_id: agent_id.to_string(),
1581            metadata: None,
1582        };
1583        let response = self.client.post(&url).json(&request).send().await?;
1584        let resp: SessionStartResponse = self.handle_response(response).await?;
1585        Ok(resp.session)
1586    }
1587
1588    /// Start a session with metadata
1589    pub async fn start_session_with_metadata(
1590        &self,
1591        agent_id: &str,
1592        metadata: serde_json::Value,
1593    ) -> Result<Session> {
1594        let url = format!("{}/v1/sessions/start", self.base_url);
1595        let request = SessionStartRequest {
1596            agent_id: agent_id.to_string(),
1597            metadata: Some(metadata),
1598        };
1599        let response = self.client.post(&url).json(&request).send().await?;
1600        let resp: SessionStartResponse = self.handle_response(response).await?;
1601        Ok(resp.session)
1602    }
1603
1604    /// End a session, optionally with a summary.
1605    /// Returns the session state and the total memory count at close.
1606    pub async fn end_session(
1607        &self,
1608        session_id: &str,
1609        summary: Option<String>,
1610    ) -> Result<SessionEndResponse> {
1611        let url = format!("{}/v1/sessions/{}/end", self.base_url, session_id);
1612        let request = SessionEndRequest { summary };
1613        let response = self.client.post(&url).json(&request).send().await?;
1614        self.handle_response(response).await
1615    }
1616
1617    /// Get a session by ID
1618    pub async fn get_session(&self, session_id: &str) -> Result<Session> {
1619        let url = format!("{}/v1/sessions/{}", self.base_url, session_id);
1620        let response = self.client.get(&url).send().await?;
1621        self.handle_response(response).await
1622    }
1623
1624    /// List sessions for an agent
1625    pub async fn list_sessions(&self, agent_id: &str) -> Result<Vec<Session>> {
1626        let url = format!("{}/v1/sessions?agent_id={}", self.base_url, agent_id);
1627        let response = self.client.get(&url).send().await?;
1628        let wrapper: ListSessionsResponse = self.handle_response(response).await?;
1629        Ok(wrapper.sessions)
1630    }
1631
1632    /// Get memories in a session
1633    pub async fn session_memories(&self, session_id: &str) -> Result<RecallResponse> {
1634        let url = format!("{}/v1/sessions/{}/memories", self.base_url, session_id);
1635        let response = self.client.get(&url).send().await?;
1636        self.handle_response(response).await
1637    }
1638
1639    // ========================================================================
1640    // CE-2: Batch Recall / Forget
1641    // ========================================================================
1642
1643    /// Bulk-recall memories using filter predicates (CE-2).
1644    ///
1645    /// Uses `POST /v1/memories/recall/batch` — no embedding required.
1646    ///
1647    /// # Example
1648    ///
1649    /// ```rust,no_run
1650    /// use dakera_client::{DakeraClient, memory::{BatchRecallRequest, BatchMemoryFilter}};
1651    ///
1652    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1653    /// let client = DakeraClient::new("http://localhost:3000")?;
1654    ///
1655    /// let filter = BatchMemoryFilter::default().with_min_importance(0.7);
1656    /// let req = BatchRecallRequest::new("agent-1").with_filter(filter).with_limit(50);
1657    /// let resp = client.batch_recall(req).await?;
1658    /// println!("Found {} memories", resp.filtered);
1659    /// # Ok(())
1660    /// # }
1661    /// ```
1662    pub async fn batch_recall(&self, request: BatchRecallRequest) -> Result<BatchRecallResponse> {
1663        let url = format!("{}/v1/memories/recall/batch", self.base_url);
1664        let response = self.client.post(&url).json(&request).send().await?;
1665        self.handle_response(response).await
1666    }
1667
1668    /// Store up to 1 000 memories in a single batched call (DAK-5508).
1669    ///
1670    /// All memories are embedded in one ONNX inference pass and written to
1671    /// RocksDB in one batch, with HNSW invalidation happening exactly once —
1672    /// yielding ≥100× throughput vs. N sequential [`store_memory`] calls.
1673    ///
1674    /// The `stored` field in the response preserves the same ordering as the
1675    /// request items, so callers can map `response.stored[i].id` back to
1676    /// `request.memories[i]` by index.
1677    ///
1678    /// # Example
1679    ///
1680    /// ```rust,no_run
1681    /// use dakera_client::{DakeraClient, memory::{BatchStoreMemoryItem, BatchStoreMemoryRequest}};
1682    ///
1683    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1684    /// let client = DakeraClient::new("http://localhost:3000")?;
1685    ///
1686    /// let items = vec![
1687    ///     BatchStoreMemoryItem::new("The user prefers dark mode").with_importance(0.8),
1688    ///     BatchStoreMemoryItem::new("The user is based in Berlin").with_importance(0.7),
1689    /// ];
1690    /// let resp = client
1691    ///     .store_memories_batch(BatchStoreMemoryRequest::new("agent-1", items))
1692    ///     .await?;
1693    /// println!("Stored {} memories", resp.stored_count);
1694    /// # Ok(())
1695    /// # }
1696    /// ```
1697    pub async fn store_memories_batch(
1698        &self,
1699        request: BatchStoreMemoryRequest,
1700    ) -> Result<BatchStoreMemoryResponse> {
1701        let url = format!("{}/v1/memories/store/batch", self.base_url);
1702        let response = self.client.post(&url).json(&request).send().await?;
1703        self.handle_response(response).await
1704    }
1705
1706    /// Bulk-delete memories using filter predicates (CE-2).
1707    ///
1708    /// Uses `DELETE /v1/memories/forget/batch`.  The server requires at least
1709    /// one filter predicate to be set as a safety guard.
1710    ///
1711    /// # Example
1712    ///
1713    /// ```rust,no_run
1714    /// use dakera_client::{DakeraClient, memory::{BatchForgetRequest, BatchMemoryFilter}};
1715    ///
1716    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1717    /// let client = DakeraClient::new("http://localhost:3000")?;
1718    ///
1719    /// let filter = BatchMemoryFilter::default().with_min_importance(0.0).with_max_importance(0.2);
1720    /// let resp = client.batch_forget(BatchForgetRequest::new("agent-1", filter)).await?;
1721    /// println!("Deleted {} memories", resp.deleted_count);
1722    /// # Ok(())
1723    /// # }
1724    /// ```
1725    pub async fn batch_forget(&self, request: BatchForgetRequest) -> Result<BatchForgetResponse> {
1726        let url = format!("{}/v1/memories/forget/batch", self.base_url);
1727        let response = self.client.delete(&url).json(&request).send().await?;
1728        self.handle_response(response).await
1729    }
1730
1731    // ========================================================================
1732    // DX-1: Memory Import / Export
1733    // ========================================================================
1734
1735    /// Import memories from an external format (DX-1).
1736    ///
1737    /// Supported formats: `"jsonl"`, `"mem0"`, `"zep"`, `"csv"`.
1738    ///
1739    /// ```no_run
1740    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1741    /// let client = dakera_client::DakeraClient::new("http://localhost:3000")?;
1742    /// let data = serde_json::json!([{"content": "hello", "agent_id": "agent-1"}]);
1743    /// let resp = client.import_memories(data, "jsonl", None, None).await?;
1744    /// println!("Imported {} memories", resp.imported_count);
1745    /// # Ok(())
1746    /// # }
1747    /// ```
1748    pub async fn import_memories(
1749        &self,
1750        data: serde_json::Value,
1751        format: &str,
1752        agent_id: Option<&str>,
1753        namespace: Option<&str>,
1754    ) -> Result<MemoryImportResponse> {
1755        let mut body = serde_json::json!({"data": data, "format": format});
1756        if let Some(aid) = agent_id {
1757            body["agent_id"] = serde_json::Value::String(aid.to_string());
1758        }
1759        if let Some(ns) = namespace {
1760            body["namespace"] = serde_json::Value::String(ns.to_string());
1761        }
1762        let url = format!("{}/v1/import", self.base_url);
1763        let response = self.client.post(&url).json(&body).send().await?;
1764        self.handle_response(response).await
1765    }
1766
1767    /// Export memories in a portable format (DX-1).
1768    ///
1769    /// Supported formats: `"jsonl"`, `"mem0"`, `"zep"`, `"csv"`.
1770    pub async fn export_memories(
1771        &self,
1772        format: &str,
1773        agent_id: Option<&str>,
1774        namespace: Option<&str>,
1775        limit: Option<u32>,
1776    ) -> Result<MemoryExportResponse> {
1777        let mut params = vec![("format", format.to_string())];
1778        if let Some(aid) = agent_id {
1779            params.push(("agent_id", aid.to_string()));
1780        }
1781        if let Some(ns) = namespace {
1782            params.push(("namespace", ns.to_string()));
1783        }
1784        if let Some(l) = limit {
1785            params.push(("limit", l.to_string()));
1786        }
1787        let url = format!("{}/v1/export", self.base_url);
1788        let response = self.client.get(&url).query(&params).send().await?;
1789        self.handle_response(response).await
1790    }
1791
1792    // ========================================================================
1793    // OBS-1: Business-Event Audit Log
1794    // ========================================================================
1795
1796    /// List paginated audit log entries (OBS-1).
1797    pub async fn list_audit_events(&self, query: AuditQuery) -> Result<AuditListResponse> {
1798        let url = format!("{}/v1/audit", self.base_url);
1799        let response = self.client.get(&url).query(&query).send().await?;
1800        self.handle_response(response).await
1801    }
1802
1803    /// Stream live audit events via SSE (OBS-1).
1804    ///
1805    /// Returns a [`tokio::sync::mpsc::Receiver`] that yields [`DakeraEvent`] results.
1806    pub async fn stream_audit_events(
1807        &self,
1808        agent_id: Option<&str>,
1809        event_type: Option<&str>,
1810    ) -> Result<tokio::sync::mpsc::Receiver<Result<crate::events::DakeraEvent>>> {
1811        let mut params: Vec<(&str, String)> = Vec::new();
1812        if let Some(aid) = agent_id {
1813            params.push(("agent_id", aid.to_string()));
1814        }
1815        if let Some(et) = event_type {
1816            params.push(("event_type", et.to_string()));
1817        }
1818        let base = format!("{}/v1/audit/stream", self.base_url);
1819        let url = if params.is_empty() {
1820            base
1821        } else {
1822            let qs = params
1823                .iter()
1824                .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
1825                .collect::<Vec<_>>()
1826                .join("&");
1827            format!("{}?{}", base, qs)
1828        };
1829        self.stream_sse(url).await
1830    }
1831
1832    /// Bulk-export audit log entries (OBS-1).
1833    pub async fn export_audit(
1834        &self,
1835        format: &str,
1836        agent_id: Option<&str>,
1837        event_type: Option<&str>,
1838        from_ts: Option<u64>,
1839        to_ts: Option<u64>,
1840    ) -> Result<AuditExportResponse> {
1841        let mut body = serde_json::json!({"format": format});
1842        if let Some(aid) = agent_id {
1843            body["agent_id"] = serde_json::Value::String(aid.to_string());
1844        }
1845        if let Some(et) = event_type {
1846            body["event_type"] = serde_json::Value::String(et.to_string());
1847        }
1848        if let Some(f) = from_ts {
1849            body["from"] = serde_json::Value::Number(f.into());
1850        }
1851        if let Some(t) = to_ts {
1852            body["to"] = serde_json::Value::Number(t.into());
1853        }
1854        let url = format!("{}/v1/audit/export", self.base_url);
1855        let response = self.client.post(&url).json(&body).send().await?;
1856        self.handle_response(response).await
1857    }
1858
1859    // ========================================================================
1860    // EXT-1: External Extraction Providers
1861    // ========================================================================
1862
1863    /// Extract entities from text using a pluggable provider (EXT-1).
1864    ///
1865    /// Provider hierarchy: per-request > namespace default > GLiNER (bundled).
1866    /// Supported providers: `"gliner"`, `"openai"`, `"anthropic"`, `"openrouter"`, `"ollama"`.
1867    pub async fn extract_text(
1868        &self,
1869        text: &str,
1870        namespace: Option<&str>,
1871        provider: Option<&str>,
1872        model: Option<&str>,
1873    ) -> Result<ExtractionResult> {
1874        let mut body = serde_json::json!({"text": text});
1875        if let Some(ns) = namespace {
1876            body["namespace"] = serde_json::Value::String(ns.to_string());
1877        }
1878        if let Some(p) = provider {
1879            body["provider"] = serde_json::Value::String(p.to_string());
1880        }
1881        if let Some(m) = model {
1882            body["model"] = serde_json::Value::String(m.to_string());
1883        }
1884        let url = format!("{}/v1/extract", self.base_url);
1885        let response = self.client.post(&url).json(&body).send().await?;
1886        self.handle_response(response).await
1887    }
1888
1889    /// List available extraction providers and their models (EXT-1).
1890    pub async fn list_extract_providers(&self) -> Result<Vec<ExtractionProviderInfo>> {
1891        let url = format!("{}/v1/extract/providers", self.base_url);
1892        let response = self.client.get(&url).send().await?;
1893        let result: ExtractProvidersResponse = self.handle_response(response).await?;
1894        Ok(match result {
1895            ExtractProvidersResponse::List(v) => v,
1896            ExtractProvidersResponse::Object { providers } => providers,
1897        })
1898    }
1899
1900    /// Set the default extraction provider for a namespace (EXT-1).
1901    pub async fn configure_namespace_extractor(
1902        &self,
1903        namespace: &str,
1904        provider: &str,
1905        model: Option<&str>,
1906    ) -> Result<serde_json::Value> {
1907        let mut body = serde_json::json!({"provider": provider});
1908        if let Some(m) = model {
1909            body["model"] = serde_json::Value::String(m.to_string());
1910        }
1911        let url = format!(
1912            "{}/v1/namespaces/{}/extractor",
1913            self.base_url,
1914            urlencoding::encode(namespace)
1915        );
1916        let response = self.client.patch(&url).json(&body).send().await?;
1917        self.handle_response(response).await
1918    }
1919
1920    // =========================================================================
1921    // SEC-3: AES-256-GCM Encryption Key Rotation
1922    // =========================================================================
1923
1924    /// Re-encrypt all memory content blobs with a new AES-256-GCM key (SEC-3).
1925    ///
1926    /// After this call the new key is active in the running process.
1927    /// The operator must update `DAKERA_ENCRYPTION_KEY` and restart to make
1928    /// the rotation durable across restarts.
1929    ///
1930    /// Requires Admin scope.
1931    ///
1932    /// # Arguments
1933    /// * `new_key` - New passphrase or 64-char hex key.
1934    /// * `namespace` - If `Some`, rotate only this namespace. `None` rotates all.
1935    pub async fn rotate_encryption_key(
1936        &self,
1937        new_key: &str,
1938        namespace: Option<&str>,
1939    ) -> Result<RotateEncryptionKeyResponse> {
1940        let body = RotateEncryptionKeyRequest {
1941            new_key: new_key.to_string(),
1942            namespace: namespace.map(|s| s.to_string()),
1943        };
1944        let url = format!("{}/v1/admin/encryption/rotate-key", self.base_url);
1945        let response = self.client.post(&url).json(&body).send().await?;
1946        self.handle_response(response).await
1947    }
1948}
1949
1950// ============================================================================
1951// Tests
1952// ============================================================================
1953
1954#[cfg(test)]
1955mod tests {
1956    use super::*;
1957
1958    // -------------------------------------------------------------------------
1959    // MemoryType serialization
1960    // -------------------------------------------------------------------------
1961
1962    #[test]
1963    fn test_memory_type_serializes_lowercase() {
1964        assert_eq!(
1965            serde_json::to_string(&MemoryType::Episodic).unwrap(),
1966            "\"episodic\""
1967        );
1968        assert_eq!(
1969            serde_json::to_string(&MemoryType::Semantic).unwrap(),
1970            "\"semantic\""
1971        );
1972        assert_eq!(
1973            serde_json::to_string(&MemoryType::Procedural).unwrap(),
1974            "\"procedural\""
1975        );
1976        assert_eq!(
1977            serde_json::to_string(&MemoryType::Working).unwrap(),
1978            "\"working\""
1979        );
1980    }
1981
1982    #[test]
1983    fn test_memory_type_default_is_episodic() {
1984        let serialized = serde_json::to_string(&MemoryType::default()).unwrap();
1985        assert_eq!(serialized, "\"episodic\"");
1986    }
1987
1988    #[test]
1989    fn test_memory_type_deserializes() {
1990        let mt: MemoryType = serde_json::from_str("\"semantic\"").unwrap();
1991        assert!(matches!(mt, MemoryType::Semantic));
1992    }
1993
1994    // -------------------------------------------------------------------------
1995    // FusionStrategy serialization
1996    // -------------------------------------------------------------------------
1997
1998    #[test]
1999    fn test_fusion_strategy_rrf_serializes() {
2000        assert_eq!(
2001            serde_json::to_string(&FusionStrategy::Rrf).unwrap(),
2002            "\"rrf\""
2003        );
2004    }
2005
2006    #[test]
2007    fn test_fusion_strategy_minmax_serializes_as_minmax() {
2008        // The rename attribute maps MinMax → "minmax" (not "min_max")
2009        assert_eq!(
2010            serde_json::to_string(&FusionStrategy::MinMax).unwrap(),
2011            "\"minmax\""
2012        );
2013    }
2014
2015    #[test]
2016    fn test_fusion_strategy_default_is_rrf() {
2017        assert_eq!(FusionStrategy::default(), FusionStrategy::Rrf);
2018    }
2019
2020    // -------------------------------------------------------------------------
2021    // RoutingMode serialization
2022    // -------------------------------------------------------------------------
2023
2024    #[test]
2025    fn test_routing_mode_serializes_snake_case() {
2026        assert_eq!(
2027            serde_json::to_string(&RoutingMode::Auto).unwrap(),
2028            "\"auto\""
2029        );
2030        assert_eq!(
2031            serde_json::to_string(&RoutingMode::Vector).unwrap(),
2032            "\"vector\""
2033        );
2034        assert_eq!(
2035            serde_json::to_string(&RoutingMode::Bm25).unwrap(),
2036            "\"bm25\""
2037        );
2038        assert_eq!(
2039            serde_json::to_string(&RoutingMode::Hybrid).unwrap(),
2040            "\"hybrid\""
2041        );
2042    }
2043
2044    #[test]
2045    fn test_routing_mode_deserializes() {
2046        let mode: RoutingMode = serde_json::from_str("\"hybrid\"").unwrap();
2047        assert_eq!(mode, RoutingMode::Hybrid);
2048    }
2049
2050    // -------------------------------------------------------------------------
2051    // StoreMemoryRequest serialization
2052    // -------------------------------------------------------------------------
2053
2054    #[test]
2055    fn test_store_memory_request_minimal_no_optional_fields() {
2056        let req = StoreMemoryRequest::new("agent-1", "hello world");
2057        let json = serde_json::to_string(&req).unwrap();
2058        // Optional fields absent — no session_id, metadata, ttl_seconds, expires_at
2059        assert!(!json.contains("session_id"));
2060        assert!(!json.contains("metadata"));
2061        assert!(!json.contains("ttl_seconds"));
2062        assert!(!json.contains("expires_at"));
2063        assert!(json.contains("\"agent_id\":\"agent-1\""));
2064        assert!(json.contains("\"content\":\"hello world\""));
2065    }
2066
2067    #[test]
2068    fn test_store_memory_request_with_session_emits_session_id() {
2069        let req = StoreMemoryRequest::new("agent-1", "content").with_session("sess-abc");
2070        let json = serde_json::to_string(&req).unwrap();
2071        assert!(json.contains("\"session_id\":\"sess-abc\""));
2072    }
2073
2074    #[test]
2075    fn test_store_memory_request_with_ttl() {
2076        let req = StoreMemoryRequest::new("agent-1", "content").with_ttl(3600);
2077        let json = serde_json::to_string(&req).unwrap();
2078        assert!(json.contains("\"ttl_seconds\":3600"));
2079        assert!(!json.contains("expires_at"));
2080    }
2081
2082    #[test]
2083    fn test_store_memory_request_with_expires_at() {
2084        let req = StoreMemoryRequest::new("a", "c").with_expires_at(1800000000);
2085        let json = serde_json::to_string(&req).unwrap();
2086        assert!(json.contains("\"expires_at\":1800000000"));
2087    }
2088
2089    #[test]
2090    fn test_store_memory_request_importance_clamps_to_zero_one() {
2091        let req = StoreMemoryRequest::new("a", "c").with_importance(1.5);
2092        assert!((req.importance - 1.0).abs() < 1e-6);
2093
2094        let req2 = StoreMemoryRequest::new("a", "c").with_importance(-0.5);
2095        assert!((req2.importance - 0.0).abs() < 1e-6);
2096    }
2097
2098    #[test]
2099    fn test_store_memory_request_default_importance_is_half() {
2100        let req = StoreMemoryRequest::new("a", "c");
2101        assert!((req.importance - 0.5).abs() < 1e-6);
2102    }
2103
2104    // -------------------------------------------------------------------------
2105    // StoreMemoryResponse custom deserialization
2106    // -------------------------------------------------------------------------
2107
2108    #[test]
2109    fn test_store_memory_response_deserializes_server_format() {
2110        let json = r#"{
2111            "memory": {
2112                "id": "mem-abc123",
2113                "agent_id": "agent-1",
2114                "namespace": "default"
2115            },
2116            "embedding_time_ms": 42
2117        }"#;
2118        let resp: StoreMemoryResponse = serde_json::from_str(json).unwrap();
2119        assert_eq!(resp.memory_id, "mem-abc123");
2120        assert_eq!(resp.agent_id, "agent-1");
2121        assert_eq!(resp.namespace, "default");
2122        assert_eq!(resp.embedding_time_ms, Some(42));
2123    }
2124
2125    #[test]
2126    fn test_store_memory_response_server_format_namespace_defaults_to_default() {
2127        let json = r#"{"memory": {"id": "mem-x", "agent_id": "a"}, "embedding_time_ms": 10}"#;
2128        let resp: StoreMemoryResponse = serde_json::from_str(json).unwrap();
2129        assert_eq!(resp.namespace, "default");
2130    }
2131
2132    #[test]
2133    fn test_store_memory_response_deserializes_legacy_format() {
2134        let json = r#"{"memory_id": "mem-legacy", "agent_id": "a", "namespace": "custom"}"#;
2135        let resp: StoreMemoryResponse = serde_json::from_str(json).unwrap();
2136        assert_eq!(resp.memory_id, "mem-legacy");
2137        assert_eq!(resp.namespace, "custom");
2138        assert!(resp.embedding_time_ms.is_none());
2139    }
2140
2141    #[test]
2142    fn test_store_memory_response_missing_id_returns_error() {
2143        // Server format without "id" should fail
2144        let json = r#"{"memory": {"agent_id": "a"}, "embedding_time_ms": 5}"#;
2145        assert!(serde_json::from_str::<StoreMemoryResponse>(json).is_err());
2146    }
2147
2148    // -------------------------------------------------------------------------
2149    // RecallRequest serialization
2150    // -------------------------------------------------------------------------
2151
2152    #[test]
2153    fn test_recall_request_minimal_no_optional_keys() {
2154        let req = RecallRequest::new("agent-1", "what did I eat?");
2155        let json = serde_json::to_string(&req).unwrap();
2156        // Optional fields absent
2157        assert!(!json.contains("memory_type"));
2158        assert!(!json.contains("session_id"));
2159        assert!(!json.contains("since"));
2160        assert!(!json.contains("until"));
2161        assert!(!json.contains("routing"));
2162        assert!(!json.contains("rerank"));
2163        assert!(!json.contains("fusion"));
2164        assert!(!json.contains("vector_weight"));
2165        assert!(!json.contains("iterations"));
2166        assert!(!json.contains("neighborhood"));
2167        // include_associated is false and uses skip_serializing_if="not"
2168        assert!(!json.contains("include_associated"));
2169        assert!(json.contains("\"agent_id\":\"agent-1\""));
2170        assert!(json.contains("\"top_k\":5"));
2171    }
2172
2173    #[test]
2174    fn test_recall_request_with_fusion_emits_fusion() {
2175        let req = RecallRequest::new("a", "q").with_fusion(FusionStrategy::MinMax);
2176        let json = serde_json::to_string(&req).unwrap();
2177        assert!(json.contains("\"fusion\":\"minmax\""));
2178    }
2179
2180    #[test]
2181    fn test_recall_request_with_routing_bm25() {
2182        let req = RecallRequest::new("a", "q").with_routing(RoutingMode::Bm25);
2183        let json = serde_json::to_string(&req).unwrap();
2184        assert!(json.contains("\"routing\":\"bm25\""));
2185    }
2186
2187    #[test]
2188    fn test_recall_request_with_associated_sets_flag() {
2189        let req = RecallRequest::new("a", "q").with_associated();
2190        assert!(req.include_associated);
2191        let json = serde_json::to_string(&req).unwrap();
2192        assert!(json.contains("\"include_associated\":true"));
2193    }
2194
2195    #[test]
2196    fn test_recall_request_with_associated_depth_implies_associated() {
2197        let req = RecallRequest::new("a", "q").with_associated_depth(2);
2198        assert!(req.include_associated);
2199        assert_eq!(req.associated_memories_depth, Some(2));
2200    }
2201
2202    #[test]
2203    fn test_recall_request_with_associated_cap_implies_associated() {
2204        let req = RecallRequest::new("a", "q").with_associated_cap(5);
2205        assert!(req.include_associated);
2206        assert_eq!(req.associated_memories_cap, Some(5));
2207    }
2208
2209    #[test]
2210    fn test_recall_request_with_rerank_false() {
2211        let req = RecallRequest::new("a", "q").with_rerank(false);
2212        let json = serde_json::to_string(&req).unwrap();
2213        assert!(json.contains("\"rerank\":false"));
2214    }
2215
2216    #[test]
2217    fn test_recall_request_with_neighborhood_false() {
2218        let req = RecallRequest::new("a", "q").with_neighborhood(false);
2219        let json = serde_json::to_string(&req).unwrap();
2220        assert!(json.contains("\"neighborhood\":false"));
2221    }
2222
2223    // -------------------------------------------------------------------------
2224    // ForgetRequest factory methods
2225    // -------------------------------------------------------------------------
2226
2227    #[test]
2228    fn test_forget_request_by_ids_serializes() {
2229        let req = ForgetRequest::by_ids("agent-1", vec!["mem-1".to_string(), "mem-2".to_string()]);
2230        let json = serde_json::to_string(&req).unwrap();
2231        assert!(json.contains("\"memory_ids\":[\"mem-1\",\"mem-2\"]"));
2232        assert!(json.contains("\"agent_id\":\"agent-1\""));
2233        assert!(!json.contains("session_id"));
2234        assert!(!json.contains("before_timestamp"));
2235    }
2236
2237    #[test]
2238    fn test_forget_request_by_tags_serializes() {
2239        let req = ForgetRequest::by_tags("a", vec!["tag-x".to_string()]);
2240        let json = serde_json::to_string(&req).unwrap();
2241        assert!(json.contains("\"tags\":[\"tag-x\"]"));
2242    }
2243
2244    #[test]
2245    fn test_forget_request_by_session_serializes() {
2246        let req = ForgetRequest::by_session("agent-1", "sess-abc");
2247        let json = serde_json::to_string(&req).unwrap();
2248        assert!(json.contains("\"session_id\":\"sess-abc\""));
2249    }
2250
2251    #[test]
2252    fn test_forget_response_deserializes() {
2253        let json = r#"{"deleted_count": 7}"#;
2254        let resp: ForgetResponse = serde_json::from_str(json).unwrap();
2255        assert_eq!(resp.deleted_count, 7);
2256    }
2257
2258    // -------------------------------------------------------------------------
2259    // Session deserialization
2260    // -------------------------------------------------------------------------
2261
2262    #[test]
2263    fn test_session_deserializes_minimal() {
2264        let json = r#"{
2265            "id": "sess-1",
2266            "agent_id": "agent-1",
2267            "started_at": 1785000000
2268        }"#;
2269        let session: Session = serde_json::from_str(json).unwrap();
2270        assert_eq!(session.id, "sess-1");
2271        assert!(session.ended_at.is_none());
2272        assert!(session.summary.is_none());
2273        assert_eq!(session.memory_count, 0); // default
2274    }
2275
2276    #[test]
2277    fn test_session_deserializes_with_all_fields() {
2278        let json = r#"{
2279            "id": "sess-2",
2280            "agent_id": "agent-1",
2281            "started_at": 1785000000,
2282            "ended_at": 1785001000,
2283            "summary": "completed task",
2284            "memory_count": 5
2285        }"#;
2286        let session: Session = serde_json::from_str(json).unwrap();
2287        assert_eq!(session.ended_at, Some(1785001000));
2288        assert_eq!(session.summary.as_deref(), Some("completed task"));
2289        assert_eq!(session.memory_count, 5);
2290    }
2291
2292    // -------------------------------------------------------------------------
2293    // UpdateMemoryRequest
2294    // -------------------------------------------------------------------------
2295
2296    #[test]
2297    fn test_update_memory_request_all_none_emits_empty_object() {
2298        let req = UpdateMemoryRequest {
2299            content: None,
2300            metadata: None,
2301            memory_type: None,
2302        };
2303        let json = serde_json::to_string(&req).unwrap();
2304        assert_eq!(json, "{}");
2305    }
2306
2307    #[test]
2308    fn test_update_memory_request_with_content_only() {
2309        let req = UpdateMemoryRequest {
2310            content: Some("new content".to_string()),
2311            metadata: None,
2312            memory_type: None,
2313        };
2314        let json = serde_json::to_string(&req).unwrap();
2315        assert!(json.contains("\"content\":\"new content\""));
2316        assert!(!json.contains("metadata"));
2317        assert!(!json.contains("memory_type"));
2318    }
2319}