Skip to main content

mockforge_foundation/intelligent_behavior/
session_state.rs

1//! Session types: `InteractionRecord` and `SessionState`
2//!
3//! Extracted from `mockforge-core::intelligent_behavior::types` (Phase 6 / A7).
4//! Uses `crate::clock::now()` which honors any registered time-travel clock.
5
6use crate::clock::now as clock_now;
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11/// A single interaction record (request + response pair)
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct InteractionRecord {
14    /// Timestamp of the interaction
15    pub timestamp: DateTime<Utc>,
16
17    /// HTTP method (GET, POST, PUT, DELETE, etc.)
18    pub method: String,
19
20    /// Request path (e.g., /api/users/123)
21    pub path: String,
22
23    /// Query parameters
24    #[serde(default)]
25    pub query_params: HashMap<String, String>,
26
27    /// Request headers
28    #[serde(default)]
29    pub headers: HashMap<String, String>,
30
31    /// Request body (if present)
32    pub request: Option<serde_json::Value>,
33
34    /// Response status code
35    pub status: u16,
36
37    /// Response body (if present)
38    pub response: Option<serde_json::Value>,
39
40    /// Vector embedding for semantic search (generated from interaction summary)
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub embedding: Option<Vec<f32>>,
43
44    /// Metadata about this interaction
45    #[serde(default)]
46    pub metadata: HashMap<String, String>,
47}
48
49impl InteractionRecord {
50    /// Create a new interaction record (timestamped at the current wall-clock time).
51    pub fn new(
52        method: impl Into<String>,
53        path: impl Into<String>,
54        request: Option<serde_json::Value>,
55        status: u16,
56        response: Option<serde_json::Value>,
57    ) -> Self {
58        Self {
59            timestamp: Utc::now(),
60            method: method.into(),
61            path: path.into(),
62            query_params: HashMap::new(),
63            headers: HashMap::new(),
64            request,
65            status,
66            response,
67            embedding: None,
68            metadata: HashMap::new(),
69        }
70    }
71
72    /// Add query parameters.
73    #[must_use]
74    pub fn with_query_params(mut self, params: HashMap<String, String>) -> Self {
75        self.query_params = params;
76        self
77    }
78
79    /// Add headers.
80    #[must_use]
81    pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
82        self.headers = headers;
83        self
84    }
85
86    /// Set embedding.
87    #[must_use]
88    pub fn with_embedding(mut self, embedding: Vec<f32>) -> Self {
89        self.embedding = Some(embedding);
90        self
91    }
92
93    /// Add metadata.
94    #[must_use]
95    pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
96        self.metadata.insert(key.into(), value.into());
97        self
98    }
99
100    /// Generate a textual summary of this interaction for embedding.
101    pub fn summary(&self) -> String {
102        let request_body = self
103            .request
104            .as_ref()
105            .map(|r| serde_json::to_string(r).unwrap_or_default())
106            .unwrap_or_default();
107
108        let response_body = self
109            .response
110            .as_ref()
111            .map(|r| serde_json::to_string(r).unwrap_or_default())
112            .unwrap_or_default();
113
114        format!(
115            "{} {} | Request: {} | Status: {} | Response: {}",
116            self.method, self.path, request_body, self.status, response_body
117        )
118    }
119}
120
121/// Session state snapshot
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct SessionState {
124    /// Session ID
125    pub session_id: String,
126
127    /// Current state data (e.g., logged-in user, cart items, etc.)
128    pub state: HashMap<String, serde_json::Value>,
129
130    /// Interaction history for this session
131    pub history: Vec<InteractionRecord>,
132
133    /// Session creation time
134    pub created_at: DateTime<Utc>,
135
136    /// Last activity time
137    pub last_activity: DateTime<Utc>,
138
139    /// Session metadata
140    #[serde(default)]
141    pub metadata: HashMap<String, String>,
142}
143
144impl SessionState {
145    /// Create a new session state.
146    ///
147    /// Uses `crate::clock::now()` which respects any registered virtual clock
148    /// (see `mockforge_foundation::clock::set_clock`).
149    pub fn new(session_id: impl Into<String>) -> Self {
150        let now = clock_now();
151        Self {
152            session_id: session_id.into(),
153            state: HashMap::new(),
154            history: Vec::new(),
155            created_at: now,
156            last_activity: now,
157            metadata: HashMap::new(),
158        }
159    }
160
161    /// Update last activity timestamp.
162    ///
163    /// Uses `crate::clock::now()` which respects any registered virtual clock.
164    pub fn touch(&mut self) {
165        self.last_activity = clock_now();
166    }
167
168    /// Add an interaction to history.
169    pub fn record_interaction(&mut self, interaction: InteractionRecord) {
170        self.history.push(interaction);
171        self.touch();
172    }
173
174    /// Get a value from state.
175    pub fn get(&self, key: &str) -> Option<&serde_json::Value> {
176        self.state.get(key)
177    }
178
179    /// Set a value in state.
180    pub fn set(&mut self, key: impl Into<String>, value: serde_json::Value) {
181        self.state.insert(key.into(), value);
182        self.touch();
183    }
184
185    /// Remove a value from state.
186    pub fn remove(&mut self, key: &str) -> Option<serde_json::Value> {
187        let result = self.state.remove(key);
188        self.touch();
189        result
190    }
191
192    /// Check if session has been inactive for a duration.
193    ///
194    /// Uses `crate::clock::now()` which respects any registered virtual clock.
195    pub fn is_inactive(&self, duration: chrono::Duration) -> bool {
196        clock_now().signed_duration_since(self.last_activity) > duration
197    }
198}