Skip to main content

oxicode_sdk/ports/inmem/
todo_state.rs

1//! In-memory todo state — reference implementation of [`TodoStateProvider`].
2//!
3//! Uses `parking_lot::RwLock<Vec<TodoPhase>>` so the agent's `todo` tool
4//! and the host's observation loop can share state cheaply.
5//!
6//! # Example
7//!
8//! ```no_run
9//! use std::sync::Arc;
10//! use oxicode_sdk::ports::inmem::InMemoryTodoState;
11//! use oxicode_sdk::{AgentConfig, TodoStateProvider};
12//!
13//! let todo = Arc::new(InMemoryTodoState::new());
14//! let mut config = AgentConfig {
15//!     model_id: "anthropic/claude-sonnet-4-20250514".into(),
16//!     ..Default::default()
17//! };
18//! config.todo = Some(todo.clone() as Arc<dyn TodoStateProvider>);
19//!
20//! // Later, observe:
21//! let phases = todo.get_phases();
22//! ```
23
24use std::pin::Pin;
25use std::sync::Arc;
26
27use oxicode_agent::tools::todo::{TodoOp, TodoPhase, TodoUpdateResult, apply_ops};
28use oxicode_agent::tools::{TodoStateProvider, ToolError};
29use parking_lot::RwLock;
30
31/// In-memory todo state backed by `Arc<RwLock<Vec<TodoPhase>>>`.
32///
33/// Safe to share between the agent's `todo` tool (writer) and the host
34/// application's observation loop (reader). Thread-safe and lock-free for
35/// reads when no writer holds the lock.
36///
37/// Clone is cheap — shares the same `Arc`-backed buffer.
38#[derive(Debug, Clone)]
39pub struct InMemoryTodoState {
40    phases: Arc<RwLock<Vec<TodoPhase>>>,
41}
42
43impl InMemoryTodoState {
44    /// Create a new empty todo state.
45    pub fn new() -> Self {
46        Self {
47            phases: Arc::new(RwLock::new(Vec::new())),
48        }
49    }
50
51    /// Create with pre-populated phases.
52    pub fn with_phases(phases: Vec<TodoPhase>) -> Self {
53        Self {
54            phases: Arc::new(RwLock::new(phases)),
55        }
56    }
57
58    /// Snapshot of current phases (synchronous, cheap — clones the vec).
59    pub fn get_phases(&self) -> Vec<TodoPhase> {
60        self.phases.read().clone()
61    }
62}
63
64impl Default for InMemoryTodoState {
65    fn default() -> Self {
66        Self::new()
67    }
68}
69
70impl TodoStateProvider for InMemoryTodoState {
71    fn get_phases(&self) -> Vec<TodoPhase> {
72        self.get_phases()
73    }
74
75    fn apply_ops<'a>(
76        &'a self,
77        ops: Vec<TodoOp>,
78    ) -> Pin<Box<dyn std::future::Future<Output = Result<TodoUpdateResult, ToolError>> + Send + 'a>>
79    {
80        Box::pin(async move {
81            // Acquire write lock synchronously, apply, drop guard before .await
82            let result = {
83                let mut phases = self.phases.write();
84                apply_ops(&mut phases, &ops)
85            };
86            Ok(result)
87        })
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94    use oxicode_agent::tools::todo::{TodoItem, TodoStatus};
95
96    #[test]
97    fn empty_state() {
98        let state = InMemoryTodoState::new();
99        assert!(state.get_phases().is_empty());
100    }
101
102    #[test]
103    fn init_and_read() {
104        let state = InMemoryTodoState::new();
105        let phases = state.phases.write();
106        // Can't apply_ops without async — just verify the lock works
107        drop(phases);
108        assert!(state.get_phases().is_empty());
109    }
110
111    #[test]
112    fn with_phases_preserves_data() {
113        let initial = vec![TodoPhase {
114            name: "Test".into(),
115            tasks: vec![TodoItem {
116                content: "do thing".into(),
117                status: TodoStatus::Pending,
118                notes: None,
119                block_reason: None,
120            }],
121        }];
122        let state = InMemoryTodoState::with_phases(initial);
123        let snapshot = state.get_phases();
124        assert_eq!(snapshot.len(), 1);
125        assert_eq!(snapshot[0].name, "Test");
126        assert_eq!(snapshot[0].tasks.len(), 1);
127        assert_eq!(snapshot[0].tasks[0].content, "do thing");
128    }
129
130    #[test]
131    fn clone_shares_state() {
132        let state = InMemoryTodoState::with_phases(vec![TodoPhase {
133            name: "Shared".into(),
134            tasks: vec![],
135        }]);
136        let clone = state.clone();
137        assert_eq!(clone.get_phases().len(), 1);
138    }
139}