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}
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    /// The ranking score — equals `smart_score` when present, then `weighted_score`, then raw `score`.
446    pub score: f32,
447    /// Raw smart_score from the server (the primary ranking key).
448    #[serde(skip_serializing_if = "Option::is_none")]
449    pub smart_score: Option<f32>,
450    /// Raw weighted_score from the server.
451    #[serde(skip_serializing_if = "Option::is_none")]
452    pub weighted_score: Option<f32>,
453    #[serde(default)]
454    pub tags: Vec<String>,
455    #[serde(skip_serializing_if = "Option::is_none")]
456    pub session_id: Option<String>,
457    #[serde(skip_serializing_if = "Option::is_none")]
458    pub metadata: Option<serde_json::Value>,
459    pub created_at: u64,
460    pub last_accessed_at: u64,
461    pub access_count: u32,
462    /// KG-3: hop depth at which this memory was found (only set on associated memories)
463    #[serde(skip_serializing_if = "Option::is_none")]
464    pub depth: Option<u8>,
465}
466
467impl<'de> serde::Deserialize<'de> for RecalledMemory {
468    fn deserialize<D: serde::Deserializer<'de>>(
469        deserializer: D,
470    ) -> std::result::Result<Self, D::Error> {
471        use serde::de::Error as _;
472        let val = serde_json::Value::deserialize(deserializer)?;
473
474        // Server wraps recall results as {memory:{...}, score, weighted_score, smart_score}.
475        // smart_score is the actual ranking key (server sorts by it); prefer it.
476        // Fall back to flat format for direct memory-get responses.
477        let smart_score = val
478            .get("smart_score")
479            .and_then(|v| v.as_f64())
480            .map(|v| v as f32);
481        let weighted_score = val
482            .get("weighted_score")
483            .and_then(|v| v.as_f64())
484            .map(|v| v as f32);
485        let score = smart_score
486            .or(weighted_score)
487            .or_else(|| val.get("score").and_then(|v| v.as_f64()).map(|v| v as f32))
488            .unwrap_or(0.0);
489
490        let mem = val.get("memory").unwrap_or(&val);
491
492        let id = mem
493            .get("id")
494            .and_then(|v| v.as_str())
495            .ok_or_else(|| D::Error::missing_field("id"))?
496            .to_string();
497        let content = mem
498            .get("content")
499            .and_then(|v| v.as_str())
500            .ok_or_else(|| D::Error::missing_field("content"))?
501            .to_string();
502        let memory_type: MemoryType = mem
503            .get("memory_type")
504            .and_then(|v| serde_json::from_value(v.clone()).ok())
505            .unwrap_or(MemoryType::Episodic);
506        let importance = mem
507            .get("importance")
508            .and_then(|v| v.as_f64())
509            .unwrap_or(0.5) as f32;
510        let tags: Vec<String> = mem
511            .get("tags")
512            .and_then(|v| serde_json::from_value(v.clone()).ok())
513            .unwrap_or_default();
514        let session_id = mem
515            .get("session_id")
516            .and_then(|v| v.as_str())
517            .map(String::from);
518        let metadata = mem.get("metadata").cloned().filter(|v| !v.is_null());
519        let created_at = mem.get("created_at").and_then(|v| v.as_u64()).unwrap_or(0);
520        let last_accessed_at = mem
521            .get("last_accessed_at")
522            .and_then(|v| v.as_u64())
523            .unwrap_or(0);
524        let access_count = mem
525            .get("access_count")
526            .and_then(|v| v.as_u64())
527            .unwrap_or(0) as u32;
528        let depth = mem.get("depth").and_then(|v| v.as_u64()).map(|v| v as u8);
529
530        Ok(Self {
531            id,
532            content,
533            memory_type,
534            importance,
535            score,
536            smart_score,
537            weighted_score,
538            tags,
539            session_id,
540            metadata,
541            created_at,
542            last_accessed_at,
543            access_count,
544            depth,
545        })
546    }
547}
548
549/// Recall response
550#[derive(Debug, Clone, Serialize, Deserialize)]
551pub struct RecallResponse {
552    pub memories: Vec<RecalledMemory>,
553    #[serde(default)]
554    pub total_found: usize,
555    /// COG-2 / KG-3: KG associated memories at configurable depth (only present when include_associated was true)
556    #[serde(skip_serializing_if = "Option::is_none")]
557    pub associated_memories: Option<Vec<RecalledMemory>>,
558}
559
560/// Forget (delete) memories request
561#[derive(Debug, Clone, Serialize, Deserialize)]
562pub struct ForgetRequest {
563    pub agent_id: String,
564    #[serde(default)]
565    pub memory_ids: Vec<String>,
566    #[serde(default)]
567    pub tags: Vec<String>,
568    #[serde(skip_serializing_if = "Option::is_none")]
569    pub session_id: Option<String>,
570    #[serde(skip_serializing_if = "Option::is_none")]
571    pub before_timestamp: Option<u64>,
572}
573
574impl ForgetRequest {
575    /// Forget specific memories by ID
576    pub fn by_ids(agent_id: impl Into<String>, ids: Vec<String>) -> Self {
577        Self {
578            agent_id: agent_id.into(),
579            memory_ids: ids,
580            tags: Vec::new(),
581            session_id: None,
582            before_timestamp: None,
583        }
584    }
585
586    /// Forget memories with specific tags
587    pub fn by_tags(agent_id: impl Into<String>, tags: Vec<String>) -> Self {
588        Self {
589            agent_id: agent_id.into(),
590            memory_ids: Vec::new(),
591            tags,
592            session_id: None,
593            before_timestamp: None,
594        }
595    }
596
597    /// Forget all memories in a session
598    pub fn by_session(agent_id: impl Into<String>, session_id: impl Into<String>) -> Self {
599        Self {
600            agent_id: agent_id.into(),
601            memory_ids: Vec::new(),
602            tags: Vec::new(),
603            session_id: Some(session_id.into()),
604            before_timestamp: None,
605        }
606    }
607}
608
609/// Forget response
610#[derive(Debug, Clone, Serialize, Deserialize)]
611pub struct ForgetResponse {
612    pub deleted_count: u64,
613}
614
615/// Session start request
616#[derive(Debug, Clone, Serialize, Deserialize)]
617pub struct SessionStartRequest {
618    pub agent_id: String,
619    #[serde(skip_serializing_if = "Option::is_none")]
620    pub metadata: Option<serde_json::Value>,
621}
622
623/// Session information
624#[derive(Debug, Clone, Serialize, Deserialize)]
625pub struct Session {
626    pub id: String,
627    pub agent_id: String,
628    pub started_at: u64,
629    #[serde(skip_serializing_if = "Option::is_none")]
630    pub ended_at: Option<u64>,
631    #[serde(skip_serializing_if = "Option::is_none")]
632    pub summary: Option<String>,
633    #[serde(skip_serializing_if = "Option::is_none")]
634    pub metadata: Option<serde_json::Value>,
635    /// Cached count of memories in this session
636    #[serde(default)]
637    pub memory_count: usize,
638}
639
640/// Session end request
641#[derive(Debug, Clone, Serialize, Deserialize)]
642pub struct SessionEndRequest {
643    #[serde(skip_serializing_if = "Option::is_none")]
644    pub summary: Option<String>,
645}
646
647/// Response from `POST /v1/sessions/start`
648#[derive(Debug, Clone, Serialize, Deserialize)]
649pub struct SessionStartResponse {
650    pub session: Session,
651}
652
653/// Response from `POST /v1/sessions/{id}/end`
654#[derive(Debug, Clone, Serialize, Deserialize)]
655pub struct SessionEndResponse {
656    pub session: Session,
657    pub memory_count: usize,
658}
659
660/// Response from `GET /v1/sessions`
661#[derive(Debug, Clone, Deserialize)]
662pub struct ListSessionsResponse {
663    pub sessions: Vec<Session>,
664    #[allow(dead_code)]
665    pub total: usize,
666}
667
668/// Request to update a memory
669#[derive(Debug, Clone, Serialize, Deserialize)]
670pub struct UpdateMemoryRequest {
671    #[serde(skip_serializing_if = "Option::is_none")]
672    pub content: Option<String>,
673    #[serde(skip_serializing_if = "Option::is_none")]
674    pub metadata: Option<serde_json::Value>,
675    #[serde(skip_serializing_if = "Option::is_none")]
676    pub memory_type: Option<MemoryType>,
677}
678
679/// Request to update memory importance
680#[derive(Debug, Clone, Serialize, Deserialize)]
681pub struct UpdateImportanceRequest {
682    pub memory_ids: Vec<String>,
683    pub importance: f32,
684}
685
686/// DBSCAN algorithm config for adaptive consolidation (CE-6).
687#[derive(Debug, Clone, Serialize, Deserialize, Default)]
688pub struct ConsolidationConfig {
689    /// Clustering algorithm: `"dbscan"` (default) or `"greedy"`.
690    #[serde(skip_serializing_if = "Option::is_none")]
691    pub algorithm: Option<String>,
692    /// Minimum cluster samples for DBSCAN.
693    #[serde(skip_serializing_if = "Option::is_none")]
694    pub min_samples: Option<u32>,
695    /// Epsilon distance parameter for DBSCAN.
696    #[serde(skip_serializing_if = "Option::is_none")]
697    pub eps: Option<f32>,
698}
699
700/// One step in the consolidation execution log (CE-6).
701#[derive(Debug, Clone, Serialize, Deserialize)]
702pub struct ConsolidationLogEntry {
703    pub step: String,
704    pub memories_before: usize,
705    pub memories_after: usize,
706    pub duration_ms: f64,
707}
708
709/// Request to consolidate memories
710#[derive(Debug, Clone, Serialize, Deserialize, Default)]
711pub struct ConsolidateRequest {
712    #[serde(skip_serializing_if = "Option::is_none")]
713    pub memory_type: Option<String>,
714    #[serde(skip_serializing_if = "Option::is_none")]
715    pub threshold: Option<f32>,
716    #[serde(default)]
717    pub dry_run: bool,
718    /// Optional DBSCAN algorithm configuration (CE-6).
719    #[serde(skip_serializing_if = "Option::is_none")]
720    pub config: Option<ConsolidationConfig>,
721}
722
723/// Response from consolidation (`POST /v1/memory/consolidate`).
724///
725/// The server returns `{"memories_removed": N, "source_memory_ids": [...], "consolidated_memory": {...}}`.
726/// `consolidated_count` is mapped from `memories_removed` for backward compat.
727#[derive(Debug, Clone, Serialize)]
728pub struct ConsolidateResponse {
729    /// Number of source memories removed (= `memories_removed` from server)
730    pub consolidated_count: usize,
731    /// Alias for consolidated_count
732    pub removed_count: usize,
733    /// IDs of source memories that were removed
734    #[serde(default)]
735    pub new_memories: Vec<String>,
736    /// Step-by-step consolidation log (CE-6, optional).
737    #[serde(default, skip_serializing_if = "Vec::is_empty")]
738    pub log: Vec<ConsolidationLogEntry>,
739}
740
741impl<'de> serde::Deserialize<'de> for ConsolidateResponse {
742    fn deserialize<D: serde::Deserializer<'de>>(
743        deserializer: D,
744    ) -> std::result::Result<Self, D::Error> {
745        let val = serde_json::Value::deserialize(deserializer)?;
746        // Server format: {"consolidated_memory":{...}, "source_memory_ids":[...], "memories_removed": N}
747        let removed = val
748            .get("memories_removed")
749            .and_then(|v| v.as_u64())
750            .or_else(|| val.get("removed_count").and_then(|v| v.as_u64()))
751            .or_else(|| val.get("consolidated_count").and_then(|v| v.as_u64()))
752            .unwrap_or(0) as usize;
753        let source_ids: Vec<String> = val
754            .get("source_memory_ids")
755            .and_then(|v| v.as_array())
756            .map(|arr| {
757                arr.iter()
758                    .filter_map(|v| v.as_str().map(String::from))
759                    .collect()
760            })
761            .unwrap_or_default();
762        Ok(Self {
763            consolidated_count: removed,
764            removed_count: removed,
765            new_memories: source_ids,
766            log: vec![],
767        })
768    }
769}
770
771// ============================================================================
772// DX-1: Memory Import / Export
773// ============================================================================
774
775/// Response from `POST /v1/import` (DX-1).
776#[derive(Debug, Clone, Serialize, Deserialize)]
777pub struct MemoryImportResponse {
778    pub imported_count: usize,
779    pub skipped_count: usize,
780    #[serde(default)]
781    pub errors: Vec<String>,
782}
783
784/// Response from `GET /v1/export` (DX-1).
785#[derive(Debug, Clone, Serialize, Deserialize)]
786pub struct MemoryExportResponse {
787    pub data: Vec<serde_json::Value>,
788    pub format: String,
789    pub count: usize,
790}
791
792// ============================================================================
793// OBS-1: Business-Event Audit Log
794// ============================================================================
795
796/// A single business-event entry from the audit log (OBS-1).
797#[derive(Debug, Clone, Serialize, Deserialize)]
798pub struct AuditEvent {
799    pub id: String,
800    pub event_type: String,
801    #[serde(skip_serializing_if = "Option::is_none")]
802    pub agent_id: Option<String>,
803    #[serde(skip_serializing_if = "Option::is_none")]
804    pub namespace: Option<String>,
805    pub timestamp: u64,
806    #[serde(default)]
807    pub details: serde_json::Value,
808}
809
810/// Response from `GET /v1/audit` (OBS-1).
811#[derive(Debug, Clone, Serialize, Deserialize)]
812pub struct AuditListResponse {
813    pub events: Vec<AuditEvent>,
814    pub total: usize,
815    #[serde(skip_serializing_if = "Option::is_none")]
816    pub cursor: Option<String>,
817}
818
819/// Response from `POST /v1/audit/export` (OBS-1).
820#[derive(Debug, Clone, Serialize, Deserialize)]
821pub struct AuditExportResponse {
822    pub data: String,
823    pub format: String,
824    pub count: usize,
825}
826
827/// Query parameters for the audit log (OBS-1).
828#[derive(Debug, Clone, Serialize, Deserialize, Default)]
829pub struct AuditQuery {
830    #[serde(skip_serializing_if = "Option::is_none")]
831    pub agent_id: Option<String>,
832    #[serde(skip_serializing_if = "Option::is_none")]
833    pub event_type: Option<String>,
834    #[serde(skip_serializing_if = "Option::is_none")]
835    pub from: Option<u64>,
836    #[serde(skip_serializing_if = "Option::is_none")]
837    pub to: Option<u64>,
838    #[serde(skip_serializing_if = "Option::is_none")]
839    pub limit: Option<u32>,
840    #[serde(skip_serializing_if = "Option::is_none")]
841    pub cursor: Option<String>,
842}
843
844// ============================================================================
845// EXT-1: External Extraction Providers
846// ============================================================================
847
848/// Result from `POST /v1/extract` (EXT-1).
849#[derive(Debug, Clone, Serialize, Deserialize)]
850pub struct ExtractionResult {
851    pub entities: Vec<serde_json::Value>,
852    pub provider: String,
853    #[serde(skip_serializing_if = "Option::is_none")]
854    pub model: Option<String>,
855    pub duration_ms: f64,
856}
857
858/// Metadata for an available extraction provider (EXT-1).
859#[derive(Debug, Clone, Serialize, Deserialize)]
860pub struct ExtractionProviderInfo {
861    pub name: String,
862    pub available: bool,
863    #[serde(default)]
864    pub models: Vec<String>,
865}
866
867/// Response from `GET /v1/extract/providers` (EXT-1).
868#[derive(Debug, Clone, Serialize, Deserialize)]
869#[serde(untagged)]
870pub enum ExtractProvidersResponse {
871    List(Vec<ExtractionProviderInfo>),
872    Object {
873        providers: Vec<ExtractionProviderInfo>,
874    },
875}
876
877// ============================================================================
878// SEC-3: AES-256-GCM Encryption Key Rotation
879// ============================================================================
880
881/// Request body for `POST /v1/admin/encryption/rotate-key` (SEC-3).
882#[derive(Debug, Clone, Serialize, Deserialize)]
883pub struct RotateEncryptionKeyRequest {
884    /// New passphrase or 64-char hex key to rotate to.
885    pub new_key: String,
886    /// If set, rotate only memories in this namespace. Omit to rotate all.
887    #[serde(skip_serializing_if = "Option::is_none")]
888    pub namespace: Option<String>,
889}
890
891/// Response from `POST /v1/admin/encryption/rotate-key` (SEC-3).
892#[derive(Debug, Clone, Serialize, Deserialize)]
893pub struct RotateEncryptionKeyResponse {
894    pub rotated: usize,
895    pub skipped: usize,
896    #[serde(default)]
897    pub namespaces: Vec<String>,
898}
899
900/// Request for memory feedback
901#[derive(Debug, Clone, Serialize, Deserialize)]
902pub struct FeedbackRequest {
903    pub memory_id: String,
904    pub feedback: String,
905    #[serde(skip_serializing_if = "Option::is_none")]
906    pub relevance_score: Option<f32>,
907}
908
909/// Response from legacy feedback endpoint (POST /v1/agents/:id/memories/feedback)
910#[derive(Debug, Clone, Serialize, Deserialize)]
911pub struct LegacyFeedbackResponse {
912    pub status: String,
913    pub updated_importance: Option<f32>,
914}
915
916// ============================================================================
917// CE-2: Batch Recall / Forget Types
918// ============================================================================
919
920/// Filter predicates for batch memory operations (CE-2).
921///
922/// All fields are optional.  For [`BatchForgetRequest`] at least one must be
923/// set (server-side safety guard).
924#[derive(Debug, Clone, Serialize, Deserialize, Default)]
925pub struct BatchMemoryFilter {
926    /// Restrict to memories that carry **all** listed tags.
927    #[serde(skip_serializing_if = "Option::is_none")]
928    pub tags: Option<Vec<String>>,
929    /// Minimum importance (inclusive).
930    #[serde(skip_serializing_if = "Option::is_none")]
931    pub min_importance: Option<f32>,
932    /// Maximum importance (inclusive).
933    #[serde(skip_serializing_if = "Option::is_none")]
934    pub max_importance: Option<f32>,
935    /// Only memories created at or after this Unix timestamp (seconds).
936    #[serde(skip_serializing_if = "Option::is_none")]
937    pub created_after: Option<u64>,
938    /// Only memories created before or at this Unix timestamp (seconds).
939    #[serde(skip_serializing_if = "Option::is_none")]
940    pub created_before: Option<u64>,
941    /// Restrict to a specific memory type.
942    #[serde(skip_serializing_if = "Option::is_none")]
943    pub memory_type: Option<MemoryType>,
944    /// Restrict to memories from a specific session.
945    #[serde(skip_serializing_if = "Option::is_none")]
946    pub session_id: Option<String>,
947}
948
949impl BatchMemoryFilter {
950    /// Convenience: filter by tags.
951    pub fn with_tags(mut self, tags: Vec<String>) -> Self {
952        self.tags = Some(tags);
953        self
954    }
955
956    /// Convenience: filter by minimum importance.
957    pub fn with_min_importance(mut self, min: f32) -> Self {
958        self.min_importance = Some(min);
959        self
960    }
961
962    /// Convenience: filter by maximum importance.
963    pub fn with_max_importance(mut self, max: f32) -> Self {
964        self.max_importance = Some(max);
965        self
966    }
967
968    /// Convenience: filter by session.
969    pub fn with_session(mut self, session_id: impl Into<String>) -> Self {
970        self.session_id = Some(session_id.into());
971        self
972    }
973}
974
975/// Request body for `POST /v1/memories/recall/batch`.
976#[derive(Debug, Clone, Serialize, Deserialize)]
977pub struct BatchRecallRequest {
978    /// Agent whose memory namespace to search.
979    pub agent_id: String,
980    /// Filter predicates to apply.
981    #[serde(default)]
982    pub filter: BatchMemoryFilter,
983    /// Maximum number of results to return (default: 100).
984    #[serde(default = "default_batch_limit")]
985    pub limit: usize,
986}
987
988fn default_batch_limit() -> usize {
989    100
990}
991
992impl BatchRecallRequest {
993    /// Create a new batch recall request for an agent.
994    pub fn new(agent_id: impl Into<String>) -> Self {
995        Self {
996            agent_id: agent_id.into(),
997            filter: BatchMemoryFilter::default(),
998            limit: 100,
999        }
1000    }
1001
1002    /// Set filter predicates.
1003    pub fn with_filter(mut self, filter: BatchMemoryFilter) -> Self {
1004        self.filter = filter;
1005        self
1006    }
1007
1008    /// Set result limit.
1009    pub fn with_limit(mut self, limit: usize) -> Self {
1010        self.limit = limit;
1011        self
1012    }
1013}
1014
1015/// Response from `POST /v1/memories/recall/batch`.
1016#[derive(Debug, Clone, Serialize, Deserialize)]
1017pub struct BatchRecallResponse {
1018    pub memories: Vec<RecalledMemory>,
1019    /// Total memories in the agent namespace.
1020    pub total: usize,
1021    /// Number of memories that passed the filter.
1022    pub filtered: usize,
1023}
1024
1025// ============================================================================
1026// DAK-5508: Batch Store — POST /v1/memories/store/batch
1027// ============================================================================
1028
1029/// A single memory entry within a [`BatchStoreMemoryRequest`].
1030///
1031/// Mirrors [`StoreMemoryRequest`] but omits `agent_id` (supplied at batch level).
1032#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1033pub struct BatchStoreMemoryItem {
1034    /// Memory content (required, max 100 000 chars).
1035    pub content: String,
1036    #[serde(default)]
1037    pub memory_type: MemoryType,
1038    #[serde(skip_serializing_if = "Option::is_none")]
1039    pub session_id: Option<String>,
1040    #[serde(default = "default_importance")]
1041    pub importance: f32,
1042    #[serde(default)]
1043    pub tags: Vec<String>,
1044    #[serde(skip_serializing_if = "Option::is_none")]
1045    pub metadata: Option<serde_json::Value>,
1046    #[serde(skip_serializing_if = "Option::is_none")]
1047    pub ttl_seconds: Option<u64>,
1048    #[serde(skip_serializing_if = "Option::is_none")]
1049    pub expires_at: Option<u64>,
1050    /// Optional custom ID. Auto-generated if not provided.
1051    #[serde(skip_serializing_if = "Option::is_none")]
1052    pub id: Option<String>,
1053}
1054
1055impl BatchStoreMemoryItem {
1056    /// Create a new item with the given content.
1057    pub fn new(content: impl Into<String>) -> Self {
1058        Self {
1059            content: content.into(),
1060            importance: default_importance(),
1061            ..Default::default()
1062        }
1063    }
1064
1065    /// Set importance.
1066    pub fn with_importance(mut self, importance: f32) -> Self {
1067        self.importance = importance;
1068        self
1069    }
1070
1071    /// Set tags.
1072    pub fn with_tags(mut self, tags: Vec<String>) -> Self {
1073        self.tags = tags;
1074        self
1075    }
1076
1077    /// Set session.
1078    pub fn with_session(mut self, session_id: impl Into<String>) -> Self {
1079        self.session_id = Some(session_id.into());
1080        self
1081    }
1082
1083    /// Set metadata.
1084    pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
1085        self.metadata = Some(metadata);
1086        self
1087    }
1088
1089    /// Set a custom memory ID.
1090    pub fn with_id(mut self, id: impl Into<String>) -> Self {
1091        self.id = Some(id.into());
1092        self
1093    }
1094}
1095
1096/// Request for `POST /v1/memories/store/batch` (DAK-5508).
1097///
1098/// Accepts up to 1 000 memories per call. The server embeds all contents in a
1099/// single ONNX inference pass and upserts them in one RocksDB write, with HNSW
1100/// invalidation happening exactly once — yielding ≥100× throughput vs. N
1101/// sequential single-store calls.
1102#[derive(Debug, Clone, Serialize, Deserialize)]
1103pub struct BatchStoreMemoryRequest {
1104    /// Agent namespace to store the memories in.
1105    pub agent_id: String,
1106    /// Memories to store (1–1000 items).
1107    pub memories: Vec<BatchStoreMemoryItem>,
1108}
1109
1110impl BatchStoreMemoryRequest {
1111    /// Create a new batch request.
1112    pub fn new(agent_id: impl Into<String>, memories: Vec<BatchStoreMemoryItem>) -> Self {
1113        Self {
1114            agent_id: agent_id.into(),
1115            memories,
1116        }
1117    }
1118}
1119
1120/// A single stored memory returned in a [`BatchStoreMemoryResponse`].
1121#[derive(Debug, Clone, Serialize, Deserialize)]
1122pub struct BatchStoredMemory {
1123    pub id: String,
1124    pub content: String,
1125    pub agent_id: String,
1126    #[serde(default)]
1127    pub tags: Vec<String>,
1128    #[serde(default)]
1129    pub importance: f32,
1130    pub created_at: u64,
1131}
1132
1133/// Response from `POST /v1/memories/store/batch`.
1134#[derive(Debug, Clone, Serialize, Deserialize)]
1135pub struct BatchStoreMemoryResponse {
1136    /// Stored memories in the same order as the request items.
1137    pub stored: Vec<BatchStoredMemory>,
1138    /// Number of memories successfully stored.
1139    pub stored_count: usize,
1140    /// Time spent on ONNX embedding for the entire batch (milliseconds).
1141    pub total_embedding_time_ms: u64,
1142}
1143
1144/// Request body for `DELETE /v1/memories/forget/batch`.
1145#[derive(Debug, Clone, Serialize, Deserialize)]
1146pub struct BatchForgetRequest {
1147    /// Agent whose memory namespace to purge from.
1148    pub agent_id: String,
1149    /// Filter predicates — **at least one must be set** (server safety guard).
1150    pub filter: BatchMemoryFilter,
1151}
1152
1153impl BatchForgetRequest {
1154    /// Create a new batch forget request with the given filter.
1155    pub fn new(agent_id: impl Into<String>, filter: BatchMemoryFilter) -> Self {
1156        Self {
1157            agent_id: agent_id.into(),
1158            filter,
1159        }
1160    }
1161}
1162
1163/// Response from `DELETE /v1/memories/forget/batch`.
1164#[derive(Debug, Clone, Serialize, Deserialize)]
1165pub struct BatchForgetResponse {
1166    pub deleted_count: usize,
1167}
1168
1169// ============================================================================
1170// Memory Client Methods
1171// ============================================================================
1172
1173impl DakeraClient {
1174    // ========================================================================
1175    // Memory Operations
1176    // ========================================================================
1177
1178    /// Store a memory for an agent
1179    ///
1180    /// # Example
1181    ///
1182    /// ```rust,no_run
1183    /// use dakera_client::{DakeraClient, memory::StoreMemoryRequest};
1184    ///
1185    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1186    /// let client = DakeraClient::new("http://localhost:3000")?;
1187    ///
1188    /// let request = StoreMemoryRequest::new("agent-1", "The user prefers dark mode")
1189    ///     .with_importance(0.8)
1190    ///     .with_tags(vec!["preferences".to_string()]);
1191    ///
1192    /// let response = client.store_memory(request).await?;
1193    /// println!("Stored memory: {}", response.memory_id);
1194    /// # Ok(())
1195    /// # }
1196    /// ```
1197    pub async fn store_memory(&self, request: StoreMemoryRequest) -> Result<StoreMemoryResponse> {
1198        let url = format!("{}/v1/memory/store", self.base_url);
1199        let response = self.client.post(&url).json(&request).send().await?;
1200        self.handle_response(response).await
1201    }
1202
1203    /// Recall memories by semantic query
1204    ///
1205    /// # Example
1206    ///
1207    /// ```rust,no_run
1208    /// use dakera_client::{DakeraClient, memory::RecallRequest};
1209    ///
1210    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1211    /// let client = DakeraClient::new("http://localhost:3000")?;
1212    ///
1213    /// let request = RecallRequest::new("agent-1", "user preferences")
1214    ///     .with_top_k(10);
1215    ///
1216    /// let response = client.recall(request).await?;
1217    /// for memory in response.memories {
1218    ///     println!("{}: {} (score: {})", memory.id, memory.content, memory.score);
1219    /// }
1220    /// # Ok(())
1221    /// # }
1222    /// ```
1223    pub async fn recall(&self, request: RecallRequest) -> Result<RecallResponse> {
1224        let url = format!("{}/v1/memory/recall", self.base_url);
1225        let response = self.client.post(&url).json(&request).send().await?;
1226        self.handle_response(response).await
1227    }
1228
1229    /// Simple recall with just agent_id and query (convenience method)
1230    pub async fn recall_simple(
1231        &self,
1232        agent_id: &str,
1233        query: &str,
1234        top_k: usize,
1235    ) -> Result<RecallResponse> {
1236        self.recall(RecallRequest::new(agent_id, query).with_top_k(top_k))
1237            .await
1238    }
1239
1240    /// Get a specific memory by ID (requires agent_id for namespace lookup)
1241    pub async fn get_memory(&self, agent_id: &str, memory_id: &str) -> Result<RecalledMemory> {
1242        let url = format!(
1243            "{}/v1/memory/get/{}?agent_id={}",
1244            self.base_url, memory_id, agent_id
1245        );
1246        let response = self.client.get(&url).send().await?;
1247        self.handle_response(response).await
1248    }
1249
1250    /// Forget (delete) memories
1251    pub async fn forget(&self, request: ForgetRequest) -> Result<ForgetResponse> {
1252        let url = format!("{}/v1/memory/forget", self.base_url);
1253        let response = self.client.post(&url).json(&request).send().await?;
1254        self.handle_response(response).await
1255    }
1256
1257    /// Search memories with advanced filters
1258    pub async fn search_memories(&self, request: RecallRequest) -> Result<RecallResponse> {
1259        let url = format!("{}/v1/memory/search", self.base_url);
1260        let response = self.client.post(&url).json(&request).send().await?;
1261        self.handle_response(response).await
1262    }
1263
1264    /// Update an existing memory
1265    pub async fn update_memory(
1266        &self,
1267        agent_id: &str,
1268        memory_id: &str,
1269        request: UpdateMemoryRequest,
1270    ) -> Result<StoreMemoryResponse> {
1271        let url = format!(
1272            "{}/v1/agents/{}/memories/{}",
1273            self.base_url, agent_id, memory_id
1274        );
1275        let response = self.client.put(&url).json(&request).send().await?;
1276        self.handle_response(response).await
1277    }
1278
1279    /// Update importance of memories
1280    pub async fn update_importance(
1281        &self,
1282        agent_id: &str,
1283        request: UpdateImportanceRequest,
1284    ) -> Result<serde_json::Value> {
1285        let url = format!("{}/v1/memory/importance", self.base_url);
1286        let mut last_result = serde_json::Value::Null;
1287        for memory_id in &request.memory_ids {
1288            let body = serde_json::json!({
1289                "agent_id": agent_id,
1290                "memory_id": memory_id,
1291                "importance": request.importance,
1292            });
1293            let response = self.client.post(&url).json(&body).send().await?;
1294            last_result = self.handle_response(response).await?;
1295        }
1296        Ok(last_result)
1297    }
1298
1299    /// Consolidate memories for an agent
1300    pub async fn consolidate(
1301        &self,
1302        agent_id: &str,
1303        request: ConsolidateRequest,
1304    ) -> Result<ConsolidateResponse> {
1305        // Server endpoint: POST /v1/memory/consolidate with agent_id in body
1306        let url = format!("{}/v1/memory/consolidate", self.base_url);
1307        let mut body = serde_json::to_value(&request)?;
1308        body["agent_id"] = serde_json::Value::String(agent_id.to_string());
1309        let response = self.client.post(&url).json(&body).send().await?;
1310        self.handle_response(response).await
1311    }
1312
1313    /// Submit feedback on a memory recall
1314    pub async fn memory_feedback(
1315        &self,
1316        agent_id: &str,
1317        request: FeedbackRequest,
1318    ) -> Result<LegacyFeedbackResponse> {
1319        let url = format!("{}/v1/agents/{}/memories/feedback", self.base_url, agent_id);
1320        let response = self.client.post(&url).json(&request).send().await?;
1321        self.handle_response(response).await
1322    }
1323
1324    // ========================================================================
1325    // Memory Feedback Loop — INT-1
1326    // ========================================================================
1327
1328    /// Submit upvote/downvote/flag feedback on a memory (INT-1).
1329    ///
1330    /// # Arguments
1331    /// * `memory_id` – The memory to give feedback on.
1332    /// * `agent_id` – The agent that owns the memory.
1333    /// * `signal` – [`FeedbackSignal`] value: `Upvote`, `Downvote`, or `Flag`.
1334    ///
1335    /// # Example
1336    /// ```no_run
1337    /// # use dakera_client::{DakeraClient, FeedbackSignal};
1338    /// # async fn example(client: &DakeraClient) -> dakera_client::Result<()> {
1339    /// let resp = client.feedback_memory("mem-abc", "agent-1", FeedbackSignal::Upvote).await?;
1340    /// println!("new importance: {}", resp.new_importance);
1341    /// # Ok(()) }
1342    /// ```
1343    pub async fn feedback_memory(
1344        &self,
1345        memory_id: &str,
1346        agent_id: &str,
1347        signal: FeedbackSignal,
1348    ) -> Result<FeedbackResponse> {
1349        let url = format!("{}/v1/memories/{}/feedback", self.base_url, memory_id);
1350        let body = MemoryFeedbackBody {
1351            agent_id: agent_id.to_string(),
1352            signal,
1353        };
1354        let response = self.client.post(&url).json(&body).send().await?;
1355        self.handle_response(response).await
1356    }
1357
1358    /// Get the full feedback history for a memory (INT-1).
1359    pub async fn get_memory_feedback_history(
1360        &self,
1361        memory_id: &str,
1362    ) -> Result<FeedbackHistoryResponse> {
1363        let url = format!("{}/v1/memories/{}/feedback", self.base_url, memory_id);
1364        let response = self.client.get(&url).send().await?;
1365        self.handle_response(response).await
1366    }
1367
1368    /// Compute a T-I-F reliability score for a memory (T-I-F RFC Phase 3).
1369    ///
1370    /// Fetches the full feedback history and reduces it to a [`TifScore`] with
1371    /// truth/indeterminacy/falsity proportions and a [`TifClassification`] label.
1372    ///
1373    /// # Arguments
1374    /// * `memory_id` – The memory to score.
1375    pub async fn evaluate_tif(&self, memory_id: &str) -> Result<TifScore> {
1376        let history = self.get_memory_feedback_history(memory_id).await?;
1377        Ok(TifScore::from_feedback_history(&history))
1378    }
1379
1380    /// Get aggregate feedback counts and health score for an agent (INT-1).
1381    pub async fn get_agent_feedback_summary(&self, agent_id: &str) -> Result<AgentFeedbackSummary> {
1382        let url = format!("{}/v1/agents/{}/feedback/summary", self.base_url, agent_id);
1383        let response = self.client.get(&url).send().await?;
1384        self.handle_response(response).await
1385    }
1386
1387    /// Directly override a memory's importance score (INT-1).
1388    ///
1389    /// # Arguments
1390    /// * `memory_id` – The memory to update.
1391    /// * `agent_id` – The agent that owns the memory.
1392    /// * `importance` – New importance value (0.0–1.0).
1393    pub async fn patch_memory_importance(
1394        &self,
1395        memory_id: &str,
1396        agent_id: &str,
1397        importance: f32,
1398    ) -> Result<FeedbackResponse> {
1399        let url = format!("{}/v1/memories/{}/importance", self.base_url, memory_id);
1400        let body = MemoryImportancePatch {
1401            agent_id: agent_id.to_string(),
1402            importance,
1403        };
1404        let response = self.client.patch(&url).json(&body).send().await?;
1405        self.handle_response(response).await
1406    }
1407
1408    /// Get overall feedback health score for an agent (INT-1).
1409    ///
1410    /// The health score is the mean importance of all non-expired memories (0.0–1.0).
1411    /// A higher score indicates a healthier, more relevant memory store.
1412    pub async fn get_feedback_health(&self, agent_id: &str) -> Result<FeedbackHealthResponse> {
1413        let url = format!("{}/v1/feedback/health?agent_id={}", self.base_url, agent_id);
1414        let response = self.client.get(&url).send().await?;
1415        self.handle_response(response).await
1416    }
1417
1418    // ========================================================================
1419    // Memory Knowledge Graph Operations (CE-5 / SDK-9)
1420    // ========================================================================
1421
1422    /// Traverse the knowledge graph from a memory node.
1423    ///
1424    /// Requires CE-5 (Memory Knowledge Graph) on the server.
1425    ///
1426    /// # Arguments
1427    /// * `memory_id` – Root memory ID to start traversal from.
1428    /// * `options` – Traversal options (depth, edge type filters).
1429    ///
1430    /// # Example
1431    /// ```no_run
1432    /// # use dakera_client::{DakeraClient, GraphOptions};
1433    /// # async fn example(client: &DakeraClient) -> dakera_client::Result<()> {
1434    /// let graph = client.memory_graph("mem-abc", GraphOptions::new().depth(2)).await?;
1435    /// println!("{} nodes, {} edges", graph.nodes.len(), graph.edges.len());
1436    /// # Ok(()) }
1437    /// ```
1438    pub async fn memory_graph(
1439        &self,
1440        memory_id: &str,
1441        options: GraphOptions,
1442    ) -> Result<MemoryGraph> {
1443        let mut url = format!("{}/v1/memories/{}/graph", self.base_url, memory_id);
1444        let depth = options.depth.unwrap_or(1);
1445        url.push_str(&format!("?depth={}", depth));
1446        if let Some(types) = &options.types {
1447            let type_strs: Vec<String> = types
1448                .iter()
1449                .map(|t| {
1450                    serde_json::to_value(t)
1451                        .unwrap()
1452                        .as_str()
1453                        .unwrap_or("")
1454                        .to_string()
1455                })
1456                .collect();
1457            if !type_strs.is_empty() {
1458                url.push_str(&format!("&types={}", type_strs.join(",")));
1459            }
1460        }
1461        let response = self.client.get(&url).send().await?;
1462        self.handle_response(response).await
1463    }
1464
1465    /// Find the shortest path between two memories in the knowledge graph.
1466    ///
1467    /// Requires CE-5 (Memory Knowledge Graph) on the server.
1468    ///
1469    /// # Example
1470    /// ```no_run
1471    /// # use dakera_client::DakeraClient;
1472    /// # async fn example(client: &DakeraClient) -> dakera_client::Result<()> {
1473    /// let path = client.memory_path("mem-abc", "mem-xyz").await?;
1474    /// println!("{} hops: {:?}", path.hops, path.path);
1475    /// # Ok(()) }
1476    /// ```
1477    pub async fn memory_path(&self, source_id: &str, target_id: &str) -> Result<GraphPath> {
1478        let url = format!(
1479            "{}/v1/memories/{}/path?target={}",
1480            self.base_url,
1481            source_id,
1482            urlencoding::encode(target_id)
1483        );
1484        let response = self.client.get(&url).send().await?;
1485        self.handle_response(response).await
1486    }
1487
1488    /// Create an explicit edge between two memories.
1489    ///
1490    /// Requires CE-5 (Memory Knowledge Graph) on the server.
1491    ///
1492    /// # Example
1493    /// ```no_run
1494    /// # use dakera_client::{DakeraClient, EdgeType};
1495    /// # async fn example(client: &DakeraClient) -> dakera_client::Result<()> {
1496    /// let resp = client.memory_link("mem-abc", "mem-xyz", EdgeType::LinkedBy).await?;
1497    /// println!("Created edge: {}", resp.edge.id);
1498    /// # Ok(()) }
1499    /// ```
1500    pub async fn memory_link(
1501        &self,
1502        source_id: &str,
1503        target_id: &str,
1504        edge_type: EdgeType,
1505    ) -> Result<GraphLinkResponse> {
1506        let url = format!("{}/v1/memories/{}/links", self.base_url, source_id);
1507        let request = GraphLinkRequest {
1508            target_id: target_id.to_string(),
1509            edge_type,
1510        };
1511        let response = self.client.post(&url).json(&request).send().await?;
1512        self.handle_response(response).await
1513    }
1514
1515    /// Export the full knowledge graph for an agent.
1516    ///
1517    /// Requires CE-5 (Memory Knowledge Graph) on the server.
1518    ///
1519    /// # Arguments
1520    /// * `agent_id` – Agent whose graph to export.
1521    /// * `format` – Export format: `"json"` (default), `"graphml"`, or `"csv"`.
1522    pub async fn agent_graph_export(&self, agent_id: &str, format: &str) -> Result<GraphExport> {
1523        let url = format!(
1524            "{}/v1/agents/{}/graph/export?format={}",
1525            self.base_url, agent_id, format
1526        );
1527        let response = self.client.get(&url).send().await?;
1528        self.handle_response(response).await
1529    }
1530
1531    // ========================================================================
1532    // Session Operations
1533    // ========================================================================
1534
1535    /// Start a new session for an agent
1536    pub async fn start_session(&self, agent_id: &str) -> Result<Session> {
1537        let url = format!("{}/v1/sessions/start", self.base_url);
1538        let request = SessionStartRequest {
1539            agent_id: agent_id.to_string(),
1540            metadata: None,
1541        };
1542        let response = self.client.post(&url).json(&request).send().await?;
1543        let resp: SessionStartResponse = self.handle_response(response).await?;
1544        Ok(resp.session)
1545    }
1546
1547    /// Start a session with metadata
1548    pub async fn start_session_with_metadata(
1549        &self,
1550        agent_id: &str,
1551        metadata: serde_json::Value,
1552    ) -> Result<Session> {
1553        let url = format!("{}/v1/sessions/start", self.base_url);
1554        let request = SessionStartRequest {
1555            agent_id: agent_id.to_string(),
1556            metadata: Some(metadata),
1557        };
1558        let response = self.client.post(&url).json(&request).send().await?;
1559        let resp: SessionStartResponse = self.handle_response(response).await?;
1560        Ok(resp.session)
1561    }
1562
1563    /// End a session, optionally with a summary.
1564    /// Returns the session state and the total memory count at close.
1565    pub async fn end_session(
1566        &self,
1567        session_id: &str,
1568        summary: Option<String>,
1569    ) -> Result<SessionEndResponse> {
1570        let url = format!("{}/v1/sessions/{}/end", self.base_url, session_id);
1571        let request = SessionEndRequest { summary };
1572        let response = self.client.post(&url).json(&request).send().await?;
1573        self.handle_response(response).await
1574    }
1575
1576    /// Get a session by ID
1577    pub async fn get_session(&self, session_id: &str) -> Result<Session> {
1578        let url = format!("{}/v1/sessions/{}", self.base_url, session_id);
1579        let response = self.client.get(&url).send().await?;
1580        self.handle_response(response).await
1581    }
1582
1583    /// List sessions for an agent
1584    pub async fn list_sessions(&self, agent_id: &str) -> Result<Vec<Session>> {
1585        let url = format!("{}/v1/sessions?agent_id={}", self.base_url, agent_id);
1586        let response = self.client.get(&url).send().await?;
1587        let wrapper: ListSessionsResponse = self.handle_response(response).await?;
1588        Ok(wrapper.sessions)
1589    }
1590
1591    /// Get memories in a session
1592    pub async fn session_memories(&self, session_id: &str) -> Result<RecallResponse> {
1593        let url = format!("{}/v1/sessions/{}/memories", self.base_url, session_id);
1594        let response = self.client.get(&url).send().await?;
1595        self.handle_response(response).await
1596    }
1597
1598    // ========================================================================
1599    // CE-2: Batch Recall / Forget
1600    // ========================================================================
1601
1602    /// Bulk-recall memories using filter predicates (CE-2).
1603    ///
1604    /// Uses `POST /v1/memories/recall/batch` — no embedding required.
1605    ///
1606    /// # Example
1607    ///
1608    /// ```rust,no_run
1609    /// use dakera_client::{DakeraClient, memory::{BatchRecallRequest, BatchMemoryFilter}};
1610    ///
1611    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1612    /// let client = DakeraClient::new("http://localhost:3000")?;
1613    ///
1614    /// let filter = BatchMemoryFilter::default().with_min_importance(0.7);
1615    /// let req = BatchRecallRequest::new("agent-1").with_filter(filter).with_limit(50);
1616    /// let resp = client.batch_recall(req).await?;
1617    /// println!("Found {} memories", resp.filtered);
1618    /// # Ok(())
1619    /// # }
1620    /// ```
1621    pub async fn batch_recall(&self, request: BatchRecallRequest) -> Result<BatchRecallResponse> {
1622        let url = format!("{}/v1/memories/recall/batch", self.base_url);
1623        let response = self.client.post(&url).json(&request).send().await?;
1624        self.handle_response(response).await
1625    }
1626
1627    /// Store up to 1 000 memories in a single batched call (DAK-5508).
1628    ///
1629    /// All memories are embedded in one ONNX inference pass and written to
1630    /// RocksDB in one batch, with HNSW invalidation happening exactly once —
1631    /// yielding ≥100× throughput vs. N sequential [`store_memory`] calls.
1632    ///
1633    /// The `stored` field in the response preserves the same ordering as the
1634    /// request items, so callers can map `response.stored[i].id` back to
1635    /// `request.memories[i]` by index.
1636    ///
1637    /// # Example
1638    ///
1639    /// ```rust,no_run
1640    /// use dakera_client::{DakeraClient, memory::{BatchStoreMemoryItem, BatchStoreMemoryRequest}};
1641    ///
1642    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1643    /// let client = DakeraClient::new("http://localhost:3000")?;
1644    ///
1645    /// let items = vec![
1646    ///     BatchStoreMemoryItem::new("The user prefers dark mode").with_importance(0.8),
1647    ///     BatchStoreMemoryItem::new("The user is based in Berlin").with_importance(0.7),
1648    /// ];
1649    /// let resp = client
1650    ///     .store_memories_batch(BatchStoreMemoryRequest::new("agent-1", items))
1651    ///     .await?;
1652    /// println!("Stored {} memories", resp.stored_count);
1653    /// # Ok(())
1654    /// # }
1655    /// ```
1656    pub async fn store_memories_batch(
1657        &self,
1658        request: BatchStoreMemoryRequest,
1659    ) -> Result<BatchStoreMemoryResponse> {
1660        let url = format!("{}/v1/memories/store/batch", self.base_url);
1661        let response = self.client.post(&url).json(&request).send().await?;
1662        self.handle_response(response).await
1663    }
1664
1665    /// Bulk-delete memories using filter predicates (CE-2).
1666    ///
1667    /// Uses `DELETE /v1/memories/forget/batch`.  The server requires at least
1668    /// one filter predicate to be set as a safety guard.
1669    ///
1670    /// # Example
1671    ///
1672    /// ```rust,no_run
1673    /// use dakera_client::{DakeraClient, memory::{BatchForgetRequest, BatchMemoryFilter}};
1674    ///
1675    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1676    /// let client = DakeraClient::new("http://localhost:3000")?;
1677    ///
1678    /// let filter = BatchMemoryFilter::default().with_min_importance(0.0).with_max_importance(0.2);
1679    /// let resp = client.batch_forget(BatchForgetRequest::new("agent-1", filter)).await?;
1680    /// println!("Deleted {} memories", resp.deleted_count);
1681    /// # Ok(())
1682    /// # }
1683    /// ```
1684    pub async fn batch_forget(&self, request: BatchForgetRequest) -> Result<BatchForgetResponse> {
1685        let url = format!("{}/v1/memories/forget/batch", self.base_url);
1686        let response = self.client.delete(&url).json(&request).send().await?;
1687        self.handle_response(response).await
1688    }
1689
1690    // ========================================================================
1691    // DX-1: Memory Import / Export
1692    // ========================================================================
1693
1694    /// Import memories from an external format (DX-1).
1695    ///
1696    /// Supported formats: `"jsonl"`, `"mem0"`, `"zep"`, `"csv"`.
1697    ///
1698    /// ```no_run
1699    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1700    /// let client = dakera_client::DakeraClient::new("http://localhost:3000")?;
1701    /// let data = serde_json::json!([{"content": "hello", "agent_id": "agent-1"}]);
1702    /// let resp = client.import_memories(data, "jsonl", None, None).await?;
1703    /// println!("Imported {} memories", resp.imported_count);
1704    /// # Ok(())
1705    /// # }
1706    /// ```
1707    pub async fn import_memories(
1708        &self,
1709        data: serde_json::Value,
1710        format: &str,
1711        agent_id: Option<&str>,
1712        namespace: Option<&str>,
1713    ) -> Result<MemoryImportResponse> {
1714        let mut body = serde_json::json!({"data": data, "format": format});
1715        if let Some(aid) = agent_id {
1716            body["agent_id"] = serde_json::Value::String(aid.to_string());
1717        }
1718        if let Some(ns) = namespace {
1719            body["namespace"] = serde_json::Value::String(ns.to_string());
1720        }
1721        let url = format!("{}/v1/import", self.base_url);
1722        let response = self.client.post(&url).json(&body).send().await?;
1723        self.handle_response(response).await
1724    }
1725
1726    /// Export memories in a portable format (DX-1).
1727    ///
1728    /// Supported formats: `"jsonl"`, `"mem0"`, `"zep"`, `"csv"`.
1729    pub async fn export_memories(
1730        &self,
1731        format: &str,
1732        agent_id: Option<&str>,
1733        namespace: Option<&str>,
1734        limit: Option<u32>,
1735    ) -> Result<MemoryExportResponse> {
1736        let mut params = vec![("format", format.to_string())];
1737        if let Some(aid) = agent_id {
1738            params.push(("agent_id", aid.to_string()));
1739        }
1740        if let Some(ns) = namespace {
1741            params.push(("namespace", ns.to_string()));
1742        }
1743        if let Some(l) = limit {
1744            params.push(("limit", l.to_string()));
1745        }
1746        let url = format!("{}/v1/export", self.base_url);
1747        let response = self.client.get(&url).query(&params).send().await?;
1748        self.handle_response(response).await
1749    }
1750
1751    // ========================================================================
1752    // OBS-1: Business-Event Audit Log
1753    // ========================================================================
1754
1755    /// List paginated audit log entries (OBS-1).
1756    pub async fn list_audit_events(&self, query: AuditQuery) -> Result<AuditListResponse> {
1757        let url = format!("{}/v1/audit", self.base_url);
1758        let response = self.client.get(&url).query(&query).send().await?;
1759        self.handle_response(response).await
1760    }
1761
1762    /// Stream live audit events via SSE (OBS-1).
1763    ///
1764    /// Returns a [`tokio::sync::mpsc::Receiver`] that yields [`DakeraEvent`] results.
1765    pub async fn stream_audit_events(
1766        &self,
1767        agent_id: Option<&str>,
1768        event_type: Option<&str>,
1769    ) -> Result<tokio::sync::mpsc::Receiver<Result<crate::events::DakeraEvent>>> {
1770        let mut params: Vec<(&str, String)> = Vec::new();
1771        if let Some(aid) = agent_id {
1772            params.push(("agent_id", aid.to_string()));
1773        }
1774        if let Some(et) = event_type {
1775            params.push(("event_type", et.to_string()));
1776        }
1777        let base = format!("{}/v1/audit/stream", self.base_url);
1778        let url = if params.is_empty() {
1779            base
1780        } else {
1781            let qs = params
1782                .iter()
1783                .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
1784                .collect::<Vec<_>>()
1785                .join("&");
1786            format!("{}?{}", base, qs)
1787        };
1788        self.stream_sse(url).await
1789    }
1790
1791    /// Bulk-export audit log entries (OBS-1).
1792    pub async fn export_audit(
1793        &self,
1794        format: &str,
1795        agent_id: Option<&str>,
1796        event_type: Option<&str>,
1797        from_ts: Option<u64>,
1798        to_ts: Option<u64>,
1799    ) -> Result<AuditExportResponse> {
1800        let mut body = serde_json::json!({"format": format});
1801        if let Some(aid) = agent_id {
1802            body["agent_id"] = serde_json::Value::String(aid.to_string());
1803        }
1804        if let Some(et) = event_type {
1805            body["event_type"] = serde_json::Value::String(et.to_string());
1806        }
1807        if let Some(f) = from_ts {
1808            body["from"] = serde_json::Value::Number(f.into());
1809        }
1810        if let Some(t) = to_ts {
1811            body["to"] = serde_json::Value::Number(t.into());
1812        }
1813        let url = format!("{}/v1/audit/export", self.base_url);
1814        let response = self.client.post(&url).json(&body).send().await?;
1815        self.handle_response(response).await
1816    }
1817
1818    // ========================================================================
1819    // EXT-1: External Extraction Providers
1820    // ========================================================================
1821
1822    /// Extract entities from text using a pluggable provider (EXT-1).
1823    ///
1824    /// Provider hierarchy: per-request > namespace default > GLiNER (bundled).
1825    /// Supported providers: `"gliner"`, `"openai"`, `"anthropic"`, `"openrouter"`, `"ollama"`.
1826    pub async fn extract_text(
1827        &self,
1828        text: &str,
1829        namespace: Option<&str>,
1830        provider: Option<&str>,
1831        model: Option<&str>,
1832    ) -> Result<ExtractionResult> {
1833        let mut body = serde_json::json!({"text": text});
1834        if let Some(ns) = namespace {
1835            body["namespace"] = serde_json::Value::String(ns.to_string());
1836        }
1837        if let Some(p) = provider {
1838            body["provider"] = serde_json::Value::String(p.to_string());
1839        }
1840        if let Some(m) = model {
1841            body["model"] = serde_json::Value::String(m.to_string());
1842        }
1843        let url = format!("{}/v1/extract", self.base_url);
1844        let response = self.client.post(&url).json(&body).send().await?;
1845        self.handle_response(response).await
1846    }
1847
1848    /// List available extraction providers and their models (EXT-1).
1849    pub async fn list_extract_providers(&self) -> Result<Vec<ExtractionProviderInfo>> {
1850        let url = format!("{}/v1/extract/providers", self.base_url);
1851        let response = self.client.get(&url).send().await?;
1852        let result: ExtractProvidersResponse = self.handle_response(response).await?;
1853        Ok(match result {
1854            ExtractProvidersResponse::List(v) => v,
1855            ExtractProvidersResponse::Object { providers } => providers,
1856        })
1857    }
1858
1859    /// Set the default extraction provider for a namespace (EXT-1).
1860    pub async fn configure_namespace_extractor(
1861        &self,
1862        namespace: &str,
1863        provider: &str,
1864        model: Option<&str>,
1865    ) -> Result<serde_json::Value> {
1866        let mut body = serde_json::json!({"provider": provider});
1867        if let Some(m) = model {
1868            body["model"] = serde_json::Value::String(m.to_string());
1869        }
1870        let url = format!(
1871            "{}/v1/namespaces/{}/extractor",
1872            self.base_url,
1873            urlencoding::encode(namespace)
1874        );
1875        let response = self.client.patch(&url).json(&body).send().await?;
1876        self.handle_response(response).await
1877    }
1878
1879    // =========================================================================
1880    // SEC-3: AES-256-GCM Encryption Key Rotation
1881    // =========================================================================
1882
1883    /// Re-encrypt all memory content blobs with a new AES-256-GCM key (SEC-3).
1884    ///
1885    /// After this call the new key is active in the running process.
1886    /// The operator must update `DAKERA_ENCRYPTION_KEY` and restart to make
1887    /// the rotation durable across restarts.
1888    ///
1889    /// Requires Admin scope.
1890    ///
1891    /// # Arguments
1892    /// * `new_key` - New passphrase or 64-char hex key.
1893    /// * `namespace` - If `Some`, rotate only this namespace. `None` rotates all.
1894    pub async fn rotate_encryption_key(
1895        &self,
1896        new_key: &str,
1897        namespace: Option<&str>,
1898    ) -> Result<RotateEncryptionKeyResponse> {
1899        let body = RotateEncryptionKeyRequest {
1900            new_key: new_key.to_string(),
1901            namespace: namespace.map(|s| s.to_string()),
1902        };
1903        let url = format!("{}/v1/admin/encryption/rotate-key", self.base_url);
1904        let response = self.client.post(&url).json(&body).send().await?;
1905        self.handle_response(response).await
1906    }
1907}