"""Tool: block_resume – Recovery-Übersicht am Chat-Start (DB-backed).""" from __future__ import annotations import json import os from typing import Any, Dict, List, Optional from helpers.tool import Tool, Response from usr.plugins.a0_software_orchestrator.helpers.compact_protocol import get_resume_payload from usr.plugins.a0_software_orchestrator.helpers.db_state_store import ( get_project_id, get_status, list_next_steps, list_todos, list_errors, list_worklog, get_kv, get_block_compact, ) from usr.plugins.a0_software_orchestrator.helpers.exceptions import PluginDBError def _resolve_name(project_name: str = "", project_root: str = "") -> str: if project_name: return project_name.strip() if project_root: return os.path.basename(project_root.rstrip("/")) return os.path.basename(os.getcwd()) def _build_status_line(artifacts_state: Dict[str, bool]) -> str: parts: List[str] = [] for key, ok in artifacts_state.items(): marker = "✅" if ok else "❌" parts.append(f"{key}={marker}") return ", ".join(parts) if parts else "(none)" class BlockResume(Tool): """Lädt die kompakte 10-Zeilen-Resume-Übersicht aus der DB. Args (kwargs): project_name (str): Projektname (bevorzugt). project_root (str): Legacy – wird zu basename() aufgelöst. include_full_resume (bool): Wenn True, zusätzlich resume_md ausgeben. include_conversation_summary (bool): Wenn True, zusätzlich conversation_summary_md. """ async def execute( self, project_name: str = "", project_root: str = "", include_full_resume: bool = False, include_conversation_summary: bool = False, **kwargs, ): name = _resolve_name(project_name, project_root) try: pid = get_project_id(name) if pid is None: raise LookupError(f"project '{name}' is not registered") except (ValueError, LookupError) as e: return Response(message=f"ERROR: {e}", break_loop=False) except PluginDBError as e: return Response(message=f"Database error: {e}", break_loop=False) bc = get_block_compact(pid) status = get_status(pid) pending_steps = list_next_steps(pid, status="pending") open_todos = list_todos(pid, status="open") open_errors = list_errors(pid, status="open") recent_worklog = list_worklog(pid, limit=5) phase = get_kv(pid, "phase", default="") # 10-Zeilen-Übersicht lines: List[str] = [f"🔁 RESUME: {name} (pid={pid}) — phase={phase}"] if bc: lines.append( f" last block: {bc.get('last_block_id')} @ {bc.get('last_compact_at')}" ) lines.append(f" next block: {bc.get('next_block') or '(none)'}") else: lines.append(" no block_compact marker yet") lines.append( f" status: {status or '(none)'}" ) lines.append( f" pending next_steps: {len(pending_steps)}, " f"open todos: {len(open_todos)}, open errors: {len(open_errors)}" ) if bc: decisions = bc.get("decisions") or [] key_findings = bc.get("key_findings") or [] open_questions = bc.get("open_questions") or [] lines.append( f" last block decisions: {len(decisions)}, " f"key_findings: {len(key_findings)}, " f"open_questions: {len(open_questions)}" ) # Optional: full resume + conversation summary if include_full_resume and bc and bc.get("resume_md"): lines.append("") lines.append("=== Full resume.md ===") lines.append(bc["resume_md"]) if include_conversation_summary and bc and bc.get("conversation_summary_md"): lines.append("") lines.append("=== Conversation summary ===") lines.append(bc["conversation_summary_md"]) # Recent worklog (top 5) if recent_worklog: lines.append("") lines.append("=== Recent worklog (top 5) ===") for w in recent_worklog: ts = w.get("created_at", "?") blk = w.get("work_block") or "-" agent = w.get("agent") or "-" summary = (w.get("summary") or "")[:100] lines.append(f" - [{ts}] {blk} ({agent}): {summary}") # Hinweise für den Orchestrator lines.append("") lines.append("Hints:") lines.append(" - Don't re-run subagents for already-done blocks (see worklog).") lines.append(" - If pending_steps is empty, ask the user before continuing.") lines.append(" - If open_errors > 0, address them first.") return Response(message="\n".join(lines), break_loop=False)