"""Lightweight task tracking for multi-step agent execution.Provides simple todo/task tracking that integrates with the agent loopto show progress during long multi-step operations."""from __future__ import annotationsimport jsonimport timefrom dataclasses import dataclass, fieldfrom enum import Enumfrom minicode.config import MINI_CODE_DIRclass TaskStatus(str, Enum):"""Task status enum."""PENDING = "pending"IN_PROGRESS = "in_progress"COMPLETED = "completed"CANCELLED = "cancelled"FAILED = "failed"@dataclassclass Task:"""A single task item."""id: strdescription: strstatus: TaskStatus = TaskStatus.PENDINGcreated_at: float = field(default_factory=time.time)updated_at: float = field(default_factory=time.time)completed_at: float | None = Noneerror: str | None = Nonedef complete(self) -> None:self.status = TaskStatus.COMPLETEDself.completed_at = time.time()self.updated_at = time.time()def fail(self, error: str) -> None:self.status = TaskStatus.FAILEDself.error = errorself.updated_at = time.time()def cancel(self) -> None:self.status = TaskStatus.CANCELLEDself.updated_at = time.time()def start(self) -> None:self.status = TaskStatus.IN_PROGRESSself.updated_at = time.time()@dataclassclass TaskList:"""A list of tasks for tracking multi-step progress."""title: str = ""tasks: list[Task] = field(default_factory=list)created_at: float = field(default_factory=time.time)updated_at: float = field(default_factory=time.time)@propertydef total(self) -> int:return len(self.tasks)@propertydef completed_count(self) -> int:return sum(1 for task in self.tasks if task.status == TaskStatus.COMPLETED)@propertydef pending_count(self) -> int:return sum(1 for task in self.tasks if task.status == TaskStatus.PENDING)@propertydef in_progress_count(self) -> int:return sum(1 for task in self.tasks if task.status == TaskStatus.IN_PROGRESS)@propertydef failed_count(self) -> int:return sum(1 for task in self.tasks if task.status == TaskStatus.FAILED)@propertydef progress_percentage(self) -> float:if self.total == 0:return 0.0return (self.completed_count / self.total) * 100@propertydef is_complete(self) -> bool:return all(task.status in (TaskStatus.COMPLETED, TaskStatus.CANCELLED, TaskStatus.FAILED)for task in self.tasks)def add_task(self, description: str) -> Task:task = Task(id=str(len(self.tasks) + 1), description=description)self.tasks.append(task)self.updated_at = time.time()return taskdef get_task(self, task_id: str) -> Task | None:for task in self.tasks:if task.id == task_id:return taskreturn Nonedef mark_completed(self, task_id: str) -> bool:task = self.get_task(task_id)if task:task.complete()self.updated_at = time.time()return Truereturn Falsedef mark_failed(self, task_id: str, error: str) -> bool:task = self.get_task(task_id)if task:task.fail(error)self.updated_at = time.time()return Truereturn Falsedef get_current_task(self) -> Task | None:for task in self.tasks:if task.status in (TaskStatus.PENDING, TaskStatus.IN_PROGRESS):return taskreturn Nonedef get_next_pending(self) -> Task | None:for task in self.tasks:if task.status == TaskStatus.PENDING:return taskreturn Noneclass TaskManager:"""Manages task lists for the current session."""def __init__(self):self.active_list: TaskList | None = Noneself.history: list[TaskList] = []def create_list(self, title: str) -> TaskList:if self.active_list:self.history.append(self.active_list)self.active_list = TaskList(title=title)return self.active_listdef add_task(self, description: str) -> Task | None:if not self.active_list:self.active_list = TaskList(title="Tasks")return self.active_list.add_task(description)def complete_task(self, task_id: str) -> bool:if not self.active_list:return Falsereturn self.active_list.mark_completed(task_id)def fail_task(self, task_id: str, error: str) -> bool:if not self.active_list:return Falsereturn self.active_list.mark_failed(task_id, error)def get_status(self) -> str:if not self.active_list:return "No active tasks"tl = self.active_liststatus_parts = []if tl.title:status_parts.append(f"📋 {tl.title}")status_parts.append(f"{tl.completed_count}/{tl.total} done ({tl.progress_percentage:.0f}%)")if tl.in_progress_count > 0:current = tl.get_current_task()if current:status_parts.append(f"→ {current.description[:50]}")if tl.failed_count > 0:status_parts.append(f"⚠ {tl.failed_count} failed")return " | ".join(status_parts)def format_details(self) -> str:if not self.active_list:return "No active task list."tl = self.active_listlines = [f"Task List: {tl.title or 'Untitled'}",f"Progress: {tl.completed_count}/{tl.total} completed ({tl.progress_percentage:.0f}%)","",]for task in tl.tasks:status_icon = {TaskStatus.PENDING: "○しろまる",TaskStatus.IN_PROGRESS: "◐",TaskStatus.COMPLETED: "●くろまる",TaskStatus.FAILED: "✗",TaskStatus.CANCELLED: "⊘",}.get(task.status, "?")lines.append(f" {status_icon} [{task.id}] {task.description}")if task.status == TaskStatus.FAILED and task.error:lines.append(f" Error: {task.error}")lines.append("")lines.append(f"Total: {tl.total} | Done: {tl.completed_count} | Pending: {tl.pending_count}")if tl.failed_count > 0:lines.append(f"Failed: {tl.failed_count}")return "\n".join(lines)def auto_detect_tasks(self, user_input: str) -> list[str] | None:import relines = user_input.strip().split("\n")numbered = []for line in lines:match = re.match(r"^\d+[\.\)]\s+(.+)", line.strip())if match:numbered.append(match.group(1))if len(numbered) >= 2:return numberedbullets = []for line in lines:match = re.match(r"^[-*•]\s+(.+)", line.strip())if match:bullets.append(match.group(1))if len(bullets) >= 2:return bulletsif "," in user_input and len(lines) == 1:steps = [s.strip() for s in user_input.split(",") if s.strip()]if len(steps) >= 3:sequential_words = ["then", "next", "after", "finally", "last"]has_sequence = any(word in user_input.lower() for word in sequential_words)if has_sequence:return stepsreturn Nonedef create_from_input(self, user_input: str, title: str = "") -> TaskList | None:tasks = self.auto_detect_tasks(user_input)if not tasks:return Nonetask_list = self.create_list(title or "Auto-detected tasks")for task_desc in tasks:task_list.add_task(task_desc)return task_listdef clear(self) -> None:if self.active_list:self.history.append(self.active_list)self.active_list = Nonedef format_task_update(task: Task, status: TaskStatus) -> str:icons = {TaskStatus.PENDING: "○しろまる",TaskStatus.IN_PROGRESS: "◐",TaskStatus.COMPLETED: "✓",TaskStatus.FAILED: "✗",TaskStatus.CANCELLED: "⊘",}icon = icons.get(status, "?")return f"{icon} Task {task.id}: {task.description}"def should_show_task_progress(task_list: TaskList | None) -> bool:if not task_list:return Falsereturn not task_list.is_complete and task_list.total > 1def format_task_progress_bar(task_list: TaskList, width: int = 30) -> str:if task_list.total == 0:return " " * widthfilled = int(width * task_list.progress_percentage / 100)empty = width - filledbar = "█" * filled + "░" * emptyreturn f"[{bar}] {task_list.progress_percentage:.0f}%"def save_task_list(task_list: TaskList, session_id: str) -> None:tasks_dir = MINI_CODE_DIR / "tasks"tasks_dir.mkdir(parents=True, exist_ok=True)task_file = tasks_dir / f"{session_id}.json"data = {"title": task_list.title,"created_at": task_list.created_at,"updated_at": task_list.updated_at,"tasks": [{"id": t.id,"description": t.description,"status": t.status.value,"created_at": t.created_at,"updated_at": t.updated_at,"completed_at": t.completed_at,"error": t.error,}for t in task_list.tasks],}task_file.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")def load_task_list(session_id: str) -> TaskList | None:task_file = MINI_CODE_DIR / "tasks" / f"{session_id}.json"if not task_file.exists():return Nonetry:data = json.loads(task_file.read_text(encoding="utf-8"))task_list = TaskList(title=data.get("title", ""),created_at=data.get("created_at", time.time()),updated_at=data.get("updated_at", time.time()),)for task_data in data.get("tasks", []):task = Task(id=task_data["id"],description=task_data["description"],status=TaskStatus(task_data.get("status", "pending")),created_at=task_data.get("created_at", time.time()),updated_at=task_data.get("updated_at", time.time()),completed_at=task_data.get("completed_at"),error=task_data.get("error"),)task_list.tasks.append(task)return task_listexcept (json.JSONDecodeError, KeyError):return None
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。