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