"""Runtime and global execution-mode service.""" from __future__ import annotations from typing import Any from opc.plugins.office_ui.snapshot_builder import build_collab_sync, build_snapshot from .context import OfficeServiceContext from .models import ServiceError, ServiceEvent, ServiceResult from .session import SessionService class RuntimeService: def __init__(self, context: OfficeServiceContext, session_service: SessionService) -> None: self.context = context self.session_service = session_service async def mode_show(self) -> ServiceResult: active_org = "" if self.context.mode_state.exec_mode == "org" and self.context.get_active_saved_org_name is not None: active_org = await self.context.get_active_saved_org_name() return ServiceResult({ "mode": self.context.mode_state.exec_mode, "profile": self.context.mode_state.company_profile, "org_id": active_org, "preferred_agent": self.context.mode_state.task_preferred_agent, }) async def status(self, *, project_id: str, limit: int = 50) -> ServiceResult: engine = await self.context.engine_for_project(project_id) store = getattr(engine, "store", None) payload: dict[str, Any] = { "project_id": project_id, "mode": self.context.mode_state.exec_mode, "profile": self.context.mode_state.company_profile, "preferred_agent": self.context.mode_state.task_preferred_agent, "active_tasks": [], "runtime_sessions": [], "external_sessions": [], "checkpoints": [], } if not self.context.store_is_ready(store): payload["available"] = False payload["reason"] = "store_not_ready" return ServiceResult(payload) from opc.core.models import TaskStatus terminal = {TaskStatus.DONE, TaskStatus.FAILED, TaskStatus.CANCELLED} tasks = await store.get_tasks(project_id=project_id) if hasattr(store, "get_tasks") else [] payload["active_tasks"] = [ { "task_id": getattr(task, "id", ""), "title": getattr(task, "title", ""), "status": getattr(getattr(task, "status", None), "value", getattr(task, "status", "")), "session_id": getattr(task, "session_id", ""), "assigned_to": getattr(task, "assigned_to", ""), } for task in tasks if getattr(task, "status", None) not in terminal ][:limit] if hasattr(store, "list_runtime_sessions"): payload["runtime_sessions"] = await store.list_runtime_sessions(project_id=project_id, limit=limit) if hasattr(store, "list_external_sessions"): payload["external_sessions"] = await store.list_external_sessions(project_id=project_id, limit=limit) if hasattr(store, "get_pending_checkpoints"): payload["checkpoints"] = await store.get_pending_checkpoints(project_id=project_id) payload["checkpoints"] = payload["checkpoints"][:limit] return ServiceResult(payload) async def mode_set( self, *, mode: str, profile: str = "corporate", preferred_agent: str | None = None, org_id: str | None = None, sync_config: bool = True, ) -> ServiceResult: new_mode = self.session_service.normalize_exec_mode(mode) normalized_org_id = self.session_service.normalize_org_id(org_id) if new_mode == "org": profile = "custom" if sync_config and normalized_org_id and self.context.load_active_org_config: if not self.context.load_active_org_config(normalized_org_id): raise ServiceError("org_not_found", "org_not_found", {"org_id": normalized_org_id}) if self.context.set_active_saved_org_name: await self.context.set_active_saved_org_name(normalized_org_id) else: normalized_org_id = "" profile = self.session_service.normalize_company_profile(profile) if new_mode == "company" and profile == "custom": profile = "corporate" agent = self.session_service.normalize_preferred_agent( preferred_agent if preferred_agent is not None else self.context.mode_state.task_preferred_agent, default=self.context.mode_state.task_preferred_agent, ) self.context.mode_state.exec_mode = new_mode self.context.mode_state.company_profile = profile self.context.mode_state.task_preferred_agent = agent if self.context.agent_store: await self.context.agent_store.set_server_state("exec_mode", new_mode) await self.context.agent_store.set_server_state("company_profile", profile) await self.context.agent_store.set_server_state("task_preferred_agent", agent) if getattr(self.context.engine, "org_engine", None) and self.context.agent_store: await self.context.agent_store.load_preset("custom" if new_mode == "org" else profile, self.context.engine.org_engine) snapshot = await build_snapshot( self.context.engine, self.context.agent_store, self.context.chat_store, self.context.event_adapter, ) snapshot["exec_mode"] = new_mode snapshot["company_profile"] = profile snapshot["task_preferred_agent"] = agent collab = await build_collab_sync( self.context.engine, self.context.agent_store, self.context.chat_store, self.context.event_adapter, exec_mode=new_mode, ) payload = {"mode": new_mode, "profile": profile, "org_id": normalized_org_id, "preferred_agent": agent} return ServiceResult(payload, [ServiceEvent("snapshot", snapshot), ServiceEvent("collab_sync_push", collab)]) async def run_task(self, *, project_id: str, task_id: str) -> ServiceResult: engine = await self.context.engine_for_project(project_id) task = await engine.store.get_task(task_id) if getattr(engine, "store", None) else None if not task: raise ServiceError("task_not_found", "task_not_found", {"task_id": task_id}) prompt = f"{getattr(task, 'title', '')}\n{getattr(task, 'description', '')}".strip() return await self.session_service.send( project_id=project_id, task_id=task_id, content=prompt, ) async def checkpoints(self, *, project_id: str, limit: int = 50) -> ServiceResult: engine = await self.context.engine_for_project(project_id) store = getattr(engine, "store", None) checkpoints = await store.get_pending_checkpoints(project_id=project_id) if store and hasattr(store, "get_pending_checkpoints") else [] return ServiceResult({"project_id": project_id, "checkpoints": checkpoints[-limit:]}) async def logs(self, *, project_id: str, task_id: str, limit: int = 100) -> ServiceResult: engine = await self.context.engine_for_project(project_id) store = getattr(engine, "store", None) task = await store.get_task(task_id) if store else None if not task: raise ServiceError("task_not_found", "task_not_found", {"task_id": task_id}) metadata = dict(getattr(task, "metadata", {}) or {}) transcript = await store.get_session_transcript(task.session_id) if getattr(task, "session_id", None) else [] runtime_sessions = [] runtime_events: list[dict[str, Any]] = [] if hasattr(store, "list_runtime_sessions"): runtime_sessions = await store.list_runtime_sessions(project_id=project_id, task_id=task_id, limit=limit) if runtime_sessions and hasattr(store, "list_runtime_events"): for session in runtime_sessions: runtime_id = str(session.get("runtime_session_id", "") or "") if runtime_id: runtime_events.extend(await store.list_runtime_events(runtime_id, limit=limit)) enriched_events = [self._runtime_event_payload(event) for event in runtime_events[-limit:]] return ServiceResult({ "project_id": project_id, "task_id": task_id, "target": { "task_id": task_id, "session_id": str(getattr(task, "session_id", "") or ""), "title": str(getattr(task, "title", "") or ""), "status": str(getattr(getattr(task, "status", None), "value", getattr(task, "status", "")) or ""), "role_id": str(metadata.get("role_id") or getattr(task, "assigned_to", "") or ""), "agent_id": str(metadata.get("agent_id") or metadata.get("preferred_agent") or ""), "work_item_id": str( metadata.get("work_item_id") or metadata.get("linked_work_item_id") or "" ), }, "transcript": transcript[-limit:], "runtime_sessions": runtime_sessions, "runtime_events": enriched_events, }) @staticmethod def _runtime_event_payload(event: Any) -> dict[str, Any]: if isinstance(event, dict): payload = dict(event) elif hasattr(event, "model_dump"): payload = dict(event.model_dump()) else: payload = dict(getattr(event, "__dict__", {}) or {}) event_type = str(payload.get("event_type") or payload.get("type") or "") raw_payload = payload.get("payload") if isinstance(raw_payload, dict): tool_name = str(raw_payload.get("tool_name") or raw_payload.get("name") or "") summary = str(raw_payload.get("summary") or raw_payload.get("result_summary") or raw_payload.get("text") or "") else: tool_name = "" summary = "" display_parts = [part for part in (event_type, tool_name, summary) if part] payload["display_text"] = " | ".join(display_parts) payload["event_type"] = event_type return payload async def recovery_scan(self, *, project_id: str) -> ServiceResult: manager = await self._recovery_manager(project_id) from opc.plugins.office_ui.recovery_manager import _serialize_status status = await manager.get_recovery_status() return ServiceResult(_serialize_status(status, project_id=project_id)) async def recovery_action(self, *, project_id: str, action: str, parent_task_id: str) -> ServiceResult: manager = await self._recovery_manager(project_id) normalized = str(action or "").strip().lower() if normalized == "scan": return await self.recovery_scan(project_id=project_id) if not str(parent_task_id or "").strip(): raise ServiceError("parent_task_id_required", "parent_task_id required") if normalized in {"resume", "retry"}: payload = await manager.resume(parent_task_id) elif normalized == "cancel": payload = await manager.cancel(parent_task_id) else: raise ServiceError("unknown_recovery_action", f"unknown action: {action}", {"action": action}) payload = {**dict(payload), "project_id": project_id, "parent_task_id": parent_task_id, "action": normalized} if not payload.get("ok", False): raise ServiceError(str(payload.get("error") or "recovery_failed"), str(payload.get("error") or "recovery_failed"), payload) return ServiceResult(payload) async def _recovery_manager(self, project_id: str) -> Any: engine = await self.context.engine_for_project(project_id) async def _noop_broadcast(_event: dict[str, Any]) -> None: return None managers = getattr(self.context, "recovery_managers", None) if managers is None: managers = {} setattr(self.context, "recovery_managers", managers) key = self.context.normalize_project_id(project_id) existing = managers.get(key) if existing is not None and getattr(existing, "_engine", None) is engine: return existing from opc.plugins.office_ui.recovery_manager import RuntimeRecoveryManager manager = RuntimeRecoveryManager(engine, _noop_broadcast) managers[key] = manager return manager