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