Skip to main content

agent_diva_core/session/
store.rs

1//! Session data structures
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6/// A conversation session
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct Session {
9    /// Session key (channel:chat_id)
10    pub key: String,
11    /// Messages in the session
12    pub messages: Vec<ChatMessage>,
13    /// Session creation time
14    pub created_at: DateTime<Utc>,
15    /// Last update time
16    pub updated_at: DateTime<Utc>,
17    /// Session metadata
18    pub metadata: serde_json::Value,
19    /// Index of last consolidated message (for memory consolidation)
20    #[serde(default)]
21    pub last_consolidated: usize,
22}
23
24impl Session {
25    /// Create a new session
26    pub fn new(key: impl Into<String>) -> Self {
27        let now = Utc::now();
28        Self {
29            key: key.into(),
30            messages: Vec::new(),
31            created_at: now,
32            updated_at: now,
33            metadata: serde_json::Value::Object(serde_json::Map::new()),
34            last_consolidated: 0,
35        }
36    }
37
38    /// Add a message to the session
39    pub fn add_message(&mut self, role: impl Into<String>, content: impl Into<String>) {
40        self.messages.push(ChatMessage {
41            role: role.into(),
42            content: content.into(),
43            timestamp: Utc::now(),
44            tool_call_id: None,
45            tool_calls: None,
46            name: None,
47            reasoning_content: None,
48            thinking_blocks: None,
49        });
50        self.updated_at = Utc::now();
51    }
52
53    /// Add a complete ChatMessage to the session
54    pub fn add_full_message(&mut self, msg: ChatMessage) {
55        self.messages.push(msg);
56        self.updated_at = Utc::now();
57    }
58
59    /// Get message history for LLM context
60    pub fn get_history(&self, max_messages: usize) -> Vec<ChatMessage> {
61        // Clamp last_consolidated to avoid out-of-bounds on corrupted data
62        let consolidated = self.last_consolidated.min(self.messages.len());
63        let unconsolidated = &self.messages[consolidated..];
64        let start = unconsolidated.len().saturating_sub(max_messages);
65        let mut sliced: Vec<ChatMessage> = unconsolidated[start..]
66            .iter()
67            .filter(|m| matches!(m.role.as_str(), "user" | "assistant" | "tool"))
68            .cloned()
69            .collect();
70        // Drop leading non-user messages to avoid orphaned tool results
71        if let Some(pos) = sliced.iter().position(|m| m.role == "user") {
72            sliced = sliced[pos..].to_vec();
73        }
74        sliced
75    }
76
77    /// Clear all messages
78    pub fn clear(&mut self) {
79        self.messages.clear();
80        self.last_consolidated = 0;
81        self.updated_at = Utc::now();
82    }
83}
84
85/// A chat message
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct ChatMessage {
88    /// Message role (user, assistant, system, tool)
89    pub role: String,
90    /// Message content
91    pub content: String,
92    /// Message timestamp
93    pub timestamp: DateTime<Utc>,
94    /// Tool call ID (for tool-result messages)
95    #[serde(skip_serializing_if = "Option::is_none", default)]
96    pub tool_call_id: Option<String>,
97    /// Tool calls made by the assistant
98    #[serde(skip_serializing_if = "Option::is_none", default)]
99    pub tool_calls: Option<Vec<serde_json::Value>>,
100    /// Tool name (for tool-result messages)
101    #[serde(skip_serializing_if = "Option::is_none", default)]
102    pub name: Option<String>,
103    /// Optional reasoning content captured from thinking-capable models
104    #[serde(skip_serializing_if = "Option::is_none", default)]
105    pub reasoning_content: Option<String>,
106    /// Optional structured thinking blocks (provider-specific)
107    #[serde(skip_serializing_if = "Option::is_none", default)]
108    pub thinking_blocks: Option<Vec<serde_json::Value>>,
109}
110
111impl ChatMessage {
112    /// Create a new chat message
113    pub fn new(role: impl Into<String>, content: impl Into<String>) -> Self {
114        Self {
115            role: role.into(),
116            content: content.into(),
117            timestamp: Utc::now(),
118            tool_call_id: None,
119            tool_calls: None,
120            name: None,
121            reasoning_content: None,
122            thinking_blocks: None,
123        }
124    }
125
126    /// Create a chat message with full tool metadata
127    pub fn with_tool_metadata(
128        role: impl Into<String>,
129        content: impl Into<String>,
130        tool_call_id: Option<String>,
131        tool_calls: Option<Vec<serde_json::Value>>,
132        name: Option<String>,
133    ) -> Self {
134        Self {
135            role: role.into(),
136            content: content.into(),
137            timestamp: Utc::now(),
138            tool_call_id,
139            tool_calls,
140            name,
141            reasoning_content: None,
142            thinking_blocks: None,
143        }
144    }
145
146    /// Convert to LLM format (role and content only)
147    pub fn to_llm_format(&self) -> serde_json::Value {
148        serde_json::json!({
149            "role": &self.role,
150            "content": &self.content,
151        })
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn test_session_creation() {
161        let session = Session::new("telegram:12345");
162        assert_eq!(session.key, "telegram:12345");
163        assert!(session.messages.is_empty());
164    }
165
166    #[test]
167    fn test_add_message() {
168        let mut session = Session::new("test");
169        session.add_message("user", "Hello");
170        session.add_message("assistant", "Hi there!");
171
172        assert_eq!(session.messages.len(), 2);
173        assert_eq!(session.messages[0].role, "user");
174        assert_eq!(session.messages[1].role, "assistant");
175    }
176
177    #[test]
178    fn test_get_history() {
179        let mut session = Session::new("test");
180        for i in 0..60 {
181            session.add_message("user", format!("Message {}", i));
182        }
183
184        let history = session.get_history(50);
185        assert_eq!(history.len(), 50);
186    }
187}