# -*- coding: utf-8 -*- """Runtime API routes exposing the latest trading run state.""" from __future__ import annotations import json from pathlib import Path from typing import Any, Dict, List, Optional from fastapi import APIRouter, HTTPException from pydantic import BaseModel from backend.runtime.agent_runtime import AgentRuntimeState from backend.runtime.context import TradingRunContext from backend.runtime.manager import TradingRuntimeManager router = APIRouter(prefix="/api/runtime", tags=["runtime"]) runtime_manager: Optional[TradingRuntimeManager] = None PROJECT_ROOT = Path(__file__).resolve().parents[2] class RunContextResponse(BaseModel): config_name: str run_dir: str bootstrap_values: Dict[str, Any] class RuntimeAgentState(BaseModel): agent_id: str status: str last_session: Optional[str] = None last_updated: str class RuntimeAgentsResponse(BaseModel): agents: List[RuntimeAgentState] class RuntimeEvent(BaseModel): timestamp: str event: str details: Dict[str, Any] session: Optional[str] class RuntimeEventsResponse(BaseModel): events: List[RuntimeEvent] def _latest_snapshot_path() -> Optional[Path]: candidates = sorted( PROJECT_ROOT.glob("runs/*/state/runtime_state.json"), key=lambda path: path.stat().st_mtime, reverse=True, ) return candidates[0] if candidates else None def _load_snapshot() -> Dict[str, Any]: snapshot_path = _latest_snapshot_path() if snapshot_path is None or not snapshot_path.exists(): raise HTTPException(status_code=503, detail="runtime manager is not initialized") return json.loads(snapshot_path.read_text(encoding="utf-8")) def _get_runtime_payload() -> Dict[str, Any]: if runtime_manager is not None: return runtime_manager.build_snapshot() return _load_snapshot() def _to_state_response(state: AgentRuntimeState) -> RuntimeAgentState: return RuntimeAgentState( agent_id=state.agent_id, status=state.status, last_session=state.last_session, last_updated=state.last_updated.isoformat(), ) @router.get("/context", response_model=RunContextResponse) async def get_run_context() -> RunContextResponse: """Return the most recent run context.""" payload = _get_runtime_payload() context = payload.get("context") if context is None: raise HTTPException(status_code=404, detail="run context is not ready") return RunContextResponse( config_name=context["config_name"], run_dir=context["run_dir"], bootstrap_values=context["bootstrap_values"], ) @router.get("/agents", response_model=RuntimeAgentsResponse) async def list_agent_states() -> RuntimeAgentsResponse: """List the current runtime state of every registered agent.""" payload = _get_runtime_payload() agents = [RuntimeAgentState(**agent) for agent in payload.get("agents", [])] return RuntimeAgentsResponse(agents=agents) @router.get("/events", response_model=RuntimeEventsResponse) async def list_runtime_events() -> RuntimeEventsResponse: """Return the recent runtime events that TradingRuntimeManager emitted.""" payload = _get_runtime_payload() events = [RuntimeEvent(**event) for event in payload.get("events", [])] return RuntimeEventsResponse(events=events) @router.get("/agents/{agent_id}", response_model=RuntimeAgentState) async def get_agent_state(agent_id: str) -> RuntimeAgentState: """Return the current runtime state for a single agent.""" payload = _get_runtime_payload() state = next( (agent for agent in payload.get("agents", []) if agent["agent_id"] == agent_id), None, ) if state is None: raise HTTPException(status_code=404, detail=f"agent '{agent_id}' not registered") return RuntimeAgentState(**state) def register_runtime_manager(manager: TradingRuntimeManager) -> None: """Allow other modules to expose the runtime manager to the API.""" global runtime_manager runtime_manager = manager def unregister_runtime_manager() -> None: """Drop the runtime manager reference (used for shutdown/testing).""" global runtime_manager runtime_manager = None