use serde::{Deserialize, Serialize};
use crate::error::Result;
use crate::memory::{RecalledMemory, Session};
use crate::types::{
AgentConsolidateResponse, AgentConsolidationConfig, AgentConsolidationLogEntry,
ConsolidationConfigPatch,
};
use crate::DakeraClient;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentSummary {
pub agent_id: String,
pub memory_count: i64,
pub session_count: i64,
pub active_sessions: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentStats {
pub agent_id: String,
pub total_memories: i64,
#[serde(default)]
pub memories_by_type: std::collections::HashMap<String, i64>,
pub total_sessions: i64,
pub active_sessions: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub avg_importance: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub oldest_memory_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub newest_memory_at: Option<String>,
}
impl DakeraClient {
pub async fn list_agents(&self) -> Result<Vec<AgentSummary>> {
let url = format!("{}/v1/agents", self.base_url);
let response = self.client.get(&url).send().await?;
self.handle_response(response).await
}
pub async fn agent_memories(
&self,
agent_id: &str,
memory_type: Option<&str>,
limit: Option<u32>,
) -> Result<Vec<RecalledMemory>> {
let mut url = format!("{}/v1/agents/{}/memories", self.base_url, agent_id);
let mut params = Vec::new();
if let Some(t) = memory_type {
params.push(format!("memory_type={}", t));
}
if let Some(l) = limit {
params.push(format!("limit={}", l));
}
if !params.is_empty() {
url.push('?');
url.push_str(¶ms.join("&"));
}
let response = self.client.get(&url).send().await?;
self.handle_response(response).await
}
pub async fn agent_stats(&self, agent_id: &str) -> Result<AgentStats> {
let url = format!("{}/v1/agents/{}/stats", self.base_url, agent_id);
let response = self.client.get(&url).send().await?;
self.handle_response(response).await
}
pub async fn subscribe_agent_events(
&self,
agent_id: &str,
tags: Option<Vec<String>>,
) -> crate::error::Result<
tokio::sync::mpsc::Receiver<crate::error::Result<crate::events::MemoryEvent>>,
> {
let (tx, rx) = tokio::sync::mpsc::channel(64);
let client = self.clone();
let agent_id = agent_id.to_owned();
tokio::spawn(async move {
loop {
match client.stream_memory_events().await {
Err(_) => {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
continue;
}
Ok(mut inner_rx) => {
while let Some(result) = inner_rx.recv().await {
match result {
Err(e) => {
let _ = tx.send(Err(e)).await;
break;
}
Ok(event) => {
if event.event_type == "connected" {
continue;
}
if event.agent_id != agent_id {
continue;
}
if let Some(ref filter_tags) = tags {
let event_tags = event.tags.as_deref().unwrap_or(&[]);
if !filter_tags.iter().any(|t| event_tags.contains(t)) {
continue;
}
}
if tx.send(Ok(event)).await.is_err() {
return; }
}
}
}
}
}
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
});
Ok(rx)
}
pub async fn agent_sessions(
&self,
agent_id: &str,
active_only: Option<bool>,
limit: Option<u32>,
) -> Result<Vec<Session>> {
let mut url = format!("{}/v1/agents/{}/sessions", self.base_url, agent_id);
let mut params = Vec::new();
if let Some(active) = active_only {
params.push(format!("active_only={}", active));
}
if let Some(l) = limit {
params.push(format!("limit={}", l));
}
if !params.is_empty() {
url.push('?');
url.push_str(¶ms.join("&"));
}
let response = self.client.get(&url).send().await?;
self.handle_response(response).await
}
pub async fn wake_up(
&self,
agent_id: &str,
top_n: Option<u32>,
min_importance: Option<f32>,
) -> Result<WakeUpResponse> {
let mut url = format!("{}/v1/agents/{}/wake-up", self.base_url, agent_id);
let mut params = Vec::new();
if let Some(n) = top_n {
params.push(format!("top_n={}", n));
}
if let Some(mi) = min_importance {
params.push(format!("min_importance={}", mi));
}
if !params.is_empty() {
url.push('?');
url.push_str(¶ms.join("&"));
}
let response = self.client.get(&url).send().await?;
self.handle_response(response).await
}
pub async fn compress(&self, agent_id: &str) -> Result<CompressResponse> {
let url = format!("{}/v1/agents/{}/compress", self.base_url, agent_id);
let response = self.client.post(&url).send().await?;
self.handle_response(response).await
}
pub async fn compress_agent(&self, agent_id: &str) -> Result<CompressResponse> {
self.compress(agent_id).await
}
#[tracing::instrument(skip(self))]
pub async fn consolidate_agent(&self, agent_id: &str) -> Result<AgentConsolidateResponse> {
let url = format!("{}/v1/agents/{}/consolidate", self.base_url, agent_id);
let response = self.client.post(&url).send().await?;
self.handle_response(response).await
}
#[tracing::instrument(skip(self))]
pub async fn get_consolidation_log(
&self,
agent_id: &str,
) -> Result<Vec<AgentConsolidationLogEntry>> {
let url = format!("{}/v1/agents/{}/consolidation/log", self.base_url, agent_id);
let response = self.client.get(&url).send().await?;
self.handle_response(response).await
}
#[tracing::instrument(skip(self, patch))]
pub async fn patch_consolidation_config(
&self,
agent_id: &str,
patch: ConsolidationConfigPatch,
) -> Result<AgentConsolidationConfig> {
let url = format!(
"{}/v1/agents/{}/consolidation/config",
self.base_url, agent_id
);
let response = self.client.patch(&url).json(&patch).send().await?;
self.handle_response(response).await
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Memory {
pub id: String,
pub content: String,
pub memory_type: String,
pub importance: f32,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub created_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub updated_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub access_count: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WakeUpResponse {
pub agent_id: String,
pub memories: Vec<Memory>,
pub total_available: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompressResponse {
pub agent_id: String,
#[serde(default)]
pub memories_scanned: i64,
#[serde(default, alias = "removed_count")]
pub originals_deprecated: i64,
#[serde(default)]
pub clusters_found: i64,
#[serde(default)]
pub summaries_created: i64,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub deprecated_ids: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub duration_ms: Option<f64>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_agent_summary_deserializes() {
let json = r#"{
"agent_id": "agent-xyz",
"memory_count": 42,
"session_count": 7,
"active_sessions": 2
}"#;
let s: AgentSummary = serde_json::from_str(json).unwrap();
assert_eq!(s.agent_id, "agent-xyz");
assert_eq!(s.memory_count, 42);
assert_eq!(s.session_count, 7);
assert_eq!(s.active_sessions, 2);
}
#[test]
fn test_agent_stats_memories_by_type_defaults_empty() {
let json =
r#"{"agent_id": "a", "total_memories": 0, "total_sessions": 0, "active_sessions": 0}"#;
let s: AgentStats = serde_json::from_str(json).unwrap();
assert!(s.memories_by_type.is_empty());
assert!(s.avg_importance.is_none());
assert!(s.oldest_memory_at.is_none());
assert!(s.newest_memory_at.is_none());
}
#[test]
fn test_agent_stats_with_type_distribution() {
let json = r#"{
"agent_id": "a",
"total_memories": 10,
"total_sessions": 3,
"active_sessions": 1,
"memories_by_type": {"episodic": 5, "semantic": 5},
"avg_importance": 0.72
}"#;
let s: AgentStats = serde_json::from_str(json).unwrap();
assert_eq!(s.memories_by_type["episodic"], 5);
assert!((s.avg_importance.unwrap() - 0.72).abs() < 1e-6);
}
#[test]
fn test_memory_optional_fields_omitted_in_serialize() {
let m = Memory {
id: "mem-1".to_string(),
content: "hello".to_string(),
memory_type: "episodic".to_string(),
importance: 0.8,
metadata: None,
created_at: None,
updated_at: None,
access_count: None,
};
let json = serde_json::to_string(&m).unwrap();
assert!(!json.contains("metadata"));
assert!(!json.contains("created_at"));
assert!(!json.contains("updated_at"));
assert!(!json.contains("access_count"));
}
#[test]
fn test_memory_with_all_optional_fields() {
let json = r#"{
"id": "m1",
"content": "test",
"memory_type": "semantic",
"importance": 0.9,
"metadata": {"key": "val"},
"created_at": "2026-01-01T00:00:00Z",
"updated_at": "2026-01-02T00:00:00Z",
"access_count": 5
}"#;
let m: Memory = serde_json::from_str(json).unwrap();
assert!(m.metadata.is_some());
assert_eq!(m.access_count, Some(5));
}
#[test]
fn test_wake_up_response_deserializes() {
let json = r#"{
"agent_id": "agent-1",
"memories": [],
"total_available": 50
}"#;
let r: WakeUpResponse = serde_json::from_str(json).unwrap();
assert_eq!(r.agent_id, "agent-1");
assert!(r.memories.is_empty());
assert_eq!(r.total_available, 50);
}
#[test]
fn test_compress_response_defaults_zero() {
let json = r#"{"agent_id": "a"}"#;
let r: CompressResponse = serde_json::from_str(json).unwrap();
assert_eq!(r.memories_scanned, 0);
assert_eq!(r.originals_deprecated, 0);
assert_eq!(r.clusters_found, 0);
assert_eq!(r.summaries_created, 0);
assert!(r.deprecated_ids.is_empty());
assert!(r.duration_ms.is_none());
}
#[test]
fn test_compress_response_alias_removed_count() {
let json = r#"{
"agent_id": "a",
"memories_scanned": 100,
"removed_count": 30,
"clusters_found": 10,
"summaries_created": 10
}"#;
let r: CompressResponse = serde_json::from_str(json).unwrap();
assert_eq!(r.originals_deprecated, 30);
}
#[test]
fn test_compress_response_deprecated_ids_omitted_when_empty() {
let r = CompressResponse {
agent_id: "a".to_string(),
memories_scanned: 0,
originals_deprecated: 0,
clusters_found: 0,
summaries_created: 0,
deprecated_ids: vec![],
duration_ms: None,
};
let json = serde_json::to_string(&r).unwrap();
assert!(!json.contains("deprecated_ids"));
}
#[test]
fn test_compress_response_with_duration_and_ids() {
let json = r#"{
"agent_id": "a",
"deprecated_ids": ["m1", "m2"],
"duration_ms": 42.5
}"#;
let r: CompressResponse = serde_json::from_str(json).unwrap();
assert_eq!(r.deprecated_ids.len(), 2);
assert!((r.duration_ms.unwrap() - 42.5).abs() < 1e-6);
}
}