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