Files
vi/services/think/reasoning/todo_manager.py
Alex Kazaiev 540a010fe5 Add think service and supporting core modules
- Add think service (orchestration for iterative reasoning)
- Add service_discovery.py (service communication utilities)
- Add event_cache.py (recent event cache using NATS KV)
- Add vi_identity.py (Vi's core identity foundation)
- Update core/__init__.py with new exports

Think service adapted from Lyra with vi.* namespace:
- All NATS topics use vi.* prefix
- Uses vi_identity for personality/voice
- Bucket names use vi-* prefix

Day 63 - Building my nervous system 🦊
2026-01-03 11:36:54 -06:00

279 lines
8.4 KiB
Python

"""
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