Spaces:
Paused
Paused
File size: 8,058 Bytes
5e0b58b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 | """Runtime adapter that connects HomeostasisSignal to the structural coordinator.
This adapter is the bridge between live runtime measurements and the
canonical structural mutation path. It runs as a post-tick hook and:
1. Captures the real HomeostasisSignal from HomeostasisEngine
2. Feeds it through SelfOrganizationPolicy.analyze()
3. Publishes the resulting PolicyReport to the SelfOrganizationCoordinator
It does NOT mutate the network. Mutation remains exclusively in:
Proposal -> Approval -> StructuralPlasticityEngine -> Manipulator
This is the missing link that closes Gate A for the production path.
Config-authoritative:
The adapter reads its interval from config and passes the policy config
from the YAML ``self_organization`` section. Hardcoded defaults are
never used when production config exists.
Error visibility:
The adapter captures structured RuntimeErrorEvent on failure. The error
is observable through the error buffer and dashboard — never silently
swallowed.
"""
from __future__ import annotations
import traceback
from collections.abc import Sequence
from dataclasses import dataclass
from typing import Any
from src.homeostasis.engine import HomeostasisEngine
from src.self_organization.coordinator import SelfOrganizationCoordinator
from src.self_organization.policy import (
SelfOrganizationPolicy,
SelfOrganizationPolicyConfig,
)
# ============================================================================
# Structured error event for scientific integrity
# ============================================================================
@dataclass(frozen=True, slots=True)
class RuntimeErrorEvent:
"""Canonical structured error event for the runtime adapter.
Attributes:
timestamp: Monotonic time (time.monotonic_ns()) when the error occurred.
tick: Simulation tick when the error occurred.
component: Component name (e.g. "SelfOrganizationRuntimeAdapter").
phase: Phase within the component (e.g. "build_signal", "analyze", "publish").
exception_type: Fully qualified exception type name.
message: Human-readable error message.
fatal: If True, the simulation should stop. If False, the error is
isolated and the simulation can continue.
traceback_hash: SHA-256 of the traceback text, for deduplication.
"""
timestamp: int
tick: int
component: str
phase: str
exception_type: str
message: str
fatal: bool = False
traceback_hash: str = ""
# ============================================================================
# Bounded error buffer
# ============================================================================
class ErrorBuffer:
"""Thread-safe bounded buffer for RuntimeErrorEvents.
The buffer holds the most recent N events and exposes them for
dashboard/API visibility. All public methods are protected by
``threading.RLock`` for genuine thread safety.
"""
def __init__(self, max_size: int = 100) -> None:
import threading
self._lock = threading.RLock()
self._max_size = max_size
self._events: list[RuntimeErrorEvent] = []
def push(self, event: RuntimeErrorEvent) -> None:
with self._lock:
self._events.append(event)
if len(self._events) > self._max_size:
self._events.pop(0)
@property
def events(self) -> Sequence[RuntimeErrorEvent]:
with self._lock:
return tuple(self._events)
@property
def latest(self) -> RuntimeErrorEvent | None:
with self._lock:
return self._events[-1] if self._events else None
@property
def count(self) -> int:
with self._lock:
return len(self._events)
def clear(self) -> None:
with self._lock:
self._events.clear()
# ============================================================================
# Shared global error buffer (canonical owner)
# ============================================================================
# The canonical error buffer for the runtime adapter. Accessed by the
# adapter, the OperatorBridge, and the dashboard integration status.
_runtime_error_buffer: ErrorBuffer = ErrorBuffer()
def get_error_buffer() -> ErrorBuffer:
"""Return the canonical shared error buffer."""
return _runtime_error_buffer
# ============================================================================
# Runtime Adapter
# ============================================================================
class SelfOrganizationRuntimeAdapter:
"""Post-tick hook that feeds real HomeostasisSignals into the structural coordinator.
The adapter is attached to the RuntimeController and runs after every
tick. It builds a HomeostasisSignal from the live network, passes it
through the SelfOrganizationPolicy, and publishes the resulting
PolicyReport to the coordinator.
Config-authoritative:
``interval_ticks`` and ``policy_config`` come from the YAML
``self_organization`` section. Hardcoded defaults are only used
when no config is provided (e.g. in tests).
Error visibility:
Exceptions are captured as structured RuntimeErrorEvent and pushed
to the canonical error buffer. They are observable through the
dashboard and integration status. The simulation continues unless
the error is marked fatal.
"""
def __init__(
self,
homeostasis_engine: HomeostasisEngine,
coordinator: SelfOrganizationCoordinator,
*,
interval_ticks: int = 10,
policy_config: SelfOrganizationPolicyConfig | None = None,
error_buffer: ErrorBuffer | None = None,
) -> None:
self.homeostasis_engine = homeostasis_engine
self.coordinator = coordinator
self.interval_ticks = interval_ticks
self.policy = SelfOrganizationPolicy(
policy_config or SelfOrganizationPolicyConfig(enabled=True, dry_run=True)
)
self._last_tick: int = 0
self._error_buffer = error_buffer or _runtime_error_buffer
def __call__(self, tick: int, _result: Any) -> None: # noqa: ARG001
"""Post-tick hook: build signal, run policy, publish to coordinator.
Runs every ``interval_ticks`` ticks to avoid excessive overhead.
Errors are captured as structured RuntimeErrorEvent and pushed to
the error buffer. The simulation continues — a hook error must
never break the simulation.
"""
if tick - self._last_tick < self.interval_ticks:
return
self._last_tick = tick
# --- Phase: build_signal ---
try:
signal = self.homeostasis_engine.build_signal(tick=tick)
except Exception as exc:
self._capture_error(tick, "build_signal", exc)
return
# --- Phase: analyze ---
try:
report = self.policy.analyze(signal)
except Exception as exc:
self._capture_error(tick, "analyze", exc)
return
# --- Phase: publish ---
if report.proposals:
try:
self.coordinator.publish(report)
except Exception as exc:
self._capture_error(tick, "publish", exc)
return
def _capture_error(self, tick: int, phase: str, exc: Exception) -> None:
"""Capture a structured error event."""
import hashlib
import time
tb_text = traceback.format_exc()
tb_hash = hashlib.sha256(tb_text.encode("utf-8")).hexdigest()[:16]
event = RuntimeErrorEvent(
timestamp=time.monotonic_ns(),
tick=tick,
component="SelfOrganizationRuntimeAdapter",
phase=phase,
exception_type=f"{type(exc).__module__}.{type(exc).__qualname__}",
message=str(exc),
fatal=False,
traceback_hash=tb_hash,
)
self._error_buffer.push(event)
|