Skip to main content

ri_agent_graph/
state.rs

1use crate::error::{AgentGraphError, Result};
2use crate::reducer::Reducer;
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use std::collections::HashMap;
6use std::sync::atomic::{AtomicU64, Ordering};
7use std::sync::Arc;
8use std::time::Duration;
9use tokio::sync::RwLock;
10
11/// Resource bounds for agent state (PRIMITIVES_CONTRACT §3).
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct StateLimits {
14    /// Maximum number of keys allowed in state. Default: 10_000.
15    pub max_keys: usize,
16    /// Maximum size in bytes for a single serialized value. Default: 1_048_576 (1 MiB).
17    pub max_value_bytes: usize,
18    /// Maximum number of history snapshots to retain. Default: 100.
19    pub max_history_len: usize,
20    /// Timeout for acquiring state locks. Default: 5 s.
21    pub lock_timeout: Duration,
22}
23
24impl Default for StateLimits {
25    fn default() -> Self {
26        Self {
27            max_keys: 10_000,
28            max_value_bytes: 1_048_576,
29            max_history_len: 100,
30            lock_timeout: Duration::from_secs(5),
31        }
32    }
33}
34
35/// A transaction over [`AgentState`] that can be committed or rolled back.
36///
37/// Created via [`AgentState::transaction()`]. Writes are staged against an
38/// isolated working copy and only become visible when
39/// [`commit()`](Self::commit) is called.
40pub struct StateTransaction {
41    state: AgentState,
42    working: RwLock<HashMap<String, Value>>,
43    snapshot_version: u64,
44    committed: bool,
45}
46
47impl StateTransaction {
48    /// Read a value within the transaction.
49    pub async fn get<T: serde::de::DeserializeOwned>(&self, key: &str) -> Result<T> {
50        let working = self.working.read().await;
51        let value = working
52            .get(key)
53            .ok_or_else(|| AgentGraphError::StateError(format!("Key not found: {}", key)))?;
54
55        serde_json::from_value(value.clone()).map_err(|e| {
56            AgentGraphError::StateError(format!("Failed to deserialize {}: {}", key, e))
57        })
58    }
59
60    /// Write a value within the transaction.
61    pub async fn set<T: Serialize>(&self, key: &str, value: T) -> Result<()> {
62        let json_value = self.state.serialize_value(key, value)?;
63        let existing = {
64            let working = self.working.read().await;
65            working.get(key).cloned()
66        };
67        let next_value = if let Some(existing) = existing.as_ref() {
68            self.state
69                .reduce_value_if_needed(key, existing, &json_value)
70                .await?
71        } else {
72            json_value
73        };
74
75        let mut working = self.working.write().await;
76        self.state.validate_insert(&working, key, &next_value)?;
77        working.insert(key.to_string(), next_value);
78        Ok(())
79    }
80
81    /// Commit the transaction. After this call, staged changes are applied atomically.
82    ///
83    /// Returns an error if the underlying state was modified concurrently since
84    /// this transaction was created.
85    pub async fn commit(mut self) -> Result<()> {
86        let current_version = self.state.version.load(Ordering::SeqCst);
87        if current_version != self.snapshot_version {
88            return Err(AgentGraphError::StateError(
89                "Transaction conflict: state was modified concurrently".to_string(),
90            ));
91        }
92        let next = self.working.read().await.clone();
93        // replace_data increments the version internally.
94        self.state.replace_data(next).await;
95        self.committed = true;
96        Ok(())
97    }
98
99    /// Roll back all staged changes.
100    pub async fn rollback(mut self) {
101        self.committed = true;
102    }
103}
104
105impl Drop for StateTransaction {
106    fn drop(&mut self) {
107        if !self.committed {
108            tracing::debug!("dropping uncommitted state transaction");
109        }
110    }
111}
112
113/// Shared state that persists across graph execution.
114/// All nodes can read and write to this state.
115#[derive(Clone)]
116pub struct AgentState {
117    data: Arc<RwLock<HashMap<String, Value>>>,
118    history: Arc<RwLock<Vec<StateSnapshot>>>,
119    pub(crate) reducers: Arc<RwLock<HashMap<String, Arc<dyn Reducer>>>>,
120    limits: StateLimits,
121    version: Arc<AtomicU64>,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct StateSnapshot {
126    pub timestamp: chrono::DateTime<chrono::Utc>,
127    pub data: HashMap<String, Value>,
128}
129
130impl AgentState {
131    /// Create a new empty state
132    pub fn new() -> Self {
133        Self::with_limits(StateLimits::default())
134    }
135
136    /// Create a new empty state with explicit limits.
137    pub fn with_limits(limits: StateLimits) -> Self {
138        Self {
139            data: Arc::new(RwLock::new(HashMap::new())),
140            history: Arc::new(RwLock::new(Vec::new())),
141            reducers: Arc::new(RwLock::new(HashMap::new())),
142            limits,
143            version: Arc::new(AtomicU64::new(0)),
144        }
145    }
146
147    /// Create state with initial data
148    pub fn with_data(data: HashMap<String, Value>) -> Self {
149        Self {
150            data: Arc::new(RwLock::new(data)),
151            history: Arc::new(RwLock::new(Vec::new())),
152            reducers: Arc::new(RwLock::new(HashMap::new())),
153            limits: StateLimits::default(),
154            version: Arc::new(AtomicU64::new(0)),
155        }
156    }
157
158    /// Create state with initial data and explicit limits.
159    pub fn with_data_and_limits(data: HashMap<String, Value>, limits: StateLimits) -> Self {
160        Self {
161            data: Arc::new(RwLock::new(data)),
162            history: Arc::new(RwLock::new(Vec::new())),
163            reducers: Arc::new(RwLock::new(HashMap::new())),
164            limits,
165            version: Arc::new(AtomicU64::new(0)),
166        }
167    }
168
169    /// Register a reducer for a specific state key.
170    /// When `set()` is called on this key and a value already exists,
171    /// the reducer is used to combine old and new values.
172    pub async fn register_reducer(&self, key: impl Into<String>, reducer: impl Reducer + 'static) {
173        match self.write_reducers().await {
174            Ok(mut reducers) => {
175                reducers.insert(key.into(), Arc::new(reducer));
176            }
177            Err(error) => {
178                tracing::warn!(error = %error, "failed to register reducer");
179            }
180        }
181    }
182
183    /// Get a value from state (type-safe)
184    pub async fn get<T>(&self, key: &str) -> Result<T>
185    where
186        T: serde::de::DeserializeOwned,
187    {
188        let data = self.read_data().await?;
189        let value = data
190            .get(key)
191            .ok_or_else(|| AgentGraphError::StateError(format!("Key not found: {}", key)))?;
192
193        serde_json::from_value(value.clone()).map_err(|e| {
194            AgentGraphError::StateError(format!("Failed to deserialize {}: {}", key, e))
195        })
196    }
197
198    /// Try to get a value (returns None if missing)
199    pub async fn get_opt<T>(&self, key: &str) -> Result<Option<T>>
200    where
201        T: serde::de::DeserializeOwned,
202    {
203        let data = self.read_data().await?;
204        match data.get(key) {
205            Some(value) => {
206                let v = serde_json::from_value(value.clone())?;
207                Ok(Some(v))
208            }
209            None => Ok(None),
210        }
211    }
212
213    /// Set a value in state (type-safe).
214    /// If a reducer is registered for this key and a value already exists,
215    /// the reducer is used to combine old and new values.
216    pub async fn set<T>(&self, key: &str, value: T) -> Result<()>
217    where
218        T: Serialize,
219    {
220        let json_value = self.serialize_value(key, value)?;
221        let existing = {
222            let data = self.read_data().await?;
223            data.get(key).cloned()
224        };
225        let next_value = if let Some(existing) = existing.as_ref() {
226            self.reduce_value_if_needed(key, existing, &json_value)
227                .await?
228        } else {
229            json_value
230        };
231
232        let mut data = self.write_data().await?;
233        self.validate_insert(&data, key, &next_value)?;
234        data.insert(key.to_string(), next_value);
235        self.version.fetch_add(1, Ordering::SeqCst);
236        Ok(())
237    }
238
239    /// Set a raw JSON value without applying reducers.
240    pub async fn set_raw(&self, key: &str, value: Value) -> Result<()> {
241        let mut data = self.write_data().await?;
242        self.validate_insert(&data, key, &value)?;
243        data.insert(key.to_string(), value);
244        self.version.fetch_add(1, Ordering::SeqCst);
245        Ok(())
246    }
247
248    /// Apply a reducer to combine current and new values for a key.
249    /// If no reducer is registered, returns the new value (last-write-wins).
250    pub async fn apply_reducer(&self, key: &str, current: &Value, new: &Value) -> Result<Value> {
251        let reducers = self.read_reducers().await?;
252        if let Some(reducer) = reducers.get(key) {
253            reducer.reduce(current, new)
254        } else {
255            Ok(new.clone())
256        }
257    }
258
259    /// Update a value using a closure
260    pub async fn update<T, F>(&self, key: &str, f: F) -> Result<()>
261    where
262        T: serde::de::DeserializeOwned + Serialize,
263        F: FnOnce(T) -> T,
264    {
265        let mut data = self.write_data().await?;
266
267        if let Some(value) = data.get(key).cloned() {
268            let current: T = serde_json::from_value(value)?;
269            let updated = f(current);
270            let updated = self.serialize_value(key, updated)?;
271            self.validate_insert(&data, key, &updated)?;
272            data.insert(key.to_string(), updated);
273            self.version.fetch_add(1, Ordering::SeqCst);
274        }
275
276        Ok(())
277    }
278
279    /// Check if a key exists
280    pub async fn contains(&self, key: &str) -> bool {
281        match self.read_data().await {
282            Ok(data) => data.contains_key(key),
283            Err(_) => false,
284        }
285    }
286
287    /// Remove a key
288    pub async fn remove(&self, key: &str) -> Option<Value> {
289        match self.write_data().await {
290            Ok(mut data) => {
291                let removed = data.remove(key);
292                if removed.is_some() {
293                    self.version.fetch_add(1, Ordering::SeqCst);
294                }
295                removed
296            }
297            Err(_) => None,
298        }
299    }
300
301    /// Get all keys
302    pub async fn keys(&self) -> Vec<String> {
303        match self.read_data().await {
304            Ok(data) => data.keys().cloned().collect(),
305            Err(_) => Vec::new(),
306        }
307    }
308
309    /// Create a snapshot of current state
310    pub async fn snapshot(&self) -> StateSnapshot {
311        StateSnapshot {
312            timestamp: chrono::Utc::now(),
313            data: self
314                .read_data()
315                .await
316                .map(|data| data.clone())
317                .unwrap_or_default(),
318        }
319    }
320
321    /// Save current state to history
322    pub async fn save_to_history(&self) {
323        let snapshot = self.snapshot().await;
324        if let Ok(mut history) = self.write_history().await {
325            if history.len() >= self.limits.max_history_len && !history.is_empty() {
326                history.remove(0);
327            }
328            history.push(snapshot);
329        }
330    }
331
332    /// Restore state from snapshot
333    pub async fn restore(&self, snapshot: &StateSnapshot) {
334        self.replace_data(snapshot.data.clone()).await;
335    }
336
337    /// Get state history
338    pub async fn get_history(&self) -> Vec<StateSnapshot> {
339        self.read_history()
340            .await
341            .map(|history| history.clone())
342            .unwrap_or_default()
343    }
344
345    /// Export state as HashMap for serialization
346    pub async fn export(&self) -> HashMap<String, Value> {
347        self.read_data()
348            .await
349            .map(|data| data.clone())
350            .unwrap_or_default()
351    }
352
353    /// Begin a transaction. Captures a snapshot so changes can be rolled back.
354    pub async fn transaction(&self) -> StateTransaction {
355        let snapshot_version = self.version.load(Ordering::SeqCst);
356        StateTransaction {
357            state: self.clone(),
358            working: RwLock::new(self.export().await),
359            snapshot_version,
360            committed: false,
361        }
362    }
363
364    /// Create an independent deep copy of this state for parallel branches.
365    /// The forked state has its own data storage but shares the same reducers.
366    pub async fn fork(&self) -> AgentState {
367        let data = self.export().await;
368        AgentState {
369            data: Arc::new(RwLock::new(data)),
370            history: Arc::new(RwLock::new(Vec::new())),
371            reducers: self.reducers.clone(),
372            limits: self.limits.clone(),
373            version: Arc::new(AtomicU64::new(0)),
374        }
375    }
376
377    fn serialize_value<T: Serialize>(&self, key: &str, value: T) -> Result<Value> {
378        let json_value = serde_json::to_value(value)?;
379        self.validate_value_size(key, &json_value)?;
380        Ok(json_value)
381    }
382
383    fn validate_value_size(&self, key: &str, value: &Value) -> Result<()> {
384        let bytes = serde_json::to_vec(value)?.len();
385        if bytes > self.limits.max_value_bytes {
386            return Err(AgentGraphError::StateError(format!(
387                "Value for key '{}' exceeds max size: {} > {} bytes",
388                key, bytes, self.limits.max_value_bytes
389            )));
390        }
391        Ok(())
392    }
393
394    fn validate_insert(
395        &self,
396        data: &HashMap<String, Value>,
397        key: &str,
398        value: &Value,
399    ) -> Result<()> {
400        self.validate_value_size(key, value)?;
401        if !data.contains_key(key) && data.len() >= self.limits.max_keys {
402            return Err(AgentGraphError::StateError(format!(
403                "State key limit exceeded: {} >= {}",
404                data.len() + 1,
405                self.limits.max_keys
406            )));
407        }
408        Ok(())
409    }
410
411    async fn reduce_value_if_needed(
412        &self,
413        key: &str,
414        current: &Value,
415        new: &Value,
416    ) -> Result<Value> {
417        let reducers = self.read_reducers().await?;
418        if let Some(reducer) = reducers.get(key) {
419            reducer.reduce(current, new)
420        } else {
421            Ok(new.clone())
422        }
423    }
424
425    async fn replace_data(&self, next: HashMap<String, Value>) {
426        if let Err(error) = self.validate_state_map(&next) {
427            tracing::warn!(error = %error, "rejected state replacement that exceeded limits");
428            return;
429        }
430
431        if let Ok(mut data) = self.write_data().await {
432            *data = next;
433            self.version.fetch_add(1, Ordering::SeqCst);
434        }
435    }
436
437    fn validate_state_map(&self, data: &HashMap<String, Value>) -> Result<()> {
438        if data.len() > self.limits.max_keys {
439            return Err(AgentGraphError::StateError(format!(
440                "State key limit exceeded: {} > {}",
441                data.len(),
442                self.limits.max_keys
443            )));
444        }
445
446        for (key, value) in data {
447            self.validate_value_size(key, value)?;
448        }
449
450        Ok(())
451    }
452
453    async fn read_data(&self) -> Result<tokio::sync::RwLockReadGuard<'_, HashMap<String, Value>>> {
454        tokio::time::timeout(self.limits.lock_timeout, self.data.read())
455            .await
456            .map_err(|_| {
457                AgentGraphError::StateError(format!(
458                    "Timed out acquiring state read lock after {} ms",
459                    self.limits.lock_timeout.as_millis()
460                ))
461            })
462    }
463
464    async fn write_data(
465        &self,
466    ) -> Result<tokio::sync::RwLockWriteGuard<'_, HashMap<String, Value>>> {
467        tokio::time::timeout(self.limits.lock_timeout, self.data.write())
468            .await
469            .map_err(|_| {
470                AgentGraphError::StateError(format!(
471                    "Timed out acquiring state write lock after {} ms",
472                    self.limits.lock_timeout.as_millis()
473                ))
474            })
475    }
476
477    async fn read_history(&self) -> Result<tokio::sync::RwLockReadGuard<'_, Vec<StateSnapshot>>> {
478        tokio::time::timeout(self.limits.lock_timeout, self.history.read())
479            .await
480            .map_err(|_| {
481                AgentGraphError::StateError(format!(
482                    "Timed out acquiring state history read lock after {} ms",
483                    self.limits.lock_timeout.as_millis()
484                ))
485            })
486    }
487
488    async fn write_history(&self) -> Result<tokio::sync::RwLockWriteGuard<'_, Vec<StateSnapshot>>> {
489        tokio::time::timeout(self.limits.lock_timeout, self.history.write())
490            .await
491            .map_err(|_| {
492                AgentGraphError::StateError(format!(
493                    "Timed out acquiring state history write lock after {} ms",
494                    self.limits.lock_timeout.as_millis()
495                ))
496            })
497    }
498
499    async fn read_reducers(
500        &self,
501    ) -> Result<tokio::sync::RwLockReadGuard<'_, HashMap<String, Arc<dyn Reducer>>>> {
502        tokio::time::timeout(self.limits.lock_timeout, self.reducers.read())
503            .await
504            .map_err(|_| {
505                AgentGraphError::StateError(format!(
506                    "Timed out acquiring reducer read lock after {} ms",
507                    self.limits.lock_timeout.as_millis()
508                ))
509            })
510    }
511
512    async fn write_reducers(
513        &self,
514    ) -> Result<tokio::sync::RwLockWriteGuard<'_, HashMap<String, Arc<dyn Reducer>>>> {
515        tokio::time::timeout(self.limits.lock_timeout, self.reducers.write())
516            .await
517            .map_err(|_| {
518                AgentGraphError::StateError(format!(
519                    "Timed out acquiring reducer write lock after {} ms",
520                    self.limits.lock_timeout.as_millis()
521                ))
522            })
523    }
524}
525
526impl Default for AgentState {
527    fn default() -> Self {
528        Self::new()
529    }
530}
531
532impl std::fmt::Debug for AgentState {
533    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
534        f.debug_struct("AgentState")
535            .field("data", &"<locked>")
536            .finish()
537    }
538}