From 397a1a111e7e113e8a5a1a272a51fedd135394a7 Mon Sep 17 00:00:00 2001 From: anti Date: Mon, 27 Apr 2026 17:51:00 -0400 Subject: [PATCH] feat(realism): LLM/breaker status on orchestrator heartbeat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Surfaces realism subsystem state on the existing worker heartbeat extra hook (system.orchestrator.health) — no new bus topic. Payload carries {llm_enabled, llm_backend, llm_model, llm_breaker_state}, so the dashboard's worker panel renders a live LLM badge with a colored breaker-state dot: closed (green) — LLM healthy half_open (amber) — cooldown elapsed; next call is a probe open (red) — short-circuiting to deterministic templates Heartbeat is the canonical worker self-report channel; piggybacking on extra(...) avoids a new topic family while keeping the snapshot recomputed each beat (30s). --- decnet/orchestrator/worker.py | 36 ++++++++++- decnet_web/src/components/Config.tsx | 61 ++++++++++++++++++- .../test_realism_health_snapshot.py | 58 ++++++++++++++++++ 3 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 tests/orchestrator/test_realism_health_snapshot.py diff --git a/decnet/orchestrator/worker.py b/decnet/orchestrator/worker.py index 909d270a..0efd1d81 100644 --- a/decnet/orchestrator/worker.py +++ b/decnet/orchestrator/worker.py @@ -116,7 +116,12 @@ async def orchestrator_worker( bus = None shutdown = asyncio.Event() - heartbeat_task = asyncio.create_task(run_health_heartbeat(bus, "orchestrator")) + heartbeat_task = asyncio.create_task( + run_health_heartbeat( + bus, "orchestrator", + extra=lambda: {"realism": _realism_health_snapshot(llm, breaker)}, + ) + ) control_task = asyncio.create_task( run_control_listener(bus, "orchestrator", shutdown), ) @@ -180,6 +185,35 @@ def _roll_action_kind(rng: secrets.SystemRandom) -> str: return _ACTION_WEIGHTS[-1][0] # unreachable, satisfy mypy +def _realism_health_snapshot( + llm: Any, breaker: Optional[LLMCircuitBreaker], +) -> dict[str, Any]: + """Snapshot of the orchestrator's realism subsystem for the + heartbeat ``extra`` payload. + + Surfaces the LLM backend / model / circuit-breaker state so the + dashboard can render a status badge without reaching into worker + process memory. Read-only — the heartbeat ticks every 30s; this + snapshot is recomputed each tick. + + When LLM is disabled (``llm is None``) the snapshot still + returns a dict so consumers can branch on ``llm_enabled`` alone. + """ + if llm is None: + return { + "llm_enabled": False, + "llm_backend": None, + "llm_model": None, + "llm_breaker_state": None, + } + return { + "llm_enabled": True, + "llm_backend": os.environ.get("DECNET_REALISM_LLM", "ollama"), + "llm_model": getattr(llm, "model", None), + "llm_breaker_state": breaker.state if breaker is not None else None, + } + + def _llm_should_enable(explicit: Optional[bool]) -> bool: """Resolve the LLM-enabled flag from CLI / env / defaults. diff --git a/decnet_web/src/components/Config.tsx b/decnet_web/src/components/Config.tsx index ec7c7668..fe438382 100644 --- a/decnet_web/src/components/Config.tsx +++ b/decnet_web/src/components/Config.tsx @@ -601,6 +601,54 @@ interface WorkersPanelProps { pushToast: ReturnType['push']; } + +// Renders the LLM status of a realism-emitting worker (today: orchestrator). +// Sourced from the heartbeat ``extra.realism`` payload published by +// :func:`decnet.orchestrator.worker._realism_health_snapshot`. +const RealismBadge: React.FC<{ + realism: { + llm_enabled?: boolean; + llm_backend?: string | null; + llm_model?: string | null; + llm_breaker_state?: 'closed' | 'open' | 'half_open' | null; + }; +}> = ({ realism }) => { + if (!realism.llm_enabled) { + return ( + + LLM OFF + + ); + } + const breaker = realism.llm_breaker_state ?? 'closed'; + const breakerColor = + breaker === 'open' ? '#ff5555' + : breaker === 'half_open' ? '#ffaa00' + : 'var(--matrix)'; + const tooltip = [ + `Backend: ${realism.llm_backend ?? '?'}`, + realism.llm_model ? `Model: ${realism.llm_model}` : null, + `Circuit breaker: ${breaker}`, + ].filter(Boolean).join('\n'); + return ( + + + LLM {(realism.llm_backend ?? 'on').toUpperCase()} + + ); +}; + const WorkersPanel: React.FC = ({ pushToast }) => { const [workers, setWorkers] = useState(null); const [busConnected, setBusConnected] = useState(null); @@ -833,10 +881,21 @@ const WorkersPanel: React.FC = ({ pushToast }) => { {workers.map((w) => { const isStopping = !!stopping[w.name]; const canStop = w.status === 'ok' && !isStopping && !busOffline; + const realism = (w.extra && (w.extra as any).realism) as + | { + llm_enabled?: boolean; + llm_backend?: string | null; + llm_model?: string | null; + llm_breaker_state?: 'closed' | 'open' | 'half_open' | null; + } + | undefined; return ( - {w.name.toUpperCase()} + + {w.name.toUpperCase()} + {realism && } +