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::DakeraClient;
8
9// ============================================================================
10// Agent Types
11// ============================================================================
12
13/// Summary of an agent
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct AgentSummary {
16    pub agent_id: String,
17    pub memory_count: i64,
18    pub session_count: i64,
19    pub active_sessions: i64,
20}
21
22/// Detailed stats for an agent
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct AgentStats {
25    pub agent_id: String,
26    pub total_memories: i64,
27    #[serde(default)]
28    pub memories_by_type: std::collections::HashMap<String, i64>,
29    pub total_sessions: i64,
30    pub active_sessions: i64,
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub avg_importance: Option<f32>,
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub oldest_memory_at: Option<String>,
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub newest_memory_at: Option<String>,
37}
38
39// ============================================================================
40// Agent Client Methods
41// ============================================================================
42
43impl DakeraClient {
44    /// List all agents
45    pub async fn list_agents(&self) -> Result<Vec<AgentSummary>> {
46        let url = format!("{}/v1/agents", self.base_url);
47        let response = self.client.get(&url).send().await?;
48        self.handle_response(response).await
49    }
50
51    /// Get memories for an agent
52    pub async fn agent_memories(
53        &self,
54        agent_id: &str,
55        memory_type: Option<&str>,
56        limit: Option<u32>,
57    ) -> Result<Vec<RecalledMemory>> {
58        let mut url = format!("{}/v1/agents/{}/memories", self.base_url, agent_id);
59        let mut params = Vec::new();
60        if let Some(t) = memory_type {
61            params.push(format!("memory_type={}", t));
62        }
63        if let Some(l) = limit {
64            params.push(format!("limit={}", l));
65        }
66        if !params.is_empty() {
67            url.push('?');
68            url.push_str(&params.join("&"));
69        }
70
71        let response = self.client.get(&url).send().await?;
72        self.handle_response(response).await
73    }
74
75    /// Get stats for an agent
76    pub async fn agent_stats(&self, agent_id: &str) -> Result<AgentStats> {
77        let url = format!("{}/v1/agents/{}/stats", self.base_url, agent_id);
78        let response = self.client.get(&url).send().await?;
79        self.handle_response(response).await
80    }
81
82    /// Subscribe to real-time memory lifecycle events for a specific agent.
83    ///
84    /// Opens a long-lived connection to `GET /v1/events/stream` and returns a
85    /// [`tokio::sync::mpsc::Receiver`] that yields [`MemoryEvent`] results filtered
86    /// to the given `agent_id`.  An optional `tags` list further restricts events
87    /// to those whose tags have at least one overlap with the filter.
88    ///
89    /// The background task reconnects automatically on stream error.  It exits
90    /// when the returned receiver is dropped.
91    ///
92    /// Requires a Read-scoped API key.
93    ///
94    /// # Example
95    ///
96    /// ```rust,no_run
97    /// use dakera_client::DakeraClient;
98    ///
99    /// #[tokio::main]
100    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
101    ///     let client = DakeraClient::new("http://localhost:3000")?;
102    ///     let mut rx = client.subscribe_agent_events("my-bot", None).await?;
103    ///     while let Some(result) = rx.recv().await {
104    ///         let event = result?;
105    ///         println!("{}: {:?}", event.event_type, event.memory_id);
106    ///     }
107    ///     Ok(())
108    /// }
109    /// ```
110    pub async fn subscribe_agent_events(
111        &self,
112        agent_id: &str,
113        tags: Option<Vec<String>>,
114    ) -> crate::error::Result<
115        tokio::sync::mpsc::Receiver<crate::error::Result<crate::events::MemoryEvent>>,
116    > {
117        let (tx, rx) = tokio::sync::mpsc::channel(64);
118        let client = self.clone();
119        let agent_id = agent_id.to_owned();
120
121        tokio::spawn(async move {
122            loop {
123                match client.stream_memory_events().await {
124                    Err(_) => {
125                        tokio::time::sleep(std::time::Duration::from_secs(1)).await;
126                        continue;
127                    }
128                    Ok(mut inner_rx) => {
129                        while let Some(result) = inner_rx.recv().await {
130                            match result {
131                                Err(e) => {
132                                    // Send the error but don't kill the reconnect loop.
133                                    let _ = tx.send(Err(e)).await;
134                                    break;
135                                }
136                                Ok(event) => {
137                                    if event.event_type == "connected" {
138                                        continue;
139                                    }
140                                    if event.agent_id != agent_id {
141                                        continue;
142                                    }
143                                    if let Some(ref filter_tags) = tags {
144                                        let event_tags = event.tags.as_deref().unwrap_or(&[]);
145                                        if !filter_tags.iter().any(|t| event_tags.contains(t)) {
146                                            continue;
147                                        }
148                                    }
149                                    if tx.send(Ok(event)).await.is_err() {
150                                        return; // Receiver dropped — exit.
151                                    }
152                                }
153                            }
154                        }
155                    }
156                }
157                // Reconnect after a short delay.
158                tokio::time::sleep(std::time::Duration::from_secs(1)).await;
159            }
160        });
161
162        Ok(rx)
163    }
164
165    /// Get sessions for an agent
166    pub async fn agent_sessions(
167        &self,
168        agent_id: &str,
169        active_only: Option<bool>,
170        limit: Option<u32>,
171    ) -> Result<Vec<Session>> {
172        let mut url = format!("{}/v1/agents/{}/sessions", self.base_url, agent_id);
173        let mut params = Vec::new();
174        if let Some(active) = active_only {
175            params.push(format!("active_only={}", active));
176        }
177        if let Some(l) = limit {
178            params.push(format!("limit={}", l));
179        }
180        if !params.is_empty() {
181            url.push('?');
182            url.push_str(&params.join("&"));
183        }
184
185        let response = self.client.get(&url).send().await?;
186        self.handle_response(response).await
187    }
188
189    /// Return top-N wake-up context memories for an agent (DAK-1690).
190    ///
191    /// Calls `GET /v1/agents/{agent_id}/wake-up`. Returns memories ranked by
192    /// `importance × exp(-ln2 × age / 14d)` — no embedding inference, served
193    /// from the metadata index for sub-millisecond latency.
194    ///
195    /// Requires Read scope on the agent namespace.
196    ///
197    /// # Arguments
198    /// * `agent_id` — Agent identifier.
199    /// * `top_n` — Maximum memories to return (default 20, max 100). Pass `None` to use default.
200    /// * `min_importance` — Only return memories with importance ≥ this value. Pass `None` for 0.0.
201    pub async fn wake_up(
202        &self,
203        agent_id: &str,
204        top_n: Option<u32>,
205        min_importance: Option<f32>,
206    ) -> Result<WakeUpResponse> {
207        let mut url = format!("{}/v1/agents/{}/wake-up", self.base_url, agent_id);
208        let mut params = Vec::new();
209        if let Some(n) = top_n {
210            params.push(format!("top_n={}", n));
211        }
212        if let Some(mi) = min_importance {
213            params.push(format!("min_importance={}", mi));
214        }
215        if !params.is_empty() {
216            url.push('?');
217            url.push_str(&params.join("&"));
218        }
219
220        let response = self.client.get(&url).send().await?;
221        self.handle_response(response).await
222    }
223}
224
225// ============================================================================
226// Wake-Up Types (DAK-1690)
227// ============================================================================
228
229/// A stored memory returned by agent endpoints (non-recall, no similarity score).
230#[derive(Debug, Clone, Serialize, Deserialize)]
231pub struct Memory {
232    /// Memory ID
233    pub id: String,
234    /// Memory content
235    pub content: String,
236    /// Memory type (episodic, semantic, procedural, working)
237    pub memory_type: String,
238    /// Importance score (0.0–1.0)
239    pub importance: f32,
240    /// Optional metadata
241    #[serde(skip_serializing_if = "Option::is_none")]
242    pub metadata: Option<serde_json::Value>,
243    /// Creation timestamp (ISO 8601)
244    #[serde(skip_serializing_if = "Option::is_none")]
245    pub created_at: Option<String>,
246    /// Last update timestamp (ISO 8601)
247    #[serde(skip_serializing_if = "Option::is_none")]
248    pub updated_at: Option<String>,
249    /// Number of times this memory has been accessed
250    #[serde(skip_serializing_if = "Option::is_none")]
251    pub access_count: Option<i64>,
252}
253
254/// Response from `GET /v1/agents/{agent_id}/wake-up` (DAK-1690).
255///
256/// Contains top-N memories ranked by recency-weighted importance for fast
257/// agent start-up context loading.
258#[derive(Debug, Clone, Serialize, Deserialize)]
259pub struct WakeUpResponse {
260    /// The agent whose memories are returned
261    pub agent_id: String,
262    /// Top-N memories ranked by `importance × exp(-ln2 × age / 14d)`
263    pub memories: Vec<Memory>,
264    /// Total memories available before `top_n` cap was applied
265    pub total_available: i64,
266}