""" Todo Manager - Task tracking system for Vi's iterative reasoning. Provides a TodoWrite-equivalent tool for Oracle to manage complex multi-step problem-solving with subtask tracking and dependency management. Storage: NATS KV bucket (ephemeral, scoped by interaction_id) """ import json import uuid from typing import List, Dict, Optional from datetime import datetime from core.logger import setup_logger from core.nats_event_bus import nats_bus class TodoItem: """Individual todo item with state tracking""" def __init__( self, content: str, active_form: str, status: str = "pending", todo_id: str = None, created_at: str = None ): self.todo_id = todo_id or str(uuid.uuid4())[:8] self.content = content self.active_form = active_form self.status = status # pending, in_progress, completed self.created_at = created_at or datetime.utcnow().isoformat() self.updated_at = datetime.utcnow().isoformat() def to_dict(self) -> dict: """Convert to dictionary for serialization""" return { "todo_id": self.todo_id, "content": self.content, "active_form": self.active_form, "status": self.status, "created_at": self.created_at, "updated_at": self.updated_at } @classmethod def from_dict(cls, data: dict) -> 'TodoItem': """Create from dictionary""" return cls( content=data["content"], active_form=data["active_form"], status=data["status"], todo_id=data["todo_id"], created_at=data.get("created_at") ) class TodoManager: """ Manages ephemeral todo lists scoped by interaction/conversation. Uses NATS KV for storage with auto-expiry. Similar to Claude's TodoWrite tool, helps Oracle track complex multi-step reasoning processes. """ BUCKET_NAME = "lyra_todos" TTL_SECONDS = 3600 # 1 hour def __init__(self): self.logger = setup_logger('todo_manager', service_name='think_service') def _get_key(self, interaction_id: str) -> str: """Generate NATS KV key for interaction""" return f"{interaction_id}:todos" async def create( self, interaction_id: str, content: str, active_form: str, status: str = "pending" ) -> TodoItem: """ Create a new todo item Args: interaction_id: Unique interaction/conversation ID content: Task description (imperative form: "Fix bug in X") active_form: Present continuous form ("Fixing bug in X") status: Initial status (default: "pending") Returns: TodoItem: Created todo item """ # Get existing todos todos = await self.list(interaction_id) # Create new todo new_todo = TodoItem(content=content, active_form=active_form, status=status) todos.append(new_todo) # Save to NATS KV await self._save_todos(interaction_id, todos) self.logger.info(f"[✓] Created todo {new_todo.todo_id}: {content}") return new_todo async def update( self, interaction_id: str, todo_id: str, status: Optional[str] = None, content: Optional[str] = None, active_form: Optional[str] = None ) -> bool: """ Update an existing todo item Args: interaction_id: Unique interaction/conversation ID todo_id: ID of todo to update status: New status (optional) content: New content (optional) active_form: New active form (optional) Returns: bool: True if updated, False if not found """ todos = await self.list(interaction_id) for todo in todos: if todo.todo_id == todo_id: if status: todo.status = status if content: todo.content = content if active_form: todo.active_form = active_form todo.updated_at = datetime.utcnow().isoformat() await self._save_todos(interaction_id, todos) self.logger.info(f"[✏️] Updated todo {todo_id}: status={status}") return True self.logger.warning(f"[⚠️] Todo {todo_id} not found") return False async def complete(self, interaction_id: str, todo_id: str) -> bool: """ Mark a todo as completed Args: interaction_id: Unique interaction/conversation ID todo_id: ID of todo to complete Returns: bool: True if completed, False if not found """ result = await self.update(interaction_id, todo_id, status="completed") if result: self.logger.info(f"[✓] Completed todo {todo_id}") return result async def list(self, interaction_id: str, status_filter: Optional[str] = None) -> List[TodoItem]: """ List all todos for an interaction Args: interaction_id: Unique interaction/conversation ID status_filter: Optional filter by status (pending, in_progress, completed) Returns: List[TodoItem]: List of todo items """ key = self._get_key(interaction_id) try: data_bytes = await nats_bus.kv_get(self.BUCKET_NAME, key) if not data_bytes: return [] data = json.loads(data_bytes.decode()) todos = [TodoItem.from_dict(item) for item in data] # Apply filter if provided if status_filter: todos = [t for t in todos if t.status == status_filter] return todos except Exception as e: self.logger.error(f"[❌] Error listing todos: {e}") return [] async def delete(self, interaction_id: str, todo_id: str) -> bool: """ Delete a todo item Args: interaction_id: Unique interaction/conversation ID todo_id: ID of todo to delete Returns: bool: True if deleted, False if not found """ todos = await self.list(interaction_id) original_count = len(todos) todos = [t for t in todos if t.todo_id != todo_id] if len(todos) < original_count: await self._save_todos(interaction_id, todos) self.logger.info(f"[🗑️] Deleted todo {todo_id}") return True self.logger.warning(f"[⚠️] Todo {todo_id} not found for deletion") return False async def clear(self, interaction_id: str) -> bool: """ Clear all todos for an interaction Args: interaction_id: Unique interaction/conversation ID Returns: bool: True if cleared """ key = self._get_key(interaction_id) try: await nats_bus.kv_delete(self.BUCKET_NAME, key) self.logger.info(f"[🧹] Cleared todos for interaction {interaction_id}") return True except Exception as e: self.logger.error(f"[❌] Error clearing todos: {e}") return False async def get_summary(self, interaction_id: str) -> Dict: """ Get summary statistics for todos Args: interaction_id: Unique interaction/conversation ID Returns: Dict: Summary with counts by status """ todos = await self.list(interaction_id) summary = { "total": len(todos), "pending": len([t for t in todos if t.status == "pending"]), "in_progress": len([t for t in todos if t.status == "in_progress"]), "completed": len([t for t in todos if t.status == "completed"]) } return summary async def _save_todos(self, interaction_id: str, todos: List[TodoItem]): """Save todos to NATS KV""" key = self._get_key(interaction_id) data = [todo.to_dict() for todo in todos] data_bytes = json.dumps(data).encode() try: await nats_bus.kv_put( self.BUCKET_NAME, key, data_bytes, ttl_seconds=self.TTL_SECONDS ) except Exception as e: self.logger.error(f"[❌] Error saving todos: {e}") raise