Spaces:
Sleeping
Sleeping
| """ | |
| Split-Brain Collapse Environment | |
| ================================= | |
| Multi-agent DC partition recovery simulation with network, storage, and database layers. | |
| 3 agents (Orchestrator, NetOps, DataOps) must cooperate via delegation to restore health. | |
| """ | |
| import copy, json, re, random | |
| from typing import Optional, Tuple, List, Dict, Any | |
| try: | |
| import networkx as nx | |
| except ImportError: | |
| nx = None | |
| from agents.split_brain.models import ( | |
| NetworkNode, NetworkEdge, HDFSCluster, DatabaseState, LedgerEntry, | |
| SplitBrainObservation, SplitBrainAction, SplitBrainStepResult, | |
| ) | |
| # Valid actions per actor | |
| ACTOR_ACTIONS = { | |
| "orchestrator": {"delegate", "assess_situation", "noop", "run_diagnostic"}, | |
| "netops": {"update_route", "verify_routing", "throttle_bandwidth", "delegate", "noop", "run_diagnostic"}, | |
| "dataops": {"tune_hdfs", "stop_replication", "force_stepdown", "reconcile_ledger", "clear_cache", "delegate", "noop", "run_diagnostic"}, | |
| } | |
| # Role-specific LLM prompts | |
| SYSTEM_PROMPTS = { | |
| "orchestrator": ( | |
| "You are the Incident Commander (Orchestrator). You analyze the situation and delegate " | |
| "tasks to specialist agents. You CANNOT fix things directly β you must delegate.\n" | |
| "Your only actions: delegate, assess_situation, noop.\n" | |
| "When delegating, specify target_agent and instruction_payload." | |
| ), | |
| "netops": ( | |
| "You are the Network Operations specialist. You fix network partitions using routing " | |
| "and bandwidth management. You understand Dijkstra's algorithm and failover routing.\n" | |
| "Your actions: update_route, verify_routing, throttle_bandwidth, delegate (back to orchestrator), noop, run_diagnostic.\n" | |
| "CRITICAL: Once verify_routing succeeds, your network job is DONE. Do NOT try to fix severed links. " | |
| "Delegate back to orchestrator IMMEDIATELY with a status report.\n" | |
| "REGIONAL WIPEOUT EXCEPTION: The dc1βdc2 primary link is permanently severed β verify_routing WILL FAIL. " | |
| "Your job is ONLY to: (1) throttle dc2_router--dc3_router to 10%, (2) establish the oob_tunnel. " | |
| "Once oob_tunnel_active is True in the state, your work is DONE. Delegate back to orchestrator immediately." | |
| ), | |
| "dataops": ( | |
| "You are the Data Operations specialist. You manage HDFS storage and NewSQL databases. " | |
| "You handle replication storms, split-brain resolution, and ledger reconciliation.\n" | |
| "Your actions: tune_hdfs, stop_replication, force_stepdown, reconcile_ledger, delegate (back to orchestrator), noop.\n" | |
| "CRITICAL: Do NOT reconcile_ledger unless routing has been verified by NetOps first." | |
| ), | |
| } | |
| class SplitBrainEnv: | |
| def __init__(self): | |
| self.current_task = None | |
| self.state_data: Optional[SplitBrainObservation] = None | |
| self.step_count = 0 | |
| self.max_steps = 35 | |
| # Milestones | |
| self.bypass_established = False | |
| self.routing_verified = False | |
| self.storm_killed = False | |
| self.stepdown_done = False | |
| self.ledger_reconciled = False | |
| # Delayed penalty | |
| self.corruption_timer = -1 | |
| self.corruption_triggered = False | |
| # Logs | |
| self.delegation_log: List[Dict] = [] | |
| def _reset_trackers(self): | |
| self.step_count = 0 | |
| self.bypass_established = False | |
| self.routing_verified = False | |
| self.storm_killed = False | |
| self.stepdown_done = False | |
| self.ledger_reconciled = False | |
| self.corruption_timer = -1 | |
| self.corruption_triggered = False | |
| self.delegation_log = [] | |
| # ββ TOPOLOGY BUILDERS ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _build_nodes(self) -> List[NetworkNode]: | |
| nodes = [] | |
| dcs = ("dc1", "dc2", "dc3") if self.current_task == "regional_wipeout" else ("dc1", "dc2") | |
| for dc in dcs: | |
| nodes.append(NetworkNode( | |
| node_id=f"{dc}_router", datacenter=dc, node_type="router", | |
| status="online", cpu_usage=35.0, bandwidth_used=200.0, bandwidth_capacity=1000.0)) | |
| nodes.append(NetworkNode( | |
| node_id=f"{dc}_switch", datacenter=dc, node_type="switch", | |
| status="online", cpu_usage=20.0, bandwidth_used=150.0, bandwidth_capacity=1000.0)) | |
| nodes.append(NetworkNode( | |
| node_id=f"{dc}_server_a", datacenter=dc, node_type="server", | |
| status="online", cpu_usage=55.0, bandwidth_used=300.0, bandwidth_capacity=1000.0)) | |
| nodes.append(NetworkNode( | |
| node_id=f"{dc}_server_b", datacenter=dc, node_type="server", | |
| status="online", cpu_usage=50.0, bandwidth_used=250.0, bandwidth_capacity=1000.0)) | |
| return nodes | |
| def _build_edges(self, partitioned=True) -> List[NetworkEdge]: | |
| internal = [ | |
| ("dc1_router", "dc1_switch", 1.0), ("dc1_switch", "dc1_server_a", 0.5), | |
| ("dc1_switch", "dc1_server_b", 0.5), ("dc2_router", "dc2_switch", 1.0), | |
| ("dc2_switch", "dc2_server_a", 0.5), ("dc2_switch", "dc2_server_b", 0.5), | |
| ] | |
| if self.current_task == "regional_wipeout": | |
| internal.extend([ | |
| ("dc3_router", "dc3_switch", 1.0), ("dc3_switch", "dc3_server_a", 0.5), | |
| ("dc3_switch", "dc3_server_b", 0.5) | |
| ]) | |
| edges = [] | |
| for src, tgt, lat in internal: | |
| edges.append(NetworkEdge( | |
| edge_id=f"{src}--{tgt}", source=src, target=tgt, | |
| status="healthy", latency_ms=lat, bandwidth_used=150.0, bandwidth_capacity=1000.0)) | |
| if self.current_task == "regional_wipeout": | |
| edges.append(NetworkEdge( | |
| edge_id="dc1_router--dc3_router", source="dc1_router", target="dc3_router", | |
| status="healthy", latency_ms=10.0, bandwidth_used=0.0, bandwidth_capacity=1000.0)) | |
| edges.append(NetworkEdge( | |
| edge_id="dc2_router--dc3_router", source="dc2_router", target="dc3_router", | |
| status="congested", latency_ms=10.0, bandwidth_used=1000.0, bandwidth_capacity=1000.0)) | |
| edges.append(NetworkEdge( | |
| edge_id="dc1_router--dc2_router", source="dc1_router", target="dc2_router", | |
| status="severed", latency_ms=5.0, bandwidth_used=0.0, bandwidth_capacity=10000.0)) | |
| else: | |
| edges.append(NetworkEdge( | |
| edge_id="dc1_router--dc2_router", source="dc1_router", target="dc2_router", | |
| status="severed" if partitioned else "healthy", | |
| latency_ms=5.0, bandwidth_used=0.0, bandwidth_capacity=10000.0)) | |
| edges.append(NetworkEdge( | |
| edge_id="dc1_router--dc2_switch", source="dc1_router", target="dc2_switch", | |
| status="congested" if partitioned else "healthy", | |
| latency_ms=25.0, bandwidth_used=950.0, bandwidth_capacity=1000.0)) | |
| edges.append(NetworkEdge( | |
| edge_id="dc1_switch--dc2_router", source="dc1_switch", target="dc2_router", | |
| status="congested" if partitioned else "healthy", | |
| latency_ms=30.0, bandwidth_used=920.0, bandwidth_capacity=1000.0)) | |
| return edges | |
| def _build_ledger(self, conflicts=0) -> List[LedgerEntry]: | |
| entries = [] | |
| for i in range(min(conflicts, 8)): | |
| entries.append(LedgerEntry( | |
| key=f"txn_{1000+i}", dc1_value=f"val_dc1_{random.randint(100,999)}", | |
| dc2_value=f"val_dc2_{random.randint(100,999)}", conflict=True, | |
| timestamp_dc1=1700000000.0 + i, timestamp_dc2=1700000000.0 + i + 0.5)) | |
| return entries | |
| # ββ RESET ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def reset(self, task: str = "partition_basic") -> SplitBrainObservation: | |
| self.current_task = task | |
| self._reset_trackers() | |
| nodes = self._build_nodes() | |
| edges = self._build_edges(partitioned=True) | |
| if task == "partition_basic": | |
| self.max_steps = 15 | |
| hdfs = HDFSCluster(io_bandwidth_used_pct=20.0, replication_storm_active=False) | |
| db = DatabaseState(dc1_role="leader", dc2_role="follower", split_brain_active=False) | |
| alerts = ["Symptom: High latency on DB-West."] | |
| health = 0.35 | |
| elif task == "replication_storm": | |
| self.max_steps = 25 | |
| hdfs = HDFSCluster(io_bandwidth_used_pct=100.0, replication_storm_active=True, | |
| under_replicated_blocks=400) | |
| db = DatabaseState(dc1_role="leader", dc2_role="follower", split_brain_active=False) | |
| alerts = ["Symptom: High latency on DB-West.", "Symptom: I/O wait times exceeding thresholds."] | |
| health = 0.15 | |
| elif task == "split_brain": | |
| self.max_steps = 35 | |
| hdfs = HDFSCluster(io_bandwidth_used_pct=100.0, replication_storm_active=True, | |
| under_replicated_blocks=400) | |
| db = DatabaseState(dc1_role="leader", dc2_role="leader", split_brain_active=True, | |
| conflicting_entries=15, ledger_sample=self._build_ledger(15)) | |
| alerts = [ | |
| "Symptom: High latency on DB-West.", | |
| "Symptom: I/O wait times exceeding thresholds.", | |
| "Symptom: Checkout service returning stale data." | |
| ] | |
| health = 0.05 | |
| elif task == "cascading_deadlock": | |
| self.max_steps = 35 | |
| hdfs = HDFSCluster(io_bandwidth_used_pct=20.0, replication_storm_active=False) | |
| db = DatabaseState(dc1_role="leader", dc2_role="follower", split_brain_active=False) | |
| alerts = [ | |
| "Symptom: API latency increased from 40ms to 900ms.", | |
| "Symptom: Database slow query log is growing." | |
| ] | |
| health = 0.40 | |
| elif task == "regional_wipeout": | |
| self.max_steps = 50 | |
| hdfs = HDFSCluster(io_bandwidth_used_pct=100.0, replication_storm_active=True, | |
| under_replicated_blocks=400) | |
| db = DatabaseState(dc1_role="leader", dc2_role="leader", split_brain_active=True, | |
| conflicting_entries=15, ledger_sample=self._build_ledger(15)) | |
| alerts = [ | |
| "Symptom: Lost all telemetry from DC-Beta.", | |
| "Symptom: DC-Gamma is reporting extreme storage I/O alerts.", | |
| "Symptom: Global transaction ledger is drifting." | |
| ] | |
| health = 0.05 | |
| else: | |
| self.max_steps = 15 | |
| hdfs = HDFSCluster(io_bandwidth_used_pct=20.0, replication_storm_active=False) | |
| db = DatabaseState() | |
| alerts = ["INFO: Systems nominal."] | |
| health = 1.0 | |
| self.state_data = SplitBrainObservation( | |
| global_health=health, current_actor="orchestrator", step=0, max_steps=self.max_steps, | |
| network_status="partitioned", dc1_dc2_connected=False, routing_verified=False, | |
| network_nodes=nodes, network_edges=edges, hdfs=hdfs, newsql=db, | |
| recent_events=alerts, delegation_context=None, | |
| auth_status="online", | |
| redis_cache_usage=55.0 if task == "cascading_deadlock" else 0.0 | |
| ) | |
| return self.state() | |
| def state(self) -> SplitBrainObservation: | |
| return copy.deepcopy(self.state_data) | |
| # ββ PATHFINDING ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _build_nx_graph(self): | |
| if nx is None: | |
| return None | |
| G = nx.Graph() | |
| for n in self.state_data.network_nodes: | |
| G.add_node(n.node_id) | |
| for e in self.state_data.network_edges: | |
| if e.status in ("healthy", "bypass", "congested"): | |
| avail = max(0.1, e.bandwidth_capacity - e.bandwidth_used) | |
| w = e.latency_ms + (100.0 / avail) | |
| G.add_edge(e.source, e.target, weight=w, edge_id=e.edge_id) | |
| return G | |
| def _check_connectivity(self) -> Tuple[bool, Optional[List[str]]]: | |
| G = self._build_nx_graph() | |
| if G is None: | |
| return False, None | |
| try: | |
| path = nx.dijkstra_path(G, "dc1_router", "dc2_router", weight="weight") | |
| return True, path | |
| except (nx.NetworkXNoPath, nx.NodeNotFound): | |
| return False, None | |
| # ββ ACTION PARSING βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _parse_action(self, action_input) -> SplitBrainAction: | |
| if isinstance(action_input, SplitBrainAction): | |
| return action_input | |
| if isinstance(action_input, dict): | |
| # Convert nested dict instruction_payloads to strings to avoid type validation errors | |
| if "instruction_payload" in action_input and isinstance(action_input["instruction_payload"], dict): | |
| action_input["instruction_payload"] = json.dumps(action_input["instruction_payload"]) | |
| try: | |
| return SplitBrainAction(**action_input) | |
| except Exception as e: | |
| print(f"[DEBUG PARSE ERROR dict] {e}") | |
| return SplitBrainAction(action_type="noop") | |
| if isinstance(action_input, str): | |
| text = re.sub(r'<think>.*?</think>', '', action_input, flags=re.DOTALL).strip() | |
| text = text.replace("```json", "").replace("```", "").strip() | |
| # First try direct JSON parse | |
| try: | |
| data = json.loads(text) | |
| if "instruction_payload" in data and isinstance(data["instruction_payload"], dict): | |
| data["instruction_payload"] = json.dumps(data["instruction_payload"]) | |
| return SplitBrainAction(**data) | |
| except Exception as e: | |
| print(f"[DEBUG PARSE ERROR direct] {e}") | |
| # Fallback regex parsing | |
| match = re.search(r'\{.*\}', text, re.DOTALL) | |
| if match: | |
| try: | |
| data = json.loads(match.group(0)) | |
| if "action_type" in data: | |
| return SplitBrainAction(**data) | |
| except Exception: | |
| pass | |
| return SplitBrainAction(action_type="noop") | |
| # ββ STEP βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def step(self, action_input) -> SplitBrainStepResult: | |
| if self.state_data is None: | |
| self.reset() | |
| self.step_count += 1 | |
| self.state_data.step = self.step_count | |
| action = self._parse_action(action_input) | |
| reward = 0.0 | |
| done = False | |
| msg = "No effect." | |
| actor = self.state_data.current_actor | |
| # Validate actor permissions | |
| valid_actions = ACTOR_ACTIONS.get(actor, set()) | |
| if action.action_type not in valid_actions: | |
| reward = -0.10 | |
| msg = f"INVALID: '{action.action_type}' not allowed for {actor}. Valid: {sorted(valid_actions)}" | |
| self._add_event(msg) | |
| return self._make_result(reward, done, msg) | |
| # Dispatch by action type | |
| if action.action_type == "delegate": | |
| reward, msg = self._handle_delegate(action) | |
| elif action.action_type == "assess_situation": | |
| reward, msg = self._handle_assess() | |
| elif action.action_type == "update_route": | |
| reward, msg = self._handle_update_route(action) | |
| elif action.action_type == "verify_routing": | |
| reward, msg = self._handle_verify_routing() | |
| elif action.action_type == "throttle_bandwidth": | |
| reward, msg = self._handle_throttle(action) | |
| elif action.action_type == "stop_replication": | |
| reward, msg = self._handle_stop_replication() | |
| elif action.action_type == "tune_hdfs": | |
| reward, msg = self._handle_tune_hdfs(action) | |
| elif action.action_type == "force_stepdown": | |
| reward, msg = self._handle_force_stepdown(action) | |
| elif action.action_type == "reconcile_ledger": | |
| reward, msg = self._handle_reconcile() | |
| elif action.action_type == "run_diagnostic": | |
| reward, msg = self._handle_run_diagnostic() | |
| elif action.action_type == "clear_cache": | |
| reward, msg = self._handle_clear_cache() | |
| elif action.action_type == "noop": | |
| reward = -0.05 | |
| msg = "WARN: No action taken." | |
| # Tick delayed corruption timer | |
| corruption_penalty = self._tick_corruption() | |
| reward += corruption_penalty | |
| # Task 4: Cascading Deadlock logic | |
| # Redis only climbs while the root cause (network) is unfixed. | |
| # Once routing is verified, the app retries stop and Redis stabilizes. | |
| if self.current_task == "cascading_deadlock": | |
| if not self.routing_verified and self.state_data.auth_status == "online": | |
| self.state_data.redis_cache_usage = min(100.0, self.state_data.redis_cache_usage + 20.0) | |
| if self.state_data.redis_cache_usage >= 100.0 and self.state_data.auth_status == "online": | |
| self.state_data.auth_status = "offline" | |
| interrupt_msg = "CRITICAL: Redis OOM. Auth offline. User session drop rate spiked by 800%." | |
| self.state_data.recent_events.append(f"[Step {self.step_count}] {interrupt_msg}") | |
| if self.state_data.auth_status == "offline": | |
| reward -= 0.05 # Bleed penalty | |
| # Time penalty for all tasks to encourage efficiency | |
| reward -= 0.02 | |
| # Update health | |
| self.state_data.global_health = self._calc_health() | |
| # Check win | |
| if self.state_data.global_health >= 1.0: | |
| done = True | |
| msg += " π GLOBAL HEALTH RESTORED TO 1.0 β INCIDENT RESOLVED." | |
| # Check max steps | |
| if not done and self.step_count >= self.max_steps: | |
| done = True | |
| msg += f" TIMEOUT: Max steps ({self.max_steps}) reached." | |
| self._add_event(msg) | |
| return self._make_result(reward, done, msg) | |
| def _make_result(self, reward, done, msg) -> SplitBrainStepResult: | |
| return SplitBrainStepResult( | |
| observation=self.state(), reward=reward, done=done, | |
| info={"message": msg, "step": self.step_count, | |
| "current_actor": self.state_data.current_actor, | |
| "delegation_log": self.delegation_log[-5:]}) | |
| def _add_event(self, msg: str): | |
| self.state_data.recent_events.append(f"[Step {self.step_count}] {msg}") | |
| if len(self.state_data.recent_events) > 20: | |
| self.state_data.recent_events = self.state_data.recent_events[-20:] | |
| # ββ ACTION HANDLERS ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _handle_delegate(self, action: SplitBrainAction) -> Tuple[float, str]: | |
| target = action.target_agent | |
| payload = action.instruction_payload or "No specific instructions." | |
| actor = self.state_data.current_actor | |
| if not target or target == actor: | |
| return -0.05, f"INVALID: Must delegate to a different agent. Got '{target}'." | |
| # Check delegation ordering for orchestrator | |
| reward = 0.05 | |
| if actor == "orchestrator": | |
| if target == "dataops" and not self.bypass_established and not self.routing_verified: | |
| reward = -0.10 | |
| msg = f"PENALTY: Delegating to DataOps before network is fixed. Network must come first." | |
| else: | |
| msg = f"Orchestrator delegates to {target}: '{payload}'" | |
| else: | |
| msg = f"{actor} reports back to {target}: '{payload}'" | |
| self.delegation_log.append({ | |
| "step": self.step_count, "from": actor, "to": target, "message": payload}) | |
| self.state_data.current_actor = target | |
| self.state_data.delegation_context = payload | |
| return reward, msg | |
| def _handle_assess(self) -> Tuple[float, str]: | |
| issues = [] | |
| if not self.bypass_established: | |
| issues.append("Network: PRIMARY LINK SEVERED, backup routes congested") | |
| elif not self.routing_verified: | |
| issues.append("Network: Bypass active but routing NOT verified") | |
| else: | |
| issues.append("Network: HEALTHY β routing verified") | |
| if self.state_data.hdfs.replication_storm_active: | |
| issues.append(f"HDFS: REPLICATION STORM β I/O at {self.state_data.hdfs.io_bandwidth_used_pct:.0f}%") | |
| else: | |
| issues.append("HDFS: Stable") | |
| if self.state_data.newsql.split_brain_active: | |
| issues.append(f"NewSQL: SPLIT-BRAIN β {self.state_data.newsql.conflicting_entries} conflicts") | |
| elif self.stepdown_done and not self.ledger_reconciled: | |
| issues.append("NewSQL: Stepdown done, ledger needs reconciliation") | |
| else: | |
| issues.append("NewSQL: Healthy") | |
| if self.current_task == "cascading_deadlock": | |
| issues.append(f"Redis Cache: {self.state_data.redis_cache_usage:.0f}% used") | |
| issues.append(f"Auth: {self.state_data.auth_status}") | |
| if self.current_task == "regional_wipeout": | |
| issues.append(f"OOB Tunnel: {'ACTIVE' if self.state_data.oob_tunnel_active else 'NOT ESTABLISHED'}") | |
| issues.append("Topology: 3-DC (DC-Alpha, DC-Beta, DC-Gamma)") | |
| summary = "SITUATION ASSESSMENT:\n" + "\n".join(f" β’ {i}" for i in issues) | |
| return 0.05, summary | |
| def _handle_run_diagnostic(self) -> Tuple[float, str]: | |
| if random.random() < 0.2: | |
| return -0.05, "ERROR: Diagnostic tools timeout. Node unresponsive." | |
| actor = self.state_data.current_actor | |
| if self.current_task == "cascading_deadlock": | |
| if actor == "netops": | |
| return 0.10, "DIAGNOSTIC: dc1_router--dc2_switch is degraded causing packet loss. Use update_route on dc1_router--dc2_switch to establish bypass." | |
| if actor == "dataops": | |
| if self.state_data.auth_status == "offline": | |
| return 0.10, "DIAGNOSTIC: Redis cache OOM (Out of Memory). Requires clear_cache immediately." | |
| return 0.05, f"DIAGNOSTIC: DB queries slow, retries piling up in Redis cache. (Usage: {self.state_data.redis_cache_usage:.0f}%)" | |
| return 0.05, "DIAGNOSTIC: Need specialist to check network and data planes." | |
| elif self.current_task == "regional_wipeout": | |
| if actor == "netops": | |
| if not self.state_data.oob_tunnel_active: | |
| dc2_dc3 = self._find_edge("dc2_router--dc3_router") | |
| bw = dc2_dc3.bandwidth_used if dc2_dc3 else 1000 | |
| return 0.10, (f"DIAGNOSTIC: CYCLIC DEPENDENCY DETECTED. " | |
| f"dc1βdc2 link is SEVERED. dc2βdc3 replication storm saturating dc3 at {bw:.0f}Mbps. " | |
| f"Cannot route through dc3 until storm traffic is reduced. " | |
| f"SOLUTION: First throttle_bandwidth on dc2_router--dc3_router to 10%, " | |
| f"then update_route with target_id 'oob_tunnel' to create management link to dc2, " | |
| f"then delegate to dataops to stop_replication via the tunnel.") | |
| else: | |
| return 0.10, "DIAGNOSTIC: OOB tunnel is active. DC-Beta is reachable via management link. Storm can be stopped." | |
| if actor == "dataops": | |
| if self.state_data.oob_tunnel_active: | |
| return 0.10, "DIAGNOSTIC: OOB tunnel active. You can now reach DC-Beta. Use stop_replication to kill the storm." | |
| return 0.05, "DIAGNOSTIC: Cannot reach DC-Beta. No management path exists. Need NetOps to establish OOB tunnel first." | |
| return 0.05, "DIAGNOSTIC: 3-DC topology. DC-Alpha(dc1) partitioned from DC-Beta(dc2). DC-Beta flooding DC-Gamma(dc3) with replication storm. You MUST now delegate to NetOps to run_diagnostic and resolve this." | |
| else: | |
| return self._handle_assess() | |
| def _handle_clear_cache(self) -> Tuple[float, str]: | |
| if self.current_task == "cascading_deadlock": | |
| if self.state_data.auth_status == "offline": | |
| self.state_data.redis_cache_usage = 0.0 | |
| self.state_data.auth_status = "online" | |
| return 0.95, "Redis cache flushed. Auth services restored." | |
| else: | |
| self.state_data.redis_cache_usage = 0.0 | |
| return 0.05, "Redis cache flushed preemptively." | |
| return 0.0, "INFO: Cache cleared." | |
| def _handle_update_route(self, action: SplitBrainAction) -> Tuple[float, str]: | |
| if random.random() < 0.2: | |
| return -0.05, "ERROR: SSH Timeout. Node unresponsive." | |
| eid = action.target_id | |
| if self.current_task == "regional_wipeout" and eid == "oob_tunnel": | |
| if self.state_data.oob_tunnel_active: | |
| return 0.0, "INFO: OOB tunnel is already active. Delegate to dataops to stop_replication." | |
| dc2_dc3 = self._find_edge("dc2_router--dc3_router") | |
| if dc2_dc3 and dc2_dc3.bandwidth_used > 100: | |
| return -0.05, "FAIL: DC-Gamma routers dropping packets due to 100% bandwidth saturation." | |
| self.state_data.oob_tunnel_active = True | |
| self.state_data.dc1_dc2_connected = True | |
| self.bypass_established = True | |
| return 0.15, "OOB TUNNEL ESTABLISHED: Low-bandwidth management link to DC-Beta active." | |
| edge = self._find_edge(eid) | |
| if not edge: | |
| return -0.05, f"WARN: Edge '{eid}' not found. Valid: {self._inter_dc_edge_ids()}" | |
| if self.current_task == "regional_wipeout" and eid == "dc2_router--dc3_router": | |
| if self.state_data.hdfs.replication_storm_active: | |
| return -0.05, "FAIL: Cannot establish bypass. DC-Gamma is completely saturated by the replication storm." | |
| if edge.status == "severed": | |
| return -0.05, f"FAIL: Edge '{eid}' is physically severed β cannot route through it." | |
| if edge.status == "congested": | |
| edge.status = "bypass" | |
| edge.bandwidth_used = min(edge.bandwidth_used * 0.4, edge.bandwidth_capacity * 0.5) | |
| edge.latency_ms = max(edge.latency_ms * 0.6, 2.0) | |
| if not self.bypass_established: | |
| self.bypass_established = True | |
| self.state_data.dc1_dc2_connected = True | |
| self.state_data.network_status = "degraded" | |
| return 0.15, f"BYPASS ESTABLISHED on '{eid}'. DC1βDC2 connectivity restored (degraded)." | |
| return 0.05, f"Additional bypass activated on '{eid}'." | |
| if edge.status == "bypass": | |
| return 0.0, f"INFO: '{eid}' is already in bypass mode." | |
| return 0.0, f"INFO: Edge '{eid}' is {edge.status}, no update needed." | |
| def _handle_verify_routing(self) -> Tuple[float, str]: | |
| connected, path = self._check_connectivity() | |
| if connected and path: | |
| if not self.routing_verified: | |
| self.routing_verified = True | |
| self.state_data.routing_verified = True | |
| self.state_data.network_status = "healthy" if self.bypass_established else "degraded" | |
| path_str = " β ".join(path) | |
| return 0.20, f"ROUTING VERIFIED β Path: {path_str}" | |
| return 0.0, f"INFO: Routing already verified. Path: {' β '.join(path)}" | |
| return 0.0, "FAIL: No route from dc1_router to dc2_router. Establish bypass first." | |
| def _handle_throttle(self, action: SplitBrainAction) -> Tuple[float, str]: | |
| eid = action.target_id | |
| edge = self._find_edge(eid) | |
| if not edge: | |
| return -0.05, f"WARN: Edge '{eid}' not found." | |
| limit = (action.parameters or {}).get("limit_pct", 50) | |
| # Regional Wipeout: The OOB tunnel requires bandwidth_used <= 100. | |
| # bandwidth_used = capacity * (limit/100), so limit must be <= 10%. | |
| # Give explicit corrective feedback if the LLM uses a bad value. | |
| if self.current_task == "regional_wipeout" and eid == "dc2_router--dc3_router": | |
| if limit > 10: | |
| edge.bandwidth_used = edge.bandwidth_capacity * (limit / 100.0) | |
| return -0.05, ( | |
| f"Bandwidth on '{eid}' throttled to {limit}%, but this is NOT ENOUGH. " | |
| f"Current bandwidth: {edge.bandwidth_used:.0f}Mbps. " | |
| f"OOB tunnel requires bandwidth BELOW 100Mbps. " | |
| f"You MUST set limit_pct to 10 or lower. " | |
| f"Use: {{\"action_type\": \"throttle_bandwidth\", \"target_id\": \"{eid}\", \"parameters\": {{\"limit_pct\": 10}}}}" | |
| ) | |
| edge.bandwidth_used = edge.bandwidth_capacity * (limit / 100.0) | |
| return 0.05, f"Bandwidth on '{eid}' throttled to {limit}%." | |
| def _handle_stop_replication(self) -> Tuple[float, str]: | |
| hdfs = self.state_data.hdfs | |
| if not hdfs.replication_storm_active: | |
| if not self.routing_verified: | |
| return 0.0, "INFO: Replication storm already halted. Routing not yet verified β delegate back to orchestrator so NetOps can verify_routing first." | |
| elif not self.stepdown_done: | |
| return 0.0, "INFO: Replication storm already halted. Proceed to force_stepdown on dc2." | |
| elif not self.ledger_reconciled: | |
| return 0.0, "INFO: Stepdown already complete. Proceed to reconcile_ledger now." | |
| else: | |
| return 0.0, "INFO: All DataOps tasks complete. Delegate back to orchestrator." | |
| if self.current_task == "regional_wipeout" and not self.state_data.oob_tunnel_active: | |
| return -0.05, "FAIL: Cannot reach DC-Beta. Primary link severed and no OOB tunnel exists." | |
| if random.random() < 0.2: | |
| return -0.05, "ERROR: SSH Timeout. Node unresponsive." | |
| hdfs.replication_storm_active = False | |
| hdfs.io_bandwidth_used_pct = 20.0 | |
| self.storm_killed = True | |
| return 0.15, "REPLICATION STORM HALTED β I/O bandwidth released. Next: force_stepdown on dc2." | |
| def _handle_tune_hdfs(self, action: SplitBrainAction) -> Tuple[float, str]: | |
| hdfs = self.state_data.hdfs | |
| params = action.parameters or {} | |
| new_rf = params.get("replication_factor", 2) | |
| old_rf = hdfs.replication_factor | |
| hdfs.target_replication_factor = new_rf | |
| hdfs.replication_factor = new_rf | |
| if hdfs.replication_storm_active: | |
| hdfs.io_bandwidth_used_pct = max(20.0, hdfs.io_bandwidth_used_pct * 0.5) | |
| if hdfs.io_bandwidth_used_pct <= 30: | |
| hdfs.replication_storm_active = False | |
| self.storm_killed = True | |
| hdfs.under_replicated_blocks = max(0, hdfs.under_replicated_blocks - 200) | |
| return 0.10, f"HDFS tuned: replication {old_rf}β{new_rf}. I/O now at {hdfs.io_bandwidth_used_pct:.0f}%." | |
| def _handle_force_stepdown(self, action: SplitBrainAction) -> Tuple[float, str]: | |
| if random.random() < 0.2: | |
| return -0.05, "ERROR: SSH Timeout. Node unresponsive." | |
| db = self.state_data.newsql | |
| if not db.split_brain_active: | |
| if self.stepdown_done and not self.ledger_reconciled: | |
| return 0.0, "INFO: Stepdown already complete. Proceed to reconcile_ledger now." | |
| return 0.0, "INFO: No split-brain β database is healthy. Delegate back to orchestrator." | |
| if not self.state_data.dc1_dc2_connected: | |
| return -0.15, "FAIL: Cannot force stepdown β DCs not connected. Fix network first." | |
| target_dc = action.target_id or "dc2" | |
| if target_dc == "dc2": | |
| db.dc2_role = "follower" | |
| else: | |
| db.dc1_role = "follower" | |
| db.split_brain_active = False | |
| self.stepdown_done = True | |
| return 0.15, f"STEPDOWN COMPLETE β {target_dc} demoted to follower. Ledger reconciliation needed." | |
| def _handle_reconcile(self) -> Tuple[float, str]: | |
| db = self.state_data.newsql | |
| if db.split_brain_active: | |
| return -0.15, "FAIL: Cannot reconcile β split-brain still active. Force stepdown first." | |
| if not self.stepdown_done: | |
| return -0.10, "FAIL: No stepdown performed. Force stepdown first." | |
| if self.ledger_reconciled: | |
| return 0.0, "INFO: Ledger already reconciled." | |
| self.ledger_reconciled = True | |
| db.conflicting_entries = 0 | |
| db.ledger_sample = [] | |
| if not self.routing_verified: | |
| # Silent trap β start corruption timer | |
| self.corruption_timer = 10 | |
| return 0.25, "LEDGER RECONCILED (β routing unverified β sync may be unreliable)." | |
| return 0.25, "LEDGER RECONCILED β All conflicts resolved via Last-Writer-Wins." | |
| # ββ DELAYED CORRUPTION βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _tick_corruption(self) -> float: | |
| if self.corruption_timer < 0: | |
| return 0.0 | |
| self.corruption_timer -= 1 | |
| if self.corruption_timer <= 0 and not self.corruption_triggered: | |
| self.corruption_triggered = True | |
| self.ledger_reconciled = False | |
| db = self.state_data.newsql | |
| db.conflicting_entries = 20 | |
| db.ledger_sample = self._build_ledger(20) | |
| self._add_event("π DELAYED CORRUPTION: Ledger re-corrupted! Reconciliation failed due to unverified routing.") | |
| return -0.50 | |
| return 0.0 | |
| # ββ HEALTH CALCULATION βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _calc_health(self) -> float: | |
| components = [] | |
| # Network component | |
| if self.routing_verified: | |
| components.append(1.0) | |
| elif self.bypass_established: | |
| components.append(0.5) | |
| else: | |
| components.append(0.0) | |
| if self.current_task in ("replication_storm", "split_brain", "regional_wipeout"): | |
| if self.storm_killed: | |
| components.append(1.0) | |
| elif self.state_data.hdfs.io_bandwidth_used_pct < 50: | |
| components.append(0.5) | |
| else: | |
| components.append(0.0) | |
| if self.current_task in ("split_brain", "regional_wipeout"): | |
| if self.ledger_reconciled and not self.corruption_triggered: | |
| components.append(1.0) | |
| elif self.stepdown_done: | |
| components.append(0.4) | |
| else: | |
| components.append(0.0) | |
| # Task 4: Redis/Auth must be healthy | |
| if self.current_task == "cascading_deadlock": | |
| if self.state_data.auth_status == "online" and self.state_data.redis_cache_usage <= 10: | |
| components.append(1.0) | |
| elif self.state_data.auth_status == "online": | |
| components.append(0.5) | |
| else: | |
| components.append(0.0) | |
| # Task 5: OOB tunnel must be established | |
| if self.current_task == "regional_wipeout": | |
| if self.state_data.oob_tunnel_active: | |
| components.append(1.0) | |
| else: | |
| components.append(0.0) | |
| return sum(components) / len(components) if components else 1.0 | |
| # ββ HELPERS ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _find_edge(self, edge_id: Optional[str]) -> Optional[NetworkEdge]: | |
| if not edge_id: | |
| return None | |
| for e in self.state_data.network_edges: | |
| if e.edge_id == edge_id: | |
| return e | |
| return None | |
| def _inter_dc_edge_ids(self) -> List[str]: | |
| return [e.edge_id for e in self.state_data.network_edges | |
| if e.source.split("_")[0] != e.target.split("_")[0]] | |
| # ββ TASK-SPECIFIC PROMPT RULES ββββββββββββββββββββββββββββββββββββββββββββ | |
| def _task_specific_rules(self) -> str: | |
| if self.current_task == "cascading_deadlock": | |
| return ( | |
| "\n\nTASK-SPECIFIC CONTEXT (CASCADING DEADLOCK):\n" | |
| "- This is a 2-phase incident: a network degradation AND a Redis cache time-bomb.\n" | |
| "- The Redis cache usage is climbing every step. When it hits 100%, Auth goes OFFLINE.\n" | |
| "- You MUST fix the network first, BUT if auth_status becomes 'offline', you must\n" | |
| " IMMEDIATELY delegate to dataops to run clear_cache before doing anything else.\n" | |
| "- The incident is only resolved when: network is fixed AND auth is online AND redis is cleared.\n" | |
| "- After clearing the cache, resume fixing the network if not already done." | |
| ) | |
| elif self.current_task == "regional_wipeout": | |
| return ( | |
| "\n\nTASK-SPECIFIC CONTEXT (REGIONAL WIPEOUT - 3 DATA CENTERS):\n" | |
| "- Topology: DC-Alpha(dc1), DC-Beta(dc2), DC-Gamma(dc3).\n" | |
| "- dc1βdc2 primary link is SEVERED (partitioned).\n" | |
| "- dc2 is sending a replication storm to dc3, saturating dc3 bandwidth at 100%.\n" | |
| "- CYCLIC DEPENDENCY: You cannot route through dc3 (saturated) and you cannot\n" | |
| " reach dc2 to stop the storm (link severed). Standard routing WILL FAIL.\n" | |
| "- Valid inter-DC edges: dc1_router--dc3_router, dc2_router--dc3_router, dc1_router--dc2_router\n" | |
| "- Special target_id: 'oob_tunnel' (creates a management link, but only works AFTER dc3 bandwidth is throttled below 100)\n" | |
| "\n" | |
| "EXACT STEP-BY-STEP SEQUENCE FOR NETOPS:\n" | |
| " Step A: throttle_bandwidth on dc2_router--dc3_router with limit_pct=10 (MUST be 10, NOT 50 or 90!)\n" | |
| " JSON: {\"action_type\": \"throttle_bandwidth\", \"target_id\": \"dc2_router--dc3_router\", \"parameters\": {\"limit_pct\": 10}}\n" | |
| " Step B: update_route with target_id 'oob_tunnel' (only works after Step A)\n" | |
| " JSON: {\"action_type\": \"update_route\", \"target_id\": \"oob_tunnel\"}\n" | |
| " Step C: verify_routing\n" | |
| " Step D: delegate back to orchestrator\n" | |
| "\n" | |
| "EXACT STEP-BY-STEP SEQUENCE FOR DATAOPS:\n" | |
| " Step E: stop_replication (only works after OOB tunnel is active)\n" | |
| " Step F: force_stepdown on dc2\n" | |
| " Step G: reconcile_ledger\n" | |
| " Step H: delegate back to orchestrator\n" | |
| "\nCRITICAL INSTRUCTION FOR ALL AGENTS: You MUST strictly obey the DELEGATION CONTEXT. " | |
| "Do exactly what the delegation context tells you to do, and nothing else. " | |
| "Do not skip steps. Do not guess future steps." | |
| ) | |
| return "" | |
| # ββ LLM PROMPTS βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def get_llm_prompts(self): | |
| """Return (system_prompt, user_prompt) for the current actor.""" | |
| actor = self.state_data.current_actor | |
| sys_prompt = SYSTEM_PROMPTS.get(actor, SYSTEM_PROMPTS["orchestrator"]) | |
| obs = self.state_data | |
| ctx = f"\nDELEGATION CONTEXT: {obs.delegation_context}" if obs.delegation_context else "" | |
| user_prompt = f"""You are the {actor.upper()} agent handling a datacenter incident. | |
| {ctx} | |
| CURRENT STATE: | |
| {obs.model_dump_json(indent=2)} | |
| RULES: | |
| 1. ORCHESTRATOR SEQUENCE OF REPAIRS: | |
| - First, MUST run_diagnostic to identify the exact issue if it is unknown. (NEVER run this more than once. If it is already in recent_events, proceed to Step 2). | |
| - Second, delegate to netops to fix the network partition and verify routing. | |
| - Third, once the network is verified, delegate to dataops to halt the replication storm. | |
| - Fourth, delegate to dataops to resolve the split-brain (force stepdown) and reconcile the ledger. | |
| - EMERGENCY: If a critical system like Auth goes offline (auth_status=offline), you must IMMEDIATELY preempt current operations, delegate to dataops to clear_cache, and then resume normal operations. | |
| 2. Network MUST be fixed before database operations. | |
| 3. Kill replication storms before reconciling data. | |
| 4. NEVER reconcile_ledger unless routing is verified. | |
| 5. When your work is done, delegate back to orchestrator. | |
| 6. DO NOT output noop if there is an active incident. Always take action or delegate. | |
| 7. If a command fails with SSH Timeout or Node unresponsive, RETRY the same command. | |
| {self._task_specific_rules()} | |
| Respond with EXACTLY ONE JSON object. No other text. | |
| Valid actions for {actor}: {sorted(ACTOR_ACTIONS[actor])} | |
| EXAMPLES: | |
| {{"action_type": "run_diagnostic"}} | |
| {{"action_type": "clear_cache"}} | |
| {{"action_type": "delegate", "target_agent": "netops", "instruction_payload": "Establish bypass routing"}} | |
| {{"action_type": "delegate", "target_agent": "dataops", "instruction_payload": "Stop replication storm and reconcile ledger"}} | |
| {{"action_type": "update_route", "target_id": "dc1_router--dc2_switch"}} | |
| {{"action_type": "throttle_bandwidth", "target_id": "dc2_router--dc3_router", "parameters": {{"limit_pct": 10}}}} | |
| {{"action_type": "update_route", "target_id": "oob_tunnel"}} | |
| {{"action_type": "stop_replication"}} | |
| {{"action_type": "force_stepdown", "target_id": "dc2"}} | |
| """ | |
| return sys_prompt, user_prompt | |