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