Skip to main content

cosh_tools/plan/
mod.rs

1pub mod todo_write;
2pub mod types;
3
4#[cfg(test)]
5mod test;
6
7pub use todo_write::todo_write;
8pub use types::{
9    Nag, PlanError, TodoItem, TodoItemInput, TodoList, TodoStatus, TodoWriteInput, TodoWriteOutput,
10};
11
12use crate::ToolDescription;
13
14/// Plan instructs the model on how to write plans and provides a
15/// stateful wrapper over the pure todo operation.
16pub struct Plan {
17    list: TodoList,
18
19    /// MCP Tool description for `todo_write`.
20    pub description_todo_write: ToolDescription,
21}
22
23impl Default for Plan {
24    fn default() -> Self {
25        Self::new()
26    }
27}
28
29impl Plan {
30    /// Creates an empty plan.
31    #[must_use]
32    pub fn new() -> Self {
33        Self {
34            list: TodoList::default(),
35            description_todo_write: serde_json::json!({
36                "name": "plan_todo_write",
37                "description": concat!(
38                    "Write the full TODO list. This replaces the previous list ",
39                    "entirely: send ALL tasks on every call, including unchanged ",
40                    "ones. To mark a task in_progress, completed, or cancelled, ",
41                    "rewrite the whole list with the updated status. Exactly one ",
42                    "task may be in_progress at a time. Ids (task-1..N) are ",
43                    "assigned in listed order; use the optional per-item `key` so ",
44                    "sibling tasks can reference each other in `depends_on` within ",
45                    "the same call. Call with an empty list to clear the plan.\n",
46                    "Example:\n",
47                    "{\"todos\": [{\"description\": \"install deps\", \"key\": \"deps\"}, ",
48                    "{\"description\": \"write tests\", \"status\": \"pending\", ",
49                    "\"depends_on\": [\"deps\"]}]}"
50                ),
51                "inputSchema": {
52                    "type": "object",
53                    "properties": {
54                        "todos": {
55                            "type": "array",
56                            "description": "The complete task list, replacing the current TODO state. Ids are assigned as task-1..N in listed order.",
57                            "items": {
58                                "type": "object",
59                                "properties": {
60                                    "key": { "type": "string", "description": "Optional alias other tasks in this same call reference in depends_on; resolved to the real task-N id" },
61                                    "description": { "type": "string", "description": "Task description" },
62                                    "status": { "type": "string", "enum": ["pending", "in_progress", "completed", "cancelled"], "description": "Defaults to pending when omitted. Exactly one task may be in_progress." },
63                                    "depends_on": { "type": "array", "items": { "type": "string" }, "description": "Sibling keys or task ids this task depends on" }
64                                },
65                                "required": ["description"]
66                            }
67                        }
68                    },
69                    "required": ["todos"]
70                }
71            }),
72        }
73    }
74
75    /// Returns a reference to the internal todo list.
76    #[must_use]
77    pub const fn list(&self) -> &TodoList {
78        &self.list
79    }
80
81    /// Restore the structured projection reconstructed from session history.
82    pub fn restore_list(&mut self, list: TodoList) {
83        self.list = list;
84    }
85
86    /// Replace the whole todo list with the submitted one.
87    ///
88    /// # Errors
89    ///
90    /// Returns `Err` if the underlying `todo_write` operation fails.
91    pub fn todo_write(&mut self, todos: &[TodoItemInput]) -> Result<TodoWriteOutput, PlanError> {
92        let output = todo_write(todos)?;
93        self.list = output.list.clone();
94        Ok(output)
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::Plan;
101
102    #[test]
103    fn new_plan_has_an_empty_list() {
104        let plan = Plan::new();
105        assert!(plan.list().items.is_empty());
106    }
107}