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