Spaces:
Running
Running
| # backend/tasks/executor.py | |
| import asyncio | |
| from dataclasses import dataclass, field | |
| from typing import Any | |
| from uuid import uuid4 | |
| class Task: | |
| id: str = field(default_factory=lambda: str(uuid4())) | |
| name: str = "" | |
| steps: list[dict] = field(default_factory=list) | |
| current_step: int = 0 | |
| status: str = "pending" # pending | running | paused | completed | failed | cancelled | |
| result: Any = None | |
| error: str = "" | |
| permission_tier: str = "approval_required" # pre_approved | approval_required | explicit_each_time | biometric | |
| class TaskExecutor: | |
| def __init__(self, app_handle, permission_gateway): | |
| self.tasks: dict[str, Task] = {} | |
| self.app_handle = app_handle | |
| self.permission_gateway = permission_gateway | |
| # Load db path | |
| import sys, os | |
| if getattr(sys, 'frozen', False): | |
| self.db_path = os.path.join(os.environ.get('APPDATA', os.path.expanduser('~')), 'JARVIS_OS', 'memory.db') | |
| else: | |
| self.db_path = os.path.join(os.getcwd(), 'memory.db') | |
| self._recover_tasks() | |
| def _recover_tasks(self): | |
| import sqlite3 | |
| try: | |
| with sqlite3.connect(self.db_path) as conn: | |
| conn.execute('PRAGMA journal_mode=WAL') | |
| cursor = conn.cursor() | |
| cursor.execute("SELECT id, name, status, steps, current_step, result, error, permission_tier FROM tasks") | |
| for row in cursor.fetchall(): | |
| status = row[2] | |
| # Safety: Fail any tasks that were running/paused during a hard crash | |
| if status in ["running", "paused"]: | |
| status = "failed" | |
| cursor.execute("UPDATE tasks SET status=?, error=? WHERE id=?", | |
| ("failed", "System rebooted mid-execution", row[0])) | |
| conn.commit() | |
| t = Task( | |
| id=row[0], name=row[1], status=status, | |
| steps=[], # Callables cannot be easily serialized to JSON, so we leave steps empty on recovery | |
| current_step=row[4], result=row[5], error=row[6], permission_tier=row[7] | |
| ) | |
| self.tasks[t.id] = t | |
| except Exception as e: | |
| import logging; logging.getLogger(__name__).error(f"Swallowed exception: {e}") # Tables might not exist yet | |
| def _save_task(self, task: Task): | |
| import sqlite3, json | |
| try: | |
| with sqlite3.connect(self.db_path) as conn: | |
| conn.execute('PRAGMA journal_mode=WAL') | |
| cursor = conn.cursor() | |
| # We do not serialize the actual 'handler' since it's a function reference. | |
| # We just serialize a dummy representation of steps for UI if needed. | |
| steps_json = json.dumps([{"name": getattr(s.get('handler'), '__name__', 'step')} for s in task.steps]) | |
| cursor.execute(''' | |
| INSERT INTO tasks (id, name, status, steps, current_step, result, error, permission_tier) | |
| VALUES (?, ?, ?, ?, ?, ?, ?, ?) | |
| ON CONFLICT(id) DO UPDATE SET | |
| status=excluded.status, | |
| current_step=excluded.current_step, | |
| result=excluded.result, | |
| error=excluded.error | |
| ''', (task.id, task.name, task.status, steps_json, task.current_step, str(task.result) if task.result else "", task.error, task.permission_tier)) | |
| conn.commit() | |
| except Exception as e: | |
| import logging; logging.getLogger(__name__).error(f"Swallowed exception: {e}") | |
| async def submit(self, task: Task) -> str: | |
| self.tasks[task.id] = task | |
| self._save_task(task) | |
| asyncio.create_task(self._run(task)) | |
| return task.id | |
| async def _run(self, task: Task): | |
| task.status = "running" | |
| self._save_task(task) | |
| self._emit("task:updated", task) | |
| for i, step in enumerate(task.steps): | |
| task.current_step = i | |
| self._save_task(task) | |
| self._emit("task:step_started", {"task_id": task.id, "step": step}) | |
| allowed = await self.permission_gateway.check(task.permission_tier, step) | |
| if not allowed: | |
| task.status = "paused" | |
| self._save_task(task) | |
| self._emit("task:awaiting_permission", {"task_id": task.id, "step": step}) | |
| allowed = await self.permission_gateway.wait_for_user_response(task.id) | |
| if not allowed: | |
| task.status = "cancelled" | |
| self._save_task(task) | |
| self._emit("task:updated", task) | |
| return | |
| try: | |
| result = await step["handler"](**step["params"]) | |
| task.result = result | |
| self._save_task(task) | |
| self._emit("task:step_completed", {"task_id": task.id, "step": step, "result": result}) | |
| except Exception as e: | |
| task.status = "failed" | |
| task.error = str(e) | |
| self._save_task(task) | |
| self._emit("task:updated", task) | |
| return | |
| task.status = "completed" | |
| self._save_task(task) | |
| self._emit("task:updated", task) | |
| def cancel(self, task_id: str): | |
| task = self.tasks.get(task_id) | |
| if task: | |
| task.status = "cancelled" | |
| self._save_task(task) | |
| self._emit("task:updated", task) | |
| def _emit(self, event: str, payload): | |
| try: | |
| from backend.ws.agent_ws import ws_manager | |
| import json | |
| from dataclasses import asdict | |
| # Serialize payload to dict safely | |
| if hasattr(payload, '__dataclass_fields__'): | |
| safe_payload = asdict(payload) | |
| else: | |
| # Fallback via json roundtrip to handle non-serializable objects cleanly | |
| safe_payload = json.loads(json.dumps(payload, default=str)) | |
| asyncio.create_task(ws_manager.broadcast({"event": event, "payload": safe_payload})) | |
| except Exception as e: | |
| import logging; logging.getLogger(__name__).error(f"Swallowed exception: {e}") | |