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