Skip to main content

dakera_client/
memory.rs

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