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 set_phases_sync(&self, phases: Vec<TodoPhase>) {
76        *self.phases.write() = phases;
77    }
78
79    fn apply_ops<'a>(
80        &'a self,
81        ops: Vec<TodoOp>,
82    ) -> Pin<Box<dyn std::future::Future<Output = Result<TodoUpdateResult, ToolError>> + Send + 'a>>
83    {
84        Box::pin(async move {
85            // Acquire write lock synchronously, apply, drop guard before .await
86            let result = {
87                let mut phases = self.phases.write();
88                apply_ops(&mut phases, &ops)
89            };
90            Ok(result)
91        })
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use oxicode_agent::tools::todo::{TodoItem, TodoStatus};
99
100    #[test]
101    fn empty_state() {
102        let state = InMemoryTodoState::new();
103        assert!(state.get_phases().is_empty());
104    }
105
106    #[test]
107    fn init_and_read() {
108        let state = InMemoryTodoState::new();
109        let phases = state.phases.write();
110        // Can't apply_ops without async — just verify the lock works
111        drop(phases);
112        assert!(state.get_phases().is_empty());
113    }
114
115    #[test]
116    fn with_phases_preserves_data() {
117        let initial = vec![TodoPhase {
118            name: "Test".into(),
119            tasks: vec![TodoItem {
120                content: "do thing".into(),
121                status: TodoStatus::Pending,
122                notes: None,
123                block_reason: None,
124            }],
125        }];
126        let state = InMemoryTodoState::with_phases(initial);
127        let snapshot = state.get_phases();
128        assert_eq!(snapshot.len(), 1);
129        assert_eq!(snapshot[0].name, "Test");
130        assert_eq!(snapshot[0].tasks.len(), 1);
131        assert_eq!(snapshot[0].tasks[0].content, "do thing");
132    }
133
134    #[test]
135    fn clone_shares_state() {
136        let state = InMemoryTodoState::with_phases(vec![TodoPhase {
137            name: "Shared".into(),
138            tasks: vec![],
139        }]);
140        let clone = state.clone();
141        assert_eq!(clone.get_phases().len(), 1);
142    }
143}