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(¶ms.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(¶ms.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(¶ms.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}