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