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