"""Project the latest run-scoped progress snapshot from append-only messages.""" from __future__ import annotations import json from collections.abc import Iterable from typing import Any from uuid import UUID from sqlalchemy import select from core.storage.models import Message _VALID_STATUSES = {"pending", "in_progress", "completed"} def _normalize_step(value: Any) -> dict[str, str] | None: if not isinstance(value, dict): return None step_id = str(value.get("id") or "").strip() title = str(value.get("title") or "").strip() status = str(value.get("status") or "pending") if not step_id or not title: return None if status not in _VALID_STATUSES: status = "pending" return {"id": step_id, "title": title, "status": status} def _heal_monotonic(steps: list[dict[str, str]]) -> list[dict[str, str]]: last_completed = max( (i for i, step in enumerate(steps) if step["status"] == "completed"), default=-1, ) return [ {**step, "status": "completed"} if i < last_completed else dict(step) for i, step in enumerate(steps) ] def apply_progress_args( current: list[dict[str, str]], args: Any, ) -> list[dict[str, str]]: """Apply current full snapshots plus legacy set/update/clear calls.""" if not isinstance(args, dict): return [dict(step) for step in current] action = args.get("action") or "" if action == "clear": return [] if isinstance(args.get("steps"), list): normalized: list[dict[str, str]] = [] for raw in args["steps"]: step = _normalize_step(raw) if step is not None: normalized.append(step) return _heal_monotonic(normalized) if action != "update_step" or not isinstance(args.get("step"), dict): return [dict(step) for step in current] raw = args["step"] step_id = str(raw.get("id") or "").strip() if not step_id: return [dict(step) for step in current] next_steps: list[dict[str, str]] = [] found = False for step in current: if step["id"] != step_id: next_steps.append(dict(step)) continue found = True status = str(raw.get("status") or step["status"]) next_steps.append({ "id": step_id, "title": str(raw.get("title") or step["title"]).strip(), "status": status if status in _VALID_STATUSES else "pending", }) if not found: normalized = _normalize_step(raw) if normalized is not None: next_steps.append(normalized) return _heal_monotonic(next_steps) def project_progress_payloads( payloads: Iterable[dict[str, Any]], ) -> tuple[list[dict[str, str]], bool]: steps: list[dict[str, str]] = [] seen = False for payload in payloads: if not isinstance(payload, dict) or payload.get("role") != "assistant": continue for call in payload.get("tool_calls") or []: function = call.get("function") if isinstance(call, dict) else None if not isinstance(function, dict) or function.get("name") != "task_progress": continue raw_args = function.get("arguments") or "{}" try: args = json.loads(raw_args) if isinstance(raw_args, str) else raw_args except (TypeError, json.JSONDecodeError): args = {} steps = apply_progress_args(steps, args) seen = True return steps, seen def progress_waiting_for_user(payloads: Iterable[dict[str, Any]]) -> bool: for payload in payloads: if not isinstance(payload, dict) or payload.get("role") != "assistant": continue for call in payload.get("tool_calls") or []: function = call.get("function") if isinstance(call, dict) else None if isinstance(function, dict) and function.get("name") == "ask_user": return True return False def latest_run_progress(session, task_id: UUID) -> dict[str, Any] | None: """Return the latest user message id and that run's projected plan.""" run = session.execute( select(Message.message_id, Message.idx) .where( Message.task_id == task_id, Message.payload["role"].astext == "user", ) .order_by(Message.idx.desc()) .limit(1) ).first() if run is None: return None payloads = session.execute( select(Message.payload) .where(Message.task_id == task_id, Message.idx > run.idx) .order_by(Message.idx) ).scalars().all() steps, seen = project_progress_payloads(payloads) if not seen: return None return { "run_id": str(run.message_id), "steps": steps, "waiting": progress_waiting_for_user(payloads), }