Skip to main content

dakera_client/
agents.rs

1//! Agent management for the Dakera client.
2
3use serde::{Deserialize, Serialize};
4
5use crate::error::Result;
6use crate::memory::{RecalledMemory, Session};
7use crate::types::{
8    AgentConsolidateResponse, AgentConsolidationConfig, AgentConsolidationLogEntry,
9    ConsolidationConfigPatch,
10};
11use crate::DakeraClient;
12
13// ============================================================================
14// Agent Types
15// ============================================================================
16
17/// Summary of an agent
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct AgentSummary {
20    pub agent_id: String,
21    pub memory_count: i64,
22    pub session_count: i64,
23    pub active_sessions: i64,
24}
25
26/// Detailed stats for an agent
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct AgentStats {
29    pub agent_id: String,
30    pub total_memories: i64,
31    #[serde(default)]
32    pub memories_by_type: std::collections::HashMap<String, i64>,
33    pub total_sessions: i64,
34    pub active_sessions: i64,
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub avg_importance: Option<f32>,
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub oldest_memory_at: Option<String>,
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub newest_memory_at: Option<String>,
41}
42
43// ============================================================================
44// Agent Client Methods
45// ============================================================================
46
47impl DakeraClient {
48    /// List all agents
49    pub async fn list_agents(&self) -> Result<Vec<AgentSummary>> {
50        let url = format!("{}/v1/agents", self.base_url);
51        let response = self.client.get(&url).send().await?;
52        self.handle_response(response).await
53    }
54
55    /// Get memories for an agent
56    pub async fn agent_memories(
57        &self,
58        agent_id: &str,
59        memory_type: Option<&str>,
60        limit: Option<u32>,
61    ) -> Result<Vec<RecalledMemory>> {
62        let mut url = format!("{}/v1/agents/{}/memories", self.base_url, agent_id);
63        let mut params = Vec::new();
64        if let Some(t) = memory_type {
65            params.push(format!("memory_type={}", t));
66        }
67        if let Some(l) = limit {
68            params.push(format!("limit={}", l));
69        }
70        if !params.is_empty() {
71            url.push('?');
72            url.push_str(&params.join("&"));
73        }
74
75        let response = self.client.get(&url).send().await?;
76        self.handle_response(response).await
77    }
78
79    /// Get stats for an agent
80    pub async fn agent_stats(&self, agent_id: &str) -> Result<AgentStats> {
81        let url = format!("{}/v1/agents/{}/stats", self.base_url, agent_id);
82        let response = self.client.get(&url).send().await?;
83        self.handle_response(response).await
84    }
85
86    /// Subscribe to real-time memory lifecycle events for a specific agent.
87    ///
88    /// Opens a long-lived connection to `GET /v1/events/stream` and returns a
89    /// [`tokio::sync::mpsc::Receiver`] that yields [`MemoryEvent`] results filtered
90    /// to the given `agent_id`.  An optional `tags` list further restricts events
91    /// to those whose tags have at least one overlap with the filter.
92    ///
93    /// The background task reconnects automatically on stream error.  It exits
94    /// when the returned receiver is dropped.
95    ///
96    /// Requires a Read-scoped API key.
97    ///
98    /// # Example
99    ///
100    /// ```rust,no_run
101    /// use dakera_client::DakeraClient;
102    ///
103    /// #[tokio::main]
104    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
105    ///     let client = DakeraClient::new("http://localhost:3000")?;
106    ///     let mut rx = client.subscribe_agent_events("my-bot", None).await?;
107    ///     while let Some(result) = rx.recv().await {
108    ///         let event = result?;
109    ///         println!("{}: {:?}", event.event_type, event.memory_id);
110    ///     }
111    ///     Ok(())
112    /// }
113    /// ```
114    pub async fn subscribe_agent_events(
115        &self,
116        agent_id: &str,
117        tags: Option<Vec<String>>,
118    ) -> crate::error::Result<
119        tokio::sync::mpsc::Receiver<crate::error::Result<crate::events::MemoryEvent>>,
120    > {
121        let (tx, rx) = tokio::sync::mpsc::channel(64);
122        let client = self.clone();
123        let agent_id = agent_id.to_owned();
124
125        tokio::spawn(async move {
126            loop {
127                match client.stream_memory_events().await {
128                    Err(_) => {
129                        tokio::time::sleep(std::time::Duration::from_secs(1)).await;
130                        continue;
131                    }
132                    Ok(mut inner_rx) => {
133                        while let Some(result) = inner_rx.recv().await {
134                            match result {
135                                Err(e) => {
136                                    // Send the error but don't kill the reconnect loop.
137                                    let _ = tx.send(Err(e)).await;
138                                    break;
139                                }
140                                Ok(event) => {
141                                    if event.event_type == "connected" {
142                                        continue;
143                                    }
144                                    if event.agent_id != agent_id {
145                                        continue;
146                                    }
147                                    if let Some(ref filter_tags) = tags {
148                                        let event_tags = event.tags.as_deref().unwrap_or(&[]);
149                                        if !filter_tags.iter().any(|t| event_tags.contains(t)) {
150                                            continue;
151                                        }
152                                    }
153                                    if tx.send(Ok(event)).await.is_err() {
154                                        return; // Receiver dropped — exit.
155                                    }
156                                }
157                            }
158                        }
159                    }
160                }
161                // Reconnect after a short delay.
162                tokio::time::sleep(std::time::Duration::from_secs(1)).await;
163            }
164        });
165
166        Ok(rx)
167    }
168
169    /// Get sessions for an agent
170    pub async fn agent_sessions(
171        &self,
172        agent_id: &str,
173        active_only: Option<bool>,
174        limit: Option<u32>,
175    ) -> Result<Vec<Session>> {
176        let mut url = format!("{}/v1/agents/{}/sessions", self.base_url, agent_id);
177        let mut params = Vec::new();
178        if let Some(active) = active_only {
179            params.push(format!("active_only={}", active));
180        }
181        if let Some(l) = limit {
182            params.push(format!("limit={}", l));
183        }
184        if !params.is_empty() {
185            url.push('?');
186            url.push_str(&params.join("&"));
187        }
188
189        let response = self.client.get(&url).send().await?;
190        self.handle_response(response).await
191    }
192
193    /// Return top-N wake-up context memories for an agent (DAK-1690).
194    ///
195    /// Calls `GET /v1/agents/{agent_id}/wake-up`. Returns memories ranked by
196    /// `importance × exp(-ln2 × age / 14d)` — no embedding inference, served
197    /// from the metadata index for sub-millisecond latency.
198    ///
199    /// Requires Read scope on the agent namespace.
200    ///
201    /// # Arguments
202    /// * `agent_id` — Agent identifier.
203    /// * `top_n` — Maximum memories to return (default 20, max 100). Pass `None` to use default.
204    /// * `min_importance` — Only return memories with importance ≥ this value. Pass `None` for 0.0.
205    pub async fn wake_up(
206        &self,
207        agent_id: &str,
208        top_n: Option<u32>,
209        min_importance: Option<f32>,
210    ) -> Result<WakeUpResponse> {
211        let mut url = format!("{}/v1/agents/{}/wake-up", self.base_url, agent_id);
212        let mut params = Vec::new();
213        if let Some(n) = top_n {
214            params.push(format!("top_n={}", n));
215        }
216        if let Some(mi) = min_importance {
217            params.push(format!("min_importance={}", mi));
218        }
219        if !params.is_empty() {
220            url.push('?');
221            url.push_str(&params.join("&"));
222        }
223
224        let response = self.client.get(&url).send().await?;
225        self.handle_response(response).await
226    }
227
228    /// Compress the memory namespace for an agent (CE-12).
229    ///
230    /// Runs a server-side compression pass that removes low-value or redundant
231    /// memories, returning statistics about the operation.
232    ///
233    /// # Arguments
234    /// * `agent_id` — Agent identifier.
235    pub async fn compress(&self, agent_id: &str) -> Result<CompressResponse> {
236        let url = format!("{}/v1/agents/{}/compress", self.base_url, agent_id);
237        let response = self.client.post(&url).send().await?;
238        self.handle_response(response).await
239    }
240
241    /// Alias for [`compress`](Self::compress) matching Python/JS/Go SDK naming.
242    pub async fn compress_agent(&self, agent_id: &str) -> Result<CompressResponse> {
243        self.compress(agent_id).await
244    }
245
246    /// Consolidate memories for an agent using the agent-scoped endpoint.
247    #[tracing::instrument(skip(self))]
248    pub async fn consolidate_agent(&self, agent_id: &str) -> Result<AgentConsolidateResponse> {
249        let url = format!("{}/v1/agents/{}/consolidate", self.base_url, agent_id);
250        let response = self.client.post(&url).send().await?;
251        self.handle_response(response).await
252    }
253
254    /// Get the consolidation execution log for an agent.
255    #[tracing::instrument(skip(self))]
256    pub async fn get_consolidation_log(
257        &self,
258        agent_id: &str,
259    ) -> Result<Vec<AgentConsolidationLogEntry>> {
260        let url = format!("{}/v1/agents/{}/consolidation/log", self.base_url, agent_id);
261        let response = self.client.get(&url).send().await?;
262        self.handle_response(response).await
263    }
264
265    /// Update the consolidation configuration for an agent.
266    #[tracing::instrument(skip(self, patch))]
267    pub async fn patch_consolidation_config(
268        &self,
269        agent_id: &str,
270        patch: ConsolidationConfigPatch,
271    ) -> Result<AgentConsolidationConfig> {
272        let url = format!(
273            "{}/v1/agents/{}/consolidation/config",
274            self.base_url, agent_id
275        );
276        let response = self.client.patch(&url).json(&patch).send().await?;
277        self.handle_response(response).await
278    }
279}
280
281// ============================================================================
282// Wake-Up Types (DAK-1690)
283// ============================================================================
284
285/// A stored memory returned by agent endpoints (non-recall, no similarity score).
286#[derive(Debug, Clone, Serialize, Deserialize)]
287pub struct Memory {
288    /// Memory ID
289    pub id: String,
290    /// Memory content
291    pub content: String,
292    /// Memory type (episodic, semantic, procedural, working)
293    pub memory_type: String,
294    /// Importance score (0.0–1.0)
295    pub importance: f32,
296    /// Optional metadata
297    #[serde(skip_serializing_if = "Option::is_none")]
298    pub metadata: Option<serde_json::Value>,
299    /// Creation timestamp (ISO 8601)
300    #[serde(skip_serializing_if = "Option::is_none")]
301    pub created_at: Option<String>,
302    /// Last update timestamp (ISO 8601)
303    #[serde(skip_serializing_if = "Option::is_none")]
304    pub updated_at: Option<String>,
305    /// Number of times this memory has been accessed
306    #[serde(skip_serializing_if = "Option::is_none")]
307    pub access_count: Option<i64>,
308}
309
310/// Response from `GET /v1/agents/{agent_id}/wake-up` (DAK-1690).
311///
312/// Contains top-N memories ranked by recency-weighted importance for fast
313/// agent start-up context loading.
314#[derive(Debug, Clone, Serialize, Deserialize)]
315pub struct WakeUpResponse {
316    /// The agent whose memories are returned
317    pub agent_id: String,
318    /// Top-N memories ranked by `importance × exp(-ln2 × age / 14d)`
319    pub memories: Vec<Memory>,
320    /// Total memories available before `top_n` cap was applied
321    pub total_available: i64,
322}
323
324// ============================================================================
325// Compress Types (CE-12)
326// ============================================================================
327
328/// Response from `POST /v1/agents/{agent_id}/compress` (CE-12).
329///
330/// Contains compression statistics for the agent's memory namespace after the
331/// server runs the DBSCAN compression pass (`POST /v1/agents/{id}/compress`).
332///
333/// Server returns: `{"agent_id":"...","memories_scanned":N,"originals_deprecated":N,
334/// "clusters_found":N,"summaries_created":N,...}`.
335#[derive(Debug, Clone, Serialize, Deserialize)]
336pub struct CompressResponse {
337    /// The agent whose namespace was compressed
338    pub agent_id: String,
339    /// Memories scanned (server field `memories_scanned`)
340    #[serde(default)]
341    pub memories_scanned: i64,
342    /// Memories removed via DBSCAN clustering (server field `originals_deprecated`)
343    #[serde(default, alias = "removed_count")]
344    pub originals_deprecated: i64,
345    /// DBSCAN clusters found
346    #[serde(default)]
347    pub clusters_found: i64,
348    /// Summary memories created from cluster centroids
349    #[serde(default)]
350    pub summaries_created: i64,
351    /// IDs of memories that were deprecated
352    #[serde(default, skip_serializing_if = "Vec::is_empty")]
353    pub deprecated_ids: Vec<String>,
354    /// Wall-clock duration of the compression pass in milliseconds
355    #[serde(skip_serializing_if = "Option::is_none")]
356    pub duration_ms: Option<f64>,
357}