"""Translate OPC engine callbacks into board-friendly runtime updates.""" from __future__ import annotations import re from typing import Any, Awaitable, Callable from opc.presentation.kanban import STATUS_TO_COLUMN BoardEventSink = Callable[[dict[str, Any]], Awaitable[None]] _TOOL_RE = re.compile(r"^\[Tool:\s*([^\]]+)\]") class CliBoardEventBridge: """Bridges in-process engine events to the board state layer.""" def __init__(self, sink: BoardEventSink) -> None: self._sink = sink async def handle_event(self, event: Any) -> None: event_type = str(getattr(event, "event_type", "") or "") payload = getattr(event, "payload", {}) or {} if event_type == "task_status_changed": task_id = str(payload.get("task_id", "") or "") status = str(payload.get("status", "") or "") if task_id and status: await self._sink( { "kind": "task_status", "task_id": task_id, "status": status, "column_id": STATUS_TO_COLUMN.get(status, "todo"), } ) if status in {"awaiting_review", "awaiting_peer", "blocked", "failed", "cancelled", "done"}: await self._sink({"kind": "refresh", "reason": f"status:{status}"}) return if event_type in {"task_created", "child_session_created", "escalation_created"}: await self._sink({"kind": "refresh", "reason": event_type}) return if event_type == "agent_status_changed": task_id = str(payload.get("task_id", "") or "") status = str(payload.get("status", "") or "") if task_id and status: await self._sink( { "kind": "runtime", "task_id": task_id, "status": status, "current_tool": None, "iteration": None, } ) return if event_type == "runtime_event": task_id = str(payload.get("task_id", "") or "") runtime_type = str(payload.get("type", "") or "").strip() if not task_id or not runtime_type: return if runtime_type == "status_snapshot": await self._sink( { "kind": "runtime", "task_id": task_id, "status": "tool_active" if payload.get("current_tool") else "reflecting", "current_tool": payload.get("current_tool"), "iteration": payload.get("iteration"), "tool_elapsed_ms": payload.get("tool_elapsed_ms"), "last_tool_summary": payload.get("last_tool_summary"), "context_tokens": payload.get("context_tokens"), "context_window": payload.get("context_window"), "context_remaining_pct": payload.get("context_remaining_pct"), "turn_cost_usd": payload.get("turn_cost_usd"), "session_cost_usd": payload.get("session_cost_usd"), "pending_permission_count": payload.get("pending_permission_count"), "drain_mode": payload.get("drain_mode"), } ) return if runtime_type in {"tool_started", "tool_progress", "tool_completed", "tool_skipped", "permission_requested", "permission_resolved"}: await self._sink( { "kind": "runtime", "task_id": task_id, "status": "tool_active" if runtime_type in {"tool_started", "tool_progress"} else "reflecting", "current_tool": payload.get("tool_name"), "iteration": payload.get("iteration"), "tool_elapsed_ms": payload.get("elapsed_ms"), "last_tool_summary": payload.get("result_summary") or payload.get("message"), } ) return if event_type == "agent_log": task_id = str(payload.get("task_id", "") or "") if not task_id: return status = str(payload.get("status", "") or "") iteration = payload.get("iteration") current_tool = None runtime_status = status if status == "thinking": runtime_status = "reflecting" current_tool = "Reflect" elif status == "executing": current_tool = str(payload.get("tool", "") or "") or None runtime_status = "tool_active" await self._sink( { "kind": "runtime", "task_id": task_id, "status": runtime_status, "current_tool": current_tool, "iteration": int(iteration) if isinstance(iteration, int) else None, } ) async def handle_progress(self, text: str, *, task_id: str | None = None, **_: Any) -> None: if not task_id: return current_tool = None match = _TOOL_RE.match(str(text or "").strip()) if match: current_tool = match.group(1).strip() or None await self._sink( { "kind": "progress", "task_id": task_id, "text": str(text or ""), "current_tool": current_tool, } )